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.
@@ -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
- // The minimal gap-fill on top of @lynx-js/testing-environment's own PAPI
4
- // polyfill — same idea as the previous mithril-lynx's testing.js, scoped
5
- // down to only what apply-patch.js actually calls so far (no gestures/lists
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
- globalThis.onInjectMainThreadGlobals = (target: any) => {
15
- target.lynx.getEngine = target.lynx.getNative;
7
+ import { installTestingPolyfills } from "../src/testing.js";
16
8
 
17
- target.__AddEventListener = (node: any, name: string, handler: (...args: unknown[]) => unknown) => {
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;