-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Expand file tree
/
Copy pathcell.spec.ts
More file actions
317 lines (273 loc) · 9.02 KB
/
cell.spec.ts
File metadata and controls
317 lines (273 loc) · 9.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { Cell } from '../../src/model/cell'
class FakeModel {
map: Record<string, any> = {}
batches: any[] = []
notify = vi.fn()
startBatch = vi.fn((name: any, data: any) => {
this.batches.push({ name, data })
})
stopBatch = vi.fn((name: any, data: any) => {
this.batches.push({ name: `stop:${name}`, data })
})
addCell = vi.fn((cell: any) => {
this.map[cell.id] = cell
})
removeCell = vi.fn((cell: any) => {
delete this.map[cell.id]
})
getCell(id: string) {
return this.map[id] || null
}
getMaxZIndex() {
return 100
}
getMinZIndex() {
return -100
}
indexOf(cell: any) {
const keys = Object.keys(this.map)
return keys.indexOf(cell.id)
}
total() {
return Object.keys(this.map).length
}
getIncomingEdges() {
return null
}
getOutgoingEdges() {
return null
}
findViewByCell() {
return null
}
}
describe('Cell core API', () => {
let model: FakeModel
beforeEach(() => {
model = new FakeModel()
})
it('generate id when missing', () => {
const c = new Cell({})
expect(typeof c.id).toBe('string')
expect(c.id.length).toBeGreaterThan(0)
})
it('static config and propHook applied in preprocess', () => {
class TestCell extends Cell {}
TestCell.config({
propHooks: (metadata: any) => {
if (metadata.tools) {
metadata.tools =
typeof metadata.tools === 'string'
? { items: [metadata.tools] }
: metadata.tools
}
return metadata
},
})
const t = new TestCell({ tools: 'toolA' } as any)
const tools = t.getPropByPath('tools')
expect(tools).toBeDefined()
expect((tools as any).items).toBeDefined()
expect((tools as any).items[0]).toBe('toolA')
})
it('getMarkup falls back to static markup', () => {
class MarkupCell extends Cell {}
const sampleMarkup = [{ tagName: 'rect' }]
MarkupCell.config({ markup: sampleMarkup })
const mc = new MarkupCell({})
expect(mc.getMarkup()).toBe(sampleMarkup)
})
it('get/set prop works for string key and object', () => {
const c = new Cell({ view: 'v1' })
expect(c.getProp('view')).toBe('v1')
c.setProp('view', 'v2')
expect(c.getProp('view')).toBe('v2')
c.setProp({ view: 'v3', data: { a: 1 } } as any)
expect(c.getProp('view')).toBe('v3')
expect(c.getData()).toEqual({ a: 1 })
})
it('attrs API: setAttrs, getAttrByPath, setAttrByPath, removeAttrByPath', () => {
const c = new Cell({})
c.setAttrs({ body: { fill: 'red', stroke: { width: 1 } } })
expect(c.getAttrs().body.fill).toBe('red')
c.setAttrByPath('body/fill', 'blue')
expect(c.getAttrByPath('body/fill')).toBe('blue')
c.updateAttrs({ body: { opacity: 0.5 } })
expect(c.getAttrByPath('body/opacity')).toBe(0.5)
c.removeAttrByPath('body/fill')
expect(c.getAttrByPath('body/fill')).toBeUndefined()
})
it('visibility API: isVisible show hide toggleVisible', () => {
const c = new Cell({})
expect(c.isVisible()).toBe(true)
c.hide()
expect(c.isVisible()).toBe(false)
c.show()
expect(c.isVisible()).toBe(true)
c.toggleVisible()
expect(c.isVisible()).toBe(false)
c.toggleVisible(true)
expect(c.isVisible()).toBe(true)
})
it('data API: setData, replaceData, updateData, removeData', () => {
const c = new Cell({ data: { n: 1 } })
expect(c.getData()).toEqual({ n: 1 })
c.updateData({ m: 2 })
expect(c.getData()).toEqual(expect.objectContaining({ n: 1, m: 2 }))
c.replaceData({ x: 9 })
expect(c.getData()).toEqual({ x: 9 })
c.setData({ nested: { a: 1 } })
c.setData({ nested: { b: 2 } }, { deep: true })
expect(c.getData().nested).toEqual({ a: 1, b: 2 })
c.removeData()
expect(c.getData()).toBeUndefined()
})
it('setParent and setChildren update store and internal references', () => {
const p = new Cell({})
const c1 = new Cell({})
const c2 = new Cell({})
p.model = model as any
c1.model = model as any
c2.model = model as any
model.addCell(c1)
model.addCell(c2)
p.setChildren([c1, c2])
expect(p.getChildCount()).toBe(2)
expect(p.children![0]).toBe(c1)
c1.setParent(null)
expect(c1.getParent()).toBeNull()
p.setChildren(null)
expect(p.getChildCount()).toBe(0)
})
it('remove clears parent children ids to avoid duplicates when recreating same id', () => {
const parent = new Cell({ id: 'parent' } as any)
const child = new Cell({ id: 'child' } as any)
parent.model = model as any
child.model = model as any
model.addCell(parent)
model.addCell(child)
parent.addChild(child)
expect((parent.getProp('children') as any as string[])?.length).toBe(1)
child.remove()
expect(parent.getProp('children')).toBeUndefined()
const recreated = new Cell({ id: 'child' } as any)
recreated.model = model as any
model.addCell(recreated)
parent.addChild(recreated)
expect(parent.getChildren()?.map((c) => c.id)).toEqual(['child'])
})
it('removeCells on parent removes children even when model is null (simulates collection flow)', () => {
const parent = new Cell({ id: 'parent' } as any)
const child1 = new Cell({ id: 'child1' } as any)
const child2 = new Cell({ id: 'child2' } as any)
parent.model = model as any
child1.model = model as any
child2.model = model as any
model.addCell(parent)
model.addCell(child1)
model.addCell(child2)
parent.addChild(child1)
parent.addChild(child2)
expect((parent.getProp('children') as any as string[])?.length).toBe(2)
// Simulate what collection.removeCells does:
// it sets model=null before calling cell.remove()
parent.model = null
parent.remove()
// Children should have been removed from the model
expect(model.map['child1']).toBeUndefined()
expect(model.map['child2']).toBeUndefined()
})
it('tools APIs: normalizeTools, set/get/add/remove/has', () => {
expect(Cell.normalizeTools('a').items[0]).toBe('a')
expect(Cell.normalizeTools(['a', 'b']).items.length).toBe(2)
const toolsObj = { name: 't', items: ['x'] as any[] }
expect(Cell.normalizeTools(toolsObj)).toBe(toolsObj)
const c = new Cell({})
c.setTools({ name: 'main', items: ['one', { name: 'two' }] })
expect(c.getTools()).toBeDefined()
expect(c.hasTools()).toBe(true)
expect(c.hasTools('main')).toBe(true)
expect(c.hasTool('one')).toBe(true)
expect(c.hasTool('two')).toBe(true)
c.addTools('three', 'main')
expect(c.hasTool('three')).toBe(true)
c.removeTool('one')
expect(c.hasTool('one')).toBe(false)
c.removeTool(0)
expect(c.getTools()).toBeTruthy()
c.removeTools()
expect(c.getTools()).toBeFalsy()
})
it('isCell recognizes a compatible object', () => {
const fake = {
[Symbol.toStringTag]: Cell.toStringTag,
isNode: () => false,
isEdge: () => false,
prop: () => ({}),
attr: () => ({}),
}
expect(Cell.isCell(fake)).toBe(true)
expect(Cell.isCell(null)).toBe(false)
expect(Cell.isCell({})).toBe(false)
})
it('clone works shallow and toJSON returns expected shape when provided', () => {
const n = new Cell({ shape: 'rect' })
n.setProp('view', 'v1')
const clone = n.clone()
expect(clone).not.toBe(n)
expect(clone.id).not.toBe(n.id)
expect(clone.getProp('shape')).toBe('rect')
const json = n.toJSON()
expect(typeof json).toBe('object')
expect((json as any).shape).toBe('rect')
})
it('notify delegates to model.notify and triggers batch pairing', () => {
const c = new Cell({})
c.model = model as any
c.notify('changed', { options: {}, cell: c } as any)
expect(model.notify).toHaveBeenCalled()
if (typeof c.startBatch === 'function') {
c.startBatch('test' as any, { a: 1 })
expect(model.startBatch).toHaveBeenCalled()
}
if (typeof c.stopBatch === 'function') {
c.stopBatch('test' as any, { a: 1 })
expect(model.stopBatch).toHaveBeenCalled()
}
const res =
typeof c.batchUpdate === 'function'
? c.batchUpdate('b' as any, () => 'ok', { x: 1 })
: 'ok'
expect(res).toBe('ok')
})
it('zIndex getters/setters and removal', () => {
const c = new Cell({})
c.setZIndex(5)
expect(c.getZIndex()).toBe(5)
c.removeZIndex()
expect(c.getZIndex()).toBeUndefined()
c.zIndex = 3
expect(c.getZIndex()).toBe(3)
c.zIndex = undefined
expect(c.getZIndex()).toBeUndefined()
})
it('transition proxies to animation start/stop/get without throwing', () => {
const c = new Cell({})
const anyCell = c as any
if (typeof anyCell.transition === 'function') {
const stop = anyCell.transition('data', 1)
expect(typeof stop).toBe('function')
}
expect(() =>
typeof anyCell.getTransitions === 'function'
? anyCell.getTransitions()
: undefined,
).not.toThrow()
expect(() =>
typeof anyCell.stopTransition === 'function'
? anyCell.stopTransition('data')
: undefined,
).not.toThrow()
})
})