@playfast/reform-proof 1.3.0 → 1.4.1
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/engine.d.ts +2 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +9 -7
- package/dist/engine.js.map +1 -1
- package/dist/engineCoverage.d.ts +11 -0
- package/dist/engineCoverage.d.ts.map +1 -0
- package/dist/engineCoverage.js +48 -0
- package/dist/engineCoverage.js.map +1 -0
- package/dist/engineMounted.d.ts +4 -4
- package/dist/engineMounted.d.ts.map +1 -1
- package/dist/engineMounted.js +9 -4
- package/dist/engineMounted.js.map +1 -1
- package/dist/engineSink.d.ts +18 -1
- package/dist/engineSink.d.ts.map +1 -1
- package/dist/engineSink.js +22 -2
- package/dist/engineSink.js.map +1 -1
- package/dist/engineTreeFacade.d.ts +2 -2
- package/dist/engineTreeFacade.d.ts.map +1 -1
- package/dist/engineTreeFacade.js +42 -8
- package/dist/engineTreeFacade.js.map +1 -1
- package/dist/engineTreeTypes.d.ts +3 -1
- package/dist/engineTreeTypes.d.ts.map +1 -1
- package/dist/engineTreeTypes.js.map +1 -1
- package/dist/fingerprint.d.ts.map +1 -1
- package/dist/fingerprint.js +32 -9
- package/dist/fingerprint.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/coverage-host.test.ts +183 -0
- package/src/engine.ts +15 -22
- package/src/engineCoverage.ts +86 -0
- package/src/engineMounted.ts +16 -13
- package/src/engineSink.ts +70 -2
- package/src/engineTreeFacade.ts +65 -15
- package/src/engineTreeTypes.ts +3 -0
- package/src/fingerprint.ts +45 -10
- package/src/index.ts +1 -1
- package/src/proof-attribution.test.ts +210 -0
- package/src/test-clock.test.ts +16 -6
package/dist/fingerprint.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import * as Arr from 'effect/Array';
|
|
1
2
|
import * as Option from 'effect/Option';
|
|
3
|
+
import * as Order from 'effect/Order';
|
|
2
4
|
const readPropertyOption = Option.liftThrowable((target, key) => Reflect.get(target, key));
|
|
3
5
|
const readProperty = (target, key) => Option.getOrElse(readPropertyOption(target, key), () => '[Property read threw]');
|
|
4
6
|
const propertyKeyFingerprint = (key) => typeof key === 'symbol'
|
|
@@ -6,7 +8,7 @@ const propertyKeyFingerprint = (key) => typeof key === 'symbol'
|
|
|
6
8
|
: typeof key === 'number'
|
|
7
9
|
? `number-key:${String(key)}`
|
|
8
10
|
: `key:${key.length}:${key}`;
|
|
9
|
-
const valueFingerprint = (candidate,
|
|
11
|
+
const valueFingerprint = (candidate, walk) => {
|
|
10
12
|
if (typeof candidate === 'bigint') {
|
|
11
13
|
return `bigint:${String(candidate)}`;
|
|
12
14
|
}
|
|
@@ -31,25 +33,46 @@ const valueFingerprint = (candidate, seen) => {
|
|
|
31
33
|
if (candidate === null) {
|
|
32
34
|
return 'null';
|
|
33
35
|
}
|
|
34
|
-
if (seen.has(candidate)) {
|
|
36
|
+
if (walk.seen.has(candidate)) {
|
|
35
37
|
return '[Circular]';
|
|
36
38
|
}
|
|
37
|
-
|
|
39
|
+
const remembered = walk.memo.get(candidate);
|
|
40
|
+
if (remembered !== undefined) {
|
|
41
|
+
return remembered;
|
|
42
|
+
}
|
|
43
|
+
// `seen` marks the path currently being walked, not everything ever visited: it
|
|
44
|
+
// is unwound below. Without that, a value that merely repeats a reference — the
|
|
45
|
+
// same config object under two keys, a DAG rather than a cycle — reads as
|
|
46
|
+
// `[Circular]` on its second appearance, so two equal values fingerprint
|
|
47
|
+
// differently and `toEqual` fails on them.
|
|
48
|
+
walk.seen.add(candidate);
|
|
49
|
+
const printed = structuralFingerprint(candidate, walk);
|
|
50
|
+
walk.seen.delete(candidate);
|
|
51
|
+
// Only a print that did not depend on where the walk had been is reusable: one
|
|
52
|
+
// containing `[Circular]` describes this path, not this value.
|
|
53
|
+
if (!printed.includes('[Circular]')) {
|
|
54
|
+
walk.memo.set(candidate, printed);
|
|
55
|
+
}
|
|
56
|
+
return printed;
|
|
57
|
+
};
|
|
58
|
+
const structuralFingerprint = (candidate, walk) => {
|
|
38
59
|
if (Array.isArray(candidate)) {
|
|
39
|
-
return `[${candidate.map((arrayEntry) => valueFingerprint(arrayEntry,
|
|
60
|
+
return `[${candidate.map((arrayEntry) => valueFingerprint(arrayEntry, walk)).join(',')}]`;
|
|
40
61
|
}
|
|
41
62
|
if (candidate instanceof Date) {
|
|
42
63
|
return `date:${String(candidate.getTime())}`;
|
|
43
64
|
}
|
|
44
65
|
if (candidate instanceof Map) {
|
|
45
|
-
return `map:[${Array.from(candidate, ([mapKey, mapEntry]) => `${valueFingerprint(mapKey,
|
|
66
|
+
return `map:[${Array.from(candidate, ([mapKey, mapEntry]) => `${valueFingerprint(mapKey, walk)}=>${valueFingerprint(mapEntry, walk)}`).join(',')}]`;
|
|
46
67
|
}
|
|
47
68
|
if (candidate instanceof Set) {
|
|
48
|
-
return `set:[${Array.from(candidate, (setEntry) => valueFingerprint(setEntry,
|
|
69
|
+
return `set:[${Array.from(candidate, (setEntry) => valueFingerprint(setEntry, walk)).join(',')}]`;
|
|
49
70
|
}
|
|
50
|
-
|
|
51
|
-
|
|
71
|
+
// Sorted by key: an object is the same value however its keys were inserted, so
|
|
72
|
+
// reordering fields in a composition's source must not break a passing proof.
|
|
73
|
+
return `{${Arr.sortWith(Reflect.ownKeys(candidate).map((key) => ({ key, printed: propertyKeyFingerprint(key) })), (entry) => entry.printed, Order.string)
|
|
74
|
+
.map((entry) => `${entry.printed}:${valueFingerprint(readProperty(candidate, entry.key), walk)}`)
|
|
52
75
|
.join(',')}}`;
|
|
53
76
|
};
|
|
54
|
-
export const formatFingerprint = (fingerprintInput) => valueFingerprint(fingerprintInput, new WeakSet());
|
|
77
|
+
export const formatFingerprint = (fingerprintInput) => valueFingerprint(fingerprintInput, { seen: new WeakSet(), memo: new WeakMap() });
|
|
55
78
|
//# sourceMappingURL=fingerprint.js.map
|
package/dist/fingerprint.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../src/fingerprint.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../src/fingerprint.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { AnyScene, CapturedRender } from '@playfast/reform/internal';
|
|
|
8
8
|
export { ProofRunner, proofRunnerLayer, withProofRunner };
|
|
9
9
|
export type { ProofRunnerApi };
|
|
10
10
|
export { makeFacade, makeProofFacade, makeRuntimeHandleFacade, makeRuntimeTreeFacade, makeSink, proofLayer, } from './engine.js';
|
|
11
|
-
export type { MountedFacade, MountedSlotFacade, MountedSlotFacadesOf, RuntimeTreeFacade, Sink, } from './engine.js';
|
|
11
|
+
export type { DispatchedAction, MountedFacade, MountedSlotFacade, MountedSlotFacadesOf, RuntimeTreeFacade, Sink, } from './engine.js';
|
|
12
12
|
export { Product, ProductRequirement } from './product.js';
|
|
13
13
|
export type { AnyRequirementClass, ProductClass, ProductComposition, ProductManifest, RequirementCapture, RequirementClass, RequirementManifest, } from './product.js';
|
|
14
14
|
export { expect, matchProps } from './assert.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAqB,MAAM,QAAQ,CAAA;AACtE,OAAO,EAAE,WAAW,EAAuB,gBAAgB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAC9F,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAiBnC,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,eAAe,EAAE,CAAA;AAGzD,OAAO,EACL,UAAU,EACV,eAAe,EACf,uBAAuB,EACvB,qBAAqB,EACrB,QAAQ,EACR,UAAU,GACX,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playfast/reform-proof",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Headless testing toolkit for reform — drive a scene's compositions and assert on the props they compute, with no DOM, timers, or text matching.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect",
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { Layer, Schema as S } from 'effect'
|
|
2
|
+
import {
|
|
3
|
+
Composition,
|
|
4
|
+
Engine,
|
|
5
|
+
Event,
|
|
6
|
+
mount,
|
|
7
|
+
one,
|
|
8
|
+
provide,
|
|
9
|
+
Reducer,
|
|
10
|
+
scene,
|
|
11
|
+
slot,
|
|
12
|
+
State,
|
|
13
|
+
StateGroup,
|
|
14
|
+
type Trigger,
|
|
15
|
+
Ui,
|
|
16
|
+
ui,
|
|
17
|
+
} from '@playfast/reform'
|
|
18
|
+
import { describe, expect, test } from 'vitest'
|
|
19
|
+
import { Product, Proof, ProductRequirement, expect as proofExpect } from './index'
|
|
20
|
+
|
|
21
|
+
// The dialog's own event, and one only the host raises.
|
|
22
|
+
class Confirmed extends Event.make('CovConfirmed', S.Struct({})) {}
|
|
23
|
+
class HostConfirmed extends Event.make('CovHostConfirmed', S.Struct({})) {}
|
|
24
|
+
|
|
25
|
+
class CountState extends State.make('covCount', S.Number) {}
|
|
26
|
+
class CountStates extends StateGroup.make(CountState) {}
|
|
27
|
+
class CountReducer extends Reducer.make('CovReducer', {
|
|
28
|
+
states: [CountState],
|
|
29
|
+
events: [Confirmed],
|
|
30
|
+
}) {}
|
|
31
|
+
const CountReducerLive = Reducer.live(CountReducer, (count) => count + 1)
|
|
32
|
+
|
|
33
|
+
class DialogUi extends ui('CovDialog')<{
|
|
34
|
+
props: { count: number }
|
|
35
|
+
events: { confirm: Trigger<Record<string, never>> }
|
|
36
|
+
}>() {}
|
|
37
|
+
class Dialog extends Composition.make('CovDialog', {
|
|
38
|
+
title: 'CovDialog',
|
|
39
|
+
states: [CountStates],
|
|
40
|
+
events: [Confirmed],
|
|
41
|
+
ui: DialogUi,
|
|
42
|
+
})<Dialog>() {}
|
|
43
|
+
const DialogLive = Composition.live(Dialog, function* () {
|
|
44
|
+
const count = yield* StateGroup.select(CountStates, 'covCount')
|
|
45
|
+
const confirm = yield* Event.trigger(Confirmed)
|
|
46
|
+
return mount({ props: { count }, slots: {}, events: { confirm } })
|
|
47
|
+
})
|
|
48
|
+
class DialogSlot extends slot('CovDialogSlot')<DialogSlot, typeof Dialog>() {}
|
|
49
|
+
|
|
50
|
+
// A sibling that raises the dialog's event without being the dialog.
|
|
51
|
+
class EchoUi extends ui('CovEcho')<{
|
|
52
|
+
props: { count: number }
|
|
53
|
+
events: { confirm: Trigger<Record<string, never>> }
|
|
54
|
+
}>() {}
|
|
55
|
+
class Echo extends Composition.make('CovEcho', {
|
|
56
|
+
title: 'CovEcho',
|
|
57
|
+
states: [CountStates],
|
|
58
|
+
events: [Confirmed],
|
|
59
|
+
ui: EchoUi,
|
|
60
|
+
})<Echo>() {}
|
|
61
|
+
const EchoLive = Composition.live(Echo, function* () {
|
|
62
|
+
const count = yield* StateGroup.select(CountStates, 'covCount')
|
|
63
|
+
const confirm = yield* Event.trigger(Confirmed)
|
|
64
|
+
return mount({ props: { count }, slots: {}, events: { confirm } })
|
|
65
|
+
})
|
|
66
|
+
class EchoSlot extends slot('CovEchoSlot')<EchoSlot, typeof Echo>() {}
|
|
67
|
+
|
|
68
|
+
// The test-only parent: it renders the dialog and re-exposes its event as
|
|
69
|
+
// `passthrough`, while its own `confirm` is a different event that happens to
|
|
70
|
+
// spell the dialog's action name.
|
|
71
|
+
class HostUi extends ui('CovHost')<{
|
|
72
|
+
props: { count: number }
|
|
73
|
+
events: {
|
|
74
|
+
confirm: Trigger<Record<string, never>>
|
|
75
|
+
passthrough: Trigger<Record<string, never>>
|
|
76
|
+
}
|
|
77
|
+
slots: { Dialog: DialogSlot; Echo: EchoSlot }
|
|
78
|
+
}>() {}
|
|
79
|
+
class Host extends Composition.make('CovHost', {
|
|
80
|
+
title: 'CovHost',
|
|
81
|
+
states: [CountStates],
|
|
82
|
+
events: [Confirmed, HostConfirmed],
|
|
83
|
+
slots: { Dialog: DialogSlot, Echo: EchoSlot },
|
|
84
|
+
ui: HostUi,
|
|
85
|
+
})<Host>() {}
|
|
86
|
+
const HostLive = Composition.live(Host, function* () {
|
|
87
|
+
const count = yield* StateGroup.select(CountStates, 'covCount')
|
|
88
|
+
const confirm = yield* Event.trigger(HostConfirmed)
|
|
89
|
+
const passthrough = yield* Event.trigger(Confirmed)
|
|
90
|
+
return mount({
|
|
91
|
+
props: { count },
|
|
92
|
+
slots: { Dialog: one({}), Echo: one({}) },
|
|
93
|
+
events: { confirm, passthrough },
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const presentations = Layer.mergeAll(
|
|
98
|
+
provide(
|
|
99
|
+
HostUi,
|
|
100
|
+
Ui.make(HostUi, () => null),
|
|
101
|
+
),
|
|
102
|
+
provide(
|
|
103
|
+
DialogUi,
|
|
104
|
+
Ui.make(DialogUi, () => null),
|
|
105
|
+
),
|
|
106
|
+
provide(
|
|
107
|
+
EchoUi,
|
|
108
|
+
Ui.make(EchoUi, () => null),
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
const Views = Layer.mergeAll(
|
|
112
|
+
HostLive,
|
|
113
|
+
DialogLive,
|
|
114
|
+
EchoLive,
|
|
115
|
+
provide(DialogSlot, Dialog),
|
|
116
|
+
provide(EchoSlot, Echo),
|
|
117
|
+
).pipe(Layer.provideMerge(presentations))
|
|
118
|
+
const Logic = CountReducerLive.pipe(
|
|
119
|
+
Layer.provideMerge(Layer.mergeAll(Engine, StateGroup.live(CountStates, { covCount: 0 }))),
|
|
120
|
+
)
|
|
121
|
+
const HostScene = scene(Host, { provide: [Views.pipe(Layer.provideMerge(Logic))] })
|
|
122
|
+
|
|
123
|
+
class ConfirmsDialog extends ProductRequirement.make(Dialog, 'confirms the dialog', {
|
|
124
|
+
events: ['confirm'],
|
|
125
|
+
}) {}
|
|
126
|
+
class DialogProduct extends Product.make(Dialog, { requirements: [ConfirmsDialog] }) {}
|
|
127
|
+
|
|
128
|
+
const run = (proof: Proof) => Proof.run(Proof.suite(DialogProduct, { proofs: [proof] }))
|
|
129
|
+
|
|
130
|
+
describe('event coverage across a test-only host (issue #26)', () => {
|
|
131
|
+
test('the composition raising the requirement’s own event covers it', async () => {
|
|
132
|
+
const viaSlot = Proof.implementVia(ConfirmsDialog, HostScene, function* (app) {
|
|
133
|
+
const dialog = yield* app.slots.Dialog.first
|
|
134
|
+
const after = yield* dialog.actions.confirm({})
|
|
135
|
+
yield* proofExpect(after.count).toBe(1)
|
|
136
|
+
})
|
|
137
|
+
const result = await run(viaSlot)
|
|
138
|
+
expect(result.results[0]?.error).toBeUndefined()
|
|
139
|
+
expect(result.ok).toBe(true)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('a host that re-raises the requirement’s event covers it too', async () => {
|
|
143
|
+
const viaHost = Proof.implementVia(ConfirmsDialog, HostScene, function* (app) {
|
|
144
|
+
const after = yield* app.actions.passthrough({})
|
|
145
|
+
yield* proofExpect(after.count).toBe(1)
|
|
146
|
+
})
|
|
147
|
+
const result = await run(viaHost)
|
|
148
|
+
expect(result.results[0]?.error).toBeUndefined()
|
|
149
|
+
expect(result.ok).toBe(true)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('a host action that only spells the name the same does not cover it', async () => {
|
|
153
|
+
const sameName = Proof.implementVia(ConfirmsDialog, HostScene, function* (app) {
|
|
154
|
+
const after = yield* app.actions.confirm({})
|
|
155
|
+
yield* proofExpect(after.count).toBe(0)
|
|
156
|
+
})
|
|
157
|
+
const result = await run(sameName)
|
|
158
|
+
expect(result.ok).toBe(false)
|
|
159
|
+
expect(result.results[0]?.error).toContain('never dispatched')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('a sibling raising the same event does not cover it', async () => {
|
|
163
|
+
const sibling = Proof.implementVia(ConfirmsDialog, HostScene, function* (app) {
|
|
164
|
+
const echo = yield* app.slots.Echo.first
|
|
165
|
+
const after = yield* echo.actions.confirm({})
|
|
166
|
+
yield* proofExpect(after.count).toBe(1)
|
|
167
|
+
})
|
|
168
|
+
const result = await run(sibling)
|
|
169
|
+
expect(result.ok).toBe(false)
|
|
170
|
+
expect(result.results[0]?.error).toContain('never dispatched')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('the failure names where the event was dispatched instead', async () => {
|
|
174
|
+
const sameName = Proof.implementVia(ConfirmsDialog, HostScene, function* (app) {
|
|
175
|
+
yield* app.actions.confirm({})
|
|
176
|
+
})
|
|
177
|
+
const result = await run(sameName)
|
|
178
|
+
expect(result.results[0]?.error).toContain('dispatched on CovHost')
|
|
179
|
+
expect(result.results[0]?.error).toContain('CovHostConfirmed')
|
|
180
|
+
expect(result.results[0]?.error).toContain('not on CovDialog')
|
|
181
|
+
expect(result.results[0]?.error).toContain('CovConfirmed')
|
|
182
|
+
})
|
|
183
|
+
})
|
package/src/engine.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from '@playfast/reform'
|
|
10
10
|
import { type AnyComposition, type AnyScene } from '@playfast/reform/internal'
|
|
11
11
|
import {
|
|
12
|
+
type DispatchedAction,
|
|
12
13
|
invalidCompositionProps,
|
|
13
14
|
type HostRuntime,
|
|
14
15
|
makeSink,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
type Sink,
|
|
18
19
|
} from './engineSink'
|
|
19
20
|
import { childComposition, readRuntimeSlotChild, slotsOf } from './engineTree'
|
|
21
|
+
import { type Coverage, coverageFailure, missingCoverage } from './engineCoverage'
|
|
20
22
|
import type { RuntimeTreeFacade } from './engineTreeTypes'
|
|
21
23
|
import { makeRuntimeTreeFacade } from './engineTreeFacade'
|
|
22
24
|
import type { ProofCase, ProofResult } from './proofCase'
|
|
@@ -26,14 +28,10 @@ import type { ProductComposition } from './product'
|
|
|
26
28
|
import type { Proof } from './index'
|
|
27
29
|
|
|
28
30
|
export { makeSink } from './engineSink'
|
|
29
|
-
export type { Sink } from './engineSink'
|
|
31
|
+
export type { DispatchedAction, Sink } from './engineSink'
|
|
30
32
|
export { proofLayer } from './engineTree'
|
|
31
33
|
export { makeFacade, makeProofFacade, makeRuntimeHandleFacade } from './engineMounted'
|
|
32
|
-
export type {
|
|
33
|
-
MountedFacade,
|
|
34
|
-
MountedSlotFacade,
|
|
35
|
-
MountedSlotFacadesOf,
|
|
36
|
-
} from './engineSink'
|
|
34
|
+
export type { MountedFacade, MountedSlotFacade, MountedSlotFacadesOf } from './engineSink'
|
|
37
35
|
export { makeRuntimeTreeFacade } from './engineTreeFacade'
|
|
38
36
|
export type { RuntimeTreeFacade } from './engineTreeTypes'
|
|
39
37
|
|
|
@@ -63,7 +61,7 @@ const withProofRuntimeTree = <
|
|
|
63
61
|
>(
|
|
64
62
|
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
65
63
|
sink: Sink,
|
|
66
|
-
dispatched: Set<
|
|
64
|
+
dispatched: Set<DispatchedAction>,
|
|
67
65
|
use: (runtime: HostRuntime, tree: RuntimeTreeFacade<C>) => Effect.Effect<A, E, never>,
|
|
68
66
|
): Effect.Effect<A, E, never> =>
|
|
69
67
|
Effect.acquireUseRelease(
|
|
@@ -81,15 +79,6 @@ const withProofRuntimeTree = <
|
|
|
81
79
|
(runtime) => Effect.sync(runtime.dispose),
|
|
82
80
|
)
|
|
83
81
|
|
|
84
|
-
const missingCoverage = (
|
|
85
|
-
proof: Pick<Proof, 'requirement'>,
|
|
86
|
-
dispatched: Set<string>,
|
|
87
|
-
): ReadonlyArray<string> =>
|
|
88
|
-
Option.match(proof.requirement.manifest.events, {
|
|
89
|
-
onNone: () => [],
|
|
90
|
-
onSome: (declared) => declared.map(String).filter((event) => !dispatched.has(event)),
|
|
91
|
-
})
|
|
92
|
-
|
|
93
82
|
interface ProofPlacement {
|
|
94
83
|
readonly requirement: Proof['requirement']
|
|
95
84
|
readonly placement: 'Root' | 'Slot'
|
|
@@ -144,7 +133,7 @@ const executeProofWithSink = Effect.fn('executeProofWithSink')(function* <
|
|
|
144
133
|
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
145
134
|
sink: Sink,
|
|
146
135
|
): Effect.fn.Return<ProofResult, never, never> {
|
|
147
|
-
const dispatched = new Set<
|
|
136
|
+
const dispatched = new Set<DispatchedAction>()
|
|
148
137
|
const statement = proof.requirement.manifest.statement
|
|
149
138
|
const runGenerator = Effect.fn('runProofGenerator')(function* (
|
|
150
139
|
generator: ReturnType<typeof proof.body>,
|
|
@@ -164,10 +153,15 @@ const executeProofWithSink = Effect.fn('executeProofWithSink')(function* <
|
|
|
164
153
|
yield* verifyProofPlacement(proof, scene, runtime)
|
|
165
154
|
yield* tree.settle
|
|
166
155
|
yield* runGenerator(proof.body(tree.facade), undefined, tree.settle)
|
|
167
|
-
const
|
|
168
|
-
|
|
156
|
+
const coverage: Coverage = {
|
|
157
|
+
requirement: proof.requirement,
|
|
158
|
+
host: compositionIdentity(scene.composition),
|
|
159
|
+
dispatched,
|
|
160
|
+
publications: tree.publications,
|
|
161
|
+
}
|
|
162
|
+
const missing = missingCoverage(coverage)
|
|
169
163
|
return missing.length > 0
|
|
170
|
-
? { requirement: statement, ok: false, error:
|
|
164
|
+
? { requirement: statement, ok: false, error: coverageFailure(coverage, missing) }
|
|
171
165
|
: { requirement: statement, ok: true, error: undefined }
|
|
172
166
|
}),
|
|
173
167
|
).pipe(
|
|
@@ -218,7 +212,7 @@ const driveProofWithSink = Effect.fn('driveProofWithSink')(function* <
|
|
|
218
212
|
const frames = yield* Ref.make<ReadonlyArray<StepFrame>>([])
|
|
219
213
|
const statement = proof.requirement.manifest.statement
|
|
220
214
|
// Driving tracks dispatches for the facade without enforcing coverage.
|
|
221
|
-
const dispatched = new Set<
|
|
215
|
+
const dispatched = new Set<DispatchedAction>()
|
|
222
216
|
const record = (index: number): Effect.Effect<void> =>
|
|
223
217
|
Ref.update(frames, (current) => [...current, { index, captures: [...sink.captures] }])
|
|
224
218
|
const pump = Effect.fn('pump')(function* (
|
|
@@ -282,4 +276,3 @@ export const driveProofEffect = (proof: Proof): Effect.Effect<DriveResult, never
|
|
|
282
276
|
|
|
283
277
|
export const driveProof = (proof: Proof): Promise<DriveResult> =>
|
|
284
278
|
Effect.runPromise(driveProofEffect(proof))
|
|
285
|
-
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Array as Arr, Option } from 'effect'
|
|
2
|
+
import { compositionIdentityOf, type DispatchedAction, type EventPublications } from './engineSink'
|
|
3
|
+
import type { Proof } from './index'
|
|
4
|
+
|
|
5
|
+
export interface Coverage {
|
|
6
|
+
readonly requirement: Proof['requirement']
|
|
7
|
+
// The scene root — the composition that renders the requirement's own, and the only
|
|
8
|
+
// one whose dispatch can stand in for it.
|
|
9
|
+
readonly host: symbol
|
|
10
|
+
readonly dispatched: Set<DispatchedAction>
|
|
11
|
+
readonly publications: EventPublications
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface DeclaredEvent {
|
|
15
|
+
readonly coverage: Coverage
|
|
16
|
+
readonly owner: symbol
|
|
17
|
+
readonly event: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const samePublication = (
|
|
21
|
+
published: Option.Option<string>,
|
|
22
|
+
declared: Option.Option<string>,
|
|
23
|
+
): boolean =>
|
|
24
|
+
Option.getOrElse(
|
|
25
|
+
Option.zipWith(published, declared, (raised, owned) => raised === owned),
|
|
26
|
+
() => false,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
// A declared event is answered by a dispatch on the requirement's own composition — or
|
|
30
|
+
// by the composition that renders it raising the very same domain Event. The second
|
|
31
|
+
// case is the test-only parent that exists to feed a slot the props it needs and
|
|
32
|
+
// re-exposes its action: that dispatch reaches the child's reducer, so withholding
|
|
33
|
+
// coverage reports "never dispatched" about an event whose effects the proof body
|
|
34
|
+
// asserts two lines later. A composition that merely spells an action the same way
|
|
35
|
+
// publishes a different Event and stays uncovered, which is the collision the identity
|
|
36
|
+
// check exists for — as does a sibling raising the event without owning the action.
|
|
37
|
+
const answeredBy =
|
|
38
|
+
({ coverage, owner, event }: DeclaredEvent) =>
|
|
39
|
+
(action: DispatchedAction): boolean =>
|
|
40
|
+
Object.is(action.identity, owner)
|
|
41
|
+
? action.event === event
|
|
42
|
+
: Object.is(action.identity, coverage.host) &&
|
|
43
|
+
samePublication(action.published, coverage.publications.publishedBy(owner, event))
|
|
44
|
+
|
|
45
|
+
export const missingCoverage = (coverage: Coverage): ReadonlyArray<string> =>
|
|
46
|
+
Option.match(coverage.requirement.manifest.events, {
|
|
47
|
+
onNone: () => [],
|
|
48
|
+
onSome: (declared) => {
|
|
49
|
+
const owner = compositionIdentityOf(coverage.requirement.manifest.composition)
|
|
50
|
+
const raised = Arr.fromIterable(coverage.dispatched)
|
|
51
|
+
return declared
|
|
52
|
+
.map(String)
|
|
53
|
+
.filter((event) => !raised.some(answeredBy({ coverage, owner, event })))
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const publicationSuffix = (published: Option.Option<string>): string =>
|
|
58
|
+
Option.match(published, {
|
|
59
|
+
onNone: () => '',
|
|
60
|
+
onSome: (tag) => ` (publishes ${tag})`,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// "never dispatched" is the wrong story when the event was dispatched on a composition
|
|
64
|
+
// the requirement does not own, so name that composition and what each side publishes.
|
|
65
|
+
const uncoveredDetail = ({ coverage, owner, event }: DeclaredEvent): string => {
|
|
66
|
+
const ownerName = coverage.requirement.manifest.composition.manifest.name
|
|
67
|
+
const elsewhere = Arr.dedupe(
|
|
68
|
+
Arr.filterMap(Arr.fromIterable(coverage.dispatched), (action) =>
|
|
69
|
+
action.event === event && !Object.is(action.identity, owner)
|
|
70
|
+
? Option.some(`${action.composition}${publicationSuffix(action.published)}`)
|
|
71
|
+
: Option.none(),
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
const owned = publicationSuffix(coverage.publications.publishedBy(owner, event))
|
|
75
|
+
return Arr.match(elsewhere, {
|
|
76
|
+
onEmpty: () => event,
|
|
77
|
+
onNonEmpty: (raisers) =>
|
|
78
|
+
`${event} — dispatched on ${Arr.join(raisers, ', ')}, not on ${ownerName}${owned}`,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const coverageFailure = (coverage: Coverage, missing: ReadonlyArray<string>): string => {
|
|
83
|
+
const owner = compositionIdentityOf(coverage.requirement.manifest.composition)
|
|
84
|
+
const detail = missing.map((event) => uncoveredDetail({ coverage, owner, event }))
|
|
85
|
+
return `requirement declares events never dispatched: ${Arr.join(detail, '; ')}`
|
|
86
|
+
}
|
package/src/engineMounted.ts
CHANGED
|
@@ -7,10 +7,12 @@ import {
|
|
|
7
7
|
type Trigger,
|
|
8
8
|
type UiContract,
|
|
9
9
|
} from '@playfast/reform'
|
|
10
|
-
import { type AnyComposition, CaptureSink } from '@playfast/reform/internal'
|
|
10
|
+
import { type AnyComposition, CaptureSink, triggerEventTag } from '@playfast/reform/internal'
|
|
11
11
|
import type { Facade } from './facade'
|
|
12
12
|
import {
|
|
13
|
+
compositionIdentityOf,
|
|
13
14
|
directSettle,
|
|
15
|
+
type DispatchedAction,
|
|
14
16
|
expectMatchingProps,
|
|
15
17
|
keyed,
|
|
16
18
|
type MountedFacade,
|
|
@@ -32,11 +34,7 @@ import {
|
|
|
32
34
|
slotCapturesFor,
|
|
33
35
|
slotsOf,
|
|
34
36
|
} from './engineTree'
|
|
35
|
-
import {
|
|
36
|
-
collectRuntimeBindings,
|
|
37
|
-
renderTreeRuntime,
|
|
38
|
-
settleTreeRuntime,
|
|
39
|
-
} from './engineRender'
|
|
37
|
+
import { collectRuntimeBindings, renderTreeRuntime, settleTreeRuntime } from './engineRender'
|
|
40
38
|
|
|
41
39
|
export function makeFacade<
|
|
42
40
|
C extends UiContract,
|
|
@@ -49,7 +47,7 @@ export function makeFacade<
|
|
|
49
47
|
runtime: ManagedRuntime.ManagedRuntime<Services | CaptureSink, never>,
|
|
50
48
|
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
51
49
|
sink: Sink,
|
|
52
|
-
dispatched: Set<
|
|
50
|
+
dispatched: Set<DispatchedAction>,
|
|
53
51
|
): {
|
|
54
52
|
readonly facade: Facade<C, never>
|
|
55
53
|
readonly settle: Effect.Effect<void, never, never>
|
|
@@ -74,7 +72,7 @@ function makeMountedFacade<
|
|
|
74
72
|
runtime: RenderRuntime,
|
|
75
73
|
root: CompositionClass<P, C, S, N, Identity>,
|
|
76
74
|
sink: Sink,
|
|
77
|
-
dispatched: Set<
|
|
75
|
+
dispatched: Set<DispatchedAction>,
|
|
78
76
|
settleBoundary: SettleBoundary,
|
|
79
77
|
): {
|
|
80
78
|
readonly facade: MountedFacade<C>
|
|
@@ -84,14 +82,14 @@ function makeMountedFacade(
|
|
|
84
82
|
runtime: RenderRuntime,
|
|
85
83
|
root: AnyComposition,
|
|
86
84
|
sink: Sink,
|
|
87
|
-
dispatched: Set<
|
|
85
|
+
dispatched: Set<DispatchedAction>,
|
|
88
86
|
settleBoundary: SettleBoundary,
|
|
89
87
|
): MountedFacadeHandleErasure
|
|
90
88
|
function makeMountedFacade(
|
|
91
89
|
runtime: RenderRuntime,
|
|
92
90
|
root: AnyComposition,
|
|
93
91
|
sink: Sink,
|
|
94
|
-
dispatched: Set<
|
|
92
|
+
dispatched: Set<DispatchedAction>,
|
|
95
93
|
settleBoundary: SettleBoundary,
|
|
96
94
|
): MountedFacadeHandleErasure {
|
|
97
95
|
const bindings = collectRuntimeBindings(root, runtime)
|
|
@@ -146,7 +144,12 @@ function makeMountedFacade(
|
|
|
146
144
|
Effect.flatMap(() => triggerOf(ref, event)),
|
|
147
145
|
Effect.flatMap((trigger) =>
|
|
148
146
|
Effect.sync(() => {
|
|
149
|
-
dispatched.add(
|
|
147
|
+
dispatched.add({
|
|
148
|
+
identity: compositionIdentityOf(comp),
|
|
149
|
+
composition: uiNameOf(comp),
|
|
150
|
+
event,
|
|
151
|
+
published: triggerEventTag(trigger),
|
|
152
|
+
})
|
|
150
153
|
trigger(payload)
|
|
151
154
|
}),
|
|
152
155
|
),
|
|
@@ -244,7 +247,7 @@ export const makeProofFacade = <
|
|
|
244
247
|
runtime: ManagedRuntime.ManagedRuntime<Services | CaptureSink, never>,
|
|
245
248
|
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
246
249
|
sink: Sink,
|
|
247
|
-
dispatched: Set<
|
|
250
|
+
dispatched: Set<DispatchedAction>,
|
|
248
251
|
): {
|
|
249
252
|
readonly facade: Facade<C, never>
|
|
250
253
|
readonly settle: Effect.Effect<void, never, never>
|
|
@@ -264,7 +267,7 @@ export const makeRuntimeHandleFacade = <
|
|
|
264
267
|
runtime: RuntimeHandle<Services>,
|
|
265
268
|
root: RuntimeRootComposition<RootIdentifier, P, C, S, N, Identity>,
|
|
266
269
|
sink: Sink,
|
|
267
|
-
dispatched: Set<
|
|
270
|
+
dispatched: Set<DispatchedAction>,
|
|
268
271
|
): {
|
|
269
272
|
readonly facade: MountedFacade<C>
|
|
270
273
|
readonly settle: Effect.Effect<void, never, never>
|