jsmdcui 0.6.3 → 0.8.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/CHANGELOG.md +121 -0
- package/README.md +216 -33
- package/clean.sh +9 -0
- package/demo.resized.jpg +0 -0
- package/{image-processor.md → demos/image-processor.md} +1 -0
- package/{image-processor.zh-TW.md → demos/image-processor.zh-TW.md} +1 -0
- package/demos/maze.md +144 -0
- package/{select.md → demos/select.md} +2 -0
- package/demos/todo-zh.md +190 -0
- package/demos/todo.md +191 -0
- package/edit +9 -0
- package/package.json +1 -1
- package/runmd.mjs +77 -16
- package/runtime/help/help.md +216 -33
- package/runtime/jsplugins/cdp/cdp.js +5 -2
- package/runtime/jsplugins/chapter/chapter.js +4 -2
- package/runtime/jsplugins/example/example.js +10 -7
- package/runtime/syntax/markdown.yaml +1 -0
- package/single-exe/packAssets.sh +1 -1
- package/src/cui/fence-events.mjs +106 -0
- package/src/cui/id-collision.mjs +10 -28
- package/src/cui/kitty-debug.mjs +25 -0
- package/src/cui/kitty-images.mjs +200 -0
- package/src/cui/rpc.mjs +169 -10
- package/src/cui/server.mjs +14 -3
- package/src/cui/task-checkbox.mjs +44 -0
- package/src/index.js +615 -101
- package/src/platform/terminal.js +5 -0
- package/src/plugins/js-bridge.js +354 -21
- package/src/screen/screen.js +108 -1
- package/tests/cat-markdown.test.js +6 -5
- package/tests/demo.test.js +99 -9
- package/tests/fence-events.test.js +405 -0
- package/tests/heading-list-selector.test.js +346 -0
- package/tests/id-collision.test.js +14 -2
- package/tests/js-plugin-prompts.test.js +46 -0
- package/tests/key-event.md +10 -0
- package/tests/kitty-demo.md +184 -0
- package/tests/kitty-images.test.js +169 -0
- package/tests/platform-terminal.test.js +8 -0
- package/tests/task-checkbox.test.js +33 -0
- package/tests/textarea.md +8 -0
- package/tests/wui-responsive-images.test.js +35 -0
- package/tests/wui.test.js +16 -1
- package/tui +4 -2
- package/wui +6 -2
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createWebDollar } from "../src/cui/rpc.mjs";
|
|
3
|
+
import { createTuiSelector } from "../src/plugins/js-bridge.js";
|
|
4
|
+
|
|
5
|
+
function tuiSelector(markdown) {
|
|
6
|
+
const ansi = String(Bun.markdown.ansi(markdown, { hyperlinks: true, columns: 80 }));
|
|
7
|
+
const buffer = {
|
|
8
|
+
lines: Bun.stripANSI(ansi).split("\n"),
|
|
9
|
+
_mdcuiTuiSourceText: markdown,
|
|
10
|
+
_mdcuiAnsiText: ansi,
|
|
11
|
+
cursor: { x: 0, y: 0 },
|
|
12
|
+
ensureCursor() {},
|
|
13
|
+
invalidateHighlightFrom() {},
|
|
14
|
+
};
|
|
15
|
+
return { $: createTuiSelector(() => buffer), buffer };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const markdown = `## Features
|
|
19
|
+
|
|
20
|
+
- [x] Search
|
|
21
|
+
- [ ] Notifications
|
|
22
|
+
- [x] Nested notification option
|
|
23
|
+
- [x] Offline
|
|
24
|
+
|
|
25
|
+
Paragraph between lists.
|
|
26
|
+
|
|
27
|
+
- [x] Later list item
|
|
28
|
+
|
|
29
|
+
## Next
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
describe("TUI heading task-list Array methods", () => {
|
|
33
|
+
test("push and unshift accept multiple items and return the new direct length", () => {
|
|
34
|
+
const { $ } = tuiSelector(markdown);
|
|
35
|
+
const features = $("#features");
|
|
36
|
+
|
|
37
|
+
expect(features.push("Export", { value: "Sync", checked: true })).toBe(5);
|
|
38
|
+
expect(features.val()).toEqual(["Search", "Offline", "Sync"]);
|
|
39
|
+
expect(features.unshift("First", { label: "Pinned", checked: true })).toBe(7);
|
|
40
|
+
expect(features.val()).toEqual(["Pinned", "Search", "Offline", "Sync"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("pop and shift return removed labels and remove nested children with the parent", () => {
|
|
44
|
+
const { $, buffer } = tuiSelector(markdown);
|
|
45
|
+
const features = $("#features");
|
|
46
|
+
|
|
47
|
+
expect(features.shift()).toBe("Search");
|
|
48
|
+
expect(buffer.lines.some((line) => line.includes("Search"))).toBe(false);
|
|
49
|
+
expect(features.shift()).toBe("Notifications");
|
|
50
|
+
expect(buffer.lines.some((line) => line.includes("Nested notification option"))).toBe(false);
|
|
51
|
+
expect(features.pop()).toBe("Offline");
|
|
52
|
+
expect(features.pop()).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("an emptied list keeps its insertion point for later pushes", () => {
|
|
56
|
+
const { $ } = tuiSelector("## Features\n\n- [ ] Only\n\n## Next\n");
|
|
57
|
+
const features = $("#features");
|
|
58
|
+
|
|
59
|
+
expect(features.pop()).toBe("Only");
|
|
60
|
+
expect(features.pop()).toBeUndefined();
|
|
61
|
+
expect(features.push({ value: "Again", checked: true })).toBe(1);
|
|
62
|
+
expect(features.val()).toEqual(["Again"]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("val and mutations stop at the first rendered task list", () => {
|
|
66
|
+
const { $ } = tuiSelector(markdown);
|
|
67
|
+
const features = $("#features");
|
|
68
|
+
|
|
69
|
+
expect(features.val()).toEqual(["Search", "Offline"]);
|
|
70
|
+
expect(features.pop()).toBe("Offline");
|
|
71
|
+
expect(features.val()).toEqual(["Search"]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("a loose task list remains one list across indented item content", () => {
|
|
75
|
+
const { $ } = tuiSelector(`## Features
|
|
76
|
+
|
|
77
|
+
- [x] First
|
|
78
|
+
|
|
79
|
+
More detail
|
|
80
|
+
|
|
81
|
+
- [x] Second
|
|
82
|
+
|
|
83
|
+
Paragraph after the list.
|
|
84
|
+
|
|
85
|
+
- [x] Later list
|
|
86
|
+
`);
|
|
87
|
+
const features = $("#features");
|
|
88
|
+
|
|
89
|
+
expect(features.val()).toEqual(["First", "Second"]);
|
|
90
|
+
expect(features.pop()).toBe("Second");
|
|
91
|
+
expect(features.pop()).toBe("First");
|
|
92
|
+
expect(features.pop()).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("splice follows Array indexing, insertion, deletion, and return semantics", () => {
|
|
96
|
+
const { $, buffer } = tuiSelector(markdown);
|
|
97
|
+
const features = $("#features");
|
|
98
|
+
|
|
99
|
+
expect(features.splice()).toEqual([]);
|
|
100
|
+
expect(features.splice(1, 1, { value: "Replacement", checked: true }, "Extra"))
|
|
101
|
+
.toEqual(["Notifications"]);
|
|
102
|
+
expect(buffer.lines.some((line) => line.includes("Nested notification option"))).toBe(false);
|
|
103
|
+
expect(features.val()).toEqual(["Search", "Replacement", "Offline"]);
|
|
104
|
+
expect(features.splice(-1)).toEqual(["Offline"]);
|
|
105
|
+
expect(features.splice(1, undefined, "Inserted")).toEqual([]);
|
|
106
|
+
expect(features.splice(0, Infinity)).toEqual(["Search", "Inserted", "Replacement", "Extra"]);
|
|
107
|
+
expect(features.val()).toEqual([]);
|
|
108
|
+
expect(features.splice(0, 0, { label: "Again", checked: true })).toEqual([]);
|
|
109
|
+
expect(features.val()).toEqual(["Again"]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("slice returns read-only item snapshots with checked state", () => {
|
|
113
|
+
const { $ } = tuiSelector(markdown);
|
|
114
|
+
const features = $("#features");
|
|
115
|
+
|
|
116
|
+
const result = features.slice(0, 2);
|
|
117
|
+
expect(result).toEqual([
|
|
118
|
+
{ value: "Search", checked: true },
|
|
119
|
+
{ value: "Notifications", checked: false },
|
|
120
|
+
]);
|
|
121
|
+
result[0].value = "Changed snapshot";
|
|
122
|
+
result[0].checked = false;
|
|
123
|
+
expect(features.slice(-1)).toEqual([{ value: "Offline", checked: true }]);
|
|
124
|
+
expect(features.val()).toEqual(["Search", "Offline"]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("a heading without a task list cannot create one implicitly", () => {
|
|
128
|
+
const { $ } = tuiSelector("## Empty\n\nParagraph.\n");
|
|
129
|
+
expect($("#empty").push("No target")).toBe(0);
|
|
130
|
+
expect($("#empty").unshift("No target")).toBe(0);
|
|
131
|
+
expect($("#empty").pop()).toBeUndefined();
|
|
132
|
+
expect($("#empty").shift()).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
class TestText {
|
|
137
|
+
constructor(text, ownerDocument) {
|
|
138
|
+
this.textContent = String(text);
|
|
139
|
+
this.ownerDocument = ownerDocument;
|
|
140
|
+
this.parentElement = null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
class TestElement {
|
|
145
|
+
constructor(tagName, ownerDocument) {
|
|
146
|
+
this.tagName = tagName.toUpperCase();
|
|
147
|
+
this.ownerDocument = ownerDocument;
|
|
148
|
+
this.parentElement = null;
|
|
149
|
+
this.childNodes = [];
|
|
150
|
+
this.classList = new Set();
|
|
151
|
+
this.id = "";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
get children() {
|
|
155
|
+
return this.childNodes.filter((node) => node instanceof TestElement);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get firstChild() {
|
|
159
|
+
return this.childNodes[0] ?? null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
get nextElementSibling() {
|
|
163
|
+
const siblings = this.parentElement?.children ?? [];
|
|
164
|
+
const index = siblings.indexOf(this);
|
|
165
|
+
return index >= 0 ? siblings[index + 1] ?? null : null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get textContent() {
|
|
169
|
+
return this.childNodes.map((node) => node.textContent).join("");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
append(...nodes) {
|
|
173
|
+
for (const node of nodes) {
|
|
174
|
+
node.parentElement = this;
|
|
175
|
+
this.childNodes.push(node);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
insertBefore(node, before) {
|
|
180
|
+
node.parentElement = this;
|
|
181
|
+
const index = before == null ? -1 : this.childNodes.indexOf(before);
|
|
182
|
+
if (index < 0) this.childNodes.push(node);
|
|
183
|
+
else this.childNodes.splice(index, 0, node);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
remove() {
|
|
187
|
+
const siblings = this.parentElement?.childNodes;
|
|
188
|
+
const index = siblings?.indexOf(this) ?? -1;
|
|
189
|
+
if (index >= 0) siblings.splice(index, 1);
|
|
190
|
+
this.parentElement = null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
matches(selector) {
|
|
194
|
+
if (selector === "ul, ol") return this.tagName === "UL" || this.tagName === "OL";
|
|
195
|
+
if (selector === "li.task-list-item")
|
|
196
|
+
return this.tagName === "LI" && this.classList.has("task-list-item");
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
closest(selector) {
|
|
201
|
+
for (let node = this; node; node = node.parentElement) {
|
|
202
|
+
if (selector === "label" && node.tagName === "LABEL") return node;
|
|
203
|
+
if (selector === "li.task-list-item" && node.matches(selector)) return node;
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
querySelector(selector) {
|
|
209
|
+
return this.querySelectorAll(selector)[0] ?? null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
querySelectorAll(selector) {
|
|
213
|
+
const matches = [];
|
|
214
|
+
const visit = (node) => {
|
|
215
|
+
for (const child of node.children) {
|
|
216
|
+
if (selector === 'input[type="checkbox"]' &&
|
|
217
|
+
child.tagName === "INPUT" && child.type === "checkbox") matches.push(child);
|
|
218
|
+
else if (selector === "ul, ol" && child.matches(selector)) matches.push(child);
|
|
219
|
+
else if (selector === "li.task-list-item" && child.matches(selector)) matches.push(child);
|
|
220
|
+
visit(child);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
visit(this);
|
|
224
|
+
return matches;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
class TestDocument {
|
|
229
|
+
constructor() {
|
|
230
|
+
this.root = new TestElement("main", this);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
createElement(tagName) {
|
|
234
|
+
return new TestElement(tagName, this);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
createTextNode(text) {
|
|
238
|
+
return new TestText(text, this);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
querySelector(selector) {
|
|
242
|
+
if (!selector.startsWith("#")) return null;
|
|
243
|
+
const id = selector.slice(1);
|
|
244
|
+
return this.querySelectorAll("heading").find((element) => element.id === id) ?? null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
querySelectorAll(selector) {
|
|
248
|
+
if (selector === "[data-mdcui-tag]" || selector === "pre > code") return [];
|
|
249
|
+
if (selector === "heading") {
|
|
250
|
+
const headings = [];
|
|
251
|
+
const visit = (node) => {
|
|
252
|
+
for (const child of node.children) {
|
|
253
|
+
if (/^H[1-6]$/.test(child.tagName)) headings.push(child);
|
|
254
|
+
visit(child);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
visit(this.root);
|
|
258
|
+
return headings;
|
|
259
|
+
}
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function appendWebItem(documentObject, list, value, checked = false) {
|
|
265
|
+
const item = documentObject.createElement("li");
|
|
266
|
+
item.classList.add("task-list-item");
|
|
267
|
+
const label = documentObject.createElement("label");
|
|
268
|
+
const checkbox = documentObject.createElement("input");
|
|
269
|
+
checkbox.type = "checkbox";
|
|
270
|
+
checkbox.checked = checked;
|
|
271
|
+
label.append(checkbox, documentObject.createTextNode(value));
|
|
272
|
+
item.append(label);
|
|
273
|
+
list.append(item);
|
|
274
|
+
return item;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function webSelector() {
|
|
278
|
+
const documentObject = new TestDocument();
|
|
279
|
+
const section = documentObject.createElement("section");
|
|
280
|
+
const heading = documentObject.createElement("h2");
|
|
281
|
+
heading.id = "features";
|
|
282
|
+
const list = documentObject.createElement("ul");
|
|
283
|
+
appendWebItem(documentObject, list, "Search", true);
|
|
284
|
+
appendWebItem(documentObject, list, "Notifications");
|
|
285
|
+
appendWebItem(documentObject, list, "Offline", true);
|
|
286
|
+
section.append(heading, list);
|
|
287
|
+
documentObject.root.append(section);
|
|
288
|
+
return { $: createWebDollar(documentObject), list };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
describe("WUI heading task-list Array methods", () => {
|
|
292
|
+
test("uses the first list and follows Array return values", () => {
|
|
293
|
+
const { $ } = webSelector();
|
|
294
|
+
const features = $("#features");
|
|
295
|
+
|
|
296
|
+
expect(features.push("Export", { value: "Sync", checked: true })).toBe(5);
|
|
297
|
+
expect(features.val()).toEqual(["Search", "Offline", "Sync"]);
|
|
298
|
+
expect(features.unshift("First", "Second")).toBe(7);
|
|
299
|
+
expect(features.shift()).toBe("First");
|
|
300
|
+
expect(features.pop()).toBe("Sync");
|
|
301
|
+
expect(features.val()).toEqual(["Search", "Offline"]);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("an empty first list remains the target", () => {
|
|
305
|
+
const { $, list } = webSelector();
|
|
306
|
+
const features = $("#features");
|
|
307
|
+
|
|
308
|
+
expect(features.shift()).toBe("Search");
|
|
309
|
+
expect(features.shift()).toBe("Notifications");
|
|
310
|
+
expect(features.shift()).toBe("Offline");
|
|
311
|
+
expect(features.shift()).toBeUndefined();
|
|
312
|
+
expect(features.val()).toEqual([]);
|
|
313
|
+
expect(features.push({ label: "Again", checked: true })).toBe(1);
|
|
314
|
+
expect(list.children).toHaveLength(1);
|
|
315
|
+
expect(features.val()).toEqual(["Again"]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("splice follows Array indexing and returns removed labels", () => {
|
|
319
|
+
const { $ } = webSelector();
|
|
320
|
+
const features = $("#features");
|
|
321
|
+
|
|
322
|
+
expect(features.splice()).toEqual([]);
|
|
323
|
+
expect(features.splice(1, 1, { value: "Replacement", checked: true }, "Extra"))
|
|
324
|
+
.toEqual(["Notifications"]);
|
|
325
|
+
expect(features.val()).toEqual(["Search", "Replacement", "Offline"]);
|
|
326
|
+
expect(features.splice(-1)).toEqual(["Offline"]);
|
|
327
|
+
expect(features.splice(0, Infinity)).toEqual(["Search", "Replacement", "Extra"]);
|
|
328
|
+
expect(features.splice(0, 0, { value: "Again", checked: true })).toEqual([]);
|
|
329
|
+
expect(features.val()).toEqual(["Again"]);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("slice returns read-only item snapshots with checked state", () => {
|
|
333
|
+
const { $ } = webSelector();
|
|
334
|
+
const features = $("#features");
|
|
335
|
+
|
|
336
|
+
const result = features.slice(0, 2);
|
|
337
|
+
expect(result).toEqual([
|
|
338
|
+
{ value: "Search", checked: true },
|
|
339
|
+
{ value: "Notifications", checked: false },
|
|
340
|
+
]);
|
|
341
|
+
result[0].value = "Changed snapshot";
|
|
342
|
+
result[0].checked = false;
|
|
343
|
+
expect(features.slice(-1)).toEqual([{ value: "Offline", checked: true }]);
|
|
344
|
+
expect(features.val()).toEqual(["Search", "Offline"]);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
@@ -5,12 +5,13 @@ import { join } from "node:path";
|
|
|
5
5
|
import { createTuiSelector } from "../src/plugins/js-bridge.js";
|
|
6
6
|
|
|
7
7
|
const tui = join(import.meta.dir, "..", "tui");
|
|
8
|
+
const bunBin = Bun.which("bun") || process.argv0;
|
|
8
9
|
|
|
9
10
|
async function runCheck(markdown) {
|
|
10
11
|
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-check-"));
|
|
11
12
|
const file = join(dir, "app.md");
|
|
12
13
|
await writeFile(file, markdown);
|
|
13
|
-
const result = Bun.spawnSync([tui, "--check", file], { stdout: "pipe", stderr: "pipe" });
|
|
14
|
+
const result = Bun.spawnSync([bunBin, tui, "--check", file], { stdout: "pipe", stderr: "pipe" });
|
|
14
15
|
await rm(dir, { recursive: true, force: true });
|
|
15
16
|
return result;
|
|
16
17
|
}
|
|
@@ -42,6 +43,17 @@ test("--check reports heading/fenced-block collisions with line details", async
|
|
|
42
43
|
expect(output.lastIndexOf("FAILED")).toBeGreaterThan(output.lastIndexOf("Suggested fix"));
|
|
43
44
|
});
|
|
44
45
|
|
|
46
|
+
test("--check keeps fenced IDs that are followed by inline event attributes", async () => {
|
|
47
|
+
const result = await runCheck(
|
|
48
|
+
'## Write Status\n\n```text#write-status @keydown="refresh(); validate(event)"\nwaiting\n```\n',
|
|
49
|
+
);
|
|
50
|
+
const output = Bun.stripANSI(result.stdout.toString());
|
|
51
|
+
expect(result.exitCode).toBe(1);
|
|
52
|
+
expect(output).toContain("ID #write-status");
|
|
53
|
+
expect(output).toContain("Type: text fenced block");
|
|
54
|
+
expect(output).toContain('Source: ```text#write-status @keydown="refresh(); validate(event)"');
|
|
55
|
+
});
|
|
56
|
+
|
|
45
57
|
test("--check reports duplicate fenced-block IDs", async () => {
|
|
46
58
|
const result = await runCheck("```text#myid\na\n```\n\n```textarea#myid\nb\n```\n");
|
|
47
59
|
expect(result.exitCode).toBe(1);
|
|
@@ -140,7 +152,7 @@ test("TUI $ selector finds one ID across every tag/class query combination", ()
|
|
|
140
152
|
});
|
|
141
153
|
|
|
142
154
|
test("--check requires exactly one file", () => {
|
|
143
|
-
const result = Bun.spawnSync([tui, "--check"], { stdout: "pipe", stderr: "pipe" });
|
|
155
|
+
const result = Bun.spawnSync([bunBin, tui, "--check"], { stdout: "pipe", stderr: "pipe" });
|
|
144
156
|
expect(result.exitCode).toBe(2);
|
|
145
157
|
expect(result.stderr.toString()).toContain("Usage: jsmdcui --check FILE.md");
|
|
146
158
|
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { buildMicroGlobal } from "../src/plugins/js-bridge.js";
|
|
3
|
+
|
|
4
|
+
test("micro prompt APIs delegate and return synchronously", () => {
|
|
5
|
+
const previousMicro = globalThis.micro;
|
|
6
|
+
const previousSelector = globalThis.$;
|
|
7
|
+
const calls = [];
|
|
8
|
+
const app = {
|
|
9
|
+
protectedAlert(message) {
|
|
10
|
+
calls.push(["alert", message]);
|
|
11
|
+
return undefined;
|
|
12
|
+
},
|
|
13
|
+
protectedConfirm(message) {
|
|
14
|
+
calls.push(["confirm", message]);
|
|
15
|
+
return true;
|
|
16
|
+
},
|
|
17
|
+
protectedPrompt(message, defaultValue) {
|
|
18
|
+
calls.push(["prompt", message, defaultValue]);
|
|
19
|
+
return "answer";
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const micro = buildMicroGlobal({ _app: app, _ctx: null, on() {} });
|
|
25
|
+
const alertResult = micro.alert("hello");
|
|
26
|
+
const confirmResult = micro.confirm("continue?");
|
|
27
|
+
const promptResult = micro.prompt("name", "Ada");
|
|
28
|
+
|
|
29
|
+
expect(alertResult).toBeUndefined();
|
|
30
|
+
expect(confirmResult).toBe(true);
|
|
31
|
+
expect(promptResult).toBe("answer");
|
|
32
|
+
expect(alertResult?.then).toBeUndefined();
|
|
33
|
+
expect(confirmResult?.then).toBeUndefined();
|
|
34
|
+
expect(promptResult?.then).toBeUndefined();
|
|
35
|
+
expect(calls).toEqual([
|
|
36
|
+
["alert", "hello"],
|
|
37
|
+
["confirm", "continue?"],
|
|
38
|
+
["prompt", "name", "Ada"],
|
|
39
|
+
]);
|
|
40
|
+
} finally {
|
|
41
|
+
if (previousMicro === undefined) delete globalThis.micro;
|
|
42
|
+
else globalThis.micro = previousMicro;
|
|
43
|
+
if (previousSelector === undefined) delete globalThis.$;
|
|
44
|
+
else globalThis.$ = previousSelector;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Kitty scrolling test
|
|
2
|
+
|
|
3
|
+
Prelude filler line 01
|
|
4
|
+
Prelude filler line 02
|
|
5
|
+
Prelude filler line 03
|
|
6
|
+
Prelude filler line 04
|
|
7
|
+
Prelude filler line 05
|
|
8
|
+
Prelude filler line 06
|
|
9
|
+
Prelude filler line 07
|
|
10
|
+
Prelude filler line 08
|
|
11
|
+
Prelude filler line 09
|
|
12
|
+
Prelude filler line 10
|
|
13
|
+
Prelude filler line 11
|
|
14
|
+
Prelude filler line 12
|
|
15
|
+
Prelude filler line 13
|
|
16
|
+
Prelude filler line 14
|
|
17
|
+
Prelude filler line 15
|
|
18
|
+
Prelude filler line 16
|
|
19
|
+
Prelude filler line 17
|
|
20
|
+
Prelude filler line 18
|
|
21
|
+
Prelude filler line 19
|
|
22
|
+
Prelude filler line 20
|
|
23
|
+
Prelude filler line 21
|
|
24
|
+
Prelude filler line 22
|
|
25
|
+
Prelude filler line 23
|
|
26
|
+
Prelude filler line 24
|
|
27
|
+
Prelude filler line 25
|
|
28
|
+
Prelude filler line 26
|
|
29
|
+
Prelude filler line 27
|
|
30
|
+
Prelude filler line 28
|
|
31
|
+
Prelude filler line 29
|
|
32
|
+
Prelude filler line 30
|
|
33
|
+
Prelude filler line 31
|
|
34
|
+
Prelude filler line 32
|
|
35
|
+
Prelude filler line 33
|
|
36
|
+
Prelude filler line 34
|
|
37
|
+
Prelude filler line 35
|
|
38
|
+
Prelude filler line 36
|
|
39
|
+
Prelude filler line 37
|
|
40
|
+
Prelude filler line 38
|
|
41
|
+
Prelude filler line 39
|
|
42
|
+
Prelude filler line 40
|
|
43
|
+
Prelude filler line 41
|
|
44
|
+
Prelude filler line 42
|
|
45
|
+
Prelude filler line 43
|
|
46
|
+
Prelude filler line 44
|
|
47
|
+
Prelude filler line 45
|
|
48
|
+
Prelude filler line 46
|
|
49
|
+
Prelude filler line 47
|
|
50
|
+
Prelude filler line 48
|
|
51
|
+
Prelude filler line 49
|
|
52
|
+
Prelude filler line 50
|
|
53
|
+
|
|
54
|
+
## Before image 01
|
|
55
|
+
Content before the image, line 01.
|
|
56
|
+
|
|
57
|
+
## Before image 02
|
|
58
|
+
Content before the image, line 02.
|
|
59
|
+
|
|
60
|
+
## Before image 03
|
|
61
|
+
Content before the image, line 03.
|
|
62
|
+
|
|
63
|
+
## Before image 04
|
|
64
|
+
Content before the image, line 04.
|
|
65
|
+
|
|
66
|
+
## Before image 05
|
|
67
|
+
Content before the image, line 05.
|
|
68
|
+
|
|
69
|
+
## Before image 06
|
|
70
|
+
Content before the image, line 06.
|
|
71
|
+
|
|
72
|
+
## Before image 07
|
|
73
|
+
Content before the image, line 07.
|
|
74
|
+
|
|
75
|
+
## Before image 08
|
|
76
|
+
Content before the image, line 08.
|
|
77
|
+
|
|
78
|
+
## Before image 09
|
|
79
|
+
Content before the image, line 09.
|
|
80
|
+
|
|
81
|
+
## Before image 10
|
|
82
|
+
Content before the image, line 10.
|
|
83
|
+
|
|
84
|
+
## Image anchor
|
|
85
|
+
|
|
86
|
+

|
|
87
|
+
|
|
88
|
+
## After image 01
|
|
89
|
+
Content after the image, line 01.
|
|
90
|
+
|
|
91
|
+
## After image 02
|
|
92
|
+
Content after the image, line 02.
|
|
93
|
+
|
|
94
|
+
## After image 03
|
|
95
|
+
Content after the image, line 03.
|
|
96
|
+
|
|
97
|
+
## After image 04
|
|
98
|
+
Content after the image, line 04.
|
|
99
|
+
|
|
100
|
+
## After image 05
|
|
101
|
+
Content after the image, line 05.
|
|
102
|
+
|
|
103
|
+
## After image 06
|
|
104
|
+
Content after the image, line 06.
|
|
105
|
+
|
|
106
|
+
## After image 07
|
|
107
|
+
Content after the image, line 07.
|
|
108
|
+
|
|
109
|
+
## After image 08
|
|
110
|
+
Content after the image, line 08.
|
|
111
|
+
|
|
112
|
+
## After image 09
|
|
113
|
+
Content after the image, line 09.
|
|
114
|
+
|
|
115
|
+
## After image 10
|
|
116
|
+
Content after the image, line 10.
|
|
117
|
+
|
|
118
|
+
## After image 11
|
|
119
|
+
Content after the image, line 11.
|
|
120
|
+
|
|
121
|
+
## After image 12
|
|
122
|
+
Content after the image, line 12.
|
|
123
|
+
|
|
124
|
+
## After image 13
|
|
125
|
+
Content after the image, line 13.
|
|
126
|
+
|
|
127
|
+
## After image 14
|
|
128
|
+
Content after the image, line 14.
|
|
129
|
+
|
|
130
|
+
## After image 15
|
|
131
|
+
Content after the image, line 15.
|
|
132
|
+
|
|
133
|
+

|
|
134
|
+
|
|
135
|
+
Epilogue filler line 01
|
|
136
|
+
Epilogue filler line 02
|
|
137
|
+
Epilogue filler line 03
|
|
138
|
+
Epilogue filler line 04
|
|
139
|
+
Epilogue filler line 05
|
|
140
|
+
Epilogue filler line 06
|
|
141
|
+
Epilogue filler line 07
|
|
142
|
+
Epilogue filler line 08
|
|
143
|
+
Epilogue filler line 09
|
|
144
|
+
Epilogue filler line 10
|
|
145
|
+
Epilogue filler line 11
|
|
146
|
+
Epilogue filler line 12
|
|
147
|
+
Epilogue filler line 13
|
|
148
|
+
Epilogue filler line 14
|
|
149
|
+
Epilogue filler line 15
|
|
150
|
+
Epilogue filler line 16
|
|
151
|
+
Epilogue filler line 17
|
|
152
|
+
Epilogue filler line 18
|
|
153
|
+
Epilogue filler line 19
|
|
154
|
+
Epilogue filler line 20
|
|
155
|
+
Epilogue filler line 21
|
|
156
|
+
Epilogue filler line 22
|
|
157
|
+
Epilogue filler line 23
|
|
158
|
+
Epilogue filler line 24
|
|
159
|
+
Epilogue filler line 25
|
|
160
|
+
Epilogue filler line 26
|
|
161
|
+
Epilogue filler line 27
|
|
162
|
+
Epilogue filler line 28
|
|
163
|
+
Epilogue filler line 29
|
|
164
|
+
Epilogue filler line 30
|
|
165
|
+
Epilogue filler line 31
|
|
166
|
+
Epilogue filler line 32
|
|
167
|
+
Epilogue filler line 33
|
|
168
|
+
Epilogue filler line 34
|
|
169
|
+
Epilogue filler line 35
|
|
170
|
+
Epilogue filler line 36
|
|
171
|
+
Epilogue filler line 37
|
|
172
|
+
Epilogue filler line 38
|
|
173
|
+
Epilogue filler line 39
|
|
174
|
+
Epilogue filler line 40
|
|
175
|
+
Epilogue filler line 41
|
|
176
|
+
Epilogue filler line 42
|
|
177
|
+
Epilogue filler line 43
|
|
178
|
+
Epilogue filler line 44
|
|
179
|
+
Epilogue filler line 45
|
|
180
|
+
Epilogue filler line 46
|
|
181
|
+
Epilogue filler line 47
|
|
182
|
+
Epilogue filler line 48
|
|
183
|
+
Epilogue filler line 49
|
|
184
|
+
Epilogue filler line 50
|