From dd3cd0324dda36c5694edb86679623415edd9023 Mon Sep 17 00:00:00 2001 From: tarrinneal Date: Thu, 13 Aug 2026 12:16:54 -0700 Subject: [PATCH 01/18] update ci chrome version to version:145.0.7632.117 --- .ci.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.ci.yaml b/.ci.yaml index 714e25471f3..52c1f652128 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -73,7 +73,7 @@ platform_properties: device_type: none dependencies: >- [ - {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"} ] windows_arm64: properties: @@ -321,7 +321,7 @@ targets: {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"}, {"dependency": "cmake", "version": "build_id:8787856497187628321"}, {"dependency": "ninja", "version": "version:1.9.0"}, - {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"} ] channel: master @@ -340,7 +340,7 @@ targets: {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"}, {"dependency": "cmake", "version": "build_id:8787856497187628321"}, {"dependency": "ninja", "version": "version:1.9.0"}, - {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"} ] channel: stable @@ -769,7 +769,7 @@ targets: # Install Chrome as a default handler for schemes for url_launcher. dependencies: >- [ - {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"} ] - name: Linux_desktop platform_tests stable @@ -783,7 +783,7 @@ targets: # Install Chrome as a default handler for schemes for url_launcher. dependencies: >- [ - {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"} ] ### iOS+macOS tasks ### From ad03d1157aa74d1895bf7c698de3833bbe31e1e2 Mon Sep 17 00:00:00 2001 From: tarrinneal Date: Thu, 13 Aug 2026 16:46:40 -0700 Subject: [PATCH 02/18] make test less precise --- .../integration_test/video_player_test.dart | 2 +- .../example/ios/Flutter/Debug.xcconfig | 1 + .../example/ios/Flutter/Release.xcconfig | 1 + .../video_player/example/ios/Podfile | 43 +++++++++++++++++++ .../macos/Flutter/Flutter-Debug.xcconfig | 1 + .../macos/Flutter/Flutter-Release.xcconfig | 1 + .../video_player/example/macos/Podfile | 42 ++++++++++++++++++ .../example/ios/Flutter/Debug.xcconfig | 1 + .../example/ios/Flutter/Release.xcconfig | 1 + .../example/ios/Podfile | 43 +++++++++++++++++++ .../macos/Flutter/Flutter-Debug.xcconfig | 1 + .../macos/Flutter/Flutter-Release.xcconfig | 1 + .../example/macos/Podfile | 42 ++++++++++++++++++ 13 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 packages/video_player/video_player/example/ios/Podfile create mode 100644 packages/video_player/video_player/example/macos/Podfile create mode 100644 packages/video_player/video_player_avfoundation/example/ios/Podfile create mode 100644 packages/video_player/video_player_avfoundation/example/macos/Podfile diff --git a/packages/video_player/video_player/example/integration_test/video_player_test.dart b/packages/video_player/video_player/example/integration_test/video_player_test.dart index b3b99c72728..ca20b053441 100644 --- a/packages/video_player/video_player/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player/example/integration_test/video_player_test.dart @@ -306,7 +306,7 @@ void main() { // Due to the duration calculation accuracy between platforms, // the milliseconds on Web will be a slightly different from natives. // The audio was made with 44100 Hz, 192 Kbps CBR, and 32 bits. - expect(controller.value.duration, const Duration(seconds: 5, milliseconds: kIsWeb ? 42 : 41)); + expect(controller.value.duration.inMilliseconds, closeTo(5000, 50)); }); testWidgets('can be played', (WidgetTester tester) async { diff --git a/packages/video_player/video_player/example/ios/Flutter/Debug.xcconfig b/packages/video_player/video_player/example/ios/Flutter/Debug.xcconfig index 592ceee85b8..ec97fc6f302 100644 --- a/packages/video_player/video_player/example/ios/Flutter/Debug.xcconfig +++ b/packages/video_player/video_player/example/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/packages/video_player/video_player/example/ios/Flutter/Release.xcconfig b/packages/video_player/video_player/example/ios/Flutter/Release.xcconfig index 592ceee85b8..c4855bfe200 100644 --- a/packages/video_player/video_player/example/ios/Flutter/Release.xcconfig +++ b/packages/video_player/video_player/example/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/packages/video_player/video_player/example/ios/Podfile b/packages/video_player/video_player/example/ios/Podfile new file mode 100644 index 00000000000..620e46eba60 --- /dev/null +++ b/packages/video_player/video_player/example/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/video_player/video_player/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/video_player/video_player/example/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b608b..4b81f9b2d20 100644 --- a/packages/video_player/video_player/example/macos/Flutter/Flutter-Debug.xcconfig +++ b/packages/video_player/video_player/example/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/video_player/video_player/example/macos/Flutter/Flutter-Release.xcconfig b/packages/video_player/video_player/example/macos/Flutter/Flutter-Release.xcconfig index c2efd0b608b..5caa9d1579e 100644 --- a/packages/video_player/video_player/example/macos/Flutter/Flutter-Release.xcconfig +++ b/packages/video_player/video_player/example/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/video_player/video_player/example/macos/Podfile b/packages/video_player/video_player/example/macos/Podfile new file mode 100644 index 00000000000..ff5ddb3b8bd --- /dev/null +++ b/packages/video_player/video_player/example/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/video_player/video_player_avfoundation/example/ios/Flutter/Debug.xcconfig b/packages/video_player/video_player_avfoundation/example/ios/Flutter/Debug.xcconfig index 592ceee85b8..ec97fc6f302 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Flutter/Debug.xcconfig +++ b/packages/video_player/video_player_avfoundation/example/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/packages/video_player/video_player_avfoundation/example/ios/Flutter/Release.xcconfig b/packages/video_player/video_player_avfoundation/example/ios/Flutter/Release.xcconfig index 592ceee85b8..c4855bfe200 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Flutter/Release.xcconfig +++ b/packages/video_player/video_player_avfoundation/example/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/packages/video_player/video_player_avfoundation/example/ios/Podfile b/packages/video_player/video_player_avfoundation/example/ios/Podfile new file mode 100644 index 00000000000..620e46eba60 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/video_player/video_player_avfoundation/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/video_player/video_player_avfoundation/example/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b608b..4b81f9b2d20 100644 --- a/packages/video_player/video_player_avfoundation/example/macos/Flutter/Flutter-Debug.xcconfig +++ b/packages/video_player/video_player_avfoundation/example/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/video_player/video_player_avfoundation/example/macos/Flutter/Flutter-Release.xcconfig b/packages/video_player/video_player_avfoundation/example/macos/Flutter/Flutter-Release.xcconfig index c2efd0b608b..5caa9d1579e 100644 --- a/packages/video_player/video_player_avfoundation/example/macos/Flutter/Flutter-Release.xcconfig +++ b/packages/video_player/video_player_avfoundation/example/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/video_player/video_player_avfoundation/example/macos/Podfile b/packages/video_player/video_player_avfoundation/example/macos/Podfile new file mode 100644 index 00000000000..ff5ddb3b8bd --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end From 3a87753c9611115c9ae80e43a0ed84cc91c5b2e0 Mon Sep 17 00:00:00 2001 From: tarrinneal Date: Thu, 13 Aug 2026 17:35:38 -0700 Subject: [PATCH 03/18] Removed the non-existent RunnerTests target blocks from example --- packages/video_player/video_player/example/ios/Podfile | 3 --- packages/video_player/video_player/example/macos/Podfile | 3 --- 2 files changed, 6 deletions(-) diff --git a/packages/video_player/video_player/example/ios/Podfile b/packages/video_player/video_player/example/ios/Podfile index 620e46eba60..17adeb14132 100644 --- a/packages/video_player/video_player/example/ios/Podfile +++ b/packages/video_player/video_player/example/ios/Podfile @@ -31,9 +31,6 @@ target 'Runner' do use_frameworks! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end end post_install do |installer| diff --git a/packages/video_player/video_player/example/macos/Podfile b/packages/video_player/video_player/example/macos/Podfile index ff5ddb3b8bd..66f6172bbb3 100644 --- a/packages/video_player/video_player/example/macos/Podfile +++ b/packages/video_player/video_player/example/macos/Podfile @@ -30,9 +30,6 @@ target 'Runner' do use_frameworks! flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end end post_install do |installer| From fd27dd4695ba4f3247cf9d1fd0d1b438ab702099 Mon Sep 17 00:00:00 2001 From: tarrinneal Date: Fri, 14 Aug 2026 14:25:13 -0700 Subject: [PATCH 04/18] attempt to shard --- .ci.yaml | 25 +++++++++++++++++++ .../web_platform_tests_material_ui.yaml | 16 ++++++++++++ .../web_platform_tests_material_ui_wasm.yaml | 17 +++++++++++++ script/configs/exclude_integration_web.yaml | 3 +++ 4 files changed, 61 insertions(+) create mode 100644 .ci/targets/web_platform_tests_material_ui.yaml create mode 100644 .ci/targets/web_platform_tests_material_ui_wasm.yaml diff --git a/.ci.yaml b/.ci.yaml index 52c1f652128..520b3fd076a 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -685,6 +685,14 @@ targets: channel: master package_sharding: "--shardIndex 2 --shardCount 3" + - name: Linux_web web_platform_tests_material_ui master + recipe: packages/packages + timeout: 90 + properties: + target_file: web_platform_tests_material_ui.yaml + version_file: flutter_master.version + channel: master + # Wasm integration tests in master - name: Linux_web web_platform_tests_wasm_shard_1 master recipe: packages/packages @@ -713,6 +721,14 @@ targets: channel: master package_sharding: "--shardIndex 2 --shardCount 3" + - name: Linux_web web_platform_tests_material_ui_wasm master + recipe: packages/packages + timeout: 90 + properties: + target_file: web_platform_tests_material_ui_wasm.yaml + version_file: flutter_master.version + channel: master + # JS integration tests in stable - name: Linux_web web_platform_tests_shard_1 stable recipe: packages/packages @@ -741,6 +757,15 @@ targets: channel: stable package_sharding: "--shardIndex 2 --shardCount 3" + - name: Linux_web web_platform_tests_material_ui stable + recipe: packages/packages + timeout: 90 + properties: + target_file: web_platform_tests_material_ui.yaml + version_file: flutter_stable.version + channel: stable + + ### Linux desktop tasks - name: Linux_desktop build_all_packages master recipe: packages/packages diff --git a/.ci/targets/web_platform_tests_material_ui.yaml b/.ci/targets/web_platform_tests_material_ui.yaml new file mode 100644 index 00000000000..ca203a755bc --- /dev/null +++ b/.ci/targets/web_platform_tests_material_ui.yaml @@ -0,0 +1,16 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + infra_step: true # Note infra steps failing prevents "always" from running. + - name: download Dart deps + script: .ci/scripts/tool_runner.sh + args: ["fetch-deps", "--web", "--supporting-target-platforms-only"] + infra_step: true + - name: drive examples + script: .ci/scripts/tool_runner.sh + args: [ + "drive-examples", + "--web", + "--run-chromedriver", + "--packages=material_ui", + ] diff --git a/.ci/targets/web_platform_tests_material_ui_wasm.yaml b/.ci/targets/web_platform_tests_material_ui_wasm.yaml new file mode 100644 index 00000000000..2a26e7c8751 --- /dev/null +++ b/.ci/targets/web_platform_tests_material_ui_wasm.yaml @@ -0,0 +1,17 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + infra_step: true # Note infra steps failing prevents "always" from running. + - name: download Dart deps + script: .ci/scripts/tool_runner.sh + args: ["fetch-deps", "--web", "--supporting-target-platforms-only"] + infra_step: true + - name: drive examples + script: .ci/scripts/tool_runner.sh + args: [ + "drive-examples", + "--web", + "--wasm", + "--run-chromedriver", + "--packages=material_ui", + ] diff --git a/script/configs/exclude_integration_web.yaml b/script/configs/exclude_integration_web.yaml index 8dbdd61b8d4..cafa68aa54d 100644 --- a/script/configs/exclude_integration_web.yaml +++ b/script/configs/exclude_integration_web.yaml @@ -2,3 +2,6 @@ - file_selector # Waiting on https://github.com/flutter/flutter/issues/145149 - google_maps_flutter/google_maps_flutter +# Moved to its own dedicated shard due to long test duration +- material_ui + From b13c76163c93bd6c51ac5944f600475fd6ee306f Mon Sep 17 00:00:00 2001 From: tarrinneal Date: Fri, 14 Aug 2026 16:06:16 -0700 Subject: [PATCH 05/18] bring up --- .ci.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.ci.yaml b/.ci.yaml index 520b3fd076a..9a1e2110480 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -687,6 +687,7 @@ targets: - name: Linux_web web_platform_tests_material_ui master recipe: packages/packages + bringup: true timeout: 90 properties: target_file: web_platform_tests_material_ui.yaml @@ -723,6 +724,7 @@ targets: - name: Linux_web web_platform_tests_material_ui_wasm master recipe: packages/packages + bringup: true timeout: 90 properties: target_file: web_platform_tests_material_ui_wasm.yaml @@ -759,6 +761,7 @@ targets: - name: Linux_web web_platform_tests_material_ui stable recipe: packages/packages + bringup: true timeout: 90 properties: target_file: web_platform_tests_material_ui.yaml From ae555c7f3420a7c00571cd1e2102d2553f91d409 Mon Sep 17 00:00:00 2001 From: tarrinneal Date: Fri, 14 Aug 2026 18:28:28 -0700 Subject: [PATCH 06/18] more shard --- .ci.yaml | 28 +++++++++++++++++++ .ci/targets/web_dart_unit_tests.yaml | 3 +- .../web_dart_unit_tests_material_ui.yaml | 11 ++++++++ .../web_dart_unit_tests_material_ui_wasm.yaml | 12 ++++++++ .ci/targets/web_dart_unit_tests_wasm.yaml | 3 +- script/configs/exclude_web_unit_tests.yaml | 2 ++ 6 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 .ci/targets/web_dart_unit_tests_material_ui.yaml create mode 100644 .ci/targets/web_dart_unit_tests_material_ui_wasm.yaml create mode 100644 script/configs/exclude_web_unit_tests.yaml diff --git a/.ci.yaml b/.ci.yaml index 9a1e2110480..b8225c1d0a8 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -209,6 +209,15 @@ targets: version_file: flutter_master.version package_sharding: "--shardIndex 2 --shardCount 3" + - name: Linux_web web_dart_unit_test_material_ui master + recipe: packages/packages + bringup: true + timeout: 90 + properties: + target_file: web_dart_unit_tests_material_ui.yaml + channel: master + version_file: flutter_master.version + - name: Linux_web web_dart_unit_test_shard_1 stable recipe: packages/packages timeout: 60 @@ -236,6 +245,15 @@ targets: version_file: flutter_stable.version package_sharding: "--shardIndex 2 --shardCount 3" + - name: Linux_web web_dart_unit_test_material_ui stable + recipe: packages/packages + bringup: true + timeout: 90 + properties: + target_file: web_dart_unit_tests_material_ui.yaml + channel: stable + version_file: flutter_stable.version + # Wasm unit tests in master # TODO(stuartmorgan): Switch to testing wasm on master and stable and non-wasm # only on master if/when wasm builds become the default on stable; see @@ -259,6 +277,16 @@ targets: version_file: flutter_master.version package_sharding: "--shardIndex 1 --shardCount 2" + - name: Linux_web web_dart_unit_test_material_ui_wasm master + recipe: packages/packages + bringup: true + timeout: 90 + properties: + target_file: web_dart_unit_tests_material_ui_wasm.yaml + channel: master + version_file: flutter_master.version + + - name: Linux analyze master recipe: packages/packages timeout: 30 diff --git a/.ci/targets/web_dart_unit_tests.yaml b/.ci/targets/web_dart_unit_tests.yaml index 16cdf1771bb..60b84133119 100644 --- a/.ci/targets/web_dart_unit_tests.yaml +++ b/.ci/targets/web_dart_unit_tests.yaml @@ -7,5 +7,6 @@ tasks: args: [ "dart-test", "--platform=chrome", - "--exclude=script/configs/dart_unit_tests_exceptions.yaml" + "--exclude=script/configs/dart_unit_tests_exceptions.yaml", + "--exclude=script/configs/exclude_web_unit_tests.yaml" ] diff --git a/.ci/targets/web_dart_unit_tests_material_ui.yaml b/.ci/targets/web_dart_unit_tests_material_ui.yaml new file mode 100644 index 00000000000..b1d505b03a6 --- /dev/null +++ b/.ci/targets/web_dart_unit_tests_material_ui.yaml @@ -0,0 +1,11 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + infra_step: true # Note infra steps failing prevents "always" from running. + - name: Dart unit tests - web (material_ui) + script: .ci/scripts/tool_runner.sh + args: [ + "dart-test", + "--platform=chrome", + "--packages=material_ui" + ] diff --git a/.ci/targets/web_dart_unit_tests_material_ui_wasm.yaml b/.ci/targets/web_dart_unit_tests_material_ui_wasm.yaml new file mode 100644 index 00000000000..7e5b17fca93 --- /dev/null +++ b/.ci/targets/web_dart_unit_tests_material_ui_wasm.yaml @@ -0,0 +1,12 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + infra_step: true # Note infra steps failing prevents "always" from running. + - name: Dart unit tests - web wasm (material_ui) + script: .ci/scripts/tool_runner.sh + args: [ + "dart-test", + "--platform=chrome", + "--wasm", + "--packages=material_ui" + ] diff --git a/.ci/targets/web_dart_unit_tests_wasm.yaml b/.ci/targets/web_dart_unit_tests_wasm.yaml index a5a6eeeddf0..67cb833b91a 100644 --- a/.ci/targets/web_dart_unit_tests_wasm.yaml +++ b/.ci/targets/web_dart_unit_tests_wasm.yaml @@ -9,5 +9,6 @@ tasks: "--platform=chrome", "--wasm", "--exclude=script/configs/dart_unit_tests_exceptions.yaml", - "--exclude=script/configs/dart_unit_tests_wasm_exceptions.yaml" + "--exclude=script/configs/dart_unit_tests_wasm_exceptions.yaml", + "--exclude=script/configs/exclude_web_unit_tests.yaml" ] diff --git a/script/configs/exclude_web_unit_tests.yaml b/script/configs/exclude_web_unit_tests.yaml new file mode 100644 index 00000000000..a1e54f8e051 --- /dev/null +++ b/script/configs/exclude_web_unit_tests.yaml @@ -0,0 +1,2 @@ +# Moved to its own dedicated shard due to long test duration +- material_ui From e20144113c97270f10c8048a92f7e39ea4cc3dea Mon Sep 17 00:00:00 2001 From: tarrinneal Date: Fri, 14 Aug 2026 20:33:12 -0700 Subject: [PATCH 07/18] more logging on chrome --- script/tool/lib/src/dart_test_command.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/tool/lib/src/dart_test_command.dart b/script/tool/lib/src/dart_test_command.dart index 29a7b15f7f5..846f3bc94b7 100644 --- a/script/tool/lib/src/dart_test_command.dart +++ b/script/tool/lib/src/dart_test_command.dart @@ -134,6 +134,7 @@ class DartTestCommand extends PackageLoopingCommand { final int exitCode = await processRunner.runAndStream(flutterCommand, [ 'test', '--color', + if (platform == 'chrome') ...['-r', 'expanded', '--timeout=5m', '-j', '2'], if (experiment.isNotEmpty) '--enable-experiment=$experiment', // Flutter defaults to VM mode (under a different name) and explicitly // setting it is deprecated, so pass nothing in that case. @@ -162,6 +163,7 @@ class DartTestCommand extends PackageLoopingCommand { 'run', if (experiment.isNotEmpty) '--enable-experiment=$experiment', 'test', + if (platform == 'chrome') ...['-r', 'expanded', '--timeout=5m', '-j', '2'], if (platform != null) '--platform=$platform', if (wasm) '--compiler=dart2wasm', ], workingDir: package.directory); From 0e1a6cdba8a8729f9a2e7fdf204b07d781ebc8cc Mon Sep 17 00:00:00 2001 From: tarrinneal Date: Fri, 14 Aug 2026 21:32:27 -0700 Subject: [PATCH 08/18] revert logs --- script/tool/lib/src/dart_test_command.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/script/tool/lib/src/dart_test_command.dart b/script/tool/lib/src/dart_test_command.dart index 846f3bc94b7..29a7b15f7f5 100644 --- a/script/tool/lib/src/dart_test_command.dart +++ b/script/tool/lib/src/dart_test_command.dart @@ -134,7 +134,6 @@ class DartTestCommand extends PackageLoopingCommand { final int exitCode = await processRunner.runAndStream(flutterCommand, [ 'test', '--color', - if (platform == 'chrome') ...['-r', 'expanded', '--timeout=5m', '-j', '2'], if (experiment.isNotEmpty) '--enable-experiment=$experiment', // Flutter defaults to VM mode (under a different name) and explicitly // setting it is deprecated, so pass nothing in that case. @@ -163,7 +162,6 @@ class DartTestCommand extends PackageLoopingCommand { 'run', if (experiment.isNotEmpty) '--enable-experiment=$experiment', 'test', - if (platform == 'chrome') ...['-r', 'expanded', '--timeout=5m', '-j', '2'], if (platform != null) '--platform=$platform', if (wasm) '--compiler=dart2wasm', ], workingDir: package.directory); From 624748e09a60556031ff1197e29950d23e5a9745 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Mon, 17 Aug 2026 15:43:45 -0400 Subject: [PATCH 09/18] Update Chrome test flags to support Chrome 145 upgrade --- packages/cross_file/dart_test.yaml | 15 +++++++++- packages/cross_file/example/dart_test.yaml | 15 +++++++++- .../dart_test.yaml | 15 +++++++++- .../plugin_platform_interface/dart_test.yaml | 15 +++++++++- packages/web_benchmarks/CHANGELOG.md | 1 + packages/web_benchmarks/lib/src/browser.dart | 30 ++++++++++++++++--- 6 files changed, 83 insertions(+), 8 deletions(-) diff --git a/packages/cross_file/dart_test.yaml b/packages/cross_file/dart_test.yaml index cdb656dbb68..36d5c9b8bca 100644 --- a/packages/cross_file/dart_test.yaml +++ b/packages/cross_file/dart_test.yaml @@ -3,4 +3,17 @@ override_platforms: chrome: settings: executable: chrome - arguments: --no-sandbox + arguments: >- + --no-sandbox + --disable-background-timer-throttling + --disable-renderer-backgrounding + --disable-background-networking + --disable-search-engine-choice-screen + --disable-extensions + --disable-popup-blocking + --disable-translate + --disable-default-apps + --no-default-browser-check + --no-first-run + --password-store=basic + --bwsi diff --git a/packages/cross_file/example/dart_test.yaml b/packages/cross_file/example/dart_test.yaml index cdb656dbb68..36d5c9b8bca 100644 --- a/packages/cross_file/example/dart_test.yaml +++ b/packages/cross_file/example/dart_test.yaml @@ -3,4 +3,17 @@ override_platforms: chrome: settings: executable: chrome - arguments: --no-sandbox + arguments: >- + --no-sandbox + --disable-background-timer-throttling + --disable-renderer-backgrounding + --disable-background-networking + --disable-search-engine-choice-screen + --disable-extensions + --disable-popup-blocking + --disable-translate + --disable-default-apps + --no-default-browser-check + --no-first-run + --password-store=basic + --bwsi diff --git a/packages/google_identity_services_web/dart_test.yaml b/packages/google_identity_services_web/dart_test.yaml index cdb656dbb68..36d5c9b8bca 100644 --- a/packages/google_identity_services_web/dart_test.yaml +++ b/packages/google_identity_services_web/dart_test.yaml @@ -3,4 +3,17 @@ override_platforms: chrome: settings: executable: chrome - arguments: --no-sandbox + arguments: >- + --no-sandbox + --disable-background-timer-throttling + --disable-renderer-backgrounding + --disable-background-networking + --disable-search-engine-choice-screen + --disable-extensions + --disable-popup-blocking + --disable-translate + --disable-default-apps + --no-default-browser-check + --no-first-run + --password-store=basic + --bwsi diff --git a/packages/plugin_platform_interface/dart_test.yaml b/packages/plugin_platform_interface/dart_test.yaml index cdb656dbb68..36d5c9b8bca 100644 --- a/packages/plugin_platform_interface/dart_test.yaml +++ b/packages/plugin_platform_interface/dart_test.yaml @@ -3,4 +3,17 @@ override_platforms: chrome: settings: executable: chrome - arguments: --no-sandbox + arguments: >- + --no-sandbox + --disable-background-timer-throttling + --disable-renderer-backgrounding + --disable-background-networking + --disable-search-engine-choice-screen + --disable-extensions + --disable-popup-blocking + --disable-translate + --disable-default-apps + --no-default-browser-check + --no-first-run + --password-store=basic + --bwsi diff --git a/packages/web_benchmarks/CHANGELOG.md b/packages/web_benchmarks/CHANGELOG.md index a20c00696c6..7aed180f224 100644 --- a/packages/web_benchmarks/CHANGELOG.md +++ b/packages/web_benchmarks/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +* Updates Chrome launch arguments to unblock Chrome 145+ upgrade. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 4.1.1 diff --git a/packages/web_benchmarks/lib/src/browser.dart b/packages/web_benchmarks/lib/src/browser.dart index d632a68e762..75e22d4b6e1 100644 --- a/packages/web_benchmarks/lib/src/browser.dart +++ b/packages/web_benchmarks/lib/src/browser.dart @@ -98,10 +98,16 @@ class Chrome { final args = [ if (options.userDataDirectory != null) '--user-data-dir=${options.userDataDirectory}', if (url != null) url, - if (io.Platform.environment['CHROME_NO_SANDBOX'] == 'true') '--no-sandbox', - if (options.headless) '--headless', - if (withDebugging) '--remote-debugging-port=${options.debugPort}', - '--window-size=${options.windowWidth},${options.windowHeight}', + '--disable-background-timer-throttling', + '--disable-renderer-backgrounding', + '--disable-background-networking', + '--disable-sync', + '--disable-client-side-phishing-detection', + '--disable-notifications', + '--disable-features=GCM', + '--gcm-checkin-url=http://127.0.0.1', + '--gcm-registration-url=http://127.0.0.1', + '--gcm-mcs-endpoint=127.0.0.1:0', '--disable-extensions', '--disable-popup-blocking', // Indicates that the browser is in "browse without sign-in" (Guest session) mode. @@ -110,6 +116,22 @@ class Chrome { '--no-default-browser-check', '--disable-default-apps', '--disable-translate', + '--password-store=basic', + if (io.Platform.isMacOS) '--use-mock-keychain', + '--disable-search-engine-choice-screen', + if (io.Platform.environment['CHROME_NO_SANDBOX'] == 'true') '--no-sandbox', + if (options.headless) ...[ + '--headless', + '--no-sandbox', + if (io.Platform.isLinux) ...[ + '--use-gl=angle', + '--use-angle=swiftshader', + '--enable-unsafe-swiftshader', + '--disable-gpu-sandbox', + ], + ], + if (withDebugging) '--remote-debugging-port=${options.debugPort}', + '--window-size=${options.windowWidth},${options.windowHeight}', ]; final io.Process chromeProcess = await io.Process.start( _findSystemChromeExecutable(), From a704d9b5a35eabe545feaf2a3c0d4684c0f9e2c3 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Mon, 17 Aug 2026 15:53:02 -0400 Subject: [PATCH 10/18] Revert unrelated Podfile, xcconfig, and video_player test changes --- .../integration_test/video_player_test.dart | 2 +- .../example/ios/Flutter/Debug.xcconfig | 1 - .../example/ios/Flutter/Release.xcconfig | 1 - .../video_player/example/ios/Podfile | 40 ----------------- .../macos/Flutter/Flutter-Debug.xcconfig | 1 - .../macos/Flutter/Flutter-Release.xcconfig | 1 - .../video_player/example/macos/Podfile | 39 ----------------- .../example/ios/Flutter/Debug.xcconfig | 1 - .../example/ios/Flutter/Release.xcconfig | 1 - .../example/ios/Podfile | 43 ------------------- .../macos/Flutter/Flutter-Debug.xcconfig | 1 - .../macos/Flutter/Flutter-Release.xcconfig | 1 - .../example/macos/Podfile | 42 ------------------ 13 files changed, 1 insertion(+), 173 deletions(-) delete mode 100644 packages/video_player/video_player/example/ios/Podfile delete mode 100644 packages/video_player/video_player/example/macos/Podfile delete mode 100644 packages/video_player/video_player_avfoundation/example/ios/Podfile delete mode 100644 packages/video_player/video_player_avfoundation/example/macos/Podfile diff --git a/packages/video_player/video_player/example/integration_test/video_player_test.dart b/packages/video_player/video_player/example/integration_test/video_player_test.dart index ca20b053441..b3b99c72728 100644 --- a/packages/video_player/video_player/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player/example/integration_test/video_player_test.dart @@ -306,7 +306,7 @@ void main() { // Due to the duration calculation accuracy between platforms, // the milliseconds on Web will be a slightly different from natives. // The audio was made with 44100 Hz, 192 Kbps CBR, and 32 bits. - expect(controller.value.duration.inMilliseconds, closeTo(5000, 50)); + expect(controller.value.duration, const Duration(seconds: 5, milliseconds: kIsWeb ? 42 : 41)); }); testWidgets('can be played', (WidgetTester tester) async { diff --git a/packages/video_player/video_player/example/ios/Flutter/Debug.xcconfig b/packages/video_player/video_player/example/ios/Flutter/Debug.xcconfig index ec97fc6f302..592ceee85b8 100644 --- a/packages/video_player/video_player/example/ios/Flutter/Debug.xcconfig +++ b/packages/video_player/video_player/example/ios/Flutter/Debug.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/packages/video_player/video_player/example/ios/Flutter/Release.xcconfig b/packages/video_player/video_player/example/ios/Flutter/Release.xcconfig index c4855bfe200..592ceee85b8 100644 --- a/packages/video_player/video_player/example/ios/Flutter/Release.xcconfig +++ b/packages/video_player/video_player/example/ios/Flutter/Release.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/packages/video_player/video_player/example/ios/Podfile b/packages/video_player/video_player/example/ios/Podfile deleted file mode 100644 index 17adeb14132..00000000000 --- a/packages/video_player/video_player/example/ios/Podfile +++ /dev/null @@ -1,40 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '13.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - use_frameworks! - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/packages/video_player/video_player/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/video_player/video_player/example/macos/Flutter/Flutter-Debug.xcconfig index 4b81f9b2d20..c2efd0b608b 100644 --- a/packages/video_player/video_player/example/macos/Flutter/Flutter-Debug.xcconfig +++ b/packages/video_player/video_player/example/macos/Flutter/Flutter-Debug.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/video_player/video_player/example/macos/Flutter/Flutter-Release.xcconfig b/packages/video_player/video_player/example/macos/Flutter/Flutter-Release.xcconfig index 5caa9d1579e..c2efd0b608b 100644 --- a/packages/video_player/video_player/example/macos/Flutter/Flutter-Release.xcconfig +++ b/packages/video_player/video_player/example/macos/Flutter/Flutter-Release.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/video_player/video_player/example/macos/Podfile b/packages/video_player/video_player/example/macos/Podfile deleted file mode 100644 index 66f6172bbb3..00000000000 --- a/packages/video_player/video_player/example/macos/Podfile +++ /dev/null @@ -1,39 +0,0 @@ -platform :osx, '10.15' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_macos_podfile_setup - -target 'Runner' do - use_frameworks! - - flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_macos_build_settings(target) - end -end diff --git a/packages/video_player/video_player_avfoundation/example/ios/Flutter/Debug.xcconfig b/packages/video_player/video_player_avfoundation/example/ios/Flutter/Debug.xcconfig index ec97fc6f302..592ceee85b8 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Flutter/Debug.xcconfig +++ b/packages/video_player/video_player_avfoundation/example/ios/Flutter/Debug.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/packages/video_player/video_player_avfoundation/example/ios/Flutter/Release.xcconfig b/packages/video_player/video_player_avfoundation/example/ios/Flutter/Release.xcconfig index c4855bfe200..592ceee85b8 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Flutter/Release.xcconfig +++ b/packages/video_player/video_player_avfoundation/example/ios/Flutter/Release.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/packages/video_player/video_player_avfoundation/example/ios/Podfile b/packages/video_player/video_player_avfoundation/example/ios/Podfile deleted file mode 100644 index 620e46eba60..00000000000 --- a/packages/video_player/video_player_avfoundation/example/ios/Podfile +++ /dev/null @@ -1,43 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '13.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - use_frameworks! - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/packages/video_player/video_player_avfoundation/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/video_player/video_player_avfoundation/example/macos/Flutter/Flutter-Debug.xcconfig index 4b81f9b2d20..c2efd0b608b 100644 --- a/packages/video_player/video_player_avfoundation/example/macos/Flutter/Flutter-Debug.xcconfig +++ b/packages/video_player/video_player_avfoundation/example/macos/Flutter/Flutter-Debug.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/video_player/video_player_avfoundation/example/macos/Flutter/Flutter-Release.xcconfig b/packages/video_player/video_player_avfoundation/example/macos/Flutter/Flutter-Release.xcconfig index 5caa9d1579e..c2efd0b608b 100644 --- a/packages/video_player/video_player_avfoundation/example/macos/Flutter/Flutter-Release.xcconfig +++ b/packages/video_player/video_player_avfoundation/example/macos/Flutter/Flutter-Release.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/video_player/video_player_avfoundation/example/macos/Podfile b/packages/video_player/video_player_avfoundation/example/macos/Podfile deleted file mode 100644 index ff5ddb3b8bd..00000000000 --- a/packages/video_player/video_player_avfoundation/example/macos/Podfile +++ /dev/null @@ -1,42 +0,0 @@ -platform :osx, '10.15' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_macos_podfile_setup - -target 'Runner' do - use_frameworks! - - flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_macos_build_settings(target) - end -end From dc07cab82dcf11a43e83f1d9222a7397dfc67822 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Mon, 17 Aug 2026 16:03:58 -0400 Subject: [PATCH 11/18] Allow tolerance in audio duration check for Chrome 145 web tests --- .../example/integration_test/video_player_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/video_player/video_player/example/integration_test/video_player_test.dart b/packages/video_player/video_player/example/integration_test/video_player_test.dart index b3b99c72728..ca20b053441 100644 --- a/packages/video_player/video_player/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player/example/integration_test/video_player_test.dart @@ -306,7 +306,7 @@ void main() { // Due to the duration calculation accuracy between platforms, // the milliseconds on Web will be a slightly different from natives. // The audio was made with 44100 Hz, 192 Kbps CBR, and 32 bits. - expect(controller.value.duration, const Duration(seconds: 5, milliseconds: kIsWeb ? 42 : 41)); + expect(controller.value.duration.inMilliseconds, closeTo(5000, 50)); }); testWidgets('can be played', (WidgetTester tester) async { From 221a7f14816cf15f350cf698700e79bffc51e772 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Mon, 17 Aug 2026 16:17:41 -0400 Subject: [PATCH 12/18] Bump web_benchmarks to 4.1.2 --- packages/web_benchmarks/CHANGELOG.md | 2 +- packages/web_benchmarks/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web_benchmarks/CHANGELOG.md b/packages/web_benchmarks/CHANGELOG.md index 7aed180f224..1db81531bfd 100644 --- a/packages/web_benchmarks/CHANGELOG.md +++ b/packages/web_benchmarks/CHANGELOG.md @@ -1,4 +1,4 @@ -## NEXT +## 4.1.2 * Updates Chrome launch arguments to unblock Chrome 145+ upgrade. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. diff --git a/packages/web_benchmarks/pubspec.yaml b/packages/web_benchmarks/pubspec.yaml index 4a687a973f8..f43d83af9d3 100644 --- a/packages/web_benchmarks/pubspec.yaml +++ b/packages/web_benchmarks/pubspec.yaml @@ -2,7 +2,7 @@ name: web_benchmarks description: A benchmark harness for performance-testing Flutter apps in Chrome. repository: https://github.com/flutter/packages/tree/main/packages/web_benchmarks issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+web_benchmarks%22 -version: 4.1.1 +version: 4.1.2 environment: sdk: ^3.10.0 From ac64fbebd31e0b895bbfaa50b3e29823396556d6 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Mon, 17 Aug 2026 17:58:44 -0400 Subject: [PATCH 13/18] Pass matching web browser flags in drive-examples and opt shared_preferences_tool out of non-vm testing --- .../shared_preferences_tool/dart_test.yaml | 1 + .../tool/lib/src/drive_examples_command.dart | 18 +++++ .../test/drive_examples_command_test.dart | 76 ++++++++++++++++++- 3 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 packages/shared_preferences/shared_preferences_tool/dart_test.yaml diff --git a/packages/shared_preferences/shared_preferences_tool/dart_test.yaml b/packages/shared_preferences/shared_preferences_tool/dart_test.yaml new file mode 100644 index 00000000000..91ec220b8e2 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_tool/dart_test.yaml @@ -0,0 +1 @@ +test_on: vm diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 2d89df0b270..defffa4204b 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -51,6 +51,22 @@ class DriveExamplesCommand extends PackageLoopingCommand { static const String _chromeDriverFlag = 'run-chromedriver'; + static const List _webChromeBrowserFlags = [ + '--web-browser-flag=--no-sandbox', + '--web-browser-flag=--disable-background-timer-throttling', + '--web-browser-flag=--disable-renderer-backgrounding', + '--web-browser-flag=--disable-background-networking', + '--web-browser-flag=--disable-search-engine-choice-screen', + '--web-browser-flag=--disable-extensions', + '--web-browser-flag=--disable-popup-blocking', + '--web-browser-flag=--disable-translate', + '--web-browser-flag=--disable-default-apps', + '--web-browser-flag=--no-default-browser-check', + '--web-browser-flag=--no-first-run', + '--web-browser-flag=--password-store=basic', + '--web-browser-flag=--bwsi', + ]; + @override final String name = 'drive-examples'; @@ -139,6 +155,8 @@ class DriveExamplesCommand extends PackageLoopingCommand { if (useWasm) '--wasm', if (platform.environment.containsKey('CHROME_EXECUTABLE')) '--chrome-binary=${platform.environment['CHROME_EXECUTABLE']}', + ..._webChromeBrowserFlags, + if (platform.isMacOS) '--web-browser-flag=--use-mock-keychain', ], if (getBoolArg(platformWindows)) platformWindows: ['-d', 'windows'], }; diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 81a0d9f4cba..aa357e5fec6 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -13,7 +13,6 @@ import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; import 'package:flutter_plugin_tools/src/drive_examples_command.dart'; import 'package:git/git.dart'; import 'package:mockito/mockito.dart'; -import 'package:platform/platform.dart'; import 'package:test/test.dart'; import 'mocks.dart'; @@ -21,10 +20,25 @@ import 'util.dart'; const String _fakeIOSDevice = '67d5c3d1-8bdf-46ad-8f6b-b00e2a972dda'; const String _fakeAndroidDevice = 'emulator-1234'; +const List _defaultWebBrowserFlags = [ + '--web-browser-flag=--no-sandbox', + '--web-browser-flag=--disable-background-timer-throttling', + '--web-browser-flag=--disable-renderer-backgrounding', + '--web-browser-flag=--disable-background-networking', + '--web-browser-flag=--disable-search-engine-choice-screen', + '--web-browser-flag=--disable-extensions', + '--web-browser-flag=--disable-popup-blocking', + '--web-browser-flag=--disable-translate', + '--web-browser-flag=--disable-default-apps', + '--web-browser-flag=--no-default-browser-check', + '--web-browser-flag=--no-first-run', + '--web-browser-flag=--password-store=basic', + '--web-browser-flag=--bwsi', +]; void main() { group('test drive_example_command', () { - late Platform mockPlatform; + late MockPlatform mockPlatform; late Directory packagesDir; late CommandRunner runner; late RecordingProcessRunner processRunner; @@ -776,6 +790,7 @@ void main() { 'web-server', '--web-port=7357', '--browser-name=chrome', + ..._defaultWebBrowserFlags, '--screenshot=/path/to/logs/plugin_example-drive', '--driver', 'test_driver/integration_test.dart', @@ -823,6 +838,7 @@ void main() { '--web-port=7357', '--browser-name=chrome', '--wasm', + ..._defaultWebBrowserFlags, '--screenshot=/path/to/logs/plugin_example-drive', '--driver', 'test_driver/integration_test.dart', @@ -870,6 +886,7 @@ void main() { 'web-server', '--web-port=7357', '--browser-name=chrome', + ..._defaultWebBrowserFlags, '--screenshot=/path/to/logs/plugin_example-drive', '--driver', 'test_driver/integration_test.dart', @@ -918,6 +935,56 @@ void main() { '--web-port=7357', '--browser-name=chrome', '--chrome-binary=/path/to/chrome', + ..._defaultWebBrowserFlags, + '--screenshot=/path/to/logs/plugin_example-drive', + '--driver', + 'test_driver/integration_test.dart', + '--target', + 'integration_test/plugin_test.dart', + ], pluginExampleDirectory.path), + ]), + ); + }); + + test('drives a web plugin on macOS with mock keychain', () async { + final RepositoryPackage plugin = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/integration_test/plugin_test.dart', + 'example/test_driver/integration_test.dart', + 'example/web/index.html', + ], + platformSupport: { + platformWeb: const PlatformDetails(PlatformSupport.inline), + }, + ); + + final Directory pluginExampleDirectory = getExampleDir(plugin); + + mockPlatform.isMacOS = true; + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--web', + ]); + + expect( + output, + containsAllInOrder([contains('Running for plugin'), contains('No issues found!')]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), const [ + 'drive', + '-d', + 'web-server', + '--web-port=7357', + '--browser-name=chrome', + ..._defaultWebBrowserFlags, + '--web-browser-flag=--use-mock-keychain', '--screenshot=/path/to/logs/plugin_example-drive', '--driver', 'test_driver/integration_test.dart', @@ -1370,6 +1437,7 @@ void main() { 'web-server', '--web-port=7357', '--browser-name=chrome', + ..._defaultWebBrowserFlags, '--screenshot=/path/to/logs/plugin_example-drive', '--driver', 'test_driver/integration_test.dart', @@ -1382,6 +1450,7 @@ void main() { 'web-server', '--web-port=7357', '--browser-name=chrome', + ..._defaultWebBrowserFlags, '--screenshot=/path/to/logs/plugin_example-drive', '--driver', 'test_driver/integration_test.dart', @@ -1488,6 +1557,7 @@ void main() { 'web-server', '--web-port=7357', '--browser-name=chrome', + ..._defaultWebBrowserFlags, '--screenshot=/path/to/logs/a_package_example-drive', '--driver', 'test_driver/integration_test.dart', @@ -1534,6 +1604,7 @@ void main() { 'web-server', '--web-port=7357', '--browser-name=chrome', + ..._defaultWebBrowserFlags, '--driver', 'test_driver/integration_test.dart', '--target', @@ -1617,6 +1688,7 @@ void main() { 'web-server', '--web-port=7357', '--browser-name=chrome', + ..._defaultWebBrowserFlags, '--screenshot=/path/to/logs/a_package_example_with_web-drive', '--driver', 'test_driver/integration_test.dart', From 473afb335493dc9bf0f1c3e9f0f7c4946a8f443b Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Tue, 18 Aug 2026 10:51:49 -0400 Subject: [PATCH 14/18] Move camera_web to dedicated web integration test shards --- .ci.yaml | 18 ++++++++++++++++++ .ci/targets/web_platform_tests_camera_web.yaml | 16 ++++++++++++++++ .../web_platform_tests_camera_web_wasm.yaml | 17 +++++++++++++++++ .../example/test_driver/integration_test.dart | 2 +- script/configs/exclude_integration_web.yaml | 1 + 5 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 .ci/targets/web_platform_tests_camera_web.yaml create mode 100644 .ci/targets/web_platform_tests_camera_web_wasm.yaml diff --git a/.ci.yaml b/.ci.yaml index b8225c1d0a8..dd284ec639c 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -722,6 +722,15 @@ targets: version_file: flutter_master.version channel: master + - name: Linux_web web_platform_tests_camera_web master + recipe: packages/packages + bringup: true + timeout: 90 + properties: + target_file: web_platform_tests_camera_web.yaml + version_file: flutter_master.version + channel: master + # Wasm integration tests in master - name: Linux_web web_platform_tests_wasm_shard_1 master recipe: packages/packages @@ -759,6 +768,15 @@ targets: version_file: flutter_master.version channel: master + - name: Linux_web web_platform_tests_camera_web_wasm master + recipe: packages/packages + bringup: true + timeout: 90 + properties: + target_file: web_platform_tests_camera_web_wasm.yaml + version_file: flutter_master.version + channel: master + # JS integration tests in stable - name: Linux_web web_platform_tests_shard_1 stable recipe: packages/packages diff --git a/.ci/targets/web_platform_tests_camera_web.yaml b/.ci/targets/web_platform_tests_camera_web.yaml new file mode 100644 index 00000000000..c21daa1dfb0 --- /dev/null +++ b/.ci/targets/web_platform_tests_camera_web.yaml @@ -0,0 +1,16 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + infra_step: true # Note infra steps failing prevents "always" from running. + - name: download Dart deps + script: .ci/scripts/tool_runner.sh + args: ["fetch-deps", "--web", "--supporting-target-platforms-only"] + infra_step: true + - name: drive examples + script: .ci/scripts/tool_runner.sh + args: [ + "drive-examples", + "--web", + "--run-chromedriver", + "--packages=camera_web", + ] diff --git a/.ci/targets/web_platform_tests_camera_web_wasm.yaml b/.ci/targets/web_platform_tests_camera_web_wasm.yaml new file mode 100644 index 00000000000..5f8c0e57144 --- /dev/null +++ b/.ci/targets/web_platform_tests_camera_web_wasm.yaml @@ -0,0 +1,17 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + infra_step: true # Note infra steps failing prevents "always" from running. + - name: download Dart deps + script: .ci/scripts/tool_runner.sh + args: ["fetch-deps", "--web", "--supporting-target-platforms-only"] + infra_step: true + - name: drive examples + script: .ci/scripts/tool_runner.sh + args: [ + "drive-examples", + "--web", + "--wasm", + "--run-chromedriver", + "--packages=camera_web", + ] diff --git a/packages/camera/camera_web/example/test_driver/integration_test.dart b/packages/camera/camera_web/example/test_driver/integration_test.dart index fb3dec00bee..b4547b1470a 100644 --- a/packages/camera/camera_web/example/test_driver/integration_test.dart +++ b/packages/camera/camera_web/example/test_driver/integration_test.dart @@ -4,4 +4,4 @@ import 'package:integration_test/integration_test_driver.dart'; -Future main() => integrationDriver(); +Future main() => integrationDriver(timeout: const Duration(minutes: 30)); diff --git a/script/configs/exclude_integration_web.yaml b/script/configs/exclude_integration_web.yaml index cafa68aa54d..67aefed53e5 100644 --- a/script/configs/exclude_integration_web.yaml +++ b/script/configs/exclude_integration_web.yaml @@ -3,5 +3,6 @@ # Waiting on https://github.com/flutter/flutter/issues/145149 - google_maps_flutter/google_maps_flutter # Moved to its own dedicated shard due to long test duration +- camera/camera_web - material_ui From 3832fa3d07d967f4ac2ac9509bc550ff6a500fd0 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Tue, 18 Aug 2026 11:37:51 -0400 Subject: [PATCH 15/18] Remove extraneous material_ui shards --- .ci.yaml | 56 ------------------- .ci/targets/web_dart_unit_tests.yaml | 3 +- .../web_dart_unit_tests_material_ui.yaml | 11 ---- .../web_dart_unit_tests_material_ui_wasm.yaml | 12 ---- .ci/targets/web_dart_unit_tests_wasm.yaml | 3 +- .../web_platform_tests_material_ui.yaml | 16 ------ .../web_platform_tests_material_ui_wasm.yaml | 17 ------ script/configs/exclude_integration_web.yaml | 1 - script/configs/exclude_web_unit_tests.yaml | 2 - 9 files changed, 2 insertions(+), 119 deletions(-) delete mode 100644 .ci/targets/web_dart_unit_tests_material_ui.yaml delete mode 100644 .ci/targets/web_dart_unit_tests_material_ui_wasm.yaml delete mode 100644 .ci/targets/web_platform_tests_material_ui.yaml delete mode 100644 .ci/targets/web_platform_tests_material_ui_wasm.yaml delete mode 100644 script/configs/exclude_web_unit_tests.yaml diff --git a/.ci.yaml b/.ci.yaml index dd284ec639c..b8132e66be5 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -209,15 +209,6 @@ targets: version_file: flutter_master.version package_sharding: "--shardIndex 2 --shardCount 3" - - name: Linux_web web_dart_unit_test_material_ui master - recipe: packages/packages - bringup: true - timeout: 90 - properties: - target_file: web_dart_unit_tests_material_ui.yaml - channel: master - version_file: flutter_master.version - - name: Linux_web web_dart_unit_test_shard_1 stable recipe: packages/packages timeout: 60 @@ -245,15 +236,6 @@ targets: version_file: flutter_stable.version package_sharding: "--shardIndex 2 --shardCount 3" - - name: Linux_web web_dart_unit_test_material_ui stable - recipe: packages/packages - bringup: true - timeout: 90 - properties: - target_file: web_dart_unit_tests_material_ui.yaml - channel: stable - version_file: flutter_stable.version - # Wasm unit tests in master # TODO(stuartmorgan): Switch to testing wasm on master and stable and non-wasm # only on master if/when wasm builds become the default on stable; see @@ -277,16 +259,6 @@ targets: version_file: flutter_master.version package_sharding: "--shardIndex 1 --shardCount 2" - - name: Linux_web web_dart_unit_test_material_ui_wasm master - recipe: packages/packages - bringup: true - timeout: 90 - properties: - target_file: web_dart_unit_tests_material_ui_wasm.yaml - channel: master - version_file: flutter_master.version - - - name: Linux analyze master recipe: packages/packages timeout: 30 @@ -713,15 +685,6 @@ targets: channel: master package_sharding: "--shardIndex 2 --shardCount 3" - - name: Linux_web web_platform_tests_material_ui master - recipe: packages/packages - bringup: true - timeout: 90 - properties: - target_file: web_platform_tests_material_ui.yaml - version_file: flutter_master.version - channel: master - - name: Linux_web web_platform_tests_camera_web master recipe: packages/packages bringup: true @@ -759,15 +722,6 @@ targets: channel: master package_sharding: "--shardIndex 2 --shardCount 3" - - name: Linux_web web_platform_tests_material_ui_wasm master - recipe: packages/packages - bringup: true - timeout: 90 - properties: - target_file: web_platform_tests_material_ui_wasm.yaml - version_file: flutter_master.version - channel: master - - name: Linux_web web_platform_tests_camera_web_wasm master recipe: packages/packages bringup: true @@ -805,16 +759,6 @@ targets: channel: stable package_sharding: "--shardIndex 2 --shardCount 3" - - name: Linux_web web_platform_tests_material_ui stable - recipe: packages/packages - bringup: true - timeout: 90 - properties: - target_file: web_platform_tests_material_ui.yaml - version_file: flutter_stable.version - channel: stable - - ### Linux desktop tasks - name: Linux_desktop build_all_packages master recipe: packages/packages diff --git a/.ci/targets/web_dart_unit_tests.yaml b/.ci/targets/web_dart_unit_tests.yaml index 60b84133119..16cdf1771bb 100644 --- a/.ci/targets/web_dart_unit_tests.yaml +++ b/.ci/targets/web_dart_unit_tests.yaml @@ -7,6 +7,5 @@ tasks: args: [ "dart-test", "--platform=chrome", - "--exclude=script/configs/dart_unit_tests_exceptions.yaml", - "--exclude=script/configs/exclude_web_unit_tests.yaml" + "--exclude=script/configs/dart_unit_tests_exceptions.yaml" ] diff --git a/.ci/targets/web_dart_unit_tests_material_ui.yaml b/.ci/targets/web_dart_unit_tests_material_ui.yaml deleted file mode 100644 index b1d505b03a6..00000000000 --- a/.ci/targets/web_dart_unit_tests_material_ui.yaml +++ /dev/null @@ -1,11 +0,0 @@ -tasks: - - name: prepare tool - script: .ci/scripts/prepare_tool.sh - infra_step: true # Note infra steps failing prevents "always" from running. - - name: Dart unit tests - web (material_ui) - script: .ci/scripts/tool_runner.sh - args: [ - "dart-test", - "--platform=chrome", - "--packages=material_ui" - ] diff --git a/.ci/targets/web_dart_unit_tests_material_ui_wasm.yaml b/.ci/targets/web_dart_unit_tests_material_ui_wasm.yaml deleted file mode 100644 index 7e5b17fca93..00000000000 --- a/.ci/targets/web_dart_unit_tests_material_ui_wasm.yaml +++ /dev/null @@ -1,12 +0,0 @@ -tasks: - - name: prepare tool - script: .ci/scripts/prepare_tool.sh - infra_step: true # Note infra steps failing prevents "always" from running. - - name: Dart unit tests - web wasm (material_ui) - script: .ci/scripts/tool_runner.sh - args: [ - "dart-test", - "--platform=chrome", - "--wasm", - "--packages=material_ui" - ] diff --git a/.ci/targets/web_dart_unit_tests_wasm.yaml b/.ci/targets/web_dart_unit_tests_wasm.yaml index 67cb833b91a..a5a6eeeddf0 100644 --- a/.ci/targets/web_dart_unit_tests_wasm.yaml +++ b/.ci/targets/web_dart_unit_tests_wasm.yaml @@ -9,6 +9,5 @@ tasks: "--platform=chrome", "--wasm", "--exclude=script/configs/dart_unit_tests_exceptions.yaml", - "--exclude=script/configs/dart_unit_tests_wasm_exceptions.yaml", - "--exclude=script/configs/exclude_web_unit_tests.yaml" + "--exclude=script/configs/dart_unit_tests_wasm_exceptions.yaml" ] diff --git a/.ci/targets/web_platform_tests_material_ui.yaml b/.ci/targets/web_platform_tests_material_ui.yaml deleted file mode 100644 index ca203a755bc..00000000000 --- a/.ci/targets/web_platform_tests_material_ui.yaml +++ /dev/null @@ -1,16 +0,0 @@ -tasks: - - name: prepare tool - script: .ci/scripts/prepare_tool.sh - infra_step: true # Note infra steps failing prevents "always" from running. - - name: download Dart deps - script: .ci/scripts/tool_runner.sh - args: ["fetch-deps", "--web", "--supporting-target-platforms-only"] - infra_step: true - - name: drive examples - script: .ci/scripts/tool_runner.sh - args: [ - "drive-examples", - "--web", - "--run-chromedriver", - "--packages=material_ui", - ] diff --git a/.ci/targets/web_platform_tests_material_ui_wasm.yaml b/.ci/targets/web_platform_tests_material_ui_wasm.yaml deleted file mode 100644 index 2a26e7c8751..00000000000 --- a/.ci/targets/web_platform_tests_material_ui_wasm.yaml +++ /dev/null @@ -1,17 +0,0 @@ -tasks: - - name: prepare tool - script: .ci/scripts/prepare_tool.sh - infra_step: true # Note infra steps failing prevents "always" from running. - - name: download Dart deps - script: .ci/scripts/tool_runner.sh - args: ["fetch-deps", "--web", "--supporting-target-platforms-only"] - infra_step: true - - name: drive examples - script: .ci/scripts/tool_runner.sh - args: [ - "drive-examples", - "--web", - "--wasm", - "--run-chromedriver", - "--packages=material_ui", - ] diff --git a/script/configs/exclude_integration_web.yaml b/script/configs/exclude_integration_web.yaml index 67aefed53e5..dd575ed478a 100644 --- a/script/configs/exclude_integration_web.yaml +++ b/script/configs/exclude_integration_web.yaml @@ -4,5 +4,4 @@ - google_maps_flutter/google_maps_flutter # Moved to its own dedicated shard due to long test duration - camera/camera_web -- material_ui diff --git a/script/configs/exclude_web_unit_tests.yaml b/script/configs/exclude_web_unit_tests.yaml deleted file mode 100644 index a1e54f8e051..00000000000 --- a/script/configs/exclude_web_unit_tests.yaml +++ /dev/null @@ -1,2 +0,0 @@ -# Moved to its own dedicated shard due to long test duration -- material_ui From 5d7329d77761de78c10a49c9a144455ab678a6b0 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Tue, 18 Aug 2026 12:17:51 -0400 Subject: [PATCH 16/18] Revert Chrome version on stable shards to 125 --- .ci.yaml | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/.ci.yaml b/.ci.yaml index b8132e66be5..4f76b98fe9f 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -217,6 +217,14 @@ targets: channel: stable version_file: flutter_stable.version package_sharding: "--shardIndex 0 --shardCount 3" + # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the + # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and + # flutter/flutter#190584) which pass --disable-search-engine-choice-screen + # and --disable-background-timer-throttling. + dependencies: >- + [ + {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + ] - name: Linux_web web_dart_unit_test_shard_2 stable recipe: packages/packages @@ -226,6 +234,14 @@ targets: channel: stable version_file: flutter_stable.version package_sharding: "--shardIndex 1 --shardCount 3" + # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the + # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and + # flutter/flutter#190584) which pass --disable-search-engine-choice-screen + # and --disable-background-timer-throttling. + dependencies: >- + [ + {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + ] - name: Linux_web web_dart_unit_test_shard_3 stable recipe: packages/packages @@ -235,6 +251,14 @@ targets: channel: stable version_file: flutter_stable.version package_sharding: "--shardIndex 2 --shardCount 3" + # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the + # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and + # flutter/flutter#190584) which pass --disable-search-engine-choice-screen + # and --disable-background-timer-throttling. + dependencies: >- + [ + {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + ] # Wasm unit tests in master # TODO(stuartmorgan): Switch to testing wasm on master and stable and non-wasm @@ -334,13 +358,17 @@ targets: version_file: flutter_stable.version target_file: linux_custom_package_tests.yaml # See comments on 'master' version above. + # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the + # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and + # flutter/flutter#190584) which pass --disable-search-engine-choice-screen + # and --disable-background-timer-throttling. dependencies: >- [ {"dependency": "android_virtual_device", "version": "android_36_google_apis_x64.textpb"}, {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"}, {"dependency": "cmake", "version": "build_id:8787856497187628321"}, {"dependency": "ninja", "version": "version:1.9.0"}, - {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"} + {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} ] channel: stable @@ -740,6 +768,14 @@ targets: version_file: flutter_stable.version channel: stable package_sharding: "--shardIndex 0 --shardCount 3" + # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the + # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and + # flutter/flutter#190584) which pass --disable-search-engine-choice-screen + # and --disable-background-timer-throttling. + dependencies: >- + [ + {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + ] - name: Linux_web web_platform_tests_shard_2 stable recipe: packages/packages @@ -749,6 +785,14 @@ targets: version_file: flutter_stable.version channel: stable package_sharding: "--shardIndex 1 --shardCount 3" + # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the + # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and + # flutter/flutter#190584) which pass --disable-search-engine-choice-screen + # and --disable-background-timer-throttling. + dependencies: >- + [ + {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + ] - name: Linux_web web_platform_tests_shard_3 stable recipe: packages/packages @@ -758,6 +802,14 @@ targets: version_file: flutter_stable.version channel: stable package_sharding: "--shardIndex 2 --shardCount 3" + # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the + # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and + # flutter/flutter#190584) which pass --disable-search-engine-choice-screen + # and --disable-background-timer-throttling. + dependencies: >- + [ + {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + ] ### Linux desktop tasks - name: Linux_desktop build_all_packages master @@ -799,9 +851,13 @@ targets: target_file: linux_platform_tests.yaml channel: stable # Install Chrome as a default handler for schemes for url_launcher. + # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the + # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and + # flutter/flutter#190584) which pass --disable-search-engine-choice-screen + # and --disable-background-timer-throttling. dependencies: >- [ - {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"} + {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} ] ### iOS+macOS tasks ### From eab0c8a215d961ec85669270109ec4125d564a1e Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Tue, 18 Aug 2026 16:50:13 -0400 Subject: [PATCH 17/18] Split camera_web_test into smaller integration test files --- .ci.yaml | 18 - .../web_platform_tests_camera_web.yaml | 16 - .../web_platform_tests_camera_web_wasm.yaml | 17 - .../camera_web_capture_test.dart | 536 ++++ .../camera_web_controls_test.dart | 600 ++++ .../camera_web_devices_test.dart | 746 +++++ .../camera_web_lifecycle_and_events_test.dart | 848 ++++++ .../camera_web_orientation_test.dart | 321 ++ .../integration_test/camera_web_test.dart | 2677 ----------------- .../example/test_driver/integration_test.dart | 2 +- script/configs/exclude_integration_web.yaml | 3 - 11 files changed, 3052 insertions(+), 2732 deletions(-) delete mode 100644 .ci/targets/web_platform_tests_camera_web.yaml delete mode 100644 .ci/targets/web_platform_tests_camera_web_wasm.yaml create mode 100644 packages/camera/camera_web/example/integration_test/camera_web_capture_test.dart create mode 100644 packages/camera/camera_web/example/integration_test/camera_web_controls_test.dart create mode 100644 packages/camera/camera_web/example/integration_test/camera_web_devices_test.dart create mode 100644 packages/camera/camera_web/example/integration_test/camera_web_lifecycle_and_events_test.dart create mode 100644 packages/camera/camera_web/example/integration_test/camera_web_orientation_test.dart delete mode 100644 packages/camera/camera_web/example/integration_test/camera_web_test.dart diff --git a/.ci.yaml b/.ci.yaml index 4f76b98fe9f..0465b3019f9 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -713,15 +713,6 @@ targets: channel: master package_sharding: "--shardIndex 2 --shardCount 3" - - name: Linux_web web_platform_tests_camera_web master - recipe: packages/packages - bringup: true - timeout: 90 - properties: - target_file: web_platform_tests_camera_web.yaml - version_file: flutter_master.version - channel: master - # Wasm integration tests in master - name: Linux_web web_platform_tests_wasm_shard_1 master recipe: packages/packages @@ -750,15 +741,6 @@ targets: channel: master package_sharding: "--shardIndex 2 --shardCount 3" - - name: Linux_web web_platform_tests_camera_web_wasm master - recipe: packages/packages - bringup: true - timeout: 90 - properties: - target_file: web_platform_tests_camera_web_wasm.yaml - version_file: flutter_master.version - channel: master - # JS integration tests in stable - name: Linux_web web_platform_tests_shard_1 stable recipe: packages/packages diff --git a/.ci/targets/web_platform_tests_camera_web.yaml b/.ci/targets/web_platform_tests_camera_web.yaml deleted file mode 100644 index c21daa1dfb0..00000000000 --- a/.ci/targets/web_platform_tests_camera_web.yaml +++ /dev/null @@ -1,16 +0,0 @@ -tasks: - - name: prepare tool - script: .ci/scripts/prepare_tool.sh - infra_step: true # Note infra steps failing prevents "always" from running. - - name: download Dart deps - script: .ci/scripts/tool_runner.sh - args: ["fetch-deps", "--web", "--supporting-target-platforms-only"] - infra_step: true - - name: drive examples - script: .ci/scripts/tool_runner.sh - args: [ - "drive-examples", - "--web", - "--run-chromedriver", - "--packages=camera_web", - ] diff --git a/.ci/targets/web_platform_tests_camera_web_wasm.yaml b/.ci/targets/web_platform_tests_camera_web_wasm.yaml deleted file mode 100644 index 5f8c0e57144..00000000000 --- a/.ci/targets/web_platform_tests_camera_web_wasm.yaml +++ /dev/null @@ -1,17 +0,0 @@ -tasks: - - name: prepare tool - script: .ci/scripts/prepare_tool.sh - infra_step: true # Note infra steps failing prevents "always" from running. - - name: download Dart deps - script: .ci/scripts/tool_runner.sh - args: ["fetch-deps", "--web", "--supporting-target-platforms-only"] - infra_step: true - - name: drive examples - script: .ci/scripts/tool_runner.sh - args: [ - "drive-examples", - "--web", - "--wasm", - "--run-chromedriver", - "--packages=camera_web", - ] diff --git a/packages/camera/camera_web/example/integration_test/camera_web_capture_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_capture_test.dart new file mode 100644 index 00000000000..6d6a0f0f0bf --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_web_capture_test.dart @@ -0,0 +1,536 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: only_throw_errors + +import 'dart:async'; +import 'dart:js_interop'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/camera_web.dart'; +// ignore_for_file: implementation_imports +import 'package:camera_web/src/camera.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:web/web.dart' hide MediaDeviceKind, OrientationType; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraPlugin', () { + const cameraId = 1; + + late MockWindow mockWindow; + late MockNavigator mockNavigator; + late MockMediaDevices mockMediaDevices; + + late Window window; + late Navigator navigator; + late MediaDevices mediaDevices; + + late HTMLVideoElement videoElement; + + late MockScreen mockScreen; + late MockScreenOrientation mockScreenOrientation; + + late Screen screen; + late ScreenOrientation screenOrientation; + + late MockDocument mockDocument; + late MockElement mockDocumentElement; + + late Document document; + late Element documentElement; + + late MockCameraService cameraService; + + setUp(() async { + mockWindow = MockWindow(); + mockNavigator = MockNavigator(); + mockMediaDevices = MockMediaDevices(); + + window = createJSInteropWrapper(mockWindow) as Window; + navigator = createJSInteropWrapper(mockNavigator) as Navigator; + mediaDevices = createJSInteropWrapper(mockMediaDevices) as MediaDevices; + + mockWindow.navigator = navigator; + mockNavigator.mediaDevices = mediaDevices; + + videoElement = getVideoElementWithBlankStream(const Size(10, 10)); + + mockScreen = MockScreen(); + mockScreenOrientation = MockScreenOrientation(); + + screen = createJSInteropWrapper(mockScreen) as Screen; + screenOrientation = createJSInteropWrapper(mockScreenOrientation) as ScreenOrientation; + + mockScreen.orientation = screenOrientation; + mockWindow.screen = screen; + + mockDocument = MockDocument(); + mockDocumentElement = MockElement(); + + document = createJSInteropWrapper(mockDocument) as Document; + documentElement = createJSInteropWrapper(mockDocumentElement) as Element; + + mockDocument.documentElement = documentElement; + mockWindow.document = document; + + cameraService = MockCameraService(); + + when( + cameraService.getMediaStreamForOptions(any, cameraId: anyNamed('cameraId')), + ).thenAnswer((_) async => videoElement.captureStream()); + + CameraPlatform.instance = CameraPlugin(cameraService: cameraService)..window = window; + }); + + group('takePicture', () { + testWidgets('captures a picture', (WidgetTester tester) async { + final camera = MockCamera(); + final capturedPicture = XFile('/bogus/test'); + + when(camera.takePicture()).thenAnswer((_) async => capturedPicture); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final XFile picture = await CameraPlatform.instance.takePicture(cameraId); + + verify(camera.takePicture()).called(1); + + expect(picture, equals(capturedPicture)); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.takePicture(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when takePicture throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('NotSupportedError'); + + when(camera.takePicture()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.takePicture(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when takePicture throws CameraWebException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.takePicture()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.takePicture(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('startVideoRecording', () { + late Camera camera; + + setUp(() { + camera = MockCamera(); + + when(camera.startVideoRecording()).thenAnswer((_) async {}); + + when(camera.onVideoRecordingError).thenAnswer((_) => const Stream.empty()); + }); + + testWidgets('starts a video recording', (WidgetTester tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.startVideoRecording(cameraId); + + verify(camera.startVideoRecording()).called(1); + }); + + testWidgets('listens to the onVideoRecordingError stream', (WidgetTester tester) async { + final videoRecordingErrorController = StreamController(); + + when(camera.onVideoRecordingError).thenAnswer((_) => videoRecordingErrorController.stream); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.startVideoRecording(cameraId); + + expect(videoRecordingErrorController.hasListener, isTrue); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.startVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when startVideoRecording throws DomException', (WidgetTester tester) async { + final exception = DOMException('InvalidStateError'); + + when(camera.startVideoRecording()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.startVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when startVideoRecording throws CameraWebException', ( + WidgetTester tester, + ) async { + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.startVideoRecording()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.startVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('startVideoCapturing', () { + late Camera camera; + + setUp(() { + camera = MockCamera(); + + when(camera.startVideoRecording()).thenAnswer((_) async {}); + + when(camera.onVideoRecordingError).thenAnswer((_) => const Stream.empty()); + }); + + testWidgets('fails if trying to stream', (WidgetTester tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.startVideoCapturing( + VideoCaptureOptions(cameraId, streamCallback: (CameraImageData imageData) {}), + ), + throwsA(isA()), + ); + }); + }); + + group('stopVideoRecording', () { + testWidgets('stops a video recording', (WidgetTester tester) async { + final camera = MockCamera(); + final capturedVideo = XFile('/bogus/test'); + + when(camera.stopVideoRecording()).thenAnswer((_) async => capturedVideo); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final XFile video = await CameraPlatform.instance.stopVideoRecording(cameraId); + + verify(camera.stopVideoRecording()).called(1); + + expect(video, capturedVideo); + }); + + testWidgets('stops listening to the onVideoRecordingError stream', ( + WidgetTester tester, + ) async { + final camera = MockCamera(); + final videoRecordingErrorController = StreamController(); + final capturedVideo = XFile('/bogus/test'); + + when(camera.startVideoRecording()).thenAnswer((_) async {}); + + when(camera.stopVideoRecording()).thenAnswer((_) async => capturedVideo); + + when(camera.onVideoRecordingError).thenAnswer((_) => videoRecordingErrorController.stream); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.startVideoRecording(cameraId); + final XFile _ = await CameraPlatform.instance.stopVideoRecording(cameraId); + + expect(videoRecordingErrorController.hasListener, isFalse); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.stopVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when stopVideoRecording throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('InvalidStateError'); + + when(camera.stopVideoRecording()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.stopVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when stopVideoRecording throws CameraWebException', ( + WidgetTester tester, + ) async { + final camera = MockCamera(); + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.stopVideoRecording()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.stopVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('pauseVideoRecording', () { + testWidgets('pauses a video recording', (WidgetTester tester) async { + final camera = MockCamera(); + + when(camera.pauseVideoRecording()).thenAnswer((_) async {}); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.pauseVideoRecording(cameraId); + + verify(camera.pauseVideoRecording()).called(1); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.pauseVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when pauseVideoRecording throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('InvalidStateError'); + + when(camera.pauseVideoRecording()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.pauseVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when pauseVideoRecording throws CameraWebException', ( + WidgetTester tester, + ) async { + final camera = MockCamera(); + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.pauseVideoRecording()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.pauseVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('resumeVideoRecording', () { + testWidgets('resumes a video recording', (WidgetTester tester) async { + final camera = MockCamera(); + + when(camera.resumeVideoRecording()).thenAnswer((_) async {}); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.resumeVideoRecording(cameraId); + + verify(camera.resumeVideoRecording()).called(1); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.resumeVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when resumeVideoRecording throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('InvalidStateError'); + + when(camera.resumeVideoRecording()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.resumeVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when resumeVideoRecording throws CameraWebException', ( + WidgetTester tester, + ) async { + final camera = MockCamera(); + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.resumeVideoRecording()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.resumeVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + }); +} diff --git a/packages/camera/camera_web/example/integration_test/camera_web_controls_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_controls_test.dart new file mode 100644 index 00000000000..70a1197c5c2 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_web_controls_test.dart @@ -0,0 +1,600 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: only_throw_errors + +import 'dart:js_interop'; +import 'dart:math'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/camera_web.dart'; +// ignore_for_file: implementation_imports +import 'package:camera_web/src/camera.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart' as widgets; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:web/web.dart' hide MediaDeviceKind, OrientationType; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraPlugin', () { + const cameraId = 1; + + late MockWindow mockWindow; + late MockNavigator mockNavigator; + late MockMediaDevices mockMediaDevices; + + late Window window; + late Navigator navigator; + late MediaDevices mediaDevices; + + late HTMLVideoElement videoElement; + + late MockScreen mockScreen; + late MockScreenOrientation mockScreenOrientation; + + late Screen screen; + late ScreenOrientation screenOrientation; + + late MockDocument mockDocument; + late MockElement mockDocumentElement; + + late Document document; + late Element documentElement; + + late MockCameraService cameraService; + + setUp(() async { + mockWindow = MockWindow(); + mockNavigator = MockNavigator(); + mockMediaDevices = MockMediaDevices(); + + window = createJSInteropWrapper(mockWindow) as Window; + navigator = createJSInteropWrapper(mockNavigator) as Navigator; + mediaDevices = createJSInteropWrapper(mockMediaDevices) as MediaDevices; + + mockWindow.navigator = navigator; + mockNavigator.mediaDevices = mediaDevices; + + videoElement = getVideoElementWithBlankStream(const Size(10, 10)); + + mockScreen = MockScreen(); + mockScreenOrientation = MockScreenOrientation(); + + screen = createJSInteropWrapper(mockScreen) as Screen; + screenOrientation = createJSInteropWrapper(mockScreenOrientation) as ScreenOrientation; + + mockScreen.orientation = screenOrientation; + mockWindow.screen = screen; + + mockDocument = MockDocument(); + mockDocumentElement = MockElement(); + + document = createJSInteropWrapper(mockDocument) as Document; + documentElement = createJSInteropWrapper(mockDocumentElement) as Element; + + mockDocument.documentElement = documentElement; + mockWindow.document = document; + + cameraService = MockCameraService(); + + when( + cameraService.getMediaStreamForOptions(any, cameraId: anyNamed('cameraId')), + ).thenAnswer((_) async => videoElement.captureStream()); + + CameraPlatform.instance = CameraPlugin(cameraService: cameraService)..window = window; + }); + + group('setFlashMode', () { + testWidgets('calls setFlashMode on the camera', (WidgetTester tester) async { + final camera = MockCamera(); + const FlashMode flashMode = FlashMode.always; + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.setFlashMode(cameraId, flashMode); + + verify(camera.setFlashMode(flashMode)).called(1); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.setFlashMode(cameraId, FlashMode.always), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when setFlashMode throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('NotSupportedError'); + + when(camera.setFlashMode(any)).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.setFlashMode(cameraId, FlashMode.always), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when setFlashMode throws CameraWebException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.setFlashMode(any)).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.setFlashMode(cameraId, FlashMode.torch), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + testWidgets('setExposureMode throws UnimplementedError', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.setExposureMode(cameraId, ExposureMode.auto), + throwsUnimplementedError, + ); + }); + + testWidgets('setExposurePoint throws UnimplementedError', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.setExposurePoint(cameraId, const Point(0, 0)), + throwsUnimplementedError, + ); + }); + + testWidgets('getMinExposureOffset throws UnimplementedError', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.getMinExposureOffset(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('getMaxExposureOffset throws UnimplementedError', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.getMaxExposureOffset(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('getExposureOffsetStepSize throws UnimplementedError', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.getExposureOffsetStepSize(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('setExposureOffset throws UnimplementedError', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.setExposureOffset(cameraId, 0), + throwsUnimplementedError, + ); + }); + + testWidgets('setFocusMode throws UnimplementedError', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.setFocusMode(cameraId, FocusMode.auto), + throwsUnimplementedError, + ); + }); + + testWidgets('setFocusPoint throws UnimplementedError', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.setFocusPoint(cameraId, const Point(0, 0)), + throwsUnimplementedError, + ); + }); + + group('getMaxZoomLevel', () { + testWidgets('calls getMaxZoomLevel on the camera', (WidgetTester tester) async { + final camera = MockCamera(); + const maximumZoomLevel = 100.0; + + when(camera.getMaxZoomLevel()).thenReturn(maximumZoomLevel); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect(await CameraPlatform.instance.getMaxZoomLevel(cameraId), equals(maximumZoomLevel)); + + verify(camera.getMaxZoomLevel()).called(1); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () async => CameraPlatform.instance.getMaxZoomLevel(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when getMaxZoomLevel throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('NotSupportedError'); + + when(camera.getMaxZoomLevel()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.getMaxZoomLevel(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when getMaxZoomLevel throws CameraWebException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.getMaxZoomLevel()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.getMaxZoomLevel(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('getMinZoomLevel', () { + testWidgets('calls getMinZoomLevel on the camera', (WidgetTester tester) async { + final camera = MockCamera(); + const minimumZoomLevel = 100.0; + + when(camera.getMinZoomLevel()).thenReturn(minimumZoomLevel); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect(await CameraPlatform.instance.getMinZoomLevel(cameraId), equals(minimumZoomLevel)); + + verify(camera.getMinZoomLevel()).called(1); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () async => CameraPlatform.instance.getMinZoomLevel(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when getMinZoomLevel throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('NotSupportedError'); + + when(camera.getMinZoomLevel()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.getMinZoomLevel(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when getMinZoomLevel throws CameraWebException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.getMinZoomLevel()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.getMinZoomLevel(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('setZoomLevel', () { + testWidgets('calls setZoomLevel on the camera', (WidgetTester tester) async { + final camera = MockCamera(); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + const zoom = 100.0; + + await CameraPlatform.instance.setZoomLevel(cameraId, zoom); + + verify(camera.setZoomLevel(zoom)).called(1); + }); + + group('throws CameraException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), + throwsA( + isA().having( + (CameraException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when setZoomLevel throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('NotSupportedError'); + + when(camera.setZoomLevel(any)).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), + throwsA( + isA().having((CameraException e) => e.code, 'code', exception.name), + ), + ); + }); + + testWidgets('when setZoomLevel throws PlatformException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = PlatformException( + code: CameraErrorCode.notSupported.toString(), + message: 'message', + ); + + when(camera.setZoomLevel(any)).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), + throwsA( + isA().having((CameraException e) => e.code, 'code', exception.code), + ), + ); + }); + + testWidgets('when setZoomLevel throws CameraWebException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.setZoomLevel(any)).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), + throwsA( + isA().having( + (CameraException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('pausePreview', () { + testWidgets('calls pause on the camera', (WidgetTester tester) async { + final camera = MockCamera(); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.pausePreview(cameraId); + + verify(camera.pause()).called(1); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () async => CameraPlatform.instance.pausePreview(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when pause throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('NotSupportedError'); + + when(camera.pause()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.pausePreview(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); + }); + + group('resumePreview', () { + testWidgets('calls play on the camera', (WidgetTester tester) async { + final camera = MockCamera(); + + when(camera.play()).thenAnswer((_) async {}); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.resumePreview(cameraId); + + verify(camera.play()).called(1); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () async => CameraPlatform.instance.resumePreview(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when play throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('NotSupportedError'); + + when(camera.play()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.resumePreview(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when play throws CameraWebException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = CameraWebException(cameraId, CameraErrorCode.unknown, 'description'); + + when(camera.play()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.resumePreview(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + testWidgets('buildPreview returns an HtmlElementView ' + 'with an appropriate view type', (WidgetTester tester) async { + final camera = Camera(textureId: cameraId, cameraService: cameraService); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + CameraPlatform.instance.buildPreview(cameraId), + isA().having( + (widgets.HtmlElementView view) => view.viewType, + 'viewType', + camera.getViewType(), + ), + ); + }); + }); +} diff --git a/packages/camera/camera_web/example/integration_test/camera_web_devices_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_devices_test.dart new file mode 100644 index 00000000000..3d09f2f1e98 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_web_devices_test.dart @@ -0,0 +1,746 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: only_throw_errors + +import 'dart:async'; +import 'dart:js_interop'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/camera_web.dart'; +// ignore_for_file: implementation_imports +import 'package:camera_web/src/camera.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:web/web.dart' hide MediaDeviceKind, OrientationType; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraPlugin', () { + const cameraId = 1; + + late MockWindow mockWindow; + late MockNavigator mockNavigator; + late MockMediaDevices mockMediaDevices; + + late Window window; + late Navigator navigator; + late MediaDevices mediaDevices; + + late HTMLVideoElement videoElement; + + late MockScreen mockScreen; + late MockScreenOrientation mockScreenOrientation; + + late Screen screen; + late ScreenOrientation screenOrientation; + + late MockDocument mockDocument; + late MockElement mockDocumentElement; + + late Document document; + late Element documentElement; + + late MockCameraService cameraService; + + setUp(() async { + mockWindow = MockWindow(); + mockNavigator = MockNavigator(); + mockMediaDevices = MockMediaDevices(); + + window = createJSInteropWrapper(mockWindow) as Window; + navigator = createJSInteropWrapper(mockNavigator) as Navigator; + mediaDevices = createJSInteropWrapper(mockMediaDevices) as MediaDevices; + + mockWindow.navigator = navigator; + mockNavigator.mediaDevices = mediaDevices; + + videoElement = getVideoElementWithBlankStream(const Size(10, 10)); + + mockScreen = MockScreen(); + mockScreenOrientation = MockScreenOrientation(); + + screen = createJSInteropWrapper(mockScreen) as Screen; + screenOrientation = createJSInteropWrapper(mockScreenOrientation) as ScreenOrientation; + + mockScreen.orientation = screenOrientation; + mockWindow.screen = screen; + + mockDocument = MockDocument(); + mockDocumentElement = MockElement(); + + document = createJSInteropWrapper(mockDocument) as Document; + documentElement = createJSInteropWrapper(mockDocumentElement) as Element; + + mockDocument.documentElement = documentElement; + mockWindow.document = document; + + cameraService = MockCameraService(); + + when( + cameraService.getMediaStreamForOptions(any, cameraId: anyNamed('cameraId')), + ).thenAnswer((_) async => videoElement.captureStream()); + + CameraPlatform.instance = CameraPlugin(cameraService: cameraService)..window = window; + }); + + group('availableCameras', () { + setUp(() { + when(cameraService.getFacingModeForVideoTrack(any)).thenReturn(null); + + mockMediaDevices.enumerateDevices = () { + return Future>.value([].toJS).toJS; + }.toJS; + }); + + testWidgets('requests video permissions', (WidgetTester tester) async { + final List _ = await CameraPlatform.instance.availableCameras(); + + verify(cameraService.getMediaStreamForOptions(const CameraOptions())).called(1); + }); + + testWidgets('releases the camera stream ' + 'used to request video permissions', (WidgetTester tester) async { + final mockVideoTrack = MockMediaStreamTrack(); + final videoTrack = createJSInteropWrapper(mockVideoTrack) as MediaStreamTrack; + + var videoTrackStopped = false; + mockVideoTrack.stop = () { + videoTrackStopped = true; + }.toJS; + + when(cameraService.getMediaStreamForOptions(const CameraOptions())).thenAnswer( + (_) => Future.value( + createJSInteropWrapper(FakeMediaStream([videoTrack])) as MediaStream, + ), + ); + + final List _ = await CameraPlatform.instance.availableCameras(); + + expect(videoTrackStopped, isTrue); + }); + + testWidgets('gets a video stream ' + 'for a video input device', (WidgetTester tester) async { + final videoDevice = + createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) + as MediaDeviceInfo; + + mockMediaDevices.enumerateDevices = () { + return Future>.value([videoDevice].toJS).toJS; + }.toJS; + + final List _ = await CameraPlatform.instance.availableCameras(); + + verify( + cameraService.getMediaStreamForOptions( + CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), + ), + ).called(1); + }); + + testWidgets('does not get a video stream ' + 'for the video input device ' + 'with an empty device id', (WidgetTester tester) async { + final videoDevice = + createJSInteropWrapper(FakeMediaDeviceInfo('', 'Camera 1', MediaDeviceKind.videoInput)) + as MediaDeviceInfo; + + mockMediaDevices.enumerateDevices = () { + return Future>.value([videoDevice].toJS).toJS; + }.toJS; + + final List _ = await CameraPlatform.instance.availableCameras(); + + verifyNever( + cameraService.getMediaStreamForOptions( + CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), + ), + ); + }); + + testWidgets('gets the facing mode ' + 'from the first available video track ' + 'of the video input device', (WidgetTester tester) async { + final videoDevice = + createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) + as MediaDeviceInfo; + + final videoStream = + createJSInteropWrapper( + FakeMediaStream([ + createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, + createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, + ]), + ) + as MediaStream; + + when( + cameraService.getMediaStreamForOptions( + CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), + ), + ).thenAnswer((_) => Future.value(videoStream)); + + mockMediaDevices.enumerateDevices = () { + return Future>.value([videoDevice].toJS).toJS; + }.toJS; + + final List _ = await CameraPlatform.instance.availableCameras(); + + verify( + cameraService.getFacingModeForVideoTrack(videoStream.getVideoTracks().toDart.first), + ).called(1); + }); + + testWidgets('returns appropriate camera descriptions ' + 'for multiple video devices ' + 'based on video streams', (WidgetTester tester) async { + final firstVideoDevice = + createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) + as MediaDeviceInfo; + + final secondVideoDevice = + createJSInteropWrapper(FakeMediaDeviceInfo('4', 'Camera 4', MediaDeviceKind.videoInput)) + as MediaDeviceInfo; + + // Create a video stream for the first video device. + final firstVideoStream = + createJSInteropWrapper( + FakeMediaStream([ + createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, + createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, + ]), + ) + as MediaStream; + + // Create a video stream for the second video device. + final secondVideoStream = + createJSInteropWrapper( + FakeMediaStream([ + createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, + ]), + ) + as MediaStream; + + // Mock media devices to return two video input devices + // and two audio devices. + mockMediaDevices.enumerateDevices = () { + return Future>.value( + [ + firstVideoDevice, + createJSInteropWrapper( + FakeMediaDeviceInfo('2', 'Audio Input 2', MediaDeviceKind.audioInput), + ) + as MediaDeviceInfo, + createJSInteropWrapper( + FakeMediaDeviceInfo('3', 'Audio Output 3', MediaDeviceKind.audioOutput), + ) + as MediaDeviceInfo, + secondVideoDevice, + ].toJS, + ).toJS; + }.toJS; + + // Mock camera service to return the first video stream + // for the first video device. + when( + cameraService.getMediaStreamForOptions( + CameraOptions(video: VideoConstraints(deviceId: firstVideoDevice.deviceId)), + ), + ).thenAnswer((_) => Future.value(firstVideoStream)); + + // Mock camera service to return the second video stream + // for the second video device. + when( + cameraService.getMediaStreamForOptions( + CameraOptions(video: VideoConstraints(deviceId: secondVideoDevice.deviceId)), + ), + ).thenAnswer((_) => Future.value(secondVideoStream)); + + // Mock camera service to return a user facing mode + // for the first video stream. + when( + cameraService.getFacingModeForVideoTrack(firstVideoStream.getVideoTracks().toDart.first), + ).thenReturn('user'); + + when( + cameraService.mapFacingModeToLensDirection('user'), + ).thenReturn(CameraLensDirection.front); + + // Mock camera service to return an environment facing mode + // for the second video stream. + when( + cameraService.getFacingModeForVideoTrack(secondVideoStream.getVideoTracks().toDart.first), + ).thenReturn('environment'); + + when( + cameraService.mapFacingModeToLensDirection('environment'), + ).thenReturn(CameraLensDirection.back); + + final List cameras = await CameraPlatform.instance.availableCameras(); + + // Expect two cameras and ignore two audio devices. + expect( + cameras, + equals([ + CameraDescription( + name: firstVideoDevice.label, + lensDirection: CameraLensDirection.front, + sensorOrientation: 0, + ), + CameraDescription( + name: secondVideoDevice.label, + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ]), + ); + }); + + testWidgets('sets camera metadata ' + 'for the camera description', (WidgetTester tester) async { + final videoDevice = + createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) + as MediaDeviceInfo; + + final videoStream = + createJSInteropWrapper( + FakeMediaStream([ + createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, + createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, + ]), + ) + as MediaStream; + + mockMediaDevices.enumerateDevices = () { + return Future>.value([videoDevice].toJS).toJS; + }.toJS; + + when( + cameraService.getMediaStreamForOptions( + CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), + ), + ).thenAnswer((_) => Future.value(videoStream)); + + when( + cameraService.getFacingModeForVideoTrack(videoStream.getVideoTracks().toDart.first), + ).thenReturn('left'); + + when( + cameraService.mapFacingModeToLensDirection('left'), + ).thenReturn(CameraLensDirection.external); + + final CameraDescription camera = (await CameraPlatform.instance.availableCameras()).first; + + expect( + (CameraPlatform.instance as CameraPlugin).camerasMetadata, + equals({ + camera: CameraMetadata(deviceId: videoDevice.deviceId, facingMode: 'left'), + }), + ); + }); + + testWidgets('releases the video stream ' + 'of a video input device', (WidgetTester tester) async { + final videoDevice = + createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) + as MediaDeviceInfo; + + final tracks = []; + final stops = List.generate(2, (_) => false); + for (var i = 0; i < stops.length; i++) { + final track = MockMediaStreamTrack(); + track.stop = () { + stops[i] = true; + }.toJS; + tracks.add(createJSInteropWrapper(track) as MediaStreamTrack); + } + + final videoStream = createJSInteropWrapper(FakeMediaStream(tracks)) as MediaStream; + + mockMediaDevices.enumerateDevices = () { + return Future>.value([videoDevice].toJS).toJS; + }.toJS; + + when( + cameraService.getMediaStreamForOptions( + CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), + ), + ).thenAnswer((_) => Future.value(videoStream)); + + final List _ = await CameraPlatform.instance.availableCameras(); + + expect(stops.every((bool e) => e), isTrue); + }); + + group('throws CameraException', () { + testWidgets('when MediaDevices.enumerateDevices throws DomException', ( + WidgetTester tester, + ) async { + final exception = DOMException('UnknownError'); + + mockMediaDevices.enumerateDevices = () { + throw exception; + // ignore: dead_code + return Future>.value([].toJS).toJS; + }.toJS; + + expect( + () => CameraPlatform.instance.availableCameras(), + throwsA( + isA().having((CameraException e) => e.code, 'code', exception.name), + ), + ); + }); + + testWidgets('when CameraService.getMediaStreamForOptions ' + 'throws CameraWebException', (WidgetTester tester) async { + final exception = CameraWebException(cameraId, CameraErrorCode.security, 'description'); + + when(cameraService.getMediaStreamForOptions(any)).thenThrow(exception); + + expect( + CameraPlatform.instance.availableCameras(), + throwsA( + isA().having( + (CameraException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + + testWidgets('when CameraService.getMediaStreamForOptions ' + 'throws PlatformException', (WidgetTester tester) async { + final exception = PlatformException( + code: CameraErrorCode.notSupported.toString(), + message: 'message', + ); + + when(cameraService.getMediaStreamForOptions(any)).thenThrow(exception); + + expect( + () => CameraPlatform.instance.availableCameras(), + throwsA( + isA().having((CameraException e) => e.code, 'code', exception.code), + ), + ); + }); + }); + }); + + group('createCamera', () { + group('creates a camera', () { + const ultraHighResolutionSize = Size(3840, 2160); + const maxResolutionSize = Size(3840, 2160); + + const cameraDescription = CameraDescription( + name: 'name', + lensDirection: CameraLensDirection.front, + sensorOrientation: 0, + ); + + const cameraMetadata = CameraMetadata(deviceId: 'deviceId', facingMode: 'user'); + + setUp(() { + // Add metadata for the camera description. + (CameraPlatform.instance as CameraPlugin).camerasMetadata[cameraDescription] = + cameraMetadata; + + when(cameraService.mapFacingModeToCameraType('user')).thenReturn(CameraType.user); + }); + + testWidgets('with appropriate options', (WidgetTester tester) async { + when( + cameraService.mapResolutionPresetToSize(ResolutionPreset.ultraHigh), + ).thenReturn(ultraHighResolutionSize); + + final int cameraId = await CameraPlatform.instance.createCamera( + cameraDescription, + ResolutionPreset.ultraHigh, + enableAudio: true, + ); + + final Camera? camera = (CameraPlatform.instance as CameraPlugin).cameras[cameraId]; + + expect(camera, isA()); + expect(camera!.textureId, cameraId); + expect(camera.options.audio.enabled, isTrue); + expect(camera.options.video.facingMode, equals(FacingModeConstraint(CameraType.user))); + expect(camera.options.video.width!.ideal, ultraHighResolutionSize.width.toInt()); + expect(camera.options.video.height!.ideal, ultraHighResolutionSize.height.toInt()); + expect(camera.options.video.deviceId, cameraMetadata.deviceId); + }); + + testWidgets('with appropriate createCameraWithSettings options', ( + WidgetTester tester, + ) async { + when( + cameraService.mapResolutionPresetToSize(ResolutionPreset.ultraHigh), + ).thenReturn(ultraHighResolutionSize); + + final int cameraId = await CameraPlatform.instance.createCameraWithSettings( + cameraDescription, + const MediaSettings( + resolutionPreset: ResolutionPreset.ultraHigh, + videoBitrate: 200000, + audioBitrate: 32000, + enableAudio: true, + ), + ); + + final Camera? camera = (CameraPlatform.instance as CameraPlugin).cameras[cameraId]; + + expect(camera, isA()); + expect(camera!.textureId, cameraId); + expect(camera.options.audio.enabled, isTrue); + expect(camera.options.video.facingMode, equals(FacingModeConstraint(CameraType.user))); + expect(camera.options.video.width!.ideal, ultraHighResolutionSize.width.toInt()); + expect(camera.options.video.height!.ideal, ultraHighResolutionSize.height.toInt()); + expect(camera.options.video.deviceId, cameraMetadata.deviceId); + }); + + testWidgets('with a max resolution preset ' + 'and enabled audio set to false ' + 'when no options are specified', (WidgetTester tester) async { + when( + cameraService.mapResolutionPresetToSize(ResolutionPreset.max), + ).thenReturn(maxResolutionSize); + + final int cameraId = await CameraPlatform.instance.createCamera(cameraDescription, null); + + final Camera? camera = (CameraPlatform.instance as CameraPlugin).cameras[cameraId]; + + expect(camera, isA()); + expect(camera!.textureId, cameraId); + expect(camera.options.audio.enabled, isFalse); + expect(camera.options.video.facingMode, equals(FacingModeConstraint(CameraType.user))); + expect(camera.options.video.width!.ideal, maxResolutionSize.width.toInt()); + expect(camera.options.video.height!.ideal, maxResolutionSize.height.toInt()); + expect(camera.options.video.deviceId, cameraMetadata.deviceId); + }); + + testWidgets('with a max resolution preset ' + 'and enabled audio set to false ' + 'when no options are specified ' + 'using createCameraWithSettings', (WidgetTester tester) async { + when( + cameraService.mapResolutionPresetToSize(ResolutionPreset.max), + ).thenReturn(maxResolutionSize); + + final int cameraId = await CameraPlatform.instance.createCameraWithSettings( + cameraDescription, + const MediaSettings(resolutionPreset: ResolutionPreset.max), + ); + + final Camera? camera = (CameraPlatform.instance as CameraPlugin).cameras[cameraId]; + + expect(camera, isA()); + expect(camera!.options.audio.enabled, isFalse); + expect(camera.options.video.facingMode, equals(FacingModeConstraint(CameraType.user))); + expect(camera.options.video.width!.ideal, maxResolutionSize.width.toInt()); + expect(camera.options.video.height!.ideal, maxResolutionSize.height.toInt()); + expect(camera.options.video.deviceId, cameraMetadata.deviceId); + }); + }); + + testWidgets('throws CameraException ' + 'with missingMetadata error ' + 'if there is no metadata ' + 'for the given camera description', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.createCamera( + const CameraDescription( + name: 'name', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.ultraHigh, + ), + throwsA( + isA().having( + (CameraException e) => e.code, + 'code', + CameraErrorCode.missingMetadata.toString(), + ), + ), + ); + }); + + testWidgets('throws CameraException ' + 'with missingMetadata error ' + 'if there is no metadata ' + 'for the given camera description ' + 'using createCameraWithSettings', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.createCameraWithSettings( + const CameraDescription( + name: 'name', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + const MediaSettings( + resolutionPreset: ResolutionPreset.low, + fps: 15, + videoBitrate: 200000, + audioBitrate: 32000, + enableAudio: true, + ), + ), + throwsA( + isA().having( + (CameraException e) => e.code, + 'code', + CameraErrorCode.missingMetadata.toString(), + ), + ), + ); + }); + }); + + group('initializeCamera', () { + late Camera camera; + late MockVideoElement mockVideoElement; + late HTMLVideoElement videoElement; + + late StreamController errorStreamController, abortStreamController; + late StreamController endedStreamController; + + setUp(() { + camera = MockCamera(); + mockVideoElement = MockVideoElement(); + videoElement = createJSInteropWrapper(mockVideoElement) as HTMLVideoElement; + + errorStreamController = StreamController(); + abortStreamController = StreamController(); + endedStreamController = StreamController(); + + when(camera.getVideoSize()).thenReturn(const Size(10, 10)); + when(camera.initialize()).thenAnswer((_) => Future.value()); + when(camera.play()).thenAnswer((_) => Future.value()); + + when(camera.videoElement).thenReturn(videoElement); + + final errorProvider = MockEventStreamProvider(); + final abortProvider = MockEventStreamProvider(); + + (CameraPlatform.instance as CameraPlugin).videoElementOnErrorProvider = errorProvider; + (CameraPlatform.instance as CameraPlugin).videoElementOnAbortProvider = abortProvider; + + when( + errorProvider.forElement(videoElement), + ).thenAnswer((_) => FakeElementStream(errorStreamController.stream)); + when( + abortProvider.forElement(videoElement), + ).thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + + when(camera.onEnded).thenAnswer((_) => endedStreamController.stream); + }); + + testWidgets('initializes and plays the camera', (WidgetTester tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.initializeCamera(cameraId); + + verify(camera.initialize()).called(1); + verify(camera.play()).called(1); + }); + + testWidgets('starts listening to the camera video error and abort events', ( + WidgetTester tester, + ) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect(errorStreamController.hasListener, isFalse); + expect(abortStreamController.hasListener, isFalse); + + await CameraPlatform.instance.initializeCamera(cameraId); + + expect(errorStreamController.hasListener, isTrue); + expect(abortStreamController.hasListener, isTrue); + }); + + testWidgets('starts listening to the camera ended events', (WidgetTester tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect(endedStreamController.hasListener, isFalse); + + await CameraPlatform.instance.initializeCamera(cameraId); + + expect(endedStreamController.hasListener, isTrue); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.initializeCamera(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when camera throws CameraWebException', (WidgetTester tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.permissionDenied, + 'description', + ); + + when(camera.initialize()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.initializeCamera(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + + testWidgets('when camera throws DomException', (WidgetTester tester) async { + final exception = DOMException('NotAllowedError'); + + when(camera.initialize()).thenAnswer((_) => Future.value()); + when(camera.play()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.initializeCamera(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); + }); + }); +} diff --git a/packages/camera/camera_web/example/integration_test/camera_web_lifecycle_and_events_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_lifecycle_and_events_test.dart new file mode 100644 index 00000000000..0b05a7ffdfb --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_web_lifecycle_and_events_test.dart @@ -0,0 +1,848 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: only_throw_errors + +import 'dart:async'; +import 'dart:js_interop'; + +import 'package:async/async.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/camera_web.dart'; +// ignore_for_file: implementation_imports +import 'package:camera_web/src/camera.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:web/web.dart' hide MediaDeviceKind, OrientationType; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraPlugin', () { + const cameraId = 1; + + late MockWindow mockWindow; + late MockNavigator mockNavigator; + late MockMediaDevices mockMediaDevices; + + late Window window; + late Navigator navigator; + late MediaDevices mediaDevices; + + late HTMLVideoElement videoElement; + + late MockScreen mockScreen; + late MockScreenOrientation mockScreenOrientation; + + late Screen screen; + late ScreenOrientation screenOrientation; + + late MockDocument mockDocument; + late MockElement mockDocumentElement; + + late Document document; + late Element documentElement; + + late MockCameraService cameraService; + + setUp(() async { + mockWindow = MockWindow(); + mockNavigator = MockNavigator(); + mockMediaDevices = MockMediaDevices(); + + window = createJSInteropWrapper(mockWindow) as Window; + navigator = createJSInteropWrapper(mockNavigator) as Navigator; + mediaDevices = createJSInteropWrapper(mockMediaDevices) as MediaDevices; + + mockWindow.navigator = navigator; + mockNavigator.mediaDevices = mediaDevices; + + videoElement = getVideoElementWithBlankStream(const Size(10, 10)); + + mockScreen = MockScreen(); + mockScreenOrientation = MockScreenOrientation(); + + screen = createJSInteropWrapper(mockScreen) as Screen; + screenOrientation = createJSInteropWrapper(mockScreenOrientation) as ScreenOrientation; + + mockScreen.orientation = screenOrientation; + mockWindow.screen = screen; + + mockDocument = MockDocument(); + mockDocumentElement = MockElement(); + + document = createJSInteropWrapper(mockDocument) as Document; + documentElement = createJSInteropWrapper(mockDocumentElement) as Element; + + mockDocument.documentElement = documentElement; + mockWindow.document = document; + + cameraService = MockCameraService(); + + when( + cameraService.getMediaStreamForOptions(any, cameraId: anyNamed('cameraId')), + ).thenAnswer((_) async => videoElement.captureStream()); + + CameraPlatform.instance = CameraPlugin(cameraService: cameraService)..window = window; + }); + + testWidgets('CameraPlugin is the live instance', (WidgetTester tester) async { + expect(CameraPlatform.instance, isA()); + }); + group('dispose', () { + late Camera camera; + late MockVideoElement mockVideoElement; + late HTMLVideoElement videoElement; + + late StreamController errorStreamController, abortStreamController; + late StreamController endedStreamController; + late StreamController videoRecordingErrorController; + + setUp(() { + camera = MockCamera(); + mockVideoElement = MockVideoElement(); + videoElement = createJSInteropWrapper(mockVideoElement) as HTMLVideoElement; + + errorStreamController = StreamController(); + abortStreamController = StreamController(); + endedStreamController = StreamController(); + videoRecordingErrorController = StreamController(); + + when(camera.getVideoSize()).thenReturn(const Size(10, 10)); + when(camera.initialize()).thenAnswer((_) => Future.value()); + when(camera.play()).thenAnswer((_) => Future.value()); + when(camera.dispose()).thenAnswer((_) => Future.value()); + + when(camera.videoElement).thenReturn(videoElement); + + final errorProvider = MockEventStreamProvider(); + final abortProvider = MockEventStreamProvider(); + + (CameraPlatform.instance as CameraPlugin).videoElementOnErrorProvider = errorProvider; + (CameraPlatform.instance as CameraPlugin).videoElementOnAbortProvider = abortProvider; + + when( + errorProvider.forElement(videoElement), + ).thenAnswer((_) => FakeElementStream(errorStreamController.stream)); + when( + abortProvider.forElement(videoElement), + ).thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + + when(camera.onEnded).thenAnswer((_) => endedStreamController.stream); + + when(camera.onVideoRecordingError).thenAnswer((_) => videoRecordingErrorController.stream); + + when(camera.startVideoRecording()).thenAnswer((_) async {}); + }); + + testWidgets('disposes the correct camera', (WidgetTester tester) async { + const firstCameraId = 0; + const secondCameraId = 1; + + final firstCamera = MockCamera(); + final secondCamera = MockCamera(); + + when(firstCamera.dispose()).thenAnswer((_) => Future.value()); + when(secondCamera.dispose()).thenAnswer((_) => Future.value()); + + // Save cameras in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras.addAll({ + firstCameraId: firstCamera, + secondCameraId: secondCamera, + }); + + // Dispose the first camera. + await CameraPlatform.instance.dispose(firstCameraId); + + // The first camera should be disposed. + verify(firstCamera.dispose()).called(1); + verifyNever(secondCamera.dispose()); + + // The first camera should be removed from the camera plugin. + expect( + (CameraPlatform.instance as CameraPlugin).cameras, + equals({secondCameraId: secondCamera}), + ); + }); + + testWidgets('cancels the camera video error and abort subscriptions', ( + WidgetTester tester, + ) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.initializeCamera(cameraId); + await CameraPlatform.instance.dispose(cameraId); + + expect(errorStreamController.hasListener, isFalse); + expect(abortStreamController.hasListener, isFalse); + }); + + testWidgets('cancels the camera ended subscriptions', (WidgetTester tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.initializeCamera(cameraId); + await CameraPlatform.instance.dispose(cameraId); + + expect(endedStreamController.hasListener, isFalse); + }); + + testWidgets('cancels the camera video recording error subscriptions', ( + WidgetTester tester, + ) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.initializeCamera(cameraId); + await CameraPlatform.instance.startVideoRecording(cameraId); + await CameraPlatform.instance.dispose(cameraId); + + expect(videoRecordingErrorController.hasListener, isFalse); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.dispose(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when dispose throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('InvalidAccessError'); + + when(camera.dispose()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.dispose(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); + }); + + group('getCamera', () { + testWidgets('returns the correct camera', (WidgetTester tester) async { + final camera = Camera(textureId: cameraId, cameraService: cameraService); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect((CameraPlatform.instance as CameraPlugin).getCamera(cameraId), equals(camera)); + }); + + testWidgets('throws PlatformException ' + 'with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () => (CameraPlatform.instance as CameraPlugin).getCamera(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + }); + + group('events', () { + late MockCamera camera; + late MockVideoElement mockVideoElement; + late HTMLVideoElement videoElement; + + late StreamController errorStreamController, abortStreamController; + late StreamController endedStreamController; + late StreamController videoRecordingErrorController; + + setUp(() { + camera = MockCamera(); + mockVideoElement = MockVideoElement(); + videoElement = createJSInteropWrapper(mockVideoElement) as HTMLVideoElement; + + errorStreamController = StreamController(); + abortStreamController = StreamController(); + endedStreamController = StreamController(); + videoRecordingErrorController = StreamController(); + + when(camera.getVideoSize()).thenReturn(const Size(10, 10)); + when(camera.initialize()).thenAnswer((_) => Future.value()); + when(camera.play()).thenAnswer((_) => Future.value()); + + when(camera.videoElement).thenReturn(videoElement); + + final errorProvider = MockEventStreamProvider(); + final abortProvider = MockEventStreamProvider(); + + (CameraPlatform.instance as CameraPlugin).videoElementOnErrorProvider = errorProvider; + (CameraPlatform.instance as CameraPlugin).videoElementOnAbortProvider = abortProvider; + + when( + errorProvider.forElement(any), + ).thenAnswer((_) => FakeElementStream(errorStreamController.stream)); + when( + abortProvider.forElement(any), + ).thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + + when(camera.onEnded).thenAnswer((_) => endedStreamController.stream); + + when(camera.onVideoRecordingError).thenAnswer((_) => videoRecordingErrorController.stream); + + when(camera.startVideoRecording()).thenAnswer((_) async {}); + }); + + testWidgets('onCameraInitialized emits a CameraInitializedEvent ' + 'on initializeCamera', (WidgetTester tester) async { + // Mock the camera to use a blank video stream of size 1280x720. + const videoSize = Size(1280, 720); + + videoElement = getVideoElementWithBlankStream(videoSize); + + when( + cameraService.getMediaStreamForOptions(any, cameraId: cameraId), + ).thenAnswer((_) async => videoElement.captureStream()); + + final camera = Camera(textureId: cameraId, cameraService: cameraService); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final Stream eventStream = CameraPlatform.instance + .onCameraInitialized(cameraId); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + + expect( + await streamQueue.next, + equals( + CameraInitializedEvent( + cameraId, + videoSize.width, + videoSize.height, + ExposureMode.auto, + false, + FocusMode.auto, + false, + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('onCameraResolutionChanged emits an empty stream', (WidgetTester tester) async { + final Stream stream = CameraPlatform.instance + .onCameraResolutionChanged(cameraId); + expect(await stream.isEmpty, isTrue); + }); + + testWidgets('onCameraClosing emits a CameraClosingEvent ' + 'on the camera ended event', (WidgetTester tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final Stream eventStream = CameraPlatform.instance.onCameraClosing( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + + endedStreamController.add( + createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, + ); + + expect(await streamQueue.next, equals(const CameraClosingEvent(cameraId))); + + await streamQueue.cancel(); + }); + + group('onCameraError', () { + setUp(() { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + }); + + testWidgets('emits a CameraErrorEvent ' + 'on the camera video error event ' + 'with a message', (WidgetTester tester) async { + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + + final error = + createJSInteropWrapper( + FakeMediaError(MediaError.MEDIA_ERR_NETWORK, 'A network error occurred.'), + ) + as MediaError; + + final CameraErrorCode errorCode = CameraErrorCode.fromMediaError(error); + + mockVideoElement.error = error; + errorStreamController.add(Event('error')); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent(cameraId, 'Error code: $errorCode, error message: ${error.message}'), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on the camera video error event ' + 'with no message', (WidgetTester tester) async { + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + + final error = + createJSInteropWrapper(FakeMediaError(MediaError.MEDIA_ERR_NETWORK)) as MediaError; + final CameraErrorCode errorCode = CameraErrorCode.fromMediaError(error); + + mockVideoElement.error = error; + errorStreamController.add(Event('error')); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: $errorCode, error message: No further diagnostic information can be determined or provided.', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on the camera video abort event', (WidgetTester tester) async { + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + + abortStreamController.add(Event('abort')); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + "Error code: ${CameraErrorCode.abort}, error message: The video element's source has not fully loaded.", + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on takePicture error', (WidgetTester tester) async { + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.takePicture()).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.takePicture(cameraId), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on setFlashMode error', (WidgetTester tester) async { + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.setFlashMode(any)).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.setFlashMode(cameraId, FlashMode.always), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on getMaxZoomLevel error', (WidgetTester tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.zoomLevelNotSupported, + 'description', + ); + + when(camera.getMaxZoomLevel()).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.getMaxZoomLevel(cameraId), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on getMinZoomLevel error', (WidgetTester tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.zoomLevelNotSupported, + 'description', + ); + + when(camera.getMinZoomLevel()).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.getMinZoomLevel(cameraId), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on setZoomLevel error', (WidgetTester tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.zoomLevelNotSupported, + 'description', + ); + + when(camera.setZoomLevel(any)).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on resumePreview error', (WidgetTester tester) async { + final exception = CameraWebException(cameraId, CameraErrorCode.unknown, 'description'); + + when(camera.play()).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.resumePreview(cameraId), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on startVideoRecording error', (WidgetTester tester) async { + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.onVideoRecordingError).thenAnswer((_) => const Stream.empty()); + + when(camera.startVideoRecording()).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.startVideoRecording(cameraId), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on the camera video recording error event', (WidgetTester tester) async { + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + await CameraPlatform.instance.startVideoRecording(cameraId); + + final errorEvent = + createJSInteropWrapper(FakeErrorEvent('type', 'message')) as ErrorEvent; + + videoRecordingErrorController.add(errorEvent); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${errorEvent.type}, error message: ${errorEvent.message}.', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on stopVideoRecording error', (WidgetTester tester) async { + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.stopVideoRecording()).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.stopVideoRecording(cameraId), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on pauseVideoRecording error', (WidgetTester tester) async { + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.pauseVideoRecording()).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.pauseVideoRecording(cameraId), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on resumeVideoRecording error', (WidgetTester tester) async { + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.resumeVideoRecording()).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.resumeVideoRecording(cameraId), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + }); + + testWidgets('onVideoRecordedEvent emits a VideoRecordedEvent', (WidgetTester tester) async { + final camera = MockCamera(); + final capturedVideo = XFile('/bogus/test'); + final stream = Stream.value( + VideoRecordedEvent(cameraId, capturedVideo, Duration.zero), + ); + when(camera.onVideoRecordedEvent).thenAnswer((_) => stream); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final streamQueue = StreamQueue( + CameraPlatform.instance.onVideoRecordedEvent(cameraId), + ); + + expect( + await streamQueue.next, + equals(VideoRecordedEvent(cameraId, capturedVideo, Duration.zero)), + ); + }); + }); + }); +} diff --git a/packages/camera/camera_web/example/integration_test/camera_web_orientation_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_orientation_test.dart new file mode 100644 index 00000000000..1272d691df8 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_web_orientation_test.dart @@ -0,0 +1,321 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: only_throw_errors + +import 'dart:async'; +import 'dart:js_interop'; + +import 'package:async/async.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/camera_web.dart'; +// ignore_for_file: implementation_imports +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:web/web.dart' hide MediaDeviceKind, OrientationType; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraPlugin', () { + const cameraId = 1; + + late MockWindow mockWindow; + late MockNavigator mockNavigator; + late MockMediaDevices mockMediaDevices; + + late Window window; + late Navigator navigator; + late MediaDevices mediaDevices; + + late HTMLVideoElement videoElement; + + late MockScreen mockScreen; + late MockScreenOrientation mockScreenOrientation; + + late Screen screen; + late ScreenOrientation screenOrientation; + + late MockDocument mockDocument; + late MockElement mockDocumentElement; + + late Document document; + late Element documentElement; + + late MockCameraService cameraService; + + setUp(() async { + mockWindow = MockWindow(); + mockNavigator = MockNavigator(); + mockMediaDevices = MockMediaDevices(); + + window = createJSInteropWrapper(mockWindow) as Window; + navigator = createJSInteropWrapper(mockNavigator) as Navigator; + mediaDevices = createJSInteropWrapper(mockMediaDevices) as MediaDevices; + + mockWindow.navigator = navigator; + mockNavigator.mediaDevices = mediaDevices; + + videoElement = getVideoElementWithBlankStream(const Size(10, 10)); + + mockScreen = MockScreen(); + mockScreenOrientation = MockScreenOrientation(); + + screen = createJSInteropWrapper(mockScreen) as Screen; + screenOrientation = createJSInteropWrapper(mockScreenOrientation) as ScreenOrientation; + + mockScreen.orientation = screenOrientation; + mockWindow.screen = screen; + + mockDocument = MockDocument(); + mockDocumentElement = MockElement(); + + document = createJSInteropWrapper(mockDocument) as Document; + documentElement = createJSInteropWrapper(mockDocumentElement) as Element; + + mockDocument.documentElement = documentElement; + mockWindow.document = document; + + cameraService = MockCameraService(); + + when( + cameraService.getMediaStreamForOptions(any, cameraId: anyNamed('cameraId')), + ).thenAnswer((_) async => videoElement.captureStream()); + + CameraPlatform.instance = CameraPlugin(cameraService: cameraService)..window = window; + }); + + group('lockCaptureOrientation', () { + setUp(() { + when( + cameraService.mapDeviceOrientationToOrientationType(any), + ).thenReturn(OrientationType.portraitPrimary); + }); + + testWidgets('requests full-screen mode ' + 'on documentElement', (WidgetTester tester) async { + var fullscreenCalls = 0; + mockDocumentElement.requestFullscreen = ([FullscreenOptions? options]) { + fullscreenCalls++; + return Future.value().toJS; + }.toJS; + + await CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitUp, + ); + + expect(fullscreenCalls, 1); + }); + + testWidgets('locks the capture orientation ' + 'based on the given device orientation', (WidgetTester tester) async { + when( + cameraService.mapDeviceOrientationToOrientationType(DeviceOrientation.landscapeRight), + ).thenReturn(OrientationType.landscapeSecondary); + + final capturedTypes = []; + mockScreenOrientation.lock = (OrientationLockType orientation) { + capturedTypes.add(orientation); + return Future.value().toJS; + }.toJS; + + await CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.landscapeRight, + ); + + verify( + cameraService.mapDeviceOrientationToOrientationType(DeviceOrientation.landscapeRight), + ).called(1); + + expect(capturedTypes.length, 1); + expect(capturedTypes[0], OrientationType.landscapeSecondary); + }); + + group('throws PlatformException', () { + testWidgets('with orientationNotSupported error ' + 'when documentElement is not available', (WidgetTester tester) async { + mockDocument.documentElement = null; + + expect( + () => CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitUp, + ), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + + mockDocument.documentElement = documentElement; + }); + + testWidgets('when lock throws DomException', (WidgetTester tester) async { + final exception = DOMException('NotAllowedError'); + + mockScreenOrientation.lock = (OrientationLockType orientation) { + throw exception; + // ignore: dead_code + return Future.value().toJS; + }.toJS; + + expect( + () => CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitDown, + ), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); + }); + + group('unlockCaptureOrientation', () { + setUp(() { + when( + cameraService.mapDeviceOrientationToOrientationType(any), + ).thenReturn(OrientationType.portraitPrimary); + }); + + testWidgets('unlocks the capture orientation', (WidgetTester tester) async { + var unlocks = 0; + mockScreenOrientation.unlock = () { + unlocks++; + }.toJS; + + await CameraPlatform.instance.unlockCaptureOrientation(cameraId); + + expect(unlocks, 1); + }); + + group('throws PlatformException', () { + testWidgets('with orientationNotSupported error ' + 'when documentElement is not available', (WidgetTester tester) async { + mockDocument.documentElement = null; + + expect( + () => CameraPlatform.instance.unlockCaptureOrientation(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + + mockDocument.documentElement = documentElement; + }); + + testWidgets('when unlock throws DomException', (WidgetTester tester) async { + final exception = DOMException('NotAllowedError'); + + mockScreenOrientation.unlock = () { + throw exception; + // ignore: dead_code + return Future.value().toJS; + }.toJS; + + expect( + () => CameraPlatform.instance.unlockCaptureOrientation(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); + }); + + group('onDeviceOrientationChanged', () { + final eventStreamController = StreamController(); + + setUp(() { + final provider = MockEventStreamProvider(); + (CameraPlatform.instance as CameraPlugin).orientationOnChangeProvider = provider; + when(provider.forTarget(any)).thenAnswer((_) => eventStreamController.stream); + }); + + testWidgets('emits the initial DeviceOrientationChangedEvent', (WidgetTester tester) async { + when( + cameraService.mapOrientationTypeToDeviceOrientation(OrientationType.portraitPrimary), + ).thenReturn(DeviceOrientation.portraitUp); + + // Set the initial screen orientation to portraitPrimary. + mockScreenOrientation.type = OrientationType.portraitPrimary; + + final Stream eventStream = CameraPlatform.instance + .onDeviceOrientationChanged(); + + final streamQueue = StreamQueue(eventStream); + + expect( + await streamQueue.next, + equals(const DeviceOrientationChangedEvent(DeviceOrientation.portraitUp)), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a DeviceOrientationChangedEvent ' + 'when the screen orientation is changed', (WidgetTester tester) async { + when( + cameraService.mapOrientationTypeToDeviceOrientation(OrientationType.landscapePrimary), + ).thenReturn(DeviceOrientation.landscapeLeft); + + when( + cameraService.mapOrientationTypeToDeviceOrientation(OrientationType.portraitSecondary), + ).thenReturn(DeviceOrientation.portraitDown); + + final Stream eventStream = CameraPlatform.instance + .onDeviceOrientationChanged(); + + final streamQueue = StreamQueue(eventStream); + + // Change the screen orientation to landscapePrimary and + // emit an event on the screenOrientation.onChange stream. + mockScreenOrientation.type = OrientationType.landscapePrimary; + + eventStreamController.add(Event('change')); + + expect( + await streamQueue.next, + equals(const DeviceOrientationChangedEvent(DeviceOrientation.landscapeLeft)), + ); + + // Change the screen orientation to portraitSecondary and + // emit an event on the screenOrientation.onChange stream. + mockScreenOrientation.type = OrientationType.portraitSecondary; + + eventStreamController.add(Event('change')); + + expect( + await streamQueue.next, + equals(const DeviceOrientationChangedEvent(DeviceOrientation.portraitDown)), + ); + + await streamQueue.cancel(); + }); + }); + }); +} diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart deleted file mode 100644 index 089b3926481..00000000000 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ /dev/null @@ -1,2677 +0,0 @@ -// Copyright 2013 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// ignore_for_file: only_throw_errors - -import 'dart:async'; -import 'dart:js_interop'; -import 'dart:math'; - -import 'package:async/async.dart'; -import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:camera_web/camera_web.dart'; -// ignore_for_file: implementation_imports -import 'package:camera_web/src/camera.dart'; -import 'package:camera_web/src/types/types.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart' as widgets; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:web/web.dart' hide MediaDeviceKind, OrientationType; - -import 'helpers/helpers.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('CameraPlugin', () { - const cameraId = 1; - - late MockWindow mockWindow; - late MockNavigator mockNavigator; - late MockMediaDevices mockMediaDevices; - - late Window window; - late Navigator navigator; - late MediaDevices mediaDevices; - - late HTMLVideoElement videoElement; - - late MockScreen mockScreen; - late MockScreenOrientation mockScreenOrientation; - - late Screen screen; - late ScreenOrientation screenOrientation; - - late MockDocument mockDocument; - late MockElement mockDocumentElement; - - late Document document; - late Element documentElement; - - late MockCameraService cameraService; - - setUp(() async { - mockWindow = MockWindow(); - mockNavigator = MockNavigator(); - mockMediaDevices = MockMediaDevices(); - - window = createJSInteropWrapper(mockWindow) as Window; - navigator = createJSInteropWrapper(mockNavigator) as Navigator; - mediaDevices = createJSInteropWrapper(mockMediaDevices) as MediaDevices; - - mockWindow.navigator = navigator; - mockNavigator.mediaDevices = mediaDevices; - - videoElement = getVideoElementWithBlankStream(const Size(10, 10)); - - mockScreen = MockScreen(); - mockScreenOrientation = MockScreenOrientation(); - - screen = createJSInteropWrapper(mockScreen) as Screen; - screenOrientation = createJSInteropWrapper(mockScreenOrientation) as ScreenOrientation; - - mockScreen.orientation = screenOrientation; - mockWindow.screen = screen; - - mockDocument = MockDocument(); - mockDocumentElement = MockElement(); - - document = createJSInteropWrapper(mockDocument) as Document; - documentElement = createJSInteropWrapper(mockDocumentElement) as Element; - - mockDocument.documentElement = documentElement; - mockWindow.document = document; - - cameraService = MockCameraService(); - - when( - cameraService.getMediaStreamForOptions(any, cameraId: anyNamed('cameraId')), - ).thenAnswer((_) async => videoElement.captureStream()); - - CameraPlatform.instance = CameraPlugin(cameraService: cameraService)..window = window; - }); - - testWidgets('CameraPlugin is the live instance', (WidgetTester tester) async { - expect(CameraPlatform.instance, isA()); - }); - - group('availableCameras', () { - setUp(() { - when(cameraService.getFacingModeForVideoTrack(any)).thenReturn(null); - - mockMediaDevices.enumerateDevices = () { - return Future>.value([].toJS).toJS; - }.toJS; - }); - - testWidgets('requests video permissions', (WidgetTester tester) async { - final List _ = await CameraPlatform.instance.availableCameras(); - - verify(cameraService.getMediaStreamForOptions(const CameraOptions())).called(1); - }); - - testWidgets('releases the camera stream ' - 'used to request video permissions', (WidgetTester tester) async { - final mockVideoTrack = MockMediaStreamTrack(); - final videoTrack = createJSInteropWrapper(mockVideoTrack) as MediaStreamTrack; - - var videoTrackStopped = false; - mockVideoTrack.stop = () { - videoTrackStopped = true; - }.toJS; - - when(cameraService.getMediaStreamForOptions(const CameraOptions())).thenAnswer( - (_) => Future.value( - createJSInteropWrapper(FakeMediaStream([videoTrack])) as MediaStream, - ), - ); - - final List _ = await CameraPlatform.instance.availableCameras(); - - expect(videoTrackStopped, isTrue); - }); - - testWidgets('gets a video stream ' - 'for a video input device', (WidgetTester tester) async { - final videoDevice = - createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) - as MediaDeviceInfo; - - mockMediaDevices.enumerateDevices = () { - return Future>.value([videoDevice].toJS).toJS; - }.toJS; - - final List _ = await CameraPlatform.instance.availableCameras(); - - verify( - cameraService.getMediaStreamForOptions( - CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), - ), - ).called(1); - }); - - testWidgets('does not get a video stream ' - 'for the video input device ' - 'with an empty device id', (WidgetTester tester) async { - final videoDevice = - createJSInteropWrapper(FakeMediaDeviceInfo('', 'Camera 1', MediaDeviceKind.videoInput)) - as MediaDeviceInfo; - - mockMediaDevices.enumerateDevices = () { - return Future>.value([videoDevice].toJS).toJS; - }.toJS; - - final List _ = await CameraPlatform.instance.availableCameras(); - - verifyNever( - cameraService.getMediaStreamForOptions( - CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), - ), - ); - }); - - testWidgets('gets the facing mode ' - 'from the first available video track ' - 'of the video input device', (WidgetTester tester) async { - final videoDevice = - createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) - as MediaDeviceInfo; - - final videoStream = - createJSInteropWrapper( - FakeMediaStream([ - createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, - createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, - ]), - ) - as MediaStream; - - when( - cameraService.getMediaStreamForOptions( - CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), - ), - ).thenAnswer((_) => Future.value(videoStream)); - - mockMediaDevices.enumerateDevices = () { - return Future>.value([videoDevice].toJS).toJS; - }.toJS; - - final List _ = await CameraPlatform.instance.availableCameras(); - - verify( - cameraService.getFacingModeForVideoTrack(videoStream.getVideoTracks().toDart.first), - ).called(1); - }); - - testWidgets('returns appropriate camera descriptions ' - 'for multiple video devices ' - 'based on video streams', (WidgetTester tester) async { - final firstVideoDevice = - createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) - as MediaDeviceInfo; - - final secondVideoDevice = - createJSInteropWrapper(FakeMediaDeviceInfo('4', 'Camera 4', MediaDeviceKind.videoInput)) - as MediaDeviceInfo; - - // Create a video stream for the first video device. - final firstVideoStream = - createJSInteropWrapper( - FakeMediaStream([ - createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, - createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, - ]), - ) - as MediaStream; - - // Create a video stream for the second video device. - final secondVideoStream = - createJSInteropWrapper( - FakeMediaStream([ - createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, - ]), - ) - as MediaStream; - - // Mock media devices to return two video input devices - // and two audio devices. - mockMediaDevices.enumerateDevices = () { - return Future>.value( - [ - firstVideoDevice, - createJSInteropWrapper( - FakeMediaDeviceInfo('2', 'Audio Input 2', MediaDeviceKind.audioInput), - ) - as MediaDeviceInfo, - createJSInteropWrapper( - FakeMediaDeviceInfo('3', 'Audio Output 3', MediaDeviceKind.audioOutput), - ) - as MediaDeviceInfo, - secondVideoDevice, - ].toJS, - ).toJS; - }.toJS; - - // Mock camera service to return the first video stream - // for the first video device. - when( - cameraService.getMediaStreamForOptions( - CameraOptions(video: VideoConstraints(deviceId: firstVideoDevice.deviceId)), - ), - ).thenAnswer((_) => Future.value(firstVideoStream)); - - // Mock camera service to return the second video stream - // for the second video device. - when( - cameraService.getMediaStreamForOptions( - CameraOptions(video: VideoConstraints(deviceId: secondVideoDevice.deviceId)), - ), - ).thenAnswer((_) => Future.value(secondVideoStream)); - - // Mock camera service to return a user facing mode - // for the first video stream. - when( - cameraService.getFacingModeForVideoTrack(firstVideoStream.getVideoTracks().toDart.first), - ).thenReturn('user'); - - when( - cameraService.mapFacingModeToLensDirection('user'), - ).thenReturn(CameraLensDirection.front); - - // Mock camera service to return an environment facing mode - // for the second video stream. - when( - cameraService.getFacingModeForVideoTrack(secondVideoStream.getVideoTracks().toDart.first), - ).thenReturn('environment'); - - when( - cameraService.mapFacingModeToLensDirection('environment'), - ).thenReturn(CameraLensDirection.back); - - final List cameras = await CameraPlatform.instance.availableCameras(); - - // Expect two cameras and ignore two audio devices. - expect( - cameras, - equals([ - CameraDescription( - name: firstVideoDevice.label, - lensDirection: CameraLensDirection.front, - sensorOrientation: 0, - ), - CameraDescription( - name: secondVideoDevice.label, - lensDirection: CameraLensDirection.back, - sensorOrientation: 0, - ), - ]), - ); - }); - - testWidgets('sets camera metadata ' - 'for the camera description', (WidgetTester tester) async { - final videoDevice = - createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) - as MediaDeviceInfo; - - final videoStream = - createJSInteropWrapper( - FakeMediaStream([ - createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, - createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, - ]), - ) - as MediaStream; - - mockMediaDevices.enumerateDevices = () { - return Future>.value([videoDevice].toJS).toJS; - }.toJS; - - when( - cameraService.getMediaStreamForOptions( - CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), - ), - ).thenAnswer((_) => Future.value(videoStream)); - - when( - cameraService.getFacingModeForVideoTrack(videoStream.getVideoTracks().toDart.first), - ).thenReturn('left'); - - when( - cameraService.mapFacingModeToLensDirection('left'), - ).thenReturn(CameraLensDirection.external); - - final CameraDescription camera = (await CameraPlatform.instance.availableCameras()).first; - - expect( - (CameraPlatform.instance as CameraPlugin).camerasMetadata, - equals({ - camera: CameraMetadata(deviceId: videoDevice.deviceId, facingMode: 'left'), - }), - ); - }); - - testWidgets('releases the video stream ' - 'of a video input device', (WidgetTester tester) async { - final videoDevice = - createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) - as MediaDeviceInfo; - - final tracks = []; - final stops = List.generate(2, (_) => false); - for (var i = 0; i < stops.length; i++) { - final track = MockMediaStreamTrack(); - track.stop = () { - stops[i] = true; - }.toJS; - tracks.add(createJSInteropWrapper(track) as MediaStreamTrack); - } - - final videoStream = createJSInteropWrapper(FakeMediaStream(tracks)) as MediaStream; - - mockMediaDevices.enumerateDevices = () { - return Future>.value([videoDevice].toJS).toJS; - }.toJS; - - when( - cameraService.getMediaStreamForOptions( - CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), - ), - ).thenAnswer((_) => Future.value(videoStream)); - - final List _ = await CameraPlatform.instance.availableCameras(); - - expect(stops.every((bool e) => e), isTrue); - }); - - group('throws CameraException', () { - testWidgets('when MediaDevices.enumerateDevices throws DomException', ( - WidgetTester tester, - ) async { - final exception = DOMException('UnknownError'); - - mockMediaDevices.enumerateDevices = () { - throw exception; - // ignore: dead_code - return Future>.value([].toJS).toJS; - }.toJS; - - expect( - () => CameraPlatform.instance.availableCameras(), - throwsA( - isA().having((CameraException e) => e.code, 'code', exception.name), - ), - ); - }); - - testWidgets('when CameraService.getMediaStreamForOptions ' - 'throws CameraWebException', (WidgetTester tester) async { - final exception = CameraWebException(cameraId, CameraErrorCode.security, 'description'); - - when(cameraService.getMediaStreamForOptions(any)).thenThrow(exception); - - expect( - CameraPlatform.instance.availableCameras(), - throwsA( - isA().having( - (CameraException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - - testWidgets('when CameraService.getMediaStreamForOptions ' - 'throws PlatformException', (WidgetTester tester) async { - final exception = PlatformException( - code: CameraErrorCode.notSupported.toString(), - message: 'message', - ); - - when(cameraService.getMediaStreamForOptions(any)).thenThrow(exception); - - expect( - () => CameraPlatform.instance.availableCameras(), - throwsA( - isA().having((CameraException e) => e.code, 'code', exception.code), - ), - ); - }); - }); - }); - - group('createCamera', () { - group('creates a camera', () { - const ultraHighResolutionSize = Size(3840, 2160); - const maxResolutionSize = Size(3840, 2160); - - const cameraDescription = CameraDescription( - name: 'name', - lensDirection: CameraLensDirection.front, - sensorOrientation: 0, - ); - - const cameraMetadata = CameraMetadata(deviceId: 'deviceId', facingMode: 'user'); - - setUp(() { - // Add metadata for the camera description. - (CameraPlatform.instance as CameraPlugin).camerasMetadata[cameraDescription] = - cameraMetadata; - - when(cameraService.mapFacingModeToCameraType('user')).thenReturn(CameraType.user); - }); - - testWidgets('with appropriate options', (WidgetTester tester) async { - when( - cameraService.mapResolutionPresetToSize(ResolutionPreset.ultraHigh), - ).thenReturn(ultraHighResolutionSize); - - final int cameraId = await CameraPlatform.instance.createCamera( - cameraDescription, - ResolutionPreset.ultraHigh, - enableAudio: true, - ); - - final Camera? camera = (CameraPlatform.instance as CameraPlugin).cameras[cameraId]; - - expect(camera, isA()); - expect(camera!.textureId, cameraId); - expect(camera.options.audio.enabled, isTrue); - expect(camera.options.video.facingMode, equals(FacingModeConstraint(CameraType.user))); - expect(camera.options.video.width!.ideal, ultraHighResolutionSize.width.toInt()); - expect(camera.options.video.height!.ideal, ultraHighResolutionSize.height.toInt()); - expect(camera.options.video.deviceId, cameraMetadata.deviceId); - }); - - testWidgets('with appropriate createCameraWithSettings options', ( - WidgetTester tester, - ) async { - when( - cameraService.mapResolutionPresetToSize(ResolutionPreset.ultraHigh), - ).thenReturn(ultraHighResolutionSize); - - final int cameraId = await CameraPlatform.instance.createCameraWithSettings( - cameraDescription, - const MediaSettings( - resolutionPreset: ResolutionPreset.ultraHigh, - videoBitrate: 200000, - audioBitrate: 32000, - enableAudio: true, - ), - ); - - final Camera? camera = (CameraPlatform.instance as CameraPlugin).cameras[cameraId]; - - expect(camera, isA()); - expect(camera!.textureId, cameraId); - expect(camera.options.audio.enabled, isTrue); - expect(camera.options.video.facingMode, equals(FacingModeConstraint(CameraType.user))); - expect(camera.options.video.width!.ideal, ultraHighResolutionSize.width.toInt()); - expect(camera.options.video.height!.ideal, ultraHighResolutionSize.height.toInt()); - expect(camera.options.video.deviceId, cameraMetadata.deviceId); - }); - - testWidgets('with a max resolution preset ' - 'and enabled audio set to false ' - 'when no options are specified', (WidgetTester tester) async { - when( - cameraService.mapResolutionPresetToSize(ResolutionPreset.max), - ).thenReturn(maxResolutionSize); - - final int cameraId = await CameraPlatform.instance.createCamera(cameraDescription, null); - - final Camera? camera = (CameraPlatform.instance as CameraPlugin).cameras[cameraId]; - - expect(camera, isA()); - expect(camera!.textureId, cameraId); - expect(camera.options.audio.enabled, isFalse); - expect(camera.options.video.facingMode, equals(FacingModeConstraint(CameraType.user))); - expect(camera.options.video.width!.ideal, maxResolutionSize.width.toInt()); - expect(camera.options.video.height!.ideal, maxResolutionSize.height.toInt()); - expect(camera.options.video.deviceId, cameraMetadata.deviceId); - }); - - testWidgets('with a max resolution preset ' - 'and enabled audio set to false ' - 'when no options are specified ' - 'using createCameraWithSettings', (WidgetTester tester) async { - when( - cameraService.mapResolutionPresetToSize(ResolutionPreset.max), - ).thenReturn(maxResolutionSize); - - final int cameraId = await CameraPlatform.instance.createCameraWithSettings( - cameraDescription, - const MediaSettings(resolutionPreset: ResolutionPreset.max), - ); - - final Camera? camera = (CameraPlatform.instance as CameraPlugin).cameras[cameraId]; - - expect(camera, isA()); - expect(camera!.options.audio.enabled, isFalse); - expect(camera.options.video.facingMode, equals(FacingModeConstraint(CameraType.user))); - expect(camera.options.video.width!.ideal, maxResolutionSize.width.toInt()); - expect(camera.options.video.height!.ideal, maxResolutionSize.height.toInt()); - expect(camera.options.video.deviceId, cameraMetadata.deviceId); - }); - }); - - testWidgets('throws CameraException ' - 'with missingMetadata error ' - 'if there is no metadata ' - 'for the given camera description', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.createCamera( - const CameraDescription( - name: 'name', - lensDirection: CameraLensDirection.back, - sensorOrientation: 0, - ), - ResolutionPreset.ultraHigh, - ), - throwsA( - isA().having( - (CameraException e) => e.code, - 'code', - CameraErrorCode.missingMetadata.toString(), - ), - ), - ); - }); - - testWidgets('throws CameraException ' - 'with missingMetadata error ' - 'if there is no metadata ' - 'for the given camera description ' - 'using createCameraWithSettings', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.createCameraWithSettings( - const CameraDescription( - name: 'name', - lensDirection: CameraLensDirection.back, - sensorOrientation: 0, - ), - const MediaSettings( - resolutionPreset: ResolutionPreset.low, - fps: 15, - videoBitrate: 200000, - audioBitrate: 32000, - enableAudio: true, - ), - ), - throwsA( - isA().having( - (CameraException e) => e.code, - 'code', - CameraErrorCode.missingMetadata.toString(), - ), - ), - ); - }); - }); - - group('initializeCamera', () { - late Camera camera; - late MockVideoElement mockVideoElement; - late HTMLVideoElement videoElement; - - late StreamController errorStreamController, abortStreamController; - late StreamController endedStreamController; - - setUp(() { - camera = MockCamera(); - mockVideoElement = MockVideoElement(); - videoElement = createJSInteropWrapper(mockVideoElement) as HTMLVideoElement; - - errorStreamController = StreamController(); - abortStreamController = StreamController(); - endedStreamController = StreamController(); - - when(camera.getVideoSize()).thenReturn(const Size(10, 10)); - when(camera.initialize()).thenAnswer((_) => Future.value()); - when(camera.play()).thenAnswer((_) => Future.value()); - - when(camera.videoElement).thenReturn(videoElement); - - final errorProvider = MockEventStreamProvider(); - final abortProvider = MockEventStreamProvider(); - - (CameraPlatform.instance as CameraPlugin).videoElementOnErrorProvider = errorProvider; - (CameraPlatform.instance as CameraPlugin).videoElementOnAbortProvider = abortProvider; - - when( - errorProvider.forElement(videoElement), - ).thenAnswer((_) => FakeElementStream(errorStreamController.stream)); - when( - abortProvider.forElement(videoElement), - ).thenAnswer((_) => FakeElementStream(abortStreamController.stream)); - - when(camera.onEnded).thenAnswer((_) => endedStreamController.stream); - }); - - testWidgets('initializes and plays the camera', (WidgetTester tester) async { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.initializeCamera(cameraId); - - verify(camera.initialize()).called(1); - verify(camera.play()).called(1); - }); - - testWidgets('starts listening to the camera video error and abort events', ( - WidgetTester tester, - ) async { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect(errorStreamController.hasListener, isFalse); - expect(abortStreamController.hasListener, isFalse); - - await CameraPlatform.instance.initializeCamera(cameraId); - - expect(errorStreamController.hasListener, isTrue); - expect(abortStreamController.hasListener, isTrue); - }); - - testWidgets('starts listening to the camera ended events', (WidgetTester tester) async { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect(endedStreamController.hasListener, isFalse); - - await CameraPlatform.instance.initializeCamera(cameraId); - - expect(endedStreamController.hasListener, isTrue); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.initializeCamera(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when camera throws CameraWebException', (WidgetTester tester) async { - final exception = CameraWebException( - cameraId, - CameraErrorCode.permissionDenied, - 'description', - ); - - when(camera.initialize()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.initializeCamera(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - - testWidgets('when camera throws DomException', (WidgetTester tester) async { - final exception = DOMException('NotAllowedError'); - - when(camera.initialize()).thenAnswer((_) => Future.value()); - when(camera.play()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.initializeCamera(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - }); - }); - - group('lockCaptureOrientation', () { - setUp(() { - when( - cameraService.mapDeviceOrientationToOrientationType(any), - ).thenReturn(OrientationType.portraitPrimary); - }); - - testWidgets('requests full-screen mode ' - 'on documentElement', (WidgetTester tester) async { - var fullscreenCalls = 0; - mockDocumentElement.requestFullscreen = ([FullscreenOptions? options]) { - fullscreenCalls++; - return Future.value().toJS; - }.toJS; - - await CameraPlatform.instance.lockCaptureOrientation( - cameraId, - DeviceOrientation.portraitUp, - ); - - expect(fullscreenCalls, 1); - }); - - testWidgets('locks the capture orientation ' - 'based on the given device orientation', (WidgetTester tester) async { - when( - cameraService.mapDeviceOrientationToOrientationType(DeviceOrientation.landscapeRight), - ).thenReturn(OrientationType.landscapeSecondary); - - final capturedTypes = []; - mockScreenOrientation.lock = (OrientationLockType orientation) { - capturedTypes.add(orientation); - return Future.value().toJS; - }.toJS; - - await CameraPlatform.instance.lockCaptureOrientation( - cameraId, - DeviceOrientation.landscapeRight, - ); - - verify( - cameraService.mapDeviceOrientationToOrientationType(DeviceOrientation.landscapeRight), - ).called(1); - - expect(capturedTypes.length, 1); - expect(capturedTypes[0], OrientationType.landscapeSecondary); - }); - - group('throws PlatformException', () { - testWidgets('with orientationNotSupported error ' - 'when documentElement is not available', (WidgetTester tester) async { - mockDocument.documentElement = null; - - expect( - () => CameraPlatform.instance.lockCaptureOrientation( - cameraId, - DeviceOrientation.portraitUp, - ), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.orientationNotSupported.toString(), - ), - ), - ); - - mockDocument.documentElement = documentElement; - }); - - testWidgets('when lock throws DomException', (WidgetTester tester) async { - final exception = DOMException('NotAllowedError'); - - mockScreenOrientation.lock = (OrientationLockType orientation) { - throw exception; - // ignore: dead_code - return Future.value().toJS; - }.toJS; - - expect( - () => CameraPlatform.instance.lockCaptureOrientation( - cameraId, - DeviceOrientation.portraitDown, - ), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - }); - }); - - group('unlockCaptureOrientation', () { - setUp(() { - when( - cameraService.mapDeviceOrientationToOrientationType(any), - ).thenReturn(OrientationType.portraitPrimary); - }); - - testWidgets('unlocks the capture orientation', (WidgetTester tester) async { - var unlocks = 0; - mockScreenOrientation.unlock = () { - unlocks++; - }.toJS; - - await CameraPlatform.instance.unlockCaptureOrientation(cameraId); - - expect(unlocks, 1); - }); - - group('throws PlatformException', () { - testWidgets('with orientationNotSupported error ' - 'when documentElement is not available', (WidgetTester tester) async { - mockDocument.documentElement = null; - - expect( - () => CameraPlatform.instance.unlockCaptureOrientation(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.orientationNotSupported.toString(), - ), - ), - ); - - mockDocument.documentElement = documentElement; - }); - - testWidgets('when unlock throws DomException', (WidgetTester tester) async { - final exception = DOMException('NotAllowedError'); - - mockScreenOrientation.unlock = () { - throw exception; - // ignore: dead_code - return Future.value().toJS; - }.toJS; - - expect( - () => CameraPlatform.instance.unlockCaptureOrientation(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - }); - }); - - group('takePicture', () { - testWidgets('captures a picture', (WidgetTester tester) async { - final camera = MockCamera(); - final capturedPicture = XFile('/bogus/test'); - - when(camera.takePicture()).thenAnswer((_) async => capturedPicture); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - final XFile picture = await CameraPlatform.instance.takePicture(cameraId); - - verify(camera.takePicture()).called(1); - - expect(picture, equals(capturedPicture)); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.takePicture(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when takePicture throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('NotSupportedError'); - - when(camera.takePicture()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.takePicture(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - - testWidgets('when takePicture throws CameraWebException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.takePicture()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.takePicture(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - group('startVideoRecording', () { - late Camera camera; - - setUp(() { - camera = MockCamera(); - - when(camera.startVideoRecording()).thenAnswer((_) async {}); - - when(camera.onVideoRecordingError).thenAnswer((_) => const Stream.empty()); - }); - - testWidgets('starts a video recording', (WidgetTester tester) async { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.startVideoRecording(cameraId); - - verify(camera.startVideoRecording()).called(1); - }); - - testWidgets('listens to the onVideoRecordingError stream', (WidgetTester tester) async { - final videoRecordingErrorController = StreamController(); - - when(camera.onVideoRecordingError).thenAnswer((_) => videoRecordingErrorController.stream); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.startVideoRecording(cameraId); - - expect(videoRecordingErrorController.hasListener, isTrue); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.startVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when startVideoRecording throws DomException', (WidgetTester tester) async { - final exception = DOMException('InvalidStateError'); - - when(camera.startVideoRecording()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.startVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - - testWidgets('when startVideoRecording throws CameraWebException', ( - WidgetTester tester, - ) async { - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.startVideoRecording()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.startVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - group('startVideoCapturing', () { - late Camera camera; - - setUp(() { - camera = MockCamera(); - - when(camera.startVideoRecording()).thenAnswer((_) async {}); - - when(camera.onVideoRecordingError).thenAnswer((_) => const Stream.empty()); - }); - - testWidgets('fails if trying to stream', (WidgetTester tester) async { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.startVideoCapturing( - VideoCaptureOptions(cameraId, streamCallback: (CameraImageData imageData) {}), - ), - throwsA(isA()), - ); - }); - }); - - group('stopVideoRecording', () { - testWidgets('stops a video recording', (WidgetTester tester) async { - final camera = MockCamera(); - final capturedVideo = XFile('/bogus/test'); - - when(camera.stopVideoRecording()).thenAnswer((_) async => capturedVideo); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - final XFile video = await CameraPlatform.instance.stopVideoRecording(cameraId); - - verify(camera.stopVideoRecording()).called(1); - - expect(video, capturedVideo); - }); - - testWidgets('stops listening to the onVideoRecordingError stream', ( - WidgetTester tester, - ) async { - final camera = MockCamera(); - final videoRecordingErrorController = StreamController(); - final capturedVideo = XFile('/bogus/test'); - - when(camera.startVideoRecording()).thenAnswer((_) async {}); - - when(camera.stopVideoRecording()).thenAnswer((_) async => capturedVideo); - - when(camera.onVideoRecordingError).thenAnswer((_) => videoRecordingErrorController.stream); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.startVideoRecording(cameraId); - final XFile _ = await CameraPlatform.instance.stopVideoRecording(cameraId); - - expect(videoRecordingErrorController.hasListener, isFalse); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.stopVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when stopVideoRecording throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('InvalidStateError'); - - when(camera.stopVideoRecording()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.stopVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - - testWidgets('when stopVideoRecording throws CameraWebException', ( - WidgetTester tester, - ) async { - final camera = MockCamera(); - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.stopVideoRecording()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.stopVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - group('pauseVideoRecording', () { - testWidgets('pauses a video recording', (WidgetTester tester) async { - final camera = MockCamera(); - - when(camera.pauseVideoRecording()).thenAnswer((_) async {}); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.pauseVideoRecording(cameraId); - - verify(camera.pauseVideoRecording()).called(1); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.pauseVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when pauseVideoRecording throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('InvalidStateError'); - - when(camera.pauseVideoRecording()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.pauseVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - - testWidgets('when pauseVideoRecording throws CameraWebException', ( - WidgetTester tester, - ) async { - final camera = MockCamera(); - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.pauseVideoRecording()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.pauseVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - group('resumeVideoRecording', () { - testWidgets('resumes a video recording', (WidgetTester tester) async { - final camera = MockCamera(); - - when(camera.resumeVideoRecording()).thenAnswer((_) async {}); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.resumeVideoRecording(cameraId); - - verify(camera.resumeVideoRecording()).called(1); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.resumeVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when resumeVideoRecording throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('InvalidStateError'); - - when(camera.resumeVideoRecording()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.resumeVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - - testWidgets('when resumeVideoRecording throws CameraWebException', ( - WidgetTester tester, - ) async { - final camera = MockCamera(); - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.resumeVideoRecording()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.resumeVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - group('setFlashMode', () { - testWidgets('calls setFlashMode on the camera', (WidgetTester tester) async { - final camera = MockCamera(); - const FlashMode flashMode = FlashMode.always; - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.setFlashMode(cameraId, flashMode); - - verify(camera.setFlashMode(flashMode)).called(1); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.setFlashMode(cameraId, FlashMode.always), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when setFlashMode throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('NotSupportedError'); - - when(camera.setFlashMode(any)).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.setFlashMode(cameraId, FlashMode.always), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - - testWidgets('when setFlashMode throws CameraWebException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.setFlashMode(any)).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.setFlashMode(cameraId, FlashMode.torch), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - testWidgets('setExposureMode throws UnimplementedError', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.setExposureMode(cameraId, ExposureMode.auto), - throwsUnimplementedError, - ); - }); - - testWidgets('setExposurePoint throws UnimplementedError', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.setExposurePoint(cameraId, const Point(0, 0)), - throwsUnimplementedError, - ); - }); - - testWidgets('getMinExposureOffset throws UnimplementedError', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.getMinExposureOffset(cameraId), - throwsUnimplementedError, - ); - }); - - testWidgets('getMaxExposureOffset throws UnimplementedError', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.getMaxExposureOffset(cameraId), - throwsUnimplementedError, - ); - }); - - testWidgets('getExposureOffsetStepSize throws UnimplementedError', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.getExposureOffsetStepSize(cameraId), - throwsUnimplementedError, - ); - }); - - testWidgets('setExposureOffset throws UnimplementedError', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.setExposureOffset(cameraId, 0), - throwsUnimplementedError, - ); - }); - - testWidgets('setFocusMode throws UnimplementedError', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.setFocusMode(cameraId, FocusMode.auto), - throwsUnimplementedError, - ); - }); - - testWidgets('setFocusPoint throws UnimplementedError', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.setFocusPoint(cameraId, const Point(0, 0)), - throwsUnimplementedError, - ); - }); - - group('getMaxZoomLevel', () { - testWidgets('calls getMaxZoomLevel on the camera', (WidgetTester tester) async { - final camera = MockCamera(); - const maximumZoomLevel = 100.0; - - when(camera.getMaxZoomLevel()).thenReturn(maximumZoomLevel); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect(await CameraPlatform.instance.getMaxZoomLevel(cameraId), equals(maximumZoomLevel)); - - verify(camera.getMaxZoomLevel()).called(1); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () async => CameraPlatform.instance.getMaxZoomLevel(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when getMaxZoomLevel throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('NotSupportedError'); - - when(camera.getMaxZoomLevel()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.getMaxZoomLevel(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - - testWidgets('when getMaxZoomLevel throws CameraWebException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.getMaxZoomLevel()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.getMaxZoomLevel(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - group('getMinZoomLevel', () { - testWidgets('calls getMinZoomLevel on the camera', (WidgetTester tester) async { - final camera = MockCamera(); - const minimumZoomLevel = 100.0; - - when(camera.getMinZoomLevel()).thenReturn(minimumZoomLevel); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect(await CameraPlatform.instance.getMinZoomLevel(cameraId), equals(minimumZoomLevel)); - - verify(camera.getMinZoomLevel()).called(1); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () async => CameraPlatform.instance.getMinZoomLevel(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when getMinZoomLevel throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('NotSupportedError'); - - when(camera.getMinZoomLevel()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.getMinZoomLevel(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - - testWidgets('when getMinZoomLevel throws CameraWebException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.getMinZoomLevel()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.getMinZoomLevel(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - group('setZoomLevel', () { - testWidgets('calls setZoomLevel on the camera', (WidgetTester tester) async { - final camera = MockCamera(); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - const zoom = 100.0; - - await CameraPlatform.instance.setZoomLevel(cameraId, zoom); - - verify(camera.setZoomLevel(zoom)).called(1); - }); - - group('throws CameraException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), - throwsA( - isA().having( - (CameraException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when setZoomLevel throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('NotSupportedError'); - - when(camera.setZoomLevel(any)).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), - throwsA( - isA().having((CameraException e) => e.code, 'code', exception.name), - ), - ); - }); - - testWidgets('when setZoomLevel throws PlatformException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = PlatformException( - code: CameraErrorCode.notSupported.toString(), - message: 'message', - ); - - when(camera.setZoomLevel(any)).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), - throwsA( - isA().having((CameraException e) => e.code, 'code', exception.code), - ), - ); - }); - - testWidgets('when setZoomLevel throws CameraWebException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.setZoomLevel(any)).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), - throwsA( - isA().having( - (CameraException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - group('pausePreview', () { - testWidgets('calls pause on the camera', (WidgetTester tester) async { - final camera = MockCamera(); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.pausePreview(cameraId); - - verify(camera.pause()).called(1); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () async => CameraPlatform.instance.pausePreview(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when pause throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('NotSupportedError'); - - when(camera.pause()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.pausePreview(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - }); - }); - - group('resumePreview', () { - testWidgets('calls play on the camera', (WidgetTester tester) async { - final camera = MockCamera(); - - when(camera.play()).thenAnswer((_) async {}); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.resumePreview(cameraId); - - verify(camera.play()).called(1); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () async => CameraPlatform.instance.resumePreview(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when play throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('NotSupportedError'); - - when(camera.play()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.resumePreview(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - - testWidgets('when play throws CameraWebException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = CameraWebException(cameraId, CameraErrorCode.unknown, 'description'); - - when(camera.play()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.resumePreview(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - testWidgets('buildPreview returns an HtmlElementView ' - 'with an appropriate view type', (WidgetTester tester) async { - final camera = Camera(textureId: cameraId, cameraService: cameraService); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - CameraPlatform.instance.buildPreview(cameraId), - isA().having( - (widgets.HtmlElementView view) => view.viewType, - 'viewType', - camera.getViewType(), - ), - ); - }); - - group('dispose', () { - late Camera camera; - late MockVideoElement mockVideoElement; - late HTMLVideoElement videoElement; - - late StreamController errorStreamController, abortStreamController; - late StreamController endedStreamController; - late StreamController videoRecordingErrorController; - - setUp(() { - camera = MockCamera(); - mockVideoElement = MockVideoElement(); - videoElement = createJSInteropWrapper(mockVideoElement) as HTMLVideoElement; - - errorStreamController = StreamController(); - abortStreamController = StreamController(); - endedStreamController = StreamController(); - videoRecordingErrorController = StreamController(); - - when(camera.getVideoSize()).thenReturn(const Size(10, 10)); - when(camera.initialize()).thenAnswer((_) => Future.value()); - when(camera.play()).thenAnswer((_) => Future.value()); - when(camera.dispose()).thenAnswer((_) => Future.value()); - - when(camera.videoElement).thenReturn(videoElement); - - final errorProvider = MockEventStreamProvider(); - final abortProvider = MockEventStreamProvider(); - - (CameraPlatform.instance as CameraPlugin).videoElementOnErrorProvider = errorProvider; - (CameraPlatform.instance as CameraPlugin).videoElementOnAbortProvider = abortProvider; - - when( - errorProvider.forElement(videoElement), - ).thenAnswer((_) => FakeElementStream(errorStreamController.stream)); - when( - abortProvider.forElement(videoElement), - ).thenAnswer((_) => FakeElementStream(abortStreamController.stream)); - - when(camera.onEnded).thenAnswer((_) => endedStreamController.stream); - - when(camera.onVideoRecordingError).thenAnswer((_) => videoRecordingErrorController.stream); - - when(camera.startVideoRecording()).thenAnswer((_) async {}); - }); - - testWidgets('disposes the correct camera', (WidgetTester tester) async { - const firstCameraId = 0; - const secondCameraId = 1; - - final firstCamera = MockCamera(); - final secondCamera = MockCamera(); - - when(firstCamera.dispose()).thenAnswer((_) => Future.value()); - when(secondCamera.dispose()).thenAnswer((_) => Future.value()); - - // Save cameras in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras.addAll({ - firstCameraId: firstCamera, - secondCameraId: secondCamera, - }); - - // Dispose the first camera. - await CameraPlatform.instance.dispose(firstCameraId); - - // The first camera should be disposed. - verify(firstCamera.dispose()).called(1); - verifyNever(secondCamera.dispose()); - - // The first camera should be removed from the camera plugin. - expect( - (CameraPlatform.instance as CameraPlugin).cameras, - equals({secondCameraId: secondCamera}), - ); - }); - - testWidgets('cancels the camera video error and abort subscriptions', ( - WidgetTester tester, - ) async { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.initializeCamera(cameraId); - await CameraPlatform.instance.dispose(cameraId); - - expect(errorStreamController.hasListener, isFalse); - expect(abortStreamController.hasListener, isFalse); - }); - - testWidgets('cancels the camera ended subscriptions', (WidgetTester tester) async { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.initializeCamera(cameraId); - await CameraPlatform.instance.dispose(cameraId); - - expect(endedStreamController.hasListener, isFalse); - }); - - testWidgets('cancels the camera video recording error subscriptions', ( - WidgetTester tester, - ) async { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.initializeCamera(cameraId); - await CameraPlatform.instance.startVideoRecording(cameraId); - await CameraPlatform.instance.dispose(cameraId); - - expect(videoRecordingErrorController.hasListener, isFalse); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.dispose(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when dispose throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('InvalidAccessError'); - - when(camera.dispose()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.dispose(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - }); - }); - - group('getCamera', () { - testWidgets('returns the correct camera', (WidgetTester tester) async { - final camera = Camera(textureId: cameraId, cameraService: cameraService); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect((CameraPlatform.instance as CameraPlugin).getCamera(cameraId), equals(camera)); - }); - - testWidgets('throws PlatformException ' - 'with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () => (CameraPlatform.instance as CameraPlugin).getCamera(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - }); - - group('events', () { - late MockCamera camera; - late MockVideoElement mockVideoElement; - late HTMLVideoElement videoElement; - - late StreamController errorStreamController, abortStreamController; - late StreamController endedStreamController; - late StreamController videoRecordingErrorController; - - setUp(() { - camera = MockCamera(); - mockVideoElement = MockVideoElement(); - videoElement = createJSInteropWrapper(mockVideoElement) as HTMLVideoElement; - - errorStreamController = StreamController(); - abortStreamController = StreamController(); - endedStreamController = StreamController(); - videoRecordingErrorController = StreamController(); - - when(camera.getVideoSize()).thenReturn(const Size(10, 10)); - when(camera.initialize()).thenAnswer((_) => Future.value()); - when(camera.play()).thenAnswer((_) => Future.value()); - - when(camera.videoElement).thenReturn(videoElement); - - final errorProvider = MockEventStreamProvider(); - final abortProvider = MockEventStreamProvider(); - - (CameraPlatform.instance as CameraPlugin).videoElementOnErrorProvider = errorProvider; - (CameraPlatform.instance as CameraPlugin).videoElementOnAbortProvider = abortProvider; - - when( - errorProvider.forElement(any), - ).thenAnswer((_) => FakeElementStream(errorStreamController.stream)); - when( - abortProvider.forElement(any), - ).thenAnswer((_) => FakeElementStream(abortStreamController.stream)); - - when(camera.onEnded).thenAnswer((_) => endedStreamController.stream); - - when(camera.onVideoRecordingError).thenAnswer((_) => videoRecordingErrorController.stream); - - when(camera.startVideoRecording()).thenAnswer((_) async {}); - }); - - testWidgets('onCameraInitialized emits a CameraInitializedEvent ' - 'on initializeCamera', (WidgetTester tester) async { - // Mock the camera to use a blank video stream of size 1280x720. - const videoSize = Size(1280, 720); - - videoElement = getVideoElementWithBlankStream(videoSize); - - when( - cameraService.getMediaStreamForOptions(any, cameraId: cameraId), - ).thenAnswer((_) async => videoElement.captureStream()); - - final camera = Camera(textureId: cameraId, cameraService: cameraService); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - final Stream eventStream = CameraPlatform.instance - .onCameraInitialized(cameraId); - - final streamQueue = StreamQueue(eventStream); - - await CameraPlatform.instance.initializeCamera(cameraId); - - expect( - await streamQueue.next, - equals( - CameraInitializedEvent( - cameraId, - videoSize.width, - videoSize.height, - ExposureMode.auto, - false, - FocusMode.auto, - false, - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('onCameraResolutionChanged emits an empty stream', (WidgetTester tester) async { - final Stream stream = CameraPlatform.instance - .onCameraResolutionChanged(cameraId); - expect(await stream.isEmpty, isTrue); - }); - - testWidgets('onCameraClosing emits a CameraClosingEvent ' - 'on the camera ended event', (WidgetTester tester) async { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - final Stream eventStream = CameraPlatform.instance.onCameraClosing( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - await CameraPlatform.instance.initializeCamera(cameraId); - - endedStreamController.add( - createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, - ); - - expect(await streamQueue.next, equals(const CameraClosingEvent(cameraId))); - - await streamQueue.cancel(); - }); - - group('onCameraError', () { - setUp(() { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - }); - - testWidgets('emits a CameraErrorEvent ' - 'on the camera video error event ' - 'with a message', (WidgetTester tester) async { - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - await CameraPlatform.instance.initializeCamera(cameraId); - - final error = - createJSInteropWrapper( - FakeMediaError(MediaError.MEDIA_ERR_NETWORK, 'A network error occurred.'), - ) - as MediaError; - - final CameraErrorCode errorCode = CameraErrorCode.fromMediaError(error); - - mockVideoElement.error = error; - errorStreamController.add(Event('error')); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent(cameraId, 'Error code: $errorCode, error message: ${error.message}'), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on the camera video error event ' - 'with no message', (WidgetTester tester) async { - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - await CameraPlatform.instance.initializeCamera(cameraId); - - final error = - createJSInteropWrapper(FakeMediaError(MediaError.MEDIA_ERR_NETWORK)) as MediaError; - final CameraErrorCode errorCode = CameraErrorCode.fromMediaError(error); - - mockVideoElement.error = error; - errorStreamController.add(Event('error')); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: $errorCode, error message: No further diagnostic information can be determined or provided.', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on the camera video abort event', (WidgetTester tester) async { - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - await CameraPlatform.instance.initializeCamera(cameraId); - - abortStreamController.add(Event('abort')); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - "Error code: ${CameraErrorCode.abort}, error message: The video element's source has not fully loaded.", - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on takePicture error', (WidgetTester tester) async { - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.takePicture()).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.takePicture(cameraId), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on setFlashMode error', (WidgetTester tester) async { - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.setFlashMode(any)).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.setFlashMode(cameraId, FlashMode.always), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on getMaxZoomLevel error', (WidgetTester tester) async { - final exception = CameraWebException( - cameraId, - CameraErrorCode.zoomLevelNotSupported, - 'description', - ); - - when(camera.getMaxZoomLevel()).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.getMaxZoomLevel(cameraId), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on getMinZoomLevel error', (WidgetTester tester) async { - final exception = CameraWebException( - cameraId, - CameraErrorCode.zoomLevelNotSupported, - 'description', - ); - - when(camera.getMinZoomLevel()).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.getMinZoomLevel(cameraId), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on setZoomLevel error', (WidgetTester tester) async { - final exception = CameraWebException( - cameraId, - CameraErrorCode.zoomLevelNotSupported, - 'description', - ); - - when(camera.setZoomLevel(any)).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on resumePreview error', (WidgetTester tester) async { - final exception = CameraWebException(cameraId, CameraErrorCode.unknown, 'description'); - - when(camera.play()).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.resumePreview(cameraId), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on startVideoRecording error', (WidgetTester tester) async { - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.onVideoRecordingError).thenAnswer((_) => const Stream.empty()); - - when(camera.startVideoRecording()).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.startVideoRecording(cameraId), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on the camera video recording error event', (WidgetTester tester) async { - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - await CameraPlatform.instance.initializeCamera(cameraId); - await CameraPlatform.instance.startVideoRecording(cameraId); - - final errorEvent = - createJSInteropWrapper(FakeErrorEvent('type', 'message')) as ErrorEvent; - - videoRecordingErrorController.add(errorEvent); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${errorEvent.type}, error message: ${errorEvent.message}.', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on stopVideoRecording error', (WidgetTester tester) async { - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.stopVideoRecording()).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.stopVideoRecording(cameraId), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on pauseVideoRecording error', (WidgetTester tester) async { - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.pauseVideoRecording()).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.pauseVideoRecording(cameraId), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on resumeVideoRecording error', (WidgetTester tester) async { - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.resumeVideoRecording()).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.resumeVideoRecording(cameraId), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - }); - - testWidgets('onVideoRecordedEvent emits a VideoRecordedEvent', (WidgetTester tester) async { - final camera = MockCamera(); - final capturedVideo = XFile('/bogus/test'); - final stream = Stream.value( - VideoRecordedEvent(cameraId, capturedVideo, Duration.zero), - ); - when(camera.onVideoRecordedEvent).thenAnswer((_) => stream); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - final streamQueue = StreamQueue( - CameraPlatform.instance.onVideoRecordedEvent(cameraId), - ); - - expect( - await streamQueue.next, - equals(VideoRecordedEvent(cameraId, capturedVideo, Duration.zero)), - ); - }); - - group('onDeviceOrientationChanged', () { - final eventStreamController = StreamController(); - - setUp(() { - final provider = MockEventStreamProvider(); - (CameraPlatform.instance as CameraPlugin).orientationOnChangeProvider = provider; - when(provider.forTarget(any)).thenAnswer((_) => eventStreamController.stream); - }); - - testWidgets('emits the initial DeviceOrientationChangedEvent', (WidgetTester tester) async { - when( - cameraService.mapOrientationTypeToDeviceOrientation(OrientationType.portraitPrimary), - ).thenReturn(DeviceOrientation.portraitUp); - - // Set the initial screen orientation to portraitPrimary. - mockScreenOrientation.type = OrientationType.portraitPrimary; - - final Stream eventStream = CameraPlatform.instance - .onDeviceOrientationChanged(); - - final streamQueue = StreamQueue(eventStream); - - expect( - await streamQueue.next, - equals(const DeviceOrientationChangedEvent(DeviceOrientation.portraitUp)), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a DeviceOrientationChangedEvent ' - 'when the screen orientation is changed', (WidgetTester tester) async { - when( - cameraService.mapOrientationTypeToDeviceOrientation(OrientationType.landscapePrimary), - ).thenReturn(DeviceOrientation.landscapeLeft); - - when( - cameraService.mapOrientationTypeToDeviceOrientation(OrientationType.portraitSecondary), - ).thenReturn(DeviceOrientation.portraitDown); - - final Stream eventStream = CameraPlatform.instance - .onDeviceOrientationChanged(); - - final streamQueue = StreamQueue(eventStream); - - // Change the screen orientation to landscapePrimary and - // emit an event on the screenOrientation.onChange stream. - mockScreenOrientation.type = OrientationType.landscapePrimary; - - eventStreamController.add(Event('change')); - - expect( - await streamQueue.next, - equals(const DeviceOrientationChangedEvent(DeviceOrientation.landscapeLeft)), - ); - - // Change the screen orientation to portraitSecondary and - // emit an event on the screenOrientation.onChange stream. - mockScreenOrientation.type = OrientationType.portraitSecondary; - - eventStreamController.add(Event('change')); - - expect( - await streamQueue.next, - equals(const DeviceOrientationChangedEvent(DeviceOrientation.portraitDown)), - ); - - await streamQueue.cancel(); - }); - }); - }); - }); -} diff --git a/packages/camera/camera_web/example/test_driver/integration_test.dart b/packages/camera/camera_web/example/test_driver/integration_test.dart index b4547b1470a..fb3dec00bee 100644 --- a/packages/camera/camera_web/example/test_driver/integration_test.dart +++ b/packages/camera/camera_web/example/test_driver/integration_test.dart @@ -4,4 +4,4 @@ import 'package:integration_test/integration_test_driver.dart'; -Future main() => integrationDriver(timeout: const Duration(minutes: 30)); +Future main() => integrationDriver(); diff --git a/script/configs/exclude_integration_web.yaml b/script/configs/exclude_integration_web.yaml index dd575ed478a..8dbdd61b8d4 100644 --- a/script/configs/exclude_integration_web.yaml +++ b/script/configs/exclude_integration_web.yaml @@ -2,6 +2,3 @@ - file_selector # Waiting on https://github.com/flutter/flutter/issues/145149 - google_maps_flutter/google_maps_flutter -# Moved to its own dedicated shard due to long test duration -- camera/camera_web - From b48fa5fd403cb70130cd66b08b0a613afa8910e2 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Tue, 18 Aug 2026 17:46:05 -0400 Subject: [PATCH 18/18] Add GitHub issue link to Chrome 125 TODO comments in .ci.yaml --- .ci.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.ci.yaml b/.ci.yaml index 0465b3019f9..f24589b43c5 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -220,7 +220,7 @@ targets: # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and # flutter/flutter#190584) which pass --disable-search-engine-choice-screen - # and --disable-background-timer-throttling. + # and --disable-background-timer-throttling; see https://github.com/flutter/flutter/issues/191299 dependencies: >- [ {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} @@ -237,7 +237,7 @@ targets: # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and # flutter/flutter#190584) which pass --disable-search-engine-choice-screen - # and --disable-background-timer-throttling. + # and --disable-background-timer-throttling; see https://github.com/flutter/flutter/issues/191299 dependencies: >- [ {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} @@ -254,7 +254,7 @@ targets: # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and # flutter/flutter#190584) which pass --disable-search-engine-choice-screen - # and --disable-background-timer-throttling. + # and --disable-background-timer-throttling; see https://github.com/flutter/flutter/issues/191299 dependencies: >- [ {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} @@ -361,7 +361,7 @@ targets: # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and # flutter/flutter#190584) which pass --disable-search-engine-choice-screen - # and --disable-background-timer-throttling. + # and --disable-background-timer-throttling; see https://github.com/flutter/flutter/issues/191299 dependencies: >- [ {"dependency": "android_virtual_device", "version": "android_36_google_apis_x64.textpb"}, @@ -753,7 +753,7 @@ targets: # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and # flutter/flutter#190584) which pass --disable-search-engine-choice-screen - # and --disable-background-timer-throttling. + # and --disable-background-timer-throttling; see https://github.com/flutter/flutter/issues/191299 dependencies: >- [ {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} @@ -770,7 +770,7 @@ targets: # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and # flutter/flutter#190584) which pass --disable-search-engine-choice-screen - # and --disable-background-timer-throttling. + # and --disable-background-timer-throttling; see https://github.com/flutter/flutter/issues/191299 dependencies: >- [ {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} @@ -787,7 +787,7 @@ targets: # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and # flutter/flutter#190584) which pass --disable-search-engine-choice-screen - # and --disable-background-timer-throttling. + # and --disable-background-timer-throttling; see https://github.com/flutter/flutter/issues/191299 dependencies: >- [ {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} @@ -836,7 +836,7 @@ targets: # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and # flutter/flutter#190584) which pass --disable-search-engine-choice-screen - # and --disable-background-timer-throttling. + # and --disable-background-timer-throttling; see https://github.com/flutter/flutter/issues/191299 dependencies: >- [ {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"}