@codehz/ecs 0.7.5 → 0.7.6
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/dist/builder.d.mts +2 -2
- package/dist/index.mjs +1 -2
- package/dist/testing.d.mts +0 -1
- package/dist/testing.mjs +63 -2
- package/dist/testing.mjs.map +1 -1
- package/dist/world.mjs +52 -71
- package/dist/world.mjs.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/world/wildcard-relation-hooks.test.ts +41 -0
- package/src/world/hooks.ts +5 -0
package/dist/builder.d.mts
CHANGED
|
@@ -24,9 +24,9 @@ declare const __entityIdTypeTag: unique symbol;
|
|
|
24
24
|
* @template T - The data type associated with this ID
|
|
25
25
|
* @template U - Discriminant for the ID kind (e.g. `"component"`, `"entity-relation"`)
|
|
26
26
|
*/
|
|
27
|
-
type EntityId<T = unknown, U
|
|
27
|
+
type EntityId<T = unknown, U = unknown> = number & {
|
|
28
28
|
readonly [__componentTypeMarker]: T;
|
|
29
|
-
readonly [__entityIdTypeTag]: U
|
|
29
|
+
readonly [__entityIdTypeTag]: U;
|
|
30
30
|
};
|
|
31
31
|
/**
|
|
32
32
|
* Component identifier. Valid values are `1` through `1023`.
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
1
|
import { a as getComponentIdByName, c as isWildcardRelationId, d as isEntityId, f as isRelationId, i as component, l as relation, n as Query, o as getComponentNameById, r as EntityBuilder, s as decodeRelationId, t as World, u as isComponentId } from "./world.mjs";
|
|
2
|
-
|
|
3
|
-
export { EntityBuilder, Query, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isEntityId, isRelationId, isWildcardRelationId, relation };
|
|
2
|
+
export { EntityBuilder, Query, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isEntityId, isRelationId, isWildcardRelationId, relation };
|
package/dist/testing.d.mts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { S as EntityId, T as WildcardRelationId, b as ComponentId, c as LifecycleHook, i as Query, m as component, n as EntityBuilder, r as World, s as LifecycleCallback, t as ComponentDef, w as RelationId, y as relation } from "./builder.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/testing/index.d.ts
|
|
4
|
-
|
|
5
4
|
/**
|
|
6
5
|
* Snapshot of a single entity's component state
|
|
7
6
|
*/
|
package/dist/testing.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { c as isWildcardRelationId, i as component, l as relation, r as EntityBuilder, t as World } from "./world.mjs";
|
|
2
|
-
|
|
3
2
|
//#region src/testing/index.ts
|
|
4
3
|
/**
|
|
5
4
|
* A test fixture that manages a World instance and provides convenient
|
|
@@ -135,16 +134,28 @@ var WorldFixture = class {
|
|
|
135
134
|
* ```
|
|
136
135
|
*/
|
|
137
136
|
const Assertions = {
|
|
137
|
+
/**
|
|
138
|
+
* Check if an entity has a specific component
|
|
139
|
+
*/
|
|
138
140
|
hasComponent(world, entity, componentId) {
|
|
139
141
|
return world.exists(entity) && world.has(entity, componentId);
|
|
140
142
|
},
|
|
143
|
+
/**
|
|
144
|
+
* Check if an entity does not have a specific component
|
|
145
|
+
*/
|
|
141
146
|
lacksComponent(world, entity, componentId) {
|
|
142
147
|
return !world.exists(entity) || !world.has(entity, componentId);
|
|
143
148
|
},
|
|
149
|
+
/**
|
|
150
|
+
* Get a component value (returns undefined if entity doesn't exist or doesn't have the component)
|
|
151
|
+
*/
|
|
144
152
|
getComponent(world, entity, componentId) {
|
|
145
153
|
if (!world.exists(entity) || !world.has(entity, componentId)) return;
|
|
146
154
|
return world.get(entity, componentId);
|
|
147
155
|
},
|
|
156
|
+
/**
|
|
157
|
+
* Get all relation instances for a wildcard relation
|
|
158
|
+
*/
|
|
148
159
|
getRelations(world, entity, componentId) {
|
|
149
160
|
if (!world.exists(entity)) return;
|
|
150
161
|
const wildcardId = relation(componentId, "*");
|
|
@@ -154,48 +165,84 @@ const Assertions = {
|
|
|
154
165
|
return [];
|
|
155
166
|
}
|
|
156
167
|
},
|
|
168
|
+
/**
|
|
169
|
+
* Check if an entity has a relation to a specific target
|
|
170
|
+
*/
|
|
157
171
|
hasRelation(world, entity, componentId, targetEntity) {
|
|
158
172
|
if (!world.exists(entity)) return false;
|
|
159
173
|
const relationId = relation(componentId, targetEntity);
|
|
160
174
|
return world.has(entity, relationId);
|
|
161
175
|
},
|
|
176
|
+
/**
|
|
177
|
+
* Check if an entity exists in the world
|
|
178
|
+
*/
|
|
162
179
|
entityExists(world, entity) {
|
|
163
180
|
return world.exists(entity);
|
|
164
181
|
},
|
|
182
|
+
/**
|
|
183
|
+
* Check if a query contains specific entities
|
|
184
|
+
*/
|
|
165
185
|
queryContains(query, ...entities) {
|
|
166
186
|
const queryEntities = query.getEntities();
|
|
167
187
|
return entities.every((e) => queryEntities.includes(e));
|
|
168
188
|
},
|
|
189
|
+
/**
|
|
190
|
+
* Check if a query contains exactly the specified entities (no more, no less)
|
|
191
|
+
*/
|
|
169
192
|
queryContainsExactly(query, ...entities) {
|
|
170
193
|
const queryEntities = query.getEntities();
|
|
171
194
|
if (queryEntities.length !== entities.length) return false;
|
|
172
195
|
return entities.every((e) => queryEntities.includes(e));
|
|
173
196
|
},
|
|
197
|
+
/**
|
|
198
|
+
* Get the count of entities in a query
|
|
199
|
+
*/
|
|
174
200
|
queryCount(query) {
|
|
175
201
|
return query.getEntities().length;
|
|
176
202
|
},
|
|
203
|
+
/**
|
|
204
|
+
* Assert that an entity has a component (throws if not)
|
|
205
|
+
*/
|
|
177
206
|
assertHasComponent(world, entity, componentId) {
|
|
178
207
|
if (!world.exists(entity)) throw new AssertionError(`Entity ${entity} does not exist`);
|
|
179
208
|
if (!world.has(entity, componentId)) throw new AssertionError(`Entity ${entity} does not have component ${componentId}`);
|
|
180
209
|
},
|
|
210
|
+
/**
|
|
211
|
+
* Assert that an entity lacks a component (throws if it has the component)
|
|
212
|
+
*/
|
|
181
213
|
assertLacksComponent(world, entity, componentId) {
|
|
182
214
|
if (world.exists(entity) && world.has(entity, componentId)) throw new AssertionError(`Entity ${entity} unexpectedly has component ${componentId}`);
|
|
183
215
|
},
|
|
216
|
+
/**
|
|
217
|
+
* Assert that a component equals an expected value (throws if not)
|
|
218
|
+
*/
|
|
184
219
|
assertComponentEquals(world, entity, componentId, expected) {
|
|
185
220
|
this.assertHasComponent(world, entity, componentId);
|
|
186
221
|
const actual = world.get(entity, componentId);
|
|
187
222
|
if (!deepEquals(actual, expected)) throw new AssertionError(`Component ${componentId} on entity ${entity} does not match expected value.\nExpected: ${JSON.stringify(expected)}\nActual: ${JSON.stringify(actual)}`);
|
|
188
223
|
},
|
|
224
|
+
/**
|
|
225
|
+
* Assert that an entity exists (throws if not)
|
|
226
|
+
*/
|
|
189
227
|
assertEntityExists(world, entity) {
|
|
190
228
|
if (!world.exists(entity)) throw new AssertionError(`Entity ${entity} does not exist`);
|
|
191
229
|
},
|
|
230
|
+
/**
|
|
231
|
+
* Assert that an entity does not exist (throws if it exists)
|
|
232
|
+
*/
|
|
192
233
|
assertEntityNotExists(world, entity) {
|
|
193
234
|
if (world.exists(entity)) throw new AssertionError(`Entity ${entity} unexpectedly exists`);
|
|
194
235
|
},
|
|
236
|
+
/**
|
|
237
|
+
* Assert that a query contains specific entities (throws if not)
|
|
238
|
+
*/
|
|
195
239
|
assertQueryContains(query, ...entities) {
|
|
196
240
|
const queryEntities = query.getEntities();
|
|
197
241
|
for (const entity of entities) if (!queryEntities.includes(entity)) throw new AssertionError(`Query does not contain entity ${entity}.\nQuery entities: [${queryEntities.join(", ")}]`);
|
|
198
242
|
},
|
|
243
|
+
/**
|
|
244
|
+
* Assert that a query does not contain specific entities (throws if it does)
|
|
245
|
+
*/
|
|
199
246
|
assertQueryNotContains(query, ...entities) {
|
|
200
247
|
const queryEntities = query.getEntities();
|
|
201
248
|
for (const entity of entities) if (queryEntities.includes(entity)) throw new AssertionError(`Query unexpectedly contains entity ${entity}.\nQuery entities: [${queryEntities.join(", ")}]`);
|
|
@@ -221,6 +268,12 @@ const Assertions = {
|
|
|
221
268
|
* ```
|
|
222
269
|
*/
|
|
223
270
|
const Snapshot = {
|
|
271
|
+
/**
|
|
272
|
+
* Capture a snapshot of specified entities and their components
|
|
273
|
+
* @param world The world to capture from
|
|
274
|
+
* @param entities Entities to include in the snapshot
|
|
275
|
+
* @param componentIds Components to capture for each entity
|
|
276
|
+
*/
|
|
224
277
|
capture(world, entities, componentIds) {
|
|
225
278
|
const entitySnapshots = [];
|
|
226
279
|
for (const entity of entities) {
|
|
@@ -238,6 +291,11 @@ const Snapshot = {
|
|
|
238
291
|
}
|
|
239
292
|
return { entities: entitySnapshots };
|
|
240
293
|
},
|
|
294
|
+
/**
|
|
295
|
+
* Compare two snapshots and return the differences
|
|
296
|
+
* @param before The 'before' snapshot
|
|
297
|
+
* @param after The 'after' snapshot
|
|
298
|
+
*/
|
|
241
299
|
compare(before, after) {
|
|
242
300
|
const beforeEntities = new Set(before.entities.map((e) => e.entity));
|
|
243
301
|
const afterEntities = new Set(after.entities.map((e) => e.entity));
|
|
@@ -285,6 +343,9 @@ const Snapshot = {
|
|
|
285
343
|
componentChanges
|
|
286
344
|
};
|
|
287
345
|
},
|
|
346
|
+
/**
|
|
347
|
+
* Check if two snapshots are equal
|
|
348
|
+
*/
|
|
288
349
|
equals(a, b) {
|
|
289
350
|
const diff = this.compare(a, b);
|
|
290
351
|
return diff.addedEntities.length === 0 && diff.removedEntities.length === 0 && diff.componentChanges.length === 0;
|
|
@@ -335,7 +396,7 @@ function deepClone(value) {
|
|
|
335
396
|
for (const key of Object.keys(value)) result[key] = deepClone(value[key]);
|
|
336
397
|
return result;
|
|
337
398
|
}
|
|
338
|
-
|
|
339
399
|
//#endregion
|
|
340
400
|
export { AssertionError, Assertions, EntityBuilder, Snapshot, World, WorldFixture, component, relation };
|
|
401
|
+
|
|
341
402
|
//# sourceMappingURL=testing.mjs.map
|
package/dist/testing.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.mjs","names":["entitySnapshots: EntitySnapshot[]","addedEntities: EntityId[]","removedEntities: EntityId[]","componentChanges: SnapshotDiff[\"componentChanges\"]","result: Record<string, unknown>"],"sources":["../src/testing/index.ts"],"sourcesContent":["/**\n * @module testing\n * Testing utilities for ECS-based game logic\n *\n * This module provides framework-agnostic testing helpers that work with\n * bun:test, vitest, jest, or any other testing framework.\n *\n * @example\n * ```typescript\n * import { describe, expect, it } from \"bun:test\";\n * import { component } from \"@codehz/ecs\";\n * import { WorldFixture, EntityBuilder, Assertions } from \"@codehz/ecs/testing\";\n *\n * const PositionId = component<{ x: number; y: number }>();\n * const VelocityId = component<{ x: number; y: number }>();\n *\n * describe(\"Movement System\", () => {\n * it(\"should update position based on velocity\", () => {\n * const fixture = new WorldFixture();\n * const entity = fixture\n * .spawn()\n * .with(PositionId, { x: 0, y: 0 })\n * .with(VelocityId, { x: 1, y: 2 })\n * .build();\n *\n * // Run your game logic here\n * movementSystem(fixture.world, 1.0);\n *\n * expect(Assertions.hasComponent(fixture.world, entity, PositionId)).toBe(true);\n * expect(Assertions.getComponent(fixture.world, entity, PositionId)).toEqual({ x: 1, y: 2 });\n * });\n * });\n * ```\n */\n\nimport type { ComponentId, EntityId, WildcardRelationId } from \"../entity\";\nimport { isWildcardRelationId, relation } from \"../entity\";\nimport type { Query } from \"../query/query\";\nimport { World } from \"../world/world\";\nexport { EntityBuilder } from \"../world/builder\";\nexport type { ComponentDef } from \"../world/builder\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * A component definition for entity building, supporting both regular components and relations\n */\nimport type { EntityBuilder } from \"../world/builder\";\n\n/**\n * Snapshot of a single entity's component state\n */\nexport interface EntitySnapshot {\n entity: EntityId;\n components: Map<EntityId<any>, unknown>;\n}\n\n/**\n * Snapshot of multiple entities' component state\n */\nexport interface WorldSnapshot {\n entities: EntitySnapshot[];\n}\n\n/**\n * Result of comparing two snapshots\n */\nexport interface SnapshotDiff {\n /** Entities that exist in 'after' but not in 'before' */\n addedEntities: EntityId[];\n /** Entities that exist in 'before' but not in 'after' */\n removedEntities: EntityId[];\n /** Changes to components on existing entities */\n componentChanges: Array<{\n entity: EntityId;\n componentId: EntityId<any>;\n before: unknown | undefined;\n after: unknown | undefined;\n changeType: \"added\" | \"removed\" | \"modified\";\n }>;\n}\n\n// =============================================================================\n// WorldFixture - Test World Factory\n// =============================================================================\n\n/**\n * A test fixture that manages a World instance and provides convenient\n * methods for setting up test scenarios.\n *\n * @example\n * ```typescript\n * const fixture = new WorldFixture();\n *\n * // Spawn entities with fluent API\n * const player = fixture\n * .spawn()\n * .with(PositionId, { x: 0, y: 0 })\n * .with(HealthId, { current: 100, max: 100 })\n * .build();\n *\n * // Access the world for running systems\n * movementSystem(fixture.world);\n *\n * // Clean up (optional - creates a fresh world)\n * fixture.reset();\n * ```\n */\nexport class WorldFixture {\n private _world: World;\n private _queries: Query[] = [];\n\n constructor() {\n this._world = new World();\n }\n\n /**\n * Get the underlying World instance\n */\n get world(): World {\n return this._world;\n }\n\n /**\n * Create a new EntityBuilder for spawning an entity with components\n */\n spawn(): EntityBuilder {\n return this._world.spawn();\n }\n\n /**\n * Spawn multiple entities with the same component configuration\n * @param count Number of entities to spawn\n * @param configure Function to configure each entity builder\n * @returns Array of created entity IDs\n */\n spawnMany(count: number, configure: (builder: EntityBuilder, index: number) => EntityBuilder): EntityId[] {\n return this._world.spawnMany(count, configure);\n }\n\n /**\n * Create a query and track it for automatic cleanup\n * @param componentTypes Component types to query for\n * @returns Query instance\n */\n createQuery(componentTypes: EntityId<any>[]): Query {\n const query = this._world.createQuery(componentTypes);\n this._queries.push(query);\n return query;\n }\n\n /**\n * Execute pending commands (alias for world.sync())\n */\n sync(): void {\n this._world.sync();\n }\n\n /**\n * Reset the fixture with a fresh World instance\n * Disposes all tracked queries\n */\n reset(): void {\n for (const query of this._queries) {\n query.dispose();\n }\n this._queries = [];\n this._world = new World();\n }\n\n /**\n * Capture a snapshot of specified entities and their components\n * @param entities Entities to capture\n * @param componentIds Components to include in the snapshot\n */\n captureSnapshot(entities: EntityId[], componentIds: EntityId<any>[]): WorldSnapshot {\n return Snapshot.capture(this._world, entities, componentIds);\n }\n\n /**\n * Symbol.dispose implementation for automatic resource management\n */\n [Symbol.dispose](): void {\n this.reset();\n }\n}\n\n// =============================================================================\n// EntityBuilder - Fluent Entity Creation\n// =============================================================================\n\n/**\n * Fluent builder for creating entities with components.\n * Supports both regular components and entity relations.\n *\n * @example\n * ```typescript\n * // Basic usage\n * // Note: build() will enqueue component commands but will NOT call world.sync().\n * // You must call world.sync() or fixture.sync() manually to apply commands.\n * const entity = new EntityBuilder(world)\n * .with(PositionId, { x: 10, y: 20 })\n * .with(VelocityId, { x: 1, y: 0 })\n * .build();\n * // Apply pending changes\n * world.sync();\n *\n * // With relations\n * const child = new EntityBuilder(world)\n * .with(PositionId, { x: 0, y: 0 })\n * .withRelation(ParentId, parentEntity, { offset: { x: 5, y: 5 } })\n * .build();\n * world.sync();\n *\n * // Tag component (void type)\n * const tagged = new EntityBuilder(world)\n * .with(PlayerTagId)\n * .build();\n * ```\n */\n// EntityBuilder is exported from world.ts; testing utilities will use world.spawn()\n\n// =============================================================================\n// Assertions - Test Assertion Helpers\n// =============================================================================\n\n/**\n * Test assertion utilities that return boolean values or throw descriptive errors.\n * These work with any testing framework's expect() function.\n *\n * @example\n * ```typescript\n * // With bun:test or vitest\n * expect(Assertions.hasComponent(world, entity, PositionId)).toBe(true);\n * expect(Assertions.getComponent(world, entity, PositionId)).toEqual({ x: 10, y: 20 });\n *\n * // Direct assertion (throws on failure)\n * Assertions.assertHasComponent(world, entity, PositionId);\n * Assertions.assertComponentEquals(world, entity, PositionId, { x: 10, y: 20 });\n * ```\n */\nexport const Assertions = {\n /**\n * Check if an entity has a specific component\n */\n hasComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): boolean {\n return world.exists(entity) && world.has(entity, componentId);\n },\n\n /**\n * Check if an entity does not have a specific component\n */\n lacksComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): boolean {\n return !world.exists(entity) || !world.has(entity, componentId);\n },\n\n /**\n * Get a component value (returns undefined if entity doesn't exist or doesn't have the component)\n */\n getComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): T | undefined {\n if (!world.exists(entity) || !world.has(entity, componentId)) {\n return undefined;\n }\n return world.get(entity, componentId);\n },\n\n /**\n * Get all relation instances for a wildcard relation\n */\n getRelations<T>(world: World, entity: EntityId, componentId: ComponentId<T>): [EntityId<unknown>, T][] | undefined {\n if (!world.exists(entity)) {\n return undefined;\n }\n const wildcardId = relation(componentId, \"*\");\n try {\n return world.get(entity, wildcardId);\n } catch {\n return [];\n }\n },\n\n /**\n * Check if an entity has a relation to a specific target\n */\n hasRelation<T>(world: World, entity: EntityId, componentId: ComponentId<T>, targetEntity: EntityId<any>): boolean {\n if (!world.exists(entity)) {\n return false;\n }\n const relationId = relation(componentId, targetEntity);\n return world.has(entity, relationId);\n },\n\n /**\n * Check if an entity exists in the world\n */\n entityExists(world: World, entity: EntityId): boolean {\n return world.exists(entity);\n },\n\n /**\n * Check if a query contains specific entities\n */\n queryContains(query: Query, ...entities: EntityId[]): boolean {\n const queryEntities = query.getEntities();\n return entities.every((e) => queryEntities.includes(e));\n },\n\n /**\n * Check if a query contains exactly the specified entities (no more, no less)\n */\n queryContainsExactly(query: Query, ...entities: EntityId[]): boolean {\n const queryEntities = query.getEntities();\n if (queryEntities.length !== entities.length) {\n return false;\n }\n return entities.every((e) => queryEntities.includes(e));\n },\n\n /**\n * Get the count of entities in a query\n */\n queryCount(query: Query): number {\n return query.getEntities().length;\n },\n\n // === Throwing assertions ===\n\n /**\n * Assert that an entity has a component (throws if not)\n */\n assertHasComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): void {\n if (!world.exists(entity)) {\n throw new AssertionError(`Entity ${entity} does not exist`);\n }\n if (!world.has(entity, componentId)) {\n throw new AssertionError(`Entity ${entity} does not have component ${componentId}`);\n }\n },\n\n /**\n * Assert that an entity lacks a component (throws if it has the component)\n */\n assertLacksComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): void {\n if (world.exists(entity) && world.has(entity, componentId)) {\n throw new AssertionError(`Entity ${entity} unexpectedly has component ${componentId}`);\n }\n },\n\n /**\n * Assert that a component equals an expected value (throws if not)\n */\n assertComponentEquals<T>(world: World, entity: EntityId, componentId: EntityId<T>, expected: T): void {\n this.assertHasComponent(world, entity, componentId);\n const actual = world.get(entity, componentId);\n if (!deepEquals(actual, expected)) {\n throw new AssertionError(\n `Component ${componentId} on entity ${entity} does not match expected value.\\n` +\n `Expected: ${JSON.stringify(expected)}\\n` +\n `Actual: ${JSON.stringify(actual)}`,\n );\n }\n },\n\n /**\n * Assert that an entity exists (throws if not)\n */\n assertEntityExists(world: World, entity: EntityId): void {\n if (!world.exists(entity)) {\n throw new AssertionError(`Entity ${entity} does not exist`);\n }\n },\n\n /**\n * Assert that an entity does not exist (throws if it exists)\n */\n assertEntityNotExists(world: World, entity: EntityId): void {\n if (world.exists(entity)) {\n throw new AssertionError(`Entity ${entity} unexpectedly exists`);\n }\n },\n\n /**\n * Assert that a query contains specific entities (throws if not)\n */\n assertQueryContains(query: Query, ...entities: EntityId[]): void {\n const queryEntities = query.getEntities();\n for (const entity of entities) {\n if (!queryEntities.includes(entity)) {\n throw new AssertionError(\n `Query does not contain entity ${entity}.\\n` + `Query entities: [${queryEntities.join(\", \")}]`,\n );\n }\n }\n },\n\n /**\n * Assert that a query does not contain specific entities (throws if it does)\n */\n assertQueryNotContains(query: Query, ...entities: EntityId[]): void {\n const queryEntities = query.getEntities();\n for (const entity of entities) {\n if (queryEntities.includes(entity)) {\n throw new AssertionError(\n `Query unexpectedly contains entity ${entity}.\\n` + `Query entities: [${queryEntities.join(\", \")}]`,\n );\n }\n }\n },\n};\n\n// =============================================================================\n// Snapshot - State Capture and Comparison\n// =============================================================================\n\n/**\n * Utilities for capturing and comparing world state snapshots.\n * Useful for testing that systems produce expected state changes.\n *\n * @example\n * ```typescript\n * const before = Snapshot.capture(world, [entity], [PositionId, VelocityId]);\n *\n * // Run game logic\n * movementSystem(world, deltaTime);\n * world.sync();\n *\n * const after = Snapshot.capture(world, [entity], [PositionId, VelocityId]);\n * const diff = Snapshot.compare(before, after);\n *\n * expect(diff.componentChanges).toHaveLength(1);\n * expect(diff.componentChanges[0].changeType).toBe(\"modified\");\n * ```\n */\nexport const Snapshot = {\n /**\n * Capture a snapshot of specified entities and their components\n * @param world The world to capture from\n * @param entities Entities to include in the snapshot\n * @param componentIds Components to capture for each entity\n */\n capture(world: World, entities: EntityId[], componentIds: EntityId<any>[]): WorldSnapshot {\n const entitySnapshots: EntitySnapshot[] = [];\n\n for (const entity of entities) {\n if (!world.exists(entity)) {\n continue;\n }\n\n const components = new Map<EntityId<any>, unknown>();\n\n for (const componentId of componentIds) {\n if (isWildcardRelationId(componentId)) {\n // For wildcard relations, capture all relation instances\n try {\n const relations = world.get(entity, componentId as WildcardRelationId<any>);\n if (relations && relations.length > 0) {\n components.set(componentId, deepClone(relations));\n }\n } catch {\n // Entity doesn't have this relation type\n }\n } else if (world.has(entity, componentId)) {\n components.set(componentId, deepClone(world.get(entity, componentId)));\n }\n }\n\n entitySnapshots.push({ entity, components });\n }\n\n return { entities: entitySnapshots };\n },\n\n /**\n * Compare two snapshots and return the differences\n * @param before The 'before' snapshot\n * @param after The 'after' snapshot\n */\n compare(before: WorldSnapshot, after: WorldSnapshot): SnapshotDiff {\n const beforeEntities = new Set(before.entities.map((e) => e.entity));\n const afterEntities = new Set(after.entities.map((e) => e.entity));\n\n const addedEntities: EntityId[] = [];\n const removedEntities: EntityId[] = [];\n const componentChanges: SnapshotDiff[\"componentChanges\"] = [];\n\n // Find added entities\n for (const entity of afterEntities) {\n if (!beforeEntities.has(entity)) {\n addedEntities.push(entity);\n }\n }\n\n // Find removed entities\n for (const entity of beforeEntities) {\n if (!afterEntities.has(entity)) {\n removedEntities.push(entity);\n }\n }\n\n // Find component changes on existing entities\n const beforeMap = new Map(before.entities.map((e) => [e.entity, e]));\n const afterMap = new Map(after.entities.map((e) => [e.entity, e]));\n\n for (const entity of beforeEntities) {\n if (!afterEntities.has(entity)) continue; // Skip removed entities\n\n const beforeEntity = beforeMap.get(entity)!;\n const afterEntity = afterMap.get(entity)!;\n\n // Check for component changes\n const allComponentIds = new Set([...beforeEntity.components.keys(), ...afterEntity.components.keys()]);\n\n for (const componentId of allComponentIds) {\n const beforeValue = beforeEntity.components.get(componentId);\n const afterValue = afterEntity.components.get(componentId);\n\n if (beforeValue === undefined && afterValue !== undefined) {\n componentChanges.push({\n entity,\n componentId,\n before: undefined,\n after: afterValue,\n changeType: \"added\",\n });\n } else if (beforeValue !== undefined && afterValue === undefined) {\n componentChanges.push({\n entity,\n componentId,\n before: beforeValue,\n after: undefined,\n changeType: \"removed\",\n });\n } else if (!deepEquals(beforeValue, afterValue)) {\n componentChanges.push({\n entity,\n componentId,\n before: beforeValue,\n after: afterValue,\n changeType: \"modified\",\n });\n }\n }\n }\n\n return { addedEntities, removedEntities, componentChanges };\n },\n\n /**\n * Check if two snapshots are equal\n */\n equals(a: WorldSnapshot, b: WorldSnapshot): boolean {\n const diff = this.compare(a, b);\n return diff.addedEntities.length === 0 && diff.removedEntities.length === 0 && diff.componentChanges.length === 0;\n },\n};\n\n// =============================================================================\n// Utilities\n// =============================================================================\n\n/**\n * Custom assertion error for testing utilities\n */\nexport class AssertionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AssertionError\";\n }\n}\n\n/**\n * Deep equality check for comparing component values\n */\nfunction deepEquals(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (typeof a !== typeof b) return false;\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEquals(a[i], b[i])) return false;\n }\n return true;\n }\n\n if (typeof a === \"object\" && typeof b === \"object\") {\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n\n if (aKeys.length !== bKeys.length) return false;\n\n for (const key of aKeys) {\n if (!Object.prototype.hasOwnProperty.call(bObj, key)) return false;\n if (!deepEquals(aObj[key], bObj[key])) return false;\n }\n return true;\n }\n\n return false;\n}\n\n/**\n * Deep clone a value for snapshot isolation\n */\nfunction deepClone<T>(value: T): T {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map(deepClone) as T;\n }\n\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(value)) {\n result[key] = deepClone((value as Record<string, unknown>)[key]);\n }\n return result as T;\n}\n\n// =============================================================================\n// Re-exports for convenience\n// =============================================================================\n\nexport { component, relation } from \"../entity\";\nexport type { ComponentId, EntityId, RelationId, WildcardRelationId } from \"../entity\";\nexport type { Query } from \"../query/query\";\nexport type { LifecycleCallback, LifecycleHook } from \"../types\";\nexport { World } from \"../world/world\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"testing.mjs","names":[],"sources":["../src/testing/index.ts"],"sourcesContent":["/**\n * @module testing\n * Testing utilities for ECS-based game logic\n *\n * This module provides framework-agnostic testing helpers that work with\n * bun:test, vitest, jest, or any other testing framework.\n *\n * @example\n * ```typescript\n * import { describe, expect, it } from \"bun:test\";\n * import { component } from \"@codehz/ecs\";\n * import { WorldFixture, EntityBuilder, Assertions } from \"@codehz/ecs/testing\";\n *\n * const PositionId = component<{ x: number; y: number }>();\n * const VelocityId = component<{ x: number; y: number }>();\n *\n * describe(\"Movement System\", () => {\n * it(\"should update position based on velocity\", () => {\n * const fixture = new WorldFixture();\n * const entity = fixture\n * .spawn()\n * .with(PositionId, { x: 0, y: 0 })\n * .with(VelocityId, { x: 1, y: 2 })\n * .build();\n *\n * // Run your game logic here\n * movementSystem(fixture.world, 1.0);\n *\n * expect(Assertions.hasComponent(fixture.world, entity, PositionId)).toBe(true);\n * expect(Assertions.getComponent(fixture.world, entity, PositionId)).toEqual({ x: 1, y: 2 });\n * });\n * });\n * ```\n */\n\nimport type { ComponentId, EntityId, WildcardRelationId } from \"../entity\";\nimport { isWildcardRelationId, relation } from \"../entity\";\nimport type { Query } from \"../query/query\";\nimport { World } from \"../world/world\";\nexport { EntityBuilder } from \"../world/builder\";\nexport type { ComponentDef } from \"../world/builder\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * A component definition for entity building, supporting both regular components and relations\n */\nimport type { EntityBuilder } from \"../world/builder\";\n\n/**\n * Snapshot of a single entity's component state\n */\nexport interface EntitySnapshot {\n entity: EntityId;\n components: Map<EntityId<any>, unknown>;\n}\n\n/**\n * Snapshot of multiple entities' component state\n */\nexport interface WorldSnapshot {\n entities: EntitySnapshot[];\n}\n\n/**\n * Result of comparing two snapshots\n */\nexport interface SnapshotDiff {\n /** Entities that exist in 'after' but not in 'before' */\n addedEntities: EntityId[];\n /** Entities that exist in 'before' but not in 'after' */\n removedEntities: EntityId[];\n /** Changes to components on existing entities */\n componentChanges: Array<{\n entity: EntityId;\n componentId: EntityId<any>;\n before: unknown | undefined;\n after: unknown | undefined;\n changeType: \"added\" | \"removed\" | \"modified\";\n }>;\n}\n\n// =============================================================================\n// WorldFixture - Test World Factory\n// =============================================================================\n\n/**\n * A test fixture that manages a World instance and provides convenient\n * methods for setting up test scenarios.\n *\n * @example\n * ```typescript\n * const fixture = new WorldFixture();\n *\n * // Spawn entities with fluent API\n * const player = fixture\n * .spawn()\n * .with(PositionId, { x: 0, y: 0 })\n * .with(HealthId, { current: 100, max: 100 })\n * .build();\n *\n * // Access the world for running systems\n * movementSystem(fixture.world);\n *\n * // Clean up (optional - creates a fresh world)\n * fixture.reset();\n * ```\n */\nexport class WorldFixture {\n private _world: World;\n private _queries: Query[] = [];\n\n constructor() {\n this._world = new World();\n }\n\n /**\n * Get the underlying World instance\n */\n get world(): World {\n return this._world;\n }\n\n /**\n * Create a new EntityBuilder for spawning an entity with components\n */\n spawn(): EntityBuilder {\n return this._world.spawn();\n }\n\n /**\n * Spawn multiple entities with the same component configuration\n * @param count Number of entities to spawn\n * @param configure Function to configure each entity builder\n * @returns Array of created entity IDs\n */\n spawnMany(count: number, configure: (builder: EntityBuilder, index: number) => EntityBuilder): EntityId[] {\n return this._world.spawnMany(count, configure);\n }\n\n /**\n * Create a query and track it for automatic cleanup\n * @param componentTypes Component types to query for\n * @returns Query instance\n */\n createQuery(componentTypes: EntityId<any>[]): Query {\n const query = this._world.createQuery(componentTypes);\n this._queries.push(query);\n return query;\n }\n\n /**\n * Execute pending commands (alias for world.sync())\n */\n sync(): void {\n this._world.sync();\n }\n\n /**\n * Reset the fixture with a fresh World instance\n * Disposes all tracked queries\n */\n reset(): void {\n for (const query of this._queries) {\n query.dispose();\n }\n this._queries = [];\n this._world = new World();\n }\n\n /**\n * Capture a snapshot of specified entities and their components\n * @param entities Entities to capture\n * @param componentIds Components to include in the snapshot\n */\n captureSnapshot(entities: EntityId[], componentIds: EntityId<any>[]): WorldSnapshot {\n return Snapshot.capture(this._world, entities, componentIds);\n }\n\n /**\n * Symbol.dispose implementation for automatic resource management\n */\n [Symbol.dispose](): void {\n this.reset();\n }\n}\n\n// =============================================================================\n// EntityBuilder - Fluent Entity Creation\n// =============================================================================\n\n/**\n * Fluent builder for creating entities with components.\n * Supports both regular components and entity relations.\n *\n * @example\n * ```typescript\n * // Basic usage\n * // Note: build() will enqueue component commands but will NOT call world.sync().\n * // You must call world.sync() or fixture.sync() manually to apply commands.\n * const entity = new EntityBuilder(world)\n * .with(PositionId, { x: 10, y: 20 })\n * .with(VelocityId, { x: 1, y: 0 })\n * .build();\n * // Apply pending changes\n * world.sync();\n *\n * // With relations\n * const child = new EntityBuilder(world)\n * .with(PositionId, { x: 0, y: 0 })\n * .withRelation(ParentId, parentEntity, { offset: { x: 5, y: 5 } })\n * .build();\n * world.sync();\n *\n * // Tag component (void type)\n * const tagged = new EntityBuilder(world)\n * .with(PlayerTagId)\n * .build();\n * ```\n */\n// EntityBuilder is exported from world.ts; testing utilities will use world.spawn()\n\n// =============================================================================\n// Assertions - Test Assertion Helpers\n// =============================================================================\n\n/**\n * Test assertion utilities that return boolean values or throw descriptive errors.\n * These work with any testing framework's expect() function.\n *\n * @example\n * ```typescript\n * // With bun:test or vitest\n * expect(Assertions.hasComponent(world, entity, PositionId)).toBe(true);\n * expect(Assertions.getComponent(world, entity, PositionId)).toEqual({ x: 10, y: 20 });\n *\n * // Direct assertion (throws on failure)\n * Assertions.assertHasComponent(world, entity, PositionId);\n * Assertions.assertComponentEquals(world, entity, PositionId, { x: 10, y: 20 });\n * ```\n */\nexport const Assertions = {\n /**\n * Check if an entity has a specific component\n */\n hasComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): boolean {\n return world.exists(entity) && world.has(entity, componentId);\n },\n\n /**\n * Check if an entity does not have a specific component\n */\n lacksComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): boolean {\n return !world.exists(entity) || !world.has(entity, componentId);\n },\n\n /**\n * Get a component value (returns undefined if entity doesn't exist or doesn't have the component)\n */\n getComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): T | undefined {\n if (!world.exists(entity) || !world.has(entity, componentId)) {\n return undefined;\n }\n return world.get(entity, componentId);\n },\n\n /**\n * Get all relation instances for a wildcard relation\n */\n getRelations<T>(world: World, entity: EntityId, componentId: ComponentId<T>): [EntityId<unknown>, T][] | undefined {\n if (!world.exists(entity)) {\n return undefined;\n }\n const wildcardId = relation(componentId, \"*\");\n try {\n return world.get(entity, wildcardId);\n } catch {\n return [];\n }\n },\n\n /**\n * Check if an entity has a relation to a specific target\n */\n hasRelation<T>(world: World, entity: EntityId, componentId: ComponentId<T>, targetEntity: EntityId<any>): boolean {\n if (!world.exists(entity)) {\n return false;\n }\n const relationId = relation(componentId, targetEntity);\n return world.has(entity, relationId);\n },\n\n /**\n * Check if an entity exists in the world\n */\n entityExists(world: World, entity: EntityId): boolean {\n return world.exists(entity);\n },\n\n /**\n * Check if a query contains specific entities\n */\n queryContains(query: Query, ...entities: EntityId[]): boolean {\n const queryEntities = query.getEntities();\n return entities.every((e) => queryEntities.includes(e));\n },\n\n /**\n * Check if a query contains exactly the specified entities (no more, no less)\n */\n queryContainsExactly(query: Query, ...entities: EntityId[]): boolean {\n const queryEntities = query.getEntities();\n if (queryEntities.length !== entities.length) {\n return false;\n }\n return entities.every((e) => queryEntities.includes(e));\n },\n\n /**\n * Get the count of entities in a query\n */\n queryCount(query: Query): number {\n return query.getEntities().length;\n },\n\n // === Throwing assertions ===\n\n /**\n * Assert that an entity has a component (throws if not)\n */\n assertHasComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): void {\n if (!world.exists(entity)) {\n throw new AssertionError(`Entity ${entity} does not exist`);\n }\n if (!world.has(entity, componentId)) {\n throw new AssertionError(`Entity ${entity} does not have component ${componentId}`);\n }\n },\n\n /**\n * Assert that an entity lacks a component (throws if it has the component)\n */\n assertLacksComponent<T>(world: World, entity: EntityId, componentId: EntityId<T>): void {\n if (world.exists(entity) && world.has(entity, componentId)) {\n throw new AssertionError(`Entity ${entity} unexpectedly has component ${componentId}`);\n }\n },\n\n /**\n * Assert that a component equals an expected value (throws if not)\n */\n assertComponentEquals<T>(world: World, entity: EntityId, componentId: EntityId<T>, expected: T): void {\n this.assertHasComponent(world, entity, componentId);\n const actual = world.get(entity, componentId);\n if (!deepEquals(actual, expected)) {\n throw new AssertionError(\n `Component ${componentId} on entity ${entity} does not match expected value.\\n` +\n `Expected: ${JSON.stringify(expected)}\\n` +\n `Actual: ${JSON.stringify(actual)}`,\n );\n }\n },\n\n /**\n * Assert that an entity exists (throws if not)\n */\n assertEntityExists(world: World, entity: EntityId): void {\n if (!world.exists(entity)) {\n throw new AssertionError(`Entity ${entity} does not exist`);\n }\n },\n\n /**\n * Assert that an entity does not exist (throws if it exists)\n */\n assertEntityNotExists(world: World, entity: EntityId): void {\n if (world.exists(entity)) {\n throw new AssertionError(`Entity ${entity} unexpectedly exists`);\n }\n },\n\n /**\n * Assert that a query contains specific entities (throws if not)\n */\n assertQueryContains(query: Query, ...entities: EntityId[]): void {\n const queryEntities = query.getEntities();\n for (const entity of entities) {\n if (!queryEntities.includes(entity)) {\n throw new AssertionError(\n `Query does not contain entity ${entity}.\\n` + `Query entities: [${queryEntities.join(\", \")}]`,\n );\n }\n }\n },\n\n /**\n * Assert that a query does not contain specific entities (throws if it does)\n */\n assertQueryNotContains(query: Query, ...entities: EntityId[]): void {\n const queryEntities = query.getEntities();\n for (const entity of entities) {\n if (queryEntities.includes(entity)) {\n throw new AssertionError(\n `Query unexpectedly contains entity ${entity}.\\n` + `Query entities: [${queryEntities.join(\", \")}]`,\n );\n }\n }\n },\n};\n\n// =============================================================================\n// Snapshot - State Capture and Comparison\n// =============================================================================\n\n/**\n * Utilities for capturing and comparing world state snapshots.\n * Useful for testing that systems produce expected state changes.\n *\n * @example\n * ```typescript\n * const before = Snapshot.capture(world, [entity], [PositionId, VelocityId]);\n *\n * // Run game logic\n * movementSystem(world, deltaTime);\n * world.sync();\n *\n * const after = Snapshot.capture(world, [entity], [PositionId, VelocityId]);\n * const diff = Snapshot.compare(before, after);\n *\n * expect(diff.componentChanges).toHaveLength(1);\n * expect(diff.componentChanges[0].changeType).toBe(\"modified\");\n * ```\n */\nexport const Snapshot = {\n /**\n * Capture a snapshot of specified entities and their components\n * @param world The world to capture from\n * @param entities Entities to include in the snapshot\n * @param componentIds Components to capture for each entity\n */\n capture(world: World, entities: EntityId[], componentIds: EntityId<any>[]): WorldSnapshot {\n const entitySnapshots: EntitySnapshot[] = [];\n\n for (const entity of entities) {\n if (!world.exists(entity)) {\n continue;\n }\n\n const components = new Map<EntityId<any>, unknown>();\n\n for (const componentId of componentIds) {\n if (isWildcardRelationId(componentId)) {\n // For wildcard relations, capture all relation instances\n try {\n const relations = world.get(entity, componentId as WildcardRelationId<any>);\n if (relations && relations.length > 0) {\n components.set(componentId, deepClone(relations));\n }\n } catch {\n // Entity doesn't have this relation type\n }\n } else if (world.has(entity, componentId)) {\n components.set(componentId, deepClone(world.get(entity, componentId)));\n }\n }\n\n entitySnapshots.push({ entity, components });\n }\n\n return { entities: entitySnapshots };\n },\n\n /**\n * Compare two snapshots and return the differences\n * @param before The 'before' snapshot\n * @param after The 'after' snapshot\n */\n compare(before: WorldSnapshot, after: WorldSnapshot): SnapshotDiff {\n const beforeEntities = new Set(before.entities.map((e) => e.entity));\n const afterEntities = new Set(after.entities.map((e) => e.entity));\n\n const addedEntities: EntityId[] = [];\n const removedEntities: EntityId[] = [];\n const componentChanges: SnapshotDiff[\"componentChanges\"] = [];\n\n // Find added entities\n for (const entity of afterEntities) {\n if (!beforeEntities.has(entity)) {\n addedEntities.push(entity);\n }\n }\n\n // Find removed entities\n for (const entity of beforeEntities) {\n if (!afterEntities.has(entity)) {\n removedEntities.push(entity);\n }\n }\n\n // Find component changes on existing entities\n const beforeMap = new Map(before.entities.map((e) => [e.entity, e]));\n const afterMap = new Map(after.entities.map((e) => [e.entity, e]));\n\n for (const entity of beforeEntities) {\n if (!afterEntities.has(entity)) continue; // Skip removed entities\n\n const beforeEntity = beforeMap.get(entity)!;\n const afterEntity = afterMap.get(entity)!;\n\n // Check for component changes\n const allComponentIds = new Set([...beforeEntity.components.keys(), ...afterEntity.components.keys()]);\n\n for (const componentId of allComponentIds) {\n const beforeValue = beforeEntity.components.get(componentId);\n const afterValue = afterEntity.components.get(componentId);\n\n if (beforeValue === undefined && afterValue !== undefined) {\n componentChanges.push({\n entity,\n componentId,\n before: undefined,\n after: afterValue,\n changeType: \"added\",\n });\n } else if (beforeValue !== undefined && afterValue === undefined) {\n componentChanges.push({\n entity,\n componentId,\n before: beforeValue,\n after: undefined,\n changeType: \"removed\",\n });\n } else if (!deepEquals(beforeValue, afterValue)) {\n componentChanges.push({\n entity,\n componentId,\n before: beforeValue,\n after: afterValue,\n changeType: \"modified\",\n });\n }\n }\n }\n\n return { addedEntities, removedEntities, componentChanges };\n },\n\n /**\n * Check if two snapshots are equal\n */\n equals(a: WorldSnapshot, b: WorldSnapshot): boolean {\n const diff = this.compare(a, b);\n return diff.addedEntities.length === 0 && diff.removedEntities.length === 0 && diff.componentChanges.length === 0;\n },\n};\n\n// =============================================================================\n// Utilities\n// =============================================================================\n\n/**\n * Custom assertion error for testing utilities\n */\nexport class AssertionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AssertionError\";\n }\n}\n\n/**\n * Deep equality check for comparing component values\n */\nfunction deepEquals(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (typeof a !== typeof b) return false;\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEquals(a[i], b[i])) return false;\n }\n return true;\n }\n\n if (typeof a === \"object\" && typeof b === \"object\") {\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n\n if (aKeys.length !== bKeys.length) return false;\n\n for (const key of aKeys) {\n if (!Object.prototype.hasOwnProperty.call(bObj, key)) return false;\n if (!deepEquals(aObj[key], bObj[key])) return false;\n }\n return true;\n }\n\n return false;\n}\n\n/**\n * Deep clone a value for snapshot isolation\n */\nfunction deepClone<T>(value: T): T {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map(deepClone) as T;\n }\n\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(value)) {\n result[key] = deepClone((value as Record<string, unknown>)[key]);\n }\n return result as T;\n}\n\n// =============================================================================\n// Re-exports for convenience\n// =============================================================================\n\nexport { component, relation } from \"../entity\";\nexport type { ComponentId, EntityId, RelationId, WildcardRelationId } from \"../entity\";\nexport type { Query } from \"../query/query\";\nexport type { LifecycleCallback, LifecycleHook } from \"../types\";\nexport { World } from \"../world/world\";\n"],"mappings":"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"}
|