basalt-core 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 (143) 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/packageApp.d.ts.map +1 -1
  6. package/dist/cli/packageApp.js +7 -1
  7. package/dist/cli/packageApp.js.map +1 -1
  8. package/dist/metro-config.d.ts.map +1 -1
  9. package/dist/metro-config.js +53 -5
  10. package/dist/metro-config.js.map +1 -1
  11. package/dist/src/KeyHandler.d.ts +1 -1
  12. package/dist/src/KeyHandler.d.ts.map +1 -1
  13. package/dist/src/KeyHandler.js.map +1 -1
  14. package/dist/src/index.d.ts +5 -0
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/overrides/Alert.d.ts +1 -1
  17. package/dist/src/overrides/Alert.js +1 -1
  18. package/dist/src/overrides/BaseViewConfig.d.ts +10 -3
  19. package/dist/src/overrides/BaseViewConfig.d.ts.map +1 -1
  20. package/dist/src/overrides/BaseViewConfig.js +21 -3
  21. package/dist/src/overrides/BaseViewConfig.js.map +1 -1
  22. package/dist/src/overrides/Button.d.ts +81 -0
  23. package/dist/src/overrides/Button.d.ts.map +1 -0
  24. package/dist/src/overrides/Button.js +109 -0
  25. package/dist/src/overrides/Button.js.map +1 -0
  26. package/dist/src/overrides/ScrollView.d.ts +25 -2
  27. package/dist/src/overrides/ScrollView.d.ts.map +1 -1
  28. package/dist/src/overrides/ScrollView.js +64 -7
  29. package/dist/src/overrides/ScrollView.js.map +1 -1
  30. package/dist/src/overrides/Settings.d.ts +81 -0
  31. package/dist/src/overrides/Settings.d.ts.map +1 -0
  32. package/dist/src/overrides/Settings.js +120 -0
  33. package/dist/src/overrides/Settings.js.map +1 -0
  34. package/dist/src/overrides/processDecelerationRate.d.ts +20 -0
  35. package/dist/src/overrides/processDecelerationRate.d.ts.map +1 -0
  36. package/dist/src/overrides/processDecelerationRate.js +31 -0
  37. package/dist/src/overrides/processDecelerationRate.js.map +1 -0
  38. package/dist/src/useContextMenu.d.ts +57 -6
  39. package/dist/src/useContextMenu.d.ts.map +1 -1
  40. package/dist/src/useContextMenu.js +34 -1
  41. package/dist/src/useContextMenu.js.map +1 -1
  42. package/dist/tsconfig.tsbuildinfo +1 -1
  43. package/native/CMakeLists.txt +51 -0
  44. package/native/core/ActionSheet.cpp +75 -0
  45. package/native/core/ActionSheet.h +90 -0
  46. package/native/core/AppStateHost.cpp +76 -0
  47. package/native/core/AppStateHost.h +68 -0
  48. package/native/core/BackgroundLayers.h +282 -0
  49. package/native/core/BlendModes.h +67 -0
  50. package/native/core/CoreModules.cpp +380 -11
  51. package/native/core/CoreModules.h +140 -4
  52. package/native/core/CrashHandler.cpp +246 -0
  53. package/native/core/CrashHandler.h +65 -0
  54. package/native/core/CursorNames.h +110 -0
  55. package/native/core/DevMenu.cpp +6 -5
  56. package/native/core/DeviceMetrics.cpp +87 -0
  57. package/native/core/DeviceMetrics.h +80 -0
  58. package/native/core/Filters.h +336 -0
  59. package/native/core/FontFitting.h +183 -0
  60. package/native/core/FontScaling.h +153 -0
  61. package/native/core/FontVariants.h +148 -0
  62. package/native/core/Gradients.h +523 -0
  63. package/native/core/ImageAnimation.h +109 -0
  64. package/native/core/ImageBytes.cpp +28 -1
  65. package/native/core/ImageBytes.h +17 -1
  66. package/native/core/ImageProgress.h +85 -0
  67. package/native/core/LabelRegistry.h +189 -0
  68. package/native/core/LegacyShadow.h +90 -0
  69. package/native/core/LiveRegions.h +114 -0
  70. package/native/core/MenuModule.cpp +35 -4
  71. package/native/core/MountingWalk.h +125 -2
  72. package/native/core/PlatformServices.h +187 -3
  73. package/native/core/SafeAreaComponent.cpp +115 -0
  74. package/native/core/SafeAreaComponent.h +105 -0
  75. package/native/core/ScreensComponent.cpp +201 -0
  76. package/native/core/ScreensComponent.h +216 -0
  77. package/native/core/ScrollBounds.cpp +8 -0
  78. package/native/core/ScrollBounds.h +14 -0
  79. package/native/core/ScrollIndicator.h +67 -0
  80. package/native/core/ScrollMomentum.cpp +24 -0
  81. package/native/core/ScrollMomentum.h +14 -0
  82. package/native/core/ScrollSnap.cpp +121 -12
  83. package/native/core/ScrollSnap.h +45 -0
  84. package/native/core/ScrollVisiblePosition.cpp +68 -0
  85. package/native/core/ScrollVisiblePosition.h +101 -0
  86. package/native/core/SettingsStore.cpp +199 -0
  87. package/native/core/SettingsStore.h +127 -0
  88. package/native/core/ShadowShape.h +38 -0
  89. package/native/core/SystemLocale.cpp +103 -0
  90. package/native/core/SystemLocale.h +86 -0
  91. package/native/core/TestDialog.cpp +45 -6
  92. package/native/core/TestDialog.h +15 -0
  93. package/native/core/TextAlignments.h +240 -0
  94. package/native/core/TextChecking.h +133 -0
  95. package/native/core/TextColors.h +83 -0
  96. package/native/core/TextDecorations.h +101 -0
  97. package/native/core/TextDirection.cpp +242 -0
  98. package/native/core/TextDirection.h +88 -0
  99. package/native/core/TextHighlight.h +30 -0
  100. package/native/core/TextSelection.cpp +97 -0
  101. package/native/core/TextSelection.h +161 -0
  102. package/native/core/TextShadows.h +96 -0
  103. package/native/core/TextVerticalAlign.h +33 -0
  104. package/native/core/UIManagerAccess.cpp +36 -0
  105. package/native/core/UIManagerAccess.h +28 -0
  106. package/native/core/WindowBoundsCache.cpp +58 -1
  107. package/native/core/WindowControl.h +24 -0
  108. package/native/core/WritingDirections.h +52 -0
  109. package/native/core/portability_probe.cpp +7 -0
  110. package/native/tests/RenamedFontFile.h +161 -0
  111. package/native/tests/TestHarness.cpp +18 -0
  112. package/native/tests/test_action_sheet.cpp +157 -0
  113. package/native/tests/test_app_state.cpp +60 -0
  114. package/native/tests/test_background_layers.cpp +281 -0
  115. package/native/tests/test_blend_modes.cpp +79 -0
  116. package/native/tests/test_blobs.cpp +1 -1
  117. package/native/tests/test_device_metrics.cpp +126 -0
  118. package/native/tests/test_filters.cpp +271 -0
  119. package/native/tests/test_font_fitting.cpp +172 -0
  120. package/native/tests/test_font_scaling.cpp +127 -0
  121. package/native/tests/test_font_variants.cpp +84 -0
  122. package/native/tests/test_gradients.cpp +390 -0
  123. package/native/tests/test_image_animation.cpp +105 -0
  124. package/native/tests/test_image_progress.cpp +94 -0
  125. package/native/tests/test_label_registry.cpp +262 -0
  126. package/native/tests/test_legacy_shadow.cpp +165 -0
  127. package/native/tests/test_live_regions.cpp +108 -0
  128. package/native/tests/test_menu_entries.cpp +192 -0
  129. package/native/tests/test_momentum.cpp +45 -0
  130. package/native/tests/test_scroll_bounds.cpp +42 -0
  131. package/native/tests/test_scroll_indicator.cpp +61 -0
  132. package/native/tests/test_scroll_snap.cpp +129 -0
  133. package/native/tests/test_scroll_visible_position.cpp +121 -0
  134. package/native/tests/test_settings_store.cpp +257 -0
  135. package/native/tests/test_shadow_shape.cpp +47 -0
  136. package/native/tests/test_system_locale.cpp +36 -0
  137. package/native/tests/test_text_alignment.cpp +299 -0
  138. package/native/tests/test_text_checking.cpp +31 -0
  139. package/native/tests/test_text_colors.cpp +92 -0
  140. package/native/tests/test_text_decorations.cpp +100 -0
  141. package/native/tests/test_text_selection.cpp +168 -0
  142. package/native/tests/test_text_shadows.cpp +132 -0
  143. package/package.json +4 -2
