@dxos/react-ui-feed 0.11.1
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 +105 -0
- package/dist/lib/chunk-src.mjs +2228 -0
- package/dist/lib/chunk-src.mjs.map +1 -0
- package/dist/lib/index.mjs +2 -0
- package/dist/lib/testing.mjs +1923 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/types/src/components/Block/HtmlBlock.d.ts +14 -0
- package/dist/types/src/components/Block/HtmlBlock.d.ts.map +1 -0
- package/dist/types/src/components/Block/MarkdownBlock.d.ts +24 -0
- package/dist/types/src/components/Block/MarkdownBlock.d.ts.map +1 -0
- package/dist/types/src/components/Block/block.stories.d.ts +21 -0
- package/dist/types/src/components/Block/block.stories.d.ts.map +1 -0
- package/dist/types/src/components/Block/construction.d.ts +36 -0
- package/dist/types/src/components/Block/construction.d.ts.map +1 -0
- package/dist/types/src/components/Block/extensions.d.ts +23 -0
- package/dist/types/src/components/Block/extensions.d.ts.map +1 -0
- package/dist/types/src/components/Block/highlight.d.ts +14 -0
- package/dist/types/src/components/Block/highlight.d.ts.map +1 -0
- package/dist/types/src/components/Block/index.d.ts +6 -0
- package/dist/types/src/components/Block/index.d.ts.map +1 -0
- package/dist/types/src/components/Block/selection-group.d.ts +21 -0
- package/dist/types/src/components/Block/selection-group.d.ts.map +1 -0
- package/dist/types/src/components/Block/widget-state.d.ts +33 -0
- package/dist/types/src/components/Block/widget-state.d.ts.map +1 -0
- package/dist/types/src/components/MessageList/MessageList.d.ts +129 -0
- package/dist/types/src/components/MessageList/MessageList.d.ts.map +1 -0
- package/dist/types/src/components/MessageList/MessageList.stories.d.ts +47 -0
- package/dist/types/src/components/MessageList/MessageList.stories.d.ts.map +1 -0
- package/dist/types/src/components/MessageList/index.d.ts +3 -0
- package/dist/types/src/components/MessageList/index.d.ts.map +1 -0
- package/dist/types/src/components/MessageList/position-log.d.ts +73 -0
- package/dist/types/src/components/MessageList/position-log.d.ts.map +1 -0
- package/dist/types/src/components/Minimap/Minimap.d.ts +28 -0
- package/dist/types/src/components/Minimap/Minimap.d.ts.map +1 -0
- package/dist/types/src/components/Minimap/Minimap.stories.d.ts +21 -0
- package/dist/types/src/components/Minimap/Minimap.stories.d.ts.map +1 -0
- package/dist/types/src/components/Minimap/index.d.ts +2 -0
- package/dist/types/src/components/Minimap/index.d.ts.map +1 -0
- package/dist/types/src/components/Outline/Outline.d.ts +46 -0
- package/dist/types/src/components/Outline/Outline.d.ts.map +1 -0
- package/dist/types/src/components/Outline/Outline.stories.d.ts +37 -0
- package/dist/types/src/components/Outline/Outline.stories.d.ts.map +1 -0
- package/dist/types/src/components/Outline/index.d.ts +2 -0
- package/dist/types/src/components/Outline/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +5 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/debug/Debug.d.ts +17 -0
- package/dist/types/src/debug/Debug.d.ts.map +1 -0
- package/dist/types/src/debug/Stats.d.ts +30 -0
- package/dist/types/src/debug/Stats.d.ts.map +1 -0
- package/dist/types/src/debug/debug-model.d.ts +32 -0
- package/dist/types/src/debug/debug-model.d.ts.map +1 -0
- package/dist/types/src/debug/debug-model.test.d.ts +2 -0
- package/dist/types/src/debug/debug-model.test.d.ts.map +1 -0
- package/dist/types/src/debug/frame-meter.d.ts +48 -0
- package/dist/types/src/debug/frame-meter.d.ts.map +1 -0
- package/dist/types/src/debug/frame-meter.test.d.ts +2 -0
- package/dist/types/src/debug/frame-meter.test.d.ts.map +1 -0
- package/dist/types/src/debug/index.d.ts +6 -0
- package/dist/types/src/debug/index.d.ts.map +1 -0
- package/dist/types/src/debug/use-debug.d.ts +19 -0
- package/dist/types/src/debug/use-debug.d.ts.map +1 -0
- package/dist/types/src/hooks/follow.d.ts +89 -0
- package/dist/types/src/hooks/follow.d.ts.map +1 -0
- package/dist/types/src/hooks/follow.stories.d.ts +25 -0
- package/dist/types/src/hooks/follow.stories.d.ts.map +1 -0
- package/dist/types/src/hooks/follow.test.d.ts +2 -0
- package/dist/types/src/hooks/follow.test.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +6 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useDecorations.d.ts +31 -0
- package/dist/types/src/hooks/useDecorations.d.ts.map +1 -0
- package/dist/types/src/hooks/useFeedNavigation.d.ts +44 -0
- package/dist/types/src/hooks/useFeedNavigation.d.ts.map +1 -0
- package/dist/types/src/hooks/useFollow.d.ts +46 -0
- package/dist/types/src/hooks/useFollow.d.ts.map +1 -0
- package/dist/types/src/hooks/useItemSelection.d.ts +34 -0
- package/dist/types/src/hooks/useItemSelection.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/model/feed-model.d.ts +67 -0
- package/dist/types/src/model/feed-model.d.ts.map +1 -0
- package/dist/types/src/model/feed-model.test.d.ts +2 -0
- package/dist/types/src/model/feed-model.test.d.ts.map +1 -0
- package/dist/types/src/model/feed.d.ts +66 -0
- package/dist/types/src/model/feed.d.ts.map +1 -0
- package/dist/types/src/model/feed.test.d.ts +2 -0
- package/dist/types/src/model/feed.test.d.ts.map +1 -0
- package/dist/types/src/model/index.d.ts +5 -0
- package/dist/types/src/model/index.d.ts.map +1 -0
- package/dist/types/src/model/list-model.d.ts +61 -0
- package/dist/types/src/model/list-model.d.ts.map +1 -0
- package/dist/types/src/model/list-model.test.d.ts +2 -0
- package/dist/types/src/model/list-model.test.d.ts.map +1 -0
- package/dist/types/src/model/useListModel.d.ts +22 -0
- package/dist/types/src/model/useListModel.d.ts.map +1 -0
- package/dist/types/src/stories/assistant.stories.d.ts +38 -0
- package/dist/types/src/stories/assistant.stories.d.ts.map +1 -0
- package/dist/types/src/stories/baseline.stories.d.ts +32 -0
- package/dist/types/src/stories/baseline.stories.d.ts.map +1 -0
- package/dist/types/src/stories/bridge.stories.d.ts +72 -0
- package/dist/types/src/stories/bridge.stories.d.ts.map +1 -0
- package/dist/types/src/stories/construction.stories.d.ts +17 -0
- package/dist/types/src/stories/construction.stories.d.ts.map +1 -0
- package/dist/types/src/stories/fill.stories.d.ts +51 -0
- package/dist/types/src/stories/fill.stories.d.ts.map +1 -0
- package/dist/types/src/stories/horizontal.stories.d.ts +17 -0
- package/dist/types/src/stories/horizontal.stories.d.ts.map +1 -0
- package/dist/types/src/stories/mount.stories.d.ts +19 -0
- package/dist/types/src/stories/mount.stories.d.ts.map +1 -0
- package/dist/types/src/stories/navigation.stories.d.ts +33 -0
- package/dist/types/src/stories/navigation.stories.d.ts.map +1 -0
- package/dist/types/src/stories/streaming.stories.d.ts +17 -0
- package/dist/types/src/stories/streaming.stories.d.ts.map +1 -0
- package/dist/types/src/stories/tail.stories.d.ts +47 -0
- package/dist/types/src/stories/tail.stories.d.ts.map +1 -0
- package/dist/types/src/stories/widget.stories.d.ts +16 -0
- package/dist/types/src/stories/widget.stories.d.ts.map +1 -0
- package/dist/types/src/testing/FeedStory.d.ts +30 -0
- package/dist/types/src/testing/FeedStory.d.ts.map +1 -0
- package/dist/types/src/testing/MessageWindow.d.ts +48 -0
- package/dist/types/src/testing/MessageWindow.d.ts.map +1 -0
- package/dist/types/src/testing/controls.d.ts +46 -0
- package/dist/types/src/testing/controls.d.ts.map +1 -0
- package/dist/types/src/testing/debug/FeedStats.d.ts +18 -0
- package/dist/types/src/testing/debug/FeedStats.d.ts.map +1 -0
- package/dist/types/src/testing/debug/index.d.ts +4 -0
- package/dist/types/src/testing/debug/index.d.ts.map +1 -0
- package/dist/types/src/testing/debug/sweep.d.ts +18 -0
- package/dist/types/src/testing/debug/sweep.d.ts.map +1 -0
- package/dist/types/src/testing/debug/use-feed-debug.d.ts +33 -0
- package/dist/types/src/testing/debug/use-feed-debug.d.ts.map +1 -0
- package/dist/types/src/testing/generator.d.ts +16 -0
- package/dist/types/src/testing/generator.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +10 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/scenarios.d.ts +58 -0
- package/dist/types/src/testing/scenarios.d.ts.map +1 -0
- package/dist/types/src/testing/stream.d.ts +22 -0
- package/dist/types/src/testing/stream.d.ts.map +1 -0
- package/dist/types/src/testing/turn.d.ts +21 -0
- package/dist/types/src/testing/turn.d.ts.map +1 -0
- package/dist/types/src/testing/widgets.d.ts +3 -0
- package/dist/types/src/testing/widgets.d.ts.map +1 -0
- package/dist/types/src/virtualizer/Window.d.ts +137 -0
- package/dist/types/src/virtualizer/Window.d.ts.map +1 -0
- package/dist/types/src/virtualizer/Window.stories.d.ts +112 -0
- package/dist/types/src/virtualizer/Window.stories.d.ts.map +1 -0
- package/dist/types/src/virtualizer/index.d.ts +3 -0
- package/dist/types/src/virtualizer/index.d.ts.map +1 -0
- package/dist/types/src/virtualizer/placement.d.ts +188 -0
- package/dist/types/src/virtualizer/placement.d.ts.map +1 -0
- package/dist/types/src/virtualizer/placement.test.d.ts +2 -0
- package/dist/types/src/virtualizer/placement.test.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +72 -0
- package/src/components/Block/HtmlBlock.tsx +26 -0
- package/src/components/Block/MarkdownBlock.tsx +157 -0
- package/src/components/Block/block.stories.tsx +140 -0
- package/src/components/Block/construction.ts +77 -0
- package/src/components/Block/extensions.ts +95 -0
- package/src/components/Block/highlight.ts +53 -0
- package/src/components/Block/index.ts +10 -0
- package/src/components/Block/selection-group.ts +53 -0
- package/src/components/Block/widget-state.tsx +75 -0
- package/src/components/MessageList/MessageList.stories.tsx +111 -0
- package/src/components/MessageList/MessageList.tsx +647 -0
- package/src/components/MessageList/index.ts +6 -0
- package/src/components/MessageList/position-log.ts +167 -0
- package/src/components/Minimap/Minimap.stories.tsx +129 -0
- package/src/components/Minimap/Minimap.tsx +84 -0
- package/src/components/Minimap/index.ts +5 -0
- package/src/components/Outline/Outline.stories.tsx +196 -0
- package/src/components/Outline/Outline.tsx +383 -0
- package/src/components/Outline/index.ts +5 -0
- package/src/components/index.ts +8 -0
- package/src/debug/Debug.tsx +90 -0
- package/src/debug/Stats.tsx +57 -0
- package/src/debug/debug-model.test.ts +36 -0
- package/src/debug/debug-model.ts +66 -0
- package/src/debug/frame-meter.test.ts +49 -0
- package/src/debug/frame-meter.ts +188 -0
- package/src/debug/index.ts +9 -0
- package/src/debug/use-debug.tsx +41 -0
- package/src/hooks/follow.stories.tsx +200 -0
- package/src/hooks/follow.test.ts +105 -0
- package/src/hooks/follow.ts +241 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useDecorations.tsx +54 -0
- package/src/hooks/useFeedNavigation.ts +90 -0
- package/src/hooks/useFollow.ts +258 -0
- package/src/hooks/useItemSelection.tsx +53 -0
- package/src/index.ts +7 -0
- package/src/model/feed-model.test.ts +84 -0
- package/src/model/feed-model.ts +214 -0
- package/src/model/feed.test.ts +65 -0
- package/src/model/feed.ts +157 -0
- package/src/model/index.ts +8 -0
- package/src/model/list-model.test.ts +71 -0
- package/src/model/list-model.ts +152 -0
- package/src/model/useListModel.ts +53 -0
- package/src/stories/assistant.stories.tsx +425 -0
- package/src/stories/baseline.stories.tsx +78 -0
- package/src/stories/bridge.stories.tsx +376 -0
- package/src/stories/construction.stories.tsx +147 -0
- package/src/stories/fill.stories.tsx +271 -0
- package/src/stories/horizontal.stories.tsx +129 -0
- package/src/stories/mount.stories.tsx +233 -0
- package/src/stories/navigation.stories.tsx +145 -0
- package/src/stories/streaming.stories.tsx +132 -0
- package/src/stories/tail.stories.tsx +149 -0
- package/src/stories/widget.stories.tsx +107 -0
- package/src/testing/FeedStory.tsx +452 -0
- package/src/testing/MessageWindow.tsx +139 -0
- package/src/testing/controls.tsx +96 -0
- package/src/testing/debug/FeedStats.tsx +182 -0
- package/src/testing/debug/index.ts +16 -0
- package/src/testing/debug/sweep.ts +85 -0
- package/src/testing/debug/use-feed-debug.ts +91 -0
- package/src/testing/generator.ts +90 -0
- package/src/testing/index.ts +13 -0
- package/src/testing/scenarios.tsx +552 -0
- package/src/testing/stream.ts +70 -0
- package/src/testing/turn.ts +91 -0
- package/src/testing/widgets.tsx +183 -0
- package/src/virtualizer/Window.stories.tsx +683 -0
- package/src/virtualizer/Window.tsx +435 -0
- package/src/virtualizer/index.ts +6 -0
- package/src/virtualizer/placement.test.ts +312 -0
- package/src/virtualizer/placement.ts +403 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { Placement } from './placement';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The placement module, with nothing rendered.
|
|
11
|
+
*
|
|
12
|
+
* These are the properties the DOM shape depends on, checked where they can be checked exactly:
|
|
13
|
+
* against arithmetic rather than against a browser. `placement/*` then checks that the DOM binding
|
|
14
|
+
* honours them, and `baseline/*` that a real item does not break them — three layers, each able to
|
|
15
|
+
* fail on its own, which is what says where a defect is (§1).
|
|
16
|
+
*
|
|
17
|
+
* Nothing here names an axis. If a test reads `top` or `height`, the axis has leaked in (§9).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const VIEWPORT = 800;
|
|
21
|
+
|
|
22
|
+
type Options = {
|
|
23
|
+
count?: number;
|
|
24
|
+
extent?: (index: number) => number;
|
|
25
|
+
exact?: boolean;
|
|
26
|
+
overscan?: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A model whose ids can be shifted, because that is what a prepend does.
|
|
31
|
+
*
|
|
32
|
+
* `getId` is read live rather than captured: the module's contract is that it reflects the model as
|
|
33
|
+
* it now stands, so a fixture that maps index to id statically cannot express a prepend at all — it
|
|
34
|
+
* describes a list where inserting at the front renames every row instead of moving it.
|
|
35
|
+
*/
|
|
36
|
+
const create = ({ count = 500, extent = () => 100, exact, overscan = 2 }: Options = {}) => {
|
|
37
|
+
const ids = Array.from({ length: count }, (_, index) => `row-${index}`);
|
|
38
|
+
const placement = new Placement({
|
|
39
|
+
count,
|
|
40
|
+
getId: (index) => ids[index],
|
|
41
|
+
extents: { of: extent, exact },
|
|
42
|
+
viewport: VIEWPORT,
|
|
43
|
+
overscan,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
placement,
|
|
48
|
+
/** Inserts `n` rows before everything, as a rewind or a page of history would. */
|
|
49
|
+
prepend: (n: number) => {
|
|
50
|
+
ids.unshift(...Array.from({ length: n }, (_, index) => `earlier-${index}`));
|
|
51
|
+
placement.setCount(ids.length, { prepended: n });
|
|
52
|
+
},
|
|
53
|
+
append: (n: number) => {
|
|
54
|
+
ids.push(...Array.from({ length: n }, (_, index) => `later-${index}`));
|
|
55
|
+
placement.setCount(ids.length);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
describe('placement', () => {
|
|
61
|
+
test('positions are exact when the host says the extents are', () => {
|
|
62
|
+
const { placement } = create({ exact: true, extent: () => 100 });
|
|
63
|
+
|
|
64
|
+
expect(placement.positionOf(0)).to.eq(0);
|
|
65
|
+
expect(placement.positionOf(7)).to.eq(700);
|
|
66
|
+
// Measuring is a no-op under `exact`: the host promised, so there is nothing to revise (§8).
|
|
67
|
+
placement.measure('row-3', 999);
|
|
68
|
+
expect(placement.positionOf(7)).to.eq(700);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('the window covers the viewport, and no more than overscan beyond it', () => {
|
|
72
|
+
const { placement } = create({ extent: () => 100, overscan: 2 });
|
|
73
|
+
placement.scrollTo(1_000);
|
|
74
|
+
|
|
75
|
+
const { first, last, visible } = placement.layout();
|
|
76
|
+
expect(first).to.eq(8);
|
|
77
|
+
expect(last).to.eq(19);
|
|
78
|
+
// What the reader can see, as against what is mounted: a readout that named a mounted row would
|
|
79
|
+
// be describing the overscan.
|
|
80
|
+
expect(visible).to.deep.eq({ first: 10, last: 17 });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('the sizer spans the whole model', () => {
|
|
84
|
+
const { placement } = create({ count: 10, extent: () => 100, exact: true });
|
|
85
|
+
|
|
86
|
+
expect(placement.layout().sizerExtent).to.eq(1_000);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
//
|
|
90
|
+
// The properties the design turns on.
|
|
91
|
+
//
|
|
92
|
+
|
|
93
|
+
test('measuring a row after the anchor does not move the anchor', () => {
|
|
94
|
+
const { placement } = create();
|
|
95
|
+
placement.scrollTo(2_000);
|
|
96
|
+
const anchor = placement.anchor;
|
|
97
|
+
const before = placement.positionOf(anchor.index);
|
|
98
|
+
|
|
99
|
+
placement.measure(`row-${anchor.index + 3}`, 400);
|
|
100
|
+
|
|
101
|
+
expect(placement.positionOf(anchor.index)).to.eq(before);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('measuring a row before the anchor moves that row, and still not the anchor', () => {
|
|
105
|
+
const { placement } = create();
|
|
106
|
+
placement.scrollTo(2_000);
|
|
107
|
+
const anchor = placement.anchor;
|
|
108
|
+
const earlier = anchor.index - 1;
|
|
109
|
+
const anchorBefore = placement.positionOf(anchor.index);
|
|
110
|
+
const earlierBefore = placement.positionOf(earlier);
|
|
111
|
+
|
|
112
|
+
placement.measure(`row-${earlier}`, 300);
|
|
113
|
+
|
|
114
|
+
// The anchor is what is being held; the row that grew is what moved.
|
|
115
|
+
expect(placement.positionOf(anchor.index)).to.eq(anchorBefore);
|
|
116
|
+
expect(placement.positionOf(earlier)).to.eq(earlierBefore - 200);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('prepending moves nothing at or after the anchor', () => {
|
|
120
|
+
const { placement, prepend } = create({ count: 100 });
|
|
121
|
+
placement.scrollTo(2_000);
|
|
122
|
+
const anchor = placement.anchor;
|
|
123
|
+
const positions = [0, 1, 2, 3].map((step) => placement.positionOf(anchor.index + step));
|
|
124
|
+
|
|
125
|
+
// Ten rows arrive before the reader: the model is longer and every index has shifted.
|
|
126
|
+
prepend(10);
|
|
127
|
+
|
|
128
|
+
expect(placement.anchor.index).to.eq(anchor.index + 10);
|
|
129
|
+
expect([0, 1, 2, 3].map((step) => placement.positionOf(placement.anchor.index + step))).to.deep.eq(positions);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('appending moves nothing at all', () => {
|
|
133
|
+
const { placement, append } = create({ count: 100 });
|
|
134
|
+
placement.scrollTo(2_000);
|
|
135
|
+
const before = placement.layout();
|
|
136
|
+
|
|
137
|
+
append(10);
|
|
138
|
+
|
|
139
|
+
const after = placement.layout();
|
|
140
|
+
expect(after.offset).to.eq(before.offset);
|
|
141
|
+
expect(after.first).to.eq(before.first);
|
|
142
|
+
// Only the region the thumb describes grew.
|
|
143
|
+
expect(after.sizerExtent).to.be.greaterThan(before.sizerExtent);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('re-anchoring preserves every position', () => {
|
|
147
|
+
const { placement } = create({ count: 500 });
|
|
148
|
+
const sampled = () => [10, 40, 80].map((index) => placement.positionOf(index));
|
|
149
|
+
|
|
150
|
+
placement.scrollTo(1_000);
|
|
151
|
+
const before = sampled();
|
|
152
|
+
// Far enough that the anchor cannot still be in the window.
|
|
153
|
+
placement.scrollTo(6_000);
|
|
154
|
+
placement.scrollTo(1_000);
|
|
155
|
+
|
|
156
|
+
expect(sampled()).to.deep.eq(before);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('extents are keyed by identity, so a prepend cannot lose them', () => {
|
|
160
|
+
const { placement, prepend } = create({ count: 100 });
|
|
161
|
+
placement.measure('row-50', 250);
|
|
162
|
+
const before = placement.extentOf(50);
|
|
163
|
+
|
|
164
|
+
prepend(5);
|
|
165
|
+
|
|
166
|
+
// `row-50` is now at index 55; the extent followed the id rather than the position.
|
|
167
|
+
expect(before).to.eq(250);
|
|
168
|
+
expect(placement.extentOf(55)).to.eq(250);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
//
|
|
172
|
+
// The edges, where estimate meets ground truth.
|
|
173
|
+
//
|
|
174
|
+
|
|
175
|
+
test('no drift is reported when the estimates were right', () => {
|
|
176
|
+
const { placement } = create({ count: 20, extent: () => 100, exact: true });
|
|
177
|
+
placement.jumpTo(0);
|
|
178
|
+
|
|
179
|
+
expect(placement.drift()).to.be.undefined;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('reaching the start reports how wrong the estimate above was', () => {
|
|
183
|
+
const { placement } = create({ count: 200, extent: () => 100 });
|
|
184
|
+
// Jumped from estimates, then the rows turn out to be half what was assumed.
|
|
185
|
+
placement.jumpTo(50);
|
|
186
|
+
for (let index = 0; index < 60; index++) {
|
|
187
|
+
placement.measure(`row-${index}`, 50);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
placement.scrollTo(0);
|
|
191
|
+
|
|
192
|
+
const drift = placement.drift();
|
|
193
|
+
expect(drift?.edge).to.eq('start');
|
|
194
|
+
// Fifty rows assumed at 100 and measured at 50: the region above was 2,500 shorter than assumed.
|
|
195
|
+
expect(drift?.delta).to.eq(2_500);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
//
|
|
199
|
+
// Following the end, which is where a chat lives.
|
|
200
|
+
//
|
|
201
|
+
|
|
202
|
+
test('the end is where the last row rests against the end of the viewport', () => {
|
|
203
|
+
const { placement } = create({ count: 20, extent: () => 100, exact: true });
|
|
204
|
+
|
|
205
|
+
// Twenty rows of 100 is 2,000 of content, and the last of them rests when the final 800 is on
|
|
206
|
+
// screen — not when the document's end is, which is a different number as soon as anything is
|
|
207
|
+
// reserved past it.
|
|
208
|
+
expect(placement.endOffset()).to.eq(1_200);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('reserved space is somewhere the reader may go, not where the end is', () => {
|
|
212
|
+
const { placement } = create({ count: 20, extent: () => 100, exact: true });
|
|
213
|
+
placement.setReserve(700);
|
|
214
|
+
|
|
215
|
+
expect(placement.endOffset()).to.eq(1_200);
|
|
216
|
+
expect(placement.layout().sizerExtent).to.eq(2_700);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('following the end settles once the rows it reveals have been measured', () => {
|
|
220
|
+
const { placement } = create({ count: 200, extent: () => 100 });
|
|
221
|
+
const offsets: number[] = [];
|
|
222
|
+
|
|
223
|
+
// What a sticky feed does on every commit: go to the end, measure whatever that mounted, ask
|
|
224
|
+
// again. The rows are half again as tall as assumed, so the first answer cannot be right.
|
|
225
|
+
for (let round = 0; round < 6; round++) {
|
|
226
|
+
placement.scrollTo(placement.endOffset());
|
|
227
|
+
const { first, last } = placement.layout();
|
|
228
|
+
for (let index = first; index <= last; index++) {
|
|
229
|
+
placement.measure(`row-${index}`, 150);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
offsets.push(placement.endOffset());
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Settled, and settled *quickly*: a round here is a React commit, and fifty of them is the
|
|
236
|
+
// update limit — which is the wall `bridge/Tail` was hitting rather than converging.
|
|
237
|
+
expect(offsets.at(-1)).to.eq(offsets.at(-2));
|
|
238
|
+
expect(offsets.indexOf(offsets.at(-1)!)).to.be.lessThan(3);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('following the end is not disturbed by rows the reader has already passed', () => {
|
|
242
|
+
const { placement } = create({ count: 200, extent: () => 100 });
|
|
243
|
+
placement.scrollTo(placement.endOffset());
|
|
244
|
+
const { first } = placement.layout();
|
|
245
|
+
const before = { end: placement.endOffset(), first: placement.positionOf(first) };
|
|
246
|
+
|
|
247
|
+
// A row far above the window turns out to be twice its estimate, as history is measured on the
|
|
248
|
+
// way through. Summing the model from index 0 to find the end would carry that 100 into the
|
|
249
|
+
// tail's position and move what the reader is looking at, for a row they cannot see.
|
|
250
|
+
placement.measure('row-3', 200);
|
|
251
|
+
|
|
252
|
+
expect({ end: placement.endOffset(), first: placement.positionOf(first) }).to.deep.eq(before);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('replacing the model keeps measurement and its lookup on the same row', () => {
|
|
256
|
+
const ids = ['a', 'b', 'c'];
|
|
257
|
+
const placement = new Placement({
|
|
258
|
+
count: 3,
|
|
259
|
+
getId: (index) => ids[index],
|
|
260
|
+
extents: { of: () => 100 },
|
|
261
|
+
viewport: VIEWPORT,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// The host appends, which in React means a *new* closure over a *new* list — the same shape as
|
|
265
|
+
// `messages.map` giving a fresh `getId` on every render.
|
|
266
|
+
const grown = ['a', 'b', 'c', 'd'];
|
|
267
|
+
placement.setGetId((index) => grown[index]);
|
|
268
|
+
placement.setCount(4);
|
|
269
|
+
placement.measure('d', 250);
|
|
270
|
+
|
|
271
|
+
// Stored against the id the binding read off the row, and read back for the same row. Held to
|
|
272
|
+
// the stale closure, index 3 resolves to `undefined` and the measurement is never found again —
|
|
273
|
+
// the binding re-measures it every commit, which is a render loop, not a size error.
|
|
274
|
+
expect(placement.extentOf(3)).to.eq(250);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('rebasing the start puts row 0 at zero and moves the view not at all', () => {
|
|
278
|
+
const { placement } = create({ count: 200, extent: () => 100 });
|
|
279
|
+
// Opened at the tail, then the rows above measure half again taller than assumed: row 0 is
|
|
280
|
+
// pushed negative, where no scroll can reach it.
|
|
281
|
+
placement.jumpTo(199, 'end');
|
|
282
|
+
const { first, last } = placement.layout();
|
|
283
|
+
for (let index = first; index <= last; index++) {
|
|
284
|
+
placement.measure(`row-${index}`, 150);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const anchorBefore = placement.anchor.index;
|
|
288
|
+
const onScreen = placement.positionOf(anchorBefore) - placement.scroll;
|
|
289
|
+
|
|
290
|
+
const shift = placement.rebaseStart();
|
|
291
|
+
|
|
292
|
+
expect(placement.positionOf(0)).to.eq(0);
|
|
293
|
+
// What the reader sees is a row's position relative to the scroll, and it has not moved.
|
|
294
|
+
expect(placement.positionOf(anchorBefore) - placement.scroll).to.eq(onScreen);
|
|
295
|
+
expect(shift).to.not.eq(0);
|
|
296
|
+
// Repaying twice is a no-op: the fact is now true.
|
|
297
|
+
expect(placement.rebaseStart()).to.eq(0);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('an empty model has no window and no drift', () => {
|
|
301
|
+
const { placement } = create({ count: 0 });
|
|
302
|
+
|
|
303
|
+
expect(placement.layout()).to.deep.eq({
|
|
304
|
+
first: 0,
|
|
305
|
+
last: -1,
|
|
306
|
+
visible: { first: 0, last: -1 },
|
|
307
|
+
offset: 0,
|
|
308
|
+
sizerExtent: 0,
|
|
309
|
+
});
|
|
310
|
+
expect(placement.drift()).to.be.undefined;
|
|
311
|
+
});
|
|
312
|
+
});
|