Skip to content

Commit f8ae3ba

Browse files
committed
docs: colocate MVCP test coverage comments into test source files
Add Trigger/Expected comments to each maintainVisibleContentPosition test in VirtualizedList-test.js and ScrollView-maintainVisibleContentPosition- itest.js, moving test coverage documentation from TESTING.md directly alongside the test code it describes.
1 parent cb9ed9b commit f8ae3ba

3 files changed

Lines changed: 106 additions & 711 deletions

File tree

packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-maintainVisibleContentPosition-itest.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ function renderItem(item) {
4747
);
4848
}
4949

50+
// Trigger: Items inserted at beginning of data array. FlatList re-renders, native mounts new views at top.
51+
// Expected: Anchor view shifts downward by total height of prepended items. MVCP captures anchor's pre-mount frame,
52+
// computes delta = newFrame - oldFrame, adjusts contentOffset to keep anchor at same screen position.
5053
test('maintainVisibleContentPosition preserves position on prepend', () => {
5154
const root = Fantom.createRoot({
5255
viewportWidth: 100,
@@ -124,6 +127,9 @@ test('maintainVisibleContentPosition preserves position on prepend', () => {
124127
expect(prependingLogs.some(log => log.includes('item_5'))).toBe(true);
125128
});
126129

130+
// Trigger: Multiple prepend operations in quick succession (no user interaction between batches).
131+
// The `pendingScrollUpdateCount` mechanism prevents render window adjustment during MVCP corrections.
132+
// Expected: Each prepend's delta applied sequentially. Anchor's final position after all prepends should be stable.
127133
test('maintainVisibleContentPosition handles consecutive prepends without drift', () => {
128134
const root = Fantom.createRoot({
129135
viewportWidth: 100,
@@ -186,6 +192,7 @@ test('maintainVisibleContentPosition handles consecutive prepends without drift'
186192
expect(lastLogs.some(log => log.includes('item_19'))).toBe(true);
187193
});
188194

195+
// Ensures normal scrolling is not affected by MVCP prop being set.
189196
test('maintainVisibleContentPosition does not interfere with normal scroll', () => {
190197
const root = Fantom.createRoot({
191198
viewportWidth: 100,
@@ -234,6 +241,8 @@ test('maintainVisibleContentPosition does not interfere with normal scroll', ()
234241
expect(logs.length).toBeGreaterThan(0);
235242
});
236243

244+
// Trigger: ScrollView with autoscrollToTopThreshold set.
245+
// Expected: When scroll offset drops below threshold, ScrollView auto-scrolls to top. MVCP should not interfere.
237246
test('maintainVisibleContentPosition with autoscrollToTopThreshold triggers scroll to top', () => {
238247
const root = Fantom.createRoot({
239248
viewportWidth: 100,
@@ -292,6 +301,9 @@ test('maintainVisibleContentPosition with autoscrollToTopThreshold triggers scro
292301
expect(prependingLogs.length).toBeGreaterThan(0);
293302
});
294303

304+
// Trigger: ScrollView with maintainVisibleContentPosition={{minIndexForVisible: N}}.
305+
// Expected: Same MVCP logic as FlatList, but ScrollView has fixed set of subviews (no virtualization).
306+
// Anchor is the Nth subview whose bottom edge is below scroll offset.
295307
test('maintainVisibleContentPosition with minIndexForVisible > 0 skips early items', () => {
296308
const root = Fantom.createRoot({
297309
viewportWidth: 100,
@@ -346,6 +358,9 @@ test('maintainVisibleContentPosition with minIndexForVisible > 0 skips early ite
346358
expect(logs2.length).toBeGreaterThan(0);
347359
});
348360

361+
// Trigger: Vertically inverted FlatList (inverted={true}). Items rendered in reverse order.
362+
// Expected: Inverted mode uses CSS transforms (scaleY: -1) to flip visual order. Native subview order unchanged.
363+
// MVCP finds first subview whose bottom edge is below scroll offset — the visually-topmost visible item.
349364
test('maintainVisibleContentPosition with inverted ScrollView preserves position on prepend', () => {
350365
const root = Fantom.createRoot({
351366
viewportWidth: 100,
@@ -406,6 +421,8 @@ test('maintainVisibleContentPosition with inverted ScrollView preserves position
406421
expect(prependingLogs.some(log => log.includes('item_5'))).toBe(true);
407422
});
408423

424+
// Trigger: Multiple prepends in inverted mode.
425+
// Expected: Tag comparison safeguard must work correctly in inverted mode.
409426
test('maintainVisibleContentPosition with inverted ScrollView handles consecutive prepends', () => {
410427
const root = Fantom.createRoot({
411428
viewportWidth: 100,
@@ -470,6 +487,9 @@ test('maintainVisibleContentPosition with inverted ScrollView handles consecutiv
470487
expect(lastLogs.some(log => log.includes('item_19'))).toBe(true);
471488
});
472489

490+
// Trigger: User actively dragging (touch-scrolling) when data change triggers MVCP.
491+
// Expected: MVCP correction may compete with user's scroll. Scroll skip guard on `patch/add-scrolling-guard`
492+
// branch would skip correction during user dragging, but this is NOT merged.
473493
test('maintainVisibleContentPosition does not interrupt scroll during prepend', () => {
474494
const root = Fantom.createRoot({
475495
viewportWidth: 100,
@@ -535,6 +555,8 @@ test('maintainVisibleContentPosition does not interrupt scroll during prepend',
535555
expect(prependLogs.some(log => log.includes('item_10'))).toBe(true);
536556
});
537557

558+
// Trigger: Horizontally scrolling list in left-to-right layout direction.
559+
// Expected: Both iOS and Android compute deltas using frames directly, in same coordinate space as contentOffset.
538560
test('maintainVisibleContentPosition preserves position on horizontal prepend', () => {
539561
const root = Fantom.createRoot({
540562
viewportWidth: VIEWPORT_HEIGHT,
@@ -589,6 +611,8 @@ test('maintainVisibleContentPosition preserves position on horizontal prepend',
589611
expect(logs.some(log => log.includes('item_5'))).toBe(true);
590612
});
591613

614+
// Trigger: Horizontally inverted FlatList.
615+
// Expected: Same as vertical inverted — CSS transform flips visual order, native subview order unchanged.
592616
test('maintainVisibleContentPosition preserves position on horizontal + inverted prepend', () => {
593617
const root = Fantom.createRoot({
594618
viewportWidth: VIEWPORT_HEIGHT,
@@ -643,6 +667,8 @@ test('maintainVisibleContentPosition preserves position on horizontal + inverted
643667
expect(logs.some(log => log.includes('item_5'))).toBe(true);
644668
});
645669

670+
// Trigger: Items inserted at end of data array.
671+
// Expected: Appends don't shift existing items' frames, so MVCP delta is 0 and no scroll correction triggered.
646672
test('maintainVisibleContentPosition does not trigger correction on append', () => {
647673
const root = Fantom.createRoot({
648674
viewportWidth: 100,
@@ -696,6 +722,8 @@ test('maintainVisibleContentPosition does not trigger correction on append', ()
696722
expect(logs.some(log => log.includes('item_20'))).toBe(true);
697723
});
698724

725+
// Trigger: Item currently at anchor position (first visible) removed from data array.
726+
// Expected: Anchor shifts to next visible item. MVCP captures new anchor's frame, computes delta, adjusts scroll.
699727
test('maintainVisibleContentPosition handles delete of anchor item', () => {
700728
const root = Fantom.createRoot({
701729
viewportWidth: 100,
@@ -746,6 +774,8 @@ test('maintainVisibleContentPosition handles delete of anchor item', () => {
746774
expect(logs.some(log => log.includes('item_6'))).toBe(true);
747775
});
748776

777+
// Trigger: Item not at anchor position removed from data array.
778+
// Expected: If deleted item is above anchor, anchor shifts up. MVCP delta = newFrame - oldFrame.
749779
test('maintainVisibleContentPosition handles delete from middle of list', () => {
750780
const root = Fantom.createRoot({
751781
viewportWidth: 100,
@@ -796,6 +826,9 @@ test('maintainVisibleContentPosition handles delete from middle of list', () =>
796826
expect(logs.some(log => log.includes('item_10'))).toBe(true);
797827
});
798828

829+
// Trigger: All items removed from data array. List becomes empty.
830+
// Expected: `_recomputeFirstVisibleViewForMaintainVisibleContentPosition` doesn't execute (loop doesn't run).
831+
// nil check prevents accessing frame on nil/invalid view.
799832
test('maintainVisibleContentPosition handles empty list gracefully', () => {
800833
const root = Fantom.createRoot({
801834
viewportWidth: 100,
@@ -842,6 +875,9 @@ test('maintainVisibleContentPosition handles empty list gracefully', () => {
842875
expect(logs.length).toBeGreaterThan(0);
843876
});
844877

878+
// Trigger: Items positioned above anchor grow in size (e.g., images load, expandable content opens).
879+
// Expected: Mathematical invariant `deltaY = newAnchorY - oldAnchorY = growth_of_items_above_anchor` holds.
880+
// Anchor's screen position remains constant. Anchor can never be pushed off-screen by sibling growth alone.
845881
test('maintainVisibleContentPosition handles sibling items above anchor growing', () => {
846882
const root = Fantom.createRoot({
847883
viewportWidth: 100,
@@ -907,6 +943,8 @@ test('maintainVisibleContentPosition handles sibling items above anchor growing'
907943
expect(logs.some(log => log.includes('item_8'))).toBe(true);
908944
});
909945

946+
// Trigger: Items positioned above anchor shrink in size.
947+
// Expected: Same invariant as growth, but delta is negative. contentOffset decreases by shrinkage amount.
910948
test('maintainVisibleContentPosition handles sibling items above anchor shrinking', () => {
911949
const root = Fantom.createRoot({
912950
viewportWidth: 100,
@@ -972,6 +1010,10 @@ test('maintainVisibleContentPosition handles sibling items above anchor shrinkin
9721010
expect(logs.some(log => log.includes('item_8'))).toBe(true);
9731011
});
9741012

1013+
// Trigger: setData([]) + scrollToOffset(0) clears and repopulates list.
1014+
// Expected: If old anchor key exists in new data, position maintained. Otherwise, JS computes adjustment as null
1015+
// and native side recomputes anchor from new view hierarchy.
1016+
// Two abort conditions: tag check (catches view recycling), superview check (catches view deletion).
9751017
test('maintainVisibleContentPosition handles data reset with entire data replacement', () => {
9761018
const root = Fantom.createRoot({
9771019
viewportWidth: 100,
@@ -1022,6 +1064,9 @@ test('maintainVisibleContentPosition handles data reset with entire data replace
10221064
expect(logs.some(log => log.includes('item_105'))).toBe(true);
10231065
});
10241066

1067+
// Trigger: List rendered with initialScrollIndex pointing to non-first item, then items prepended.
1068+
// Expected: If initialScrollIndex refers to item pushed by prepend, scroll destination may be wrong
1069+
// because JS's initial scroll calculation doesn't account for MVCP corrections.
10251070
test('maintainVisibleContentPosition with initialScrollIndex + prepend after remount', () => {
10261071
const root = Fantom.createRoot({
10271072
viewportWidth: 100,
@@ -1072,6 +1117,9 @@ test('maintainVisibleContentPosition with initialScrollIndex + prepend after rem
10721117
expect(logs.some(log => log.includes('item_20'))).toBe(true);
10731118
});
10741119

1120+
// Trigger: Horizontally scrolling list in right-to-left layout direction.
1121+
// Expected: Frame-based delta computation should work for RTL since frames are in same coordinate space as contentOffset.
1122+
// contentInset handling in RTL not explicitly tested.
10751123
test('maintainVisibleContentPosition preserves position on horizontal prepend in RTL', () => {
10761124
const root = Fantom.createRoot({
10771125
viewportWidth: VIEWPORT_HEIGHT,
@@ -1128,6 +1176,8 @@ test('maintainVisibleContentPosition preserves position on horizontal prepend in
11281176
expect(logs.some(log => log.includes('item_20'))).toBe(true);
11291177
});
11301178

1179+
// Trigger: Multiple mutation types in same data batch: items prepended at top, appended at bottom, deleted from middle.
1180+
// Expected: Anchor's final frame reflects ALL changes, delta correct for net effect.
11311181
test('maintainVisibleContentPosition handles complex concurrent mutations (prepend + append + middle delete)', () => {
11321182
const root = Fantom.createRoot({
11331183
viewportWidth: 100,
@@ -1190,6 +1240,9 @@ test('maintainVisibleContentPosition handles complex concurrent mutations (prepe
11901240
expect(logs.some(log => log.includes('item_20'))).toBe(true);
11911241
});
11921242

1243+
// Trigger: FlatList with getItemLayout prop providing fixed item dimensions.
1244+
// Expected: Native MVCP always reads actual frames, so accurate regardless of JS metrics.
1245+
// getItemLayout doesn't affect native MVCP.
11931246
test('maintainVisibleContentPosition with getItemLayout prop', () => {
11941247
const root = Fantom.createRoot({
11951248
viewportWidth: 100,
@@ -1257,6 +1310,8 @@ test('maintainVisibleContentPosition with getItemLayout prop', () => {
12571310
expect(logs.some(log => log.includes('item_7'))).toBe(true);
12581311
});
12591312

1313+
// Trigger: Content view has only spacers (placeholder views with no data binding) in visible area.
1314+
// Expected: Anchor selection incorrect. Delta computed from spacer's frame is meaningless.
12601315
test('maintainVisibleContentPosition handles all items culled (spacers only in viewport)', () => {
12611316
const root = Fantom.createRoot({
12621317
viewportWidth: 100,
@@ -1342,6 +1397,8 @@ test('maintainVisibleContentPosition handles all items culled (spacers only in v
13421397
expect(logs.some(log => log.includes('item_10'))).toBe(true);
13431398
});
13441399

1400+
// Trigger: User performs pull-to-refresh which triggers data prepend.
1401+
// Expected: Pull-to-refresh typically scrolls to top, then prepends items. MVCP handles prepend delta after refresh.
13451402
test('maintainVisibleContentPosition simulates pull-to-refresh pattern', () => {
13461403
const root = Fantom.createRoot({
13471404
viewportWidth: 100,
@@ -1404,6 +1461,9 @@ test('maintainVisibleContentPosition simulates pull-to-refresh pattern', () => {
14041461
expect(logs.some(log => log.includes('item_5'))).toBe(true);
14051462
});
14061463

1464+
// Trigger: ScrollView unmounted (user navigates away), then remounted (new screen).
1465+
// Expected: iOS Fabric: prepareForRecycle resets anchor state. Android: stop() removes UIManager listener.
1466+
// Fresh MVCP state initialization on remount.
14071467
test('maintainVisibleContentPosition handles unmount/remount (navigation pattern)', () => {
14081468
const root = Fantom.createRoot({
14091469
viewportWidth: 100,
@@ -1465,6 +1525,8 @@ test('maintainVisibleContentPosition handles unmount/remount (navigation pattern
14651525
expect(remountLogs.some(log => log.includes('item_55'))).toBe(true);
14661526
});
14671527

1528+
// Trigger: Keyboard or safe area insets change, changing ScrollView's contentInset.
1529+
// Expected: Frame-based MVCP delta computation not affected by inset changes because frames are in content coordinates.
14681530
test('maintainVisibleContentPosition handles contentInset changes (keyboard/safe area)', () => {
14691531
const root = Fantom.createRoot({
14701532
viewportWidth: 100,
@@ -1520,6 +1582,9 @@ test('maintainVisibleContentPosition handles contentInset changes (keyboard/safe
15201582
expect(logs.some(log => log.includes('item_8'))).toBe(true);
15211583
});
15221584

1585+
// Trigger: Items prepended at top AND removed from bottom in same data batch.
1586+
// Expected: Native side unaffected by bottom deletes since MVCP only looks at first visible view.
1587+
// TODO: detect and handle/ignore re-ordering comment at RCTScrollViewComponentView.mm:1110 explicitly unhandled.
15231588
test('maintainVisibleContentPosition handles prepend with delete from bottom', () => {
15241589
const root = Fantom.createRoot({
15251590
viewportWidth: 100,
@@ -1573,6 +1638,9 @@ test('maintainVisibleContentPosition handles prepend with delete from bottom', (
15731638
expect(logs.some(log => log.includes('item_5'))).toBe(true);
15741639
});
15751640

1641+
// Trigger: Large number of items (50+) inserted at beginning of data array.
1642+
// Expected: Anchor view may be recycled by FlatList's view pool. Tag comparison safeguard detects recycled view
1643+
// and aborts correction. Without this check, delta computed from wrong view.
15761644
test('maintainVisibleContentPosition handles large prepend (50+ items)', () => {
15771645
const root = Fantom.createRoot({
15781646
viewportWidth: 100,
@@ -1626,6 +1694,9 @@ test('maintainVisibleContentPosition handles large prepend (50+ items)', () => {
16261694
expect(logs.some(log => log.includes('item_5'))).toBe(true);
16271695
});
16281696

1697+
// Trigger: Very first prepend after initial list mount. Anchor state not yet initialized by prior MVCP cycle.
1698+
// Expected: On first mount, `_prepareForMaintainVisibleScrollPosition` initializes anchor state.
1699+
// First prepend should work correctly because initial mount establishes baseline.
16291700
test('maintainVisibleContentPosition handles first prepend after initial mount', () => {
16301701
const root = Fantom.createRoot({
16311702
viewportWidth: 100,
@@ -1673,6 +1744,10 @@ test('maintainVisibleContentPosition handles first prepend after initial mount',
16731744
expect(logs.some(log => log.includes('item_5'))).toBe(true);
16741745
});
16751746

1747+
// Trigger: Items have dynamic heights (images loading, variable text). Anchor's frame size may differ between
1748+
// pre-mount capture and post-layout measurement.
1749+
// Expected: Delta formula `newFrame - oldFrame` conflates position shift from prepended items and size change of
1750+
// anchor item itself. Frame-based approach inherently correct but first correction may be inaccurate.
16761751
test('maintainVisibleContentPosition handles variable-height items', () => {
16771752
const root = Fantom.createRoot({
16781753
viewportWidth: 100,
@@ -1755,6 +1830,9 @@ test('maintainVisibleContentPosition handles variable-height items', () => {
17551830
expect(logs.some(log => log.includes('item_6'))).toBe(true);
17561831
});
17571832

1833+
// Trigger: Items above anchor grow, pushing anchor off top of visible area. Culling removes off-screen views.
1834+
// Expected: On next mount cycle, `_recomputeFirstVisibleViewForMaintainVisibleContentPosition` finds new anchor.
1835+
// Tag comparison safeguard detects when anchor view was recycled (different tag) and aborts correction.
17581836
test('maintainVisibleContentPosition handles anchor culled (pushed off-screen)', () => {
17591837
const root = Fantom.createRoot({
17601838
viewportWidth: 100,
@@ -1808,6 +1886,8 @@ test('maintainVisibleContentPosition handles anchor culled (pushed off-screen)',
18081886
expect(logs.some(log => log.includes('item_13'))).toBe(true);
18091887
});
18101888

1889+
// Trigger: Inverted list with culling enabled, causing view recycling during prepends.
1890+
// Expected: Tag comparison safeguard must work correctly in inverted mode. Tag check always active.
18111891
test('maintainVisibleContentPosition with inverted + recycling', () => {
18121892
const root = Fantom.createRoot({
18131893
viewportWidth: 100,
@@ -1863,6 +1943,9 @@ test('maintainVisibleContentPosition with inverted + recycling', () => {
18631943
expect(logs.some(log => log.includes('item_5'))).toBe(true);
18641944
});
18651945

1946+
// Trigger: Many rapid state updates cause many re-renders in quick succession.
1947+
// Expected: If scroll events throttled, `pendingScrollUpdateCount` may not decrement promptly, blocking render
1948+
// window updates. Android scroll throttle fix ensures JS state current after MVCP adjustments.
18661949
test('maintainVisibleContentPosition handles rapid state updates', () => {
18671950
const root = Fantom.createRoot({
18681951
viewportWidth: 100,
@@ -1921,6 +2004,9 @@ test('maintainVisibleContentPosition handles rapid state updates', () => {
19212004
expect(logs.some(log => log.includes('item_8'))).toBe(true);
19222005
});
19232006

2007+
// Trigger: Programmatic scrollToOffset call while MVCP is active.
2008+
// Expected: Programmatic scrollToOffset during MVCP active can cause incorrect final position.
2009+
// MVCP delta is additive, adds to whatever current scroll position is. Known open issue.
19242010
test('maintainVisibleContentPosition with scrollToOffset (non-animated)', () => {
19252011
const root = Fantom.createRoot({
19262012
viewportWidth: 100,
@@ -1962,6 +2048,9 @@ test('maintainVisibleContentPosition with scrollToOffset (non-animated)', () =>
19622048
expect(logs.length).toBeGreaterThan(0);
19632049
});
19642050

2051+
// Trigger: Animated scrollToOffset call in progress when MVCP correction applied.
2052+
// Expected: Animated scrollToOffset interrupted by MVCP correction because setting contentOffset directly
2053+
// (iOS) or calling scrollToPreservingMomentum (Android) replaces any ongoing animation.
19652054
test('maintainVisibleContentPosition with scrollToOffset (animated)', () => {
19662055
const root = Fantom.createRoot({
19672056
viewportWidth: 100,
@@ -2003,6 +2092,8 @@ test('maintainVisibleContentPosition with scrollToOffset (animated)', () => {
20032092
expect(logs.length).toBeGreaterThan(0);
20042093
});
20052094

2095+
// Trigger: Item's content changes but rendered size stays the same.
2096+
// Expected: No frame change, no delta, no scroll correction. Anchor stays at same position.
20062097
test('maintainVisibleContentPosition handles content change with same size', () => {
20072098
const root = Fantom.createRoot({
20082099
viewportWidth: 100,

0 commit comments

Comments
 (0)