@adobe/data-testing 0.9.96
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/LICENSE +21 -0
- package/README.md +44 -0
- package/dist/conformance/discover.d.ts +7 -0
- package/dist/conformance/discover.js +48 -0
- package/dist/conformance/discover.js.map +1 -0
- package/dist/conformance/effects.type-test.d.ts +1 -0
- package/dist/conformance/effects.type-test.js +39 -0
- package/dist/conformance/effects.type-test.js.map +1 -0
- package/dist/conformance/entity-ref.d.ts +5 -0
- package/dist/conformance/entity-ref.js +28 -0
- package/dist/conformance/entity-ref.js.map +1 -0
- package/dist/conformance/public.d.ts +7 -0
- package/dist/conformance/public.js +7 -0
- package/dist/conformance/public.js.map +1 -0
- package/dist/conformance/record-effects.d.ts +16 -0
- package/dist/conformance/record-effects.js +93 -0
- package/dist/conformance/record-effects.js.map +1 -0
- package/dist/conformance/resolve.d.ts +3 -0
- package/dist/conformance/resolve.js +8 -0
- package/dist/conformance/resolve.js.map +1 -0
- package/dist/conformance/run-actions.d.ts +14 -0
- package/dist/conformance/run-actions.js +34 -0
- package/dist/conformance/run-actions.js.map +1 -0
- package/dist/conformance/run-computeds.d.ts +14 -0
- package/dist/conformance/run-computeds.js +45 -0
- package/dist/conformance/run-computeds.js.map +1 -0
- package/dist/conformance/run-feature.d.ts +30 -0
- package/dist/conformance/run-feature.js +61 -0
- package/dist/conformance/run-feature.js.map +1 -0
- package/dist/conformance/run-spec.d.ts +10 -0
- package/dist/conformance/run-spec.empty-cases.test.d.ts +1 -0
- package/dist/conformance/run-spec.empty-cases.test.js +14 -0
- package/dist/conformance/run-spec.empty-cases.test.js.map +1 -0
- package/dist/conformance/run-spec.empty.test.d.ts +1 -0
- package/dist/conformance/run-spec.empty.test.js +11 -0
- package/dist/conformance/run-spec.empty.test.js.map +1 -0
- package/dist/conformance/run-spec.js +83 -0
- package/dist/conformance/run-spec.js.map +1 -0
- package/dist/conformance/run-spec.test.d.ts +1 -0
- package/dist/conformance/run-spec.test.js +28 -0
- package/dist/conformance/run-spec.test.js.map +1 -0
- package/dist/conformance/run-transactions.d.ts +13 -0
- package/dist/conformance/run-transactions.js +31 -0
- package/dist/conformance/run-transactions.js.map +1 -0
- package/dist/conformance/types.d.ts +29 -0
- package/dist/conformance/types.js +3 -0
- package/dist/conformance/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/match/assert.d.ts +2 -0
- package/dist/match/assert.js +19 -0
- package/dist/match/assert.js.map +1 -0
- package/dist/match/match.d.ts +10 -0
- package/dist/match/match.js +82 -0
- package/dist/match/match.js.map +1 -0
- package/dist/match/match.test.d.ts +1 -0
- package/dist/match/match.test.js +42 -0
- package/dist/match/match.test.js.map +1 -0
- package/dist/match/matchers.d.ts +2 -0
- package/dist/match/matchers.js +14 -0
- package/dist/match/matchers.js.map +1 -0
- package/dist/match/public.d.ts +3 -0
- package/dist/match/public.js +5 -0
- package/dist/match/public.js.map +1 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Adobe, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @adobe/data-testing
|
|
2
|
+
|
|
3
|
+
Conformance-testing utilities for `@adobe/data` ECS features — the `Match` and
|
|
4
|
+
`Conformance` namespaces used by the co-located conformance pattern (see
|
|
5
|
+
`@adobe/data-ai`'s `features/data/state.md` and
|
|
6
|
+
`features/services/main-service/conformance.md`).
|
|
7
|
+
|
|
8
|
+
This package exists separately from `@adobe/data` so that installing
|
|
9
|
+
`@adobe/data` never pulls in a `vitest` peer dependency — only projects that
|
|
10
|
+
actually author conformance tests (and therefore already depend on `vitest`)
|
|
11
|
+
need it.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pnpm add -D @adobe/data-testing vitest
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// data/state/conformance-case.ts
|
|
23
|
+
import { Conformance as ConformanceApi } from "@adobe/data-testing";
|
|
24
|
+
import type { State } from "./state.js";
|
|
25
|
+
|
|
26
|
+
export type Conformance<F extends (...args: never[]) => unknown> = ConformanceApi.Cases<State, F>;
|
|
27
|
+
export const entity = ConformanceApi.entity;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
// data/state/spec.test.ts
|
|
32
|
+
import { Conformance } from "@adobe/data-testing";
|
|
33
|
+
import { State } from "./state.js";
|
|
34
|
+
import { transitions } from "./transitions.js";
|
|
35
|
+
|
|
36
|
+
Conformance.runSpec({ state: State, transitions });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`Match` provides tolerant, matcher-aware value comparison (`matches`, `ref`,
|
|
40
|
+
`anyNumber`, `anyString`) used by the `after` side of a conformance case.
|
|
41
|
+
`Conformance` provides the case types (`Cases`, `DerivationCases`, `Effects`)
|
|
42
|
+
and the runner drivers (`runSpec`, `runTransactions`, `runActions`,
|
|
43
|
+
`runComputeds`, `runFeature`) that drive vitest's `describe`/`it` against
|
|
44
|
+
spec-owned cases.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface Discovered {
|
|
2
|
+
readonly fn: (...args: unknown[]) => unknown;
|
|
3
|
+
readonly cases: readonly Record<string, unknown>[];
|
|
4
|
+
}
|
|
5
|
+
export declare const discoverTransitions: (modules: Record<string, Record<string, unknown>>) => Map<string, Discovered>;
|
|
6
|
+
export declare const discoverDerivations: (modules: Record<string, Record<string, unknown>>) => Map<string, Discovered>;
|
|
7
|
+
export declare const discoverOps: (source: Record<string, unknown>) => Map<string, (...a: never[]) => unknown>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
const scan = (modules, isKind) => {
|
|
3
|
+
const out = new Map();
|
|
4
|
+
for (const [path, module] of Object.entries(modules)) {
|
|
5
|
+
const names = Object.keys(module);
|
|
6
|
+
if (!names.includes("cases"))
|
|
7
|
+
continue;
|
|
8
|
+
const cases = module["cases"];
|
|
9
|
+
if (!Array.isArray(cases) || cases.length === 0)
|
|
10
|
+
continue;
|
|
11
|
+
const first = cases[0];
|
|
12
|
+
if (typeof first !== "object" || first === null || !isKind(first))
|
|
13
|
+
continue;
|
|
14
|
+
const fnName = names.find((key) => typeof module[key] === "function");
|
|
15
|
+
if (!fnName)
|
|
16
|
+
throw new Error(`${path} exports \`cases\` but no function to pair`);
|
|
17
|
+
out.set(fnName, { fn: module[fnName], cases: cases });
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
};
|
|
21
|
+
// Transitions — files whose cases are `{ before, args?, after }` — keyed by the
|
|
22
|
+
// transform's function name (the name the ecs transaction/action must share).
|
|
23
|
+
export const discoverTransitions = (modules) => scan(modules, (c) => "after" in c);
|
|
24
|
+
// Derivations — files whose cases are `{ input, value }` — keyed by the
|
|
25
|
+
// derivation's function name (the name the ecs computed must share).
|
|
26
|
+
export const discoverDerivations = (modules) => scan(modules, (c) => "value" in c);
|
|
27
|
+
// Normalize the ecs-op source to `name → fn`. Accepts EITHER a facet barrel
|
|
28
|
+
// (`import * as x` — values are the functions, keyed by export name) OR a directory
|
|
29
|
+
// glob (`import.meta.glob(..., { eager: true })` — values are modules, each
|
|
30
|
+
// contributing its function exports). The glob form finds ops that live beside a
|
|
31
|
+
// barrel but aren't registered in it (a conformance-only action kept out of the
|
|
32
|
+
// plugin facet), so they still pair by name.
|
|
33
|
+
export const discoverOps = (source) => {
|
|
34
|
+
const out = new Map();
|
|
35
|
+
for (const [key, value] of Object.entries(source)) {
|
|
36
|
+
if (typeof value === "function") {
|
|
37
|
+
out.set(key, value);
|
|
38
|
+
}
|
|
39
|
+
else if (value !== null && typeof value === "object") {
|
|
40
|
+
for (const [name, member] of Object.entries(value)) {
|
|
41
|
+
if (typeof member === "function")
|
|
42
|
+
out.set(name, member);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=discover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover.js","sourceRoot":"","sources":["../../src/conformance/discover.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAQvD,MAAM,IAAI,GAAG,CACX,OAAgD,EAChD,MAAuD,EAC9B,EAAE;IAC3B,MAAM,GAAG,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC1C,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACrD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAClC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,SAAS;QACvC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;QAC9B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAC1D,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACvB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,KAAgC,CAAC;YAAE,SAAS;QACvG,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,UAAU,CAAC,CAAC;QACtE,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,4CAA4C,CAAC,CAAC;QAClF,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,MAAM,CAAqB,EAAE,KAAK,EAAE,KAA4B,EAAE,CAAC,CAAC;IACnG,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF,gFAAgF;AAChF,8EAA8E;AAC9E,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,OAAgD,EAA2B,EAAE,CAC/G,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC;AAErC,wEAAwE;AACxE,qEAAqE;AACrE,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,OAAgD,EAA2B,EAAE,CAC/G,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC;AAErC,4EAA4E;AAC5E,oFAAoF;AACpF,4EAA4E;AAC5E,iFAAiF;AACjF,gFAAgF;AAChF,6CAA6C;AAC7C,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAA+B,EAA2C,EAAE;IACtG,MAAM,GAAG,GAAG,IAAI,GAAG,EAAsC,CAAC;IAC1D,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,OAAO,KAAK,KAAK,UAAU,EAAE,CAAC;YAChC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,KAAmC,CAAC,CAAC;QACpD,CAAC;aAAM,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YACvD,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBACnD,IAAI,OAAO,MAAM,KAAK,UAAU;oBAAE,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,MAAoC,CAAC,CAAC;YACxF,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// ===== POSITIVE — must compile =====
|
|
2
|
+
const ordered = { analytics: [["todoCreated", { name: "a" }], ["todoToggled"]] };
|
|
3
|
+
const anyOrder = {
|
|
4
|
+
analytics: new Set([["todoToggled"], ["allTodosCleared"]]),
|
|
5
|
+
};
|
|
6
|
+
const noArgMethod = { analytics: [["displayCompletedToggled"]] };
|
|
7
|
+
const empty = {};
|
|
8
|
+
void ordered;
|
|
9
|
+
void anyOrder;
|
|
10
|
+
void noArgMethod;
|
|
11
|
+
void empty;
|
|
12
|
+
// ===== NEGATIVE — each must error =====
|
|
13
|
+
const badMethod = {
|
|
14
|
+
// @ts-expect-error - "noSuchMethod" is not a method of AnalyticsService
|
|
15
|
+
analytics: [["noSuchMethod", { name: "a" }]],
|
|
16
|
+
};
|
|
17
|
+
const badArgs = {
|
|
18
|
+
// @ts-expect-error - todoCreated takes { name: string }, not { count }
|
|
19
|
+
analytics: [["todoCreated", { count: 1 }]],
|
|
20
|
+
};
|
|
21
|
+
const missingArgs = {
|
|
22
|
+
// @ts-expect-error - todoCreated requires its args element
|
|
23
|
+
analytics: [["todoCreated"]],
|
|
24
|
+
};
|
|
25
|
+
const extraArg = {
|
|
26
|
+
// @ts-expect-error - todoToggled takes no args
|
|
27
|
+
analytics: [["todoToggled", { name: "a" }]],
|
|
28
|
+
};
|
|
29
|
+
const dataKey = {
|
|
30
|
+
// @ts-expect-error - "name" is a data arg, not a service
|
|
31
|
+
name: [["todoCreated", { name: "a" }]],
|
|
32
|
+
};
|
|
33
|
+
void badMethod;
|
|
34
|
+
void badArgs;
|
|
35
|
+
void missingArgs;
|
|
36
|
+
void extraArg;
|
|
37
|
+
void dataKey;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=effects.type-test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effects.type-test.js","sourceRoot":"","sources":["../../src/conformance/effects.type-test.ts"],"names":[],"mappings":"AAqBA,sCAAsC;AACtC,MAAM,OAAO,GAAkB,EAAE,SAAS,EAAE,CAAC,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC;AAChG,MAAM,QAAQ,GAAkB;IAC9B,SAAS,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,aAAa,CAAU,EAAE,CAAC,iBAAiB,CAAU,CAAC,CAAC;CAC7E,CAAC;AACF,MAAM,WAAW,GAAkB,EAAE,SAAS,EAAE,CAAC,CAAC,yBAAyB,CAAC,CAAC,EAAE,CAAC;AAChF,MAAM,KAAK,GAAkB,EAAE,CAAC;AAChC,KAAK,OAAO,CAAC;AACb,KAAK,QAAQ,CAAC;AACd,KAAK,WAAW,CAAC;AACjB,KAAK,KAAK,CAAC;AAEX,yCAAyC;AACzC,MAAM,SAAS,GAAkB;IAC/B,wEAAwE;IACxE,SAAS,EAAE,CAAC,CAAC,cAAc,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;CAC7C,CAAC;AACF,MAAM,OAAO,GAAkB;IAC7B,uEAAuE;IACvE,SAAS,EAAE,CAAC,CAAC,aAAa,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;CAC3C,CAAC;AACF,MAAM,WAAW,GAAkB;IACjC,2DAA2D;IAC3D,SAAS,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC;CAC7B,CAAC;AACF,MAAM,QAAQ,GAAkB;IAC9B,+CAA+C;IAC/C,SAAS,EAAE,CAAC,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;CAC5C,CAAC;AACF,MAAM,OAAO,GAAkB;IAC7B,yDAAyD;IACzD,IAAI,EAAE,CAAC,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;CACvC,CAAC;AACF,KAAK,SAAS,CAAC;AACf,KAAK,OAAO,CAAC;AACb,KAAK,WAAW,CAAC;AACjB,KAAK,QAAQ,CAAC;AACd,KAAK,OAAO,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// A spec entity reference inside a case's `args`: `{ id: entity(1) }` names "the
|
|
2
|
+
// entity seeded for spec-id 1". It closes the one irreducible spec↔ecs vocabulary
|
|
3
|
+
// gap — identity: the pure spec reads the data-id, the ecs reads the entity the
|
|
4
|
+
// runner resolves. Typed as the id it stands in for, so it slots into the
|
|
5
|
+
// transform's own arg type (`{ id: number }`), exactly like `Match.anyNumber`.
|
|
6
|
+
const ENTITY_REF = Symbol.for("@adobe/data-testing:entity-ref");
|
|
7
|
+
export const entity = (specId) => ({ [ENTITY_REF]: specId });
|
|
8
|
+
const isEntityRef = (value) => typeof value === "object" && value !== null && ENTITY_REF in value;
|
|
9
|
+
// Adapt a case's `args` for one side of the conformance. `resolve` present → the
|
|
10
|
+
// ecs side (refs become seeded entities); absent → the pure-spec side (refs become
|
|
11
|
+
// their data-id). Only the top-level arg values are inspected — a `ref` is always a
|
|
12
|
+
// direct arg field. Non-ref values pass through untouched; a non-object `args`
|
|
13
|
+
// (a scalar `dt`, or `undefined`) passes through whole.
|
|
14
|
+
export const adaptArgs = (args, resolve) => {
|
|
15
|
+
if (args === null || typeof args !== "object" || Array.isArray(args))
|
|
16
|
+
return args;
|
|
17
|
+
let changed = false;
|
|
18
|
+
const next = { ...args };
|
|
19
|
+
for (const [key, value] of Object.entries(next)) {
|
|
20
|
+
if (isEntityRef(value)) {
|
|
21
|
+
const specId = value[ENTITY_REF];
|
|
22
|
+
next[key] = resolve ? resolve(specId) : specId;
|
|
23
|
+
changed = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return (changed ? next : args);
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=entity-ref.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-ref.js","sourceRoot":"","sources":["../../src/conformance/entity-ref.ts"],"names":[],"mappings":"AAIA,iFAAiF;AACjF,kFAAkF;AAClF,gFAAgF;AAChF,0EAA0E;AAC1E,+EAA+E;AAC/E,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;AAEhE,MAAM,CAAC,MAAM,MAAM,GAAG,CAAI,MAAS,EAAK,EAAE,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAiB,CAAC;AAEtF,MAAM,WAAW,GAAG,CAAC,KAAc,EAA+C,EAAE,CAClF,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,UAAU,IAAI,KAAK,CAAC;AAErE,iFAAiF;AACjF,mFAAmF;AACnF,oFAAoF;AACpF,+EAA+E;AAC/E,wDAAwD;AACxD,MAAM,CAAC,MAAM,SAAS,GAAG,CAAO,IAAU,EAAE,OAA0B,EAAQ,EAAE;IAC9E,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAClF,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,IAAI,GAA4B,EAAE,GAAI,IAAe,EAAE,CAAC;IAC9D,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAChD,IAAI,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC;YACjC,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAE,OAAO,CAAC,MAAM,CAAa,CAAC,CAAC,CAAC,MAAM,CAAC;YAC5D,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAS,CAAC;AACzC,CAAC,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { Case, Cases, DerivationCase, DerivationCases, Effects, ServiceCall } from "./types.js";
|
|
2
|
+
export { entity } from "./entity-ref.js";
|
|
3
|
+
export { runSpec, type SpecRunConfig } from "./run-spec.js";
|
|
4
|
+
export { runTransactions, type TransactionRunConfig } from "./run-transactions.js";
|
|
5
|
+
export { runActions, type ActionRunConfig } from "./run-actions.js";
|
|
6
|
+
export { runComputeds, type ComputedRunConfig } from "./run-computeds.js";
|
|
7
|
+
export { runFeature, type FeatureRunConfig, type Projection } from "./run-feature.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { entity } from "./entity-ref.js";
|
|
2
|
+
export { runSpec } from "./run-spec.js";
|
|
3
|
+
export { runTransactions } from "./run-transactions.js";
|
|
4
|
+
export { runActions } from "./run-actions.js";
|
|
5
|
+
export { runComputeds } from "./run-computeds.js";
|
|
6
|
+
export { runFeature } from "./run-feature.js";
|
|
7
|
+
//# sourceMappingURL=public.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public.js","sourceRoot":"","sources":["../../src/conformance/public.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,OAAO,EAAsB,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAE,eAAe,EAA6B,MAAM,uBAAuB,CAAC;AACnF,OAAO,EAAE,UAAU,EAAwB,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,YAAY,EAA0B,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,UAAU,EAA0C,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Effects } from "./types.js";
|
|
2
|
+
export type RecordedCall = readonly [string, ...unknown[]];
|
|
3
|
+
export declare const recordCalls: <S extends object>(service: S) => {
|
|
4
|
+
service: S;
|
|
5
|
+
calls: RecordedCall[];
|
|
6
|
+
};
|
|
7
|
+
export declare const recordArgServices: <Args>(args: Args) => {
|
|
8
|
+
args: Args;
|
|
9
|
+
calls: Record<string, RecordedCall[]>;
|
|
10
|
+
};
|
|
11
|
+
export declare const splitAndRecordServices: <Args>(args: Args) => {
|
|
12
|
+
services: Record<string, object>;
|
|
13
|
+
input: Record<string, unknown>;
|
|
14
|
+
calls: Record<string, RecordedCall[]>;
|
|
15
|
+
};
|
|
16
|
+
export declare const expectEffects: (calls: Record<string, readonly RecordedCall[]>, effects: Effects<Record<string, unknown>> | undefined) => void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { equalsUnordered } from "@adobe/data";
|
|
3
|
+
import { matches } from "../match/match.js";
|
|
4
|
+
// A runtime service value: a non-array object with at least one method (mirrors
|
|
5
|
+
// the compile-time service detection in `types.ts`).
|
|
6
|
+
const isServiceValue = (value) => typeof value === "object" &&
|
|
7
|
+
value !== null &&
|
|
8
|
+
!Array.isArray(value) &&
|
|
9
|
+
Object.values(value).some((member) => typeof member === "function");
|
|
10
|
+
// Wrap a plain-object service so each method call is recorded, then delegates.
|
|
11
|
+
// No Proxy (services are plain objects with own enumerable methods), so we
|
|
12
|
+
// enumerate and closure-wrap each function.
|
|
13
|
+
export const recordCalls = (service) => {
|
|
14
|
+
const calls = [];
|
|
15
|
+
const wrapped = Object.fromEntries(Object.entries(service).map(([key, value]) => [
|
|
16
|
+
key,
|
|
17
|
+
typeof value === "function"
|
|
18
|
+
? (...args) => {
|
|
19
|
+
calls.push([key, ...args]);
|
|
20
|
+
return value(...args);
|
|
21
|
+
}
|
|
22
|
+
: value,
|
|
23
|
+
]));
|
|
24
|
+
return { service: wrapped, calls };
|
|
25
|
+
};
|
|
26
|
+
// Wrap the injected services IN PLACE within a case's `args` (leaving plain data
|
|
27
|
+
// untouched), returning the args ready to pass to a pure transform plus the
|
|
28
|
+
// per-service `calls` map. Used by the pure spec runner, which calls the transform
|
|
29
|
+
// with its full args; the ecs action runner uses `splitAndRecordServices` instead.
|
|
30
|
+
export const recordArgServices = (args) => {
|
|
31
|
+
const calls = {};
|
|
32
|
+
if (args === null || typeof args !== "object" || Array.isArray(args))
|
|
33
|
+
return { args, calls };
|
|
34
|
+
const next = { ...args };
|
|
35
|
+
for (const [key, value] of Object.entries(args)) {
|
|
36
|
+
if (isServiceValue(value)) {
|
|
37
|
+
const recorded = recordCalls(value);
|
|
38
|
+
next[key] = recorded.service;
|
|
39
|
+
calls[key] = recorded.calls;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { args: next, calls };
|
|
43
|
+
};
|
|
44
|
+
// Split a case's `args` into the injected services (wrapped for recording) and
|
|
45
|
+
// the remaining plain data. Keyed by the same arg name so `calls` matches against
|
|
46
|
+
// `effects`. A no-arg case (`undefined` args) splits into nothing.
|
|
47
|
+
export const splitAndRecordServices = (args) => {
|
|
48
|
+
const services = {};
|
|
49
|
+
const input = {};
|
|
50
|
+
const calls = {};
|
|
51
|
+
if (args !== null && typeof args === "object" && !Array.isArray(args)) {
|
|
52
|
+
for (const [key, value] of Object.entries(args)) {
|
|
53
|
+
if (isServiceValue(value)) {
|
|
54
|
+
const recorded = recordCalls(value);
|
|
55
|
+
services[key] = recorded.service;
|
|
56
|
+
calls[key] = recorded.calls;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
input[key] = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { services, input, calls };
|
|
64
|
+
};
|
|
65
|
+
const showCalls = (calls) => {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.stringify(calls) ?? String(calls);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return String(calls);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
// Assert the calls recorded against one service match the case's expectation for
|
|
74
|
+
// it: an Array expects exactly these calls in order; a Set expects the same calls
|
|
75
|
+
// in any order (multiset). Absent expectation ⇒ no calls expected. Ordered calls
|
|
76
|
+
// compare matcher-aware, so a call arg may itself use `anyNumber`.
|
|
77
|
+
const expectServiceCalls = (key, recorded, expected) => {
|
|
78
|
+
const ok = expected instanceof Set ? equalsUnordered(recorded, [...expected]) : matches(recorded, expected ?? []);
|
|
79
|
+
if (!ok) {
|
|
80
|
+
throw new Error(`effects mismatch on "${key}":\n recorded: ${showCalls(recorded)}\n expected: ${showCalls(expected instanceof Set ? [...expected] : (expected ?? []))}`);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
// Assert each service DECLARED in `effects` saw exactly its expected calls (an
|
|
84
|
+
// extra or missing call on a declared service fails). Services not listed — e.g. a
|
|
85
|
+
// value-returning read like `generateName` — are ignored, so `effects` captures the
|
|
86
|
+
// fire-and-forget side effects a case chooses to assert.
|
|
87
|
+
export const expectEffects = (calls, effects) => {
|
|
88
|
+
const expected = (effects ?? {});
|
|
89
|
+
for (const key of Object.keys(expected)) {
|
|
90
|
+
expectServiceCalls(key, calls[key] ?? [], expected[key]);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
//# sourceMappingURL=record-effects.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-effects.js","sourceRoot":"","sources":["../../src/conformance/record-effects.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { Entity } from "@adobe/data/ecs";
|
|
3
|
+
// Build a resolver from a `fromState` seed map. A feature whose transactions are
|
|
4
|
+
// addressed by index or are singleton (no id → entity mapping) returns `void`
|
|
5
|
+
// from `fromState`; its resolver is then never called, and any id resolves to
|
|
6
|
+
// `Entity.none`.
|
|
7
|
+
export const resolver = (seeded) => (id) => (seeded ? seeded.get(id) : undefined) ?? Entity.none;
|
|
8
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/conformance/resolve.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AASzC,iFAAiF;AACjF,8EAA8E;AAC9E,8EAA8E;AAC9E,iBAAiB;AACjB,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAK,MAAsC,EAAe,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAC1F,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Entity } from "@adobe/data/ecs";
|
|
2
|
+
import type { MatchOptions } from "../match/match.js";
|
|
3
|
+
export interface ActionRunConfig<Db, Store, State> {
|
|
4
|
+
readonly makeDb: (services: Record<string, object>) => Db;
|
|
5
|
+
readonly store: (db: Db) => Store;
|
|
6
|
+
readonly fromState: (store: Store, before: State) => ReadonlyMap<unknown, Entity> | void;
|
|
7
|
+
readonly toState: (store: Store) => State;
|
|
8
|
+
readonly transitions: Record<string, Record<string, unknown>>;
|
|
9
|
+
readonly actions: Record<string, unknown>;
|
|
10
|
+
readonly initial?: State;
|
|
11
|
+
readonly seedContext?: (db: Db, before: State, args: unknown) => void;
|
|
12
|
+
readonly match?: MatchOptions;
|
|
13
|
+
}
|
|
14
|
+
export declare function runActions<Db, Store, State>(config: ActionRunConfig<Db, Store, State>): void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { describe, it } from "vitest";
|
|
3
|
+
import { assert } from "../match/assert.js";
|
|
4
|
+
import { adaptArgs } from "./entity-ref.js";
|
|
5
|
+
import { discoverTransitions, discoverOps } from "./discover.js";
|
|
6
|
+
import { splitAndRecordServices, expectEffects } from "./record-effects.js";
|
|
7
|
+
import { resolver } from "./resolve.js";
|
|
8
|
+
// The single conformance test for every ecs action: each transition's cases run
|
|
9
|
+
// against its same-named action, asserting state and the declared effects.
|
|
10
|
+
export function runActions(config) {
|
|
11
|
+
const transitions = discoverTransitions(config.transitions);
|
|
12
|
+
for (const [name, action] of discoverOps(config.actions)) {
|
|
13
|
+
const paired = transitions.get(name);
|
|
14
|
+
if (!paired)
|
|
15
|
+
continue; // action with no transition (e.g. a streaming port) — not conformed here
|
|
16
|
+
describe(`${name} action conforms`, () => {
|
|
17
|
+
for (const testCase of paired.cases) {
|
|
18
|
+
it(testCase.name, async () => {
|
|
19
|
+
const { services, input, calls } = splitAndRecordServices(testCase.args);
|
|
20
|
+
// Case `before` is a delta over the feature default.
|
|
21
|
+
const before = { ...(config.initial ?? {}), ...testCase.before };
|
|
22
|
+
const db = config.makeDb(services);
|
|
23
|
+
const resolve = resolver(config.fromState(config.store(db), before));
|
|
24
|
+
config.seedContext?.(db, before, testCase.args);
|
|
25
|
+
await action(db, adaptArgs(input, resolve));
|
|
26
|
+
// `after` is a writes patch — compare `toState` against it merged over `before`.
|
|
27
|
+
assert(config.toState(config.store(db)), { ...before, ...testCase.after }, config.match);
|
|
28
|
+
expectEffects(calls, testCase.effects);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=run-actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-actions.js","sourceRoot":"","sources":["../../src/conformance/run-actions.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAwBxC,gFAAgF;AAChF,2EAA2E;AAC3E,MAAM,UAAU,UAAU,CAAmB,MAAyC;IACpF,MAAM,WAAW,GAAG,mBAAmB,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAC5D,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QACzD,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,CAAC,MAAM;YAAE,SAAS,CAAC,yEAAyE;QAChG,QAAQ,CAAC,GAAG,IAAI,kBAAkB,EAAE,GAAG,EAAE;YACvC,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;gBACpC,EAAE,CAAC,QAAQ,CAAC,IAAc,EAAE,KAAK,IAAI,EAAE;oBACrC,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,sBAAsB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;oBACzE,qDAAqD;oBACrD,MAAM,MAAM,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,GAAI,QAAQ,CAAC,MAAiB,EAAW,CAAC;oBACtF,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;oBACnC,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;oBACrE,MAAM,CAAC,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;oBAChD,MAAO,MAAuD,CAAC,EAAE,EAAE,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;oBAC9F,iFAAiF;oBACjF,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,GAAI,MAAiB,EAAE,GAAI,QAAQ,CAAC,KAAgB,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;oBACjH,aAAa,CAAC,KAAK,EAAE,QAAQ,CAAC,OAAgB,CAAC,CAAC;gBAClD,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Entity } from "@adobe/data/ecs";
|
|
2
|
+
import type { MatchOptions } from "../match/match.js";
|
|
3
|
+
export interface ComputedRunConfig<Db, Store, State> {
|
|
4
|
+
readonly makeDb: () => Db;
|
|
5
|
+
readonly store: (db: Db) => Store;
|
|
6
|
+
readonly fromState: (store: Store, input: State) => unknown;
|
|
7
|
+
readonly toData?: (store: Store, entity: Entity) => unknown;
|
|
8
|
+
readonly derivations: Record<string, Record<string, unknown>>;
|
|
9
|
+
readonly computeds: Record<string, unknown>;
|
|
10
|
+
readonly hydrate?: readonly string[];
|
|
11
|
+
readonly initial?: State;
|
|
12
|
+
readonly match?: MatchOptions;
|
|
13
|
+
}
|
|
14
|
+
export declare function runComputeds<Db, Store, State>(config: ComputedRunConfig<Db, Store, State>): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { describe, it } from "vitest";
|
|
3
|
+
import { assert } from "../match/assert.js";
|
|
4
|
+
import { discoverDerivations, discoverOps } from "./discover.js";
|
|
5
|
+
// Read a computed's synchronous emission: subscribe once, capture, unsubscribe.
|
|
6
|
+
const readComputed = (observe) => {
|
|
7
|
+
let value;
|
|
8
|
+
let read = false;
|
|
9
|
+
const unsubscribe = observe((next) => {
|
|
10
|
+
value = next;
|
|
11
|
+
read = true;
|
|
12
|
+
});
|
|
13
|
+
unsubscribe();
|
|
14
|
+
if (!read)
|
|
15
|
+
throw new Error("computed did not emit synchronously on subscribe");
|
|
16
|
+
return value;
|
|
17
|
+
};
|
|
18
|
+
// The single conformance test for every ecs computed backing a `data/state`
|
|
19
|
+
// derivation. Build `makeDb` from the `ComputedDatabase` layer (not the assembled
|
|
20
|
+
// db): a behaviour layer above may `withCache` a pre-seed value that a direct
|
|
21
|
+
// `fromState` seed cannot invalidate.
|
|
22
|
+
export function runComputeds(config) {
|
|
23
|
+
const derivations = discoverDerivations(config.derivations);
|
|
24
|
+
const hydrate = new Set(config.hydrate ?? []);
|
|
25
|
+
for (const [name, computed] of discoverOps(config.computeds)) {
|
|
26
|
+
const paired = derivations.get(name);
|
|
27
|
+
if (!paired)
|
|
28
|
+
continue; // computed with no `state/` derivation — covered by its data/<type> helper
|
|
29
|
+
describe(`${name} computed conforms`, () => {
|
|
30
|
+
for (const testCase of paired.cases) {
|
|
31
|
+
it(testCase.name, () => {
|
|
32
|
+
const db = config.makeDb();
|
|
33
|
+
const input = { ...(config.initial ?? {}), ...testCase.input };
|
|
34
|
+
config.fromState(config.store(db), input);
|
|
35
|
+
const raw = readComputed(computed(db));
|
|
36
|
+
const value = hydrate.has(name) && config.toData
|
|
37
|
+
? raw.map((e) => config.toData(config.store(db), e))
|
|
38
|
+
: raw;
|
|
39
|
+
assert(value, testCase.value, config.match);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=run-computeds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-computeds.js","sourceRoot":"","sources":["../../src/conformance/run-computeds.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAGtC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjE,gFAAgF;AAChF,MAAM,YAAY,GAAG,CAAI,OAAmB,EAAK,EAAE;IACjD,IAAI,KAAS,CAAC;IACd,IAAI,IAAI,GAAG,KAAK,CAAC;IACjB,MAAM,WAAW,GAAG,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QACnC,KAAK,GAAG,IAAI,CAAC;QACb,IAAI,GAAG,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;IACH,WAAW,EAAE,CAAC;IACd,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IAC/E,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAsBF,4EAA4E;AAC5E,kFAAkF;AAClF,8EAA8E;AAC9E,sCAAsC;AACtC,MAAM,UAAU,YAAY,CAAmB,MAA2C;IACxF,MAAM,WAAW,GAAG,mBAAmB,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;IAC9C,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7D,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,CAAC,MAAM;YAAE,SAAS,CAAC,2EAA2E;QAClG,QAAQ,CAAC,GAAG,IAAI,oBAAoB,EAAE,GAAG,EAAE;YACzC,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;gBACpC,EAAE,CAAC,QAAQ,CAAC,IAAc,EAAE,GAAG,EAAE;oBAC/B,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;oBAC3B,MAAM,KAAK,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,GAAI,QAAQ,CAAC,KAAgB,EAAW,CAAC;oBACpF,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;oBAC1C,MAAM,GAAG,GAAG,YAAY,CAAE,QAAwC,CAAC,EAAE,CAAC,CAAC,CAAC;oBACxE,MAAM,KAAK,GACT,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,MAAM;wBAChC,CAAC,CAAE,GAAyB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,MAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;wBAC5E,CAAC,CAAC,GAAG,CAAC;oBACV,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC9C,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Database } from "@adobe/data/ecs";
|
|
2
|
+
import type { Entity } from "@adobe/data/ecs";
|
|
3
|
+
import type { MatchOptions } from "../match/match.js";
|
|
4
|
+
export interface Projection<Store, State> {
|
|
5
|
+
readonly fromState: (store: Store, state: State) => ReadonlyMap<unknown, Entity> | void;
|
|
6
|
+
readonly toState: (store: Store) => State;
|
|
7
|
+
readonly toData?: (store: Store, entity: Entity) => unknown;
|
|
8
|
+
}
|
|
9
|
+
export interface FeatureRunConfig<State, StoreT, Db extends {
|
|
10
|
+
store: StoreT;
|
|
11
|
+
}> {
|
|
12
|
+
readonly state: {
|
|
13
|
+
create(): State;
|
|
14
|
+
readonly samples?: readonly State[];
|
|
15
|
+
};
|
|
16
|
+
readonly transitions: Record<string, Record<string, unknown>>;
|
|
17
|
+
readonly plugin: Database.Plugin;
|
|
18
|
+
readonly computedPlugin?: Database.Plugin;
|
|
19
|
+
readonly projection: Projection<StoreT, State>;
|
|
20
|
+
readonly hydrate?: readonly string[];
|
|
21
|
+
readonly match?: MatchOptions;
|
|
22
|
+
readonly ops?: {
|
|
23
|
+
readonly transactions?: Record<string, unknown>;
|
|
24
|
+
readonly actions?: Record<string, unknown>;
|
|
25
|
+
readonly computeds?: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export declare function runFeature<State, StoreT, Db extends {
|
|
29
|
+
store: StoreT;
|
|
30
|
+
}>(config: FeatureRunConfig<State, StoreT, Db>): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { describe, it } from "vitest";
|
|
3
|
+
import { Database, Store } from "@adobe/data/ecs";
|
|
4
|
+
import { assert } from "../match/assert.js";
|
|
5
|
+
import { runTransactions } from "./run-transactions.js";
|
|
6
|
+
import { runActions } from "./run-actions.js";
|
|
7
|
+
import { runComputeds } from "./run-computeds.js";
|
|
8
|
+
export function runFeature(config) {
|
|
9
|
+
const initial = config.state.create();
|
|
10
|
+
const { fromState, toState, toData } = config.projection;
|
|
11
|
+
const facets = config.plugin;
|
|
12
|
+
// A plugin carries the schema facets, so `Store.create` / `Database.create`
|
|
13
|
+
// accept it; the resulting store/db is the projection's `StoreT`/`Db`.
|
|
14
|
+
const makeStore = () => Store.create(config.plugin);
|
|
15
|
+
runTransactions({
|
|
16
|
+
createStore: makeStore,
|
|
17
|
+
fromState,
|
|
18
|
+
toState,
|
|
19
|
+
initial,
|
|
20
|
+
transitions: config.transitions,
|
|
21
|
+
transactions: config.ops?.transactions ?? facets.transactions,
|
|
22
|
+
match: config.match,
|
|
23
|
+
});
|
|
24
|
+
runActions({
|
|
25
|
+
makeDb: (services) => Database.toSystemDatabase(Database.create(config.plugin, { services })),
|
|
26
|
+
store: (db) => db.store,
|
|
27
|
+
fromState,
|
|
28
|
+
toState,
|
|
29
|
+
initial,
|
|
30
|
+
transitions: config.transitions,
|
|
31
|
+
actions: config.ops?.actions ?? facets.actions,
|
|
32
|
+
match: config.match,
|
|
33
|
+
});
|
|
34
|
+
if (config.computedPlugin) {
|
|
35
|
+
const computedFacets = config.computedPlugin;
|
|
36
|
+
runComputeds({
|
|
37
|
+
makeDb: () => Database.toSystemDatabase(Database.create(config.computedPlugin)),
|
|
38
|
+
store: (db) => db.store,
|
|
39
|
+
fromState,
|
|
40
|
+
toData,
|
|
41
|
+
initial,
|
|
42
|
+
derivations: config.transitions,
|
|
43
|
+
computeds: config.ops?.computeds ?? computedFacets.computed,
|
|
44
|
+
hydrate: config.hydrate,
|
|
45
|
+
match: config.match,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const samples = config.state.samples ?? [];
|
|
49
|
+
if (samples.length > 0) {
|
|
50
|
+
describe("projection round-trips (toState ∘ fromState ≡ identity)", () => {
|
|
51
|
+
samples.forEach((sample, index) => {
|
|
52
|
+
it(`sample ${index}`, () => {
|
|
53
|
+
const store = makeStore();
|
|
54
|
+
fromState(store, sample);
|
|
55
|
+
assert(toState(store), sample, config.match);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=run-feature.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-feature.js","sourceRoot":"","sources":["../../src/conformance/run-feature.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MatchOptions } from "../match/match.js";
|
|
2
|
+
export interface SpecRunConfig {
|
|
3
|
+
readonly state?: {
|
|
4
|
+
create(): object;
|
|
5
|
+
};
|
|
6
|
+
readonly transitions: Record<string, Record<string, unknown>>;
|
|
7
|
+
readonly match?: MatchOptions;
|
|
8
|
+
readonly label?: (path: string, fnName: string | undefined) => string;
|
|
9
|
+
}
|
|
10
|
+
export declare const runSpec: (config: SpecRunConfig) => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|