mithril-lynx 2.5.0 → 2.6.2
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_f2f02336cffeO0ssvuxqZ05iZq.json +10 -0
- package/.omo/run-continuation/ses_f2fd5cb5fffeN1tjdUyPqdKG6B.json +10 -0
- package/LICENSE.txt +21 -0
- package/PLAN_REVIEW.md +134 -0
- package/README.md +2 -1
- package/REQUEST.md +5 -2
- package/ROUTE.md +1 -1
- package/informe-contrato-mithril-lynx.md +206 -0
- package/package.json +5 -1
- package/prompts-analisis-contrato-mithril-lynx.md +141 -0
- package/src/apply-patch.js +87 -27
- package/src/backends/virtual-backend.js +18 -4
- package/src/background.d.ts +14 -1
- package/src/channel.js +10 -2
- package/src/fake-dom.js +13 -10
- package/src/list-cell.d.ts +20 -0
- package/src/list-cell.js +73 -0
- package/src/list-support.d.ts +12 -9
- package/src/list-support.js +144 -114
- package/src/main-thread.js +15 -2
- package/src/mount-redraw.d.ts +10 -0
- package/src/mount-redraw.js +55 -2
- package/src/patch-protocol.js +61 -6
- package/src/request.d.ts +2 -0
- package/src/request.js +21 -3
- package/src/route.d.ts +22 -3
- package/src/route.js +25 -6
- package/src/testing.js +24 -11
- package/test/list.test.ts +86 -40
- package/test/patch-protocol.test.ts +41 -0
- package/test/remove-event.test.ts +26 -0
- package/test/route-hot-reload.test.ts +2 -1
- package/test/route.test.ts +5 -2
- package/test/setup.ts +10 -0
package/src/route.js
CHANGED
|
@@ -116,15 +116,32 @@ export function createRoute() {
|
|
|
116
116
|
if (!compiled.some((entry) => entry.check(defaultData))) {
|
|
117
117
|
throw new ReferenceError("Default route doesn't match any known routes.");
|
|
118
118
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
if (!ready) {
|
|
120
|
+
history = [defaultRoute];
|
|
121
|
+
historyIndex = 0;
|
|
122
|
+
ready = true;
|
|
123
|
+
resolveRoute(defaultRoute, null);
|
|
124
|
+
} else {
|
|
125
|
+
// Re-registration (e.g. HMR of the route module): keep the history
|
|
126
|
+
// stack and re-resolve the CURRENT path against the new table —
|
|
127
|
+
// the same thing the documented HMR pattern does by hand with
|
|
128
|
+
// route.set(route.get(), null, {replace: true}). Resolving
|
|
129
|
+
// defaultRoute here instead would silently jump the screen back to
|
|
130
|
+
// the initial route and discard the user's back/forward stack.
|
|
131
|
+
resolveRoute(currentPath != null ? currentPath : defaultRoute, null);
|
|
132
|
+
}
|
|
123
133
|
}
|
|
124
134
|
|
|
125
135
|
route.SKIP = {};
|
|
126
136
|
|
|
127
137
|
route.set = function (path, data, options) {
|
|
138
|
+
if (!ready) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
"[mithril-lynx] route.set() called before route(defaultRoute, routes) — " +
|
|
141
|
+
"the route table does not exist yet, so this navigation would be " +
|
|
142
|
+
"silently dropped. Call route() first.",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
128
145
|
if (lastUpdate != null) {
|
|
129
146
|
options = options || {};
|
|
130
147
|
options.replace = true;
|
|
@@ -161,14 +178,16 @@ export function createRoute() {
|
|
|
161
178
|
* is F4/F5 of the plan, not done yet.
|
|
162
179
|
*/
|
|
163
180
|
route.back = function () {
|
|
164
|
-
if (historyIndex <= 0) return;
|
|
181
|
+
if (historyIndex <= 0) return false;
|
|
165
182
|
historyIndex--;
|
|
166
183
|
resolveRoute(history[historyIndex], null);
|
|
184
|
+
return true;
|
|
167
185
|
};
|
|
168
186
|
route.forward = function () {
|
|
169
|
-
if (historyIndex >= history.length - 1) return;
|
|
187
|
+
if (historyIndex >= history.length - 1) return false;
|
|
170
188
|
historyIndex++;
|
|
171
189
|
resolveRoute(history[historyIndex], null);
|
|
190
|
+
return true;
|
|
172
191
|
};
|
|
173
192
|
|
|
174
193
|
// Lynx has no `<a>`/`onclick` — this renders a tap-driven element
|
package/src/testing.js
CHANGED
|
@@ -24,25 +24,38 @@ export { createPatchApplier } from "./apply-patch.js";
|
|
|
24
24
|
/**
|
|
25
25
|
* Installs the polyfill on the main-thread globals object
|
|
26
26
|
* @lynx-js/testing-environment hands to onInjectMainThreadGlobals. Scoped
|
|
27
|
-
* to
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
27
|
+
* to the one real gap: @lynx-js/testing-environment already implements
|
|
28
|
+
* __CreateView/__CreateText/__CreateElement/__CreateRawText/__AppendElement/
|
|
29
|
+
* __InsertElementBefore/__RemoveElement/__SetAttribute/__SetClasses/
|
|
30
|
+
* __AddInlineStyle/__FlushElementTree/__GetElementUniqueID — the missing
|
|
31
|
+
* piece is __AddEventListener/__RemoveEventListener (the testing environment
|
|
32
|
+
* only implements the string/worklet-event __AddEvent family that ReactLynx
|
|
33
|
+
* uses; mithril-lynx binds real JS function listeners directly). Gesture and
|
|
34
|
+
* list ops (Op.SetGestureDetector/Op.CreateList) are covered by the testing
|
|
35
|
+
* environment itself, not by this polyfill — see test/gesture.test.ts and
|
|
36
|
+
* test/list.test.ts.
|
|
36
37
|
*/
|
|
37
38
|
export function installTestingPolyfills(target) {
|
|
38
39
|
target.lynx.getEngine = target.lynx.getNative;
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
// Faithful stand-ins for the native `__AddEventListener` /
|
|
42
|
+
// `__RemoveEventListener` PAPIs (element, name, callback, options). The
|
|
43
|
+
// options object is accepted and ignored here — the real native side
|
|
44
|
+
// requires it (FiberRemoveEventListener throws "param size should >= 4"
|
|
45
|
+
// without it), so __RemoveEventListener mirrors that arity requirement to
|
|
46
|
+
// keep the test environment honest about the wire contract.
|
|
47
|
+
target.__AddEventListener = (node, name, handler, _options) => {
|
|
41
48
|
node.__vanillaListeners ??= {};
|
|
42
49
|
(node.__vanillaListeners[name] ??= new Set()).add(handler);
|
|
43
50
|
};
|
|
44
51
|
|
|
45
|
-
target.__RemoveEventListener = (node, name, handler)
|
|
52
|
+
target.__RemoveEventListener = function (node, name, handler, _options) {
|
|
53
|
+
if (arguments.length < 4) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"[mithril-lynx/testing] __RemoveEventListener requires 4 params " +
|
|
56
|
+
"(element, name, callback, options) — matching FiberRemoveEventListener.",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
46
59
|
node.__vanillaListeners?.[name]?.delete(handler);
|
|
47
60
|
};
|
|
48
61
|
}
|
package/test/list.test.ts
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { describe, expect, it } from "@rstest/core";
|
|
2
2
|
import m from "mithril";
|
|
3
|
+
import renderFactory from "mithril-runtime/render/render.js";
|
|
3
4
|
import { createPatchApplier } from "../src/apply-patch.js";
|
|
4
|
-
import {
|
|
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";
|
|
5
8
|
import { Op } from "../src/patch-protocol.js";
|
|
6
9
|
|
|
7
|
-
// Op.CreateList end-to-end
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// directly
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// already does against mithril-lynx-v1's list.js.
|
|
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.
|
|
17
19
|
|
|
18
20
|
function requestCell(listHandle: any, index: number, opId = 1) {
|
|
19
21
|
const listId = __GetElementUniqueID(listHandle);
|
|
@@ -34,37 +36,37 @@ function textOf(node: any): string {
|
|
|
34
36
|
return out;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
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() {
|
|
38
52
|
lynxTestingEnv.switchToMainThread();
|
|
39
53
|
const pageId = __GetElementUniqueID(__CreatePage());
|
|
40
54
|
const applier = createPatchApplier(pageId);
|
|
41
55
|
applier.registerPageRoot(__CreateView(pageId));
|
|
42
|
-
applier.applyPatch([Op.CreateList, 1,
|
|
56
|
+
applier.applyPatch([Op.CreateList, 1, "vertical", "single", 1]);
|
|
43
57
|
const listHandle = applier.getHandle(1) as any;
|
|
44
58
|
return { applier, listHandle };
|
|
45
59
|
}
|
|
46
60
|
|
|
47
|
-
function
|
|
48
|
-
applier.applyPatch([Op.SetListItems, 1,
|
|
61
|
+
function setCells(applier: ReturnType<typeof createPatchApplier>, cells: unknown[]) {
|
|
62
|
+
applier.applyPatch([Op.SetListItems, 1, cells]);
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
describe("Op.CreateList (native virtualized list support)", () => {
|
|
52
|
-
it("
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
applier.registerPageRoot(__CreateView(pageId));
|
|
57
|
-
|
|
58
|
-
expect(() => applier.applyPatch([Op.CreateList, 1, "nonexistent-key", "vertical", "single", 1])).toThrow(
|
|
59
|
-
/no list renderer registered for "nonexistent-key"/,
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("renders real content per cell via the registered renderer, driven by componentAtIndex", () => {
|
|
64
|
-
registerListRenderer("basic", (item: string, index: number) => m("text", {}, `${index}:${item}`));
|
|
65
|
-
|
|
66
|
-
const { applier, listHandle } = setupList("basic");
|
|
67
|
-
setItems(applier, ["a", "b", "c"]);
|
|
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"]));
|
|
68
70
|
|
|
69
71
|
requestCell(listHandle, 0);
|
|
70
72
|
requestCell(listHandle, 1);
|
|
@@ -73,10 +75,9 @@ describe("Op.CreateList (native virtualized list support)", () => {
|
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
it("recycles a cell for a different index, and its content updates to match", () => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
setItems(applier, ["a", "b", "c", "d"]);
|
|
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"]));
|
|
80
81
|
|
|
81
82
|
const signA = requestCell(listHandle, 0);
|
|
82
83
|
const wrapperA = listHandle.children[0];
|
|
@@ -91,15 +92,60 @@ describe("Op.CreateList (native virtualized list support)", () => {
|
|
|
91
92
|
});
|
|
92
93
|
|
|
93
94
|
it("SetListItems with a larger array requests the newly available indices without error", () => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const { applier, listHandle } = setupList("grow");
|
|
95
|
+
const { buildCells } = makeCellSource((item: string, index: number) => m("text", {}, `${index}:${item}`));
|
|
96
|
+
const { applier, listHandle } = setupList();
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
setCells(applier, buildCells(["a", "b"]));
|
|
99
99
|
expect(() => requestCell(listHandle, 1)).not.toThrow();
|
|
100
100
|
expect(() => requestCell(listHandle, 2)).toThrow(/cellIndex 2 out of range/);
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
setCells(applier, buildCells(["a", "b", "c"]));
|
|
103
103
|
expect(() => requestCell(listHandle, 2)).not.toThrow();
|
|
104
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 recycled cell is repositioned in the real tree to match its new index, not left where it was created", () => {
|
|
120
|
+
const { buildCells } = makeCellSource((item: string, index: number) => m("text", {}, `${index}:${item}`));
|
|
121
|
+
const { applier, listHandle } = setupList();
|
|
122
|
+
setCells(applier, buildCells(["a", "b", "c", "d"]));
|
|
123
|
+
|
|
124
|
+
requestCell(listHandle, 0);
|
|
125
|
+
const signB = requestCell(listHandle, 1);
|
|
126
|
+
requestCell(listHandle, 2);
|
|
127
|
+
// Real tree order matches request order so far: a, b, c.
|
|
128
|
+
expect([listHandle.children[0], listHandle.children[1], listHandle.children[2]].map(textOf)).toEqual(["0:a", "1:b", "2:c"]);
|
|
129
|
+
|
|
130
|
+
releaseCell(listHandle, signB); // b's wrapper goes to the recycle pool, still sitting in the middle of the tree
|
|
131
|
+
requestCell(listHandle, 3); // recycled for "d" — index 3 is after every other attached cell, so it belongs at the END
|
|
132
|
+
|
|
133
|
+
const children = [listHandle.children[0], listHandle.children[1], listHandle.children[2]];
|
|
134
|
+
expect(children.map(textOf)).toEqual(["0:a", "2:c", "3:d"]); // not ["0:a", "3:d", "2:c"] — the bug this test guards against
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("a tap inside a cell dispatches through the background thread's own fake-dom node", () => {
|
|
138
|
+
const { document, buildCells } = makeCellSource(() =>
|
|
139
|
+
m("text", { ontap: () => { taps += 1; } }, "tap me"),
|
|
140
|
+
);
|
|
141
|
+
let taps = 0;
|
|
142
|
+
const { applier, listHandle } = setupList();
|
|
143
|
+
const cells = buildCells([{}]);
|
|
144
|
+
setCells(applier, cells);
|
|
145
|
+
requestCell(listHandle, 0);
|
|
146
|
+
|
|
147
|
+
const node = document.getNodeById((cells[0] as any).rootChildIds[0]);
|
|
148
|
+
node!.dispatchEvent({ type: "tap", currentTarget: node, preventDefault() {}, stopPropagation() {} });
|
|
149
|
+
expect(taps).toBe(1);
|
|
150
|
+
});
|
|
105
151
|
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it } from "@rstest/core";
|
|
2
|
+
import { Op, OP_ARITY, PROTOCOL_VERSION, forEachOp, pushOp } from "../src/patch-protocol.js";
|
|
3
|
+
|
|
4
|
+
// M8: OP_ARITY is the "single source of truth" for walking a flat op array,
|
|
5
|
+
// but apply-patch.js re-implements the arity inline in its own switch. These
|
|
6
|
+
// tests lock the two to each other: every opcode must have an arity entry
|
|
7
|
+
// (no gap → no silent desync in forEachOp/list-cell), and the version prefix
|
|
8
|
+
// must never collide with a real opcode.
|
|
9
|
+
describe("patch-protocol OP_ARITY completeness", () => {
|
|
10
|
+
it("has an arity entry for every opcode in Op", () => {
|
|
11
|
+
const opcodes = Object.values(Op) as number[];
|
|
12
|
+
expect(opcodes.length).toBeGreaterThan(0);
|
|
13
|
+
for (const opcode of opcodes) {
|
|
14
|
+
expect(OP_ARITY[opcode], `OP_ARITY is missing opcode ${opcode}`).toBeDefined();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("PROTOCOL_VERSION does not collide with any opcode value", () => {
|
|
19
|
+
for (const opcode of Object.values(Op) as number[]) {
|
|
20
|
+
expect(opcode).not.toBe(PROTOCOL_VERSION);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("forEachOp walks a flat array built by pushOp without desyncing", () => {
|
|
25
|
+
const ops: unknown[] = [];
|
|
26
|
+
pushOp(ops, Op.CreateElement, "view", 1);
|
|
27
|
+
pushOp(ops, Op.SetText, 1, "hi");
|
|
28
|
+
pushOp(ops, Op.InsertBefore, 0, 1, -1);
|
|
29
|
+
|
|
30
|
+
const seen: number[] = [];
|
|
31
|
+
forEachOp(ops, (opcode, args) => {
|
|
32
|
+
seen.push(opcode);
|
|
33
|
+
if (opcode === Op.InsertBefore) expect(args).toEqual([0, 1, -1]);
|
|
34
|
+
});
|
|
35
|
+
expect(seen).toEqual([Op.CreateElement, Op.SetText, Op.InsertBefore]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("forEachOp throws on an unknown opcode instead of silently desyncing", () => {
|
|
39
|
+
expect(() => forEachOp([999], () => {})).toThrow(/Unknown patch opcode/);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "@rstest/core";
|
|
2
|
+
import { createPatchApplier } from "../src/apply-patch.js";
|
|
3
|
+
import { Op } from "../src/patch-protocol.js";
|
|
4
|
+
|
|
5
|
+
// R3: Op.RemoveEvent must actually remove the native listener, not silently
|
|
6
|
+
// no-op — otherwise a remove→re-add cycle on a kept element accumulates
|
|
7
|
+
// duplicate native listeners and the app's handler fires N times per event
|
|
8
|
+
// (see informe-contrato-mithril-lynx.md §R3).
|
|
9
|
+
describe("Op.RemoveEvent removes the native listener (no duplicate-fire leak)", () => {
|
|
10
|
+
it("AddEvent → RemoveEvent → AddEvent leaves exactly one listener", () => {
|
|
11
|
+
lynxTestingEnv.switchToMainThread();
|
|
12
|
+
const pageId = __GetElementUniqueID(__CreatePage());
|
|
13
|
+
const applier = createPatchApplier(pageId);
|
|
14
|
+
applier.registerPageRoot(__CreateView(pageId));
|
|
15
|
+
|
|
16
|
+
applier.applyPatch([Op.CreateElement, "view", 1, Op.AddEvent, 1, "tap"]);
|
|
17
|
+
const handle = applier.getHandle(1) as any;
|
|
18
|
+
expect(handle.__vanillaListeners.tap.size).toBe(1);
|
|
19
|
+
|
|
20
|
+
applier.applyPatch([Op.RemoveEvent, 1, "tap"]);
|
|
21
|
+
expect(handle.__vanillaListeners.tap.size).toBe(0);
|
|
22
|
+
|
|
23
|
+
applier.applyPatch([Op.AddEvent, 1, "tap"]);
|
|
24
|
+
expect(handle.__vanillaListeners.tap.size).toBe(1);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -15,7 +15,8 @@ describe("route.js + stable-host: a same-path re-resolve patches in place, never
|
|
|
15
15
|
lynxTestingEnv.switchToMainThread();
|
|
16
16
|
const capturedOps: unknown[][] = [];
|
|
17
17
|
lynx.getJSContext().addEventListener("MithrilLynx:Patch", (event: any) => {
|
|
18
|
-
|
|
18
|
+
// event.data is [PROTOCOL_VERSION, ...ops] — drop the version prefix.
|
|
19
|
+
capturedOps.push(event.data.slice(1));
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
lynxTestingEnv.switchToBackgroundThread();
|
package/test/route.test.ts
CHANGED
|
@@ -22,9 +22,12 @@ function setupRealTree() {
|
|
|
22
22
|
// tree inspected.
|
|
23
23
|
lynxTestingEnv.switchToMainThread();
|
|
24
24
|
lynx.getJSContext().addEventListener("MithrilLynx:Patch", (event: any) => {
|
|
25
|
-
|
|
25
|
+
// event.data is [PROTOCOL_VERSION, ...ops] — mirror main-thread.js's
|
|
26
|
+
// own onPatch by stripping the version prefix before applying.
|
|
27
|
+
const ops = event.data.slice(1);
|
|
28
|
+
capturedOps.push(ops);
|
|
26
29
|
lynxTestingEnv.switchToMainThread();
|
|
27
|
-
applier.applyPatch(
|
|
30
|
+
applier.applyPatch(ops);
|
|
28
31
|
});
|
|
29
32
|
|
|
30
33
|
return { applier, capturedOps };
|
package/test/setup.ts
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
// for everyone else (src/testing.js's installTestingPolyfills) — see that
|
|
5
5
|
// file's header for what it covers and why.
|
|
6
6
|
|
|
7
|
+
import { afterEach } from "@rstest/core";
|
|
7
8
|
import { installTestingPolyfills } from "../src/testing.js";
|
|
9
|
+
import { unregister } from "../src/mount-redraw.js";
|
|
8
10
|
|
|
9
11
|
globalThis.onInjectMainThreadGlobals = installTestingPolyfills;
|
|
12
|
+
|
|
13
|
+
// mount-redraw.js is a single-slot singleton and register() now fails fast
|
|
14
|
+
// on a second registration (R2) — clear it between tests so each test's own
|
|
15
|
+
// renderApp() starts from a clean slate instead of colliding with the
|
|
16
|
+
// previous test's still-registered redraw.
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
unregister();
|
|
19
|
+
});
|