basalt-win32 0.0.0-rc5 → 0.1.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 (44) hide show
  1. package/native/CMakeLists.txt +37 -6
  2. package/native/tests/test_win32_accessibility.cpp +314 -0
  3. package/native/tests/test_win32_animated_image.cpp +351 -0
  4. package/native/tests/test_win32_clipboard.cpp +1 -1
  5. package/native/tests/test_win32_hittest.cpp +340 -0
  6. package/native/tests/test_win32_image.cpp +131 -0
  7. package/native/tests/test_win32_input.cpp +150 -0
  8. package/native/tests/test_win32_mounting.cpp +347 -0
  9. package/native/tests/test_win32_paint.cpp +1295 -1
  10. package/native/tests/test_win32_scrollview.cpp +265 -3
  11. package/native/tests/test_win32_selection.cpp +215 -0
  12. package/native/tests/test_win32_text.cpp +1198 -0
  13. package/native/tests/test_win32_textinput.cpp +988 -3
  14. package/native/win32/ComponentRegistryWin32.cpp +20 -0
  15. package/native/win32/DirectWriteLayout.cpp +402 -33
  16. package/native/win32/DirectWriteLayout.h +43 -2
  17. package/native/win32/DirectWriteLayoutManager.cpp +40 -10
  18. package/native/win32/PlatformServicesWin32.cpp +118 -17
  19. package/native/win32/RnWin32Accessible.cpp +86 -1
  20. package/native/win32/RnWin32Accessible.h +39 -0
  21. package/native/win32/RnWin32Image.cpp +330 -7
  22. package/native/win32/RnWin32Image.h +49 -3
  23. package/native/win32/RnWin32TextLayout.cpp +1105 -36
  24. package/native/win32/RnWin32TextLayout.h +231 -1
  25. package/native/win32/RnWin32View.cpp +1714 -39
  26. package/native/win32/RnWin32View.h +421 -1
  27. package/native/win32/Win32Clip.h +37 -1
  28. package/native/win32/Win32Cursors.cpp +309 -0
  29. package/native/win32/Win32Cursors.h +121 -0
  30. package/native/win32/Win32ImageLoader.cpp +44 -12
  31. package/native/win32/Win32ImageLoader.h +30 -1
  32. package/native/win32/Win32InputScopes.h +92 -0
  33. package/native/win32/Win32MenuBar.cpp +30 -0
  34. package/native/win32/Win32MountingManager.cpp +580 -25
  35. package/native/win32/Win32MountingManager.h +40 -0
  36. package/native/win32/Win32Offscreen.h +53 -0
  37. package/native/win32/Win32ScrollView.cpp +203 -0
  38. package/native/win32/Win32ScrollView.h +48 -0
  39. package/native/win32/Win32TextInput.cpp +497 -12
  40. package/native/win32/Win32TextInput.h +109 -0
  41. package/native/win32/Win32TouchDispatcher.cpp +135 -1
  42. package/native/win32/Win32TouchDispatcher.h +48 -0
  43. package/native/win32/main_win32.cpp +278 -37
  44. package/package.json +4 -3
@@ -159,24 +159,33 @@ add_library(basalt_win32_view STATIC
159
159
  # why this is in the view target rather than beside the demo: it is what the
160
160
  # rendering assertions in tests/test_win32_paint.cpp are built on.
161
161
  win32/Win32Snapshot.cpp
162
+ # The cursors Windows has no stock art for -- six CSS keywords drawn here as
163
+ # DIBs. Header-only until they were drawn; see Win32Cursors.h.
164
+ win32/Win32Cursors.cpp
162
165
  # Where a hidden title bar's pieces are -- the caption buttons and the drag
163
166
  # regions. Pure geometry over the view tree, so it is tested here with no
164
167
  # window and no React Native.
165
168
  win32/Win32TitleBarLayout.cpp)
166
169
  target_compile_options(basalt_win32_view PRIVATE ${BASALT_WIN32_WARNINGS})
167
170
  target_include_directories(basalt_win32_view PUBLIC win32)
168
- # core/FocusRing.h and core/Backface.h, and nothing else from there. Both are
169
- # header-only and have no React Native in them, so taking them does not make
170
- # this target depend on the half that does -- and the alternative is a focus
171
- # ring that is a different blue on each desktop, and three toolkits each with
172
- # their own idea of which way a rotated card is facing.
171
+ # The header-only part of core, and only that: FocusRing.h, Backface.h,
172
+ # ImageAnimation.h and ShadowShape.h so far. None of them has React Native in
173
+ # it, so taking them does not make this target depend on the half that does --
174
+ # and the alternative is a focus ring that is a different blue on each desktop,
175
+ # three toolkits each with their own idea of which way a rotated card is facing,
176
+ # a GIF paced differently on each, and a hand-rolled copy of CSS's shadow-corner
177
+ # cubic per platform.
173
178
  #
174
179
  # Header-only is the constraint, not a preference: this library links no core,
175
180
  # so anything it takes from there has to be `inline`.
