basalt-win32 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runWindows.js +35 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +429 -0
  5. package/native/tests/test_win32_accessibility.cpp +585 -0
  6. package/native/tests/test_win32_animated_image.cpp +351 -0
  7. package/native/tests/test_win32_clipboard.cpp +121 -0
  8. package/native/tests/test_win32_focus.cpp +195 -0
  9. package/native/tests/test_win32_hittest.cpp +750 -0
  10. package/native/tests/test_win32_image.cpp +370 -0
  11. package/native/tests/test_win32_imageloader.cpp +208 -0
  12. package/native/tests/test_win32_input.cpp +511 -0
  13. package/native/tests/test_win32_key_events.cpp +84 -0
  14. package/native/tests/test_win32_mounting.cpp +757 -0
  15. package/native/tests/test_win32_paint.cpp +1799 -0
  16. package/native/tests/test_win32_scrollview.cpp +647 -0
  17. package/native/tests/test_win32_selection.cpp +215 -0
  18. package/native/tests/test_win32_text.cpp +1456 -0
  19. package/native/tests/test_win32_textinput.cpp +1594 -0
  20. package/native/tests/test_win32_titlebar.cpp +111 -0
  21. package/native/tests/test_win32_titlebar_actions.cpp +62 -0
  22. package/native/tests/test_win32_view.cpp +222 -0
  23. package/native/win32/ColorSchemeWin32.cpp +56 -0
  24. package/native/win32/ComponentRegistryWin32.cpp +156 -0
  25. package/native/win32/DirectWriteLayout.cpp +485 -0
  26. package/native/win32/DirectWriteLayout.h +77 -0
  27. package/native/win32/DirectWriteLayoutManager.cpp +154 -0
  28. package/native/win32/FontRegistryDirectWrite.cpp +117 -0
  29. package/native/win32/PlatformServicesWin32.cpp +744 -0
  30. package/native/win32/RnWin32Accessible.cpp +343 -0
  31. package/native/win32/RnWin32Accessible.h +130 -0
  32. package/native/win32/RnWin32Image.cpp +588 -0
  33. package/native/win32/RnWin32Image.h +139 -0
  34. package/native/win32/RnWin32TextLayout.cpp +1379 -0
  35. package/native/win32/RnWin32TextLayout.h +390 -0
  36. package/native/win32/RnWin32View.cpp +2956 -0
  37. package/native/win32/RnWin32View.h +926 -0
  38. package/native/win32/Win32AnimationChoreographer.cpp +76 -0
  39. package/native/win32/Win32AnimationChoreographer.h +61 -0
  40. package/native/win32/Win32Clip.h +209 -0
  41. package/native/win32/Win32Cursors.cpp +309 -0
  42. package/native/win32/Win32Cursors.h +121 -0
  43. package/native/win32/Win32DropTarget.cpp +567 -0
  44. package/native/win32/Win32DropTarget.h +42 -0
  45. package/native/win32/Win32Focus.cpp +262 -0
  46. package/native/win32/Win32Focus.h +109 -0
  47. package/native/win32/Win32ImageLoader.cpp +199 -0
  48. package/native/win32/Win32ImageLoader.h +133 -0
  49. package/native/win32/Win32InputScopes.h +92 -0
  50. package/native/win32/Win32KeyEvents.cpp +126 -0
  51. package/native/win32/Win32KeyEvents.h +20 -0
  52. package/native/win32/Win32MenuBar.cpp +382 -0
  53. package/native/win32/Win32MenuBar.h +16 -0
  54. package/native/win32/Win32MountingManager.cpp +1282 -0
  55. package/native/win32/Win32MountingManager.h +243 -0
  56. package/native/win32/Win32Offscreen.h +53 -0
  57. package/native/win32/Win32Packaging.cpp +161 -0
  58. package/native/win32/Win32Packaging.h +56 -0
  59. package/native/win32/Win32RunLoopObserver.cpp +48 -0
  60. package/native/win32/Win32RunLoopObserver.h +42 -0
  61. package/native/win32/Win32ScrollView.cpp +718 -0
  62. package/native/win32/Win32ScrollView.h +256 -0
  63. package/native/win32/Win32SkiaContext.cpp +221 -0
  64. package/native/win32/Win32SkiaContext.h +53 -0
  65. package/native/win32/Win32SkiaModule.cpp +80 -0
  66. package/native/win32/Win32SkiaModule.h +50 -0
  67. package/native/win32/Win32SkiaPeer.cpp +358 -0
  68. package/native/win32/Win32SkiaPeer.h +66 -0
  69. package/native/win32/Win32SkiaUnicode.cpp +42 -0
  70. package/native/win32/Win32Snapshot.cpp +201 -0
  71. package/native/win32/Win32Snapshot.h +66 -0
  72. package/native/win32/Win32Strings.h +54 -0
  73. package/native/win32/Win32TextInput.cpp +1296 -0
  74. package/native/win32/Win32TextInput.h +341 -0
  75. package/native/win32/Win32TitleBar.cpp +562 -0
  76. package/native/win32/Win32TitleBar.h +146 -0
  77. package/native/win32/Win32TitleBarLayout.cpp +46 -0
  78. package/native/win32/Win32TitleBarLayout.h +58 -0
  79. package/native/win32/Win32TouchDispatcher.cpp +577 -0
  80. package/native/win32/Win32TouchDispatcher.h +201 -0
  81. package/native/win32/Win32UiThread.h +27 -0
  82. package/native/win32/Win32WindowControl.cpp +329 -0
  83. package/native/win32/Win32WindowModule.cpp +209 -0
  84. package/native/win32/Win32WindowModule.h +72 -0
  85. package/native/win32/demo_layout_win32.cpp +232 -0
  86. package/native/win32/main_win32.cpp +2722 -0
  87. package/native/win32/mount_harness_win32.cpp +316 -0
  88. package/package.json +25 -4
  89. package/react-native.config.js +22 -0
  90. package/README.md +0 -3
