@dtudury/streamo 1.0.1 → 3.0.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.
@@ -1,373 +0,0 @@
1
- import { WebSocketServer } from 'ws'
2
- import { describe } from './utils/testing.js'
3
- import { RepoRegistry } from './RepoRegistry.js'
4
- import { attachStreamSync } from './outletSync.js'
5
- import { registrySync } from './registrySync.js'
6
-
7
- // makeVerifiedWritableStream only rejects invalid SIGNATURE chunks —
8
- // plain data chunks pass through even with fake keys. 33 bytes = compressed pubkey size.
9
- const fakeKey = (n = 0) => '02' + n.toString(16).padStart(2, '0').repeat(32) // 66-char hex
10
-
11
- /** Wait up to `ms` ms for `fn()` to return truthy, polling every 10 ms. */
12
- function waitFor (fn, ms = 500) {
13
- return new Promise((resolve, reject) => {
14
- const start = Date.now()
15
- const poll = () => {
16
- const v = fn()
17
- if (v) return resolve(v)
18
- if (Date.now() - start > ms) return reject(new Error('waitFor timeout'))
19
- setTimeout(poll, 10)
20
- }
21
- poll()
22
- })
23
- }
24
-
25
- /** Start a WebSocketServer on a random port backed by a registry. */
26
- function startServer (registry) {
27
- return new Promise((resolve, reject) => {
28
- const wss = new WebSocketServer({ port: 0 })
29
- wss.on('listening', () => {
30
- const { port } = wss.address()
31
- attachStreamSync(wss, registry, 'test-outlet')
32
- resolve({ wss, port })
33
- })
34
- wss.on('error', reject)
35
- })
36
- }
37
-
38
- describe(import.meta.url, ({ test }) => {
39
- test('onOpen fires after registry.open resolves', async ({ assert }) => {
40
- const registry = new RepoRegistry()
41
- const calls = []
42
- registry.onOpen((key, repo) => calls.push({ key, repo }))
43
- const repo = await registry.open('abc')
44
- assert.equal(calls.length, 1)
45
- assert.equal(calls[0].key, 'abc')
46
- assert.ok(calls[0].repo === repo)
47
- })
48
-
49
- test('offOpen removes the callback', async ({ assert }) => {
50
- const registry = new RepoRegistry()
51
- let count = 0
52
- const cb = () => count++
53
- registry.onOpen(cb)
54
- await registry.open('x')
55
- registry.offOpen(cb)
56
- await registry.open('y')
57
- assert.equal(count, 1)
58
- })
59
-
60
- test('onOpen not called for already-open key (concurrent open)', async ({ assert }) => {
61
- const registry = new RepoRegistry()
62
- let count = 0
63
- registry.onOpen(() => count++)
64
- await Promise.all([registry.open('k'), registry.open('k'), registry.open('k')])
65
- assert.equal(count, 1)
66
- })
67
-
68
- test('two registries sync an existing repo via registrySync', async ({ assert }) => {
69
- const serverRegistry = new RepoRegistry()
70
- const { wss, port } = await startServer(serverRegistry)
71
-
72
- const keyHex = fakeKey(1)
73
- const serverRepo = await serverRegistry.open(keyHex)
74
- serverRepo.set({ hello: 'world' })
75
-
76
- const clientRegistry = new RepoRegistry()
77
- const { ws } = await registrySync(clientRegistry, 'localhost', port, { filter: k => k === keyHex })
78
-
79
- await waitFor(() => clientRegistry.get(keyHex)?.get('hello') === 'world')
80
- assert.equal(clientRegistry.get(keyHex).get('hello'), 'world')
81
-
82
- ws.close()
83
- await new Promise(r => wss.close(r))
84
- })
85
-
86
- test('changes on server after connect are synced to client', async ({ assert }) => {
87
- const serverRegistry = new RepoRegistry()
88
- const { wss, port } = await startServer(serverRegistry)
89
-
90
- const keyHex = fakeKey(2)
91
- const serverRepo = await serverRegistry.open(keyHex)
92
- serverRepo.set({ v: 1 })
93
-
94
- const clientRegistry = new RepoRegistry()
95
- const { ws } = await registrySync(clientRegistry, 'localhost', port, { filter: k => k === keyHex })
96
-
97
- await waitFor(() => clientRegistry.get(keyHex)?.get('v') === 1)
98
-
99
- serverRepo.set({ v: 2 })
100
- await waitFor(() => clientRegistry.get(keyHex)?.get('v') === 2)
101
- assert.equal(clientRegistry.get(keyHex).get('v'), 2)
102
-
103
- ws.close()
104
- await new Promise(r => wss.close(r))
105
- })
106
-
107
- test('newly opened server repos are announced and synced', async ({ assert }) => {
108
- const serverRegistry = new RepoRegistry()
109
- const { wss, port } = await startServer(serverRegistry)
110
-
111
- const keyHex = fakeKey(3)
112
- const clientRegistry = new RepoRegistry()
113
- const { ws } = await registrySync(clientRegistry, 'localhost', port)
114
-
115
- const serverRepo = await serverRegistry.open(keyHex)
116
- serverRepo.set({ late: true })
117
-
118
- await waitFor(() => clientRegistry.get(keyHex)?.get('late') === true)
119
- assert.equal(clientRegistry.get(keyHex).get('late'), true)
120
-
121
- ws.close()
122
- await new Promise(r => wss.close(r))
123
- })
124
-
125
- test('filter prevents unwanted repos from syncing', async ({ assert }) => {
126
- const serverRegistry = new RepoRegistry()
127
- const { wss, port } = await startServer(serverRegistry)
128
-
129
- const keyA = fakeKey(4)
130
- const keyB = fakeKey(5)
131
-
132
- const repoA = await serverRegistry.open(keyA)
133
- repoA.set({ name: 'a' })
134
- const repoB = await serverRegistry.open(keyB)
135
- repoB.set({ name: 'b' })
136
-
137
- const clientRegistry = new RepoRegistry()
138
- const { ws } = await registrySync(clientRegistry, 'localhost', port, { filter: k => k === keyA })
139
-
140
- await waitFor(() => clientRegistry.get(keyA)?.get('name') === 'a')
141
- assert.equal(clientRegistry.get(keyA).get('name'), 'a')
142
-
143
- await new Promise(r => setTimeout(r, 100))
144
- assert.equal(clientRegistry.get(keyB), undefined, 'keyB was filtered out')
145
-
146
- ws.close()
147
- await new Promise(r => wss.close(r))
148
- })
149
-
150
- test('two peers with different repos each sync both after connecting', async ({ assert }) => {
151
- const registryA = new RepoRegistry()
152
- const registryB = new RepoRegistry()
153
-
154
- const keyA = fakeKey(6)
155
- const keyB = fakeKey(7)
156
-
157
- const repoA = await registryA.open(keyA)
158
- repoA.set({ owner: 'A' })
159
- const repoB = await registryB.open(keyB)
160
- repoB.set({ owner: 'B' })
161
-
162
- const { wss, port } = await startServer(registryA)
163
- const { ws } = await registrySync(registryB, 'localhost', port)
164
-
165
- await waitFor(() => registryA.get(keyB)?.get('owner') === 'B')
166
- await waitFor(() => registryB.get(keyA)?.get('owner') === 'A')
167
-
168
- assert.equal(registryA.get(keyB).get('owner'), 'B')
169
- assert.equal(registryB.get(keyA).get('owner'), 'A')
170
-
171
- ws.close()
172
- await new Promise(r => wss.close(r))
173
- })
174
-
175
- test('follow: auto-subscribes to repos referenced in a synced repo\'s value', async ({ assert }) => {
176
- // Simulates a chat app: rootRepo lists participant keys; client follows the
177
- // root and should automatically discover and sync all participant repos.
178
- const serverRegistry = new RepoRegistry()
179
- const { wss, port } = await startServer(serverRegistry)
180
-
181
- const rootKey = fakeKey(10)
182
- const aliceKey = fakeKey(11)
183
- const bobKey = fakeKey(12)
184
-
185
- const rootRepo = await serverRegistry.open(rootKey)
186
- const aliceRepo = await serverRegistry.open(aliceKey)
187
- const bobRepo = await serverRegistry.open(bobKey)
188
-
189
- aliceRepo.set({ name: 'alice', message: 'hello' })
190
- bobRepo.set({ name: 'bob', message: 'hey' })
191
- rootRepo.set({ members: [aliceKey, bobKey] })
192
-
193
- const clientRegistry = new RepoRegistry()
194
- const { ws } = await registrySync(clientRegistry, 'localhost', port, {
195
- filter: k => k === rootKey, // only explicitly subscribe to root
196
- follow: (keyHex, repo, subscribe) => {
197
- // extract participant keys from the chat repo
198
- for (const memberKey of repo.get('members') ?? []) subscribe(memberKey)
199
- }
200
- })
201
-
202
- // Root syncs via filter; participants sync via follow
203
- await waitFor(() => clientRegistry.get(aliceKey)?.get('name') === 'alice')
204
- await waitFor(() => clientRegistry.get(bobKey)?.get('name') === 'bob')
205
-
206
- assert.equal(clientRegistry.get(aliceKey).get('name'), 'alice')
207
- assert.equal(clientRegistry.get(bobKey).get('name'), 'bob')
208
-
209
- ws.close()
210
- await new Promise(r => wss.close(r))
211
- })
212
-
213
- test('follow: re-runs when a repo changes and discovers newly added refs', async ({ assert }) => {
214
- const serverRegistry = new RepoRegistry()
215
- const { wss, port } = await startServer(serverRegistry)
216
-
217
- const rootKey = fakeKey(13)
218
- const carolKey = fakeKey(14)
219
-
220
- const rootRepo = await serverRegistry.open(rootKey)
221
- rootRepo.set({ members: [] }) // starts empty
222
-
223
- const clientRegistry = new RepoRegistry()
224
- const { ws } = await registrySync(clientRegistry, 'localhost', port, {
225
- filter: k => k === rootKey,
226
- follow: (keyHex, repo, subscribe) => {
227
- for (const memberKey of repo.get('members') ?? []) subscribe(memberKey)
228
- }
229
- })
230
-
231
- await waitFor(() => clientRegistry.get(rootKey)?.get('members') !== undefined)
232
-
233
- // Carol joins: her repo is added to the server, root is updated to list her
234
- const carolRepo = await serverRegistry.open(carolKey)
235
- carolRepo.set({ name: 'carol' })
236
- rootRepo.set({ members: [carolKey] })
237
-
238
- await waitFor(() => clientRegistry.get(carolKey)?.get('name') === 'carol')
239
- assert.equal(clientRegistry.get(carolKey).get('name'), 'carol')
240
-
241
- ws.close()
242
- await new Promise(r => wss.close(r))
243
- })
244
-
245
- test('announce is routed to interested peers', async ({ assert }) => {
246
- const { wss, port } = await startServer(new RepoRegistry())
247
- const topic = fakeKey(20)
248
- const announced = fakeKey(21)
249
-
250
- const received = []
251
- const sessionA = await registrySync(new RepoRegistry(), 'localhost', port, {
252
- onAnnounce: (key, t) => received.push({ key, topic: t })
253
- })
254
- sessionA.interest(topic)
255
-
256
- const sessionB = await registrySync(new RepoRegistry(), 'localhost', port)
257
-
258
- // Give the interest message time to reach the server
259
- await new Promise(r => setTimeout(r, 50))
260
- sessionB.announce(announced, topic)
261
-
262
- await waitFor(() => received.length === 1)
263
- assert.equal(received[0].key, announced)
264
- assert.equal(received[0].topic, topic)
265
-
266
- sessionA.close()
267
- sessionB.close()
268
- await new Promise(r => wss.close(r))
269
- })
270
-
271
- test('announce is not received without interest', async ({ assert }) => {
272
- const { wss, port } = await startServer(new RepoRegistry())
273
- const topic = fakeKey(22)
274
- const announced = fakeKey(23)
275
-
276
- const received = []
277
- const sessionA = await registrySync(new RepoRegistry(), 'localhost', port, {
278
- onAnnounce: (key) => received.push(key)
279
- })
280
- // sessionA does NOT call interest(topic)
281
-
282
- const sessionB = await registrySync(new RepoRegistry(), 'localhost', port)
283
- await new Promise(r => setTimeout(r, 50))
284
- sessionB.announce(announced, topic)
285
-
286
- await new Promise(r => setTimeout(r, 100))
287
- assert.equal(received.length, 0, 'no announcements without interest')
288
-
289
- sessionA.close()
290
- sessionB.close()
291
- await new Promise(r => wss.close(r))
292
- })
293
-
294
- test('announce reaches multiple interested peers', async ({ assert }) => {
295
- const { wss, port } = await startServer(new RepoRegistry())
296
- const topic = fakeKey(24)
297
- const announced = fakeKey(25)
298
-
299
- const receivedA = [], receivedB = []
300
- const sessionA = await registrySync(new RepoRegistry(), 'localhost', port, {
301
- onAnnounce: (key) => receivedA.push(key)
302
- })
303
- const sessionB = await registrySync(new RepoRegistry(), 'localhost', port, {
304
- onAnnounce: (key) => receivedB.push(key)
305
- })
306
- sessionA.interest(topic)
307
- sessionB.interest(topic)
308
-
309
- const sessionC = await registrySync(new RepoRegistry(), 'localhost', port)
310
- await new Promise(r => setTimeout(r, 50))
311
- sessionC.announce(announced, topic)
312
-
313
- await waitFor(() => receivedA.length === 1 && receivedB.length === 1)
314
- assert.equal(receivedA[0], announced)
315
- assert.equal(receivedB[0], announced)
316
-
317
- sessionA.close()
318
- sessionB.close()
319
- sessionC.close()
320
- await new Promise(r => wss.close(r))
321
- })
322
-
323
- test('announce is not echoed back to the sender', async ({ assert }) => {
324
- const { wss, port } = await startServer(new RepoRegistry())
325
- const topic = fakeKey(26)
326
- const announced = fakeKey(27)
327
-
328
- const received = []
329
- const session = await registrySync(new RepoRegistry(), 'localhost', port, {
330
- onAnnounce: (key) => received.push(key)
331
- })
332
- session.interest(topic)
333
-
334
- await new Promise(r => setTimeout(r, 50))
335
- session.announce(announced, topic) // sender declares interest and announces
336
-
337
- await new Promise(r => setTimeout(r, 100))
338
- assert.equal(received.length, 0, 'sender should not receive its own announcement')
339
-
340
- session.close()
341
- await new Promise(r => wss.close(r))
342
- })
343
-
344
- test('after disconnect, interest is cleaned up and announcements stop', async ({ assert }) => {
345
- const { wss, port } = await startServer(new RepoRegistry())
346
- const topic = fakeKey(28)
347
- const announced = fakeKey(29)
348
-
349
- const received = []
350
- const sessionA = await registrySync(new RepoRegistry(), 'localhost', port, {
351
- onAnnounce: (key) => received.push(key)
352
- })
353
- sessionA.interest(topic)
354
-
355
- const sessionB = await registrySync(new RepoRegistry(), 'localhost', port)
356
-
357
- // Confirm routing works before disconnect
358
- await new Promise(r => setTimeout(r, 50))
359
- sessionB.announce(announced, topic)
360
- await waitFor(() => received.length === 1)
361
-
362
- // Disconnect A, then announce again
363
- sessionA.close()
364
- await new Promise(r => setTimeout(r, 50))
365
- sessionB.announce(announced, topic)
366
-
367
- await new Promise(r => setTimeout(r, 100))
368
- assert.equal(received.length, 1, 'no more announcements after disconnect')
369
-
370
- sessionB.close()
371
- await new Promise(r => wss.close(r))
372
- })
373
- })
@@ -1,144 +0,0 @@
1
- import { describe } from './utils/testing.js'
2
- import { RepoRegistry as StreamRegistry } from './RepoRegistry.js'
3
- import { outletSync } from './outletSync.js'
4
- import { originSync } from './originSync.js'
5
-
6
- const KEY = 'aabbccddeeff0011'
7
-
8
- // Wait until predicate(stream) returns true.
9
- // Accesses stream.byteLength explicitly so the watcher re-runs on every append,
10
- // regardless of whether the predicate itself touches a reactive path.
11
- function waitFor (stream, predicate, timeout = 2000) {
12
- return new Promise((resolve, reject) => {
13
- const t = setTimeout(() => reject(new Error('timeout')), timeout)
14
- let done = false
15
- stream.watch('waitFor', () => {
16
- if (!done && predicate(stream)) {
17
- done = true
18
- clearTimeout(t)
19
- resolve()
20
- }
21
- })
22
- })
23
- }
24
-
25
- describe(import.meta.url, ({ test }) => {
26
- test('outlet syncs existing stream data to a new origin', async ({ assert }) => {
27
- const serverRegistry = new StreamRegistry()
28
- const serverStream = await serverRegistry.open(KEY)
29
- serverStream.set({ hello: 'world' })
30
-
31
- const wss = outletSync(serverRegistry, 0)
32
- await new Promise(resolve => wss.on('listening', resolve))
33
- const { port } = wss.address()
34
-
35
- const clientRegistry = new StreamRegistry()
36
- const clientStream = await clientRegistry.open(KEY)
37
- const ws = await originSync(clientStream, KEY, 'localhost', port)
38
-
39
- await waitFor(clientStream, s => s.get('hello') === 'world')
40
- assert.equal(clientStream.get('hello'), 'world', 'client received server data')
41
-
42
- ws.close()
43
- for (const c of wss.clients) c.terminate()
44
- wss.close()
45
- })
46
-
47
- test('origin syncs local data up to the outlet', async ({ assert }) => {
48
- const serverRegistry = new StreamRegistry()
49
- const wss = outletSync(serverRegistry, 0)
50
- await new Promise(resolve => wss.on('listening', resolve))
51
- const { port } = wss.address()
52
-
53
- const clientRegistry = new StreamRegistry()
54
- const clientStream = await clientRegistry.open(KEY)
55
- clientStream.set({ from: 'client' })
56
-
57
- const ws = await originSync(clientStream, KEY, 'localhost', port)
58
- const serverStream = await serverRegistry.open(KEY)
59
-
60
- await waitFor(serverStream, s => s.get('from') === 'client')
61
- assert.equal(serverStream.get('from'), 'client', 'server received client data')
62
-
63
- ws.close()
64
- for (const c of wss.clients) c.terminate()
65
- wss.close()
66
- })
67
-
68
- test('two origins converge on the same byte stream', async ({ assert }) => {
69
- const serverRegistry = new StreamRegistry()
70
- const wss = outletSync(serverRegistry, 0)
71
- await new Promise(resolve => wss.on('listening', resolve))
72
- const { port } = wss.address()
73
-
74
- const r1 = new StreamRegistry()
75
- const r2 = new StreamRegistry()
76
- const s1 = await r1.open(KEY)
77
- const s2 = await r2.open(KEY)
78
-
79
- s1.set({ x: 1 })
80
- s2.set({ x: 2 })
81
-
82
- // NOTE: these are bare Streamos, not Repos — no commit records, no parent
83
- // pointers. The two streams have conflicting chunks at the same byte
84
- // offsets; both arrive at the server and each other via dedup-append.
85
- // byteLength convergence is all we can assert here: the merged stream
86
- // contains all unique chunks from both writers but the second writer's
87
- // value address is no longer valid in the merged layout. This is a known
88
- // limitation; see ROADMAP "multi-device write conflict detection".
89
- const ws1 = await originSync(s1, KEY, 'localhost', port)
90
- const ws2 = await originSync(s2, KEY, 'localhost', port)
91
-
92
- const serverStream = await serverRegistry.open(KEY)
93
- await waitFor(serverStream, s => s.byteLength >= s1.byteLength && s.byteLength >= s2.byteLength)
94
- await waitFor(s1, s => s.byteLength >= serverStream.byteLength)
95
- await waitFor(s2, s => s.byteLength >= serverStream.byteLength)
96
-
97
- assert.equal(s1.byteLength, s2.byteLength, 'both clients converged to same byteLength')
98
- assert.equal(s1.byteLength, serverStream.byteLength, 'clients match server')
99
-
100
- ws1.close(); ws2.close()
101
- for (const c of wss.clients) c.terminate()
102
- wss.close()
103
- })
104
-
105
- test('relay forwards data between server and client without writing its own commits', async ({ assert }) => {
106
- // Server
107
- const serverRegistry = new StreamRegistry()
108
- const serverStream = await serverRegistry.open(KEY)
109
- serverStream.set({ hello: 'from-server' })
110
- const serverWss = outletSync(serverRegistry, 0)
111
- await new Promise(resolve => serverWss.on('listening', resolve))
112
- const serverPort = serverWss.address().port
113
-
114
- // Relay: originSync upstream to server, outletSync downstream for clients.
115
- // The relay never calls set() or commit() — it only accumulates and re-serves
116
- // the byte stream it receives.
117
- const relayRegistry = new StreamRegistry()
118
- const relayStream = await relayRegistry.open(KEY)
119
- await originSync(relayStream, KEY, 'localhost', serverPort)
120
- const relayWss = outletSync(relayRegistry, 0)
121
- await new Promise(resolve => relayWss.on('listening', resolve))
122
- const relayPort = relayWss.address().port
123
-
124
- // Client connects to relay only — no direct server connection
125
- const clientRegistry = new StreamRegistry()
126
- const clientStream = await clientRegistry.open(KEY)
127
- const clientWs = await originSync(clientStream, KEY, 'localhost', relayPort)
128
-
129
- // Server data reaches client via relay
130
- await waitFor(clientStream, s => s.get('hello') === 'from-server')
131
- assert.equal(clientStream.get('hello'), 'from-server', 'relay forwarded server data to client')
132
-
133
- // Client data propagates back through relay to server
134
- clientStream.set({ hello: 'from-client' })
135
- await waitFor(serverStream, s => s.get('hello') === 'from-client')
136
- assert.equal(serverStream.get('hello'), 'from-client', 'relay forwarded client data to server')
137
-
138
- clientWs.close()
139
- for (const c of relayWss.clients) c.terminate()
140
- relayWss.close()
141
- for (const c of serverWss.clients) c.terminate()
142
- serverWss.close()
143
- })
144
- })
@@ -1,113 +0,0 @@
1
- /**
2
- * Minimal DOM mock for testing mount() in Node.
3
- *
4
- * Implements only the subset of DOM APIs that hx.js mount() actually calls:
5
- * - createElement / createElementNS / createTextNode / createComment / createDocumentFragment
6
- * - appendChild, remove, before, nextSibling, childNodes
7
- * - setAttribute, getAttribute, removeAttribute, toggleAttribute
8
- * - textContent (read)
9
- */
10
-
11
- class MockNode {
12
- #children = []
13
- #parent = null
14
-
15
- constructor (nodeType) {
16
- this.nodeType = nodeType
17
- }
18
-
19
- get childNodes () { return [...this.#children] }
20
- get parentNode () { return this.#parent }
21
-
22
- get textContent () {
23
- return this.#children.map(c => c.textContent).join('')
24
- }
25
-
26
- get nextSibling () {
27
- if (!this.#parent) return null
28
- const sibs = this.#parent.#children
29
- return sibs[sibs.indexOf(this) + 1] ?? null
30
- }
31
-
32
- appendChild (child) {
33
- if (child.#parent) child.remove()
34
- child.#parent = this
35
- this.#children.push(child)
36
- return child
37
- }
38
-
39
- remove () {
40
- if (!this.#parent) return
41
- this.#parent.#children = this.#parent.#children.filter(c => c !== this)
42
- this.#parent = null
43
- }
44
-
45
- before (...nodes) {
46
- if (!this.#parent) return
47
- const i = this.#parent.#children.indexOf(this)
48
- const toInsert = []
49
- for (const n of nodes) {
50
- if (n.nodeType === 11) { // DocumentFragment
51
- const cs = [...n.#children]
52
- n.#children = []
53
- cs.forEach(c => { c.#parent = this.#parent })
54
- toInsert.push(...cs)
55
- } else {
56
- if (n.#parent) n.remove()
57
- n.#parent = this.#parent
58
- toInsert.push(n)
59
- }
60
- }
61
- this.#parent.#children.splice(i, 0, ...toInsert)
62
- }
63
- }
64
-
65
- class MockElement extends MockNode {
66
- #attrs = {}
67
-
68
- constructor (tag) {
69
- super(1)
70
- this.tag = tag
71
- }
72
-
73
- setAttribute (name, val) { this.#attrs[name] = String(val) }
74
- getAttribute (name) { return this.#attrs[name] ?? null }
75
- removeAttribute (name) { delete this.#attrs[name] }
76
- toggleAttribute (name, force) {
77
- if (force) this.#attrs[name] = ''
78
- else delete this.#attrs[name]
79
- }
80
- addEventListener () {}
81
- }
82
-
83
- class MockText extends MockNode {
84
- constructor (value) {
85
- super(3)
86
- this.nodeValue = value
87
- }
88
-
89
- get textContent () { return this.nodeValue }
90
- }
91
-
92
- class MockComment extends MockNode {
93
- constructor (value) {
94
- super(8)
95
- this.nodeValue = value
96
- }
97
-
98
- get textContent () { return '' }
99
- }
100
-
101
- class MockFragment extends MockNode {
102
- constructor () { super(11) }
103
- }
104
-
105
- export const mockDocument = {
106
- createElement: tag => new MockElement(tag),
107
- createElementNS: (_ns, tag) => new MockElement(tag),
108
- createTextNode: text => new MockText(text),
109
- createComment: text => new MockComment(text),
110
- createDocumentFragment: () => new MockFragment()
111
- }
112
-
113
- export { MockNode }