basalt-core 0.0.0-rc5 → 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 (149) hide show
  1. package/dist/cli/basalt.d.ts.map +1 -1
  2. package/dist/cli/basalt.js +18 -1
  3. package/dist/cli/basalt.js.map +1 -1
  4. package/dist/cli/bundle-cli.js +0 -0
  5. package/dist/cli/desktop.d.ts.map +1 -1
  6. package/dist/cli/desktop.js +53 -4
  7. package/dist/cli/desktop.js.map +1 -1
  8. package/dist/cli/packageApp.d.ts.map +1 -1
  9. package/dist/cli/packageApp.js +7 -1
  10. package/dist/cli/packageApp.js.map +1 -1
  11. package/dist/metro-config.d.ts.map +1 -1
  12. package/dist/metro-config.js +53 -5
  13. package/dist/metro-config.js.map +1 -1
  14. package/dist/src/KeyHandler.d.ts +1 -1
  15. package/dist/src/KeyHandler.d.ts.map +1 -1
  16. package/dist/src/KeyHandler.js.map +1 -1
  17. package/dist/src/index.d.ts +5 -0
  18. package/dist/src/index.d.ts.map +1 -1
  19. package/dist/src/overrides/Alert.d.ts +1 -1
  20. package/dist/src/overrides/Alert.js +1 -1
  21. package/dist/src/overrides/BaseViewConfig.d.ts +10 -3
  22. package/dist/src/overrides/BaseViewConfig.d.ts.map +1 -1
  23. package/dist/src/overrides/BaseViewConfig.js +21 -3
  24. package/dist/src/overrides/BaseViewConfig.js.map +1 -1
  25. package/dist/src/overrides/Button.d.ts +81 -0
  26. package/dist/src/overrides/Button.d.ts.map +1 -0
  27. package/dist/src/overrides/Button.js +109 -0
  28. package/dist/src/overrides/Button.js.map +1 -0
  29. package/dist/src/overrides/ScrollView.d.ts +25 -2
  30. package/dist/src/overrides/ScrollView.d.ts.map +1 -1
  31. package/dist/src/overrides/ScrollView.js +64 -7
  32. package/dist/src/overrides/ScrollView.js.map +1 -1
  33. package/dist/src/overrides/Settings.d.ts +81 -0
  34. package/dist/src/overrides/Settings.d.ts.map +1 -0
  35. package/dist/src/overrides/Settings.js +120 -0
  36. package/dist/src/overrides/Settings.js.map +1 -0
  37. package/dist/src/overrides/processDecelerationRate.d.ts +20 -0
  38. package/dist/src/overrides/processDecelerationRate.d.ts.map +1 -0
  39. package/dist/src/overrides/processDecelerationRate.js +31 -0
  40. package/dist/src/overrides/processDecelerationRate.js.map +1 -0
  41. package/dist/src/useContextMenu.d.ts +57 -6
  42. package/dist/src/useContextMenu.d.ts.map +1 -1
  43. package/dist/src/useContextMenu.js +34 -1
  44. package/dist/src/useContextMenu.js.map +1 -1
  45. package/dist/tsconfig.tsbuildinfo +1 -1
  46. package/native/CMakeLists.txt +85 -1
  47. package/native/cmake/Skia.cmake +29 -6
  48. package/native/core/ActionSheet.cpp +75 -0
  49. package/native/core/ActionSheet.h +90 -0
  50. package/native/core/AppStateHost.cpp +76 -0
  51. package/native/core/AppStateHost.h +68 -0
  52. package/native/core/BackgroundLayers.h +282 -0
  53. package/native/core/BlendModes.h +67 -0
  54. package/native/core/CoreModules.cpp +380 -11
  55. package/native/core/CoreModules.h +140 -4
  56. package/native/core/CrashHandler.cpp +246 -0
  57. package/native/core/CrashHandler.h +65 -0
  58. package/native/core/CursorNames.h +110 -0
  59. package/native/core/DevMenu.cpp +6 -5
  60. package/native/core/DeviceMetrics.cpp +87 -0
  61. package/native/core/DeviceMetrics.h +80 -0
  62. package/native/core/Filters.h +336 -0
  63. package/native/core/FontFitting.h +183 -0
  64. package/native/core/FontScaling.h +153 -0
  65. package/native/core/FontVariants.h +148 -0
  66. package/native/core/Gradients.h +523 -0
  67. package/native/core/ImageAnimation.h +109 -0
  68. package/native/core/ImageBytes.cpp +28 -1
  69. package/native/core/ImageBytes.h +17 -1
  70. package/native/core/ImageProgress.h +85 -0
  71. package/native/core/LabelRegistry.h +189 -0
  72. package/native/core/LegacyShadow.h +90 -0
  73. package/native/core/LiveRegions.h +114 -0
  74. package/native/core/MenuModule.cpp +35 -4
  75. package/native/core/MountingWalk.h +125 -2
  76. package/native/core/PlatformServices.h +187 -3
  77. package/native/core/SafeAreaComponent.cpp +115 -0
  78. package/native/core/SafeAreaComponent.h +105 -0
  79. package/native/core/ScreensComponent.cpp +201 -0
  80. package/native/core/ScreensComponent.h +216 -0
  81. package/native/core/ScrollBounds.cpp +8 -0
  82. package/native/core/ScrollBounds.h +14 -0
  83. package/native/core/ScrollIndicator.h +67 -0
  84. package/native/core/ScrollMomentum.cpp +24 -0
  85. package/native/core/ScrollMomentum.h +14 -0
  86. package/native/core/ScrollSnap.cpp +121 -12
  87. package/native/core/ScrollSnap.h +45 -0
  88. package/native/core/ScrollVisiblePosition.cpp +68 -0
  89. package/native/core/ScrollVisiblePosition.h +101 -0
  90. package/native/core/SettingsStore.cpp +199 -0
  91. package/native/core/SettingsStore.h +127 -0
  92. package/native/core/ShadowShape.h +38 -0
  93. package/native/core/SkiaViewComponent.cpp +11 -0
  94. package/native/core/SkiaViewComponent.h +50 -0
  95. package/native/core/SystemLocale.cpp +103 -0
  96. package/native/core/SystemLocale.h +86 -0
  97. package/native/core/TestDialog.cpp +45 -6
  98. package/native/core/TestDialog.h +15 -0
  99. package/native/core/TextAlignments.h +240 -0
  100. package/native/core/TextChecking.h +133 -0
  101. package/native/core/TextColors.h +83 -0
  102. package/native/core/TextDecorations.h +101 -0
  103. package/native/core/TextDirection.cpp +242 -0
  104. package/native/core/TextDirection.h +88 -0
  105. package/native/core/TextHighlight.h +30 -0
  106. package/native/core/TextSelection.cpp +97 -0
  107. package/native/core/TextSelection.h +161 -0
  108. package/native/core/TextShadows.h +96 -0
  109. package/native/core/TextVerticalAlign.h +33 -0
  110. package/native/core/UIManagerAccess.cpp +36 -0
  111. package/native/core/UIManagerAccess.h +28 -0
  112. package/native/core/WindowBoundsCache.cpp +58 -1
  113. package/native/core/WindowControl.h +24 -0
  114. package/native/core/WritingDirections.h +52 -0
  115. package/native/core/portability_probe.cpp +7 -0
  116. package/native/tests/RenamedFontFile.h +161 -0
  117. package/native/tests/TestHarness.cpp +18 -0
  118. package/native/tests/test_action_sheet.cpp +157 -0
  119. package/native/tests/test_app_state.cpp +60 -0
  120. package/native/tests/test_background_layers.cpp +281 -0
  121. package/native/tests/test_blend_modes.cpp +79 -0
  122. package/native/tests/test_blobs.cpp +1 -1
  123. package/native/tests/test_device_metrics.cpp +126 -0
  124. package/native/tests/test_filters.cpp +271 -0
  125. package/native/tests/test_font_fitting.cpp +172 -0
  126. package/native/tests/test_font_scaling.cpp +127 -0
  127. package/native/tests/test_font_variants.cpp +84 -0
  128. package/native/tests/test_gradients.cpp +390 -0
  129. package/native/tests/test_image_animation.cpp +105 -0
  130. package/native/tests/test_image_progress.cpp +94 -0
  131. package/native/tests/test_label_registry.cpp +262 -0
  132. package/native/tests/test_legacy_shadow.cpp +165 -0
  133. package/native/tests/test_live_regions.cpp +108 -0
  134. package/native/tests/test_menu_entries.cpp +192 -0
  135. package/native/tests/test_momentum.cpp +45 -0
  136. package/native/tests/test_scroll_bounds.cpp +42 -0
  137. package/native/tests/test_scroll_indicator.cpp +61 -0
  138. package/native/tests/test_scroll_snap.cpp +129 -0
  139. package/native/tests/test_scroll_visible_position.cpp +121 -0
  140. package/native/tests/test_settings_store.cpp +257 -0
  141. package/native/tests/test_shadow_shape.cpp +47 -0
  142. package/native/tests/test_system_locale.cpp +36 -0
  143. package/native/tests/test_text_alignment.cpp +299 -0
  144. package/native/tests/test_text_checking.cpp +31 -0
  145. package/native/tests/test_text_colors.cpp +92 -0
  146. package/native/tests/test_text_decorations.cpp +100 -0
  147. package/native/tests/test_text_selection.cpp +168 -0
  148. package/native/tests/test_text_shadows.cpp +132 -0
  149. package/package.json +4 -2
