| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "flutter/shell/platform/windows/flutter_windows_view.h" |
| |
| #include <comdef.h> |
| #include <comutil.h> |
| #include <oleacc.h> |
| |
| #include <future> |
| #include <iostream> |
| #include <vector> |
| |
| #include "flutter/shell/platform/common/json_message_codec.h" |
| #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h" |
| #include "flutter/shell/platform/windows/flutter_window.h" |
| #include "flutter/shell/platform/windows/flutter_windows_engine.h" |
| #include "flutter/shell/platform/windows/flutter_windows_texture_registrar.h" |
| #include "flutter/shell/platform/windows/testing/engine_modifier.h" |
| #include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h" |
| #include "flutter/shell/platform/windows/testing/test_keyboard.h" |
| |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| |
| namespace flutter { |
| namespace testing { |
| |
| constexpr uint64_t kScanCodeKeyA = 0x1e; |
| constexpr uint64_t kVirtualKeyA = 0x41; |
| |
| namespace { |
| |
| // A struct to use as a FlutterPlatformMessageResponseHandle so it can keep the |
| // callbacks and user data passed to the engine's |
| // PlatformMessageCreateResponseHandle for use in the SendPlatformMessage |
| // overridden function. |
| struct TestResponseHandle { |
| FlutterDesktopBinaryReply callback; |
| void* user_data; |
| }; |
| |
| static bool test_response = false; |
| |
| constexpr uint64_t kKeyEventFromChannel = 0x11; |
| constexpr uint64_t kKeyEventFromEmbedder = 0x22; |
| static std::vector<int> key_event_logs; |
| |
| std::unique_ptr<std::vector<uint8_t>> keyHandlingResponse(bool handled) { |
| rapidjson::Document document; |
| auto& allocator = document.GetAllocator(); |
| document.SetObject(); |
| document.AddMember("handled", test_response, allocator); |
| return flutter::JsonMessageCodec::GetInstance().EncodeMessage(document); |
| } |
| |
| // Returns an engine instance configured with dummy project path values, and |
| // overridden methods for sending platform messages, so that the engine can |
| // respond as if the framework were connected. |
| std::unique_ptr<FlutterWindowsEngine> GetTestEngine() { |
| FlutterDesktopEngineProperties properties = {}; |
| properties.assets_path = L"C:\\foo\\flutter_assets"; |
| properties.icu_data_path = L"C:\\foo\\icudtl.dat"; |
| properties.aot_library_path = L"C:\\foo\\aot.so"; |
| FlutterProjectBundle project(properties); |
| auto engine = std::make_unique<FlutterWindowsEngine>(project); |
| |
| EngineModifier modifier(engine.get()); |
| |
| auto key_response_controller = std::make_shared<MockKeyResponseController>(); |
| key_response_controller->SetChannelResponse( |
| [](MockKeyResponseController::ResponseCallback callback) { |
| key_event_logs.push_back(kKeyEventFromChannel); |
| callback(test_response); |
| }); |
| key_response_controller->SetEmbedderResponse( |
| [](const FlutterKeyEvent* event, |
| MockKeyResponseController::ResponseCallback callback) { |
| key_event_logs.push_back(kKeyEventFromEmbedder); |
| callback(test_response); |
| }); |
| modifier.embedder_api().NotifyDisplayUpdate = |
| MOCK_ENGINE_PROC(NotifyDisplayUpdate, |
| ([engine_instance = engine.get()]( |
| FLUTTER_API_SYMBOL(FlutterEngine) raw_engine, |
| const FlutterEngineDisplaysUpdateType update_type, |
| const FlutterEngineDisplay* embedder_displays, |
| size_t display_count) { return kSuccess; })); |
| |
| MockEmbedderApiForKeyboard(modifier, key_response_controller); |
| |
| engine->Run(); |
| return engine; |
| } |
| |
| } // namespace |
| |
| TEST(FlutterWindowsViewTest, KeySequence) { |
| std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine(); |
| |
| test_response = false; |
| |
| auto window_binding_handler = |
| std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>(); |
| FlutterWindowsView view(std::move(window_binding_handler)); |
| view.SetEngine(std::move(engine)); |
| |
| view.OnKey(kVirtualKeyA, kScanCodeKeyA, WM_KEYDOWN, 'a', false, false, |
| [](bool handled) {}); |
| |
| EXPECT_EQ(key_event_logs.size(), 2); |
| EXPECT_EQ(key_event_logs[0], kKeyEventFromEmbedder); |
| EXPECT_EQ(key_event_logs[1], kKeyEventFromChannel); |
| |
| key_event_logs.clear(); |
| } |
| |
| TEST(FlutterWindowsViewTest, RestartClearsKeyboardState) { |
| std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine(); |
| |
| auto window_binding_handler = |
| std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>(); |
| FlutterWindowsView view(std::move(window_binding_handler)); |
| view.SetEngine(std::move(engine)); |
| |
| test_response = false; |
| |
| // Receives a KeyA down. Events are dispatched and decided unhandled. Now the |
| // keyboard key handler is waiting for the redispatched event. |
| view.OnKey(kVirtualKeyA, kScanCodeKeyA, WM_KEYDOWN, 'a', false, false, |
| [](bool handled) {}); |
| EXPECT_EQ(key_event_logs.size(), 2); |
| EXPECT_EQ(key_event_logs[0], kKeyEventFromEmbedder); |
| EXPECT_EQ(key_event_logs[1], kKeyEventFromChannel); |
| key_event_logs.clear(); |
| |
| // Resets state so that the keyboard key handler is no longer waiting. |
| view.OnPreEngineRestart(); |
| |
| // Receives another KeyA down. If the state had not been cleared, this event |
| // will be considered the redispatched event and ignored. |
| view.OnKey(kVirtualKeyA, kScanCodeKeyA, WM_KEYDOWN, 'a', false, false, |
| [](bool handled) {}); |
| EXPECT_EQ(key_event_logs.size(), 2); |
| EXPECT_EQ(key_event_logs[0], kKeyEventFromEmbedder); |
| EXPECT_EQ(key_event_logs[1], kKeyEventFromChannel); |
| key_event_logs.clear(); |
| } |
| |
| TEST(FlutterWindowsViewTest, EnableSemantics) { |
| std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine(); |
| EngineModifier modifier(engine.get()); |
| |
| bool semantics_enabled = false; |
| modifier.embedder_api().UpdateSemanticsEnabled = MOCK_ENGINE_PROC( |
| UpdateSemanticsEnabled, |
| [&semantics_enabled](FLUTTER_API_SYMBOL(FlutterEngine) engine, |
| bool enabled) { |
| semantics_enabled = enabled; |
| return kSuccess; |
| }); |
| |
| auto window_binding_handler = |
| std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>(); |
| FlutterWindowsView view(std::move(window_binding_handler)); |
| view.SetEngine(std::move(engine)); |
| |
| view.OnUpdateSemanticsEnabled(true); |
| EXPECT_TRUE(semantics_enabled); |
| } |
| |
| TEST(FlutterWindowsView, AddSemanticsNodeUpdate) { |
| std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine(); |
| EngineModifier modifier(engine.get()); |
| modifier.embedder_api().UpdateSemanticsEnabled = |
| [](FLUTTER_API_SYMBOL(FlutterEngine) engine, bool enabled) { |
| return kSuccess; |
| }; |
| |
| auto window_binding_handler = |
| std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>(); |
| FlutterWindowsView view(std::move(window_binding_handler)); |
| view.SetEngine(std::move(engine)); |
| |
| // Enable semantics to instantiate accessibility bridge. |
| view.OnUpdateSemanticsEnabled(true); |
| |
| auto bridge = view.GetEngine()->accessibility_bridge().lock(); |
| ASSERT_TRUE(bridge); |
| |
| // Add root node. |
| FlutterSemanticsNode node{sizeof(FlutterSemanticsNode), 0}; |
| node.label = "name"; |
| node.value = "value"; |
| node.platform_view_id = -1; |
| bridge->AddFlutterSemanticsNodeUpdate(&node); |
| bridge->CommitUpdates(); |
| |
| // Look up the root windows node delegate. |
| auto node_delegate = bridge |
| ->GetFlutterPlatformNodeDelegateFromID( |
| AccessibilityBridge::kRootNodeId) |
| .lock(); |
| ASSERT_TRUE(node_delegate); |
| EXPECT_EQ(node_delegate->GetChildCount(), 0); |
| |
| // Get the native IAccessible object. |
| IAccessible* native_view = node_delegate->GetNativeViewAccessible(); |
| ASSERT_TRUE(native_view != nullptr); |
| |
| // Property lookups will be made against this node itself. |
| VARIANT varchild{}; |
| varchild.vt = VT_I4; |
| varchild.lVal = CHILDID_SELF; |
| |
| // Verify node name matches our label. |
| BSTR bname = nullptr; |
| ASSERT_EQ(native_view->get_accName(varchild, &bname), S_OK); |
| std::string name(_com_util::ConvertBSTRToString(bname)); |
| EXPECT_EQ(name, "name"); |
| |
| // Verify node value matches. |
| BSTR bvalue = nullptr; |
| ASSERT_EQ(native_view->get_accValue(varchild, &bvalue), S_OK); |
| std::string value(_com_util::ConvertBSTRToString(bvalue)); |
| EXPECT_EQ(value, "value"); |
| |
| // Verify node type is static text. |
| VARIANT varrole{}; |
| varrole.vt = VT_I4; |
| ASSERT_EQ(native_view->get_accRole(varchild, &varrole), S_OK); |
| EXPECT_EQ(varrole.lVal, ROLE_SYSTEM_STATICTEXT); |
| } |
| |
| // Verify the native IAccessible COM object tree is an accurate reflection of |
| // the platform-agnostic tree. Verify both a root node with children as well as |
| // a non-root node with children, since the AX tree includes special handling |
| // for the root. |
| // |
| // node0 |
| // / \ |
| // node1 node2 |
| // | |
| // node3 |
| // |
| // node0 and node2 are grouping nodes. node1 and node2 are static text nodes. |
| TEST(FlutterWindowsView, AddSemanticsNodeUpdateWithChildren) { |
| std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine(); |
| EngineModifier modifier(engine.get()); |
| modifier.embedder_api().UpdateSemanticsEnabled = |
| [](FLUTTER_API_SYMBOL(FlutterEngine) engine, bool enabled) { |
| return kSuccess; |
| }; |
| |
| auto window_binding_handler = |
| std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>(); |
| FlutterWindowsView view(std::move(window_binding_handler)); |
| view.SetEngine(std::move(engine)); |
| |
| // Enable semantics to instantiate accessibility bridge. |
| view.OnUpdateSemanticsEnabled(true); |
| |
| auto bridge = view.GetEngine()->accessibility_bridge().lock(); |
| ASSERT_TRUE(bridge); |
| |
| // Add root node. |
| FlutterSemanticsNode node0{sizeof(FlutterSemanticsNode), 0}; |
| std::vector<int32_t> node0_children{1, 2}; |
| node0.child_count = node0_children.size(); |
| node0.children_in_traversal_order = node0_children.data(); |
| node0.children_in_hit_test_order = node0_children.data(); |
| |
| FlutterSemanticsNode node1{sizeof(FlutterSemanticsNode), 1}; |
| node1.label = "prefecture"; |
| node1.value = "Kyoto"; |
| FlutterSemanticsNode node2{sizeof(FlutterSemanticsNode), 2}; |
| std::vector<int32_t> node2_children{3}; |
| node2.child_count = node2_children.size(); |
| node2.children_in_traversal_order = node2_children.data(); |
| node2.children_in_hit_test_order = node2_children.data(); |
| FlutterSemanticsNode node3{sizeof(FlutterSemanticsNode), 3}; |
| node3.label = "city"; |
| node3.value = "Uji"; |
| |
| bridge->AddFlutterSemanticsNodeUpdate(&node0); |
| bridge->AddFlutterSemanticsNodeUpdate(&node1); |
| bridge->AddFlutterSemanticsNodeUpdate(&node2); |
| bridge->AddFlutterSemanticsNodeUpdate(&node3); |
| bridge->CommitUpdates(); |
| |
| // Look up the root windows node delegate. |
| auto node_delegate = bridge |
| ->GetFlutterPlatformNodeDelegateFromID( |
| AccessibilityBridge::kRootNodeId) |
| .lock(); |
| ASSERT_TRUE(node_delegate); |
| EXPECT_EQ(node_delegate->GetChildCount(), 2); |
| |
| // Get the native IAccessible object. |
| IAccessible* node0_accessible = node_delegate->GetNativeViewAccessible(); |
| ASSERT_TRUE(node0_accessible != nullptr); |
| |
| // Property lookups will be made against this node itself. |
| VARIANT varchild{}; |
| varchild.vt = VT_I4; |
| varchild.lVal = CHILDID_SELF; |
| |
| // Verify node type is a group. |
| VARIANT varrole{}; |
| varrole.vt = VT_I4; |
| ASSERT_EQ(node0_accessible->get_accRole(varchild, &varrole), S_OK); |
| EXPECT_EQ(varrole.lVal, ROLE_SYSTEM_GROUPING); |
| |
| // Verify child count. |
| long node0_child_count = 0; |
| ASSERT_EQ(node0_accessible->get_accChildCount(&node0_child_count), S_OK); |
| EXPECT_EQ(node0_child_count, 2); |
| |
| { |
| // Look up first child of node0 (node1), a static text node. |
| varchild.lVal = 1; |
| IDispatch* node1_dispatch = nullptr; |
| ASSERT_EQ(node0_accessible->get_accChild(varchild, &node1_dispatch), S_OK); |
| ASSERT_TRUE(node1_dispatch != nullptr); |
| IAccessible* node1_accessible = nullptr; |
| ASSERT_EQ(node1_dispatch->QueryInterface( |
| IID_IAccessible, reinterpret_cast<void**>(&node1_accessible)), |
| S_OK); |
| ASSERT_TRUE(node1_accessible != nullptr); |
| |
| // Verify node name matches our label. |
| varchild.lVal = CHILDID_SELF; |
| BSTR bname = nullptr; |
| ASSERT_EQ(node1_accessible->get_accName(varchild, &bname), S_OK); |
| std::string name(_com_util::ConvertBSTRToString(bname)); |
| EXPECT_EQ(name, "prefecture"); |
| |
| // Verify node value matches. |
| BSTR bvalue = nullptr; |
| ASSERT_EQ(node1_accessible->get_accValue(varchild, &bvalue), S_OK); |
| std::string value(_com_util::ConvertBSTRToString(bvalue)); |
| EXPECT_EQ(value, "Kyoto"); |
| |
| // Verify node type is static text. |
| VARIANT varrole{}; |
| varrole.vt = VT_I4; |
| ASSERT_EQ(node1_accessible->get_accRole(varchild, &varrole), S_OK); |
| EXPECT_EQ(varrole.lVal, ROLE_SYSTEM_STATICTEXT); |
| |
| // Verify the parent node is the root. |
| IDispatch* parent_dispatch; |
| node1_accessible->get_accParent(&parent_dispatch); |
| IAccessible* parent_accessible; |
| ASSERT_EQ( |
| parent_dispatch->QueryInterface( |
| IID_IAccessible, reinterpret_cast<void**>(&parent_accessible)), |
| S_OK); |
| EXPECT_EQ(parent_accessible, node0_accessible); |
| } |
| |
| // Look up second child of node0 (node2), a parent group for node3. |
| varchild.lVal = 2; |
| IDispatch* node2_dispatch = nullptr; |
| ASSERT_EQ(node0_accessible->get_accChild(varchild, &node2_dispatch), S_OK); |
| ASSERT_TRUE(node2_dispatch != nullptr); |
| IAccessible* node2_accessible = nullptr; |
| ASSERT_EQ(node2_dispatch->QueryInterface( |
| IID_IAccessible, reinterpret_cast<void**>(&node2_accessible)), |
| S_OK); |
| ASSERT_TRUE(node2_accessible != nullptr); |
| |
| { |
| // Verify child count. |
| long node2_child_count = 0; |
| ASSERT_EQ(node2_accessible->get_accChildCount(&node2_child_count), S_OK); |
| EXPECT_EQ(node2_child_count, 1); |
| |
| // Verify node type is static text. |
| varchild.lVal = CHILDID_SELF; |
| VARIANT varrole{}; |
| varrole.vt = VT_I4; |
| ASSERT_EQ(node2_accessible->get_accRole(varchild, &varrole), S_OK); |
| EXPECT_EQ(varrole.lVal, ROLE_SYSTEM_GROUPING); |
| |
| // Verify the parent node is the root. |
| IDispatch* parent_dispatch; |
| node2_accessible->get_accParent(&parent_dispatch); |
| IAccessible* parent_accessible; |
| ASSERT_EQ( |
| parent_dispatch->QueryInterface( |
| IID_IAccessible, reinterpret_cast<void**>(&parent_accessible)), |
| S_OK); |
| EXPECT_EQ(parent_accessible, node0_accessible); |
| } |
| |
| { |
| // Look up only child of node2 (node3), a static text node. |
| varchild.lVal = 1; |
| IDispatch* node3_dispatch = nullptr; |
| ASSERT_EQ(node2_accessible->get_accChild(varchild, &node3_dispatch), S_OK); |
| ASSERT_TRUE(node3_dispatch != nullptr); |
| IAccessible* node3_accessible = nullptr; |
| ASSERT_EQ(node3_dispatch->QueryInterface( |
| IID_IAccessible, reinterpret_cast<void**>(&node3_accessible)), |
| S_OK); |
| ASSERT_TRUE(node3_accessible != nullptr); |
| |
| // Verify node name matches our label. |
| varchild.lVal = CHILDID_SELF; |
| BSTR bname = nullptr; |
| ASSERT_EQ(node3_accessible->get_accName(varchild, &bname), S_OK); |
| std::string name(_com_util::ConvertBSTRToString(bname)); |
| EXPECT_EQ(name, "city"); |
| |
| // Verify node value matches. |
| BSTR bvalue = nullptr; |
| ASSERT_EQ(node3_accessible->get_accValue(varchild, &bvalue), S_OK); |
| std::string value(_com_util::ConvertBSTRToString(bvalue)); |
| EXPECT_EQ(value, "Uji"); |
| |
| // Verify node type is static text. |
| VARIANT varrole{}; |
| varrole.vt = VT_I4; |
| ASSERT_EQ(node3_accessible->get_accRole(varchild, &varrole), S_OK); |
| EXPECT_EQ(varrole.lVal, ROLE_SYSTEM_STATICTEXT); |
| |
| // Verify the parent node is node2. |
| IDispatch* parent_dispatch; |
| node3_accessible->get_accParent(&parent_dispatch); |
| IAccessible* parent_accessible; |
| ASSERT_EQ( |
| parent_dispatch->QueryInterface( |
| IID_IAccessible, reinterpret_cast<void**>(&parent_accessible)), |
| S_OK); |
| EXPECT_EQ(parent_accessible, node2_accessible); |
| } |
| } |
| |
| // Verify the native IAccessible accHitTest method returns the correct |
| // IAccessible COM object for the given coordinates. |
| // |
| // +-----------+ |
| // | | | |
| // node0 | | B | |
| // / \ | A |-----| |
| // node1 node2 | | C | |
| // | | | | |
| // node3 +-----------+ |
| // |
| // node0 and node2 are grouping nodes. node1 and node2 are static text nodes. |
| // |
| // node0 is located at 0,0 with size 500x500. It spans areas A, B, and C. |
| // node1 is located at 0,0 with size 250x500. It spans area A. |
| // node2 is located at 250,0 with size 250x500. It spans areas B and C. |
| // node3 is located at 250,250 with size 250x250. It spans area C. |
| TEST(FlutterWindowsViewTest, AccessibilityHitTesting) { |
| constexpr FlutterTransformation kIdentityTransform = {1, 0, 0, // |
| 0, 1, 0, // |
| 0, 0, 1}; |
| |
| std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine(); |
| EngineModifier modifier(engine.get()); |
| modifier.embedder_api().UpdateSemanticsEnabled = |
| [](FLUTTER_API_SYMBOL(FlutterEngine) engine, bool enabled) { |
| return kSuccess; |
| }; |
| |
| auto window_binding_handler = |
| std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>(); |
| FlutterWindowsView view(std::move(window_binding_handler)); |
| view.SetEngine(std::move(engine)); |
| |
| // Enable semantics to instantiate accessibility bridge. |
| view.OnUpdateSemanticsEnabled(true); |
| |
| auto bridge = view.GetEngine()->accessibility_bridge().lock(); |
| ASSERT_TRUE(bridge); |
| |
| // Add root node at origin. Size 500x500. |
| FlutterSemanticsNode node0{sizeof(FlutterSemanticsNode), 0}; |
| std::vector<int32_t> node0_children{1, 2}; |
| node0.rect = {0, 0, 500, 500}; |
| node0.transform = kIdentityTransform; |
| node0.child_count = node0_children.size(); |
| node0.children_in_traversal_order = node0_children.data(); |
| node0.children_in_hit_test_order = node0_children.data(); |
| |
| // Add node 1 located at 0,0 relative to node 0. Size 250x500. |
| FlutterSemanticsNode node1{sizeof(FlutterSemanticsNode), 1}; |
| node1.rect = {0, 0, 250, 500}; |
| node1.transform = kIdentityTransform; |
| node1.label = "prefecture"; |
| node1.value = "Kyoto"; |
| |
| // Add node 2 located at 250,0 relative to node 0. Size 250x500. |
| FlutterSemanticsNode node2{sizeof(FlutterSemanticsNode), 2}; |
| std::vector<int32_t> node2_children{3}; |
| node2.rect = {0, 0, 250, 500}; |
| node2.transform = {1, 0, 250, 0, 1, 0, 0, 0, 1}; |
| node2.child_count = node2_children.size(); |
| node2.children_in_traversal_order = node2_children.data(); |
| node2.children_in_hit_test_order = node2_children.data(); |
| |
| // Add node 3 located at 0,250 relative to node 2. Size 250, 250. |
| FlutterSemanticsNode node3{sizeof(FlutterSemanticsNode), 3}; |
| node3.rect = {0, 0, 250, 250}; |
| node3.transform = {1, 0, 0, 0, 1, 250, 0, 0, 1}; |
| node3.label = "city"; |
| node3.value = "Uji"; |
| |
| bridge->AddFlutterSemanticsNodeUpdate(&node0); |
| bridge->AddFlutterSemanticsNodeUpdate(&node1); |
| bridge->AddFlutterSemanticsNodeUpdate(&node2); |
| bridge->AddFlutterSemanticsNodeUpdate(&node3); |
| bridge->CommitUpdates(); |
| |
| // Look up the root windows node delegate. |
| auto node0_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock(); |
| ASSERT_TRUE(node0_delegate); |
| auto node1_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock(); |
| ASSERT_TRUE(node1_delegate); |
| auto node2_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(2).lock(); |
| ASSERT_TRUE(node2_delegate); |
| auto node3_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(3).lock(); |
| ASSERT_TRUE(node3_delegate); |
| |
| // Get the native IAccessible root object. |
| IAccessible* node0_accessible = node0_delegate->GetNativeViewAccessible(); |
| ASSERT_TRUE(node0_accessible != nullptr); |
| |
| // Perform a hit test that should hit node 1. |
| VARIANT varchild{}; |
| ASSERT_TRUE(SUCCEEDED(node0_accessible->accHitTest(150, 150, &varchild))); |
| EXPECT_EQ(varchild.vt, VT_DISPATCH); |
| EXPECT_EQ(varchild.pdispVal, node1_delegate->GetNativeViewAccessible()); |
| |
| // Perform a hit test that should hit node 2. |
| varchild = {}; |
| ASSERT_TRUE(SUCCEEDED(node0_accessible->accHitTest(450, 150, &varchild))); |
| EXPECT_EQ(varchild.vt, VT_DISPATCH); |
| EXPECT_EQ(varchild.pdispVal, node2_delegate->GetNativeViewAccessible()); |
| |
| // Perform a hit test that should hit node 3. |
| varchild = {}; |
| ASSERT_TRUE(SUCCEEDED(node0_accessible->accHitTest(450, 450, &varchild))); |
| EXPECT_EQ(varchild.vt, VT_DISPATCH); |
| EXPECT_EQ(varchild.pdispVal, node3_delegate->GetNativeViewAccessible()); |
| } |
| |
| TEST(FlutterWindowsViewTest, WindowResizeTests) { |
| std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine(); |
| EngineModifier modifier(engine.get()); |
| |
| auto window_binding_handler = |
| std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>(); |
| |
| FlutterWindowsView view(std::move(window_binding_handler)); |
| view.SetEngine(std::move(engine)); |
| |
| bool send_window_metrics_event_called = false; |
| modifier.embedder_api().SendWindowMetricsEvent = MOCK_ENGINE_PROC( |
| SendWindowMetricsEvent, |
| ([&send_window_metrics_event_called]( |
| auto engine, const FlutterWindowMetricsEvent* even) { |
| send_window_metrics_event_called = true; |
| return kSuccess; |
| })); |
| |
| std::promise<bool> resize_completed; |
| std::thread([&resize_completed, &view]() { |
| view.OnWindowSizeChanged(500, 500); |
| resize_completed.set_value(true); |
| }).detach(); |
| |
| auto result = resize_completed.get_future().wait_for(std::chrono::seconds(1)); |
| EXPECT_EQ(std::future_status::ready, result); |
| EXPECT_TRUE(send_window_metrics_event_called); |
| } |
| |
| TEST(FlutterWindowsViewTest, WindowRepaintTests) { |
| std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine(); |
| EngineModifier modifier(engine.get()); |
| |
| FlutterWindowsView view(std::make_unique<flutter::FlutterWindow>(100, 100)); |
| view.SetEngine(std::move(engine)); |
| view.CreateRenderSurface(); |
| |
| bool schedule_frame_called = false; |
| modifier.embedder_api().ScheduleFrame = |
| MOCK_ENGINE_PROC(ScheduleFrame, ([&schedule_frame_called](auto engine) { |
| schedule_frame_called = true; |
| return kSuccess; |
| })); |
| |
| view.OnWindowRepaint(); |
| EXPECT_TRUE(schedule_frame_called); |
| } |
| |
| // Ensure that checkboxes have their checked status set apropriately |
| // Previously, only Radios could have this flag updated |
| // Resulted in the issue seen at |
| // https://github.com/flutter/flutter/issues/96218 |
| // This test ensures that the native state of Checkboxes on Windows, |
| // specifically, is updated as desired. |
| TEST(FlutterWindowsViewTest, CheckboxNativeState) { |
| std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine(); |
| EngineModifier modifier(engine.get()); |
| modifier.embedder_api().UpdateSemanticsEnabled = |
| [](FLUTTER_API_SYMBOL(FlutterEngine) engine, bool enabled) { |
| return kSuccess; |
| }; |
| |
| auto window_binding_handler = |
| std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>(); |
| FlutterWindowsView view(std::move(window_binding_handler)); |
| view.SetEngine(std::move(engine)); |
| |
| // Enable semantics to instantiate accessibility bridge. |
| view.OnUpdateSemanticsEnabled(true); |
| |
| auto bridge = view.GetEngine()->accessibility_bridge().lock(); |
| ASSERT_TRUE(bridge); |
| |
| FlutterSemanticsNode root{sizeof(FlutterSemanticsNode), 0}; |
| root.id = 0; |
| root.label = "root"; |
| root.hint = ""; |
| root.value = ""; |
| root.increased_value = ""; |
| root.decreased_value = ""; |
| root.child_count = 0; |
| root.custom_accessibility_actions_count = 0; |
| root.flags = static_cast<FlutterSemanticsFlag>( |
| FlutterSemanticsFlag::kFlutterSemanticsFlagHasCheckedState | |
| FlutterSemanticsFlag::kFlutterSemanticsFlagIsChecked); |
| bridge->AddFlutterSemanticsNodeUpdate(&root); |
| |
| bridge->CommitUpdates(); |
| |
| { |
| auto root_node = bridge |
| ->GetFlutterPlatformNodeDelegateFromID( |
| AccessibilityBridge::kRootNodeId) |
| .lock(); |
| EXPECT_EQ(root_node->GetData().role, ax::mojom::Role::kCheckBox); |
| EXPECT_EQ(root_node->GetData().GetCheckedState(), |
| ax::mojom::CheckedState::kTrue); |
| |
| // Get the IAccessible for the root node. |
| IAccessible* native_view = root_node->GetNativeViewAccessible(); |
| ASSERT_TRUE(native_view != nullptr); |
| |
| // Look up against the node itself (not one of its children). |
| VARIANT varchild = {}; |
| varchild.vt = VT_I4; |
| |
| // Verify the checkbox is checked. |
| varchild.lVal = CHILDID_SELF; |
| VARIANT native_state = {}; |
| ASSERT_TRUE(SUCCEEDED(native_view->get_accState(varchild, &native_state))); |
| EXPECT_TRUE(native_state.lVal & STATE_SYSTEM_CHECKED); |
| } |
| |
| // Test unchecked too. |
| root.flags = static_cast<FlutterSemanticsFlag>( |
| FlutterSemanticsFlag::kFlutterSemanticsFlagHasCheckedState); |
| bridge->AddFlutterSemanticsNodeUpdate(&root); |
| bridge->CommitUpdates(); |
| |
| { |
| auto root_node = bridge |
| ->GetFlutterPlatformNodeDelegateFromID( |
| AccessibilityBridge::kRootNodeId) |
| .lock(); |
| EXPECT_EQ(root_node->GetData().role, ax::mojom::Role::kCheckBox); |
| EXPECT_EQ(root_node->GetData().GetCheckedState(), |
| ax::mojom::CheckedState::kFalse); |
| |
| // Get the IAccessible for the root node. |
| IAccessible* native_view = root_node->GetNativeViewAccessible(); |
| ASSERT_TRUE(native_view != nullptr); |
| |
| // Look up against the node itself (not one of its children). |
| VARIANT varchild = {}; |
| varchild.vt = VT_I4; |
| |
| // Verify the checkbox is unchecked. |
| varchild.lVal = CHILDID_SELF; |
| VARIANT native_state = {}; |
| ASSERT_TRUE(SUCCEEDED(native_view->get_accState(varchild, &native_state))); |
| EXPECT_FALSE(native_state.lVal & STATE_SYSTEM_CHECKED); |
| } |
| |
| // Now check mixed state. |
| root.flags = static_cast<FlutterSemanticsFlag>( |
| FlutterSemanticsFlag::kFlutterSemanticsFlagHasCheckedState | |
| FlutterSemanticsFlag::kFlutterSemanticsFlagIsCheckStateMixed); |
| bridge->AddFlutterSemanticsNodeUpdate(&root); |
| bridge->CommitUpdates(); |
| |
| { |
| auto root_node = bridge |
| ->GetFlutterPlatformNodeDelegateFromID( |
| AccessibilityBridge::kRootNodeId) |
| .lock(); |
| EXPECT_EQ(root_node->GetData().role, ax::mojom::Role::kCheckBox); |
| EXPECT_EQ(root_node->GetData().GetCheckedState(), |
| ax::mojom::CheckedState::kMixed); |
| |
| // Get the IAccessible for the root node. |
| IAccessible* native_view = root_node->GetNativeViewAccessible(); |
| ASSERT_TRUE(native_view != nullptr); |
| |
| // Look up against the node itself (not one of its children). |
| VARIANT varchild = {}; |
| varchild.vt = VT_I4; |
| |
| // Verify the checkbox is mixed. |
| varchild.lVal = CHILDID_SELF; |
| VARIANT native_state = {}; |
| ASSERT_TRUE(SUCCEEDED(native_view->get_accState(varchild, &native_state))); |
| EXPECT_TRUE(native_state.lVal & STATE_SYSTEM_MIXED); |
| } |
| } |
| |
| // Ensure that switches have their toggle status set apropriately |
| TEST(FlutterWindowsViewTest, SwitchNativeState) { |
| std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine(); |
| EngineModifier modifier(engine.get()); |
| modifier.embedder_api().UpdateSemanticsEnabled = |
| [](FLUTTER_API_SYMBOL(FlutterEngine) engine, bool enabled) { |
| return kSuccess; |
| }; |
| |
| auto window_binding_handler = |
| std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>(); |
| FlutterWindowsView view(std::move(window_binding_handler)); |
| view.SetEngine(std::move(engine)); |
| |
| // Enable semantics to instantiate accessibility bridge. |
| view.OnUpdateSemanticsEnabled(true); |
| |
| auto bridge = view.GetEngine()->accessibility_bridge().lock(); |
| ASSERT_TRUE(bridge); |
| |
| FlutterSemanticsNode root{sizeof(FlutterSemanticsNode), 0}; |
| root.id = 0; |
| root.label = "root"; |
| root.hint = ""; |
| root.value = ""; |
| root.increased_value = ""; |
| root.decreased_value = ""; |
| root.child_count = 0; |
| root.custom_accessibility_actions_count = 0; |
| root.flags = static_cast<FlutterSemanticsFlag>( |
| FlutterSemanticsFlag::kFlutterSemanticsFlagHasToggledState | |
| FlutterSemanticsFlag::kFlutterSemanticsFlagIsToggled); |
| bridge->AddFlutterSemanticsNodeUpdate(&root); |
| |
| bridge->CommitUpdates(); |
| |
| { |
| auto root_node = bridge |
| ->GetFlutterPlatformNodeDelegateFromID( |
| AccessibilityBridge::kRootNodeId) |
| .lock(); |
| EXPECT_EQ(root_node->GetData().role, ax::mojom::Role::kToggleButton); |
| EXPECT_EQ(root_node->GetData().GetCheckedState(), |
| ax::mojom::CheckedState::kTrue); |
| |
| // Get the IAccessible for the root node. |
| IAccessible* native_view = root_node->GetNativeViewAccessible(); |
| ASSERT_TRUE(native_view != nullptr); |
| |
| // Look up against the node itself (not one of its children). |
| VARIANT varchild = {}; |
| varchild.vt = VT_I4; |
| |
| varchild.lVal = CHILDID_SELF; |
| VARIANT varrole = {}; |
| |
| // Verify the role of the switch is CHECKBUTTON |
| ASSERT_EQ(native_view->get_accRole(varchild, &varrole), S_OK); |
| ASSERT_EQ(varrole.lVal, ROLE_SYSTEM_CHECKBUTTON); |
| |
| // Verify the switch is pressed. |
| VARIANT native_state = {}; |
| ASSERT_TRUE(SUCCEEDED(native_view->get_accState(varchild, &native_state))); |
| EXPECT_TRUE(native_state.lVal & STATE_SYSTEM_PRESSED); |
| } |
| |
| // Test unpressed too. |
| root.flags = static_cast<FlutterSemanticsFlag>( |
| FlutterSemanticsFlag::kFlutterSemanticsFlagHasToggledState); |
| bridge->AddFlutterSemanticsNodeUpdate(&root); |
| bridge->CommitUpdates(); |
| |
| { |
| auto root_node = bridge |
| ->GetFlutterPlatformNodeDelegateFromID( |
| AccessibilityBridge::kRootNodeId) |
| .lock(); |
| EXPECT_EQ(root_node->GetData().role, ax::mojom::Role::kToggleButton); |
| EXPECT_EQ(root_node->GetData().GetCheckedState(), |
| ax::mojom::CheckedState::kFalse); |
| |
| // Get the IAccessible for the root node. |
| IAccessible* native_view = root_node->GetNativeViewAccessible(); |
| ASSERT_TRUE(native_view != nullptr); |
| |
| // Look up against the node itself (not one of its children). |
| VARIANT varchild = {}; |
| varchild.vt = VT_I4; |
| |
| // Verify the switch is not pressed. |
| varchild.lVal = CHILDID_SELF; |
| VARIANT native_state = {}; |
| ASSERT_TRUE(SUCCEEDED(native_view->get_accState(varchild, &native_state))); |
| EXPECT_FALSE(native_state.lVal & STATE_SYSTEM_PRESSED); |
| } |
| } |
| |
| } // namespace testing |
| } // namespace flutter |