mithril-lynx 2.0.2 → 2.6.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/.omo/run-continuation/ses_f48265d07ffenKG0GAz9ZRFvY6.json +10 -0
- package/package.json +17 -1
- package/src/apply-patch.js +268 -16
- package/src/backends/virtual-backend.js +28 -0
- package/src/fake-dom.js +99 -7
- package/src/list-cell.d.ts +20 -0
- package/src/list-cell.js +73 -0
- package/src/list-support.d.ts +13 -0
- package/src/list-support.js +171 -0
- package/src/mount-redraw.d.ts +13 -0
- package/src/mount-redraw.js +7 -0
- package/src/patch-protocol.js +62 -0
- package/src/testing.d.ts +16 -0
- package/src/testing.js +48 -0
- package/test/gesture.test.ts +175 -0
- package/test/list.test.ts +133 -0
- package/test/setup.ts +5 -21
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, expect, it } from "@rstest/core";
|
|
2
|
+
import m from "mithril";
|
|
3
|
+
import renderFactory from "mithril-runtime/render/render.js";
|
|
4
|
+
import { createPatchApplier } from "../src/apply-patch.js";
|
|
5
|
+
import { createVirtualBackend } from "../src/backends/virtual-backend.js";
|
|
6
|
+
import { createLynxDocument } from "../src/fake-dom.js";
|
|
7
|
+
import { renderListCell } from "../src/list-cell.js";
|
|
8
|
+
import { Op } from "../src/patch-protocol.js";
|
|
9
|
+
|
|
10
|
+
// Op.CreateList end-to-end. A cell's own content is computed on the
|
|
11
|
+
// BACKGROUND thread (list-cell.js's renderListCell(), through the app's own
|
|
12
|
+
// document/render — no separate render pipeline) and only replayed on the
|
|
13
|
+
// main thread (list-support.js) — see
|
|
14
|
+
// docs/native-papi/papi-06-virtualized-lists.md in mithril-lynx-ui. This
|
|
15
|
+
// test drives both halves directly: a background document renders each item
|
|
16
|
+
// into cells, then a main-thread applier replays Op.CreateList/
|
|
17
|
+
// Op.SetListItems with those cells, exactly like mithril-lynx-ui's own List
|
|
18
|
+
// component does end to end.
|
|
19
|
+
|
|
20
|
+
function requestCell(listHandle: any, index: number, opId = 1) {
|
|
21
|
+
const listId = __GetElementUniqueID(listHandle);
|
|
22
|
+
return listHandle.componentAtIndex(listHandle, listId, index, opId);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function releaseCell(listHandle: any, sign: unknown) {
|
|
26
|
+
const listId = __GetElementUniqueID(listHandle);
|
|
27
|
+
listHandle.enqueueComponent(listHandle, listId, sign);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function textOf(node: any): string {
|
|
31
|
+
if (node == null) return "";
|
|
32
|
+
let out = "";
|
|
33
|
+
for (let child = node.firstChild; child != null; child = child.nextSibling) {
|
|
34
|
+
out += child.nodeType === 3 ? child.nodeValue : textOf(child);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Stands in for mithril-lynx-ui's <List>: one persistent background
|
|
40
|
+
* document + render instance, reused across calls — see list-cell.js's own
|
|
41
|
+
* header for why callers keep one of these per list, not one per cell. */
|
|
42
|
+
function makeCellSource(renderItem: (item: any, index: number) => unknown) {
|
|
43
|
+
const document = createLynxDocument(createVirtualBackend());
|
|
44
|
+
const render = renderFactory();
|
|
45
|
+
return {
|
|
46
|
+
document,
|
|
47
|
+
buildCells: (items: unknown[]) => items.map((item, index) => renderListCell(document, render, () => {}, renderItem, item, index)),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function setupList() {
|
|
52
|
+
lynxTestingEnv.switchToMainThread();
|
|
53
|
+
const pageId = __GetElementUniqueID(__CreatePage());
|
|
54
|
+
const applier = createPatchApplier(pageId);
|
|
55
|
+
applier.registerPageRoot(__CreateView(pageId));
|
|
56
|
+
applier.applyPatch([Op.CreateList, 1, "vertical", "single", 1]);
|
|
57
|
+
const listHandle = applier.getHandle(1) as any;
|
|
58
|
+
return { applier, listHandle };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function setCells(applier: ReturnType<typeof createPatchApplier>, cells: unknown[]) {
|
|
62
|
+
applier.applyPatch([Op.SetListItems, 1, JSON.stringify(cells)]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("Op.CreateList (native virtualized list support)", () => {
|
|
66
|
+
it("renders real content per cell from ops the background thread already computed", () => {
|
|
67
|
+
const { buildCells } = makeCellSource((item: string, index: number) => m("text", {}, `${index}:${item}`));
|
|
68
|
+
const { applier, listHandle } = setupList();
|
|
69
|
+
setCells(applier, buildCells(["a", "b", "c"]));
|
|
70
|
+
|
|
71
|
+
requestCell(listHandle, 0);
|
|
72
|
+
requestCell(listHandle, 1);
|
|
73
|
+
const cellWrapper = listHandle.children[1]; // second appended cell -> index 1
|
|
74
|
+
expect(textOf(cellWrapper)).toBe("1:b");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("recycles a cell for a different index, and its content updates to match", () => {
|
|
78
|
+
const { buildCells } = makeCellSource((item: string, index: number) => m("text", {}, `${index}:${item}`));
|
|
79
|
+
const { applier, listHandle } = setupList();
|
|
80
|
+
setCells(applier, buildCells(["a", "b", "c", "d"]));
|
|
81
|
+
|
|
82
|
+
const signA = requestCell(listHandle, 0);
|
|
83
|
+
const wrapperA = listHandle.children[0];
|
|
84
|
+
expect(textOf(wrapperA)).toBe("0:a");
|
|
85
|
+
|
|
86
|
+
releaseCell(listHandle, signA);
|
|
87
|
+
const signD = requestCell(listHandle, 3);
|
|
88
|
+
|
|
89
|
+
// Recycled: the SAME sign/wrapper comes back, now showing the new index's content.
|
|
90
|
+
expect(signD).toBe(signA);
|
|
91
|
+
expect(textOf(wrapperA)).toBe("3:d");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("SetListItems with a larger array requests the newly available indices without error", () => {
|
|
95
|
+
const { buildCells } = makeCellSource((item: string, index: number) => m("text", {}, `${index}:${item}`));
|
|
96
|
+
const { applier, listHandle } = setupList();
|
|
97
|
+
|
|
98
|
+
setCells(applier, buildCells(["a", "b"]));
|
|
99
|
+
expect(() => requestCell(listHandle, 1)).not.toThrow();
|
|
100
|
+
expect(() => requestCell(listHandle, 2)).toThrow(/cellIndex 2 out of range/);
|
|
101
|
+
|
|
102
|
+
setCells(applier, buildCells(["a", "b", "c"]));
|
|
103
|
+
expect(() => requestCell(listHandle, 2)).not.toThrow();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("SetListItems re-flushes an already-attached cell's content in place", () => {
|
|
107
|
+
const { buildCells } = makeCellSource((item: string, index: number) => m("text", {}, `${index}:${item}`));
|
|
108
|
+
const { applier, listHandle } = setupList();
|
|
109
|
+
setCells(applier, buildCells(["a", "b"]));
|
|
110
|
+
|
|
111
|
+
requestCell(listHandle, 0);
|
|
112
|
+
const wrapper = listHandle.children[0];
|
|
113
|
+
expect(textOf(wrapper)).toBe("0:a");
|
|
114
|
+
|
|
115
|
+
setCells(applier, buildCells(["z", "b"])); // same count, index 0's content changed
|
|
116
|
+
expect(textOf(wrapper)).toBe("0:z");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("a tap inside a cell dispatches through the background thread's own fake-dom node", () => {
|
|
120
|
+
const { document, buildCells } = makeCellSource(() =>
|
|
121
|
+
m("text", { ontap: () => { taps += 1; } }, "tap me"),
|
|
122
|
+
);
|
|
123
|
+
let taps = 0;
|
|
124
|
+
const { applier, listHandle } = setupList();
|
|
125
|
+
const cells = buildCells([{}]);
|
|
126
|
+
setCells(applier, cells);
|
|
127
|
+
requestCell(listHandle, 0);
|
|
128
|
+
|
|
129
|
+
const node = document.getNodeById((cells[0] as any).rootChildIds[0]);
|
|
130
|
+
node!.dispatchEvent({ type: "tap", currentTarget: node, preventDefault() {}, stopPropagation() {} });
|
|
131
|
+
expect(taps).toBe(1);
|
|
132
|
+
});
|
|
133
|
+
});
|
package/test/setup.ts
CHANGED
|
@@ -1,25 +1,9 @@
|
|
|
1
1
|
// test/setup.ts
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// yet, see the plan's non-goals). `@lynx-js/testing-environment` already
|
|
7
|
-
// implements __CreateView/__CreateText/__CreateElement/__CreateRawText/
|
|
8
|
-
// __AppendElement/__InsertElementBefore/__RemoveElement/__SetAttribute/
|
|
9
|
-
// __SetClasses/__AddInlineStyle/__FlushElementTree/__GetElementUniqueID —
|
|
10
|
-
// the one real gap is __AddEventListener (the testing environment only
|
|
11
|
-
// implements the string/worklet-event __AddEvent family that ReactLynx
|
|
12
|
-
// uses; mithril-lynx binds real JS function listeners directly).
|
|
3
|
+
// This package's own tests use the exact same polyfill it now publishes
|
|
4
|
+
// for everyone else (src/testing.js's installTestingPolyfills) — see that
|
|
5
|
+
// file's header for what it covers and why.
|
|
13
6
|
|
|
14
|
-
|
|
15
|
-
target.lynx.getEngine = target.lynx.getNative;
|
|
7
|
+
import { installTestingPolyfills } from "../src/testing.js";
|
|
16
8
|
|
|
17
|
-
|
|
18
|
-
node.__vanillaListeners ??= {};
|
|
19
|
-
(node.__vanillaListeners[name] ??= new Set()).add(handler);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
target.__RemoveEventListener = (node: any, name: string, handler: unknown) => {
|
|
23
|
-
node.__vanillaListeners?.[name]?.delete(handler);
|
|
24
|
-
};
|
|
25
|
-
};
|
|
9
|
+
globalThis.onInjectMainThreadGlobals = installTestingPolyfills;
|