@@ -0,0 +1,105 @@
1
+ // core/ImageAnimation.h: the clamp and the frame arithmetic.
2
+ //
3
+ // Both hosts animate from an elapsed time rather than from a clock of their
4
+ // own, so this is where the awkward cases are asserted and each host's suite
5
+ // then asks only whether the right pixels arrived.
6
+
7
+ #include "TestHarness.h"
8
+
9
+ #include "ImageAnimation.h"
10
+
11
+ #include <sstream>
12
+ #include <vector>
13
+
14
+ namespace {
15
+
16
+ const std::vector<unsigned> kTwoFrames{100U, 100U};
17
+
18
+ } // namespace
19
+
20
+ TEST(image_animation_a_single_frame_is_finished_before_it_starts) {
21
+ const auto step = basalt::imageAnimationStep({100U}, 0U, 0U);
22
+ EXPECT_EQ(step.frame, std::size_t(0));
23
+ EXPECT(step.finished);
24
+ EXPECT_EQ(step.nextInMs, 0U);
25
+
26
+ // And no frames at all, which is what a still image decodes to.
27
+ const auto none = basalt::imageAnimationStep({}, 0U, 500U);
28
+ EXPECT_EQ(none.frame, std::size_t(0));
29
+ EXPECT(none.finished);
30
+ }
31
+
32
+ TEST(image_animation_the_frame_follows_the_elapsed_time) {
33
+ const auto at = [](std::uint64_t elapsed) {
34
+ return basalt::imageAnimationStep(kTwoFrames, 0U, elapsed);
35
+ };
36
+
37
+ EXPECT_EQ(at(0U).frame, std::size_t(0));
38
+ EXPECT_EQ(at(99U).frame, std::size_t(0));
39
+ EXPECT_EQ(at(100U).frame, std::size_t(1));
40
+ EXPECT_EQ(at(199U).frame, std::size_t(1));
41
+ // A loop count of zero is forever, so the third hundred milliseconds is the
42
+ // first frame again.
43
+ EXPECT_EQ(at(200U).frame, std::size_t(0));
44
+ EXPECT(!at(200U).finished);
45
+ }
46
+
47
+ TEST(image_animation_says_when_the_next_frame_is_due) {
48
+ // What a host sets its timer to. The whole delay at the start of a frame, and
49
+ // what is left of it in the middle.
50
+ EXPECT_EQ(basalt::imageAnimationStep(kTwoFrames, 0U, 0U).nextInMs, 100U);
51
+ EXPECT_EQ(basalt::imageAnimationStep(kTwoFrames, 0U, 60U).nextInMs, 40U);
52
+ EXPECT_EQ(basalt::imageAnimationStep(kTwoFrames, 0U, 199U).nextInMs, 1U);
53
+ }
54
+
55
+ TEST(image_animation_frames_have_their_own_delays) {
56
+ // The common shape of a hand-drawn GIF: one long frame and several short
57
+ // ones. A single delay for the whole file would put the boundaries in the
58
+ // wrong places, which is invisible in a two-frame test where they are equal.
59
+ const std::vector<unsigned> uneven{500U, 50U, 50U};
60
+
61
+ const auto at = [&](std::uint64_t elapsed) {
62
+ return basalt::imageAnimationStep(uneven, 0U, elapsed).frame;
63
+ };
64
+ EXPECT_EQ(at(0U), std::size_t(0));
65
+ EXPECT_EQ(at(499U), std::size_t(0));
66
+ EXPECT_EQ(at(500U), std::size_t(1));
67
+ EXPECT_EQ(at(549U), std::size_t(1));
68
+ EXPECT_EQ(at(550U), std::size_t(2));
69
+ EXPECT_EQ(at(600U), std::size_t(0));
70
+ }
71
+
72
+ TEST(image_animation_a_delay_of_zero_becomes_a_tenth_of_a_second) {
73
+ // The browsers' rule, and the reason a GIF written with no delay at all does
74
+ // not spin at the display's refresh rate.
75
+ EXPECT_EQ(basalt::clampedImageFrameDelay(0U), 100U);
76
+ EXPECT_EQ(basalt::clampedImageFrameDelay(10U), 100U);
77
+ EXPECT_EQ(basalt::clampedImageFrameDelay(11U), 11U);
78
+ EXPECT_EQ(basalt::clampedImageFrameDelay(1000U), 1000U);
79
+
80
+ // And through the arithmetic, where it decides the boundary: without the
81
+ // clamp two zero-delay frames have no cycle to divide and the first frame
82
+ // would be the only one ever shown.
83
+ const std::vector<unsigned> instant{0U, 0U};
84
+ EXPECT_EQ(basalt::imageAnimationStep(instant, 0U, 0U).frame, std::size_t(0));
85
+ EXPECT_EQ(basalt::imageAnimationStep(instant, 0U, 100U).frame, std::size_t(1));
86
+ EXPECT_EQ(basalt::imageAnimationStep(instant, 0U, 100U).nextInMs, 100U);
87
+ }
88
+
89
+ TEST(image_animation_a_loop_count_stops_on_the_last_frame) {
90
+ const auto at = [](std::uint64_t elapsed) {
91
+ return basalt::imageAnimationStep(kTwoFrames, 2U, elapsed);
92
+ };
93
+
94
+ EXPECT_EQ(at(0U).frame, std::size_t(0));
95
+ EXPECT(!at(0U).finished);
96
+ // Second pass, still running.
97
+ EXPECT_EQ(at(300U).frame, std::size_t(1));
98
+ EXPECT(!at(300U).finished);
99
+ // Two passes of 200ms are over, so it stops, and on the frame the animation
100
+ // was drawn to end on rather than back at the first.
101
+ EXPECT_EQ(at(400U).frame, std::size_t(1));
102
+ EXPECT(at(400U).finished);
103
+ EXPECT_EQ(at(400U).nextInMs, 0U);
104
+ EXPECT(at(100000U).finished);
105
+ }
@@ -0,0 +1,94 @@
1
+ // Which image-fetch ticks become `<Image onProgress>` events.
2
+ //
3
+ // The wiring -- libcurl's callback, the fetch thread, the event emitter -- is
4
+ // each host's and is proved end to end by the scenario that serves an image
5
+ // slowly over http. What is tested here is the decision: see
6
+ // core/ImageProgress.h.
7
+
8
+ #include "TestHarness.h"
9
+
10
+ #include "ImageProgress.h"
11
+
12
+ #include <sstream>
13
+
14
+ using basalt::ImageProgressTicker;
15
+
16
+ TEST(image_progress_reports_each_whole_percent) {
17
+ ImageProgressTicker ticker;
18
+
19
+ const auto first = ticker.tick(0, 1000);
20
+ EXPECT(first.has_value());
21
+ EXPECT_EQ((long)(*first * 1000), 0L);
22
+
23
+ const auto tenth = ticker.tick(100, 1000);
24
+ EXPECT(tenth.has_value());
25
+ EXPECT_EQ((long)(*tenth * 1000), 100L);
26
+ EXPECT_EQ((long)ticker.reportedPercent(), 10L);
27
+
28
+ const auto whole = ticker.tick(1000, 1000);
29
+ EXPECT(whole.has_value());
30
+ EXPECT_EQ((long)(*whole * 1000), 1000L);
31
+ EXPECT_EQ((long)ticker.reportedPercent(), 100L);
32
+ }
33
+
34
+ // The whole reason the gate exists. libcurl calls its progress function once
35
+ // per socket read, which for a large image is thousands of times; without this
36
+ // each one would be a Fabric event and a React render.
37
+ TEST(image_progress_says_nothing_twice) {
38
+ ImageProgressTicker ticker;
39
+
40
+ EXPECT(ticker.tick(500, 100000).has_value());
41
+ // Five more bytes of a hundred thousand is the same percentage, and a bar
42
+ // cannot draw the difference.
43
+ EXPECT(!ticker.tick(501, 100000).has_value());
44
+ EXPECT(!ticker.tick(502, 100000).has_value());
45
+ EXPECT(!ticker.tick(999, 100000).has_value());
46
+ // And the next percentage is.
47
+ EXPECT(ticker.tick(1000, 100000).has_value());
48
+ }
49
+
50
+ // A chunked response has no Content-Length, which is not an error: the event
51
+ // carries the bytes and a zero total, and an app shows bytes rather than a
52
+ // fraction. Every tick, because there is no percentage to throttle on.
53
+ TEST(image_progress_an_unknown_total_reports_every_tick) {
54
+ ImageProgressTicker ticker;
55
+
56
+ const auto first = ticker.tick(1000, 0);
57
+ EXPECT(first.has_value());
58
+ EXPECT_EQ((long)(*first * 1000), 0L);
59
+ EXPECT(ticker.tick(1001, 0).has_value());
60
+ EXPECT(ticker.tick(1002, 0).has_value());
61
+ }
62
+
63
+ // libcurl's opening tick, before the response headers have been read. Nothing
64
+ // has arrived and nothing is known, so there is nothing to tell an app.
65
+ TEST(image_progress_the_opening_tick_is_not_an_event) {
66
+ ImageProgressTicker ticker;
67
+
68
+ EXPECT(!ticker.tick(0, 0).has_value());
69
+ EXPECT_EQ((long)ticker.reportedPercent(), -1L);
70
+ // And the first real tick still is one.
71
+ EXPECT(ticker.tick(0, 1000).has_value());
72
+ }
73
+
74
+ // A server that sends more than it promised. `progress` is what an app
75
+ // multiplies its bar's width by, so it may not exceed one.
76
+ TEST(image_progress_never_passes_one) {
77
+ ImageProgressTicker ticker;
78
+
79
+ const auto over = ticker.tick(2000, 1000);
80
+ EXPECT(over.has_value());
81
+ EXPECT_EQ((long)(*over * 1000), 1000L);
82
+ EXPECT_EQ((long)ticker.reportedPercent(), 100L);
83
+ }
84
+
85
+ // Negative counts, which libcurl has been seen to report for a transfer it has
86
+ // not started: not an event's worth of information, and certainly not a
87
+ // negative bar.
88
+ TEST(image_progress_a_negative_count_is_zero) {
89
+ ImageProgressTicker ticker;
90
+
91
+ const auto negative = ticker.tick(-5, 1000);
92
+ EXPECT(negative.has_value());
93
+ EXPECT_EQ((long)(*negative * 1000), 0L);
94
+ }
@@ -0,0 +1,262 @@
1
+ // Resolving `accessibilityLabelledBy`, which is a lookup this project did not
2
+ // have: a view names another by its `nativeID` and the registry is keyed by tag.
3
+ //
4
+ // What is asserted here is the ordering, because that is the whole difficulty.
5
+ // Fabric mounts in tree order, so a field labelled by the text after it is
6
+ // mounted before its label exists; a relation resolved once at mount time would
7
+ // find nothing and stay wrong for the life of the screen. See core/LabelRegistry.h.
8
+
9
+ #include "TestHarness.h"
10
+
11
+ #include "LabelRegistry.h"
12
+
13
+ #include <sstream>
14
+
15
+ using basalt::LabelRegistry;
16
+
17
+ namespace {
18
+
19
+ // The resolution for one tag out of a batch of changes, and whether it was in
20
+ // there at all.
21
+ bool labelsFor(const std::vector<LabelRegistry::Resolved> &changes,
22
+ LabelRegistry::Tag tag,
23
+ std::vector<LabelRegistry::Tag> *out) {
24
+ for (const auto &change : changes) {
25
+ if (change.tag == tag) {
26
+ *out = change.labels;
27
+ return true;
28
+ }
29
+ }
30
+ return false;
31
+ }
32
+
33
+ } // namespace
34
+
35
+ TEST(labels_resolve_an_id_to_the_view_that_has_it) {
36
+ LabelRegistry registry;
37
+ registry.setNativeId(20, "name-label");
38
+ registry.setLabelledBy(10, {"name-label"});
39
+
40
+ std::vector<LabelRegistry::Tag> labels;
41
+ EXPECT(labelsFor(registry.changes(), 10, &labels));
42
+ EXPECT_EQ((long)labels.size(), 1L);
43
+ EXPECT_EQ((long)labels[0], 20L);
44
+ }
45
+
46
+ // The case that makes this a registry rather than a lookup: the label mounts
47
+ // second, which is what happens whenever the label comes after the field in the
48
+ // tree.
49
+ TEST(labels_a_relation_waits_for_an_id_that_has_not_arrived) {
50
+ LabelRegistry registry;
51
+ registry.setLabelledBy(10, {"name-label"});
52
+
53
+ std::vector<LabelRegistry::Tag> labels;
54
+ // Reported, and resolving to nothing: the host has a relation to reset rather
55
+ // than a reference to a view that does not exist.
56
+ EXPECT(labelsFor(registry.changes(), 10, &labels));
57
+ EXPECT_EQ((long)labels.size(), 0L);
58
+
59
+ registry.setNativeId(20, "name-label");
60
+ EXPECT(labelsFor(registry.changes(), 10, &labels));
61
+ EXPECT_EQ((long)labels.size(), 1L);
62
+ EXPECT_EQ((long)labels[0], 20L);
63
+ }
64
+
65
+ // Order is the app's. A field labelled by a heading and a hint is read in that
66
+ // order, and sorting them would change what is said.
67
+ TEST(labels_keep_the_order_the_app_wrote) {
68
+ LabelRegistry registry;
69
+ registry.setNativeId(30, "hint");
70
+ registry.setNativeId(20, "heading");
71
+ registry.setLabelledBy(10, {"heading", "hint"});
72
+
73
+ std::vector<LabelRegistry::Tag> labels;
74
+ EXPECT(labelsFor(registry.changes(), 10, &labels));
75
+ EXPECT_EQ((long)labels.size(), 2L);
76
+ EXPECT_EQ((long)labels[0], 20L);
77
+ EXPECT_EQ((long)labels[1], 30L);
78
+ }
79
+
80
+ // Nothing is reported twice. Applying an unchanged relation again tells
81
+ // assistive technology that something happened when nothing did.
82
+ TEST(labels_an_unchanged_relation_is_not_reported_again) {
83
+ LabelRegistry registry;
84
+ registry.setNativeId(20, "name-label");
85
+ registry.setLabelledBy(10, {"name-label"});
86
+ EXPECT_EQ((long)registry.changes().size(), 1L);
87
+ EXPECT_EQ((long)registry.changes().size(), 0L);
88
+
89
+ // Re-sending the same props, which every mutation does, changes nothing.
90
+ registry.setNativeId(20, "name-label");
91
+ registry.setLabelledBy(10, {"name-label"});
92
+ EXPECT_EQ((long)registry.changes().size(), 0L);
93
+ }
94
+
95
+ // A label that goes away leaves a dangling reference, and a screen reader
96
+ // following it would read a view that is no longer on screen.
97
+ TEST(labels_a_label_that_is_unmounted_empties_the_relation) {
98
+ LabelRegistry registry;
99
+ registry.setNativeId(20, "name-label");
100
+ registry.setLabelledBy(10, {"name-label"});
101
+ registry.changes();
102
+
103
+ registry.forget(20);
104
+ std::vector<LabelRegistry::Tag> labels;
105
+ EXPECT(labelsFor(registry.changes(), 10, &labels));
106
+ EXPECT_EQ((long)labels.size(), 0L);
107
+ }
108
+
109
+ // And a view that stops asking is reported once, so the host can reset the
110
+ // relation, and then not again.
111
+ TEST(labels_a_view_that_stops_asking_is_reported_once) {
112
+ LabelRegistry registry;
113
+ registry.setNativeId(20, "name-label");
114
+ registry.setLabelledBy(10, {"name-label"});
115
+ registry.changes();
116
+
117
+ registry.setLabelledBy(10, {});
118
+ std::vector<LabelRegistry::Tag> labels;
119
+ EXPECT(labelsFor(registry.changes(), 10, &labels));
120
+ EXPECT_EQ((long)labels.size(), 0L);
121
+ EXPECT_EQ((long)registry.changes().size(), 0L);
122
+ }
123
+
124
+ // A view cannot label itself, which an app can ask for by giving one view both
125
+ // props. Honouring it would be a cycle for anything walking the relation.
126
+ TEST(labels_a_view_does_not_label_itself) {
127
+ LabelRegistry registry;
128
+ registry.setNativeId(10, "mine");
129
+ registry.setLabelledBy(10, {"mine"});
130
+
131
+ std::vector<LabelRegistry::Tag> labels;
132
+ EXPECT(labelsFor(registry.changes(), 10, &labels));
133
+ EXPECT_EQ((long)labels.size(), 0L);
134
+ }
135
+
136
+ // An id taken over by another view follows the new one. Two views with the same
137
+ // nativeID is an app bug; what matters is that the old one letting go does not
138
+ // take the id with it.
139
+ TEST(labels_an_id_that_moves_follows_the_view_that_has_it) {
140
+ LabelRegistry registry;
141
+ registry.setNativeId(20, "name-label");
142
+ registry.setLabelledBy(10, {"name-label"});
143
+ registry.changes();
144
+
145
+ registry.setNativeId(30, "name-label");
146
+ std::vector<LabelRegistry::Tag> labels;
147
+ EXPECT(labelsFor(registry.changes(), 10, &labels));
148
+ EXPECT_EQ((long)labels[0], 30L);
149
+
150
+ // The view that used to own it goes away without disturbing the new owner.
151
+ registry.forget(20);
152
+ EXPECT_EQ((long)registry.changes().size(), 0L);
153
+ }
154
+
155
+ // A view with no relation costs nothing, which is what `empty()` is for: the
156
+ // hosts ask it before doing any work per transaction.
157
+ TEST(labels_a_registry_with_no_relations_is_empty) {
158
+ LabelRegistry registry;
159
+ EXPECT(registry.empty());
160
+ registry.setNativeId(20, "name-label");
161
+ EXPECT(registry.empty());
162
+ registry.setLabelledBy(10, {"name-label"});
163
+ EXPECT(!registry.empty());
164
+ }
165
+
166
+ // But not while a relation it applied is still on a widget, which is the half
167
+ // that is easy to get wrong: a host checking `empty()` first would skip the one
168
+ // transaction that had to take the relation off, and it would stay there for the
169
+ // rest of the screen's life. Caught by writing the host side, not by this test.
170
+ TEST(labels_a_registry_is_not_empty_while_a_relation_is_still_applied) {
171
+ LabelRegistry registry;
172
+ registry.setNativeId(20, "name-label");
173
+ registry.setLabelledBy(10, {"name-label"});
174
+ registry.changes();
175
+
176
+ registry.setLabelledBy(10, {});
177
+ EXPECT(!registry.empty());
178
+ // Reported once, so the host can reset it, and then it really is empty.
179
+ EXPECT_EQ((long)registry.changes().size(), 1L);
180
+ EXPECT(registry.empty());
181
+ }
182
+
183
+ // `experimental_accessibilityOrder`: the second relation, which shares the
184
+ // `nativeID` index with the first. See core/LabelRegistry.h.
185
+ TEST(label_registry_an_order_resolves_in_the_order_it_was_given) {
186
+ basalt::LabelRegistry registry;
187
+ registry.setNativeId(1, "first");
188
+ registry.setNativeId(2, "second");
189
+ registry.setNativeId(3, "third");
190
+ // Deliberately not the mount order: the point of the prop is to override it.
191
+ registry.setAccessibilityOrder(10, {"third", "first", "second"});
192
+
193
+ const auto changes = registry.orderChanges();
194
+ EXPECT_EQ(changes.size(), std::size_t{1});
195
+ if (changes.size() == 1) {
196
+ EXPECT_EQ(changes[0].tag, 10);
197
+ EXPECT_EQ(changes[0].labels.size(), std::size_t{3});
198
+ EXPECT_EQ(changes[0].labels[0], 3);
199
+ EXPECT_EQ(changes[0].labels[1], 1);
200
+ EXPECT_EQ(changes[0].labels[2], 2);
201
+ }
202
+
203
+ // Reported once: applying an unchanged order again would tell assistive
204
+ // technology that something happened when nothing did.
205
+ EXPECT(registry.orderChanges().empty());
206
+ }
207
+
208
+ // An id that names nothing yet is dropped rather than losing the whole order,
209
+ // and is taken up when it arrives.
210
+ TEST(label_registry_an_order_takes_the_children_it_has) {
211
+ basalt::LabelRegistry registry;
212
+ registry.setNativeId(1, "first");
213
+ registry.setAccessibilityOrder(10, {"first", "later"});
214
+
215
+ auto changes = registry.orderChanges();
216
+ EXPECT_EQ(changes.size(), std::size_t{1});
217
+ EXPECT(changes.size() == 1 && changes[0].labels.size() == std::size_t{1});
218
+
219
+ registry.setNativeId(2, "later");
220
+ changes = registry.orderChanges();
221
+ EXPECT_EQ(changes.size(), std::size_t{1});
222
+ if (changes.size() == 1) {
223
+ EXPECT_EQ(changes[0].labels.size(), std::size_t{2});
224
+ EXPECT_EQ(changes[0].labels[1], 2);
225
+ }
226
+ }
227
+
228
+ // The two relations do not interfere: the same view can be labelled by one
229
+ // thing and order another, and each is reported on its own.
230
+ TEST(label_registry_the_two_relations_are_independent) {
231
+ basalt::LabelRegistry registry;
232
+ registry.setNativeId(1, "label");
233
+ registry.setNativeId(2, "child");
234
+ registry.setLabelledBy(10, {"label"});
235
+ registry.setAccessibilityOrder(10, {"child"});
236
+
237
+ const auto labels = registry.changes();
238
+ EXPECT_EQ(labels.size(), std::size_t{1});
239
+ EXPECT(labels.size() == 1 && labels[0].labels.size() == std::size_t{1}
240
+ && labels[0].labels[0] == 1);
241
+ // Asking for one does not consume the other.
242
+ const auto order = registry.orderChanges();
243
+ EXPECT_EQ(order.size(), std::size_t{1});
244
+ EXPECT(order.size() == 1 && order[0].labels.size() == std::size_t{1}
245
+ && order[0].labels[0] == 2);
246
+ }
247
+
248
+ // A view that stops asking is reported empty once, which is a host's cue to
249
+ // take the relation off the widget, and then forgotten.
250
+ TEST(label_registry_an_order_that_goes_away_is_reported_empty) {
251
+ basalt::LabelRegistry registry;
252
+ registry.setNativeId(1, "child");
253
+ registry.setAccessibilityOrder(10, {"child"});
254
+ EXPECT_EQ(registry.orderChanges().size(), std::size_t{1});
255
+
256
+ registry.setAccessibilityOrder(10, {});
257
+ const auto changes = registry.orderChanges();
258
+ EXPECT_EQ(changes.size(), std::size_t{1});
259
+ EXPECT(changes.size() == 1 && changes[0].labels.empty());
260
+ EXPECT(registry.orderChanges().empty());
261
+ EXPECT(registry.empty());
262
+ }
@@ -0,0 +1,165 @@
1
+ // The iOS shadow props as a CSS box shadow. See core/LegacyShadow.h.
2
+ //
3
+ // Four props that every pre-`boxShadow` React Native component sets, and which
4
+ // no host here read. The conversion is shared because the alternative is each
5
+ // host doubling a blur radius on its own, which is exactly the kind of arithmetic
6
+ // that comes out half as big on one platform and nobody notices for a year.
7
+
8
+ #include "TestHarness.h"
9
+
10
+ #include "LegacyShadow.h"
11
+
12
+ #include <sstream>
13
+
14
+ using facebook::react::BaseViewProps;
15
+ using facebook::react::BoxShadow;
16
+ using facebook::react::ColorComponents;
17
+
18
+ namespace {
19
+
20
+ // Props as React Native hands them over, with nothing set but the shadow: the
21
+ // defaults for `shadowOffset` and `shadowRadius` are React Native's own, which
22
+ // is what makes "set only a colour and an opacity" a case worth testing.
23
+ //
24
+ // A struct holding the props rather than a function returning them, which is
25
+ // not a style choice: React Native 0.86's `BaseViewProps` has an implicitly
26
+ // deleted copy constructor, so a helper that returns one by value does not
27
+ // compile there at all. 0.87's is copyable, which is why this went unnoticed
28
+ // until a build against the version Expo installs. Taking `.props` off a
29
+ // temporary is a reference into it and lives as long as the full expression.
30
+ struct Shadowed {
31
+ BaseViewProps props;
32
+
33
+ Shadowed(float red, float green, float blue, float alpha, double opacity) {
34
+ props.shadowColor =
35
+ facebook::react::colorFromComponents(ColorComponents{red, green, blue, alpha});
36
+ props.shadowOpacity = opacity;
37
+ }
38
+ };
39
+
40
+ ColorComponents componentsOf(const BoxShadow &shadow) {
41
+ return facebook::react::colorComponentsFromColor(shadow.color);
42
+ }
43
+
44
+ } // namespace
45
+
46
+ TEST(legacy_shadow_defaults_are_the_shadow_ios_would_draw) {
47
+ Shadowed shadowed(0.0F, 0.0F, 0.0F, 1.0F, 0.5);
48
+ BaseViewProps &props = shadowed.props;
49
+ const std::optional<BoxShadow> shadow = basalt::legacyShadow(props);
50
+ EXPECT(shadow.has_value());
51
+ if (!shadow.has_value()) {
52
+ return;
53
+ }
54
+
55
+ // React Native's own defaults: offset (0, -3), radius 3.
56
+ EXPECT_NEAR(shadow->offsetX, 0.0, 0.001);
57
+ EXPECT_NEAR(shadow->offsetY, -3.0, 0.001);
58
+ // Doubled, a CALayer radius being a standard deviation where CSS's is twice
59
+ // one. A conversion that forgot this draws a shadow half as soft, which is
60
+ // the sort of wrong that looks fine on its own and wrong beside the other
61
+ // host.
62
+ EXPECT_NEAR(shadow->blurRadius, 6.0, 0.001);
63
+ // No spread, and not inset: the legacy props cannot ask for either.
64
+ EXPECT_NEAR(shadow->spreadDistance, 0.0, 0.001);
65
+ EXPECT(!shadow->inset);
66
+ }
67
+
68
+ // `shadowOpacity` multiplies the colour's alpha, which is what
69
+ // `CALayer.shadowOpacity` does on top of `shadowColor`. Both halves matter: an
70
+ // opacity ignored gives a shadow twice as dark, and a colour alpha ignored gives
71
+ // one that cannot be made translucent by its colour.
72
+ TEST(legacy_shadow_opacity_multiplies_the_colours_alpha) {
73
+ const std::optional<BoxShadow> half =
74
+ basalt::legacyShadow(Shadowed(0.0F, 0.0F, 0.0F, 1.0F, 0.5).props);
75
+ EXPECT(half.has_value());
76
+ EXPECT_NEAR(componentsOf(*half).alpha, 0.5, 0.01);
77
+
78
+ const std::optional<BoxShadow> quarter =
79
+ basalt::legacyShadow(Shadowed(0.0F, 0.0F, 0.0F, 0.5F, 0.5).props);
80
+ EXPECT(quarter.has_value());
81
+ EXPECT_NEAR(componentsOf(*quarter).alpha, 0.25, 0.01);
82
+
83
+ // And the colour itself is the colour, which an alpha multiply can trample if
84
+ // it is applied to the wrong component.
85
+ const std::optional<BoxShadow> red =
86
+ basalt::legacyShadow(Shadowed(1.0F, 0.0F, 0.0F, 1.0F, 1.0).props);
87
+ EXPECT(red.has_value());
88
+ EXPECT_NEAR(componentsOf(*red).red, 1.0, 0.01);
89
+ EXPECT_NEAR(componentsOf(*red).green, 0.0, 0.01);
90
+ EXPECT_NEAR(componentsOf(*red).alpha, 1.0, 0.01);
91
+ }
92
+
93
+ // An opacity over one is a value React Native will pass straight through, and
94
+ // a colour cannot hold an alpha above one.
95
+ TEST(legacy_shadow_a_large_opacity_is_clamped) {
96
+ const std::optional<BoxShadow> shadow =
97
+ basalt::legacyShadow(Shadowed(0.0F, 0.0F, 0.0F, 1.0F, 4.0).props);
98
+ EXPECT(shadow.has_value());
99
+ EXPECT_NEAR(componentsOf(*shadow).alpha, 1.0, 0.01);
100
+ }
101
+
102
+ // The gates, which are what keeps every view in every app from gaining a shadow:
103
+ // `shadowOpacity` defaults to zero and `shadowColor` to nothing.
104
+ TEST(legacy_shadow_nothing_is_asked_for_by_default) {
105
+ const BaseViewProps plain;
106
+ EXPECT(!basalt::legacyShadow(plain).has_value());
107
+ EXPECT(basalt::allShadows(plain).empty());
108
+
109
+ // A colour with no opacity, which is the default for every styled view that
110
+ // sets `shadowColor` and forgets the rest.
111
+ EXPECT(!basalt::legacyShadow(Shadowed(0.0F, 0.0F, 0.0F, 1.0F, 0.0).props).has_value());
112
+ // An opacity with no colour.
113
+ BaseViewProps colourless;
114
+ colourless.shadowOpacity = 0.5;
115
+ EXPECT(!basalt::legacyShadow(colourless).has_value());
116
+ // And a transparent colour, which is a colour React Native did parse.
117
+ EXPECT(!basalt::legacyShadow(Shadowed(0.0F, 0.0F, 0.0F, 0.0F, 0.5).props).has_value());
118
+ }
119
+
120
+ // Both mechanisms at once, which iOS allows: the layer's own shadow and the
121
+ // extra layers `boxShadow` gets. The legacy one goes last, which is the back of
122
+ // the list, so adding a `boxShadow` to an old component cannot be hidden behind
123
+ // the shadow it already had.
124
+ TEST(legacy_shadow_goes_behind_the_css_shadows) {
125
+ Shadowed shadowed(0.0F, 0.0F, 0.0F, 1.0F, 0.5);
126
+ BaseViewProps &props = shadowed.props;
127
+ BoxShadow css;
128
+ css.offsetX = 1;
129
+ css.offsetY = 2;
130
+ css.blurRadius = 4;
131
+ css.color = facebook::react::colorFromComponents(ColorComponents{1.0F, 0.0F, 0.0F, 1.0F});
132
+ props.boxShadow = {css};
133
+
134
+ const std::vector<BoxShadow> all = basalt::allShadows(props);
135
+ EXPECT_EQ((int)all.size(), 2);
136
+ if (all.size() != 2) {
137
+ return;
138
+ }
139
+ // The CSS one first, as the app wrote it.
140
+ EXPECT_NEAR(all[0].offsetX, 1.0, 0.001);
141
+ EXPECT_NEAR(componentsOf(all[0]).red, 1.0, 0.01);
142
+ // The legacy one behind it, converted.
143
+ EXPECT_NEAR(all[1].offsetY, -3.0, 0.001);
144
+ EXPECT_NEAR(all[1].blurRadius, 6.0, 0.001);
145
+ }
146
+
147
+ // And a `boxShadow` on its own is untouched, which is the case that was already
148
+ // working and must stay that way: nothing is appended, nothing is reordered.
149
+ TEST(legacy_shadow_leaves_a_css_only_view_alone) {
150
+ BaseViewProps props;
151
+ BoxShadow css;
152
+ css.offsetY = 4;
153
+ css.blurRadius = 8;
154
+ css.inset = true;
155
+ css.color = facebook::react::colorFromComponents(ColorComponents{0.0F, 0.0F, 0.0F, 0.25F});
156
+ props.boxShadow = {css};
157
+
158
+ const std::vector<BoxShadow> all = basalt::allShadows(props);
159
+ EXPECT_EQ((int)all.size(), 1);
160
+ if (all.empty()) {
161
+ return;
162
+ }
163
+ EXPECT(all[0].inset);
164
+ EXPECT_NEAR(all[0].blurRadius, 8.0, 0.001);
165
+ }