jsmdcui 0.5.0 → 0.7.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 +228 -0
- package/README.md +305 -25
- package/clean.sh +9 -0
- package/demo.resized.jpg +0 -0
- package/demos/image-processor.md +361 -0
- package/demos/image-processor.zh-TW.md +361 -0
- package/demos/select.md +63 -0
- package/demos/todo-zh.md +92 -0
- package/demos/todo.md +93 -0
- package/package.json +1 -1
- package/runmd.mjs +110 -17
- package/runtime/help/help.md +305 -25
- package/single-exe/packAssets.sh +1 -1
- package/src/cui/id-collision.mjs +151 -0
- package/src/cui/kitty-debug.mjs +25 -0
- package/src/cui/kitty-images.mjs +190 -0
- package/src/cui/rpc.mjs +227 -1
- package/src/cui/server.mjs +1 -1
- package/src/cui/task-checkbox.mjs +60 -0
- package/src/index.js +297 -23
- package/src/plugins/js-bridge.js +346 -4
- package/src/screen/screen.js +108 -1
- package/tests/cat-markdown.test.js +6 -5
- package/tests/demo.test.js +203 -4
- package/tests/heading-list-selector.test.js +346 -0
- package/tests/id-collision.test.js +147 -0
- package/tests/kitty-demo.md +184 -0
- package/tests/kitty-images.test.js +147 -0
- package/tests/task-checkbox.test.js +33 -0
- package/tests/wui-responsive-images.test.js +35 -0
- package/tests/wui.test.js +30 -0
- package/tui +4 -2
- package/wui +6 -2
package/tests/demo.test.js
CHANGED
|
@@ -1,14 +1,47 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
|
-
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
6
|
const tui = join(import.meta.dir, "..", "tui");
|
|
7
|
+
const bunBin = Bun.which("bun") || process.argv0;
|
|
8
|
+
|
|
9
|
+
test("--help describes the non-overwriting demo behavior", () => {
|
|
10
|
+
const result = Bun.spawnSync([bunBin, tui, "--help"], {
|
|
11
|
+
stdout: "pipe",
|
|
12
|
+
stderr: "pipe",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
expect(result.exitCode).toBe(0);
|
|
16
|
+
const output = result.stdout.toString();
|
|
17
|
+
expect(output).toContain("use the existing ./testapp.md without overwriting it");
|
|
18
|
+
expect(output).toContain("If ./testapp.md is missing, write the bundled demo there first");
|
|
19
|
+
expect(output).toContain("--demo-<filename>");
|
|
20
|
+
expect(output).toContain("demos/<filename>.md");
|
|
21
|
+
expect(output.match(/Open it in the TUI and write 5 generated files beside it/g)?.length).toBe(2);
|
|
22
|
+
expect(output).toContain("--demo-imgtool");
|
|
23
|
+
expect(output).toContain("--demo-imgtool-zh");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("--demo-list lists root and automatically discovered demos", () => {
|
|
27
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo-list"], {
|
|
28
|
+
stdout: "pipe",
|
|
29
|
+
stderr: "pipe",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(result.exitCode).toBe(0);
|
|
33
|
+
const output = result.stdout.toString();
|
|
34
|
+
expect(output).toMatch(/--demo\s+testapp\.md/);
|
|
35
|
+
expect(output).toMatch(/--demo-image-processor\s+demos\/image-processor\.md/);
|
|
36
|
+
expect(output).toMatch(/--demo-select\s+demos\/select\.md/);
|
|
37
|
+
expect(output).toMatch(/--demo-todo-zh\s+demos\/todo-zh\.md/);
|
|
38
|
+
expect(output).toContain("--demo-imgtool --demo-image-processor");
|
|
39
|
+
});
|
|
7
40
|
|
|
8
41
|
test("--demo writes bundled testapp.md to cwd before opening it", async () => {
|
|
9
42
|
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-demo-"));
|
|
10
43
|
try {
|
|
11
|
-
const result = Bun.spawnSync([tui, "--demo", "-cat", "-encoding", "utf8"], {
|
|
44
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo", "-cat", "-encoding", "utf8"], {
|
|
12
45
|
cwd: dir,
|
|
13
46
|
stdout: "pipe",
|
|
14
47
|
stderr: "pipe",
|
|
@@ -16,8 +49,174 @@ test("--demo writes bundled testapp.md to cwd before opening it", async () => {
|
|
|
16
49
|
|
|
17
50
|
expect(result.exitCode).toBe(0);
|
|
18
51
|
const written = await readFile(join(dir, "testapp.md"), "utf8");
|
|
19
|
-
expect(written).toContain("
|
|
20
|
-
expect(Bun.stripANSI(result.stdout.toString())).toContain("
|
|
52
|
+
expect(written).toContain("計算機");
|
|
53
|
+
expect(Bun.stripANSI(result.stdout.toString())).toContain("計算機");
|
|
54
|
+
} finally {
|
|
55
|
+
await rm(dir, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("--demo preserves an existing testapp.md", async () => {
|
|
60
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-demo-existing-"));
|
|
61
|
+
const existing = "# Keep my demo\n";
|
|
62
|
+
try {
|
|
63
|
+
await writeFile(join(dir, "testapp.md"), existing);
|
|
64
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo", "-cat", "-encoding", "utf8"], {
|
|
65
|
+
cwd: dir,
|
|
66
|
+
stdout: "pipe",
|
|
67
|
+
stderr: "pipe",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(result.exitCode).toBe(0);
|
|
71
|
+
expect(await readFile(join(dir, "testapp.md"), "utf8")).toBe(existing);
|
|
72
|
+
expect(Bun.stripANSI(result.stdout.toString())).toContain("Keep my demo");
|
|
73
|
+
} finally {
|
|
74
|
+
await rm(dir, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("--demo-<filename> automatically loads a matching demos file", async () => {
|
|
79
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-demo-generic-"));
|
|
80
|
+
try {
|
|
81
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo-todo", "-cat", "-encoding", "utf8"], {
|
|
82
|
+
cwd: dir,
|
|
83
|
+
stdout: "pipe",
|
|
84
|
+
stderr: "pipe",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(result.exitCode).toBe(0);
|
|
88
|
+
const written = await readFile(join(dir, "todo.md"), "utf8");
|
|
89
|
+
expect(written).toContain("# Todo List");
|
|
90
|
+
expect(written).toContain("javascript:addTodo()");
|
|
91
|
+
expect(Bun.stripANSI(result.stdout.toString())).toContain("Show Completed");
|
|
92
|
+
} finally {
|
|
93
|
+
await rm(dir, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("--demo-<filename> preserves an existing local copy", async () => {
|
|
98
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-demo-generic-existing-"));
|
|
99
|
+
const existing = "# Keep my selector demo\n";
|
|
100
|
+
try {
|
|
101
|
+
await writeFile(join(dir, "select.md"), existing);
|
|
102
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo-select", "-cat", "-encoding", "utf8"], {
|
|
103
|
+
cwd: dir,
|
|
104
|
+
stdout: "pipe",
|
|
105
|
+
stderr: "pipe",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(result.exitCode).toBe(0);
|
|
109
|
+
expect(await readFile(join(dir, "select.md"), "utf8")).toBe(existing);
|
|
110
|
+
expect(Bun.stripANSI(result.stdout.toString())).toContain("Keep my selector demo");
|
|
111
|
+
} finally {
|
|
112
|
+
await rm(dir, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("an unknown --demo-<filename> reports an error without creating a file", async () => {
|
|
117
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-demo-unknown-"));
|
|
118
|
+
try {
|
|
119
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo-does-not-exist", "-cat", "-encoding", "utf8"], {
|
|
120
|
+
cwd: dir,
|
|
121
|
+
stdout: "pipe",
|
|
122
|
+
stderr: "pipe",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(result.exitCode).toBe(2);
|
|
126
|
+
expect(result.stderr.toString()).toContain("Unknown demo --demo-does-not-exist");
|
|
127
|
+
expect(await Bun.file(join(dir, "does-not-exist.md")).exists()).toBe(false);
|
|
128
|
+
} finally {
|
|
129
|
+
await rm(dir, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("--demo-image-processor works through generic demo discovery", async () => {
|
|
134
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-imgtool-generic-"));
|
|
135
|
+
try {
|
|
136
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo-image-processor", "-cat", "-encoding", "utf8"], {
|
|
137
|
+
cwd: dir,
|
|
138
|
+
stdout: "pipe",
|
|
139
|
+
stderr: "pipe",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(result.exitCode).toBe(0);
|
|
143
|
+
expect(await readFile(join(dir, "image-processor.md"), "utf8")).toContain("# Bun.Image Processor");
|
|
144
|
+
} finally {
|
|
145
|
+
await rm(dir, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("--demo-imgtool writes the bundled image processor to cwd", async () => {
|
|
150
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-imgtool-"));
|
|
151
|
+
try {
|
|
152
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo-imgtool", "-cat", "-encoding", "utf8"], {
|
|
153
|
+
cwd: dir,
|
|
154
|
+
stdout: "pipe",
|
|
155
|
+
stderr: "pipe",
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(result.exitCode).toBe(0);
|
|
159
|
+
const written = await readFile(join(dir, "image-processor.md"), "utf8");
|
|
160
|
+
expect(written).toContain("# Bun.Image Processor");
|
|
161
|
+
expect(written).toContain("javascript:readMetadata()");
|
|
162
|
+
expect(Bun.stripANSI(result.stdout.toString())).toContain("Bun.Image Processor");
|
|
163
|
+
} finally {
|
|
164
|
+
await rm(dir, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("--demo-imgtool preserves an existing image-processor.md", async () => {
|
|
169
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-imgtool-existing-"));
|
|
170
|
+
const existing = "# Keep my image tool\n";
|
|
171
|
+
try {
|
|
172
|
+
await writeFile(join(dir, "image-processor.md"), existing);
|
|
173
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo-imgtool", "-cat", "-encoding", "utf8"], {
|
|
174
|
+
cwd: dir,
|
|
175
|
+
stdout: "pipe",
|
|
176
|
+
stderr: "pipe",
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(result.exitCode).toBe(0);
|
|
180
|
+
expect(await readFile(join(dir, "image-processor.md"), "utf8")).toBe(existing);
|
|
181
|
+
expect(Bun.stripANSI(result.stdout.toString())).toContain("Keep my image tool");
|
|
182
|
+
} finally {
|
|
183
|
+
await rm(dir, { recursive: true, force: true });
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("--demo-imgtool-zh writes the bundled Traditional Chinese image processor to cwd", async () => {
|
|
188
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-imgtool-zh-"));
|
|
189
|
+
try {
|
|
190
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo-imgtool-zh", "-cat", "-encoding", "utf8"], {
|
|
191
|
+
cwd: dir,
|
|
192
|
+
stdout: "pipe",
|
|
193
|
+
stderr: "pipe",
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(result.exitCode).toBe(0);
|
|
197
|
+
const written = await readFile(join(dir, "image-processor.zh-TW.md"), "utf8");
|
|
198
|
+
expect(written).toContain("先把本機圖片路徑貼到下方");
|
|
199
|
+
expect(written).toContain("javascript:readMetadata()");
|
|
200
|
+
expect(Bun.stripANSI(result.stdout.toString())).toContain("先把本機圖片路徑貼到下方");
|
|
201
|
+
} finally {
|
|
202
|
+
await rm(dir, { recursive: true, force: true });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("--demo-imgtool-zh preserves an existing image-processor.zh-TW.md", async () => {
|
|
207
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-imgtool-zh-existing-"));
|
|
208
|
+
const existing = "# 保留我的圖片工具\n";
|
|
209
|
+
try {
|
|
210
|
+
await writeFile(join(dir, "image-processor.zh-TW.md"), existing);
|
|
211
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo-imgtool-zh", "-cat", "-encoding", "utf8"], {
|
|
212
|
+
cwd: dir,
|
|
213
|
+
stdout: "pipe",
|
|
214
|
+
stderr: "pipe",
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(result.exitCode).toBe(0);
|
|
218
|
+
expect(await readFile(join(dir, "image-processor.zh-TW.md"), "utf8")).toBe(existing);
|
|
219
|
+
expect(Bun.stripANSI(result.stdout.toString())).toContain("保留我的圖片工具");
|
|
21
220
|
} finally {
|
|
22
221
|
await rm(dir, { recursive: true, force: true });
|
|
23
222
|
}
|
|
@@ -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
|
+
});
|