176
181
  target_include_directories(basalt_win32_view PUBLIC
177
182
  ${CMAKE_CURRENT_SOURCE_DIR}/../../basalt-core/native/core)
183
+ # dxguid is for the effect CLSIDs, and it is the kind of thing that only a
184
+ # linker tells you: `CLSID_D2D1Shadow` and its siblings are `DEFINE_GUID`
185
+ # declarations in d2d1effects.h, so the symbols live in that library rather
186
+ # than in d2d1. Without it a text shadow compiles and fails to link.
178
187
  target_link_libraries(basalt_win32_view PUBLIC
179
- d2d1 dwrite windowscodecs uiautomationcore ole32 oleaut32 user32 shcore)
188
+ d2d1 dwrite windowscodecs uiautomationcore ole32 oleaut32 user32 shcore dxguid)
180
189
 
181
190
  add_executable(demo_layout_win32 win32/demo_layout_win32.cpp)
182
191
  target_compile_options(demo_layout_win32 PRIVATE ${BASALT_WIN32_WARNINGS})
@@ -343,6 +352,14 @@ if(TARGET basalt_core)
343
352
  # This host is still the only one that can check the table, and the
344
353
  # Windows CI shards, which do have React Native, still do.
345
354
  tests/test_win32_key_events.cpp
355
+ # Selecting text with a pointer, here for the same reason and found the
356
+ # same way: it drives a Win32TouchDispatcher and asserts against
357
+ # core/TextSelection.h, both of which speak React Native, and it sat in
358
+ # the unconditional set above from the day it was written. The MSBuild job
359
+ # is the only build without a React Native, so it failed there and nowhere
360
+ # else -- the second time this file has learnt that, which is why the
361
+ # rule is now in the comment above rather than in a commit message.
362
+ tests/test_win32_selection.cpp
346
363
  win32/Win32KeyEvents.cpp
347
364
  # A package's own tests; see the AppKit package for why they sit with core's.
348
365
  ${BASALT_PACKAGE_TESTS}
@@ -353,6 +370,11 @@ if(TARGET basalt_core)
353
370
  # The image loader: the cache, the failure paths, and the two lifetimes.
354
371
  # In the RN-dependent set because the fetch is core/ImageBytes.cpp.
355
372
  tests/test_win32_imageloader.cpp
373
+ # Animated GIFs: the frames WIC decodes and the pacing core shares. Here
374
+ # for the same reason as the loader's own tests -- the fixtures are `data:`
375
+ # URIs, read by core/ImageBytes.cpp -- even though most of the file is the
376
+ # view layer's.
377
+ tests/test_win32_animated_image.cpp
356
378
  # The touch state machine and the gesture handover, asking what
357
379
  # tests/test_appkit_input.mm asks of AppKit.
358
380
  tests/test_win32_input.cpp
@@ -383,6 +405,15 @@ endif()
383
405
  # needs onScroll and scroll state, and <TextInput>, which needs an EDIT peer.
384
406
  # ---------------------------------------------------------------------------
385
407
 
408
+ # Without React Native there is no basalt_core to link, and the harness calls
409
+ # `basalt::installCrashHandler` whatever it is linked against -- so this build
410
+ # compiles that one file itself. The MSBuild job in release.yml is the only
411
+ # build that gets here, which is why the link error it answered with was
412
+ # invisible to every CI run.
413
+ if(NOT TARGET basalt_core)
414
+ list(APPEND BASALT_WIN32_TEST_SOURCES ${BASALT_CRASH_HANDLER})
415
+ endif()
416
+
386
417
  add_executable(basalt_win32_tests ${BASALT_WIN32_TEST_SOURCES})
387
418
  target_compile_options(basalt_win32_tests PRIVATE ${BASALT_WIN32_WARNINGS})
388
419
  target_include_directories(basalt_win32_tests PRIVATE ${BASALT_TEST_INCLUDE})
@@ -14,6 +14,7 @@
14
14
  #include "TestHarness.h"
15
15
 
16
16
  #include "RnWin32Accessible.h"
17
+ #include "RnWin32TextLayout.h"
17
18
  #include "RnWin32View.h"
18
19
  // For narrow(): UIA answers in BSTRs and these assertions are written in UTF-8,
19
20
  // like the rest of the project.
@@ -36,6 +37,8 @@
36
37
  using Microsoft::WRL::ComPtr;
37
38
  using basalt::win32::RnAccessibleFlag;
38
39
  using basalt::win32::RnAccessibleInfo;
40
+ using basalt::win32::RnTextStyle;
41
+ using basalt::win32::RnWin32TextLayout;
39
42
  using basalt::win32::RnWin32View;
40
43
 
41
44
  namespace {
@@ -269,3 +272,314 @@ TEST(accessibility_role_can_change_after_mount) {
269
272
  asCheckbox.Attach(view->createAccessibleProvider());
270
273
  EXPECT_EQ(controlTypeOf(asCheckbox), static_cast<long>(UIA_CheckBoxControlTypeId));
271
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
+ }