| Flutter Windows Embedder |

flutter_windows_view_unittests.cc
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "flutter/shell/platform/windows/flutter_windows_view.h"
6
7 #include <UIAutomation.h>
8 #include <comdef.h>
9 #include <comutil.h>
10 #include <oleacc.h>
11
12 #include <future>
13 #include <vector>
14
15 #include "flutter/fml/synchronization/waitable_event.h"
16 #include "flutter/shell/platform/common/json_message_codec.h"
17 #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h"
18 #include "flutter/shell/platform/windows/flutter_window.h"
19 #include "flutter/shell/platform/windows/flutter_windows_engine.h"
20 #include "flutter/shell/platform/windows/flutter_windows_texture_registrar.h"
21 #include "flutter/shell/platform/windows/testing/engine_modifier.h"
22 #include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h"
23 #include "flutter/shell/platform/windows/testing/test_keyboard.h"
24
25 #include "gmock/gmock.h"
26 #include "gtest/gtest.h"
27
28 namespace flutter {
29 namespace testing {
30
31 using ::testing::_;
32 using ::testing::InSequence;
33 using ::testing::NiceMock;
34 using ::testing::Return;
35
36 constexpr uint64_t kScanCodeKeyA = 0x1e;
37 constexpr uint64_t kVirtualKeyA = 0x41;
38
39 namespace {
40
41 // A struct to use as a FlutterPlatformMessageResponseHandle so it can keep the
42 // callbacks and user data passed to the engine's
43 // PlatformMessageCreateResponseHandle for use in the SendPlatformMessage
44 // overridden function.
45 struct TestResponseHandle {
46 FlutterDesktopBinaryReply callback;
47 void* user_data;
48 };
49
50 static bool test_response = false;
51
52 constexpr uint64_t kKeyEventFromChannel = 0x11;
53 constexpr uint64_t kKeyEventFromEmbedder = 0x22;
54 static std::vector<int> key_event_logs;
55
56 std::unique_ptr<std::vector<uint8_t>> keyHandlingResponse(bool handled) {
57 rapidjson::Document document;
58 auto& allocator = document.GetAllocator();
59 document.SetObject();
60 document.AddMember("handled", test_response, allocator);
61 return flutter::JsonMessageCodec::GetInstance().EncodeMessage(document);
62 }
63
64 // Returns a Flutter project with the required path values to create
65 // a test engine.
66 FlutterProjectBundle GetTestProject() {
67 FlutterDesktopEngineProperties properties = {};
68 properties.assets_path = L"C:\\foo\\flutter_assets";
69 properties.icu_data_path = L"C:\\foo\\icudtl.dat";
70 properties.aot_library_path = L"C:\\foo\\aot.so";
71
72 return FlutterProjectBundle{properties};
73 }
74
75 // Returns an engine instance configured with test project path values, and
76 // overridden methods for sending platform messages, so that the engine can
77 // respond as if the framework were connected.
78 std::unique_ptr<FlutterWindowsEngine> GetTestEngine() {
79 auto engine = std::make_unique<FlutterWindowsEngine>(GetTestProject());
80
81 EngineModifier modifier(engine.get());
82
83 auto key_response_controller = std::make_shared<MockKeyResponseController>();
84 key_response_controller->SetChannelResponse(
85 [](MockKeyResponseController::ResponseCallback callback) {
86 key_event_logs.push_back(kKeyEventFromChannel);
87 callback(test_response);
88 });
89 key_response_controller->SetEmbedderResponse(
90 [](const FlutterKeyEvent* event,
91 MockKeyResponseController::ResponseCallback callback) {
92 key_event_logs.push_back(kKeyEventFromEmbedder);
93 callback(test_response);
94 });
95 modifier.embedder_api().NotifyDisplayUpdate =
96 MOCK_ENGINE_PROC(NotifyDisplayUpdate,
97 ([engine_instance = engine.get()](
98 FLUTTER_API_SYMBOL(FlutterEngine) raw_engine,
99 const FlutterEngineDisplaysUpdateType update_type,
100 const FlutterEngineDisplay* embedder_displays,
101 size_t display_count) { return kSuccess; }));
102
103 MockEmbedderApiForKeyboard(modifier, key_response_controller);
104
105 engine->Run();
106 return engine;
107 }
108
109 class MockFlutterWindowsEngine : public FlutterWindowsEngine {
110 public:
111 MockFlutterWindowsEngine() : FlutterWindowsEngine(GetTestProject()) {}
112
113 MOCK_METHOD(bool, Stop, (), ());
114 MOCK_METHOD(bool, PostRasterThreadTask, (fml::closure), ());
115
116 private:
117 FML_DISALLOW_COPY_AND_ASSIGN(MockFlutterWindowsEngine);
118 };
119
120 class MockAngleSurfaceManager : public AngleSurfaceManager {
121 public:
122 MockAngleSurfaceManager() : AngleSurfaceManager(false) {}
123
124 MOCK_METHOD(bool,
125 CreateSurface,
126 (WindowsRenderTarget*, EGLint, EGLint, bool),
127 (override));
128 MOCK_METHOD(void,
129 ResizeSurface,
130 (WindowsRenderTarget*, EGLint, EGLint, bool),
131 (override));
132 MOCK_METHOD(void, DestroySurface, (), (override));
133
134 MOCK_METHOD(void, SetVSyncEnabled, (bool), (override));
135
136 private:
137 FML_DISALLOW_COPY_AND_ASSIGN(MockAngleSurfaceManager);
138 };
139
140 } // namespace
141
142 // Ensure that submenu buttons have their expanded/collapsed status set
143 // apropriately.
144 TEST(FlutterWindowsViewTest, SubMenuExpandedState) {
145 std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine();
146 EngineModifier modifier(engine.get());
147 modifier.embedder_api().UpdateSemanticsEnabled =
148 [](FLUTTER_API_SYMBOL(FlutterEngine) engine, bool enabled) {
149 return kSuccess;
150 };
151
152 auto window_binding_handler =
153 std::make_unique<NiceMock<MockWindowBindingHandler>>();
154 FlutterWindowsView view(std::move(window_binding_handler));
155 view.SetEngine(std::move(engine));
156
157 // Enable semantics to instantiate accessibility bridge.
158 view.OnUpdateSemanticsEnabled(true);
159
160 auto bridge = view.accessibility_bridge().lock();
161 ASSERT_TRUE(bridge);
162
163 FlutterSemanticsNode2 root{sizeof(FlutterSemanticsNode2), 0};
164 root.id = 0;
165 root.label = "root";
166 root.hint = "";
167 root.value = "";
168 root.increased_value = "";
169 root.decreased_value = "";
170 root.child_count = 0;
171 root.custom_accessibility_actions_count = 0;
172 root.flags = static_cast<FlutterSemanticsFlag>(
173 FlutterSemanticsFlag::kFlutterSemanticsFlagHasExpandedState |
174 FlutterSemanticsFlag::kFlutterSemanticsFlagIsExpanded);
175 bridge->AddFlutterSemanticsNodeUpdate(root);
176
177 bridge->CommitUpdates();
178
179 {
180 auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
181 EXPECT_TRUE(root_node->GetData().HasState(ax::mojom::State::kExpanded));
182
183 // Get the IAccessible for the root node.
184 IAccessible* native_view = root_node->GetNativeViewAccessible();
185 ASSERT_TRUE(native_view != nullptr);
186
187 // Look up against the node itself (not one of its children).
188 VARIANT varchild = {};
189 varchild.vt = VT_I4;
190
191 // Verify the submenu is expanded.
192 varchild.lVal = CHILDID_SELF;
193 VARIANT native_state = {};
194 ASSERT_TRUE(SUCCEEDED(native_view->get_accState(varchild, &native_state)));
195 EXPECT_TRUE(native_state.lVal & STATE_SYSTEM_EXPANDED);
196
197 // Perform similar tests for UIA value;
198 IRawElementProviderSimple* uia_node;
199 native_view->QueryInterface(IID_PPV_ARGS(&uia_node));
200 ASSERT_TRUE(SUCCEEDED(uia_node->GetPropertyValue(
201 UIA_ExpandCollapseExpandCollapseStatePropertyId, &native_state)));
202 EXPECT_EQ(native_state.lVal, ExpandCollapseState_Expanded);
203
204 ASSERT_TRUE(SUCCEEDED(uia_node->GetPropertyValue(
205 UIA_AriaPropertiesPropertyId, &native_state)));
206 EXPECT_NE(std::wcsstr(native_state.bstrVal, L"expanded=true"), nullptr);
207 }
208
209 // Test collapsed too.
210 root.flags = static_cast<FlutterSemanticsFlag>(
211 FlutterSemanticsFlag::kFlutterSemanticsFlagHasExpandedState);
212 bridge->AddFlutterSemanticsNodeUpdate(root);
213 bridge->CommitUpdates();
214
215 {
216 auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
217 EXPECT_TRUE(root_node->GetData().HasState(ax::mojom::State::kCollapsed));
218
219 // Get the IAccessible for the root node.
220 IAccessible* native_view = root_node->GetNativeViewAccessible();
221 ASSERT_TRUE(native_view != nullptr);
222
223 // Look up against the node itself (not one of its children).
224 VARIANT varchild = {};
225 varchild.vt = VT_I4;
226
227 // Verify the submenu is collapsed.
228 varchild.lVal = CHILDID_SELF;
229 VARIANT native_state = {};
230 ASSERT_TRUE(SUCCEEDED(native_view->get_accState(varchild, &native_state)));
231 EXPECT_TRUE(native_state.lVal & STATE_SYSTEM_COLLAPSED);
232
233 // Perform similar tests for UIA value;
234 IRawElementProviderSimple* uia_node;
235 native_view->QueryInterface(IID_PPV_ARGS(&uia_node));
236 ASSERT_TRUE(SUCCEEDED(uia_node->GetPropertyValue(
237 UIA_ExpandCollapseExpandCollapseStatePropertyId, &native_state)));
238 EXPECT_EQ(native_state.lVal, ExpandCollapseState_Collapsed);
239
240 ASSERT_TRUE(SUCCEEDED(uia_node->GetPropertyValue(
241 UIA_AriaPropertiesPropertyId, &native_state)));
242 EXPECT_NE(std::wcsstr(native_state.bstrVal, L"expanded=false"), nullptr);
243 }
244 }
245
246 // The view's surface must be destroyed after the engine is shutdown.
247 // See: https://github.com/flutter/flutter/issues/124463
248 TEST(FlutterWindowsViewTest, Shutdown) {
249 std::unique_ptr<MockFlutterWindowsEngine> engine =
250 std::make_unique<MockFlutterWindowsEngine>();
251 auto window_binding_handler =
252 std::make_unique<NiceMock<MockWindowBindingHandler>>();
253 std::unique_ptr<MockAngleSurfaceManager> surface_manager =
254 std::make_unique<MockAngleSurfaceManager>();
255
256 EngineModifier modifier(engine.get());
257 FlutterWindowsView view(std::move(window_binding_handler));
258
259 // The engine must be stopped before the surface can be destroyed.
260 InSequence s;
261 EXPECT_CALL(*engine.get(), Stop).Times(1);
262 EXPECT_CALL(*surface_manager.get(), DestroySurface).Times(1);
263
264 modifier.SetSurfaceManager(surface_manager.release());
265 view.SetEngine(std::move(engine));
266 }
267
268 TEST(FlutterWindowsViewTest, KeySequence) {
269 std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine();
270
271 test_response = false;
272
273 auto window_binding_handler =
274 std::make_unique<NiceMock<MockWindowBindingHandler>>();
275 FlutterWindowsView view(std::move(window_binding_handler));
276 view.SetEngine(std::move(engine));
277
278 view.OnKey(kVirtualKeyA, kScanCodeKeyA, WM_KEYDOWN, 'a', false, false,
279 [](bool handled) {});
280
281 EXPECT_EQ(key_event_logs.size(), 2);
282 EXPECT_EQ(key_event_logs[0], kKeyEventFromEmbedder);
283 EXPECT_EQ(key_event_logs[1], kKeyEventFromChannel);
284
285 key_event_logs.clear();
286 }
287
288 TEST(FlutterWindowsViewTest, EnableSemantics) {
289 std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine();
290 EngineModifier modifier(engine.get());
291
292 bool semantics_enabled = false;
293 modifier.embedder_api().UpdateSemanticsEnabled = MOCK_ENGINE_PROC(
294 UpdateSemanticsEnabled,
295 [&semantics_enabled](FLUTTER_API_SYMBOL(FlutterEngine) engine,
296 bool enabled) {
297 semantics_enabled = enabled;
298 return kSuccess;
299 });
300
301 auto window_binding_handler =
302 std::make_unique<NiceMock<MockWindowBindingHandler>>();
303 FlutterWindowsView view(std::move(window_binding_handler));
304 view.SetEngine(std::move(engine));
305
306 view.OnUpdateSemanticsEnabled(true);
307 EXPECT_TRUE(semantics_enabled);
308 }
309
310 TEST(FlutterWindowsViewTest, AddSemanticsNodeUpdate) {
311 std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine();
312 EngineModifier modifier(engine.get());
313 modifier.embedder_api().UpdateSemanticsEnabled =
314 [](FLUTTER_API_SYMBOL(FlutterEngine) engine, bool enabled) {
315 return kSuccess;
316 };
317
318 auto window_binding_handler =
319 std::make_unique<NiceMock<MockWindowBindingHandler>>();
320 FlutterWindowsView view(std::move(window_binding_handler));
321 view.SetEngine(std::move(engine));
322
323 // Enable semantics to instantiate accessibility bridge.
324 view.OnUpdateSemanticsEnabled(true);
325
326 auto bridge = view.accessibility_bridge().lock();
327 ASSERT_TRUE(bridge);
328
329 // Add root node.
330 FlutterSemanticsNode2 node{sizeof(FlutterSemanticsNode2), 0};
331 node.label = "name";
332 node.value = "value";
333 node.platform_view_id = -1;
334 bridge->AddFlutterSemanticsNodeUpdate(node);
335 bridge->CommitUpdates();
336
337 // Look up the root windows node delegate.
338 auto node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
339 ASSERT_TRUE(node_delegate);
340 EXPECT_EQ(node_delegate->GetChildCount(), 0);
341
342 // Get the native IAccessible object.
343 IAccessible* native_view = node_delegate->GetNativeViewAccessible();
344 ASSERT_TRUE(native_view != nullptr);
345
346 // Property lookups will be made against this node itself.
347 VARIANT varchild{};
348 varchild.vt = VT_I4;
349 varchild.lVal = CHILDID_SELF;
350
351 // Verify node name matches our label.
352 BSTR bname = nullptr;
353 ASSERT_EQ(native_view->get_accName(varchild, &bname), S_OK);
354 std::string name(_com_util::ConvertBSTRToString(bname));
355 EXPECT_EQ(name, "name");
356
357 // Verify node value matches.
358 BSTR bvalue = nullptr;
359 ASSERT_EQ(native_view->get_accValue(varchild, &bvalue), S_OK);
360 std::string value(_com_util::ConvertBSTRToString(bvalue));
361 EXPECT_EQ(value, "value");
362
363 // Verify node type is static text.
364 VARIANT varrole{};
365 varrole.vt = VT_I4;
366 ASSERT_EQ(native_view->get_accRole(varchild, &varrole), S_OK);
367 EXPECT_EQ(varrole.lVal, ROLE_SYSTEM_STATICTEXT);
368
369 // Get the IRawElementProviderFragment object.
370 IRawElementProviderSimple* uia_view;
371 native_view->QueryInterface(IID_PPV_ARGS(&uia_view));
372 ASSERT_TRUE(uia_view != nullptr);
373
374 // Verify name property matches our label.
375 VARIANT varname{};
376 ASSERT_EQ(uia_view->GetPropertyValue(UIA_NamePropertyId, &varname), S_OK);
377 EXPECT_EQ(varname.vt, VT_BSTR);
378 name = _com_util::ConvertBSTRToString(varname.bstrVal);
379 EXPECT_EQ(name, "name");
380
381 // Verify value property matches our label.
382 VARIANT varvalue{};
383 ASSERT_EQ(uia_view->GetPropertyValue(UIA_ValueValuePropertyId, &varvalue),
384 S_OK);
385 EXPECT_EQ(varvalue.vt, VT_BSTR);
386 value = _com_util::ConvertBSTRToString(varvalue.bstrVal);
387 EXPECT_EQ(value, "value");
388
389 // Verify node control type is text.
390 varrole = {};
391 ASSERT_EQ(uia_view->GetPropertyValue(UIA_ControlTypePropertyId, &varrole),
392 S_OK);
393 EXPECT_EQ(varrole.vt, VT_I4);
394 EXPECT_EQ(varrole.lVal, UIA_TextControlTypeId);
395 }
396
397 // Verify the native IAccessible COM object tree is an accurate reflection of
398 // the platform-agnostic tree. Verify both a root node with children as well as
399 // a non-root node with children, since the AX tree includes special handling
400 // for the root.
401 //
402 // node0
403 // / \
404 // node1 node2
405 // |
406 // node3
407 //
408 // node0 and node2 are grouping nodes. node1 and node2 are static text nodes.
409 TEST(FlutterWindowsViewTest, AddSemanticsNodeUpdateWithChildren) {
410 std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine();
411 EngineModifier modifier(engine.get());
412 modifier.embedder_api().UpdateSemanticsEnabled =
413 [](FLUTTER_API_SYMBOL(FlutterEngine) engine, bool enabled) {
414 return kSuccess;
415 };
416
417 auto window_binding_handler =
418 std::make_unique<NiceMock<MockWindowBindingHandler>>();
419 FlutterWindowsView view(std::move(window_binding_handler));
420 view.SetEngine(std::move(engine));
421
422 // Enable semantics to instantiate accessibility bridge.
423 view.OnUpdateSemanticsEnabled(true);
424
425 auto bridge = view.accessibility_bridge().lock();
426 ASSERT_TRUE(bridge);
427
428 // Add root node.
429 FlutterSemanticsNode2 node0{sizeof(FlutterSemanticsNode2), 0};
430 std::vector<int32_t> node0_children{1, 2};
431 node0.child_count = node0_children.size();
432 node0.children_in_traversal_order = node0_children.data();
433 node0.children_in_hit_test_order = node0_children.data();
434
435 FlutterSemanticsNode2 node1{sizeof(FlutterSemanticsNode2), 1};
436 node1.label = "prefecture";
437 node1.value = "Kyoto";
438 FlutterSemanticsNode2 node2{sizeof(FlutterSemanticsNode2), 2};
439 std::vector<int32_t> node2_children{3};
440 node2.child_count = node2_children.size();
441 node2.children_in_traversal_order = node2_children.data();
442 node2.children_in_hit_test_order = node2_children.data();
443 FlutterSemanticsNode2 node3{sizeof(FlutterSemanticsNode2), 3};
444 node3.label = "city";
445 node3.value = "Uji";
446
447 bridge->AddFlutterSemanticsNodeUpdate(node0);
448 bridge->AddFlutterSemanticsNodeUpdate(node1);
449 bridge->AddFlutterSemanticsNodeUpdate(node2);
450 bridge->AddFlutterSemanticsNodeUpdate(node3);
451 bridge->CommitUpdates();
452
453 // Look up the root windows node delegate.
454 auto node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
455 ASSERT_TRUE(node_delegate);
456 EXPECT_EQ(node_delegate->GetChildCount(), 2);
457
458 // Get the native IAccessible object.
459 IAccessible* node0_accessible = node_delegate->GetNativeViewAccessible();
460 ASSERT_TRUE(node0_accessible != nullptr);
461
462 // Property lookups will be made against this node itself.
463 VARIANT varchild{};
464 varchild.vt = VT_I4;
465 varchild.lVal = CHILDID_SELF;
466
467 // Verify node type is a group.
468 VARIANT varrole{};
469 varrole.vt = VT_I4;
470 ASSERT_EQ(node0_accessible->get_accRole(varchild, &varrole), S_OK);
471 EXPECT_EQ(varrole.lVal, ROLE_SYSTEM_GROUPING);
472
473 // Verify child count.
474 long node0_child_count = 0;
475 ASSERT_EQ(node0_accessible->get_accChildCount(&node0_child_count), S_OK);
476 EXPECT_EQ(node0_child_count, 2);
477
478 {
479 // Look up first child of node0 (node1), a static text node.
480 varchild.lVal = 1;
481 IDispatch* node1_dispatch = nullptr;
482 ASSERT_EQ(node0_accessible->get_accChild(varchild, &node1_dispatch), S_OK);
483 ASSERT_TRUE(node1_dispatch != nullptr);
484 IAccessible* node1_accessible = nullptr;
485 ASSERT_EQ(node1_dispatch->QueryInterface(
486 IID_IAccessible, reinterpret_cast<void**>(&node1_accessible)),
487 S_OK);
488 ASSERT_TRUE(node1_accessible != nullptr);
489
490 // Verify node name matches our label.
491 varchild.lVal = CHILDID_SELF;
492 BSTR bname = nullptr;
493 ASSERT_EQ(node1_accessible->get_accName(varchild, &bname), S_OK);
494 std::string name(_com_util::ConvertBSTRToString(bname));
495 EXPECT_EQ(name, "prefecture");
496
497 // Verify node value matches.
498 BSTR bvalue = nullptr;
499 ASSERT_EQ(node1_accessible->get_accValue(varchild, &bvalue), S_OK);
500 std::string value(_com_util::ConvertBSTRToString(bvalue));
501 EXPECT_EQ(value, "Kyoto");
502
503 // Verify node type is static text.
504 VARIANT varrole{};
505 varrole.vt = VT_I4;
506 ASSERT_EQ(node1_accessible->get_accRole(varchild, &varrole), S_OK);
507 EXPECT_EQ(varrole.lVal, ROLE_SYSTEM_STATICTEXT);
508
509 // Verify the parent node is the root.
510 IDispatch* parent_dispatch;
511 node1_accessible->get_accParent(&parent_dispatch);
512 IAccessible* parent_accessible;
513 ASSERT_EQ(
514 parent_dispatch->QueryInterface(
515 IID_IAccessible, reinterpret_cast<void**>(&parent_accessible)),
516 S_OK);
517 EXPECT_EQ(parent_accessible, node0_accessible);
518 }
519
520 // Look up second child of node0 (node2), a parent group for node3.
521 varchild.lVal = 2;
522 IDispatch* node2_dispatch = nullptr;
523 ASSERT_EQ(node0_accessible->get_accChild(varchild, &node2_dispatch), S_OK);
524 ASSERT_TRUE(node2_dispatch != nullptr);
525 IAccessible* node2_accessible = nullptr;
526 ASSERT_EQ(node2_dispatch->QueryInterface(
527 IID_IAccessible, reinterpret_cast<void**>(&node2_accessible)),
528 S_OK);
529 ASSERT_TRUE(node2_accessible != nullptr);
530
531 {
532 // Verify child count.
533 long node2_child_count = 0;
534 ASSERT_EQ(node2_accessible->get_accChildCount(&node2_child_count), S_OK);
535 EXPECT_EQ(node2_child_count, 1);
536
537 // Verify node type is static text.
538 varchild.lVal = CHILDID_SELF;
539 VARIANT varrole{};
540 varrole.vt = VT_I4;
541 ASSERT_EQ(node2_accessible->get_accRole(varchild, &varrole), S_OK);
542 EXPECT_EQ(varrole.lVal, ROLE_SYSTEM_GROUPING);
543
544 // Verify the parent node is the root.
545 IDispatch* parent_dispatch;
546 node2_accessible->get_accParent(&parent_dispatch);
547 IAccessible* parent_accessible;
548 ASSERT_EQ(
549 parent_dispatch->QueryInterface(
550 IID_IAccessible, reinterpret_cast<void**>(&parent_accessible)),
551 S_OK);
552 EXPECT_EQ(parent_accessible, node0_accessible);
553 }
554
555 {
556 // Look up only child of node2 (node3), a static text node.
557 varchild.lVal = 1;
558 IDispatch* node3_dispatch = nullptr;
559 ASSERT_EQ(node2_accessible->get_accChild(varchild, &node3_dispatch), S_OK);
560 ASSERT_TRUE(node3_dispatch != nullptr);
561 IAccessible* node3_accessible = nullptr;
562 ASSERT_EQ(node3_dispatch->QueryInterface(
563 IID_IAccessible, reinterpret_cast<void**>(&node3_accessible)),
564 S_OK);
565 ASSERT_TRUE(node3_accessible != nullptr);
566
567 // Verify node name matches our label.
568 varchild.lVal = CHILDID_SELF;
569 BSTR bname = nullptr;
570 ASSERT_EQ(node3_accessible->get_accName(varchild, &bname), S_OK);
571 std::string name(_com_util::ConvertBSTRToString(bname));
572 EXPECT_EQ(name, "city");
573
574 // Verify node value matches.
575 BSTR bvalue = nullptr;
576 ASSERT_EQ(node3_accessible->get_accValue(varchild, &bvalue), S_OK);
577 std::string value(_com_util::ConvertBSTRToString(bvalue));
578 EXPECT_EQ(value, "Uji");
579
580 // Verify node type is static text.
581 VARIANT varrole{};
582 varrole.vt = VT_I4;
583 ASSERT_EQ(node3_accessible->get_accRole(varchild, &varrole), S_OK);
584 EXPECT_EQ(varrole.lVal, ROLE_SYSTEM_STATICTEXT);
585
586 // Verify the parent node is node2.
587 IDispatch* parent_dispatch;
588 node3_accessible->get_accParent(&parent_dispatch);
589 IAccessible* parent_accessible;
590 ASSERT_EQ(
591 parent_dispatch->QueryInterface(
592 IID_IAccessible, reinterpret_cast<void**>(&parent_accessible)),
593 S_OK);
594 EXPECT_EQ(parent_accessible, node2_accessible);
595 }
596 }
597
598 // Flutter used to assume that the accessibility root had ID 0.
599 // In a multi-view world, each view has its own accessibility root
600 // with a globally unique node ID.
601 //
602 // node1
603 // |
604 // node2
605 //
606 // node1 is a grouping node, node0 is a static text node.
607 TEST(FlutterWindowsViewTest, NonZeroSemanticsRoot) {
608 std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine();
609 EngineModifier modifier(engine.get());
610 modifier.embedder_api().UpdateSemanticsEnabled =
611 [](FLUTTER_API_SYMBOL(FlutterEngine) engine, bool enabled) {
612 return kSuccess;
613 };
614
615 auto window_binding_handler =
616 std::make_unique<NiceMock<MockWindowBindingHandler>>();
617 FlutterWindowsView view(std::move(window_binding_handler));
618 view.SetEngine(std::move(engine));
619
620 // Enable semantics to instantiate accessibility bridge.
621 view.OnUpdateSemanticsEnabled(true);
622
623 auto bridge = view.accessibility_bridge().lock();
624 ASSERT_TRUE(bridge);
625
626 // Add root node.
627 FlutterSemanticsNode2 node1{sizeof(FlutterSemanticsNode2), 1};
628 std::vector<int32_t> node1_children{2};
629 node1.child_count = node1_children.size();
630 node1.children_in_traversal_order = node1_children.data();
631 node1.children_in_hit_test_order = node1_children.data();
632
633 FlutterSemanticsNode2 node2{sizeof(FlutterSemanticsNode2), 2};
634 node2.label = "prefecture";
635 node2.value = "Kyoto";
636
637 bridge->AddFlutterSemanticsNodeUpdate(node1);
638 bridge->AddFlutterSemanticsNodeUpdate(node2);
639 bridge->CommitUpdates();
640
641 // Look up the root windows node delegate.
642 auto root_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock();
643 ASSERT_TRUE(root_delegate);
644 EXPECT_EQ(root_delegate->GetChildCount(), 1);
645
646 // Look up the child node delegate
647 auto child_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(2).lock();
648 ASSERT_TRUE(child_delegate);
649 EXPECT_EQ(child_delegate->GetChildCount(), 0);
650
651 // Ensure a node with ID 0 does not exist.
652 auto fake_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
653 ASSERT_FALSE(fake_delegate);
654
655 // Get the root's native IAccessible object.
656 IAccessible* node1_accessible = root_delegate->GetNativeViewAccessible();
657 ASSERT_TRUE(node1_accessible != nullptr);
658
659 // Property lookups will be made against this node itself.
660 VARIANT varchild{};
661 varchild.vt = VT_I4;
662 varchild.lVal = CHILDID_SELF;
663
664 // Verify node type is a group.
665 VARIANT varrole{};
666 varrole.vt = VT_I4;
667 ASSERT_EQ(node1_accessible->get_accRole(varchild, &varrole), S_OK);
668 EXPECT_EQ(varrole.lVal, ROLE_SYSTEM_GROUPING);
669
670 // Verify child count.
671 long node1_child_count = 0;
672 ASSERT_EQ(node1_accessible->get_accChildCount(&node1_child_count), S_OK);
673 EXPECT_EQ(node1_child_count, 1);
674
675 {
676 // Look up first child of node1 (node0), a static text node.
677 varchild.lVal = 1;
678 IDispatch* node2_dispatch = nullptr;
679 ASSERT_EQ(node1_accessible->get_accChild(varchild, &node2_dispatch), S_OK);
680 ASSERT_TRUE(node2_dispatch != nullptr);
681 IAccessible* node2_accessible = nullptr;
682 ASSERT_EQ(node2_dispatch->QueryInterface(
683 IID_IAccessible, reinterpret_cast<void**>(&node2_accessible)),
684 S_OK);
685 ASSERT_TRUE(node2_accessible != nullptr);
686
687 // Verify node name matches our label.
688 varchild.lVal = CHILDID_SELF;
689 BSTR bname = nullptr;
690 ASSERT_EQ(node2_accessible->get_accName(varchild, &bname), S_OK);
691 std::string name(_com_util::ConvertBSTRToString(bname));
692 EXPECT_EQ(name, "prefecture");
693
694 // Verify node value matches.
695 BSTR bvalue = nullptr;
696 ASSERT_EQ(node2_accessible->get_accValue(varchild, &bvalue), S_OK);
697 std::string value(_com_util::ConvertBSTRToString(bvalue));
698 EXPECT_EQ(value, "Kyoto");
699
700 // Verify node type is static text.
701 VARIANT varrole{};
702 varrole.vt = VT_I4;
703 ASSERT_EQ(node2_accessible->get_accRole(varchild, &varrole), S_OK);
704 EXPECT_EQ(varrole.lVal, ROLE_SYSTEM_STATICTEXT);
705
706 // Verify the parent node is the root.
707 IDispatch* parent_dispatch;
708 node2_accessible->get_accParent(&parent_dispatch);
709 IAccessible* parent_accessible;
710 ASSERT_EQ(
711 parent_dispatch->QueryInterface(
712 IID_IAccessible, reinterpret_cast<void**>(&parent_accessible)),
713 S_OK);
714 EXPECT_EQ(parent_accessible, node1_accessible);
715 }
716 }
717
718 // Verify the native IAccessible accHitTest method returns the correct
719 // IAccessible COM object for the given coordinates.
720 //
721 // +-----------+
722 // | | |
723 // node0 | | B |
724 // / \ | A |-----|
725 // node1 node2 | | C |
726 // | | | |
727 // node3 +-----------+
728 //
729 // node0 and node2 are grouping nodes. node1 and node2 are static text nodes.
730 //
731 // node0 is located at 0,0 with size 500x500. It spans areas A, B, and C.
732 // node1 is located at 0,0 with size 250x500. It spans area A.
733 // node2 is located at 250,0 with size 250x500. It spans areas B and C.
734 // node3 is located at 250,250 with size 250x250. It spans area C.
735 TEST(FlutterWindowsViewTest, AccessibilityHitTesting) {
736 constexpr FlutterTransformation kIdentityTransform = {1, 0, 0, //
737 0, 1, 0, //
738 0, 0, 1};
739
740 std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine();
741 EngineModifier modifier(engine.get());
742 modifier.embedder_api().UpdateSemanticsEnabled =
743 [](FLUTTER_API_SYMBOL(FlutterEngine) engine, bool enabled) {
744 return kSuccess;
745 };
746
747 auto window_binding_handler =
748 std::make_unique<NiceMock<MockWindowBindingHandler>>();
749 FlutterWindowsView view(std::move(window_binding_handler));
750 view.SetEngine(std::move(engine));
751
752 // Enable semantics to instantiate accessibility bridge.
753 view.OnUpdateSemanticsEnabled(true);
754
755 auto bridge = view.accessibility_bridge().lock();
756 ASSERT_TRUE(bridge);
757
758 // Add root node at origin. Size 500x500.
759 FlutterSemanticsNode2 node0{sizeof(FlutterSemanticsNode2), 0};
760 std::vector<int32_t> node0_children{1, 2};
761 node0.rect = {0, 0, 500, 500};
762 node0.transform = kIdentityTransform;
763 node0.child_count = node0_children.size();
764 node0.children_in_traversal_order = node0_children.data();
765 node0.children_in_hit_test_order = node0_children.data();
766
767 // Add node 1 located at 0,0 relative to node 0. Size 250x500.
768 FlutterSemanticsNode2 node1{sizeof(FlutterSemanticsNode2), 1};
769 node1.rect = {0, 0, 250, 500};
770 node1.transform = kIdentityTransform;
771 node1.label = "prefecture";
772 node1.value = "Kyoto";
773
774 // Add node 2 located at 250,0 relative to node 0. Size 250x500.
775 FlutterSemanticsNode2 node2{sizeof(FlutterSemanticsNode2), 2};
776 std::vector<int32_t> node2_children{3};
777 node2.rect = {0, 0, 250, 500};
778 node2.transform = {1, 0, 250, 0, 1, 0, 0, 0, 1};
779 node2.child_count = node2_children.size();
780 node2.children_in_traversal_order = node2_children.data();
781 node2.children_in_hit_test_order = node2_children.data();
782
783 // Add node 3 located at 0,250 relative to node 2. Size 250, 250.
784 FlutterSemanticsNode2 node3{sizeof(FlutterSemanticsNode2), 3};
785 node3.rect = {0, 0, 250, 250};
786 node3.transform = {1, 0, 0, 0, 1, 250, 0, 0, 1};
787 node3.label = "city";
788 node3.value = "Uji";
789
790 bridge->AddFlutterSemanticsNodeUpdate(node0);
791 bridge->AddFlutterSemanticsNodeUpdate(node1);
792 bridge->AddFlutterSemanticsNodeUpdate(node2);
793 bridge->AddFlutterSemanticsNodeUpdate(node3);
794 bridge->CommitUpdates();
795
796 // Look up the root windows node delegate.
797 auto node0_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
798 ASSERT_TRUE(node0_delegate);
799 auto node1_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock();
800 ASSERT_TRUE(node1_delegate);
801 auto node2_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(2).lock();
802 ASSERT_TRUE(node2_delegate);
803 auto node3_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(3).lock();
804 ASSERT_TRUE(node3_delegate);
805
806 // Get the native IAccessible root object.
807 IAccessible* node0_accessible = node0_delegate->GetNativeViewAccessible();
808 ASSERT_TRUE(node0_accessible != nullptr);
809
810 // Perform a hit test that should hit node 1.
811 VARIANT varchild{};
812 ASSERT_TRUE(SUCCEEDED(node0_accessible->accHitTest(150, 150, &varchild)));
813 EXPECT_EQ(varchild.vt, VT_DISPATCH);
814 EXPECT_EQ(varchild.pdispVal, node1_delegate->GetNativeViewAccessible());
815
816 // Perform a hit test that should hit node 2.
817 varchild = {};
818 ASSERT_TRUE(SUCCEEDED(node0_accessible->accHitTest(450, 150, &varchild)));
819 EXPECT_EQ(varchild.vt, VT_DISPATCH);
820 EXPECT_EQ(varchild.pdispVal, node2_delegate->GetNativeViewAccessible());
821
822 // Perform a hit test that should hit node 3.
823 varchild = {};
824 ASSERT_TRUE(SUCCEEDED(node0_accessible->accHitTest(450, 450, &varchild)));
825 EXPECT_EQ(varchild.vt, VT_DISPATCH);
826 EXPECT_EQ(varchild.pdispVal, node3_delegate->GetNativeViewAccessible());
827 }
828
829 TEST(FlutterWindowsViewTest, WindowResizeTests) {
830 std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine();
831 EngineModifier modifier(engine.get());
832
833 auto window_binding_handler =
834 std::make_unique<NiceMock<MockWindowBindingHandler>>();
835 std::unique_ptr<MockAngleSurfaceManager> surface_manager =
836 std::make_unique<MockAngleSurfaceManager>();
837
838 EXPECT_CALL(*window_binding_handler.get(), NeedsVSync)
839 .WillOnce(Return(false));
840 EXPECT_CALL(
841 *surface_manager.get(),
842 ResizeSurface(_, /*width=*/500, /*height=*/500, /*enable_vsync=*/false))
843 .Times(1);
844 EXPECT_CALL(*surface_manager.get(), DestroySurface).Times(1);
845
846 FlutterWindowsView view(std::move(window_binding_handler));
847 modifier.SetSurfaceManager(surface_manager.release());
848 view.SetEngine(std::move(engine));
849
850 fml::AutoResetWaitableEvent metrics_sent_latch;
851 modifier.embedder_api().SendWindowMetricsEvent = MOCK_ENGINE_PROC(
852 SendWindowMetricsEvent,
853 ([&metrics_sent_latch](auto engine,
854 const FlutterWindowMetricsEvent* event) {
855 metrics_sent_latch.Signal();
856 return kSuccess;
857 }));
858
859 fml::AutoResetWaitableEvent resized_latch;
860 std::thread([&resized_latch, &view]() {
861 // Start the window resize. This sends the new window metrics
862 // and then blocks until another thread completes the window resize.
863 view.OnWindowSizeChanged(500, 500);
864 resized_latch.Signal();
865 }).detach();
866
867 // Wait until the platform thread has started the window resize.
868 metrics_sent_latch.Wait();
869
870 // Complete the window resize by requesting a buffer with the new window size.
871 view.GetFrameBufferId(500, 500);
872 resized_latch.Wait();
873 }
874
875 TEST(FlutterWindowsViewTest, WindowRepaintTests) {
876 std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine();
877 EngineModifier modifier(engine.get());
878
879 FlutterWindowsView view(std::make_unique<flutter::FlutterWindow>(100, 100));
880 view.SetEngine(std::move(engine));
881 view.CreateRenderSurface();
882
883 bool schedule_frame_called = false;
884 modifier.embedder_api().ScheduleFrame =
885 MOCK_ENGINE_PROC(ScheduleFrame, ([&schedule_frame_called](auto engine) {
886 schedule_frame_called = true;
887 return kSuccess;
888 }));
889
890 view.OnWindowRepaint();
891 EXPECT_TRUE(schedule_frame_called);
892 }
893
894 // Ensure that checkboxes have their checked status set apropriately
895 // Previously, only Radios could have this flag updated
896 // Resulted in the issue seen at
897 // https://github.com/flutter/flutter/issues/96218
898 // This test ensures that the native state of Checkboxes on Windows,
899 // specifically, is updated as desired.
900 TEST(FlutterWindowsViewTest, CheckboxNativeState) {
901 std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine();
902 EngineModifier modifier(engine.get());
903 modifier.embedder_api().UpdateSemanticsEnabled =
904 [](FLUTTER_API_SYMBOL(FlutterEngine) engine, bool enabled) {
905 return kSuccess;
906 };
907
908 auto window_binding_handler =
909 std::make_unique<NiceMock<MockWindowBindingHandler>>();
910 FlutterWindowsView view(std::move(window_binding_handler));
911 view.SetEngine(std::move(engine));
912
913 // Enable semantics to instantiate accessibility bridge.
914 view.OnUpdateSemanticsEnabled(true);
915
916 auto bridge = view.accessibility_bridge().lock();
917 ASSERT_TRUE(bridge);
918
919 FlutterSemanticsNode2 root{sizeof(FlutterSemanticsNode2), 0};
920 root.id = 0;
921 root.label = "root";
922 root.hint = "";
923 root.value = "";
924 root.increased_value = "";
925 root.decreased_value = "";
926 root.child_count = 0;
927 root.custom_accessibility_actions_count = 0;
928 root.flags = static_cast<FlutterSemanticsFlag>(
929 FlutterSemanticsFlag::kFlutterSemanticsFlagHasCheckedState |
930 FlutterSemanticsFlag::kFlutterSemanticsFlagIsChecked);
931 bridge->AddFlutterSemanticsNodeUpdate(root);
932
933 bridge->CommitUpdates();
934
935 {
936 auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
937 EXPECT_EQ(root_node->GetData().role, ax::mojom::Role::kCheckBox);
938 EXPECT_EQ(root_node->GetData().GetCheckedState(),
939 ax::mojom::CheckedState::kTrue);
940
941 // Get the IAccessible for the root node.
942 IAccessible* native_view = root_node->GetNativeViewAccessible();
943 ASSERT_TRUE(native_view != nullptr);
944
945 // Look up against the node itself (not one of its children).
946 VARIANT varchild = {};
947 varchild.vt = VT_I4;
948
949 // Verify the checkbox is checked.
950 varchild.lVal = CHILDID_SELF;
951 VARIANT native_state = {};
952 ASSERT_TRUE(SUCCEEDED(native_view->get_accState(varchild, &native_state)));
953 EXPECT_TRUE(native_state.lVal & STATE_SYSTEM_CHECKED);
954
955 // Perform similar tests for UIA value;
956 IRawElementProviderSimple* uia_node;
957 native_view->QueryInterface(IID_PPV_ARGS(&uia_node));
958 ASSERT_TRUE(SUCCEEDED(uia_node->GetPropertyValue(
959 UIA_ToggleToggleStatePropertyId, &native_state)));
960 EXPECT_EQ(native_state.lVal, ToggleState_On);
961
962 ASSERT_TRUE(SUCCEEDED(uia_node->GetPropertyValue(
963 UIA_AriaPropertiesPropertyId, &native_state)));
964 EXPECT_NE(std::wcsstr(native_state.bstrVal, L"checked=true"), nullptr);
965 }
966
967 // Test unchecked too.
968 root.flags = static_cast<FlutterSemanticsFlag>(
969 FlutterSemanticsFlag::kFlutterSemanticsFlagHasCheckedState);
970 bridge->AddFlutterSemanticsNodeUpdate(root);
971 bridge->CommitUpdates();
972
973 {
974 auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
975 EXPECT_EQ(root_node->GetData().role, ax::mojom::Role::kCheckBox);
976 EXPECT_EQ(root_node->GetData().GetCheckedState(),
977 ax::mojom::CheckedState::kFalse);
978
979 // Get the IAccessible for the root node.
980 IAccessible* native_view = root_node->GetNativeViewAccessible();
981 ASSERT_TRUE(native_view != nullptr);
982
983 // Look up against the node itself (not one of its children).
984 VARIANT varchild = {};
985 varchild.vt = VT_I4;
986
987 // Verify the checkbox is unchecked.
988 varchild.lVal = CHILDID_SELF;
989 VARIANT native_state = {};
990 ASSERT_TRUE(SUCCEEDED(native_view->get_accState(varchild, &native_state)));
991 EXPECT_FALSE(native_state.lVal & STATE_SYSTEM_CHECKED);
992
993 // Perform similar tests for UIA value;
994 IRawElementProviderSimple* uia_node;
995 native_view->QueryInterface(IID_PPV_ARGS(&uia_node));
996 ASSERT_TRUE(SUCCEEDED(uia_node->GetPropertyValue(
997 UIA_ToggleToggleStatePropertyId, &native_state)));
998 EXPECT_EQ(native_state.lVal, ToggleState_Off);
999
1000 ASSERT_TRUE(SUCCEEDED(uia_node->GetPropertyValue(
1001 UIA_AriaPropertiesPropertyId, &native_state)));
1002 EXPECT_NE(std::wcsstr(native_state.bstrVal, L"checked=false"), nullptr);
1003 }
1004
1005 // Now check mixed state.
1006 root.flags = static_cast<FlutterSemanticsFlag>(
1007 FlutterSemanticsFlag::kFlutterSemanticsFlagHasCheckedState |
1008 FlutterSemanticsFlag::kFlutterSemanticsFlagIsCheckStateMixed);
1009 bridge->AddFlutterSemanticsNodeUpdate(root);
1010 bridge->CommitUpdates();
1011
1012 {
1013 auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
1014 EXPECT_EQ(root_node->GetData().role, ax::mojom::Role::kCheckBox);
1015 EXPECT_EQ(root_node->GetData().GetCheckedState(),
1016 ax::mojom::CheckedState::kMixed);
1017
1018 // Get the IAccessible for the root node.
1019 IAccessible* native_view = root_node->GetNativeViewAccessible();
1020 ASSERT_TRUE(native_view != nullptr);
1021
1022 // Look up against the node itself (not one of its children).
1023 VARIANT varchild = {};
1024 varchild.vt = VT_I4;
1025
1026 // Verify the checkbox is mixed.
1027 varchild.lVal = CHILDID_SELF;
1028 VARIANT native_state = {};
1029 ASSERT_TRUE(SUCCEEDED(native_view->get_accState(varchild, &native_state)));
1030 EXPECT_TRUE(native_state.lVal & STATE_SYSTEM_MIXED);
1031
1032 // Perform similar tests for UIA value;
1033 IRawElementProviderSimple* uia_node;
1034 native_view->QueryInterface(IID_PPV_ARGS(&uia_node));
1035 ASSERT_TRUE(SUCCEEDED(uia_node->GetPropertyValue(
1036 UIA_ToggleToggleStatePropertyId, &native_state)));
1037 EXPECT_EQ(native_state.lVal, ToggleState_Indeterminate);
1038
1039 ASSERT_TRUE(SUCCEEDED(uia_node->GetPropertyValue(
1040 UIA_AriaPropertiesPropertyId, &native_state)));
1041 EXPECT_NE(std::wcsstr(native_state.bstrVal, L"checked=mixed"), nullptr);
1042 }
1043 }
1044
1045 // Ensure that switches have their toggle status set apropriately
1046 TEST(FlutterWindowsViewTest, SwitchNativeState) {
1047 std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine();
1048 EngineModifier modifier(engine.get());
1049 modifier.embedder_api().UpdateSemanticsEnabled =
1050 [](FLUTTER_API_SYMBOL(FlutterEngine) engine, bool enabled) {
1051 return kSuccess;
1052 };
1053
1054 auto window_binding_handler =
1055 std::make_unique<NiceMock<MockWindowBindingHandler>>();
1056 FlutterWindowsView view(std::move(window_binding_handler));
1057 view.SetEngine(std::move(engine));
1058
1059 // Enable semantics to instantiate accessibility bridge.
1060 view.OnUpdateSemanticsEnabled(true);
1061
1062 auto bridge = view.accessibility_bridge().lock();
1063 ASSERT_TRUE(bridge);
1064
1065 FlutterSemanticsNode2 root{sizeof(FlutterSemanticsNode2), 0};
1066 root.id = 0;
1067 root.label = "root";
1068 root.hint = "";
1069 root.value = "";
1070 root.increased_value = "";
1071 root.decreased_value = "";
1072 root.child_count = 0;
1073 root.custom_accessibility_actions_count = 0;
1074 root.flags = static_cast<FlutterSemanticsFlag>(
1075 FlutterSemanticsFlag::kFlutterSemanticsFlagHasToggledState |
1076 FlutterSemanticsFlag::kFlutterSemanticsFlagIsToggled);
1077 bridge->AddFlutterSemanticsNodeUpdate(root);
1078
1079 bridge->CommitUpdates();
1080
1081 {
1082 auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
1083 EXPECT_EQ(root_node->GetData().role, ax::mojom::Role::kToggleButton);
1084 EXPECT_EQ(root_node->GetData().GetCheckedState(),
1085 ax::mojom::CheckedState::kTrue);
1086
1087 // Get the IAccessible for the root node.
1088 IAccessible* native_view = root_node->GetNativeViewAccessible();
1089 ASSERT_TRUE(native_view != nullptr);
1090
1091 // Look up against the node itself (not one of its children).
1092 VARIANT varchild = {};
1093 varchild.vt = VT_I4;
1094
1095 varchild.lVal = CHILDID_SELF;
1096 VARIANT varrole = {};
1097
1098 // Verify the role of the switch is CHECKBUTTON
1099 ASSERT_EQ(native_view->get_accRole(varchild, &varrole), S_OK);
1100 ASSERT_EQ(varrole.lVal, ROLE_SYSTEM_CHECKBUTTON);
1101
1102 // Verify the switch is pressed.
1103 VARIANT native_state = {};
1104 ASSERT_TRUE(SUCCEEDED(native_view->get_accState(varchild, &native_state)));
1105 EXPECT_TRUE(native_state.lVal & STATE_SYSTEM_PRESSED);
1106
1107 // Test similarly on UIA node.
1108 IRawElementProviderSimple* uia_node;
1109 native_view->QueryInterface(IID_PPV_ARGS(&uia_node));
1110 ASSERT_EQ(uia_node->GetPropertyValue(UIA_ControlTypePropertyId, &varrole),
1111 S_OK);
1112 EXPECT_EQ(varrole.lVal, UIA_ButtonControlTypeId);
1113 ASSERT_EQ(uia_node->GetPropertyValue(UIA_ToggleToggleStatePropertyId,
1114 &native_state),
1115 S_OK);
1116 EXPECT_EQ(native_state.lVal, ToggleState_On);
1117 ASSERT_EQ(
1118 uia_node->GetPropertyValue(UIA_AriaPropertiesPropertyId, &native_state),
1119 S_OK);
1120 EXPECT_NE(std::wcsstr(native_state.bstrVal, L"pressed=true"), nullptr);
1121 }
1122
1123 // Test unpressed too.
1124 root.flags = static_cast<FlutterSemanticsFlag>(
1125 FlutterSemanticsFlag::kFlutterSemanticsFlagHasToggledState);
1126 bridge->AddFlutterSemanticsNodeUpdate(root);
1127 bridge->CommitUpdates();
1128
1129 {
1130 auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
1131 EXPECT_EQ(root_node->GetData().role, ax::mojom::Role::kToggleButton);
1132 EXPECT_EQ(root_node->GetData().GetCheckedState(),
1133 ax::mojom::CheckedState::kFalse);
1134
1135 // Get the IAccessible for the root node.
1136 IAccessible* native_view = root_node->GetNativeViewAccessible();
1137 ASSERT_TRUE(native_view != nullptr);
1138
1139 // Look up against the node itself (not one of its children).
1140 VARIANT varchild = {};
1141 varchild.vt = VT_I4;
1142
1143 // Verify the switch is not pressed.
1144 varchild.lVal = CHILDID_SELF;
1145 VARIANT native_state = {};
1146 ASSERT_TRUE(SUCCEEDED(native_view->get_accState(varchild, &native_state)));
1147 EXPECT_FALSE(native_state.lVal & STATE_SYSTEM_PRESSED);
1148
1149 // Test similarly on UIA node.
1150 IRawElementProviderSimple* uia_node;
1151 native_view->QueryInterface(IID_PPV_ARGS(&uia_node));
1152 ASSERT_EQ(uia_node->GetPropertyValue(UIA_ToggleToggleStatePropertyId,
1153 &native_state),
1154 S_OK);
1155 EXPECT_EQ(native_state.lVal, ToggleState_Off);
1156 ASSERT_EQ(
1157 uia_node->GetPropertyValue(UIA_AriaPropertiesPropertyId, &native_state),
1158 S_OK);
1159 EXPECT_NE(std::wcsstr(native_state.bstrVal, L"pressed=false"), nullptr);
1160 }
1161 }
1162
1163 TEST(FlutterWindowsViewTest, TooltipNodeData) {
1164 std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine();
1165 EngineModifier modifier(engine.get());
1166 modifier.embedder_api().UpdateSemanticsEnabled =
1167 [](FLUTTER_API_SYMBOL(FlutterEngine) engine, bool enabled) {
1168 return kSuccess;
1169 };
1170
1171 auto window_binding_handler =
1172 std::make_unique<NiceMock<MockWindowBindingHandler>>();
1173 FlutterWindowsView view(std::move(window_binding_handler));
1174 view.SetEngine(std::move(engine));
1175
1176 // Enable semantics to instantiate accessibility bridge.
1177 view.OnUpdateSemanticsEnabled(true);
1178
1179 auto bridge = view.accessibility_bridge().lock();
1180 ASSERT_TRUE(bridge);
1181
1182 FlutterSemanticsNode2 root{sizeof(FlutterSemanticsNode2), 0};
1183 root.id = 0;
1184 root.label = "root";
1185 root.hint = "";
1186 root.value = "";
1187 root.increased_value = "";
1188 root.decreased_value = "";
1189 root.tooltip = "tooltip";
1190 root.child_count = 0;
1191 root.custom_accessibility_actions_count = 0;
1192 root.flags = static_cast<FlutterSemanticsFlag>(
1193 FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField);
1194 bridge->AddFlutterSemanticsNodeUpdate(root);
1195
1196 bridge->CommitUpdates();
1197 auto root_node = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
1198 std::string tooltip = root_node->GetData().GetStringAttribute(
1199 ax::mojom::StringAttribute::kTooltip);
1200 EXPECT_EQ(tooltip, "tooltip");
1201
1202 // Check that MSAA name contains the tooltip.
1203 IAccessible* native_view = bridge->GetFlutterPlatformNodeDelegateFromID(0)
1204 .lock()
1205 ->GetNativeViewAccessible();
1206 VARIANT varchild = {.vt = VT_I4, .lVal = CHILDID_SELF};
1207 BSTR bname;
1208 ASSERT_EQ(native_view->get_accName(varchild, &bname), S_OK);
1209 EXPECT_NE(std::wcsstr(bname, L"tooltip"), nullptr);
1210
1211 // Check that UIA help text is equal to the tooltip.
1212 IRawElementProviderSimple* uia_node;
1213 native_view->QueryInterface(IID_PPV_ARGS(&uia_node));
1214 VARIANT varname{};
1215 ASSERT_EQ(uia_node->GetPropertyValue(UIA_HelpTextPropertyId, &varname), S_OK);
1216 std::string uia_tooltip = _com_util::ConvertBSTRToString(varname.bstrVal);
1217 EXPECT_EQ(uia_tooltip, "tooltip");
1218 }
1219
1220 // Don't block until the v-blank if it is disabled by the window.
1221 TEST(FlutterWindowsViewTest, DisablesVSync) {
1222 std::unique_ptr<MockFlutterWindowsEngine> engine =
1223 std::make_unique<MockFlutterWindowsEngine>();
1224 auto window_binding_handler =
1225 std::make_unique<NiceMock<MockWindowBindingHandler>>();
1226 std::unique_ptr<MockAngleSurfaceManager> surface_manager =
1227 std::make_unique<MockAngleSurfaceManager>();
1228
1229 EXPECT_CALL(*window_binding_handler.get(), NeedsVSync)
1230 .WillOnce(Return(false));
1231
1232 EngineModifier modifier(engine.get());
1233 FlutterWindowsView view(std::move(window_binding_handler));
1234
1235 InSequence s;
1236 EXPECT_CALL(*surface_manager.get(),
1237 CreateSurface(_, _, _, /*vsync_enabled=*/false))
1238 .Times(1)
1239 .WillOnce(Return(true));
1240
1241 EXPECT_CALL(*engine.get(), Stop).Times(1);
1242 EXPECT_CALL(*surface_manager.get(), DestroySurface).Times(1);
1243
1244 modifier.SetSurfaceManager(surface_manager.release());
1245 view.SetEngine(std::move(engine));
1246
1247 view.CreateRenderSurface();
1248 }
1249
1250 // Blocks until the v-blank if it is enabled by the window.
1251 TEST(FlutterWindowsViewTest, EnablesVSync) {
1252 std::unique_ptr<MockFlutterWindowsEngine> engine =
1253 std::make_unique<MockFlutterWindowsEngine>();
1254 auto window_binding_handler =
1255 std::make_unique<NiceMock<MockWindowBindingHandler>>();
1256 std::unique_ptr<MockAngleSurfaceManager> surface_manager =
1257 std::make_unique<MockAngleSurfaceManager>();
1258
1259 EXPECT_CALL(*window_binding_handler.get(), NeedsVSync).WillOnce(Return(true));
1260
1261 EngineModifier modifier(engine.get());
1262 FlutterWindowsView view(std::move(window_binding_handler));
1263
1264 InSequence s;
1265 EXPECT_CALL(*surface_manager.get(),
1266 CreateSurface(_, _, _, /*vsync_enabled=*/true))
1267 .Times(1)
1268 .WillOnce(Return(true));
1269
1270 EXPECT_CALL(*engine.get(), Stop).Times(1);
1271 EXPECT_CALL(*surface_manager.get(), DestroySurface).Times(1);
1272
1273 modifier.SetSurfaceManager(surface_manager.release());
1274 view.SetEngine(std::move(engine));
1275
1276 view.CreateRenderSurface();
1277 }
1278
1279 // Desktop Window Manager composition can be disabled on Windows 7.
1280 // If this happens, the app must synchronize with the vsync to prevent
1281 // screen tearing.
1282 TEST(FlutterWindowsViewTest, UpdatesVSyncOnDwmUpdates) {
1283 std::unique_ptr<MockFlutterWindowsEngine> engine =
1284 std::make_unique<MockFlutterWindowsEngine>();
1285 auto window_binding_handler =
1286 std::make_unique<NiceMock<MockWindowBindingHandler>>();
1287 std::unique_ptr<MockAngleSurfaceManager> surface_manager =
1288 std::make_unique<MockAngleSurfaceManager>();
1289
1290 EXPECT_CALL(*engine.get(), PostRasterThreadTask)
1291 .Times(2)
1292 .WillRepeatedly([](fml::closure callback) {
1293 callback();
1294 return true;
1295 });
1296
1297 EXPECT_CALL(*window_binding_handler.get(), NeedsVSync)
1298 .WillOnce(Return(true))
1299 .WillOnce(Return(false));
1300
1301 EngineModifier modifier(engine.get());
1302 FlutterWindowsView view(std::move(window_binding_handler));
1303
1304 InSequence s;
1305 EXPECT_CALL(*surface_manager.get(), SetVSyncEnabled(true)).Times(1);
1306 EXPECT_CALL(*surface_manager.get(), SetVSyncEnabled(false)).Times(1);
1307
1308 EXPECT_CALL(*engine.get(), Stop).Times(1);
1309 EXPECT_CALL(*surface_manager.get(), DestroySurface).Times(1);
1310
1311 modifier.SetSurfaceManager(surface_manager.release());
1312 view.SetEngine(std::move(engine));
1313
1314 view.GetEngine()->OnDwmCompositionChanged();
1315 view.GetEngine()->OnDwmCompositionChanged();
1316 }
1317
1318 } // namespace testing
1319 } // namespace flutter
FlutterDesktopEngineProperties::aot_library_path
const wchar_t * aot_library_path
Definition: flutter_windows.h:51
flutter::FlutterWindowsView::CreateRenderSurface
void CreateRenderSurface()
Definition: flutter_windows_view.cc:590
flutter::FlutterWindowsView::OnWindowSizeChanged
void OnWindowSizeChanged(size_t width, size_t height) override
Definition: flutter_windows_view.cc:107
flutter_windows_texture_registrar.h
flutter::FlutterWindowsView
Definition: flutter_windows_view.h:35
flutter::JsonMessageCodec::GetInstance
static const JsonMessageCodec & GetInstance()
Definition: json_message_codec.cc:17
FlutterDesktopEngineProperties
Definition: flutter_windows.h:36
flutter::FlutterEngine
Definition: flutter_engine.h:28
FlutterDesktopBinaryReply
void(* FlutterDesktopBinaryReply)(const uint8_t *data, size_t data_size, void *user_data)
Definition: flutter_messenger.h:26
user_data
void * user_data
Definition: flutter_windows_view_unittests.cc:47
flutter::WindowsRenderTarget
std::variant< HWND > WindowsRenderTarget
Definition: window_binding_handler.h:44
FlutterDesktopEngineProperties::icu_data_path
const wchar_t * icu_data_path
Definition: flutter_windows.h:45
json_message_codec.h
flutter::FlutterWindowsView::OnWindowRepaint
void OnWindowRepaint() override
Definition: flutter_windows_view.cc:141
flutter::FlutterWindowsEngine::OnDwmCompositionChanged
void OnDwmCompositionChanged()
Definition: flutter_windows_engine.cc:805
flutter_windows_view.h
flutter::FlutterWindowsView::SetEngine
void SetEngine(std::unique_ptr< FlutterWindowsEngine > engine)
Definition: flutter_windows_view.cc:61
flutter_window.h
flutter::testing::kScanCodeKeyA
constexpr uint64_t kScanCodeKeyA
Definition: flutter_windows_view_unittests.cc:36
flutter
Definition: accessibility_bridge_windows.cc:11
flutter_windows_engine.h
flutter::MessageCodec::EncodeMessage
std::unique_ptr< std::vector< uint8_t > > EncodeMessage(const T &message) const
Definition: message_codec.h:45
flutter::testing::TEST
TEST(AccessibilityBridgeWindows, GetParent)
Definition: accessibility_bridge_windows_unittests.cc:233
FlutterDesktopEngineProperties::assets_path
const wchar_t * assets_path
Definition: flutter_windows.h:40
flutter::testing::kVirtualKeyA
constexpr uint64_t kVirtualKeyA
Definition: flutter_windows_view_unittests.cc:37
flutter::FlutterWindowsView::GetEngine
FlutterWindowsEngine * GetEngine()
Definition: flutter_windows_view.cc:630
flutter::FlutterWindowsView::GetFrameBufferId
uint32_t GetFrameBufferId(size_t width, size_t height)
Definition: flutter_windows_view.cc:73
callback
FlutterDesktopBinaryReply callback
Definition: flutter_windows_view_unittests.cc:46
node_delegate
std::shared_ptr< FlutterPlatformNodeDelegateWindows > node_delegate
Definition: accessibility_bridge_windows_unittests.cc:32