@@ -0,0 +1,157 @@
1
+ // The menu an action sheet becomes, and reading an answer back out of it.
2
+ //
3
+ // All of the interesting behaviour is the index arithmetic, which is why it is a
4
+ // function rather than part of the module: `ActionSheetIOS` numbers the array it
5
+ // passed, a menu index counts every entry including the title and the message,
6
+ // and getting that wrong means an app runs the wrong choice -- the quietest
7
+ // possible bug, because every index is a valid index.
8
+ //
9
+ // What is asserted here and nowhere else: a dismissal is not an option, a
10
+ // header cannot be chosen, and the offset is exactly the number of entries the
11
+ // title and message took.
12
+
13
+ #include "TestHarness.h"
14
+
15
+ #include "ActionSheet.h"
16
+
17
+ #include <sstream>
18
+ #include <string>
19
+
20
+ using basalt::ActionSheetMenu;
21
+ using basalt::ActionSheetRequest;
22
+ using basalt::actionSheetChoice;
23
+ using basalt::actionSheetMenu;
24
+
25
+ namespace {
26
+
27
+ ActionSheetRequest threeOptions() {
28
+ ActionSheetRequest request;
29
+ request.options = {"Save", "Discard", "Cancel"};
30
+ request.cancelIndex = 2;
31
+ return request;
32
+ }
33
+
34
+ } // namespace
35
+
36
+ TEST(action_sheet_options_with_no_title_start_at_the_top) {
37
+ const ActionSheetRequest request = threeOptions();
38
+ const ActionSheetMenu menu = actionSheetMenu(request);
39
+ EXPECT_EQ(menu.offset, 0);
40
+ EXPECT_EQ((long)menu.menu.entries.size(), 3L);
41
+ EXPECT_EQ(menu.menu.entries[0].label, std::string("Save"));
42
+ // Negative is "wherever the pointer is", which is what a sheet with no anchor
43
+ // wants and what MenuRequest already means by it.
44
+ EXPECT(menu.menu.x < 0.0);
45
+ EXPECT(menu.menu.y < 0.0);
46
+ }
47
+
48
+ TEST(action_sheet_a_title_and_a_message_are_disabled_entries_above_a_separator) {
49
+ ActionSheetRequest request = threeOptions();
50
+ request.title = "Unsaved changes";
51
+ request.message = "Three files have not been written";
52
+
53
+ const ActionSheetMenu menu = actionSheetMenu(request);
54
+ EXPECT_EQ(menu.offset, 3);
55
+ EXPECT_EQ((long)menu.menu.entries.size(), 6L);
56
+ EXPECT_EQ(menu.menu.entries[0].label, std::string("Unsaved changes"));
57
+ EXPECT(!menu.menu.entries[0].enabled);
58
+ EXPECT(!menu.menu.entries[1].enabled);
59
+ EXPECT(menu.menu.entries[2].isSeparator());
60
+ EXPECT_EQ(menu.menu.entries[3].label, std::string("Save"));
61
+ EXPECT(menu.menu.entries[3].enabled);
62
+ }
63
+
64
+ TEST(action_sheet_a_message_alone_still_takes_a_separator) {
65
+ ActionSheetRequest request = threeOptions();
66
+ request.message = "Three files have not been written";
67
+ const ActionSheetMenu menu = actionSheetMenu(request);
68
+ EXPECT_EQ(menu.offset, 2);
69
+ EXPECT(menu.menu.entries[1].isSeparator());
70
+ }
71
+
72
+ TEST(action_sheet_disabled_indices_grey_the_option_they_name) {
73
+ ActionSheetRequest request = threeOptions();
74
+ request.disabledIndices = {1};
75
+ request.title = "Unsaved changes";
76
+
77
+ const ActionSheetMenu menu = actionSheetMenu(request);
78
+ // Offset 2: a title and its separator. The disabled option is the app's
79
+ // index 1, which is this menu's index 3.
80
+ EXPECT_EQ(menu.offset, 2);
81
+ EXPECT(menu.menu.entries[2].enabled);
82
+ EXPECT(!menu.menu.entries[3].enabled);
83
+ EXPECT(menu.menu.entries[4].enabled);
84
+ }
85
+
86
+ TEST(action_sheet_an_option_chosen_is_the_apps_own_index) {
87
+ ActionSheetRequest request = threeOptions();
88
+ request.title = "Unsaved changes";
89
+ const ActionSheetMenu menu = actionSheetMenu(request);
90
+
91
+ // A title and its separator, so menu index 2 is the first option -- which the
92
+ // app knows as 0. This is the whole reason the offset is carried rather than
93
+ // recomputed, and the arithmetic is the one thing here that can be silently
94
+ // wrong: every index is a valid index.
95
+ EXPECT_EQ(menu.offset, 2);
96
+ EXPECT(actionSheetChoice(request, menu, 2).has_value());
97
+ EXPECT_EQ(*actionSheetChoice(request, menu, 2), 0);
98
+ EXPECT_EQ(*actionSheetChoice(request, menu, 4), 2);
99
+ // One past the last option, which the menu does not have.
100
+ EXPECT(!actionSheetChoice(request, menu, 5).has_value());
101
+ }
102
+
103
+ TEST(action_sheet_a_header_is_not_a_choice) {
104
+ ActionSheetRequest request = threeOptions();
105
+ request.title = "Unsaved changes";
106
+ const ActionSheetMenu menu = actionSheetMenu(request);
107
+ // A person cannot click a disabled entry and a script cannot either, but the
108
+ // arithmetic has to refuse it on its own: index 0 minus an offset of 2 is not
109
+ // an option, and reporting -2 to an app would be worse than reporting nothing.
110
+ EXPECT(!actionSheetChoice(request, menu, 0).has_value());
111
+ EXPECT(!actionSheetChoice(request, menu, 1).has_value());
112
+ }
113
+
114
+ TEST(action_sheet_an_index_past_the_end_is_not_a_choice) {
115
+ const ActionSheetRequest request = threeOptions();
116
+ const ActionSheetMenu menu = actionSheetMenu(request);
117
+ EXPECT(!actionSheetChoice(request, menu, 3).has_value());
118
+ EXPECT(!actionSheetChoice(request, menu, 99).has_value());
119
+ }
120
+
121
+ TEST(action_sheet_a_dismissal_is_the_cancel_choice_when_there_is_one) {
122
+ // The one place this platform answers something iOS does not. Escape closing
123
+ // a menu is ordinary where an iPad popover being tapped away is not, and an
124
+ // app that named a cancel choice has already said what to do with it.
125
+ const ActionSheetRequest request = threeOptions();
126
+ const ActionSheetMenu menu = actionSheetMenu(request);
127
+ EXPECT(actionSheetChoice(request, menu, -1).has_value());
128
+ EXPECT_EQ(*actionSheetChoice(request, menu, -1), 2);
129
+ }
130
+
131
+ TEST(action_sheet_a_dismissal_with_no_cancel_choice_reports_nothing) {
132
+ // Nothing rather than -1: `UIAlertController` calls the callback from a
133
+ // button's handler, so a sheet that goes away without one calls nothing, and
134
+ // an app never has to understand an index that is not an index.
135
+ ActionSheetRequest request = threeOptions();
136
+ request.cancelIndex = -1;
137
+ const ActionSheetMenu menu = actionSheetMenu(request);
138
+ EXPECT(!actionSheetChoice(request, menu, -1).has_value());
139
+ }
140
+
141
+ TEST(action_sheet_a_cancel_index_the_options_do_not_have_reports_nothing) {
142
+ // An app that passed a stale index. Reporting it would be reporting an index
143
+ // into an array that is too short, which is a crash in somebody's handler.
144
+ ActionSheetRequest request = threeOptions();
145
+ request.cancelIndex = 7;
146
+ const ActionSheetMenu menu = actionSheetMenu(request);
147
+ EXPECT(!actionSheetChoice(request, menu, -1).has_value());
148
+ }
149
+
150
+ TEST(action_sheet_an_anchor_puts_the_menu_where_the_caller_said) {
151
+ ActionSheetRequest request = threeOptions();
152
+ request.x = 120.0;
153
+ request.y = 240.0;
154
+ const ActionSheetMenu menu = actionSheetMenu(request);
155
+ EXPECT_NEAR(menu.menu.x, 120.0, 0.01);
156
+ EXPECT_NEAR(menu.menu.y, 240.0, 0.01);
157
+ }
@@ -0,0 +1,60 @@
1
+ // What `AppState.currentState` says, which was the empty string on every host
2
+ // until 2026-10-10. See core/AppStateHost.h.
3
+
4
+ #include "TestHarness.h"
5
+
6
+ #include "AppStateHost.h"
7
+
8
+ #include <sstream>
9
+ #include <string>
10
+ #include <vector>
11
+
12
+ using basalt::AppStateKind;
13
+ using basalt::appStateFor;
14
+ using basalt::appStateName;
15
+
16
+ TEST(app_state_is_the_two_facts_a_desktop_has) {
17
+ EXPECT(appStateFor(true, true) == AppStateKind::Active);
18
+ // Visible and not frontmost: iOS's `inactive` is a transition, and the
19
+ // desktop analogue is another window being in front -- which is the same
20
+ // thing an app uses it for.
21
+ EXPECT(appStateFor(false, true) == AppStateKind::Inactive);
22
+ // Invisible wins over focused: a minimised window that still holds focus is
23
+ // not something an app should treat as frontmost.
24
+ EXPECT(appStateFor(true, false) == AppStateKind::Background);
25
+ EXPECT(appStateFor(false, false) == AppStateKind::Background);
26
+ }
27
+
28
+ TEST(app_state_names_are_react_natives_own) {
29
+ EXPECT(std::string(appStateName(AppStateKind::Active)) == "active");
30
+ EXPECT(std::string(appStateName(AppStateKind::Inactive)) == "inactive");
31
+ EXPECT(std::string(appStateName(AppStateKind::Background)) == "background");
32
+ }
33
+
34
+ // An application that has drawn a window is active, which is what an app
35
+ // reading `currentState` before any event arrives should be told. Upstream
36
+ // answers the empty string, which is not one of React Native's own states.
37
+ TEST(app_state_starts_active) {
38
+ EXPECT(basalt::currentAppState() == AppStateKind::Active);
39
+ }
40
+
41
+ TEST(app_state_tells_the_listener_only_when_it_changes) {
42
+ std::vector<AppStateKind> seen;
43
+ basalt::setAppStateListener([&seen](AppStateKind state) { seen.push_back(state); });
44
+
45
+ basalt::notifyAppState(AppStateKind::Active); // already active
46
+ basalt::notifyAppState(AppStateKind::Inactive);
47
+ basalt::notifyAppState(AppStateKind::Inactive);
48
+ basalt::notifyAppState(AppStateKind::Background);
49
+ basalt::notifyAppState(AppStateKind::Active);
50
+
51
+ EXPECT_EQ((long)seen.size(), 3L);
52
+ EXPECT(seen[0] == AppStateKind::Inactive);
53
+ EXPECT(seen[1] == AppStateKind::Background);
54
+ EXPECT(seen[2] == AppStateKind::Active);
55
+
56
+ // And the state is readable without a listener, which is where
57
+ // `getCurrentAppState` reads it.
58
+ EXPECT(basalt::currentAppState() == AppStateKind::Active);
59
+ basalt::setAppStateListener(nullptr);
60
+ }
@@ -0,0 +1,281 @@
1
+ // Where a background image goes and how it tiles. See core/BackgroundLayers.h.
2
+ //
3
+ // Every number is specified and every wrong answer still paints a gradient, so
4
+ // these are the spec's own cases: percentages against the area rather than the
5
+ // image, the far edges against the space the image leaves, `round` changing the
6
+ // size before anything tiles, and `space` changing the step and the anchor but
7
+ // not the size.
8
+ //
9
+ // The areas are deliberately different throughout -- a 100x100 border box with a
10
+ // 10pt border, so the padding box is (10,10,80,80) -- because an implementation
11
+ // that used one for both is the mistake this file exists to catch, and it is
12
+ // invisible on a view with no border.
13
+
14
+ #include "TestHarness.h"
15
+
16
+ #include "BackgroundLayers.h"
17
+
18
+ #include <sstream>
19
+
20
+ using basalt::BackgroundArea;
21
+ using basalt::BackgroundLayer;
22
+ using basalt::resolveBackgroundLayer;
23
+ using facebook::react::BackgroundPosition;
24
+ using facebook::react::BackgroundRepeat;
25
+ using facebook::react::BackgroundRepeatStyle;
26
+ using facebook::react::BackgroundSize;
27
+ using facebook::react::BackgroundSizeLengthPercentage;
28
+ using facebook::react::UnitType;
29
+ using facebook::react::ValueUnit;
30
+
31
+ namespace {
32
+
33
+ const BackgroundArea kPainting{0.0F, 0.0F, 100.0F, 100.0F};
34
+ const BackgroundArea kPositioning{10.0F, 10.0F, 80.0F, 80.0F};
35
+
36
+ // React Native's defaults, which are what an app that sets none of the three
37
+ // props sends: auto size, the top left corner, and repeat in both axes.
38
+ BackgroundSize autoSize() { return BackgroundSizeLengthPercentage{}; }
39
+
40
+ BackgroundSize sized(float x, UnitType xUnit, float y, UnitType yUnit) {
41
+ BackgroundSizeLengthPercentage size;
42
+ size.x = ValueUnit{x, xUnit};
43
+ size.y = ValueUnit{y, yUnit};
44
+ return size;
45
+ }
46
+
47
+ BackgroundPosition at(float x, UnitType xUnit, float y, UnitType yUnit) {
48
+ BackgroundPosition position;
49
+ position.left = ValueUnit{x, xUnit};
50
+ position.top = ValueUnit{y, yUnit};
51
+ return position;
52
+ }
53
+
54
+ BackgroundRepeat repeating(BackgroundRepeatStyle x, BackgroundRepeatStyle y) {
55
+ BackgroundRepeat repeat;
56
+ repeat.x = x;
57
+ repeat.y = y;
58
+ return repeat;
59
+ }
60
+
61
+ bool near(float a, float b) { return std::fabs(a - b) < 0.01F; }
62
+
63
+ } // namespace
64
+
65
+ // The defaults, which is the case every view with a gradient and nothing else
66
+ // goes through. The image fills the *padding* box, not the border box, which is
67
+ // the difference from what this platform drew before the props were read.
68
+ TEST(background_the_defaults_fill_the_padding_box) {
69
+ const BackgroundLayer layer = resolveBackgroundLayer(
70
+ kPositioning, kPainting, autoSize(), BackgroundPosition{}, BackgroundRepeat{});
71
+
72
+ EXPECT(near(layer.width, 80.0F));
73
+ EXPECT(near(layer.height, 80.0F));
74
+ // Repeat is React Native's default, so the tile is the image and the pattern
75
+ // is backed up far enough to cover the border box it is clipped to.
76
+ EXPECT(layer.repeatsX);
77
+ EXPECT(layer.repeatsY);
78
+ EXPECT(near(layer.tileWidth, 80.0F));
79
+ EXPECT(near(layer.tileHeight, 80.0F));
80
+ EXPECT(near(layer.tileX, -70.0F));
81
+ EXPECT(near(layer.tileY, -70.0F));
82
+ // The image is drawn at the tile's origin, the tile being the image itself.
83
+ EXPECT(near(layer.x, layer.tileX));
84
+ EXPECT(near(layer.y, layer.tileY));
85
+ }
86
+
87
+ // A size in points and a size in percent, where the percentage is of the
88
+ // positioning area and not of the painting area: 50% of an 80pt padding box is
89
+ // 40, where 50% of the 100pt border box would be 50.
90
+ TEST(background_a_size_resolves_against_the_positioning_area) {
91
+ const BackgroundLayer points = resolveBackgroundLayer(
92
+ kPositioning,
93
+ kPainting,
94
+ sized(30.0F, UnitType::Point, 20.0F, UnitType::Point),
95
+ BackgroundPosition{},
96
+ repeating(BackgroundRepeatStyle::NoRepeat, BackgroundRepeatStyle::NoRepeat));
97
+ EXPECT(near(points.width, 30.0F));
98
+ EXPECT(near(points.height, 20.0F));
99
+
100
+ const BackgroundLayer percent = resolveBackgroundLayer(
101
+ kPositioning,
102
+ kPainting,
103
+ sized(50.0F, UnitType::Percent, 25.0F, UnitType::Percent),
104
+ BackgroundPosition{},
105
+ repeating(BackgroundRepeatStyle::NoRepeat, BackgroundRepeatStyle::NoRepeat));
106
+ EXPECT(near(percent.width, 40.0F));
107
+ EXPECT(near(percent.height, 20.0F));
108
+ }
109
+
110
+ // `cover` and `contain` come to the area, which is what CSS says for an image
111
+ // with no intrinsic size -- a gradient -- and is what iOS does with them.
112
+ TEST(background_cover_and_contain_are_the_area_for_a_gradient) {
113
+ for (const auto keyword :
114
+ {facebook::react::BackgroundSizeKeyword::Cover, facebook::react::BackgroundSizeKeyword::Contain}) {
115
+ const BackgroundLayer layer = resolveBackgroundLayer(
116
+ kPositioning, kPainting, keyword, BackgroundPosition{}, BackgroundRepeat{});
117
+ EXPECT(near(layer.width, 80.0F));
118
+ EXPECT(near(layer.height, 80.0F));
119
+ }
120
+ }
121
+
122
+ // The position, against the space the image leaves rather than the whole area.
123
+ // A 30pt image in an 80pt area has 50pt of slack, so 50% of it is 25 -- not 40.
124
+ TEST(background_a_position_resolves_against_the_space_the_image_leaves) {
125
+ const BackgroundLayer layer = resolveBackgroundLayer(
126
+ kPositioning,
127
+ kPainting,
128
+ sized(30.0F, UnitType::Point, 30.0F, UnitType::Point),
129
+ at(50.0F, UnitType::Percent, 100.0F, UnitType::Percent),
130
+ repeating(BackgroundRepeatStyle::NoRepeat, BackgroundRepeatStyle::NoRepeat));
131
+
132
+ EXPECT(near(layer.x, 10.0F + 25.0F));
133
+ // 100% of the slack is flush with the far edge of the padding box.
134
+ EXPECT(near(layer.y, 10.0F + 50.0F));
135
+ }
136
+
137
+ // The far edges are not the near ones. `right: 25%` is a quarter of the slack in
138
+ // from the right, which is three quarters along, and an implementation that
139
+ // resolved both the same way would be right exactly half the time.
140
+ TEST(background_the_far_edges_are_measured_from_the_far_side) {
141
+ BackgroundPosition position;
142
+ position.right = ValueUnit{25.0F, UnitType::Percent};
143
+ position.bottom = ValueUnit{10.0F, UnitType::Point};
144
+
145
+ const BackgroundLayer layer = resolveBackgroundLayer(
146
+ kPositioning,
147
+ kPainting,
148
+ sized(40.0F, UnitType::Point, 40.0F, UnitType::Point),
149
+ position,
150
+ repeating(BackgroundRepeatStyle::NoRepeat, BackgroundRepeatStyle::NoRepeat));
151
+
152
+ // 40pt of slack, a quarter of it from the right: 10 + 30.
153
+ EXPECT(near(layer.x, 40.0F));
154
+ // 10pt in from the bottom of the padding box: 10 + 40 - 10.
155
+ EXPECT(near(layer.y, 40.0F));
156
+ }
157
+
158
+ // `no-repeat` leaves the tile as the painting area, so a host that repeats in
159
+ // both axes at once draws it exactly once.
160
+ TEST(background_no_repeat_is_one_tile_over_the_painting_area) {
161
+ const BackgroundLayer layer = resolveBackgroundLayer(
162
+ kPositioning,
163
+ kPainting,
164
+ sized(30.0F, UnitType::Point, 30.0F, UnitType::Point),
165
+ BackgroundPosition{},
166
+ repeating(BackgroundRepeatStyle::NoRepeat, BackgroundRepeatStyle::NoRepeat));
167
+
168
+ EXPECT(!layer.repeatsX);
169
+ EXPECT(!layer.repeatsY);
170
+ EXPECT(near(layer.tileX, 0.0F));
171
+ EXPECT(near(layer.tileY, 0.0F));
172
+ EXPECT(near(layer.tileWidth, 100.0F));
173
+ EXPECT(near(layer.tileHeight, 100.0F));
174
+ // And the image stays where the position put it.
175
+ EXPECT(near(layer.x, 10.0F));
176
+ EXPECT(near(layer.y, 10.0F));
177
+ }
178
+
179
+ // One axis each way, which is `repeat-x`: the tile spans the painting area
180
+ // downwards and steps by the image's width across.
181
+ TEST(background_one_axis_can_repeat_while_the_other_does_not) {
182
+ const BackgroundLayer layer = resolveBackgroundLayer(
183
+ kPositioning,
184
+ kPainting,
185
+ sized(20.0F, UnitType::Point, 20.0F, UnitType::Point),
186
+ at(0.0F, UnitType::Point, 30.0F, UnitType::Point),
187
+ repeating(BackgroundRepeatStyle::Repeat, BackgroundRepeatStyle::NoRepeat));
188
+
189
+ EXPECT(layer.repeatsX);
190
+ EXPECT(!layer.repeatsY);
191
+ EXPECT(near(layer.tileWidth, 20.0F));
192
+ EXPECT(near(layer.tileHeight, 100.0F));
193
+ // Backed up to cover the border box: 10 - 20 is -10.
194
+ EXPECT(near(layer.tileX, -10.0F));
195
+ EXPECT(near(layer.tileY, 0.0F));
196
+ // The row sits where the position put it, inside the tile.
197
+ EXPECT(near(layer.y, 40.0F));
198
+ }
199
+
200
+ // `round` divides the area into a whole number of tiles before anything else
201
+ // happens, because it changes the size: 30pt into 80pt is 2.67 tiles, so the
202
+ // nearest whole number is 3 and the tile becomes 26.67.
203
+ TEST(background_round_divides_the_area_into_whole_tiles) {
204
+ const BackgroundLayer layer = resolveBackgroundLayer(
205
+ kPositioning,
206
+ kPainting,
207
+ sized(30.0F, UnitType::Point, 30.0F, UnitType::Point),
208
+ BackgroundPosition{},
209
+ repeating(BackgroundRepeatStyle::Round, BackgroundRepeatStyle::Round));
210
+
211
+ EXPECT(near(layer.width, 80.0F / 3.0F));
212
+ EXPECT(near(layer.height, 80.0F / 3.0F));
213
+ // And it tiles by that size, with no gap: `round` is `repeat` with the size
214
+ // adjusted.
215
+ EXPECT(near(layer.tileWidth, layer.width));
216
+ EXPECT(layer.repeatsX);
217
+
218
+ // A size that already divides the area is left alone, which is the branch that
219
+ // stops a division by a remainder of zero turning into a wrong size.
220
+ const BackgroundLayer exact = resolveBackgroundLayer(
221
+ kPositioning,
222
+ kPainting,
223
+ sized(40.0F, UnitType::Point, 40.0F, UnitType::Point),
224
+ BackgroundPosition{},
225
+ repeating(BackgroundRepeatStyle::Round, BackgroundRepeatStyle::Round));
226
+ EXPECT(near(exact.width, 40.0F));
227
+ }
228
+
229
+ // `space` keeps the size and shares the slack between the tiles, and the first
230
+ // tile is flush with the area rather than where the position asked: 30pt into
231
+ // 80pt fits twice with 20pt left over, so the step is 50.
232
+ TEST(background_space_shares_the_slack_between_tiles) {
233
+ const BackgroundLayer layer = resolveBackgroundLayer(
234
+ kPositioning,
235
+ kPainting,
236
+ sized(30.0F, UnitType::Point, 30.0F, UnitType::Point),
237
+ at(50.0F, UnitType::Percent, 50.0F, UnitType::Percent),
238
+ repeating(BackgroundRepeatStyle::Space, BackgroundRepeatStyle::Space));
239
+
240
+ EXPECT(near(layer.width, 30.0F));
241
+ EXPECT(near(layer.tileWidth, 50.0F));
242
+ EXPECT(layer.repeatsX);
243
+ // Anchored on the area, not on the position: 10 backed up by one step is -40.
244
+ EXPECT(near(layer.tileX, -40.0F));
245
+ EXPECT(near(layer.x, layer.tileX));
246
+ }
247
+
248
+ // `space` with one tile has no slack to share, so it stops repeating -- which is
249
+ // the case that would otherwise divide by zero.
250
+ TEST(background_space_with_one_tile_does_not_repeat) {
251
+ const BackgroundLayer layer = resolveBackgroundLayer(
252
+ kPositioning,
253
+ kPainting,
254
+ sized(60.0F, UnitType::Point, 60.0F, UnitType::Point),
255
+ BackgroundPosition{},
256
+ repeating(BackgroundRepeatStyle::Space, BackgroundRepeatStyle::Space));
257
+
258
+ EXPECT(!layer.repeatsX);
259
+ EXPECT(!layer.repeatsY);
260
+ EXPECT(near(layer.width, 60.0F));
261
+ EXPECT(near(layer.x, 10.0F));
262
+ }
263
+
264
+ // Nothing to paint, which both hosts have to be told rather than left to divide
265
+ // by it: a view mid-layout has no size at all.
266
+ TEST(background_an_empty_area_is_nothing_to_paint) {
267
+ const BackgroundArea nothing{0.0F, 0.0F, 0.0F, 0.0F};
268
+ EXPECT(resolveBackgroundLayer(nothing, kPainting, autoSize(), BackgroundPosition{},
269
+ BackgroundRepeat{})
270
+ .empty());
271
+ EXPECT(resolveBackgroundLayer(kPositioning, nothing, autoSize(), BackgroundPosition{},
272
+ BackgroundRepeat{})
273
+ .empty());
274
+ // And a size of zero, which a stylesheet can ask for.
275
+ EXPECT(resolveBackgroundLayer(kPositioning,
276
+ kPainting,
277
+ sized(0.0F, UnitType::Point, 10.0F, UnitType::Point),
278
+ BackgroundPosition{},
279
+ BackgroundRepeat{})
280
+ .empty());
281
+ }
@@ -0,0 +1,79 @@
1
+ // The `mixBlendMode` keyword table, against React Native's own parser.
2
+ //
3
+ // core/BlendModes.h is seventeen lines of switch, which is seventeen chances to
4
+ // return the wrong neighbour's name -- and a wrong name is not a build error on
5
+ // either host: it is a view that blends with the wrong mode, or stops blending.
6
+ //
7
+ // So the assertion is a round trip rather than a list compared with itself.
8
+ // React Native parses these keywords with `blendModeFromString`, which is what
9
+ // turns the app's style into the enum in the first place, so feeding the name
10
+ // back through it has to give the value it came from. Nothing in this test
11
+ // agrees with the table by construction.
12
+
13
+ #include "TestHarness.h"
14
+
15
+ #include "BlendModes.h"
16
+
17
+ #include <set>
18
+ #include <sstream>
19
+ #include <string>
20
+
21
+ using facebook::react::BlendMode;
22
+ using facebook::react::blendModeFromString;
23
+
24
+ namespace {
25
+
26
+ // The enum is dense and PlusLighter is its last member, which is what makes a
27
+ // loop over it legitimate: a mode added upstream lands inside this range and the
28
+ // count below is what notices.
29
+ constexpr int kModeCount = static_cast<int>(BlendMode::PlusLighter) + 1;
30
+
31
+ } // namespace
32
+
33
+ TEST(blend_modes_every_mode_round_trips_through_react_natives_parser) {
34
+ for (int i = 0; i < kModeCount; i++) {
35
+ const BlendMode mode = static_cast<BlendMode>(i);
36
+ const char *name = basalt::blendModeName(mode);
37
+ if (mode == BlendMode::Normal) {
38
+ // `normal` is the default and asks for nothing, so it crosses as nothing.
39
+ EXPECT(name == nullptr);
40
+ continue;
41
+ }
42
+ EXPECT(name != nullptr);
43
+ if (name == nullptr) {
44
+ continue;
45
+ }
46
+ const std::optional<BlendMode> parsed = blendModeFromString(name);
47
+ EXPECT(parsed.has_value());
48
+ EXPECT(parsed.has_value() && *parsed == mode);
49
+ }
50
+ }
51
+
52
+ // And no two modes share a name, which the round trip above would catch only if
53
+ // the parser were injective -- it is, but this says the thing directly, and it is
54
+ // the failure a copy-paste in a sixteen-line table actually produces.
55
+ TEST(blend_modes_no_two_modes_share_a_name) {
56
+ std::set<std::string> seen;
57
+ int named = 0;
58
+ for (int i = 0; i < kModeCount; i++) {
59
+ const char *name = basalt::blendModeName(static_cast<BlendMode>(i));
60
+ if (name == nullptr) {
61
+ continue;
62
+ }
63
+ named++;
64
+ seen.insert(name);
65
+ }
66
+ EXPECT_EQ(named, kModeCount - 1);
67
+ EXPECT_EQ(static_cast<int>(seen.size()), named);
68
+ }
69
+
70
+ // The count, so that a mode added upstream is a failing test here rather than a
71
+ // view that silently does not blend. CSS has sixteen separable and non-separable
72
+ // blend modes plus `normal`, and React Native carries all seventeen.
73
+ TEST(blend_modes_there_are_seventeen_of_them) {
74
+ EXPECT_EQ(kModeCount, 17);
75
+ // The ends of the enum, so a reordering upstream is not silently fine either.
76
+ EXPECT(basalt::blendModeName(BlendMode::Multiply) != nullptr);
77
+ EXPECT(std::string(basalt::blendModeName(BlendMode::Multiply)) == "multiply");
78
+ EXPECT(std::string(basalt::blendModeName(BlendMode::PlusLighter)) == "plus-lighter");
79
+ }
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // What is *not* tested here is the module: createFromParts and readAsText take
7
7
  // jsi values, which need a runtime, and the thing worth proving about them --
8
- // that a Blob round-trips through JavaScript -- is proven by js/blob.js running
8
+ // that a Blob round-trips through JavaScript -- is proven by e2e/blob.js running
9
9
  // on both hosts. This covers the half underneath, where an off-by-one lives.
10
10
 
11
11
  #include "TestHarness.h"