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