diff --git a/shell/platform/windows/fixtures/main.dart b/shell/platform/windows/fixtures/main.dart index 3e1c25eb6da0e..bf59e664496c7 100644 --- a/shell/platform/windows/fixtures/main.dart +++ b/shell/platform/windows/fixtures/main.dart @@ -43,8 +43,22 @@ void hiPlatformChannels() { }); } +/// Returns a future that completes when +/// `PlatformDispatcher.instance.onSemanticsEnabledChanged` fires. +Future get semanticsChanged { + final Completer semanticsChanged = Completer(); + ui.PlatformDispatcher.instance.onSemanticsEnabledChanged = + semanticsChanged.complete; + return semanticsChanged.future; +} + @pragma('vm:entry-point') -void alertPlatformChannel() async { +void sendAccessiblityAnnouncement() async { + // Wait until semantics are enabled. + if (!ui.PlatformDispatcher.instance.semanticsEnabled) { + await semanticsChanged; + } + // Serializers for data types are in the framework, so this will be hardcoded. const int valueMap = 13, valueString = 7; // Corresponds to: @@ -78,13 +92,9 @@ void alertPlatformChannel() async { ]); final ByteData byteData = data.buffer.asByteData(); - final Completer enabled = Completer(); - ui.PlatformDispatcher.instance.sendPlatformMessage('semantics', ByteData(0), (ByteData? reply){ - enabled.complete(reply); + ui.PlatformDispatcher.instance.sendPlatformMessage('flutter/accessibility', byteData, (ByteData? _){ + signal(); }); - await enabled.future; - - ui.PlatformDispatcher.instance.sendPlatformMessage('flutter/accessibility', byteData, (ByteData? _){}); } @pragma('vm:entry-point') diff --git a/shell/platform/windows/flutter_windows_engine.cc b/shell/platform/windows/flutter_windows_engine.cc index 7ccb66037874b..fac1d85d6a146 100644 --- a/shell/platform/windows/flutter_windows_engine.cc +++ b/shell/platform/windows/flutter_windows_engine.cc @@ -493,7 +493,7 @@ std::unique_ptr FlutterWindowsEngine::CreateView( auto view = std::make_unique( kImplicitViewId, this, std::move(window), windows_proc_table_); - view_ = view.get(); + views_[kImplicitViewId] = view.get(); InitializeKeyboard(); return std::move(view); @@ -528,9 +528,12 @@ std::chrono::nanoseconds FlutterWindowsEngine::FrameInterval() { } FlutterWindowsView* FlutterWindowsEngine::view(FlutterViewId view_id) const { - FML_DCHECK(view_id == kImplicitViewId); + auto iterator = views_.find(view_id); + if (iterator == views_.end()) { + return nullptr; + } - return view_; + return iterator->second; } // Returns the currently configured Plugin Registrar. @@ -669,7 +672,7 @@ void FlutterWindowsEngine::SendSystemLocales() { } void FlutterWindowsEngine::InitializeKeyboard() { - if (view_ == nullptr) { + if (views_.empty()) { FML_LOG(ERROR) << "Cannot initialize keyboard on Windows headless mode."; } @@ -756,16 +759,24 @@ bool FlutterWindowsEngine::DispatchSemanticsAction( } void FlutterWindowsEngine::UpdateSemanticsEnabled(bool enabled) { - if (engine_ && semantics_enabled_ != enabled) { - semantics_enabled_ = enabled; - embedder_api_.UpdateSemanticsEnabled(engine_, enabled); - view_->UpdateSemanticsEnabled(enabled); + if (!engine_ || semantics_enabled_ == enabled) { + return; + } + + semantics_enabled_ = enabled; + embedder_api_.UpdateSemanticsEnabled(engine_, enabled); + + for (auto iterator = views_.begin(); iterator != views_.end(); iterator++) { + iterator->second->UpdateSemanticsEnabled(enabled); } } void FlutterWindowsEngine::OnPreEngineRestart() { // Reset the keyboard's state on hot restart. - if (view_) { + // TODO(loicsharma): Always reset the keyboard once + // it no longer depends on the implicit view. + // https://github.com/flutter/flutter/issues/115611 + if (!views_.empty()) { InitializeKeyboard(); } } @@ -821,7 +832,13 @@ void FlutterWindowsEngine::HandleAccessibilityMessage( std::string text = std::get(data_map.at(EncodableValue("message"))); std::wstring wide_text = fml::Utf8ToWideString(text); - view_->AnnounceAlert(wide_text); + + // TODO(loicsharma): Remove implicit view assumption. + // https://github.com/flutter/flutter/issues/142845 + auto view = this->view(kImplicitViewId); + if (view) { + view->AnnounceAlert(wide_text); + } } } SendPlatformMessageResponse(message->response_handle, @@ -843,7 +860,9 @@ void FlutterWindowsEngine::OnQuit(std::optional hwnd, } void FlutterWindowsEngine::OnDwmCompositionChanged() { - view_->OnDwmCompositionChanged(); + for (auto iterator = views_.begin(); iterator != views_.end(); iterator++) { + iterator->second->OnDwmCompositionChanged(); + } } void FlutterWindowsEngine::OnWindowStateEvent(HWND hwnd, diff --git a/shell/platform/windows/flutter_windows_engine.h b/shell/platform/windows/flutter_windows_engine.h index 53710b141e839..d8966db010e55 100644 --- a/shell/platform/windows/flutter_windows_engine.h +++ b/shell/platform/windows/flutter_windows_engine.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include "flutter/fml/closure.h" @@ -117,12 +118,13 @@ class FlutterWindowsEngine { // Returns false if stopping the engine fails, or if it was not running. virtual bool Stop(); - // Create the view that is displaying this engine's content. + // Create a view that can display this engine's content. std::unique_ptr CreateView( std::unique_ptr window); - // The view displaying this engine's content, if any. This will be null for - // headless engines. + // Get a view that displays this engine's content. + // + // Returns null if the view does not exist. FlutterWindowsView* view(FlutterViewId view_id) const; // Returns the currently configured Plugin Registrar. @@ -348,8 +350,8 @@ class FlutterWindowsEngine { // AOT data, if any. UniqueAotDataPtr aot_data_; - // The view displaying the content running in this engine, if any. - FlutterWindowsView* view_ = nullptr; + // The views displaying the content running in this engine, if any. + std::unordered_map views_; // Task runner for tasks posted from the engine. std::unique_ptr task_runner_; diff --git a/shell/platform/windows/flutter_windows_engine_unittests.cc b/shell/platform/windows/flutter_windows_engine_unittests.cc index d72a2e4e3dc46..ce1cdc51ffbbc 100644 --- a/shell/platform/windows/flutter_windows_engine_unittests.cc +++ b/shell/platform/windows/flutter_windows_engine_unittests.cc @@ -16,6 +16,7 @@ #include "flutter/shell/platform/windows/testing/mock_windows_proc_table.h" #include "flutter/shell/platform/windows/testing/test_keyboard.h" #include "flutter/shell/platform/windows/testing/windows_test.h" +#include "flutter/shell/platform/windows/testing/windows_test_config_builder.h" #include "flutter/third_party/accessibility/ax/platform/ax_platform_node_win.h" #include "fml/synchronization/waitable_event.h" #include "gmock/gmock.h" @@ -27,10 +28,23 @@ namespace flutter { namespace testing { +using ::testing::NiceMock; using ::testing::Return; class FlutterWindowsEngineTest : public WindowsTest {}; +TEST_F(FlutterWindowsEngineTest, RunHeadless) { + FlutterWindowsEngineBuilder builder{GetContext()}; + std::unique_ptr engine = builder.Build(); + + EngineModifier modifier(engine.get()); + modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; }; + + ASSERT_TRUE(engine->Run()); + ASSERT_EQ(engine->view(kImplicitViewId), nullptr); + ASSERT_EQ(engine->view(123), nullptr); +} + TEST_F(FlutterWindowsEngineTest, RunDoesExpectedInitialization) { FlutterWindowsEngineBuilder builder{GetContext()}; builder.AddDartEntrypointArgument("arg1"); @@ -636,44 +650,64 @@ class MockFlutterWindowsView : public FlutterWindowsView { FML_DISALLOW_COPY_AND_ASSIGN(MockFlutterWindowsView); }; -TEST_F(FlutterWindowsEngineTest, AlertPlatformMessage) { - FlutterWindowsEngineBuilder builder{GetContext()}; - builder.SetDartEntrypoint("alertPlatformChannel"); +// Verify the view is notified of accessibility announcements. +TEST_F(FlutterWindowsEngineTest, AccessibilityAnnouncement) { + auto& context = GetContext(); + WindowsConfigBuilder builder{context}; + builder.SetDartEntrypoint("sendAccessiblityAnnouncement"); + + bool done = false; + auto native_entry = + CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { done = true; }); + context.AddNativeFunction("Signal", native_entry); + + EnginePtr engine{builder.RunHeadless()}; + ASSERT_NE(engine, nullptr); - auto engine = builder.Build(); - auto window_binding_handler = - std::make_unique<::testing::NiceMock>(); ui::AXPlatformNodeDelegateBase parent_delegate; - AlertPlatformNodeDelegate delegate(parent_delegate); + AlertPlatformNodeDelegate delegate{parent_delegate}; + + auto window_binding_handler = + std::make_unique>(); EXPECT_CALL(*window_binding_handler, GetAlertDelegate) - .WillRepeatedly(Return(&delegate)); - MockFlutterWindowsView view(engine.get(), std::move(window_binding_handler)); + .WillOnce(Return(&delegate)); - EngineModifier modifier(engine.get()); + auto windows_engine = reinterpret_cast(engine.get()); + MockFlutterWindowsView view{windows_engine, + std::move(window_binding_handler)}; + EngineModifier modifier{windows_engine}; modifier.SetImplicitView(&view); - modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; }; - auto binary_messenger = - std::make_unique(engine->messenger()); - binary_messenger->SetMessageHandler( - "semantics", [&engine](const uint8_t* message, size_t message_size, - BinaryReply reply) { - engine->UpdateSemanticsEnabled(true); - char response[] = ""; - reply(reinterpret_cast(response), 0); - }); + windows_engine->UpdateSemanticsEnabled(true); - bool did_call = false; - EXPECT_CALL(view, NotifyWinEventWrapper) - .WillOnce([&did_call](ui::AXPlatformNodeWin* node, - ax::mojom::Event event) { did_call = true; }); + EXPECT_CALL(view, NotifyWinEventWrapper).Times(1); - engine->UpdateSemanticsEnabled(true); - engine->Run(); + // Rely on timeout mechanism in CI. + while (!done) { + windows_engine->task_runner()->ProcessTasks(); + } +} + +// Verify the app can send accessibility announcements while in headless mode. +TEST_F(FlutterWindowsEngineTest, AccessibilityAnnouncementHeadless) { + auto& context = GetContext(); + WindowsConfigBuilder builder{context}; + builder.SetDartEntrypoint("sendAccessiblityAnnouncement"); + + bool done = false; + auto native_entry = + CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { done = true; }); + context.AddNativeFunction("Signal", native_entry); + + EnginePtr engine{builder.RunHeadless()}; + ASSERT_NE(engine, nullptr); + + auto windows_engine = reinterpret_cast(engine.get()); + windows_engine->UpdateSemanticsEnabled(true); // Rely on timeout mechanism in CI. - while (!did_call) { - engine->task_runner()->ProcessTasks(); + while (!done) { + windows_engine->task_runner()->ProcessTasks(); } } diff --git a/shell/platform/windows/flutter_windows_unittests.cc b/shell/platform/windows/flutter_windows_unittests.cc index e012254d976b2..a35bfae0f6cea 100644 --- a/shell/platform/windows/flutter_windows_unittests.cc +++ b/shell/platform/windows/flutter_windows_unittests.cc @@ -116,10 +116,8 @@ TEST_F(WindowsTest, LaunchCustomEntrypointInEngineRunInvocation) { TEST_F(WindowsTest, LaunchHeadlessEngine) { auto& context = GetContext(); WindowsConfigBuilder builder(context); - EnginePtr engine{builder.InitializeEngine()}; + EnginePtr engine{builder.RunHeadless()}; ASSERT_NE(engine, nullptr); - - ASSERT_TRUE(FlutterDesktopEngineRun(engine.get(), nullptr)); } // Verify that accessibility features are initialized when a view is created. diff --git a/shell/platform/windows/testing/engine_modifier.h b/shell/platform/windows/testing/engine_modifier.h index d070dc10993fe..bc2a921d86f63 100644 --- a/shell/platform/windows/testing/engine_modifier.h +++ b/shell/platform/windows/testing/engine_modifier.h @@ -39,7 +39,9 @@ class EngineModifier { // Override the engine's implicit view. This is the "default" view // that Flutter apps render to. - void SetImplicitView(FlutterWindowsView* view) { engine_->view_ = view; } + void SetImplicitView(FlutterWindowsView* view) { + engine_->views_[kImplicitViewId] = view; + } /// Reset the start_time field that is used to align vsync events. void SetStartTime(uint64_t start_time_nanos) { diff --git a/shell/platform/windows/testing/windows_test_config_builder.cc b/shell/platform/windows/testing/windows_test_config_builder.cc index 35376e241f062..be04bf4126c0e 100644 --- a/shell/platform/windows/testing/windows_test_config_builder.cc +++ b/shell/platform/windows/testing/windows_test_config_builder.cc @@ -71,6 +71,27 @@ EnginePtr WindowsConfigBuilder::InitializeEngine() const { return EnginePtr(FlutterDesktopEngineCreate(&engine_properties)); } +EnginePtr WindowsConfigBuilder::RunHeadless() const { + InitializeCOM(); + + EnginePtr engine = InitializeEngine(); + if (!engine) { + return {}; + } + + // Register native functions. + FlutterWindowsEngine* windows_engine = + reinterpret_cast(engine.get()); + windows_engine->SetRootIsolateCreateCallback( + context_.GetRootIsolateCallback()); + + if (!FlutterDesktopEngineRun(engine.get(), /* entry_point */ nullptr)) { + return {}; + } + + return engine; +} + ViewControllerPtr WindowsConfigBuilder::Run() const { InitializeCOM(); @@ -87,8 +108,8 @@ ViewControllerPtr WindowsConfigBuilder::Run() const { int width = 600; int height = 400; - ViewControllerPtr controller( - FlutterDesktopViewControllerCreate(width, height, engine.release())); + ViewControllerPtr controller{ + FlutterDesktopViewControllerCreate(width, height, engine.release())}; if (!controller) { return {}; } diff --git a/shell/platform/windows/testing/windows_test_config_builder.h b/shell/platform/windows/testing/windows_test_config_builder.h index 7ec9a3352b741..e22559d567e64 100644 --- a/shell/platform/windows/testing/windows_test_config_builder.h +++ b/shell/platform/windows/testing/windows_test_config_builder.h @@ -64,7 +64,11 @@ class WindowsConfigBuilder { // Returns a configured and initialized engine. EnginePtr InitializeEngine() const; - // Returns a configured and initialized view controller running the default + // Returns a configured and initialized engine running the + // Dart entrypoint. + EnginePtr RunHeadless() const; + + // Returns a configured and initialized view controller running the // Dart entrypoint. ViewControllerPtr Run() const;