Skip to content

Commit e34fd20

Browse files
authored
feat(core): add collision detection and update methods (#103)
1 parent 829cbda commit e34fd20

File tree

7 files changed

+629
-0
lines changed

7 files changed

+629
-0
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import type { DevToolsDockEntry, DevToolsNodeContext } from '@vitejs/devtools-kit'
2+
import { describe, expect, it } from 'vitest'
3+
import { DevToolsDockHost } from '../host-docks'
4+
5+
describe('devToolsDockHost', () => {
6+
const mockContext = {} as DevToolsNodeContext
7+
8+
describe('register() collision detection', () => {
9+
it('should register a new dock successfully', () => {
10+
const host = new DevToolsDockHost(mockContext)
11+
const dock: DevToolsDockEntry = {
12+
type: 'iframe',
13+
id: 'test-dock',
14+
title: 'Test Dock',
15+
icon: 'test-icon',
16+
url: 'http://localhost:3000',
17+
}
18+
19+
expect(() => host.register(dock)).not.toThrow()
20+
expect(host.views.has('test-dock')).toBe(true)
21+
})
22+
23+
it('should throw error when registering duplicate dock ID', () => {
24+
const host = new DevToolsDockHost(mockContext)
25+
const dock1: DevToolsDockEntry = {
26+
type: 'iframe',
27+
id: 'duplicate-dock',
28+
title: 'First Dock',
29+
icon: 'icon1',
30+
url: 'http://localhost:3001',
31+
}
32+
const dock2: DevToolsDockEntry = {
33+
type: 'iframe',
34+
id: 'duplicate-dock',
35+
title: 'Second Dock',
36+
icon: 'icon2',
37+
url: 'http://localhost:3002',
38+
}
39+
40+
host.register(dock1)
41+
42+
expect(() => host.register(dock2)).toThrow()
43+
expect(() => host.register(dock2)).toThrow('duplicate-dock')
44+
expect(() => host.register(dock2)).toThrow('already registered')
45+
})
46+
47+
it('should include the duplicate ID in error message', () => {
48+
const host = new DevToolsDockHost(mockContext)
49+
const dock: DevToolsDockEntry = {
50+
type: 'webcomponent',
51+
id: 'my-special-panel',
52+
title: 'Special Panel',
53+
icon: 'special',
54+
from: './component.js',
55+
import: 'MyComponent',
56+
}
57+
58+
host.register(dock)
59+
60+
expect(() => host.register(dock)).toThrow('my-special-panel')
61+
})
62+
63+
it('should allow different dock IDs', () => {
64+
const host = new DevToolsDockHost(mockContext)
65+
66+
host.register({
67+
type: 'iframe',
68+
id: 'dock-1',
69+
title: 'Dock 1',
70+
icon: 'icon1',
71+
url: 'http://localhost:3001',
72+
})
73+
74+
host.register({
75+
type: 'iframe',
76+
id: 'dock-2',
77+
title: 'Dock 2',
78+
icon: 'icon2',
79+
url: 'http://localhost:3002',
80+
})
81+
82+
expect(host.views.size).toBe(2)
83+
})
84+
})
85+
86+
describe('update() existence validation', () => {
87+
it('should throw error when updating non-existent dock', () => {
88+
const host = new DevToolsDockHost(mockContext)
89+
const dock: DevToolsDockEntry = {
90+
type: 'iframe',
91+
id: 'nonexistent',
92+
title: 'Does Not Exist',
93+
icon: 'icon',
94+
url: 'http://localhost:3000',
95+
}
96+
97+
expect(() => host.update(dock)).toThrow()
98+
expect(() => host.update(dock)).toThrow('nonexistent')
99+
expect(() => host.update(dock)).toThrow('not registered')
100+
expect(() => host.update(dock)).toThrow('Use register()')
101+
})
102+
103+
it('should update existing dock successfully', () => {
104+
const host = new DevToolsDockHost(mockContext)
105+
const dock1: DevToolsDockEntry = {
106+
type: 'iframe',
107+
id: 'update-test',
108+
title: 'Original Title',
109+
icon: 'original',
110+
url: 'http://localhost:3001',
111+
}
112+
const dock2: DevToolsDockEntry = {
113+
type: 'iframe',
114+
id: 'update-test',
115+
title: 'Updated Title',
116+
icon: 'updated',
117+
url: 'http://localhost:3002',
118+
}
119+
120+
host.register(dock1)
121+
expect(() => host.update(dock2)).not.toThrow()
122+
123+
const updated = host.views.get('update-test')
124+
expect(updated?.title).toBe('Updated Title')
125+
if (updated?.type === 'iframe') {
126+
expect(updated.url).toBe('http://localhost:3002')
127+
}
128+
})
129+
130+
it('should validate that update only works on existing entries', () => {
131+
const host = new DevToolsDockHost(mockContext)
132+
133+
// Register one dock
134+
host.register({
135+
type: 'iframe',
136+
id: 'exists',
137+
title: 'Exists',
138+
icon: 'icon',
139+
url: 'http://localhost:3000',
140+
})
141+
142+
// Update should work for existing
143+
expect(() =>
144+
host.update({
145+
type: 'iframe',
146+
id: 'exists',
147+
title: 'Updated',
148+
icon: 'icon',
149+
url: 'http://localhost:3001',
150+
}),
151+
).not.toThrow()
152+
153+
// Update should fail for non-existing
154+
expect(() =>
155+
host.update({
156+
type: 'iframe',
157+
id: 'does-not-exist',
158+
title: 'Failed',
159+
icon: 'icon',
160+
url: 'http://localhost:3002',
161+
}),
162+
).toThrow()
163+
})
164+
165+
it('should preserve dock in values() after update', () => {
166+
const host = new DevToolsDockHost(mockContext)
167+
168+
host.register({
169+
type: 'iframe',
170+
id: 'test',
171+
title: 'Original',
172+
icon: 'icon',
173+
url: 'http://localhost:3000',
174+
})
175+
176+
host.update({
177+
type: 'iframe',
178+
id: 'test',
179+
title: 'Updated',
180+
icon: 'newicon',
181+
url: 'http://localhost:3001',
182+
})
183+
184+
const docks = host.values()
185+
expect(docks.length).toBe(1)
186+
expect(docks[0]?.title).toBe('Updated')
187+
})
188+
})
189+
})
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import type { DevToolsNodeContext, RpcFunctionDefinition } from '@vitejs/devtools-kit'
2+
import { describe, expect, it } from 'vitest'
3+
import { RpcFunctionsHost } from '../host-functions'
4+
5+
async function emptyHandler() { /* empty */ }
6+
const returnFirst = async () => 'first'
7+
const returnSecond = async () => 'second'
8+
const returnV1 = async () => 'v1'
9+
const returnV2 = async () => 'v2'
10+
const setupWith = <T>(handler: () => Promise<T>) => async () => ({ handler })
11+
12+
describe('rpcFunctionsHost', () => {
13+
const mockContext = {} as DevToolsNodeContext
14+
15+
describe('register() collision detection', () => {
16+
it('should register a new RPC function successfully', () => {
17+
const host = new RpcFunctionsHost(mockContext)
18+
const fn: RpcFunctionDefinition<string, any, any, any> = {
19+
name: 'test-function',
20+
type: 'action',
21+
setup: setupWith(emptyHandler),
22+
}
23+
24+
expect(() => host.register(fn)).not.toThrow()
25+
expect(host.definitions.has('test-function')).toBe(true)
26+
})
27+
28+
it('should throw error when registering duplicate RPC function ID', () => {
29+
const host = new RpcFunctionsHost(mockContext)
30+
const fn1: RpcFunctionDefinition<string, any, any, any> = {
31+
name: 'duplicate-fn',
32+
type: 'action',
33+
setup: setupWith(returnFirst),
34+
}
35+
const fn2: RpcFunctionDefinition<string, any, any, any> = {
36+
name: 'duplicate-fn',
37+
type: 'action',
38+
setup: setupWith(returnSecond),
39+
}
40+
41+
host.register(fn1)
42+
43+
const registerDuplicate = () => host.register(fn2)
44+
expect(registerDuplicate).toThrow()
45+
expect(registerDuplicate).toThrow('duplicate-fn')
46+
expect(registerDuplicate).toThrow('already registered')
47+
})
48+
49+
it('should include the duplicate ID in error message', () => {
50+
const host = new RpcFunctionsHost(mockContext)
51+
const fn: RpcFunctionDefinition<string, any, any, any> = {
52+
name: 'my-special-function',
53+
type: 'query',
54+
setup: setupWith(emptyHandler),
55+
}
56+
57+
host.register(fn)
58+
59+
const registerAgain = () => host.register(fn)
60+
expect(registerAgain).toThrow('my-special-function')
61+
})
62+
})
63+
64+
describe('update() existence validation', () => {
65+
it('should throw error when updating non-existent RPC function', () => {
66+
const host = new RpcFunctionsHost(mockContext)
67+
const fn: RpcFunctionDefinition<string, any, any, any> = {
68+
name: 'nonexistent',
69+
type: 'action',
70+
setup: setupWith(emptyHandler),
71+
}
72+
73+
const updateNonexistent = () => host.update(fn)
74+
expect(updateNonexistent).toThrow()
75+
expect(updateNonexistent).toThrow('nonexistent')
76+
expect(updateNonexistent).toThrow('not registered')
77+
expect(updateNonexistent).toThrow('Use register()')
78+
})
79+
80+
it('should update existing RPC function successfully', () => {
81+
const host = new RpcFunctionsHost(mockContext)
82+
const fn1: RpcFunctionDefinition<string, any, any, any> = {
83+
name: 'update-test',
84+
type: 'action',
85+
setup: setupWith(returnV1),
86+
}
87+
const fn2: RpcFunctionDefinition<string, any, any, any> = {
88+
name: 'update-test',
89+
type: 'action',
90+
setup: setupWith(returnV2),
91+
}
92+
93+
host.register(fn1)
94+
const doUpdate = () => host.update(fn2)
95+
expect(doUpdate).not.toThrow()
96+
97+
const updated = host.definitions.get('update-test')
98+
expect(updated).toBe(fn2)
99+
})
100+
101+
it('should validate that update only works on existing entries', () => {
102+
const host = new RpcFunctionsHost(mockContext)
103+
104+
// Register one function
105+
host.register({
106+
name: 'exists',
107+
type: 'action',
108+
setup: setupWith(emptyHandler),
109+
})
110+
111+
// Update should work for existing
112+
const updateExisting = () =>
113+
host.update({
114+
name: 'exists',
115+
type: 'action',
116+
setup: setupWith(emptyHandler),
117+
})
118+
expect(updateExisting).not.toThrow()
119+
120+
// Update should fail for non-existing
121+
const updateMissing = () =>
122+
host.update({
123+
name: 'does-not-exist',
124+
type: 'action',
125+
setup: setupWith(emptyHandler),
126+
})
127+
expect(updateMissing).toThrow()
128+
})
129+
})
130+
})

0 commit comments

Comments
 (0)