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.
- 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/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 +51 -0
- 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/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,121 @@
|
|
|
1
|
+
// Keeping the content still while items are inserted above it.
|
|
2
|
+
//
|
|
3
|
+
// Arithmetic, so it can be asserted without a display and without a mounting
|
|
4
|
+
// transaction -- which is most of the reason it lives in core rather than in
|
|
5
|
+
// three hosts. What each host adds is the children's boxes.
|
|
6
|
+
|
|
7
|
+
#include "TestHarness.h"
|
|
8
|
+
|
|
9
|
+
#include "ScrollVisiblePosition.h"
|
|
10
|
+
|
|
11
|
+
#include <sstream>
|
|
12
|
+
#include <vector>
|
|
13
|
+
|
|
14
|
+
using basalt::firstVisibleChild;
|
|
15
|
+
using basalt::ScrollChildBox;
|
|
16
|
+
using basalt::visiblePositionAdjustment;
|
|
17
|
+
|
|
18
|
+
namespace {
|
|
19
|
+
|
|
20
|
+
// Ten rows of a hundred points each, tagged 100 upwards.
|
|
21
|
+
std::vector<ScrollChildBox> rows(int count = 10, double height = 100.0) {
|
|
22
|
+
std::vector<ScrollChildBox> boxes;
|
|
23
|
+
for (int index = 0; index < count; index++) {
|
|
24
|
+
boxes.push_back(ScrollChildBox{.tag = 100 + index,
|
|
25
|
+
.leading = static_cast<double>(index) * height,
|
|
26
|
+
.length = height});
|
|
27
|
+
}
|
|
28
|
+
return boxes;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
} // namespace
|
|
32
|
+
|
|
33
|
+
TEST(visible_the_first_visible_child_is_the_one_watched) {
|
|
34
|
+
// Scrolled 250 down: the first two rows are above the viewport entirely and
|
|
35
|
+
// the third is the first with anything on screen.
|
|
36
|
+
const auto pinned = firstVisibleChild(rows(), 0, 250.0);
|
|
37
|
+
EXPECT(pinned.has);
|
|
38
|
+
EXPECT_EQ(pinned.tag, 102);
|
|
39
|
+
EXPECT_NEAR(pinned.leading, 200.0, 0.0001);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
TEST(visible_a_partly_visible_child_counts_as_visible) {
|
|
43
|
+
// 199 down: one point of the second row is on screen, which is enough -- the
|
|
44
|
+
// rule is the trailing edge being past the offset, not the leading edge.
|
|
45
|
+
const auto pinned = firstVisibleChild(rows(), 0, 199.0);
|
|
46
|
+
EXPECT(pinned.has);
|
|
47
|
+
EXPECT_EQ(pinned.tag, 101);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
TEST(visible_min_index_skips_the_children_before_it) {
|
|
51
|
+
// What `minIndexForVisible` is for: a header that is not part of the list's
|
|
52
|
+
// content and must not be the thing held still.
|
|
53
|
+
const auto pinned = firstVisibleChild(rows(), 3, 0.0);
|
|
54
|
+
EXPECT(pinned.has);
|
|
55
|
+
EXPECT_EQ(pinned.tag, 103);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
TEST(visible_the_last_child_is_the_fallback) {
|
|
59
|
+
// Scrolled past everything: no child is visible, and the last one is watched
|
|
60
|
+
// anyway. Without this a list at its end would adjust by nothing, which is
|
|
61
|
+
// the case a chat view is in almost all the time.
|
|
62
|
+
const auto pinned = firstVisibleChild(rows(), 0, 5000.0);
|
|
63
|
+
EXPECT(pinned.has);
|
|
64
|
+
EXPECT_EQ(pinned.tag, 109);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
TEST(visible_an_empty_list_has_nothing_to_watch) {
|
|
68
|
+
EXPECT(!firstVisibleChild({}, 0, 0.0).has);
|
|
69
|
+
// And an index past the end is the app saying none of these: watching the
|
|
70
|
+
// last child there would be watching one it excluded.
|
|
71
|
+
EXPECT(!firstVisibleChild(rows(3), 5, 0.0).has);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
TEST(visible_a_child_that_moved_down_moves_the_offset_with_it) {
|
|
75
|
+
// A row inserted above the watched one: it is now 100 further down, so the
|
|
76
|
+
// offset goes 100 further down and the content under the eye does not move.
|
|
77
|
+
const auto adjustment = visiblePositionAdjustment(200.0, 300.0, 250.0, std::nullopt);
|
|
78
|
+
EXPECT(adjustment.move);
|
|
79
|
+
EXPECT_NEAR(adjustment.delta, 100.0, 0.0001);
|
|
80
|
+
EXPECT(!adjustment.autoscrollToStart);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
TEST(visible_a_child_that_moved_up_moves_the_offset_up) {
|
|
84
|
+
// The other direction, which is a row above being removed.
|
|
85
|
+
const auto adjustment = visiblePositionAdjustment(300.0, 200.0, 350.0, std::nullopt);
|
|
86
|
+
EXPECT(adjustment.move);
|
|
87
|
+
EXPECT_NEAR(adjustment.delta, -100.0, 0.0001);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
TEST(visible_half_a_point_is_not_a_move) {
|
|
91
|
+
// Layout repeats to within a rounding error, and a list that moved by that on
|
|
92
|
+
// every transaction would drift under the person reading it.
|
|
93
|
+
EXPECT(!visiblePositionAdjustment(200.0, 200.4, 250.0, std::nullopt).move);
|
|
94
|
+
EXPECT(!visiblePositionAdjustment(200.0, 199.6, 250.0, std::nullopt).move);
|
|
95
|
+
EXPECT(visiblePositionAdjustment(200.0, 200.6, 250.0, std::nullopt).move);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
TEST(visible_a_list_near_the_start_follows_the_new_content) {
|
|
99
|
+
// `autoscrollToTopThreshold`, which reads backwards until the case is named:
|
|
100
|
+
// a chat list sitting at the top, where the new messages arrive, should
|
|
101
|
+
// follow them rather than hold its place.
|
|
102
|
+
const auto adjustment = visiblePositionAdjustment(200.0, 300.0, 40.0, 50);
|
|
103
|
+
EXPECT(adjustment.move);
|
|
104
|
+
EXPECT(adjustment.autoscrollToStart);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
TEST(visible_a_list_away_from_the_start_holds_its_place) {
|
|
108
|
+
// The same insertion with the list further down: it holds still, which is the
|
|
109
|
+
// behaviour the prop is mostly about.
|
|
110
|
+
const auto adjustment = visiblePositionAdjustment(200.0, 300.0, 400.0, 50);
|
|
111
|
+
EXPECT(adjustment.move);
|
|
112
|
+
EXPECT(!adjustment.autoscrollToStart);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
TEST(visible_no_threshold_never_autoscrolls) {
|
|
116
|
+
// The prop's other half is optional, and an absent one must not be read as
|
|
117
|
+
// zero: a list exactly at the start would then be dragged there every time.
|
|
118
|
+
const auto adjustment = visiblePositionAdjustment(200.0, 300.0, 0.0, std::nullopt);
|
|
119
|
+
EXPECT(adjustment.move);
|
|
120
|
+
EXPECT(!adjustment.autoscrollToStart);
|
|
121
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// `Settings`, which is a file on this platform.
|
|
2
|
+
//
|
|
3
|
+
// What is worth asserting is not that a key round-trips -- a map does that --
|
|
4
|
+
// but the four things this store has to get right for the API above it to
|
|
5
|
+
// behave the way an app written against iOS expects:
|
|
6
|
+
//
|
|
7
|
+
// * a value outlives the process, which is the whole point of `Settings`;
|
|
8
|
+
// * `merge` answers only the keys that *changed*, because that is what a
|
|
9
|
+
// watcher would be told about and what decides whether anything is written;
|
|
10
|
+
// * a null value deletes rather than stores a null, which is what
|
|
11
|
+
// `RCTSettingsManager` does and what `Settings.set({k: null})` means;
|
|
12
|
+
// * every way the file can be wrong leaves an app running, because a settings
|
|
13
|
+
// file somebody hand-edited is not a reason to refuse to start.
|
|
14
|
+
//
|
|
15
|
+
// The module over it is three lines of jsi per method and is covered by
|
|
16
|
+
// e2e/settings.tsx, which runs a host twice and reads back what the first run
|
|
17
|
+
// wrote. This file is the half that can be asserted without a runtime.
|
|
18
|
+
|
|
19
|
+
#include "TestHarness.h"
|
|
20
|
+
|
|
21
|
+
#include "SettingsStore.h"
|
|
22
|
+
|
|
23
|
+
#include <cstdint>
|
|
24
|
+
#include <cstdlib>
|
|
25
|
+
#include <filesystem>
|
|
26
|
+
#include <fstream>
|
|
27
|
+
#include <sstream>
|
|
28
|
+
#include <string>
|
|
29
|
+
|
|
30
|
+
using basalt::SettingsStore;
|
|
31
|
+
|
|
32
|
+
namespace {
|
|
33
|
+
|
|
34
|
+
void setEnvironment(const char *name, const std::string &value) {
|
|
35
|
+
#if defined(_WIN32)
|
|
36
|
+
_putenv_s(name, value.c_str());
|
|
37
|
+
#else
|
|
38
|
+
setenv(name, value.c_str(), 1);
|
|
39
|
+
#endif
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
void unsetEnvironment(const char *name) {
|
|
43
|
+
#if defined(_WIN32)
|
|
44
|
+
// The documented way to remove one: `_putenv_s` with an empty value.
|
|
45
|
+
_putenv_s(name, "");
|
|
46
|
+
#else
|
|
47
|
+
unsetenv(name);
|
|
48
|
+
#endif
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A directory of its own per test, removed afterwards. A test that leaves files
|
|
52
|
+
// behind makes the next run's failure someone else's.
|
|
53
|
+
struct Scratch {
|
|
54
|
+
std::filesystem::path directory;
|
|
55
|
+
|
|
56
|
+
Scratch() {
|
|
57
|
+
directory = std::filesystem::temp_directory_path() /
|
|
58
|
+
("basalt-settings-" + std::to_string(reinterpret_cast<std::uintptr_t>(this)));
|
|
59
|
+
std::filesystem::create_directories(directory);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
~Scratch() {
|
|
63
|
+
std::error_code ignored;
|
|
64
|
+
std::filesystem::remove_all(directory, ignored);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Scratch(const Scratch &) = delete;
|
|
68
|
+
Scratch &operator=(const Scratch &) = delete;
|
|
69
|
+
|
|
70
|
+
std::filesystem::path file() const {
|
|
71
|
+
return directory / "settings.json";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
void write(const std::string &contents) const {
|
|
75
|
+
std::ofstream stream(file());
|
|
76
|
+
stream << contents;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
std::string read() const {
|
|
80
|
+
std::ifstream stream(file());
|
|
81
|
+
std::stringstream buffer;
|
|
82
|
+
buffer << stream.rdbuf();
|
|
83
|
+
return buffer.str();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
std::string stringAt(const SettingsStore &store, const char *key) {
|
|
88
|
+
const folly::dynamic &values = store.values();
|
|
89
|
+
const auto found = values.find(key);
|
|
90
|
+
return found == values.items().end() || !found->second.isString()
|
|
91
|
+
? std::string{}
|
|
92
|
+
: found->second.asString();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
} // namespace
|
|
96
|
+
|
|
97
|
+
TEST(settings_a_first_run_has_no_settings_and_is_not_an_error) {
|
|
98
|
+
const Scratch scratch;
|
|
99
|
+
const SettingsStore store(scratch.file());
|
|
100
|
+
EXPECT(store.values().isObject());
|
|
101
|
+
EXPECT_EQ((long)store.values().size(), 0L);
|
|
102
|
+
// Nothing is written by reading, so an app that only ever reads settings
|
|
103
|
+
// leaves no file behind.
|
|
104
|
+
EXPECT(!std::filesystem::exists(scratch.file()));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
TEST(settings_outlive_the_process) {
|
|
108
|
+
// The claim the whole module exists for. Two stores over one file is what two
|
|
109
|
+
// runs of a host are, minus the process.
|
|
110
|
+
const Scratch scratch;
|
|
111
|
+
{
|
|
112
|
+
SettingsStore store(scratch.file());
|
|
113
|
+
store.merge(folly::dynamic::object("tab", "inbox")("seen", 3));
|
|
114
|
+
}
|
|
115
|
+
const SettingsStore reopened(scratch.file());
|
|
116
|
+
EXPECT_EQ(stringAt(reopened, "tab"), std::string("inbox"));
|
|
117
|
+
EXPECT_EQ((long)reopened.values()["seen"].asInt(), 3L);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
TEST(settings_merge_answers_only_the_keys_that_changed) {
|
|
121
|
+
const Scratch scratch;
|
|
122
|
+
SettingsStore store(scratch.file());
|
|
123
|
+
|
|
124
|
+
const std::vector<std::string> first = store.merge(folly::dynamic::object("tab", "inbox"));
|
|
125
|
+
EXPECT_EQ((long)first.size(), 1L);
|
|
126
|
+
EXPECT_EQ(first[0], std::string("tab"));
|
|
127
|
+
|
|
128
|
+
// The same value again. An app re-rendering and setting what it already set
|
|
129
|
+
// is the normal case, and it is neither a change to report nor a reason to
|
|
130
|
+
// write the file.
|
|
131
|
+
const std::vector<std::string> again = store.merge(folly::dynamic::object("tab", "inbox"));
|
|
132
|
+
EXPECT(again.empty());
|
|
133
|
+
|
|
134
|
+
const std::vector<std::string> changed = store.merge(folly::dynamic::object("tab", "archive"));
|
|
135
|
+
EXPECT_EQ((long)changed.size(), 1L);
|
|
136
|
+
EXPECT_EQ(stringAt(store, "tab"), std::string("archive"));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
TEST(settings_merge_is_one_key_deep_like_object_assign) {
|
|
140
|
+
// `Settings.set` is `Object.assign` in JavaScript and this has to agree with
|
|
141
|
+
// it, or the copy in memory and the copy on disk disagree about what a nested
|
|
142
|
+
// object holds.
|
|
143
|
+
const Scratch scratch;
|
|
144
|
+
SettingsStore store(scratch.file());
|
|
145
|
+
store.merge(folly::dynamic::object("window", folly::dynamic::object("x", 10)("y", 20)));
|
|
146
|
+
store.merge(folly::dynamic::object("window", folly::dynamic::object("x", 30)));
|
|
147
|
+
EXPECT_EQ((long)store.values()["window"]["x"].asInt(), 30L);
|
|
148
|
+
EXPECT(store.values()["window"].find("y") == store.values()["window"].items().end());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
TEST(settings_a_null_value_deletes_the_key) {
|
|
152
|
+
// iOS's behaviour, and not an accident of it: `RCTSettingsManager` calls
|
|
153
|
+
// `removeObjectForKey:` when the value does not convert to a property list,
|
|
154
|
+
// so `Settings.set({tab: null})` forgets the key there and has to here.
|
|
155
|
+
const Scratch scratch;
|
|
156
|
+
SettingsStore store(scratch.file());
|
|
157
|
+
store.merge(folly::dynamic::object("tab", "inbox"));
|
|
158
|
+
|
|
159
|
+
const std::vector<std::string> changed = store.merge(folly::dynamic::object("tab", nullptr));
|
|
160
|
+
EXPECT_EQ((long)changed.size(), 1L);
|
|
161
|
+
EXPECT_EQ((long)store.values().size(), 0L);
|
|
162
|
+
|
|
163
|
+
// And deleting what is not there changes nothing, so it is not reported.
|
|
164
|
+
EXPECT(store.merge(folly::dynamic::object("tab", nullptr)).empty());
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
TEST(settings_deleting_answers_what_was_actually_there) {
|
|
168
|
+
const Scratch scratch;
|
|
169
|
+
SettingsStore store(scratch.file());
|
|
170
|
+
store.merge(folly::dynamic::object("tab", "inbox")("seen", 3));
|
|
171
|
+
|
|
172
|
+
const std::vector<std::string> removed = store.erase({"tab", "never-set"});
|
|
173
|
+
EXPECT_EQ((long)removed.size(), 1L);
|
|
174
|
+
EXPECT_EQ(removed[0], std::string("tab"));
|
|
175
|
+
EXPECT_EQ((long)store.values().size(), 1L);
|
|
176
|
+
|
|
177
|
+
const SettingsStore reopened(scratch.file());
|
|
178
|
+
EXPECT_EQ((long)reopened.values().size(), 1L);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
TEST(settings_a_file_that_is_not_json_starts_empty_rather_than_failing) {
|
|
182
|
+
const Scratch scratch;
|
|
183
|
+
scratch.write("this is not json at all");
|
|
184
|
+
const SettingsStore store(scratch.file());
|
|
185
|
+
EXPECT_EQ((long)store.values().size(), 0L);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
TEST(settings_a_file_of_the_wrong_shape_starts_empty) {
|
|
189
|
+
// Valid JSON, wrong thing. Keyed access over an array would throw on every
|
|
190
|
+
// call afterwards, so the shape is checked once, at the read.
|
|
191
|
+
const Scratch scratch;
|
|
192
|
+
scratch.write("[1, 2, 3]");
|
|
193
|
+
const SettingsStore store(scratch.file());
|
|
194
|
+
EXPECT(store.values().isObject());
|
|
195
|
+
EXPECT_EQ((long)store.values().size(), 0L);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
TEST(settings_a_bad_file_is_replaced_by_the_next_write) {
|
|
199
|
+
const Scratch scratch;
|
|
200
|
+
scratch.write("{ truncated");
|
|
201
|
+
SettingsStore store(scratch.file());
|
|
202
|
+
store.merge(folly::dynamic::object("tab", "inbox"));
|
|
203
|
+
EXPECT_EQ(stringAt(SettingsStore(scratch.file()), "tab"), std::string("inbox"));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
TEST(settings_the_file_is_replaced_whole_and_leaves_nothing_beside_it) {
|
|
207
|
+
// The write goes to a sibling and is renamed over, so that a process that
|
|
208
|
+
// dies mid-write leaves the previous file rather than half of a new one. The
|
|
209
|
+
// sibling must not survive a successful write: a stale `settings.json.new`
|
|
210
|
+
// next to the real file would be a puzzle for whoever looked.
|
|
211
|
+
const Scratch scratch;
|
|
212
|
+
SettingsStore store(scratch.file());
|
|
213
|
+
store.merge(folly::dynamic::object("tab", "inbox"));
|
|
214
|
+
EXPECT(!std::filesystem::exists(scratch.file().string() + ".new"));
|
|
215
|
+
EXPECT(scratch.read().find("inbox") != std::string::npos);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
TEST(settings_a_directory_that_does_not_exist_yet_is_created) {
|
|
219
|
+
// The first run of a packaged app: nothing under the config directory has its
|
|
220
|
+
// identifier yet.
|
|
221
|
+
const Scratch scratch;
|
|
222
|
+
const std::filesystem::path nested = scratch.directory / "dev.example.demo" / "settings.json";
|
|
223
|
+
SettingsStore store(nested);
|
|
224
|
+
store.merge(folly::dynamic::object("tab", "inbox"));
|
|
225
|
+
EXPECT(std::filesystem::exists(nested));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
TEST(settings_with_nowhere_to_put_them_stay_in_memory) {
|
|
229
|
+
// A container with no HOME and no APPDATA. The alternative would be a module
|
|
230
|
+
// that throws at startup, which is the failure this whole entry was about.
|
|
231
|
+
SettingsStore store({});
|
|
232
|
+
const std::vector<std::string> changed = store.merge(folly::dynamic::object("tab", "inbox"));
|
|
233
|
+
EXPECT_EQ((long)changed.size(), 1L);
|
|
234
|
+
EXPECT_EQ(stringAt(store, "tab"), std::string("inbox"));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
TEST(settings_the_path_can_be_pointed_somewhere_by_the_environment) {
|
|
238
|
+
const Scratch scratch;
|
|
239
|
+
setEnvironment(basalt::kSettingsFileVar, scratch.file().string());
|
|
240
|
+
EXPECT_EQ(basalt::settingsFilePath().string(), scratch.file().string());
|
|
241
|
+
unsetEnvironment(basalt::kSettingsFileVar);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
TEST(settings_live_under_this_desktop_own_config_directory_by_default) {
|
|
245
|
+
// Not the contents of the path -- that is the operating system's answer and
|
|
246
|
+
// differs per machine -- but its shape: somewhere absolute, in a directory of
|
|
247
|
+
// the application's own, called settings.json. An app with no identity file
|
|
248
|
+
// lands under `basalt`, which is what a host built by hand is.
|
|
249
|
+
unsetEnvironment(basalt::kSettingsFileVar);
|
|
250
|
+
const std::filesystem::path path = basalt::settingsFilePath();
|
|
251
|
+
// Empty would mean neither HOME nor APPDATA was set, which is not a machine
|
|
252
|
+
// any suite runs on; if this ever fires, the environment is the finding.
|
|
253
|
+
EXPECT(!path.empty());
|
|
254
|
+
EXPECT(path.is_absolute());
|
|
255
|
+
EXPECT_EQ(path.filename().string(), std::string("settings.json"));
|
|
256
|
+
EXPECT(!path.parent_path().filename().empty());
|
|
257
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// A corner radius grown by a shadow's spread. See core/ShadowShape.h.
|
|
2
|
+
//
|
|
3
|
+
// The curve is the part worth a test: an addition would pass the first two
|
|
4
|
+
// assertions below and fail every other one, and a shadow with the wrong corner
|
|
5
|
+
// is the kind of difference nobody looks at closely enough to report.
|
|
6
|
+
|
|
7
|
+
#include "TestHarness.h"
|
|
8
|
+
|
|
9
|
+
#include "ShadowShape.h"
|
|
10
|
+
|
|
11
|
+
#include <sstream>
|
|
12
|
+
|
|
13
|
+
using basalt::spreadRadius;
|
|
14
|
+
|
|
15
|
+
TEST(shadow_shape_a_square_corner_stays_square_without_a_spread) {
|
|
16
|
+
EXPECT_NEAR(spreadRadius(0.0, 0.0), 0.0, 0.0001);
|
|
17
|
+
EXPECT_NEAR(spreadRadius(12.0, 0.0), 12.0, 0.0001);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
TEST(shadow_shape_a_radius_wider_than_the_spread_just_grows) {
|
|
21
|
+
// Past the knee the curve is the addition: the ratio only bends a corner
|
|
22
|
+
// tighter than the spread.
|
|
23
|
+
EXPECT_NEAR(spreadRadius(20.0, 5.0), 25.0, 0.0001);
|
|
24
|
+
EXPECT_NEAR(spreadRadius(10.0, 10.0), 20.0, 0.0001);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
TEST(shadow_shape_a_tight_corner_is_rounded_more_gently_than_the_spread) {
|
|
28
|
+
// A square corner with a wide spread stays square: the ratio is zero, so the
|
|
29
|
+
// adjustment is multiplied by 1 + (-1)^3, which is nothing. Adding the spread
|
|
30
|
+
// would answer 10 here and turn a card's corner into a circle.
|
|
31
|
+
EXPECT_NEAR(spreadRadius(0.0, 10.0), 0.0, 0.0001);
|
|
32
|
+
|
|
33
|
+
// And in between, less than the addition but more than nothing: a quarter of
|
|
34
|
+
// the way up the ratio the cubic is still most of the way down.
|
|
35
|
+
const double grown = spreadRadius(2.0, 8.0);
|
|
36
|
+
EXPECT(grown > 2.0);
|
|
37
|
+
EXPECT(grown < 10.0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
TEST(shadow_shape_a_negative_spread_shrinks_and_stops_at_zero) {
|
|
41
|
+
// What an inset shadow's hole is built from.
|
|
42
|
+
EXPECT_NEAR(spreadRadius(20.0, -5.0), 15.0, 0.0001);
|
|
43
|
+
// Never negative: a radius that would go through zero is zero, and a negative
|
|
44
|
+
// one would turn a geometry inside out.
|
|
45
|
+
EXPECT_NEAR(spreadRadius(4.0, -20.0), 0.0, 0.0001);
|
|
46
|
+
EXPECT(spreadRadius(0.0, -10.0) >= 0.0);
|
|
47
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// `I18nManager.isRTL`, and the three things that decide it. See
|
|
2
|
+
// core/SystemLocale.h.
|
|
3
|
+
|
|
4
|
+
#include "TestHarness.h"
|
|
5
|
+
|
|
6
|
+
#include "SystemLocale.h"
|
|
7
|
+
|
|
8
|
+
#include <sstream>
|
|
9
|
+
|
|
10
|
+
using basalt::resolveRightToLeft;
|
|
11
|
+
|
|
12
|
+
TEST(rtl_follows_the_locale_when_nobody_has_said_otherwise) {
|
|
13
|
+
EXPECT(resolveRightToLeft(true, std::nullopt, true));
|
|
14
|
+
EXPECT(!resolveRightToLeft(true, std::nullopt, false));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// `forceRTL(true)` is for an app that wants to see its own right-to-left
|
|
18
|
+
// layout on a left-to-right machine, which is the only way most developers
|
|
19
|
+
// ever test one.
|
|
20
|
+
TEST(rtl_can_be_forced_against_the_locale) {
|
|
21
|
+
EXPECT(resolveRightToLeft(true, true, false));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// And `forceRTL(false)` clears the force rather than forcing the other way,
|
|
25
|
+
// which is what React Native's own platforms do: an app turning it off goes
|
|
26
|
+
// back to following the system.
|
|
27
|
+
TEST(rtl_unforced_is_not_forced_left_to_right) {
|
|
28
|
+
EXPECT(resolveRightToLeft(true, std::nullopt, true));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// `allowRTL(false)` wins over everything, which is how an app that has not
|
|
32
|
+
// been laid out for mirroring opts out on a machine whose owner reads Arabic.
|
|
33
|
+
TEST(rtl_can_be_refused_outright) {
|
|
34
|
+
EXPECT(!resolveRightToLeft(false, std::nullopt, true));
|
|
35
|
+
EXPECT(!resolveRightToLeft(false, true, true));
|
|
36
|
+
}
|