@@ -0,0 +1,585 @@
1
+ // What a screen reader is told.
2
+ //
3
+ // The union of tests/test_accessibility.cpp on GTK and
4
+ // tests/test_appkit_accessibility.mm on AppKit. What differs is how the answer
5
+ // is obtained: those two set a property on a widget and read it back, while
6
+ // UI Automation is a pull interface, so these build the provider and ask it the
7
+ // same questions a screen reader would -- by property id, through
8
+ // GetPropertyValue.
9
+ //
10
+ // Nothing here needs a window. UIA's tree navigation does, because a fragment
11
+ // root is an HWND, and that half lands with the host; the property half is
12
+ // answerable now and is most of what the other two suites check.
13
+
14
+ #include "TestHarness.h"
15
+
16
+ #include "RnWin32Accessible.h"
17
+ #include "RnWin32TextLayout.h"
18
+ #include "RnWin32View.h"
19
+ // For narrow(): UIA answers in BSTRs and these assertions are written in UTF-8,
20
+ // like the rest of the project.
21
+ #include "Win32Strings.h"
22
+
23
+ #include <windows.h>
24
+
25
+ // WIN32_LEAN_AND_MEAN keeps combaseapi.h out of windows.h, and that is where the
26
+ // `interface` macro lives -- without it UIAutomationCore.h fails on its own
27
+ // first typedef, which reads as a broken SDK rather than a missing include.
28
+ #include <objbase.h>
29
+
30
+ #include <uiautomation.h>
31
+ #include <wrl/client.h>
32
+
33
+ #include <memory>
34
+ #include <sstream>
35
+ #include <string>
36
+
37
+ using Microsoft::WRL::ComPtr;
38
+ using basalt::win32::RnAccessibleFlag;
39
+ using basalt::win32::RnAccessibleInfo;
40
+ using basalt::win32::RnTextStyle;
41
+ using basalt::win32::RnWin32TextLayout;
42
+ using basalt::win32::RnWin32View;
43
+
44
+ namespace {
45
+
46
+ ComPtr<IRawElementProviderSimple> providerFor(const RnAccessibleInfo &info) {
47
+ ComPtr<IRawElementProviderSimple> provider;
48
+ provider.Attach(basalt::win32::createAccessibleProvider(info));
49
+ return provider;
50
+ }
51
+
52
+ // The control type a provider reports, or -1 if it has none.
53
+ long controlTypeOf(const ComPtr<IRawElementProviderSimple> &provider) {
54
+ if (!provider) {
55
+ return -1;
56
+ }
57
+ VARIANT value;
58
+ VariantInit(&value);
59
+ if (FAILED(provider->GetPropertyValue(UIA_ControlTypePropertyId, &value))) {
60
+ return -1;
61
+ }
62
+ const long result = value.vt == VT_I4 ? value.lVal : -1;
63
+ VariantClear(&value);
64
+ return result;
65
+ }
66
+
67
+ // A string property, or "" when the provider declined to answer.
68
+ std::string stringProperty(const ComPtr<IRawElementProviderSimple> &provider, PROPERTYID id) {
69
+ if (!provider) {
70
+ return {};
71
+ }
72
+ VARIANT value;
73
+ VariantInit(&value);
74
+ if (FAILED(provider->GetPropertyValue(id, &value))) {
75
+ return {};
76
+ }
77
+ std::string result;
78
+ if (value.vt == VT_BSTR && value.bstrVal != nullptr) {
79
+ const std::wstring wide(value.bstrVal, SysStringLen(value.bstrVal));
80
+ result = basalt::win32::narrow(wide);
81
+ }
82
+ VariantClear(&value);
83
+ return result;
84
+ }
85
+
86
+ // A VARIANT's type tag, so a test can say "this property was left unset" --
87
+ // which is a different claim from "this property is false".
88
+ VARTYPE propertyType(const ComPtr<IRawElementProviderSimple> &provider, PROPERTYID id) {
89
+ if (!provider) {
90
+ return VT_EMPTY;
91
+ }
92
+ VARIANT value;
93
+ VariantInit(&value);
94
+ if (FAILED(provider->GetPropertyValue(id, &value))) {
95
+ return VT_EMPTY;
96
+ }
97
+ const VARTYPE type = value.vt;
98
+ VariantClear(&value);
99
+ return type;
100
+ }
101
+
102
+ long longProperty(const ComPtr<IRawElementProviderSimple> &provider, PROPERTYID id) {
103
+ VARIANT value;
104
+ VariantInit(&value);
105
+ provider->GetPropertyValue(id, &value);
106
+ const long result = value.vt == VT_I4 ? value.lVal : -1;
107
+ VariantClear(&value);
108
+ return result;
109
+ }
110
+
111
+ bool boolProperty(const ComPtr<IRawElementProviderSimple> &provider, PROPERTYID id) {
112
+ VARIANT value;
113
+ VariantInit(&value);
114
+ provider->GetPropertyValue(id, &value);
115
+ const bool result = value.vt == VT_BOOL && value.boolVal == VARIANT_TRUE;
116
+ VariantClear(&value);
117
+ return result;
118
+ }
119
+
120
+ RnAccessibleInfo withRole(const std::string &role) {
121
+ RnAccessibleInfo info;
122
+ info.role = role;
123
+ return info;
124
+ }
125
+
126
+ } // namespace
127
+
128
+ TEST(accessibility_maps_react_native_roles_onto_uia) {
129
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("button"))),
130
+ static_cast<long>(UIA_ButtonControlTypeId));
131
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("link"))),
132
+ static_cast<long>(UIA_HyperlinkControlTypeId));
133
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("image"))),
134
+ static_cast<long>(UIA_ImageControlTypeId));
135
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("text"))),
136
+ static_cast<long>(UIA_TextControlTypeId));
137
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("checkbox"))),
138
+ static_cast<long>(UIA_CheckBoxControlTypeId));
139
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("adjustable"))),
140
+ static_cast<long>(UIA_SliderControlTypeId));
141
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("tablist"))),
142
+ static_cast<long>(UIA_TabControlTypeId));
143
+ }
144
+
145
+ TEST(accessibility_falls_back_to_a_group) {
146
+ // A role UIA has no control type for is a group, not a guess. A wrong control
147
+ // type is worse than a vague one: it makes a view announce itself as
148
+ // something it is not.
149
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("summary"))),
150
+ static_cast<long>(UIA_GroupControlTypeId));
151
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("somethingnobodyhasheardof"))),
152
+ static_cast<long>(UIA_GroupControlTypeId));
153
+
154
+ // And a switch is a checkbox, because UIA has no switch. Named here rather
155
+ // than left to be rediscovered, since it is a deliberate approximation.
156
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("switch"))),
157
+ static_cast<long>(UIA_CheckBoxControlTypeId));
158
+ }
159
+
160
+ TEST(accessibility_leaves_plain_views_out_of_the_tree) {
161
+ // A <View> with no role, no label and no hint is scaffolding. A tree full of
162
+ // untyped groups is worse for a screen reader user than a tree without them.
163
+ RnAccessibleInfo plain;
164
+ EXPECT(!basalt::win32::isAccessibilityElement(plain));
165
+ EXPECT(basalt::win32::createAccessibleProvider(plain) == nullptr);
166
+
167
+ // A label alone is enough to make it worth reporting.
168
+ RnAccessibleInfo labelled;
169
+ labelled.label = "Close";
170
+ EXPECT(basalt::win32::isAccessibilityElement(labelled));
171
+ }
172
+
173
+ TEST(accessibility_honours_a_presentational_role) {
174
+ EXPECT(basalt::win32::roleIsPresentational("none"));
175
+ EXPECT(basalt::win32::roleIsPresentational("presentation"));
176
+ EXPECT(!basalt::win32::roleIsPresentational("button"));
177
+
178
+ // Explicitly presentational beats having a label: the app said not to report
179
+ // this one.
180
+ RnAccessibleInfo info = withRole("none");
181
+ info.label = "ignored";
182
+ EXPECT(basalt::win32::createAccessibleProvider(info) == nullptr);
183
+ }
184
+
185
+ TEST(accessibility_label_and_hint_reach_uia) {
186
+ RnAccessibleInfo info = withRole("button");
187
+ info.label = "Save";
188
+ info.hint = "Writes the file to disk";
189
+
190
+ auto provider = providerFor(info);
191
+ EXPECT(provider != nullptr);
192
+ EXPECT_EQ(stringProperty(provider, UIA_NamePropertyId), std::string("Save"));
193
+ // React Native's hint is "what happens if you do this", which is what UIA
194
+ // calls help text.
195
+ EXPECT_EQ(stringProperty(provider, UIA_HelpTextPropertyId),
196
+ std::string("Writes the file to disk"));
197
+ }
198
+
199
+ TEST(accessibility_states_reach_uia) {
200
+ RnAccessibleInfo info = withRole("checkbox");
201
+ info.state.disabled = RnAccessibleFlag::True;
202
+ info.state.checked = RnAccessibleFlag::True;
203
+ info.state.selected = RnAccessibleFlag::True;
204
+ info.state.expanded = RnAccessibleFlag::False;
205
+
206
+ auto provider = providerFor(info);
207
+ EXPECT(provider != nullptr);
208
+
209
+ EXPECT(!boolProperty(provider, UIA_IsEnabledPropertyId));
210
+ EXPECT_EQ(longProperty(provider, UIA_ToggleToggleStatePropertyId),
211
+ static_cast<long>(ToggleState_On));
212
+ EXPECT(boolProperty(provider, UIA_SelectionItemIsSelectedPropertyId));
213
+ EXPECT_EQ(longProperty(provider, UIA_ExpandCollapseExpandCollapseStatePropertyId),
214
+ static_cast<long>(ExpandCollapseState_Collapsed));
215
+ }
216
+
217
+ TEST(accessibility_unset_leaves_uia_alone) {
218
+ // The tri-state, stated as a test. A view that never mentions being checked
219
+ // is not an unchecked checkbox, and must not report ToggleState_Off -- which
220
+ // a screen reader would read aloud as "not checked".
221
+ auto provider = providerFor(withRole("button"));
222
+ EXPECT(provider != nullptr);
223
+
224
+ EXPECT_EQ(propertyType(provider, UIA_ToggleToggleStatePropertyId), VT_EMPTY);
225
+ EXPECT_EQ(propertyType(provider, UIA_SelectionItemIsSelectedPropertyId), VT_EMPTY);
226
+ EXPECT_EQ(propertyType(provider, UIA_ExpandCollapseExpandCollapseStatePropertyId), VT_EMPTY);
227
+
228
+ // IsEnabled is the exception, and deliberately: UIA has no "unknown" for it,
229
+ // and its default is enabled, which is React Native's default too.
230
+ EXPECT_EQ(propertyType(provider, UIA_IsEnabledPropertyId), VT_BOOL);
231
+ EXPECT(boolProperty(provider, UIA_IsEnabledPropertyId));
232
+ }
233
+
234
+ TEST(accessibility_hidden_takes_a_view_out_of_the_tree) {
235
+ RnAccessibleInfo info = withRole("button");
236
+ info.label = "Save";
237
+ info.hidden = true;
238
+
239
+ EXPECT(!basalt::win32::isAccessibilityElement(info));
240
+ EXPECT(basalt::win32::createAccessibleProvider(info) == nullptr);
241
+ }
242
+
243
+ TEST(accessibility_role_is_reported_in_the_tree) {
244
+ auto view = std::make_unique<RnWin32View>(3);
245
+ view->setFrame(0, 0, 100, 40);
246
+
247
+ RnAccessibleInfo info = withRole("button");
248
+ info.label = "Save";
249
+ view->setAccessibleInfo(info);
250
+
251
+ // React Native's role name, not UIA's -- the same string GTK and AppKit
252
+ // print, so the three dumps compare.
253
+ EXPECT_EQ(view->describeTree(), std::string("view tag=3 frame=(0,0 100x40) role=button\n"));
254
+ }
255
+
256
+ TEST(accessibility_role_can_change_after_mount) {
257
+ // GTK cannot do this: a GtkAccessible's role is construct-only, so
258
+ // `docs/DECISIONS.md` records that accessibilityRole is fixed once a widget
259
+ // exists. UI Automation pulls rather than being pushed to, so there is
260
+ // nothing baked in, and the mounting manager should not copy GTK's
261
+ // restriction when it arrives.
262
+ auto view = std::make_unique<RnWin32View>(4);
263
+ view->setAccessibleInfo(withRole("button"));
264
+
265
+ ComPtr<IRawElementProviderSimple> asButton;
266
+ asButton.Attach(view->createAccessibleProvider());
267
+ EXPECT_EQ(controlTypeOf(asButton), static_cast<long>(UIA_ButtonControlTypeId));
268
+
269
+ view->setAccessibleInfo(withRole("checkbox"));
270
+
271
+ ComPtr<IRawElementProviderSimple> asCheckbox;
272
+ asCheckbox.Attach(view->createAccessibleProvider());
273
+ EXPECT_EQ(controlTypeOf(asCheckbox), static_cast<long>(UIA_CheckBoxControlTypeId));
274
+ }
275
+
276
+ // `testID`, which UIA calls the automation id: the property every Windows test
277
+ // runner looks a control up by, and the same idea as AppKit's
278
+ // `accessibilityIdentifier` and GTK's accessible id.
279
+ TEST(accessibility_a_test_id_becomes_the_automation_id) {
280
+ RnAccessibleInfo info = withRole("button");
281
+ info.label = "Save";
282
+ info.testId = "save-button";
283
+
284
+ auto provider = providerFor(info);
285
+ EXPECT(provider != nullptr);
286
+ EXPECT_EQ(stringProperty(provider, UIA_AutomationIdPropertyId), std::string("save-button"));
287
+ // Not the name: a runner searching by automation id and a screen reader
288
+ // reading the label are different questions, and `testID` answers only one.
289
+ EXPECT_EQ(stringProperty(provider, UIA_NamePropertyId), std::string("Save"));
290
+
291
+ auto view = std::make_unique<RnWin32View>(4);
292
+ view->setFrame(0, 0, 100, 40);
293
+ view->setAccessibleInfo(info);
294
+ EXPECT_EQ(view->describeTree(),
295
+ std::string("view tag=4 frame=(0,0 100x40) role=button testid=save-button\n"));
296
+ }
297
+
298
+ // And a view with no testID leaves the property unset rather than empty: a
299
+ // runner searching for "" would otherwise match the first control it saw.
300
+ TEST(accessibility_no_test_id_leaves_the_automation_id_unset) {
301
+ auto provider = providerFor(withRole("button"));
302
+ EXPECT(provider != nullptr);
303
+ EXPECT_EQ(propertyType(provider, UIA_AutomationIdPropertyId), VT_EMPTY);
304
+ }
305
+
306
+ // `accessibilityViewIsModal`, which UIA calls a dialog: a client that honours
307
+ // `IsDialog` stops offering what is behind the element.
308
+ TEST(accessibility_a_modal_view_is_a_dialog) {
309
+ RnAccessibleInfo info = withRole("button");
310
+ info.label = "Save";
311
+ info.modal = true;
312
+
313
+ auto provider = providerFor(info);
314
+ EXPECT(provider != nullptr);
315
+ EXPECT_EQ(propertyType(provider, UIA_IsDialogPropertyId), VT_BOOL);
316
+ EXPECT(boolProperty(provider, UIA_IsDialogPropertyId));
317
+
318
+ auto view = std::make_unique<RnWin32View>(5);
319
+ view->setFrame(0, 0, 100, 40);
320
+ view->setAccessibleInfo(info);
321
+ EXPECT_EQ(view->describeTree(),
322
+ std::string("view tag=5 frame=(0,0 100x40) role=button modal\n"));
323
+ }
324
+
325
+ // Unset rather than false: "this is not a dialog" is a claim, and the other two
326
+ // hosts stay quiet about it too.
327
+ TEST(accessibility_a_plain_view_claims_no_dialog) {
328
+ auto provider = providerFor(withRole("button"));
329
+ EXPECT(provider != nullptr);
330
+ EXPECT_EQ(propertyType(provider, UIA_IsDialogPropertyId), VT_EMPTY);
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // `accessibilityLabelledBy`, which is the one accessibility prop that is a
335
+ // relation between two views rather than a value on one.
336
+ //
337
+ // A relation rather than a copied string: the field's name lives on the caption
338
+ // beside it, so a caption that changes its text does not leave a stale copy
339
+ // behind. GTK sets an AT-SPI LABELLED_BY relation, macOS an
340
+ // `accessibilityTitleUIElement`, and UIA answers `UIA_LabeledByPropertyId` with
341
+ // an element -- which is why these go through a view rather than through a bare
342
+ // `RnAccessibleInfo`: the view is what knows the other view.
343
+ // ---------------------------------------------------------------------------
344
+
345
+ TEST(accessibility_labelled_by_answers_with_the_labelling_element) {
346
+ auto field = std::make_unique<RnWin32View>(1);
347
+ auto caption = std::make_unique<RnWin32View>(2);
348
+
349
+ // A field with no label of its own, which is the whole point of the relation:
350
+ // its name is the caption's. `role` has to be something, though -- "none" is
351
+ // presentational and has no provider at all, which is the right behaviour and
352
+ // what the first version of this test tripped over.
353
+ RnAccessibleInfo fieldInfo;
354
+ fieldInfo.role = "textbox";
355
+ fieldInfo.testId = "field";
356
+ field->setAccessibleInfo(fieldInfo);
357
+
358
+ RnAccessibleInfo captionInfo;
359
+ captionInfo.role = "text";
360
+ captionInfo.label = "Save the document";
361
+ caption->setAccessibleInfo(captionInfo);
362
+
363
+ // Nothing yet: a view with no relation answers with no element rather than
364
+ // with itself.
365
+ ComPtr<IRawElementProviderSimple> before;
366
+ before.Attach(field->createAccessibleProvider());
367
+ EXPECT(before != nullptr);
368
+ if (before) {
369
+ EXPECT_EQ(propertyType(before, UIA_LabeledByPropertyId), VT_EMPTY);
370
+ }
371
+
372
+ field->setLabelledBy({caption.get()});
373
+
374
+ ComPtr<IRawElementProviderSimple> provider;
375
+ provider.Attach(field->createAccessibleProvider());
376
+ EXPECT(provider != nullptr);
377
+ if (!provider) {
378
+ return;
379
+ }
380
+
381
+ VARIANT value;
382
+ VariantInit(&value);
383
+ EXPECT(SUCCEEDED(provider->GetPropertyValue(UIA_LabeledByPropertyId, &value)));
384
+ EXPECT_EQ(value.vt, VT_UNKNOWN);
385
+ if (value.vt == VT_UNKNOWN && value.punkVal != nullptr) {
386
+ // The element is a provider, and what it is for is reading the caption's
387
+ // name: a relation pointing at the wrong view is the failure that matters.
388
+ ComPtr<IRawElementProviderSimple> label;
389
+ EXPECT(SUCCEEDED(value.punkVal->QueryInterface(IID_PPV_ARGS(label.GetAddressOf()))));
390
+ EXPECT_EQ(stringProperty(label, UIA_NamePropertyId), std::string("Save the document"));
391
+ }
392
+ VariantClear(&value);
393
+ }
394
+
395
+ // The relation comes apart again, which is what a caption being unmounted means
396
+ // and what a stale element would get wrong.
397
+ TEST(accessibility_labelled_by_can_be_taken_away) {
398
+ auto field = std::make_unique<RnWin32View>(1);
399
+ auto caption = std::make_unique<RnWin32View>(2);
400
+ RnAccessibleInfo info;
401
+ info.label = "Name";
402
+ field->setAccessibleInfo(info);
403
+ caption->setAccessibleInfo(info);
404
+
405
+ field->setLabelledBy({caption.get()});
406
+ field->setLabelledBy({});
407
+
408
+ ComPtr<IRawElementProviderSimple> provider;
409
+ provider.Attach(field->createAccessibleProvider());
410
+ EXPECT(provider != nullptr);
411
+ if (provider) {
412
+ EXPECT_EQ(propertyType(provider, UIA_LabeledByPropertyId), VT_EMPTY);
413
+ }
414
+ }
415
+
416
+ // The resolved relation in the dump, by tag, which is what the end-to-end
417
+ // scenario reads on all three hosts.
418
+ TEST(accessibility_labelled_by_is_reported_in_the_tree) {
419
+ auto field = std::make_unique<RnWin32View>(7);
420
+ auto caption = std::make_unique<RnWin32View>(9);
421
+ EXPECT(field->describeTree().find("labelled-by=") == std::string::npos);
422
+
423
+ field->setLabelledBy({caption.get()});
424
+ EXPECT(field->describeTree().find("labelled-by=9") != std::string::npos);
425
+ }
426
+
427
+ // ---------------------------------------------------------------------------
428
+ // `accessibilityLiveRegion`: the text a status message says, and saying it.
429
+ //
430
+ // The change detection is `core/LiveRegions.h`'s and has its own tests there;
431
+ // what is this host's is collecting the text and raising the announcement. UIA
432
+ // has no "say this" except `UiaRaiseNotificationEvent`, and nothing in a test
433
+ // run is listening to it, so what is asserted here is the text -- which is the
434
+ // half that was wrong on both other hosts first.
435
+ // ---------------------------------------------------------------------------
436
+
437
+ TEST(accessibility_a_live_region_collects_the_text_of_its_subtree) {
438
+ auto region = std::make_unique<RnWin32View>(1);
439
+ auto first = std::make_unique<RnWin32View>(2);
440
+ auto second = std::make_unique<RnWin32View>(3);
441
+ RnTextStyle style;
442
+ first->setTextLayout(RnWin32TextLayout::create("Saving", style, 0));
443
+ second->setTextLayout(RnWin32TextLayout::create("one of three", style, 0));
444
+ region->insertChild(first.get(), 0);
445
+ region->insertChild(second.get(), 1);
446
+
447
+ // In paint order, joined with a space: a status line is a <View> with text
448
+ // inside it, and the view itself carries no string at all. Both other hosts
449
+ // collect it the same way.
450
+ EXPECT_EQ(region->collectText(), std::string("Saving one of three"));
451
+
452
+ // A view with nothing in it says nothing, rather than a stray space.
453
+ auto empty = std::make_unique<RnWin32View>(4);
454
+ EXPECT(empty->collectText().empty());
455
+
456
+ region->removeChild(first.get());
457
+ region->removeChild(second.get());
458
+ }
459
+
460
+ TEST(accessibility_an_announcement_is_remembered_and_an_empty_one_is_not_made) {
461
+ auto view = std::make_unique<RnWin32View>(1);
462
+ RnAccessibleInfo info;
463
+ info.role = "text";
464
+ info.label = "Saved";
465
+ view->setAccessibleInfo(info);
466
+
467
+ EXPECT(view->lastAnnouncement().empty());
468
+
469
+ view->announce("Saved", false);
470
+ EXPECT_EQ(view->lastAnnouncement(), std::string("Saved"));
471
+
472
+ // Nothing to say is not an announcement: a region whose text went empty must
473
+ // not read out silence, and `UiaRaiseNotificationEvent` with an empty string
474
+ // is a notification a screen reader still reports as one.
475
+ view->announce("", true);
476
+ EXPECT_EQ(view->lastAnnouncement(), std::string("Saved"));
477
+
478
+ view->announce("Saved twice", true);
479
+ EXPECT_EQ(view->lastAnnouncement(), std::string("Saved twice"));
480
+ }
481
+
482
+ // ---------------------------------------------------------------------------
483
+ // `accessibilityValue`: a range, a position in it, and a text form.
484
+ //
485
+ // Four independent optionals, and the whole difficulty is that an absent part
486
+ // has to stay absent: a view that never said what its range is must not be
487
+ // announced as sitting at the bottom of one. GTK resets a property per part and
488
+ // AppKit leaves its attribute nil; UIA has the RangeValue properties and a
489
+ // separate Value one for the text, so each is answered or left VT_EMPTY.
490
+ // ---------------------------------------------------------------------------
491
+
492
+ TEST(accessibility_value_answers_the_range_and_the_position) {
493
+ RnAccessibleInfo info;
494
+ info.role = "slider";
495
+ info.value.min = 0;
496
+ info.value.max = 10;
497
+ info.value.now = 7;
498
+
499
+ const auto provider = providerFor(info);
500
+ EXPECT(provider != nullptr);
501
+ if (!provider) {
502
+ return;
503
+ }
504
+
505
+ const auto number = [&provider](PROPERTYID id) {
506
+ VARIANT value;
507
+ VariantInit(&value);
508
+ if (FAILED(provider->GetPropertyValue(id, &value))) {
509
+ return -1.0;
510
+ }
511
+ const double result = value.vt == VT_R8 ? value.dblVal : -1.0;
512
+ VariantClear(&value);
513
+ return result;
514
+ };
515
+
516
+ EXPECT_NEAR(number(UIA_RangeValueMinimumPropertyId), 0.0, 0.001);
517
+ EXPECT_NEAR(number(UIA_RangeValueMaximumPropertyId), 10.0, 0.001);
518
+ EXPECT_NEAR(number(UIA_RangeValueValuePropertyId), 7.0, 0.001);
519
+ // Read-only, because nothing here lets a client change it: the pattern that
520
+ // would is `IRangeValueProvider`, and React Native expresses "you may change
521
+ // this" as an accessibility action.
522
+ EXPECT_EQ(propertyType(provider, UIA_RangeValueIsReadOnlyPropertyId), VT_BOOL);
523
+ // And no text form was given, so none is claimed.
524
+ EXPECT_EQ(propertyType(provider, UIA_ValueValuePropertyId), VT_EMPTY);
525
+ }
526
+
527
+ TEST(accessibility_value_leaves_a_part_nobody_mentioned_unset) {
528
+ RnAccessibleInfo info;
529
+ info.role = "progressbar";
530
+ // Only the position, which is what a progress bar with no declared range
531
+ // looks like. The other two parts must stay absent rather than become zeros.
532
+ info.value.now = 3;
533
+
534
+ const auto provider = providerFor(info);
535
+ EXPECT(provider != nullptr);
536
+ if (!provider) {
537
+ return;
538
+ }
539
+ EXPECT_EQ(propertyType(provider, UIA_RangeValueValuePropertyId), VT_R8);
540
+ EXPECT_EQ(propertyType(provider, UIA_RangeValueMinimumPropertyId), VT_EMPTY);
541
+ EXPECT_EQ(propertyType(provider, UIA_RangeValueMaximumPropertyId), VT_EMPTY);
542
+
543
+ // A view with no value at all says nothing about any of them, and does not
544
+ // claim to be read-only either.
545
+ RnAccessibleInfo plain;
546
+ plain.role = "button";
547
+ const auto bare = providerFor(plain);
548
+ EXPECT(bare != nullptr);
549
+ if (bare) {
550
+ EXPECT_EQ(propertyType(bare, UIA_RangeValueValuePropertyId), VT_EMPTY);
551
+ EXPECT_EQ(propertyType(bare, UIA_RangeValueIsReadOnlyPropertyId), VT_EMPTY);
552
+ }
553
+ }
554
+
555
+ TEST(accessibility_value_text_is_its_own_property) {
556
+ RnAccessibleInfo info;
557
+ info.role = "adjustable";
558
+ info.value.now = 1530;
559
+ info.value.text = "half past three";
560
+
561
+ const auto provider = providerFor(info);
562
+ EXPECT(provider != nullptr);
563
+ if (!provider) {
564
+ return;
565
+ }
566
+ // The text a screen reader reads instead of the number, which UIA keeps in
567
+ // the Value pattern rather than RangeValue -- so both are answered and
568
+ // neither replaces the other.
569
+ EXPECT_EQ(stringProperty(provider, UIA_ValueValuePropertyId),
570
+ std::string("half past three"));
571
+ EXPECT_EQ(propertyType(provider, UIA_RangeValueValuePropertyId), VT_R8);
572
+ }
573
+
574
+ // A value on its own makes a view worth reporting, which is the one change this
575
+ // made to what counts as an accessibility element: a <View> that says where it
576
+ // is in a range means to be heard, even if it named no role.
577
+ TEST(accessibility_a_value_alone_is_enough_to_be_an_element) {
578
+ RnAccessibleInfo info;
579
+ info.value.now = 2;
580
+ info.value.max = 5;
581
+ EXPECT(basalt::win32::isAccessibilityElement(info));
582
+
583
+ RnAccessibleInfo scaffolding;
584
+ EXPECT(!basalt::win32::isAccessibilityElement(scaffolding));
585
+ }