archetype-ecs 1.2.0 → 2.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.
@@ -0,0 +1,499 @@
1
+ import { describe, it, beforeEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createEntityManager } from '../src/EntityManager.js';
4
+ import { component } from '../src/index.js';
5
+ describe('EntityManager', () => {
6
+ let em;
7
+ const Position = component('Position', 'f32', ['x', 'y']);
8
+ const Velocity = component('Velocity', 'f32', ['vx', 'vy']);
9
+ const Health = component('Health', 'f32', ['hp']);
10
+ beforeEach(() => {
11
+ em = createEntityManager();
12
+ });
13
+ describe('createEntity / destroyEntity', () => {
14
+ it('creates entities with incrementing ids', () => {
15
+ const a = em.createEntity();
16
+ const b = em.createEntity();
17
+ assert.equal(b, a + 1);
18
+ });
19
+ it('destroyEntity removes entity', () => {
20
+ const id = em.createEntity();
21
+ em.addComponent(id, Position, { x: 0, y: 0 });
22
+ em.destroyEntity(id);
23
+ assert.deepEqual(em.getAllEntities(), []);
24
+ assert.equal(em.getComponent(id, Position), undefined);
25
+ });
26
+ });
27
+ describe('addComponent / getComponent / hasComponent', () => {
28
+ it('adds and retrieves a component', () => {
29
+ const id = em.createEntity();
30
+ em.addComponent(id, Position, { x: 1, y: 2 });
31
+ assert.deepEqual(em.getComponent(id, Position), { x: 1, y: 2 });
32
+ assert.equal(em.hasComponent(id, Position), true);
33
+ });
34
+ it('returns undefined for missing component', () => {
35
+ const id = em.createEntity();
36
+ assert.equal(em.getComponent(id, Position), undefined);
37
+ assert.equal(em.hasComponent(id, Position), false);
38
+ });
39
+ it('overwrites component data on duplicate add', () => {
40
+ const id = em.createEntity();
41
+ em.addComponent(id, Position, { x: 1, y: 2 });
42
+ em.addComponent(id, Position, { x: 10, y: 20 });
43
+ assert.deepEqual(em.getComponent(id, Position), { x: 10, y: 20 });
44
+ });
45
+ it('adds multiple component types', () => {
46
+ const id = em.createEntity();
47
+ em.addComponent(id, Position, { x: 0, y: 0 });
48
+ em.addComponent(id, Velocity, { vx: 1, vy: 1 });
49
+ assert.deepEqual(em.getComponent(id, Position), { x: 0, y: 0 });
50
+ assert.deepEqual(em.getComponent(id, Velocity), { vx: 1, vy: 1 });
51
+ });
52
+ });
53
+ describe('removeComponent', () => {
54
+ it('removes a component', () => {
55
+ const id = em.createEntity();
56
+ em.addComponent(id, Position, { x: 1, y: 2 });
57
+ em.addComponent(id, Velocity, { vx: 1, vy: 1 });
58
+ em.removeComponent(id, Position);
59
+ assert.equal(em.hasComponent(id, Position), false);
60
+ assert.equal(em.hasComponent(id, Velocity), true);
61
+ });
62
+ it('removing last component leaves entity alive but without archetype', () => {
63
+ const id = em.createEntity();
64
+ em.addComponent(id, Position, { x: 1, y: 2 });
65
+ em.removeComponent(id, Position);
66
+ assert.ok(em.getAllEntities().includes(id));
67
+ assert.equal(em.hasComponent(id, Position), false);
68
+ });
69
+ it('removing non-existent component is a no-op', () => {
70
+ const id = em.createEntity();
71
+ em.removeComponent(id, Position);
72
+ assert.ok(em.getAllEntities().includes(id));
73
+ });
74
+ });
75
+ describe('query', () => {
76
+ it('returns entities matching component types', () => {
77
+ const a = em.createEntity();
78
+ em.addComponent(a, Position, { x: 0, y: 0 });
79
+ em.addComponent(a, Velocity, { vx: 1, vy: 1 });
80
+ const b = em.createEntity();
81
+ em.addComponent(b, Position, { x: 5, y: 5 });
82
+ const result = em.query([Position, Velocity]);
83
+ assert.ok(result.includes(a));
84
+ assert.ok(!result.includes(b));
85
+ });
86
+ it('exclude types filters out entities', () => {
87
+ const a = em.createEntity();
88
+ em.addComponent(a, Position, { x: 0, y: 0 });
89
+ const b = em.createEntity();
90
+ em.addComponent(b, Position, { x: 1, y: 1 });
91
+ em.addComponent(b, Health, { hp: 100 });
92
+ const result = em.query([Position], [Health]);
93
+ assert.ok(result.includes(a));
94
+ assert.ok(!result.includes(b));
95
+ });
96
+ });
97
+ describe('createEntityWith', () => {
98
+ it('creates entity with multiple components at once', () => {
99
+ const id = em.createEntityWith(Position, { x: 3, y: 4 }, Velocity, { vx: 1, vy: 0 });
100
+ assert.deepEqual(em.getComponent(id, Position), { x: 3, y: 4 });
101
+ assert.deepEqual(em.getComponent(id, Velocity), { vx: 1, vy: 0 });
102
+ });
103
+ });
104
+ describe('count', () => {
105
+ it('counts entities matching query', () => {
106
+ const a = em.createEntity();
107
+ em.addComponent(a, Position, { x: 0, y: 0 });
108
+ const b = em.createEntity();
109
+ em.addComponent(b, Position, { x: 1, y: 1 });
110
+ em.addComponent(b, Velocity, { vx: 1, vy: 0 });
111
+ assert.equal(em.count([Position]), 2);
112
+ assert.equal(em.count([Position, Velocity]), 1);
113
+ });
114
+ });
115
+ describe('serialize / deserialize', () => {
116
+ const symbolToName = new Map([
117
+ [Position._sym, 'Position'],
118
+ [Velocity._sym, 'Velocity'],
119
+ [Health._sym, 'Health']
120
+ ]);
121
+ const nameToSymbol = { Position, Velocity, Health };
122
+ it('round-trips entities and components', () => {
123
+ const a = em.createEntity();
124
+ em.addComponent(a, Position, { x: 1, y: 2 });
125
+ em.addComponent(a, Velocity, { vx: 3, vy: 4 });
126
+ const b = em.createEntity();
127
+ em.addComponent(b, Position, { x: 5, y: 6 });
128
+ const data = em.serialize(symbolToName);
129
+ em.deserialize(data, nameToSymbol);
130
+ assert.deepEqual(em.getAllEntities().sort(), [a, b].sort());
131
+ assert.deepEqual(em.getComponent(a, Position), { x: 1, y: 2 });
132
+ assert.deepEqual(em.getComponent(a, Velocity), { vx: 3, vy: 4 });
133
+ assert.deepEqual(em.getComponent(b, Position), { x: 5, y: 6 });
134
+ });
135
+ it('strip components excludes component data but keeps entity', () => {
136
+ const a = em.createEntity();
137
+ em.addComponent(a, Position, { x: 1, y: 2 });
138
+ em.addComponent(a, Velocity, { vx: 3, vy: 4 });
139
+ const data = em.serialize(symbolToName, [Velocity]);
140
+ assert.equal(data.components['Velocity'], undefined);
141
+ assert.notEqual(data.components['Position'], undefined);
142
+ });
143
+ it('skip entities with component excludes entire entity', () => {
144
+ const a = em.createEntity();
145
+ em.addComponent(a, Position, { x: 1, y: 2 });
146
+ const b = em.createEntity();
147
+ em.addComponent(b, Position, { x: 5, y: 6 });
148
+ em.addComponent(b, Health, { hp: 100 });
149
+ const data = em.serialize(symbolToName, [], [Health]);
150
+ assert.ok(data.entities.includes(a));
151
+ assert.ok(!data.entities.includes(b));
152
+ });
153
+ it('custom serializers are used when provided', () => {
154
+ const Meta = component('Meta', { x: 'f32', y: 'f32', secret: 'i32' });
155
+ const metaSymbolToName = new Map([...symbolToName, [Meta._sym, 'Meta']]);
156
+ const a = em.createEntity();
157
+ em.addComponent(a, Meta, { x: 1, y: 2, secret: 42 });
158
+ const serializers = new Map([
159
+ ['Meta', (data) => ({ x: data.x, y: data.y })]
160
+ ]);
161
+ const result = em.serialize(metaSymbolToName, [], [], { serializers });
162
+ assert.deepEqual(result.components['Meta'][a], { x: 1, y: 2 });
163
+ assert.equal(result.components['Meta'][a].secret, undefined);
164
+ });
165
+ it('custom deserializers are used when provided', () => {
166
+ const Meta = component('Meta2', { x: 'f32', y: 'f32' });
167
+ const metaSymbolToName = new Map([...symbolToName, [Meta._sym, 'Meta2']]);
168
+ const metaNameToSymbol = { ...nameToSymbol, Meta2: Meta };
169
+ const a = em.createEntity();
170
+ em.addComponent(a, Meta, { x: 1, y: 2 });
171
+ const data = em.serialize(metaSymbolToName);
172
+ const deserializers = new Map([
173
+ ['Meta2', (compData) => ({ ...compData, restored: true })]
174
+ ]);
175
+ em.deserialize(data, metaNameToSymbol, { deserializers });
176
+ const result = em.getComponent(a, Meta);
177
+ assert.ok(Math.abs(result.x - 1) < 0.01);
178
+ assert.ok(Math.abs(result.y - 2) < 0.01);
179
+ });
180
+ it('deserialize clears previous state', () => {
181
+ const a = em.createEntity();
182
+ em.addComponent(a, Position, { x: 1, y: 2 });
183
+ em.deserialize({ nextId: 1, entities: [], components: {} }, nameToSymbol);
184
+ assert.deepEqual(em.getAllEntities(), []);
185
+ });
186
+ });
187
+ });
188
+ describe('Typed Components (SoA)', () => {
189
+ let em;
190
+ beforeEach(() => {
191
+ em = createEntityManager();
192
+ });
193
+ it('typed component round-trip (add/get)', () => {
194
+ const Pos = component('Pos', 'f32', ['x', 'y']);
195
+ const id = em.createEntity();
196
+ em.addComponent(id, Pos, { x: 1.5, y: 2.5 });
197
+ const result = em.getComponent(id, Pos);
198
+ assert.ok(Math.abs(result.x - 1.5) < 0.001);
199
+ assert.ok(Math.abs(result.y - 2.5) < 0.001);
200
+ });
201
+ it('growth past initial capacity (>64 entities)', () => {
202
+ const Pos = component('PosGrow', 'f32', ['x', 'y']);
203
+ const ids = [];
204
+ for (let i = 0; i < 100; i++) {
205
+ const id = em.createEntity();
206
+ em.addComponent(id, Pos, { x: i, y: i * 2 });
207
+ ids.push(id);
208
+ }
209
+ for (let i = 0; i < 100; i++) {
210
+ const result = em.getComponent(ids[i], Pos);
211
+ assert.ok(Math.abs(result.x - i) < 0.001);
212
+ assert.ok(Math.abs(result.y - i * 2) < 0.001);
213
+ }
214
+ });
215
+ it('swap-remove preserves typed data', () => {
216
+ const Pos = component('PosSwap', 'f32', ['x', 'y']);
217
+ const a = em.createEntity();
218
+ const b = em.createEntity();
219
+ const c = em.createEntity();
220
+ em.addComponent(a, Pos, { x: 1, y: 2 });
221
+ em.addComponent(b, Pos, { x: 3, y: 4 });
222
+ em.addComponent(c, Pos, { x: 5, y: 6 });
223
+ em.destroyEntity(a);
224
+ const resultB = em.getComponent(b, Pos);
225
+ assert.ok(Math.abs(resultB.x - 3) < 0.001);
226
+ const resultC = em.getComponent(c, Pos);
227
+ assert.ok(Math.abs(resultC.x - 5) < 0.001);
228
+ });
229
+ it('typed + tag on same entity', () => {
230
+ const Pos = component('PosMixed', 'f32', ['x', 'y']);
231
+ const Tag = component('Tag');
232
+ const id = em.createEntity();
233
+ em.addComponent(id, Pos, { x: 10, y: 20 });
234
+ em.addComponent(id, Tag, {});
235
+ const pos = em.getComponent(id, Pos);
236
+ assert.ok(Math.abs(pos.x - 10) < 0.001);
237
+ assert.equal(em.getComponent(id, Tag), undefined);
238
+ assert.equal(em.hasComponent(id, Tag), true);
239
+ });
240
+ it('forEach raw field access and mutation', () => {
241
+ const Pos = component('PosLoop', 'f32', ['x', 'y']);
242
+ const Vel = component('VelLoop', 'f32', ['vx', 'vy']);
243
+ for (let i = 0; i < 10; i++) {
244
+ const id = em.createEntity();
245
+ em.addComponent(id, Pos, { x: i, y: 0 });
246
+ em.addComponent(id, Vel, { vx: 1, vy: 2 });
247
+ }
248
+ em.forEach([Pos, Vel], (arch) => {
249
+ const px = arch.field(Pos.x);
250
+ const py = arch.field(Pos.y);
251
+ const vx = arch.field(Vel.vx);
252
+ const vy = arch.field(Vel.vy);
253
+ for (let i = 0; i < arch.count; i++) {
254
+ px[i] += vx[i];
255
+ py[i] += vy[i];
256
+ }
257
+ });
258
+ const ids = em.query([Pos, Vel]);
259
+ for (const id of ids) {
260
+ const pos = em.getComponent(id, Pos);
261
+ assert.ok(pos.x >= 1);
262
+ assert.ok(Math.abs(pos.y - 2) < 0.001);
263
+ }
264
+ });
265
+ it('serialize/deserialize round-trip with typed components', () => {
266
+ const Pos = component('PosSer', 'f32', ['x', 'y']);
267
+ const symbolToName = new Map([[Pos._sym, 'PosSer']]);
268
+ const nameToSymbol = { PosSer: Pos };
269
+ const a = em.createEntity();
270
+ em.addComponent(a, Pos, { x: 1.5, y: 2.5 });
271
+ const b = em.createEntity();
272
+ em.addComponent(b, Pos, { x: 3.5, y: 4.5 });
273
+ const data = em.serialize(symbolToName);
274
+ em.deserialize(data, nameToSymbol);
275
+ const posA = em.getComponent(a, Pos);
276
+ assert.ok(Math.abs(posA.x - 1.5) < 0.01);
277
+ const posB = em.getComponent(b, Pos);
278
+ assert.ok(Math.abs(posB.x - 3.5) < 0.01);
279
+ });
280
+ it('archetype migration with typed components', () => {
281
+ const Pos = component('PosMig', 'f32', ['x', 'y']);
282
+ const Vel = component('VelMig', 'f32', ['vx', 'vy']);
283
+ const id = em.createEntity();
284
+ em.addComponent(id, Pos, { x: 5, y: 10 });
285
+ em.addComponent(id, Vel, { vx: 1, vy: 2 });
286
+ const pos = em.getComponent(id, Pos);
287
+ assert.ok(Math.abs(pos.x - 5) < 0.001);
288
+ em.removeComponent(id, Vel);
289
+ const pos2 = em.getComponent(id, Pos);
290
+ assert.ok(Math.abs(pos2.x - 5) < 0.001);
291
+ assert.equal(em.hasComponent(id, Vel), false);
292
+ });
293
+ it('overwrite typed component data in-place', () => {
294
+ const Pos = component('PosOw', 'f32', ['x', 'y']);
295
+ const id = em.createEntity();
296
+ em.addComponent(id, Pos, { x: 1, y: 2 });
297
+ em.addComponent(id, Pos, { x: 99, y: 88 });
298
+ const result = em.getComponent(id, Pos);
299
+ assert.ok(Math.abs(result.x - 99) < 0.001);
300
+ });
301
+ it('get/set for zero-allocation field access', () => {
302
+ const Pos = component('PosGS', 'f32', ['x', 'y']);
303
+ const id = em.createEntity();
304
+ em.addComponent(id, Pos, { x: 3.5, y: 7.5 });
305
+ assert.ok(Math.abs(em.get(id, Pos.x) - 3.5) < 0.001);
306
+ em.set(id, Pos.x, 42);
307
+ assert.ok(Math.abs(em.get(id, Pos.x) - 42) < 0.001);
308
+ });
309
+ it('get returns undefined for missing entity/component', () => {
310
+ const Pos = component('PosGFM', 'f32', ['x', 'y']);
311
+ assert.equal(em.get(999, Pos.x), undefined);
312
+ const id = em.createEntity();
313
+ assert.equal(em.get(id, Pos.x), undefined);
314
+ });
315
+ it('forEach field returns undefined for tag component', () => {
316
+ const Tag = component('TagFE');
317
+ const Pos = component('PosFE', 'f32', ['x', 'y']);
318
+ const id = em.createEntity();
319
+ em.addComponent(id, Pos, { x: 1, y: 2 });
320
+ em.addComponent(id, Tag, {});
321
+ em.forEach([Pos, Tag], (arch) => {
322
+ assert.ok(arch.field(Pos.x) instanceof Float32Array);
323
+ });
324
+ });
325
+ it('string component round-trip (add/get/set)', () => {
326
+ const Name = component('NameRT', { name: 'string', title: 'string' });
327
+ const id = em.createEntity();
328
+ em.addComponent(id, Name, { name: 'Hero', title: 'Sir' });
329
+ assert.equal(em.get(id, Name.name), 'Hero');
330
+ assert.equal(em.get(id, Name.title), 'Sir');
331
+ em.set(id, Name.name, 'Villain');
332
+ assert.equal(em.get(id, Name.name), 'Villain');
333
+ const obj = em.getComponent(id, Name);
334
+ assert.deepEqual(obj, { name: 'Villain', title: 'Sir' });
335
+ });
336
+ it('string component short form', () => {
337
+ const Label = component('LabelSF', 'string', ['text', 'color']);
338
+ const id = em.createEntity();
339
+ em.addComponent(id, Label, { text: 'hello', color: 'red' });
340
+ assert.equal(em.get(id, Label.text), 'hello');
341
+ assert.equal(em.get(id, Label.color), 'red');
342
+ });
343
+ it('string component growth past capacity', () => {
344
+ const Name = component('NameGrow', 'string', ['value']);
345
+ for (let i = 0; i < 100; i++) {
346
+ const id = em.createEntity();
347
+ em.addComponent(id, Name, { value: `entity_${i}` });
348
+ }
349
+ const ids = em.query([Name]);
350
+ assert.equal(ids.length, 100);
351
+ assert.equal(em.get(ids[0], Name.value), 'entity_0');
352
+ assert.equal(em.get(ids[99], Name.value), 'entity_99');
353
+ });
354
+ it('string component swap-remove preserves data', () => {
355
+ const Name = component('NameSwap', 'string', ['value']);
356
+ const a = em.createEntity();
357
+ const b = em.createEntity();
358
+ const c = em.createEntity();
359
+ em.addComponent(a, Name, { value: 'aaa' });
360
+ em.addComponent(b, Name, { value: 'bbb' });
361
+ em.addComponent(c, Name, { value: 'ccc' });
362
+ em.destroyEntity(a);
363
+ assert.equal(em.get(b, Name.value), 'bbb');
364
+ assert.equal(em.get(c, Name.value), 'ccc');
365
+ });
366
+ it('mixed string + numeric fields in one component', () => {
367
+ const Item = component('Item', { name: 'string', weight: 'f32' });
368
+ const id = em.createEntity();
369
+ em.addComponent(id, Item, { name: 'Sword', weight: 3.5 });
370
+ assert.equal(em.get(id, Item.name), 'Sword');
371
+ assert.ok(Math.abs(em.get(id, Item.weight) - 3.5) < 0.01);
372
+ });
373
+ it('string component forEach field access', () => {
374
+ const Name = component('NameFE', 'string', ['value']);
375
+ for (let i = 0; i < 5; i++) {
376
+ const id = em.createEntity();
377
+ em.addComponent(id, Name, { value: `e${i}` });
378
+ }
379
+ em.forEach([Name], (arch) => {
380
+ const values = arch.field(Name.value);
381
+ assert.ok(Array.isArray(values));
382
+ assert.equal(values[0], 'e0');
383
+ assert.equal(values[4], 'e4');
384
+ });
385
+ });
386
+ });
387
+ describe('Deferred Hooks (onAdd / onRemove)', () => {
388
+ let em;
389
+ const Position = component('HPos', 'f32', ['x', 'y']);
390
+ const Velocity = component('HVel', 'f32', ['vx', 'vy']);
391
+ const Health = component('HHealth', 'f32', ['hp']);
392
+ beforeEach(() => {
393
+ em = createEntityManager();
394
+ });
395
+ it('onAdd fires after flushHooks with correct entity IDs', () => {
396
+ const added = [];
397
+ em.onAdd(Position, (id) => added.push(id));
398
+ const a = em.createEntity();
399
+ em.addComponent(a, Position, { x: 1, y: 2 });
400
+ const b = em.createEntity();
401
+ em.addComponent(b, Position, { x: 3, y: 4 });
402
+ assert.deepEqual(added, []);
403
+ em.flushHooks();
404
+ assert.deepEqual(added, [a, b]);
405
+ });
406
+ it('onRemove fires after flushHooks on removeComponent', () => {
407
+ const removed = [];
408
+ em.onRemove(Position, (id) => removed.push(id));
409
+ const id = em.createEntity();
410
+ em.addComponent(id, Position, { x: 1, y: 2 });
411
+ em.removeComponent(id, Position);
412
+ assert.deepEqual(removed, []);
413
+ em.flushHooks();
414
+ assert.deepEqual(removed, [id]);
415
+ });
416
+ it('onRemove fires for each component on destroyEntity', () => {
417
+ const removedPos = [];
418
+ const removedVel = [];
419
+ em.onRemove(Position, (id) => removedPos.push(id));
420
+ em.onRemove(Velocity, (id) => removedVel.push(id));
421
+ const id = em.createEntityWith(Position, { x: 1, y: 2 }, Velocity, { vx: 3, vy: 4 });
422
+ em.destroyEntity(id);
423
+ em.flushHooks();
424
+ assert.deepEqual(removedPos, [id]);
425
+ assert.deepEqual(removedVel, [id]);
426
+ });
427
+ it('createEntityWith triggers onAdd for all component types', () => {
428
+ const addedPos = [];
429
+ const addedVel = [];
430
+ em.onAdd(Position, (id) => addedPos.push(id));
431
+ em.onAdd(Velocity, (id) => addedVel.push(id));
432
+ const id = em.createEntityWith(Position, { x: 1, y: 2 }, Velocity, { vx: 3, vy: 4 });
433
+ em.flushHooks();
434
+ assert.deepEqual(addedPos, [id]);
435
+ assert.deepEqual(addedVel, [id]);
436
+ });
437
+ it('callbacks do not fire before flushHooks (deferred)', () => {
438
+ const added = [];
439
+ em.onAdd(Position, (id) => added.push(id));
440
+ const id = em.createEntity();
441
+ em.addComponent(id, Position, { x: 1, y: 2 });
442
+ assert.deepEqual(added, []);
443
+ });
444
+ it('unsubscribe prevents callback from firing', () => {
445
+ const added = [];
446
+ const unsub = em.onAdd(Position, (id) => added.push(id));
447
+ const a = em.createEntity();
448
+ em.addComponent(a, Position, { x: 1, y: 2 });
449
+ unsub();
450
+ em.flushHooks();
451
+ assert.deepEqual(added, []);
452
+ });
453
+ it('overwrite (addComponent with existing component) does not trigger onAdd', () => {
454
+ const added = [];
455
+ em.onAdd(Position, (id) => added.push(id));
456
+ const id = em.createEntity();
457
+ em.addComponent(id, Position, { x: 1, y: 2 });
458
+ em.flushHooks();
459
+ added.length = 0;
460
+ em.addComponent(id, Position, { x: 99, y: 88 });
461
+ em.flushHooks();
462
+ assert.deepEqual(added, []);
463
+ });
464
+ it('multiple callbacks on the same component', () => {
465
+ const added1 = [];
466
+ const added2 = [];
467
+ em.onAdd(Position, (id) => added1.push(id));
468
+ em.onAdd(Position, (id) => added2.push(id));
469
+ const id = em.createEntity();
470
+ em.addComponent(id, Position, { x: 1, y: 2 });
471
+ em.flushHooks();
472
+ assert.deepEqual(added1, [id]);
473
+ assert.deepEqual(added2, [id]);
474
+ });
475
+ it('flushHooks is a no-op when no hooks registered', () => {
476
+ const id = em.createEntity();
477
+ em.addComponent(id, Position, { x: 1, y: 2 });
478
+ em.flushHooks();
479
+ });
480
+ it('addComponent migration triggers onAdd', () => {
481
+ const added = [];
482
+ em.onAdd(Velocity, (id) => added.push(id));
483
+ const id = em.createEntity();
484
+ em.addComponent(id, Position, { x: 1, y: 2 });
485
+ em.addComponent(id, Velocity, { vx: 1, vy: 1 });
486
+ em.flushHooks();
487
+ assert.deepEqual(added, [id]);
488
+ });
489
+ it('pending arrays are cleared after flushHooks', () => {
490
+ const added = [];
491
+ em.onAdd(Position, (id) => added.push(id));
492
+ const a = em.createEntity();
493
+ em.addComponent(a, Position, { x: 1, y: 2 });
494
+ em.flushHooks();
495
+ assert.deepEqual(added, [a]);
496
+ em.flushHooks();
497
+ assert.deepEqual(added, [a]);
498
+ });
499
+ });
@@ -0,0 +1 @@
1
+ export {};