@adobe/data 0.9.21 → 0.9.22
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/service/dynamic-service/create.d.ts +84 -0
- package/dist/service/dynamic-service/create.js +97 -0
- package/dist/service/dynamic-service/create.js.map +1 -0
- package/dist/service/dynamic-service/create.test.d.ts +1 -0
- package/dist/service/dynamic-service/create.test.js +688 -0
- package/dist/service/dynamic-service/create.test.js.map +1 -0
- package/dist/service/dynamic-service/dynamic-service.d.ts +2 -1
- package/dist/service/dynamic-service/dynamic-service.js.map +1 -1
- package/dist/service/dynamic-service/public.d.ts +1 -1
- package/dist/service/dynamic-service/public.js +1 -1
- package/dist/service/dynamic-service/public.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { Database } from "../../ecs/index.js";
|
|
4
|
+
import { Observe } from "../../observe/index.js";
|
|
5
|
+
import { create, state, action } from "./create.js";
|
|
6
|
+
import { createPlugin } from "../../ecs/database/create-plugin.js";
|
|
7
|
+
// ──────────────────────────────────────────────────────────────
|
|
8
|
+
// Compile-time type inference checks using Assert<Equal<...>>
|
|
9
|
+
// These verify that Schema.ToType resolves exactly—not `any`.
|
|
10
|
+
// ──────────────────────────────────────────────────────────────
|
|
11
|
+
{
|
|
12
|
+
// number schema → input: number
|
|
13
|
+
action({
|
|
14
|
+
description: "Number",
|
|
15
|
+
schema: { type: "number" },
|
|
16
|
+
execute: (input) => {
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
// string schema → input: string
|
|
20
|
+
action({
|
|
21
|
+
description: "String",
|
|
22
|
+
schema: { type: "string" },
|
|
23
|
+
execute: (input) => {
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
// boolean schema → input: boolean
|
|
27
|
+
action({
|
|
28
|
+
description: "Boolean",
|
|
29
|
+
schema: { type: "boolean" },
|
|
30
|
+
execute: (input) => {
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
// object schema → input: { readonly hp: number; readonly name: string }
|
|
34
|
+
action({
|
|
35
|
+
description: "Object",
|
|
36
|
+
schema: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {
|
|
39
|
+
hp: { type: "number" },
|
|
40
|
+
name: { type: "string" },
|
|
41
|
+
},
|
|
42
|
+
required: ["hp", "name"],
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
},
|
|
45
|
+
execute: (input) => {
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
// object schema with optional property
|
|
49
|
+
action({
|
|
50
|
+
description: "Object with optional",
|
|
51
|
+
schema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
hp: { type: "number" },
|
|
55
|
+
label: { type: "string" },
|
|
56
|
+
},
|
|
57
|
+
required: ["hp"],
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
},
|
|
60
|
+
execute: (input) => {
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
// state value types
|
|
64
|
+
state({
|
|
65
|
+
schema: { type: "number" },
|
|
66
|
+
value: Observe.fromConstant(42),
|
|
67
|
+
});
|
|
68
|
+
state({
|
|
69
|
+
schema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
x: { type: "number" },
|
|
73
|
+
y: { type: "number" },
|
|
74
|
+
},
|
|
75
|
+
required: ["x", "y"],
|
|
76
|
+
additionalProperties: false,
|
|
77
|
+
},
|
|
78
|
+
value: Observe.fromConstant({ x: 1, y: 2 }),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// ──────────────────────────────────────────────────────────────
|
|
82
|
+
// Test suite
|
|
83
|
+
// ──────────────────────────────────────────────────────────────
|
|
84
|
+
const testPlugin = createPlugin({
|
|
85
|
+
resources: {
|
|
86
|
+
health: { default: 100 },
|
|
87
|
+
name: { default: "" },
|
|
88
|
+
alive: { default: true },
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const createTestDb = () => Database.create(testPlugin);
|
|
92
|
+
describe("DynamicService.create", () => {
|
|
93
|
+
describe("type acceptance", () => {
|
|
94
|
+
it("should accept matching state schema and value types", () => {
|
|
95
|
+
const db = createTestDb();
|
|
96
|
+
create({
|
|
97
|
+
states: {
|
|
98
|
+
currentHealth: state({
|
|
99
|
+
schema: { type: "number" },
|
|
100
|
+
enabled: db.observe.resources.alive,
|
|
101
|
+
value: db.observe.resources.health,
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
actions: {},
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
it("should accept string schema with string value", () => {
|
|
108
|
+
const db = createTestDb();
|
|
109
|
+
create({
|
|
110
|
+
states: {
|
|
111
|
+
playerName: state({
|
|
112
|
+
schema: { type: "string" },
|
|
113
|
+
enabled: db.observe.resources.alive,
|
|
114
|
+
value: db.observe.resources.name,
|
|
115
|
+
}),
|
|
116
|
+
},
|
|
117
|
+
actions: {},
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
it("should accept multiple states with different schemas", () => {
|
|
121
|
+
const db = createTestDb();
|
|
122
|
+
create({
|
|
123
|
+
states: {
|
|
124
|
+
currentHealth: state({
|
|
125
|
+
schema: { type: "number" },
|
|
126
|
+
value: db.observe.resources.health,
|
|
127
|
+
}),
|
|
128
|
+
playerName: state({
|
|
129
|
+
schema: { type: "string" },
|
|
130
|
+
value: db.observe.resources.name,
|
|
131
|
+
}),
|
|
132
|
+
},
|
|
133
|
+
actions: {},
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
it("should accept action with schema-typed input", () => {
|
|
137
|
+
create({
|
|
138
|
+
states: {},
|
|
139
|
+
actions: {
|
|
140
|
+
setHealth: action({
|
|
141
|
+
description: "Set health value",
|
|
142
|
+
schema: { type: "number" },
|
|
143
|
+
execute: async (input) => {
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
it("should accept action with schema: false (void execute)", () => {
|
|
150
|
+
create({
|
|
151
|
+
states: {},
|
|
152
|
+
actions: {
|
|
153
|
+
reset: action({
|
|
154
|
+
description: "Reset to defaults",
|
|
155
|
+
schema: false,
|
|
156
|
+
execute: async () => { },
|
|
157
|
+
}),
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
it("should accept object schema with matching value type", () => {
|
|
162
|
+
create({
|
|
163
|
+
states: {
|
|
164
|
+
stats: state({
|
|
165
|
+
schema: {
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: {
|
|
168
|
+
hp: { type: "number" },
|
|
169
|
+
label: { type: "string" },
|
|
170
|
+
},
|
|
171
|
+
required: ["hp"],
|
|
172
|
+
additionalProperties: false,
|
|
173
|
+
},
|
|
174
|
+
value: Observe.fromConstant({ hp: 50, label: "test" }),
|
|
175
|
+
}),
|
|
176
|
+
},
|
|
177
|
+
actions: {},
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
it("should accept action with object schema input", () => {
|
|
181
|
+
create({
|
|
182
|
+
states: {},
|
|
183
|
+
actions: {
|
|
184
|
+
configure: action({
|
|
185
|
+
description: "Configure settings",
|
|
186
|
+
schema: {
|
|
187
|
+
type: "object",
|
|
188
|
+
properties: {
|
|
189
|
+
hp: { type: "number" },
|
|
190
|
+
name: { type: "string" },
|
|
191
|
+
},
|
|
192
|
+
required: ["hp", "name"],
|
|
193
|
+
additionalProperties: false,
|
|
194
|
+
},
|
|
195
|
+
execute: async (input) => {
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
it("should accept mixed actions with and without schemas", () => {
|
|
202
|
+
create({
|
|
203
|
+
states: {},
|
|
204
|
+
actions: {
|
|
205
|
+
heal: action({
|
|
206
|
+
description: "Heal player",
|
|
207
|
+
schema: { type: "number" },
|
|
208
|
+
execute: async (amount) => {
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
reset: action({
|
|
212
|
+
description: "Reset to defaults",
|
|
213
|
+
schema: false,
|
|
214
|
+
execute: async () => { },
|
|
215
|
+
}),
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
it("should accept synchronous void-returning execute", () => {
|
|
220
|
+
let called = false;
|
|
221
|
+
create({
|
|
222
|
+
states: {},
|
|
223
|
+
actions: {
|
|
224
|
+
sync: action({
|
|
225
|
+
description: "Sync action",
|
|
226
|
+
schema: false,
|
|
227
|
+
execute: () => { called = true; },
|
|
228
|
+
}),
|
|
229
|
+
syncWithInput: action({
|
|
230
|
+
description: "Sync with input",
|
|
231
|
+
schema: { type: "number" },
|
|
232
|
+
execute: (input) => {
|
|
233
|
+
},
|
|
234
|
+
}),
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
it("should accept omitted enabled (defaults to always true)", () => {
|
|
239
|
+
const db = createTestDb();
|
|
240
|
+
create({
|
|
241
|
+
states: {
|
|
242
|
+
health: state({
|
|
243
|
+
schema: { type: "number" },
|
|
244
|
+
value: db.observe.resources.health,
|
|
245
|
+
}),
|
|
246
|
+
},
|
|
247
|
+
actions: {
|
|
248
|
+
heal: action({
|
|
249
|
+
description: "Heal player",
|
|
250
|
+
schema: { type: "number" },
|
|
251
|
+
execute: async (input) => { },
|
|
252
|
+
}),
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
describe("type rejection", () => {
|
|
258
|
+
it("should reject mismatched state value type (number schema, string value)", () => {
|
|
259
|
+
const db = createTestDb();
|
|
260
|
+
state({
|
|
261
|
+
schema: { type: "number" },
|
|
262
|
+
// @ts-expect-error - string value does not match number schema
|
|
263
|
+
value: db.observe.resources.name,
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
it("should reject mismatched state value type (string schema, number value)", () => {
|
|
267
|
+
const db = createTestDb();
|
|
268
|
+
state({
|
|
269
|
+
schema: { type: "string" },
|
|
270
|
+
// @ts-expect-error - number value does not match string schema
|
|
271
|
+
value: db.observe.resources.health,
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
it("should reject action execute with wrong input type", () => {
|
|
275
|
+
action({
|
|
276
|
+
description: "Set health value",
|
|
277
|
+
schema: { type: "number" },
|
|
278
|
+
// @ts-expect-error - execute receives string but schema requires number
|
|
279
|
+
execute: async (input) => { },
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
it("should reject void action that declares an input parameter", () => {
|
|
283
|
+
// @ts-expect-error - schema: false means no input parameter
|
|
284
|
+
action({
|
|
285
|
+
description: "Reset to defaults",
|
|
286
|
+
schema: false,
|
|
287
|
+
execute: async (input) => { },
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
describe("serviceName", () => {
|
|
292
|
+
it("should set serviceName for isService compatibility", () => {
|
|
293
|
+
const service = create({ states: {}, actions: {} });
|
|
294
|
+
expect(service.serviceName).toBe("dynamic-service");
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
describe("enabled defaults", () => {
|
|
298
|
+
it("should default state enabled to always true when omitted", async () => {
|
|
299
|
+
const db = createTestDb();
|
|
300
|
+
const service = create({
|
|
301
|
+
states: {
|
|
302
|
+
health: state({
|
|
303
|
+
schema: { type: "number" },
|
|
304
|
+
value: db.observe.resources.health,
|
|
305
|
+
}),
|
|
306
|
+
},
|
|
307
|
+
actions: {},
|
|
308
|
+
});
|
|
309
|
+
const states = await Observe.toPromise(service.states);
|
|
310
|
+
expect(states).toHaveProperty("health");
|
|
311
|
+
expect(states.health.value).toBe(100);
|
|
312
|
+
});
|
|
313
|
+
it("should default action enabled to always true when omitted", async () => {
|
|
314
|
+
const service = create({
|
|
315
|
+
states: {},
|
|
316
|
+
actions: {
|
|
317
|
+
heal: action({
|
|
318
|
+
description: "Heal player",
|
|
319
|
+
schema: { type: "number" },
|
|
320
|
+
execute: async (input) => { },
|
|
321
|
+
}),
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
const actions = await Observe.toPromise(service.actions);
|
|
325
|
+
expect(actions).toHaveProperty("heal");
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
describe("states observable", () => {
|
|
329
|
+
it("should emit enabled states with resolved values", async () => {
|
|
330
|
+
const db = createTestDb();
|
|
331
|
+
const service = create({
|
|
332
|
+
states: {
|
|
333
|
+
currentHealth: state({
|
|
334
|
+
schema: { type: "number" },
|
|
335
|
+
value: db.observe.resources.health,
|
|
336
|
+
}),
|
|
337
|
+
},
|
|
338
|
+
actions: {},
|
|
339
|
+
});
|
|
340
|
+
const states = await Observe.toPromise(service.states);
|
|
341
|
+
expect(states).toHaveProperty("currentHealth");
|
|
342
|
+
expect(states.currentHealth.schema).toEqual({ type: "number" });
|
|
343
|
+
expect(states.currentHealth.value).toBe(100);
|
|
344
|
+
});
|
|
345
|
+
it("should omit disabled states", async () => {
|
|
346
|
+
const db = createTestDb();
|
|
347
|
+
const service = create({
|
|
348
|
+
states: {
|
|
349
|
+
visible: state({
|
|
350
|
+
schema: { type: "number" },
|
|
351
|
+
enabled: Observe.fromConstant(true),
|
|
352
|
+
value: db.observe.resources.health,
|
|
353
|
+
}),
|
|
354
|
+
hidden: state({
|
|
355
|
+
schema: { type: "string" },
|
|
356
|
+
enabled: Observe.fromConstant(false),
|
|
357
|
+
value: db.observe.resources.name,
|
|
358
|
+
}),
|
|
359
|
+
},
|
|
360
|
+
actions: {},
|
|
361
|
+
});
|
|
362
|
+
const states = await Observe.toPromise(service.states);
|
|
363
|
+
expect(states).toHaveProperty("visible");
|
|
364
|
+
expect(states).not.toHaveProperty("hidden");
|
|
365
|
+
});
|
|
366
|
+
it("should update when enabled changes", () => {
|
|
367
|
+
const db = createTestDb();
|
|
368
|
+
const [enabledObserve, setEnabled] = Observe.createState(true);
|
|
369
|
+
const service = create({
|
|
370
|
+
states: {
|
|
371
|
+
health: state({
|
|
372
|
+
schema: { type: "number" },
|
|
373
|
+
enabled: enabledObserve,
|
|
374
|
+
value: db.observe.resources.health,
|
|
375
|
+
}),
|
|
376
|
+
},
|
|
377
|
+
actions: {},
|
|
378
|
+
});
|
|
379
|
+
const results = [];
|
|
380
|
+
service.states((s) => results.push(s));
|
|
381
|
+
expect(Object.keys(results[0])).toContain("health");
|
|
382
|
+
setEnabled(false);
|
|
383
|
+
expect(Object.keys(results[1])).not.toContain("health");
|
|
384
|
+
setEnabled(true);
|
|
385
|
+
expect(Object.keys(results[2])).toContain("health");
|
|
386
|
+
});
|
|
387
|
+
it("should update when value changes", () => {
|
|
388
|
+
const [healthObserve, setHealth] = Observe.createState(100);
|
|
389
|
+
const service = create({
|
|
390
|
+
states: {
|
|
391
|
+
health: state({
|
|
392
|
+
schema: { type: "number" },
|
|
393
|
+
value: healthObserve,
|
|
394
|
+
}),
|
|
395
|
+
},
|
|
396
|
+
actions: {},
|
|
397
|
+
});
|
|
398
|
+
const results = [];
|
|
399
|
+
service.states((s) => results.push(s));
|
|
400
|
+
expect(results[0].health.value).toBe(100);
|
|
401
|
+
setHealth(50);
|
|
402
|
+
expect(results[1].health.value).toBe(50);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
describe("actions observable", () => {
|
|
406
|
+
it("should emit enabled actions with bound execute", async () => {
|
|
407
|
+
const service = create({
|
|
408
|
+
states: {},
|
|
409
|
+
actions: {
|
|
410
|
+
heal: action({
|
|
411
|
+
description: "Heal player",
|
|
412
|
+
schema: { type: "number" },
|
|
413
|
+
execute: async (amount) => { },
|
|
414
|
+
}),
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
const actions = await Observe.toPromise(service.actions);
|
|
418
|
+
expect(actions).toHaveProperty("heal");
|
|
419
|
+
expect(actions.heal.description).toBe("Heal player");
|
|
420
|
+
expect(actions.heal.schema).toEqual({ type: "number" });
|
|
421
|
+
expect(typeof actions.heal.execute).toBe("function");
|
|
422
|
+
});
|
|
423
|
+
it("should omit disabled actions", async () => {
|
|
424
|
+
const service = create({
|
|
425
|
+
states: {},
|
|
426
|
+
actions: {
|
|
427
|
+
available: action({
|
|
428
|
+
description: "Available action",
|
|
429
|
+
schema: { type: "number" },
|
|
430
|
+
enabled: Observe.fromConstant(true),
|
|
431
|
+
execute: async (input) => { },
|
|
432
|
+
}),
|
|
433
|
+
unavailable: action({
|
|
434
|
+
description: "Unavailable action",
|
|
435
|
+
schema: false,
|
|
436
|
+
enabled: Observe.fromConstant(false),
|
|
437
|
+
execute: async () => { },
|
|
438
|
+
}),
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
const actions = await Observe.toPromise(service.actions);
|
|
442
|
+
expect(actions).toHaveProperty("available");
|
|
443
|
+
expect(actions).not.toHaveProperty("unavailable");
|
|
444
|
+
});
|
|
445
|
+
it("should update when enabled changes", () => {
|
|
446
|
+
const [enabledObserve, setEnabled] = Observe.createState(true);
|
|
447
|
+
const service = create({
|
|
448
|
+
states: {},
|
|
449
|
+
actions: {
|
|
450
|
+
heal: action({
|
|
451
|
+
description: "Heal player",
|
|
452
|
+
schema: { type: "number" },
|
|
453
|
+
enabled: enabledObserve,
|
|
454
|
+
execute: async (input) => { },
|
|
455
|
+
}),
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
const results = [];
|
|
459
|
+
service.actions((a) => results.push(a));
|
|
460
|
+
expect(Object.keys(results[0])).toContain("heal");
|
|
461
|
+
setEnabled(false);
|
|
462
|
+
expect(Object.keys(results[1])).not.toContain("heal");
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
describe("execute", () => {
|
|
466
|
+
it("should dispatch to the correct action with input", async () => {
|
|
467
|
+
let received;
|
|
468
|
+
const service = create({
|
|
469
|
+
states: {},
|
|
470
|
+
actions: {
|
|
471
|
+
setHealth: action({
|
|
472
|
+
description: "Set health value",
|
|
473
|
+
schema: { type: "number" },
|
|
474
|
+
execute: async (input) => { received = input; },
|
|
475
|
+
}),
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
const result = await service.execute("setHealth", 42);
|
|
479
|
+
expect(received).toBe(42);
|
|
480
|
+
expect(result).toBeUndefined();
|
|
481
|
+
});
|
|
482
|
+
it("should dispatch void action (no input)", async () => {
|
|
483
|
+
let called = false;
|
|
484
|
+
const service = create({
|
|
485
|
+
states: {},
|
|
486
|
+
actions: {
|
|
487
|
+
reset: action({
|
|
488
|
+
description: "Reset to defaults",
|
|
489
|
+
schema: false,
|
|
490
|
+
execute: async () => { called = true; },
|
|
491
|
+
}),
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
await service.execute("reset", undefined);
|
|
495
|
+
expect(called).toBe(true);
|
|
496
|
+
});
|
|
497
|
+
it("should dispatch synchronous void action", async () => {
|
|
498
|
+
let called = false;
|
|
499
|
+
const service = create({
|
|
500
|
+
states: {},
|
|
501
|
+
actions: {
|
|
502
|
+
sync: action({
|
|
503
|
+
description: "Sync action",
|
|
504
|
+
schema: false,
|
|
505
|
+
execute: () => { called = true; },
|
|
506
|
+
}),
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
await service.execute("sync", undefined);
|
|
510
|
+
expect(called).toBe(true);
|
|
511
|
+
});
|
|
512
|
+
it("should return error for unavailable action", async () => {
|
|
513
|
+
const service = create({
|
|
514
|
+
states: {},
|
|
515
|
+
actions: {
|
|
516
|
+
heal: action({
|
|
517
|
+
description: "Heal player",
|
|
518
|
+
schema: { type: "number" },
|
|
519
|
+
enabled: Observe.fromConstant(false),
|
|
520
|
+
execute: async (input) => { },
|
|
521
|
+
}),
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
const result = await service.execute("heal", 10);
|
|
525
|
+
expect(typeof result).toBe("string");
|
|
526
|
+
});
|
|
527
|
+
it("should return error for nonexistent action", async () => {
|
|
528
|
+
const service = create({
|
|
529
|
+
states: {},
|
|
530
|
+
actions: {},
|
|
531
|
+
});
|
|
532
|
+
const result = await service.execute("doesNotExist", undefined);
|
|
533
|
+
expect(typeof result).toBe("string");
|
|
534
|
+
});
|
|
535
|
+
it("should propagate ActionError from execute", async () => {
|
|
536
|
+
const service = create({
|
|
537
|
+
states: {},
|
|
538
|
+
actions: {
|
|
539
|
+
fail: action({
|
|
540
|
+
description: "Failing action",
|
|
541
|
+
schema: false,
|
|
542
|
+
execute: async () => "something went wrong",
|
|
543
|
+
}),
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
const result = await service.execute("fail", undefined);
|
|
547
|
+
expect(result).toBe("something went wrong");
|
|
548
|
+
});
|
|
549
|
+
it("should reject action that was available but is now disabled", async () => {
|
|
550
|
+
const [enabledObserve, setEnabled] = Observe.createState(true);
|
|
551
|
+
let called = false;
|
|
552
|
+
const service = create({
|
|
553
|
+
states: {},
|
|
554
|
+
actions: {
|
|
555
|
+
heal: action({
|
|
556
|
+
description: "Heal player",
|
|
557
|
+
schema: { type: "number" },
|
|
558
|
+
enabled: enabledObserve,
|
|
559
|
+
execute: async (input) => { called = true; },
|
|
560
|
+
}),
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
const ok = await service.execute("heal", 10);
|
|
564
|
+
expect(called).toBe(true);
|
|
565
|
+
expect(ok).toBeUndefined();
|
|
566
|
+
called = false;
|
|
567
|
+
setEnabled(false);
|
|
568
|
+
const err = await service.execute("heal", 10);
|
|
569
|
+
expect(called).toBe(false);
|
|
570
|
+
expect(typeof err).toBe("string");
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
describe("independent conditional toggles", () => {
|
|
574
|
+
it("should toggle states independently", () => {
|
|
575
|
+
const db = createTestDb();
|
|
576
|
+
const [enableA, setEnableA] = Observe.createState(true);
|
|
577
|
+
const [enableB, setEnableB] = Observe.createState(true);
|
|
578
|
+
const service = create({
|
|
579
|
+
states: {
|
|
580
|
+
health: state({
|
|
581
|
+
schema: { type: "number" },
|
|
582
|
+
enabled: enableA,
|
|
583
|
+
value: db.observe.resources.health,
|
|
584
|
+
}),
|
|
585
|
+
name: state({
|
|
586
|
+
schema: { type: "string" },
|
|
587
|
+
enabled: enableB,
|
|
588
|
+
value: db.observe.resources.name,
|
|
589
|
+
}),
|
|
590
|
+
},
|
|
591
|
+
actions: {},
|
|
592
|
+
});
|
|
593
|
+
const results = [];
|
|
594
|
+
service.states((s) => results.push(s));
|
|
595
|
+
expect(Object.keys(results[0])).toEqual(expect.arrayContaining(["health", "name"]));
|
|
596
|
+
setEnableA(false);
|
|
597
|
+
expect(results[1]).not.toHaveProperty("health");
|
|
598
|
+
expect(results[1]).toHaveProperty("name");
|
|
599
|
+
setEnableB(false);
|
|
600
|
+
expect(Object.keys(results[2])).toEqual([]);
|
|
601
|
+
setEnableB(true);
|
|
602
|
+
expect(results[3]).not.toHaveProperty("health");
|
|
603
|
+
expect(results[3]).toHaveProperty("name");
|
|
604
|
+
});
|
|
605
|
+
it("should toggle actions independently", () => {
|
|
606
|
+
const [enableHeal, setEnableHeal] = Observe.createState(true);
|
|
607
|
+
const [enableReset, setEnableReset] = Observe.createState(true);
|
|
608
|
+
const service = create({
|
|
609
|
+
states: {},
|
|
610
|
+
actions: {
|
|
611
|
+
heal: action({
|
|
612
|
+
description: "Heal player",
|
|
613
|
+
schema: { type: "number" },
|
|
614
|
+
enabled: enableHeal,
|
|
615
|
+
execute: async (input) => { },
|
|
616
|
+
}),
|
|
617
|
+
reset: action({
|
|
618
|
+
description: "Reset to defaults",
|
|
619
|
+
schema: false,
|
|
620
|
+
enabled: enableReset,
|
|
621
|
+
execute: async () => { },
|
|
622
|
+
}),
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
const results = [];
|
|
626
|
+
service.actions((a) => results.push(a));
|
|
627
|
+
expect(Object.keys(results[0])).toEqual(expect.arrayContaining(["heal", "reset"]));
|
|
628
|
+
setEnableHeal(false);
|
|
629
|
+
expect(results[1]).not.toHaveProperty("heal");
|
|
630
|
+
expect(results[1]).toHaveProperty("reset");
|
|
631
|
+
setEnableHeal(true);
|
|
632
|
+
setEnableReset(false);
|
|
633
|
+
const latest = results[results.length - 1];
|
|
634
|
+
expect(latest).toHaveProperty("heal");
|
|
635
|
+
expect(latest).not.toHaveProperty("reset");
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
describe("bound action execute", () => {
|
|
639
|
+
it("should execute action directly from actions observable", async () => {
|
|
640
|
+
let received;
|
|
641
|
+
const service = create({
|
|
642
|
+
states: {},
|
|
643
|
+
actions: {
|
|
644
|
+
heal: action({
|
|
645
|
+
description: "Heal player",
|
|
646
|
+
schema: { type: "number" },
|
|
647
|
+
execute: async (input) => { received = input; },
|
|
648
|
+
}),
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
const actions = await Observe.toPromise(service.actions);
|
|
652
|
+
await actions.heal.execute(77);
|
|
653
|
+
expect(received).toBe(77);
|
|
654
|
+
});
|
|
655
|
+
it("should execute void action directly from actions observable", async () => {
|
|
656
|
+
let called = false;
|
|
657
|
+
const service = create({
|
|
658
|
+
states: {},
|
|
659
|
+
actions: {
|
|
660
|
+
reset: action({
|
|
661
|
+
description: "Reset to defaults",
|
|
662
|
+
schema: false,
|
|
663
|
+
execute: async () => { called = true; },
|
|
664
|
+
}),
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
const actions = await Observe.toPromise(service.actions);
|
|
668
|
+
await actions.reset.execute();
|
|
669
|
+
expect(called).toBe(true);
|
|
670
|
+
});
|
|
671
|
+
it("should propagate ActionError from bound action execute", async () => {
|
|
672
|
+
const service = create({
|
|
673
|
+
states: {},
|
|
674
|
+
actions: {
|
|
675
|
+
fail: action({
|
|
676
|
+
description: "Failing action",
|
|
677
|
+
schema: false,
|
|
678
|
+
execute: async () => "bound error",
|
|
679
|
+
}),
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
const actions = await Observe.toPromise(service.actions);
|
|
683
|
+
const result = await actions.fail.execute();
|
|
684
|
+
expect(result).toBe("bound error");
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
//# sourceMappingURL=create.test.js.map
|