basalt-win32 0.0.0-stage → 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/LICENSE +21 -0
- package/cli/runWindows.js +35 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +429 -0
- package/native/tests/test_win32_accessibility.cpp +585 -0
- package/native/tests/test_win32_animated_image.cpp +351 -0
- package/native/tests/test_win32_clipboard.cpp +121 -0
- package/native/tests/test_win32_focus.cpp +195 -0
- package/native/tests/test_win32_hittest.cpp +750 -0
- package/native/tests/test_win32_image.cpp +370 -0
- package/native/tests/test_win32_imageloader.cpp +208 -0
- package/native/tests/test_win32_input.cpp +511 -0
- package/native/tests/test_win32_key_events.cpp +84 -0
- package/native/tests/test_win32_mounting.cpp +757 -0
- package/native/tests/test_win32_paint.cpp +1799 -0
- package/native/tests/test_win32_scrollview.cpp +647 -0
- package/native/tests/test_win32_selection.cpp +215 -0
- package/native/tests/test_win32_text.cpp +1456 -0
- package/native/tests/test_win32_textinput.cpp +1594 -0
- package/native/tests/test_win32_titlebar.cpp +111 -0
- package/native/tests/test_win32_titlebar_actions.cpp +62 -0
- package/native/tests/test_win32_view.cpp +222 -0
- package/native/win32/ColorSchemeWin32.cpp +56 -0
- package/native/win32/ComponentRegistryWin32.cpp +156 -0
- package/native/win32/DirectWriteLayout.cpp +485 -0
- package/native/win32/DirectWriteLayout.h +77 -0
- package/native/win32/DirectWriteLayoutManager.cpp +154 -0
- package/native/win32/FontRegistryDirectWrite.cpp +117 -0
- package/native/win32/PlatformServicesWin32.cpp +744 -0
- package/native/win32/RnWin32Accessible.cpp +343 -0
- package/native/win32/RnWin32Accessible.h +130 -0
- package/native/win32/RnWin32Image.cpp +588 -0
- package/native/win32/RnWin32Image.h +139 -0
- package/native/win32/RnWin32TextLayout.cpp +1379 -0
- package/native/win32/RnWin32TextLayout.h +390 -0
- package/native/win32/RnWin32View.cpp +2956 -0
- package/native/win32/RnWin32View.h +926 -0
- package/native/win32/Win32AnimationChoreographer.cpp +76 -0
- package/native/win32/Win32AnimationChoreographer.h +61 -0
- package/native/win32/Win32Clip.h +209 -0
- package/native/win32/Win32Cursors.cpp +309 -0
- package/native/win32/Win32Cursors.h +121 -0
- package/native/win32/Win32DropTarget.cpp +567 -0
- package/native/win32/Win32DropTarget.h +42 -0
- package/native/win32/Win32Focus.cpp +262 -0
- package/native/win32/Win32Focus.h +109 -0
- package/native/win32/Win32ImageLoader.cpp +199 -0
- package/native/win32/Win32ImageLoader.h +133 -0
- package/native/win32/Win32InputScopes.h +92 -0
- package/native/win32/Win32KeyEvents.cpp +126 -0
- package/native/win32/Win32KeyEvents.h +20 -0
- package/native/win32/Win32MenuBar.cpp +382 -0
- package/native/win32/Win32MenuBar.h +16 -0
- package/native/win32/Win32MountingManager.cpp +1282 -0
- package/native/win32/Win32MountingManager.h +243 -0
- package/native/win32/Win32Offscreen.h +53 -0
- package/native/win32/Win32Packaging.cpp +161 -0
- package/native/win32/Win32Packaging.h +56 -0
- package/native/win32/Win32RunLoopObserver.cpp +48 -0
- package/native/win32/Win32RunLoopObserver.h +42 -0
- package/native/win32/Win32ScrollView.cpp +718 -0
- package/native/win32/Win32ScrollView.h +256 -0
- package/native/win32/Win32SkiaContext.cpp +221 -0
- package/native/win32/Win32SkiaContext.h +53 -0
- package/native/win32/Win32SkiaModule.cpp +80 -0
- package/native/win32/Win32SkiaModule.h +50 -0
- package/native/win32/Win32SkiaPeer.cpp +358 -0
- package/native/win32/Win32SkiaPeer.h +66 -0
- package/native/win32/Win32SkiaUnicode.cpp +42 -0
- package/native/win32/Win32Snapshot.cpp +201 -0
- package/native/win32/Win32Snapshot.h +66 -0
- package/native/win32/Win32Strings.h +54 -0
- package/native/win32/Win32TextInput.cpp +1296 -0
- package/native/win32/Win32TextInput.h +341 -0
- package/native/win32/Win32TitleBar.cpp +562 -0
- package/native/win32/Win32TitleBar.h +146 -0
- package/native/win32/Win32TitleBarLayout.cpp +46 -0
- package/native/win32/Win32TitleBarLayout.h +58 -0
- package/native/win32/Win32TouchDispatcher.cpp +577 -0
- package/native/win32/Win32TouchDispatcher.h +201 -0
- package/native/win32/Win32UiThread.h +27 -0
- package/native/win32/Win32WindowControl.cpp +329 -0
- package/native/win32/Win32WindowModule.cpp +209 -0
- package/native/win32/Win32WindowModule.h +72 -0
- package/native/win32/demo_layout_win32.cpp +232 -0
- package/native/win32/main_win32.cpp +2722 -0
- package/native/win32/mount_harness_win32.cpp +316 -0
- package/package.json +25 -4
- package/react-native.config.js +22 -0
- package/README.md +0 -3
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
// Tests for <ScrollView> on Windows: the offset, the clamp, the wheel's
|
|
2
|
+
// routing, and the commands.
|
|
3
|
+
//
|
|
4
|
+
// The first tests any of the three desktops has for a scroll manager. GTK's and
|
|
5
|
+
// AppKit's are exercised end to end by `scripts/compare_hosts.sh` running
|
|
6
|
+
// `e2e/scroll.js` on both, which is a better test in every way except that it
|
|
7
|
+
// needs two working desktops and this machine has one -- WSL2 will not start
|
|
8
|
+
// here, so nothing else can check Windows against another host. So the
|
|
9
|
+
// arithmetic gets tested directly instead.
|
|
10
|
+
//
|
|
11
|
+
// What is observable from here is narrower than what the manager does, and the
|
|
12
|
+
// two limits are worth naming rather than working around. A ShadowView built by
|
|
13
|
+
// hand has no ShadowNodeFamily, so `State::updateState` finds nothing to commit
|
|
14
|
+
// into and the unthrottled state write-back cannot be asserted on. And an
|
|
15
|
+
// EventEmitter built by hand has no EventDispatcher, so `onScroll` goes
|
|
16
|
+
// nowhere. What is left is the part that decides where the content actually
|
|
17
|
+
// sits: the clamp, the adoption, the routing and the commands.
|
|
18
|
+
|
|
19
|
+
#include "TestHarness.h"
|
|
20
|
+
|
|
21
|
+
#include "Win32MountingManager.h"
|
|
22
|
+
#include "Win32ScrollView.h"
|
|
23
|
+
// For hitTestTag: what a press would find, which is the other half of "the
|
|
24
|
+
// scroll moved" and the half a scroll manager can get silently wrong.
|
|
25
|
+
#include "Win32TouchDispatcher.h"
|
|
26
|
+
|
|
27
|
+
#include <react/renderer/components/scrollview/ScrollViewProps.h>
|
|
28
|
+
#include <react/renderer/components/scrollview/ScrollViewShadowNode.h>
|
|
29
|
+
#include <react/renderer/components/scrollview/ScrollViewState.h>
|
|
30
|
+
#include <react/renderer/components/view/ViewProps.h>
|
|
31
|
+
|
|
32
|
+
#include <memory>
|
|
33
|
+
#include <optional>
|
|
34
|
+
#include <string>
|
|
35
|
+
|
|
36
|
+
using basalt::Win32MountingManager;
|
|
37
|
+
using basalt::Win32ScrollViewManager;
|
|
38
|
+
using basalt::win32::RnWin32View;
|
|
39
|
+
using facebook::react::LayoutMetrics;
|
|
40
|
+
using facebook::react::MountingTransaction;
|
|
41
|
+
using facebook::react::Point;
|
|
42
|
+
using facebook::react::Rect;
|
|
43
|
+
using facebook::react::ScrollViewProps;
|
|
44
|
+
using facebook::react::ScrollViewShadowNode;
|
|
45
|
+
using facebook::react::ScrollViewState;
|
|
46
|
+
using facebook::react::ShadowNodeFamily;
|
|
47
|
+
using facebook::react::ShadowView;
|
|
48
|
+
using facebook::react::ShadowViewMutation;
|
|
49
|
+
using facebook::react::ShadowViewMutationList;
|
|
50
|
+
using facebook::react::Size;
|
|
51
|
+
using facebook::react::SurfaceId;
|
|
52
|
+
using facebook::react::Tag;
|
|
53
|
+
using facebook::react::TransactionTelemetry;
|
|
54
|
+
using facebook::react::ViewProps;
|
|
55
|
+
|
|
56
|
+
namespace {
|
|
57
|
+
|
|
58
|
+
constexpr SurfaceId kSurfaceId = 1;
|
|
59
|
+
|
|
60
|
+
ShadowView makeView(Tag tag, float x, float y, float width, float height) {
|
|
61
|
+
LayoutMetrics metrics;
|
|
62
|
+
metrics.frame = {.origin = {.x = x, .y = y}, .size = {.width = width, .height = height}};
|
|
63
|
+
|
|
64
|
+
ShadowView view;
|
|
65
|
+
view.componentName = "View";
|
|
66
|
+
view.surfaceId = kSurfaceId;
|
|
67
|
+
view.tag = tag;
|
|
68
|
+
view.props = std::make_shared<ViewProps>();
|
|
69
|
+
view.layoutMetrics = metrics;
|
|
70
|
+
return view;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// A ScrollView whose viewport is `width`x`height` and whose content is
|
|
74
|
+
// `contentWidth`x`contentHeight` -- which is the whole of what the manager
|
|
75
|
+
// needs, because Yoga has already laid the content out at its full size by the
|
|
76
|
+
// time a mutation arrives.
|
|
77
|
+
ShadowView makeScrollView(Tag tag,
|
|
78
|
+
float x,
|
|
79
|
+
float y,
|
|
80
|
+
float width,
|
|
81
|
+
float height,
|
|
82
|
+
float contentWidth,
|
|
83
|
+
float contentHeight,
|
|
84
|
+
bool scrollEnabled = true,
|
|
85
|
+
Point initialOffset = Point{0, 0},
|
|
86
|
+
bool centerContent = false,
|
|
87
|
+
facebook::react::ScrollViewIndicatorStyle indicatorStyle =
|
|
88
|
+
facebook::react::ScrollViewIndicatorStyle::Default,
|
|
89
|
+
std::optional<facebook::react::ScrollViewMaintainVisibleContentPosition>
|
|
90
|
+
maintainVisible = std::nullopt) {
|
|
91
|
+
LayoutMetrics metrics;
|
|
92
|
+
metrics.frame = {.origin = {.x = x, .y = y}, .size = {.width = width, .height = height}};
|
|
93
|
+
|
|
94
|
+
auto props = std::make_shared<ScrollViewProps>();
|
|
95
|
+
props->scrollEnabled = scrollEnabled;
|
|
96
|
+
// The prop as well as the state, because that is how the two arrive for real:
|
|
97
|
+
// `ScrollViewShadowNode::initialStateData` seeds the state from this prop, so
|
|
98
|
+
// a test that set only one of them would be testing a shape React Native does
|
|
99
|
+
// not produce.
|
|
100
|
+
props->contentOffset = initialOffset;
|
|
101
|
+
props->centerContent = centerContent;
|
|
102
|
+
props->indicatorStyle = indicatorStyle;
|
|
103
|
+
props->maintainVisibleContentPosition = maintainVisible;
|
|
104
|
+
|
|
105
|
+
ScrollViewState data;
|
|
106
|
+
data.contentOffset = initialOffset;
|
|
107
|
+
data.contentBoundingRect = Rect{.origin = {.x = 0, .y = 0},
|
|
108
|
+
.size = Size{.width = contentWidth, .height = contentHeight}};
|
|
109
|
+
|
|
110
|
+
ShadowView view;
|
|
111
|
+
view.componentName = "ScrollView";
|
|
112
|
+
view.surfaceId = kSurfaceId;
|
|
113
|
+
view.tag = tag;
|
|
114
|
+
view.props = props;
|
|
115
|
+
view.layoutMetrics = metrics;
|
|
116
|
+
// An empty family: nothing can be committed back through it, which is what
|
|
117
|
+
// makes the state write-back unobservable here. See the file header.
|
|
118
|
+
view.state = std::make_shared<const ScrollViewShadowNode::ConcreteState>(
|
|
119
|
+
std::make_shared<const ScrollViewState>(data), ShadowNodeFamily::Weak{});
|
|
120
|
+
return view;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
void apply(Win32MountingManager &manager, ShadowViewMutationList &&mutations) {
|
|
124
|
+
manager.applyTransaction(
|
|
125
|
+
kSurfaceId, MountingTransaction(kSurfaceId, 1, std::move(mutations), TransactionTelemetry{}));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Creates and inserts one shadow view under `parent`, in a single transaction.
|
|
129
|
+
void mount(Win32MountingManager &manager, Tag parent, const ShadowView &shadowView) {
|
|
130
|
+
ShadowViewMutationList mutations;
|
|
131
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(shadowView));
|
|
132
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(parent, shadowView, 0));
|
|
133
|
+
apply(manager, std::move(mutations));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// One notch's worth of pixels, in the direction a contentOffset grows.
|
|
137
|
+
constexpr double kNotch = Win32ScrollViewManager::kWheelStepPixels;
|
|
138
|
+
|
|
139
|
+
} // namespace
|
|
140
|
+
|
|
141
|
+
// The offset is applied to the view, which is where painting and hit testing
|
|
142
|
+
// both read it -- so this is the one assertion that covers all three.
|
|
143
|
+
TEST(win32_a_wheel_scrolls_the_view_under_it) {
|
|
144
|
+
Win32MountingManager manager;
|
|
145
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
146
|
+
root->setFrame(0, 0, 400, 300);
|
|
147
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
|
|
148
|
+
|
|
149
|
+
EXPECT(manager.scrollAt(root, 200, 150, 0, 5 * kNotch));
|
|
150
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 5 * kNotch, 0.01);
|
|
151
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollX(), 0.0, 0.01);
|
|
152
|
+
|
|
153
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// The clamp, at both ends. Content 1000 tall in a 300 viewport can scroll 700
|
|
157
|
+
// and no further, and nothing can scroll above the top.
|
|
158
|
+
TEST(win32_the_offset_is_clamped_to_the_content) {
|
|
159
|
+
Win32MountingManager manager;
|
|
160
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
161
|
+
root->setFrame(0, 0, 400, 300);
|
|
162
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
|
|
163
|
+
|
|
164
|
+
manager.scrollAt(root, 200, 150, 0, 100 * kNotch);
|
|
165
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 700.0, 0.01);
|
|
166
|
+
|
|
167
|
+
manager.scrollAt(root, 200, 150, 0, -100 * kNotch);
|
|
168
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
|
|
169
|
+
|
|
170
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Content that fits does not scroll at all, but the wheel is still consumed --
|
|
174
|
+
// matching AppKit, and matching what every desktop does: a list at its end must
|
|
175
|
+
// not hand the wheel to whatever is behind it.
|
|
176
|
+
TEST(win32_content_that_fits_consumes_the_wheel_and_does_not_move) {
|
|
177
|
+
Win32MountingManager manager;
|
|
178
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
179
|
+
root->setFrame(0, 0, 400, 300);
|
|
180
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 200));
|
|
181
|
+
|
|
182
|
+
EXPECT(manager.scrollAt(root, 200, 150, 0, 5 * kNotch));
|
|
183
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
|
|
184
|
+
|
|
185
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// scrollEnabled: false does not consume, so the wheel keeps going up the tree.
|
|
189
|
+
TEST(win32_a_disabled_scroll_view_passes_the_wheel_on) {
|
|
190
|
+
Win32MountingManager manager;
|
|
191
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
192
|
+
root->setFrame(0, 0, 400, 300);
|
|
193
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000, false));
|
|
194
|
+
|
|
195
|
+
EXPECT(!manager.scrollAt(root, 200, 150, 0, 5 * kNotch));
|
|
196
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
|
|
197
|
+
|
|
198
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// A wheel over a plain row scrolls the list containing it. GTK gets this from
|
|
202
|
+
// attaching a controller per widget and AppKit from the responder chain; here
|
|
203
|
+
// it is the parent walk in scrollAt, so it is worth an assertion.
|
|
204
|
+
TEST(win32_a_wheel_over_a_child_scrolls_its_scroll_view) {
|
|
205
|
+
Win32MountingManager manager;
|
|
206
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
207
|
+
root->setFrame(0, 0, 400, 300);
|
|
208
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
|
|
209
|
+
mount(manager, 10, makeView(11, 0, 0, 400, 80));
|
|
210
|
+
|
|
211
|
+
// The point lands on the row, not on the ScrollView.
|
|
212
|
+
EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 200, 40)), 11L);
|
|
213
|
+
|
|
214
|
+
EXPECT(manager.scrollAt(root, 200, 40, 0, 2 * kNotch));
|
|
215
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 2 * kNotch, 0.01);
|
|
216
|
+
|
|
217
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// A list inside a list scrolls the inner one. Innermost-first is the whole
|
|
221
|
+
// reason the walk starts at the hit view rather than at the root.
|
|
222
|
+
TEST(win32_a_nested_scroll_view_takes_the_wheel_first) {
|
|
223
|
+
Win32MountingManager manager;
|
|
224
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
225
|
+
root->setFrame(0, 0, 400, 300);
|
|
226
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
|
|
227
|
+
mount(manager, 10, makeScrollView(11, 0, 0, 400, 100, 400, 500));
|
|
228
|
+
|
|
229
|
+
EXPECT(manager.scrollAt(root, 200, 50, 0, 1 * kNotch));
|
|
230
|
+
EXPECT_NEAR(manager.viewForTag(11)->scrollY(), kNotch, 0.01);
|
|
231
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
|
|
232
|
+
|
|
233
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Scrolling moves what a press lands on. `hitTest` adds the scroll offset back
|
|
237
|
+
// on the way down, so this needs nothing from the scroll manager -- which is
|
|
238
|
+
// exactly the claim being checked, because a list that scrolls correctly and
|
|
239
|
+
// answers presses meant for the row above is the bug this prevents.
|
|
240
|
+
TEST(win32_hit_testing_follows_the_scroll) {
|
|
241
|
+
Win32MountingManager manager;
|
|
242
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
243
|
+
root->setFrame(0, 0, 400, 300);
|
|
244
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
|
|
245
|
+
// Two rows, the second below the fold at y=400.
|
|
246
|
+
mount(manager, 10, makeView(12, 0, 400, 400, 100));
|
|
247
|
+
mount(manager, 10, makeView(11, 0, 0, 400, 100));
|
|
248
|
+
|
|
249
|
+
EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 200, 50)), 11L);
|
|
250
|
+
|
|
251
|
+
manager.scrollAt(root, 200, 150, 0, 400);
|
|
252
|
+
EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 200, 50)), 12L);
|
|
253
|
+
|
|
254
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// A ScrollView must clip whatever its props say. Its content is deliberately
|
|
258
|
+
// larger than its frame, and without this it paints over its siblings.
|
|
259
|
+
TEST(win32_a_scroll_view_clips_regardless_of_overflow) {
|
|
260
|
+
Win32MountingManager manager;
|
|
261
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
262
|
+
root->setFrame(0, 0, 400, 300);
|
|
263
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
|
|
264
|
+
|
|
265
|
+
EXPECT(manager.viewForTag(10)->clipsChildren());
|
|
266
|
+
|
|
267
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// An offset already in the state is adopted rather than reset. That is what a
|
|
271
|
+
// `contentOffset` prop looks like by the time it reaches here, and what a
|
|
272
|
+
// surface that was suspended and remounted carries.
|
|
273
|
+
TEST(win32_an_initial_offset_in_the_state_is_adopted) {
|
|
274
|
+
Win32MountingManager manager;
|
|
275
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
276
|
+
root->setFrame(0, 0, 400, 300);
|
|
277
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000, true, Point{0, 120}));
|
|
278
|
+
|
|
279
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 120.0, 0.01);
|
|
280
|
+
|
|
281
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Content can shrink under a scrolled offset -- a list whose items were
|
|
285
|
+
// filtered away. The re-clamp on every mutation is what stops the view from
|
|
286
|
+
// being left showing empty space below its own content.
|
|
287
|
+
TEST(win32_shrinking_content_pulls_the_offset_back) {
|
|
288
|
+
Win32MountingManager manager;
|
|
289
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
290
|
+
root->setFrame(0, 0, 400, 300);
|
|
291
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
|
|
292
|
+
|
|
293
|
+
manager.scrollAt(root, 200, 150, 0, 700);
|
|
294
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 700.0, 0.01);
|
|
295
|
+
|
|
296
|
+
ShadowViewMutationList mutations;
|
|
297
|
+
mutations.push_back(
|
|
298
|
+
ShadowViewMutation::UpdateMutation(makeScrollView(10, 0, 0, 400, 300, 400, 1000),
|
|
299
|
+
makeScrollView(10, 0, 0, 400, 300, 400, 500),
|
|
300
|
+
kSurfaceId));
|
|
301
|
+
apply(manager, std::move(mutations));
|
|
302
|
+
|
|
303
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 200.0, 0.01);
|
|
304
|
+
|
|
305
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// scrollTo and scrollToEnd, which arrive through dispatchCommand. e2e/scroll.js
|
|
309
|
+
// drives itself through a ref, so this is the path that file exercises and the
|
|
310
|
+
// only one that needs no wheel.
|
|
311
|
+
TEST(win32_scroll_to_and_scroll_to_end_move_the_offset) {
|
|
312
|
+
Win32MountingManager manager;
|
|
313
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
314
|
+
root->setFrame(0, 0, 400, 300);
|
|
315
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
|
|
316
|
+
|
|
317
|
+
manager.applyCommand(10, "scrollTo", folly::dynamic::array(0, 265, false));
|
|
318
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 265.0, 0.01);
|
|
319
|
+
|
|
320
|
+
manager.applyCommand(10, "scrollToEnd", folly::dynamic::array(false));
|
|
321
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 700.0, 0.01);
|
|
322
|
+
|
|
323
|
+
// scrollTo past the end is clamped like any other offset.
|
|
324
|
+
manager.applyCommand(10, "scrollTo", folly::dynamic::array(0, 5000, false));
|
|
325
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 700.0, 0.01);
|
|
326
|
+
|
|
327
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// A command for a tag that is not a ScrollView, and one for a tag that is gone.
|
|
331
|
+
// Both are ordinary: a scrollTo can arrive after the surface that owned it was
|
|
332
|
+
// torn down.
|
|
333
|
+
TEST(win32_a_command_for_an_unknown_tag_is_survivable) {
|
|
334
|
+
Win32MountingManager manager;
|
|
335
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
336
|
+
root->setFrame(0, 0, 400, 300);
|
|
337
|
+
mount(manager, kSurfaceId, makeView(10, 0, 0, 100, 100));
|
|
338
|
+
|
|
339
|
+
manager.applyCommand(10, "scrollTo", folly::dynamic::array(0, 100, false));
|
|
340
|
+
manager.applyCommand(999, "scrollTo", folly::dynamic::array(0, 100, false));
|
|
341
|
+
manager.applyCommand(10, "somethingElse", folly::dynamic::array());
|
|
342
|
+
|
|
343
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// The entry has to go with the view. A wheel arriving after the ScrollView was
|
|
347
|
+
// deleted must find nothing rather than a dangling RnWin32View pointer.
|
|
348
|
+
TEST(win32_deleting_a_scroll_view_forgets_its_entry) {
|
|
349
|
+
Win32MountingManager manager;
|
|
350
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
351
|
+
root->setFrame(0, 0, 400, 300);
|
|
352
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
|
|
353
|
+
EXPECT(manager.scrollAt(root, 200, 150, 0, kNotch));
|
|
354
|
+
|
|
355
|
+
ShadowViewMutationList mutations;
|
|
356
|
+
mutations.push_back(ShadowViewMutation::RemoveMutation(
|
|
357
|
+
kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000), 0));
|
|
358
|
+
mutations.push_back(
|
|
359
|
+
ShadowViewMutation::DeleteMutation(makeScrollView(10, 0, 0, 400, 300, 400, 1000)));
|
|
360
|
+
apply(manager, std::move(mutations));
|
|
361
|
+
|
|
362
|
+
EXPECT(manager.viewForTag(10) == nullptr);
|
|
363
|
+
EXPECT(!manager.scrollAt(root, 200, 150, 0, kNotch));
|
|
364
|
+
manager.applyCommand(10, "scrollTo", folly::dynamic::array(0, 100, false));
|
|
365
|
+
|
|
366
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Nothing under the pointer, and no surface at all. The host reads the return
|
|
370
|
+
// value to decide whether to leave the message to DefWindowProc, so "false"
|
|
371
|
+
// here is the difference between a wheel over a non-scrolling app doing nothing
|
|
372
|
+
// and doing nothing *and* being swallowed.
|
|
373
|
+
TEST(win32_a_wheel_over_nothing_is_not_consumed) {
|
|
374
|
+
Win32MountingManager manager;
|
|
375
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
376
|
+
root->setFrame(0, 0, 400, 300);
|
|
377
|
+
mount(manager, kSurfaceId, makeView(10, 0, 0, 100, 100));
|
|
378
|
+
|
|
379
|
+
EXPECT(!manager.scrollAt(root, 50, 50, 0, kNotch));
|
|
380
|
+
EXPECT(!manager.scrollAt(root, 5000, 5000, 0, kNotch));
|
|
381
|
+
EXPECT(!manager.scrollAt(nullptr, 50, 50, 0, kNotch));
|
|
382
|
+
|
|
383
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Horizontal, which travels the same path with the other axis and is easy to
|
|
387
|
+
// leave half-wired: WM_MOUSEHWHEEL is a separate message with the opposite
|
|
388
|
+
// sign convention to WM_MOUSEWHEEL.
|
|
389
|
+
TEST(win32_a_horizontal_wheel_scrolls_the_other_axis) {
|
|
390
|
+
Win32MountingManager manager;
|
|
391
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
392
|
+
root->setFrame(0, 0, 400, 300);
|
|
393
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 1200, 300));
|
|
394
|
+
|
|
395
|
+
EXPECT(manager.scrollAt(root, 200, 150, 3 * kNotch, 0));
|
|
396
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollX(), 3 * kNotch, 0.01);
|
|
397
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
|
|
398
|
+
|
|
399
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// A `contentOffset` that *changes* moves the list, which no host did until
|
|
403
|
+
// 2026-10-10: the first value arrives through the state and is adopted above,
|
|
404
|
+
// and a later one arrives as an ordinary prop update.
|
|
405
|
+
TEST(win32_a_changed_content_offset_moves_the_list) {
|
|
406
|
+
Win32MountingManager manager;
|
|
407
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
408
|
+
root->setFrame(0, 0, 400, 300);
|
|
409
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 4000, true, Point{0, 120}));
|
|
410
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 120.0, 0.01);
|
|
411
|
+
|
|
412
|
+
ShadowViewMutationList mutations;
|
|
413
|
+
mutations.push_back(ShadowViewMutation::UpdateMutation(
|
|
414
|
+
makeScrollView(10, 0, 0, 400, 300, 400, 4000, true, Point{0, 120}),
|
|
415
|
+
makeScrollView(10, 0, 0, 400, 300, 400, 4000, true, Point{0, 700}),
|
|
416
|
+
kSurfaceId));
|
|
417
|
+
apply(manager, std::move(mutations));
|
|
418
|
+
|
|
419
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 700.0, 0.01);
|
|
420
|
+
|
|
421
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// And one that has not changed leaves the list alone, which is the rule rather
|
|
425
|
+
// than an optimisation: React Native re-renders for all sorts of reasons and
|
|
426
|
+
// every one of them carries the same `contentOffset` the app wrote once, so a
|
|
427
|
+
// host that applied it whenever it differed would drag the list back under the
|
|
428
|
+
// person reading it.
|
|
429
|
+
TEST(win32_an_unchanged_content_offset_leaves_the_list_where_it_is) {
|
|
430
|
+
Win32MountingManager manager;
|
|
431
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
432
|
+
root->setFrame(0, 0, 400, 300);
|
|
433
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 4000, true, Point{0, 120}));
|
|
434
|
+
|
|
435
|
+
manager.scrollAt(root, 200, 150, 0, 900);
|
|
436
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 1020.0, 0.01);
|
|
437
|
+
|
|
438
|
+
ShadowViewMutationList mutations;
|
|
439
|
+
mutations.push_back(ShadowViewMutation::UpdateMutation(
|
|
440
|
+
makeScrollView(10, 0, 0, 400, 300, 400, 4000, true, Point{0, 120}),
|
|
441
|
+
makeScrollView(10, 0, 0, 400, 300, 400, 4000, true, Point{0, 120}),
|
|
442
|
+
kSurfaceId));
|
|
443
|
+
apply(manager, std::move(mutations));
|
|
444
|
+
|
|
445
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 1020.0, 0.01);
|
|
446
|
+
|
|
447
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// `centerContent`: content smaller than the container sits in the middle of it.
|
|
451
|
+
// The arithmetic is core/ScrollBounds.h's -- an inset of half the slack at each
|
|
452
|
+
// end, leaving one offset to rest at -- so this asserts the prop reaches it and
|
|
453
|
+
// that the clamp this host already ran puts the content there.
|
|
454
|
+
TEST(win32_center_content_centres_content_that_fits) {
|
|
455
|
+
Win32MountingManager manager;
|
|
456
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
457
|
+
root->setFrame(0, 0, 400, 300);
|
|
458
|
+
// 200 of content in a 300 container: 100 of slack, so an offset of -50.
|
|
459
|
+
mount(manager,
|
|
460
|
+
kSurfaceId,
|
|
461
|
+
makeScrollView(10, 0, 0, 400, 300, 400, 200, true, Point{0, 0}, /*centerContent=*/true));
|
|
462
|
+
|
|
463
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), -50.0, 0.01);
|
|
464
|
+
|
|
465
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
TEST(win32_content_that_fits_is_not_centred_unless_asked) {
|
|
469
|
+
Win32MountingManager manager;
|
|
470
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
471
|
+
root->setFrame(0, 0, 400, 300);
|
|
472
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 200));
|
|
473
|
+
|
|
474
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
|
|
475
|
+
|
|
476
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
TEST(win32_center_content_does_nothing_to_a_list_that_scrolls) {
|
|
480
|
+
Win32MountingManager manager;
|
|
481
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
482
|
+
root->setFrame(0, 0, 400, 300);
|
|
483
|
+
mount(manager,
|
|
484
|
+
kSurfaceId,
|
|
485
|
+
makeScrollView(10, 0, 0, 400, 300, 400, 4000, true, Point{0, 0}, /*centerContent=*/true));
|
|
486
|
+
|
|
487
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
|
|
488
|
+
// And the list cannot be pulled above its first row, which an inset here
|
|
489
|
+
// would have allowed.
|
|
490
|
+
manager.scrollAt(root, 200, 150, 0, -50);
|
|
491
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
|
|
492
|
+
|
|
493
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// `indicatorStyle`: the prop reaching the thumb's colour, and the dump line
|
|
497
|
+
// that is the only part of it a cross-host diff can see. What the colour draws
|
|
498
|
+
// as is a pixel question, which test_win32_paint.cpp answers.
|
|
499
|
+
TEST(win32_indicator_style_reaches_the_thumb) {
|
|
500
|
+
Win32MountingManager manager;
|
|
501
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
502
|
+
root->setFrame(0, 0, 400, 300);
|
|
503
|
+
mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 4000));
|
|
504
|
+
|
|
505
|
+
const std::string black = manager.viewForTag(10)->describeTree();
|
|
506
|
+
EXPECT(black.find("scrollbar-v=") != std::string::npos);
|
|
507
|
+
// A default thumb prints no colour, like every other field at its default.
|
|
508
|
+
EXPECT(black.find("scrollbar-colour") == std::string::npos);
|
|
509
|
+
|
|
510
|
+
ShadowViewMutationList mutations;
|
|
511
|
+
mutations.push_back(ShadowViewMutation::UpdateMutation(
|
|
512
|
+
makeScrollView(10, 0, 0, 400, 300, 400, 4000),
|
|
513
|
+
makeScrollView(10,
|
|
514
|
+
0,
|
|
515
|
+
0,
|
|
516
|
+
400,
|
|
517
|
+
300,
|
|
518
|
+
400,
|
|
519
|
+
4000,
|
|
520
|
+
true,
|
|
521
|
+
Point{0, 0},
|
|
522
|
+
false,
|
|
523
|
+
facebook::react::ScrollViewIndicatorStyle::White),
|
|
524
|
+
kSurfaceId));
|
|
525
|
+
apply(manager, std::move(mutations));
|
|
526
|
+
|
|
527
|
+
const std::string white = manager.viewForTag(10)->describeTree();
|
|
528
|
+
EXPECT(white.find("scrollbar-colour=(1,1,1,0.35)") != std::string::npos);
|
|
529
|
+
|
|
530
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// --- maintainVisibleContentPosition ------------------------------------------
|
|
534
|
+
//
|
|
535
|
+
// The prop a chat list is written with: messages arrive above what the person
|
|
536
|
+
// is reading, and the content under their eye must not jump by the height of
|
|
537
|
+
// whatever was inserted.
|
|
538
|
+
//
|
|
539
|
+
// Driven through real transactions, which is the only way to drive it: the two
|
|
540
|
+
// halves run before and after the mutations, so a test that called them itself
|
|
541
|
+
// would not be testing that anything calls them. The arithmetic is
|
|
542
|
+
// core/tests/test_scroll_visible_position.cpp's.
|
|
543
|
+
namespace {
|
|
544
|
+
|
|
545
|
+
facebook::react::ScrollViewMaintainVisibleContentPosition maintainFrom(int minIndex) {
|
|
546
|
+
facebook::react::ScrollViewMaintainVisibleContentPosition maintain;
|
|
547
|
+
maintain.minIndexForVisible = minIndex;
|
|
548
|
+
return maintain;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
ShadowView listScroller(
|
|
552
|
+
std::optional<facebook::react::ScrollViewMaintainVisibleContentPosition> maintain) {
|
|
553
|
+
return makeScrollView(10,
|
|
554
|
+
0,
|
|
555
|
+
0,
|
|
556
|
+
400,
|
|
557
|
+
300,
|
|
558
|
+
400,
|
|
559
|
+
1000,
|
|
560
|
+
true,
|
|
561
|
+
Point{0, 0},
|
|
562
|
+
false,
|
|
563
|
+
facebook::react::ScrollViewIndicatorStyle::Default,
|
|
564
|
+
maintain);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// A ScrollView with a content view and two hundred-point rows under it, which
|
|
568
|
+
// is the smallest tree the prop can be asked about.
|
|
569
|
+
void mountList(Win32MountingManager &manager, const ShadowView &scroller) {
|
|
570
|
+
ShadowViewMutationList mutations;
|
|
571
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(scroller));
|
|
572
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, scroller, 0));
|
|
573
|
+
const ShadowView content = makeView(11, 0, 0, 400, 1000);
|
|
574
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(content));
|
|
575
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(10, content, 0));
|
|
576
|
+
const ShadowView first = makeView(12, 0, 0, 400, 100);
|
|
577
|
+
const ShadowView second = makeView(13, 0, 100, 400, 100);
|
|
578
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(first));
|
|
579
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(11, first, 0));
|
|
580
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(second));
|
|
581
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(11, second, 1));
|
|
582
|
+
apply(manager, std::move(mutations));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// The first row grows by a hundred points, which pushes the second one down by
|
|
586
|
+
// the same -- the shape an insertion above the visible content has.
|
|
587
|
+
void growTheFirstRow(Win32MountingManager &manager) {
|
|
588
|
+
ShadowViewMutationList mutations;
|
|
589
|
+
mutations.push_back(ShadowViewMutation::UpdateMutation(
|
|
590
|
+
makeView(12, 0, 0, 400, 100), makeView(12, 0, 0, 400, 200), kSurfaceId));
|
|
591
|
+
mutations.push_back(ShadowViewMutation::UpdateMutation(
|
|
592
|
+
makeView(13, 0, 100, 400, 100), makeView(13, 0, 200, 400, 100), kSurfaceId));
|
|
593
|
+
apply(manager, std::move(mutations));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
} // namespace
|
|
597
|
+
|
|
598
|
+
TEST(win32_maintain_visible_content_position_holds_the_content_still) {
|
|
599
|
+
Win32MountingManager manager;
|
|
600
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
601
|
+
root->setFrame(0, 0, 400, 300);
|
|
602
|
+
mountList(manager, listScroller(maintainFrom(0)));
|
|
603
|
+
|
|
604
|
+
// Scrolled to 150: the first row is entirely above the viewport and the
|
|
605
|
+
// second is the first with anything on screen, so the second is held still.
|
|
606
|
+
manager.scrollAt(root, 200, 150, 0, 150);
|
|
607
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 150.0, 0.01);
|
|
608
|
+
|
|
609
|
+
growTheFirstRow(manager);
|
|
610
|
+
|
|
611
|
+
// The row moved down by a hundred, so the offset did: what was under the eye
|
|
612
|
+
// still is.
|
|
613
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 250.0, 0.01);
|
|
614
|
+
|
|
615
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
TEST(win32_a_list_that_did_not_ask_lets_the_content_jump) {
|
|
619
|
+
// The control, and the behaviour of every list that never set the prop.
|
|
620
|
+
Win32MountingManager manager;
|
|
621
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
622
|
+
root->setFrame(0, 0, 400, 300);
|
|
623
|
+
mountList(manager, listScroller(std::nullopt));
|
|
624
|
+
|
|
625
|
+
manager.scrollAt(root, 200, 150, 0, 150);
|
|
626
|
+
growTheFirstRow(manager);
|
|
627
|
+
|
|
628
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 150.0, 0.01);
|
|
629
|
+
|
|
630
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
TEST(win32_min_index_for_visible_skips_a_header) {
|
|
634
|
+
// At the top of the list the first row is the visible one, and an index of 1
|
|
635
|
+
// takes it out of the running -- so the second row is watched, and that one
|
|
636
|
+
// moves. A list that watched the header would adjust by nothing.
|
|
637
|
+
Win32MountingManager manager;
|
|
638
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
639
|
+
root->setFrame(0, 0, 400, 300);
|
|
640
|
+
mountList(manager, listScroller(maintainFrom(1)));
|
|
641
|
+
|
|
642
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
|
|
643
|
+
growTheFirstRow(manager);
|
|
644
|
+
EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 100.0, 0.01);
|
|
645
|
+
|
|
646
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
647
|
+
}
|