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.
- package/dist/cli/basalt.d.ts.map +1 -1
- package/dist/cli/basalt.js +18 -1
- package/dist/cli/basalt.js.map +1 -1
- package/dist/cli/bundle-cli.js +0 -0
- package/dist/cli/desktop.d.ts.map +1 -1
- package/dist/cli/desktop.js +53 -4
- package/dist/cli/desktop.js.map +1 -1
- package/dist/cli/packageApp.d.ts.map +1 -1
- package/dist/cli/packageApp.js +7 -1
- package/dist/cli/packageApp.js.map +1 -1
- package/dist/metro-config.d.ts.map +1 -1
- package/dist/metro-config.js +53 -5
- package/dist/metro-config.js.map +1 -1
- package/dist/src/KeyHandler.d.ts +1 -1
- package/dist/src/KeyHandler.d.ts.map +1 -1
- package/dist/src/KeyHandler.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/overrides/Alert.d.ts +1 -1
- package/dist/src/overrides/Alert.js +1 -1
- package/dist/src/overrides/BaseViewConfig.d.ts +10 -3
- package/dist/src/overrides/BaseViewConfig.d.ts.map +1 -1
- package/dist/src/overrides/BaseViewConfig.js +21 -3
- package/dist/src/overrides/BaseViewConfig.js.map +1 -1
- package/dist/src/overrides/Button.d.ts +81 -0
- package/dist/src/overrides/Button.d.ts.map +1 -0
- package/dist/src/overrides/Button.js +109 -0
- package/dist/src/overrides/Button.js.map +1 -0
- package/dist/src/overrides/ScrollView.d.ts +25 -2
- package/dist/src/overrides/ScrollView.d.ts.map +1 -1
- package/dist/src/overrides/ScrollView.js +64 -7
- package/dist/src/overrides/ScrollView.js.map +1 -1
- package/dist/src/overrides/Settings.d.ts +81 -0
- package/dist/src/overrides/Settings.d.ts.map +1 -0
- package/dist/src/overrides/Settings.js +120 -0
- package/dist/src/overrides/Settings.js.map +1 -0
- package/dist/src/overrides/processDecelerationRate.d.ts +20 -0
- package/dist/src/overrides/processDecelerationRate.d.ts.map +1 -0
- package/dist/src/overrides/processDecelerationRate.js +31 -0
- package/dist/src/overrides/processDecelerationRate.js.map +1 -0
- package/dist/src/useContextMenu.d.ts +57 -6
- package/dist/src/useContextMenu.d.ts.map +1 -1
- package/dist/src/useContextMenu.js +34 -1
- package/dist/src/useContextMenu.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/native/CMakeLists.txt +85 -1
- package/native/cmake/Skia.cmake +29 -6
- package/native/core/ActionSheet.cpp +75 -0
- package/native/core/ActionSheet.h +90 -0
- package/native/core/AppStateHost.cpp +76 -0
- package/native/core/AppStateHost.h +68 -0
- package/native/core/BackgroundLayers.h +282 -0
- package/native/core/BlendModes.h +67 -0
- package/native/core/CoreModules.cpp +380 -11
- package/native/core/CoreModules.h +140 -4
- package/native/core/CrashHandler.cpp +246 -0
- package/native/core/CrashHandler.h +65 -0
- package/native/core/CursorNames.h +110 -0
- package/native/core/DevMenu.cpp +6 -5
- package/native/core/DeviceMetrics.cpp +87 -0
- package/native/core/DeviceMetrics.h +80 -0
- package/native/core/Filters.h +336 -0
- package/native/core/FontFitting.h +183 -0
- package/native/core/FontScaling.h +153 -0
- package/native/core/FontVariants.h +148 -0
- package/native/core/Gradients.h +523 -0
- package/native/core/ImageAnimation.h +109 -0
- package/native/core/ImageBytes.cpp +28 -1
- package/native/core/ImageBytes.h +17 -1
- package/native/core/ImageProgress.h +85 -0
- package/native/core/LabelRegistry.h +189 -0
- package/native/core/LegacyShadow.h +90 -0
- package/native/core/LiveRegions.h +114 -0
- package/native/core/MenuModule.cpp +35 -4
- package/native/core/MountingWalk.h +125 -2
- package/native/core/PlatformServices.h +187 -3
- package/native/core/SafeAreaComponent.cpp +115 -0
- package/native/core/SafeAreaComponent.h +105 -0
- package/native/core/ScreensComponent.cpp +201 -0
- package/native/core/ScreensComponent.h +216 -0
- package/native/core/ScrollBounds.cpp +8 -0
- package/native/core/ScrollBounds.h +14 -0
- package/native/core/ScrollIndicator.h +67 -0
- package/native/core/ScrollMomentum.cpp +24 -0
- package/native/core/ScrollMomentum.h +14 -0
- package/native/core/ScrollSnap.cpp +121 -12
- package/native/core/ScrollSnap.h +45 -0
- package/native/core/ScrollVisiblePosition.cpp +68 -0
- package/native/core/ScrollVisiblePosition.h +101 -0
- package/native/core/SettingsStore.cpp +199 -0
- package/native/core/SettingsStore.h +127 -0
- package/native/core/ShadowShape.h +38 -0
- package/native/core/SkiaViewComponent.cpp +11 -0
- package/native/core/SkiaViewComponent.h +50 -0
- package/native/core/SystemLocale.cpp +103 -0
- package/native/core/SystemLocale.h +86 -0
- package/native/core/TestDialog.cpp +45 -6
- package/native/core/TestDialog.h +15 -0
- package/native/core/TextAlignments.h +240 -0
- package/native/core/TextChecking.h +133 -0
- package/native/core/TextColors.h +83 -0
- package/native/core/TextDecorations.h +101 -0
- package/native/core/TextDirection.cpp +242 -0
- package/native/core/TextDirection.h +88 -0
- package/native/core/TextHighlight.h +30 -0
- package/native/core/TextSelection.cpp +97 -0
- package/native/core/TextSelection.h +161 -0
- package/native/core/TextShadows.h +96 -0
- package/native/core/TextVerticalAlign.h +33 -0
- package/native/core/UIManagerAccess.cpp +36 -0
- package/native/core/UIManagerAccess.h +28 -0
- package/native/core/WindowBoundsCache.cpp +58 -1
- package/native/core/WindowControl.h +24 -0
- package/native/core/WritingDirections.h +52 -0
- package/native/core/portability_probe.cpp +7 -0
- package/native/tests/RenamedFontFile.h +161 -0
- package/native/tests/TestHarness.cpp +18 -0
- package/native/tests/test_action_sheet.cpp +157 -0
- package/native/tests/test_app_state.cpp +60 -0
- package/native/tests/test_background_layers.cpp +281 -0
- package/native/tests/test_blend_modes.cpp +79 -0
- package/native/tests/test_blobs.cpp +1 -1
- package/native/tests/test_device_metrics.cpp +126 -0
- package/native/tests/test_filters.cpp +271 -0
- package/native/tests/test_font_fitting.cpp +172 -0
- package/native/tests/test_font_scaling.cpp +127 -0
- package/native/tests/test_font_variants.cpp +84 -0
- package/native/tests/test_gradients.cpp +390 -0
- package/native/tests/test_image_animation.cpp +105 -0
- package/native/tests/test_image_progress.cpp +94 -0
- package/native/tests/test_label_registry.cpp +262 -0
- package/native/tests/test_legacy_shadow.cpp +165 -0
- package/native/tests/test_live_regions.cpp +108 -0
- package/native/tests/test_menu_entries.cpp +192 -0
- package/native/tests/test_momentum.cpp +45 -0
- package/native/tests/test_scroll_bounds.cpp +42 -0
- package/native/tests/test_scroll_indicator.cpp +61 -0
- package/native/tests/test_scroll_snap.cpp +129 -0
- package/native/tests/test_scroll_visible_position.cpp +121 -0
- package/native/tests/test_settings_store.cpp +257 -0
- package/native/tests/test_shadow_shape.cpp +47 -0
- package/native/tests/test_system_locale.cpp +36 -0
- package/native/tests/test_text_alignment.cpp +299 -0
- package/native/tests/test_text_checking.cpp +31 -0
- package/native/tests/test_text_colors.cpp +92 -0
- package/native/tests/test_text_decorations.cpp +100 -0
- package/native/tests/test_text_selection.cpp +168 -0
- package/native/tests/test_text_shadows.cpp +132 -0
- 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
|
+
}
|