@wardic/test 0.0.0-stage → 1.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.js","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,0BAA0B,CAAA;AACvD,OAAO,KAAK,IAAI,MAAM,gBAAgB,CAAA;AACtC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,SAAS,MAAM,kBAAkB,CAAA;AAC7C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,WAAW,MAAM,oBAAoB,CAAA;AACjD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAEnC;;;;;;;;;;GAUG;AACH,yCAAyC;AACzC,MAAM,CAAC,MAAM,KAAK,GAAG,CACnB,aAAa,GAAgD,EAAE,EAC/D,WAAW,GAA8C,EAAE,EAC3D,SAAS,GAAiD,EAAE,EACtB,EAAE,CACxC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,WAAW,EAAE;IACrC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC;IAC1C,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC;IACvC,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC;IACtC,YAAY,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE;CAClC,CAAC,CAAA;AAEJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wFAAwF;AACxF,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,OAA+E,EAKrG,EAAE,CACH,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;IACf,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,EAAgC,CAAA;IACzD,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,EAA8B,CAAA;IACrD,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,EAAiC,CAAA;IACvD,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,WAAW,EAAE;YAC5C,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI;YAC3B,QAAQ,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI;YACzB,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI;YACvB,WAAW,EAAE,WAAW,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC;YAC3D,YAAY,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE;gBAC7B,IAAI,gBAAgB,CAAC,GAAG,CAAC,EAAE,CAAC;oBAC1B,MAAM,IAAI,GAAiC,EAAE,GAAG,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;oBAClF,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;oBAClC,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBAC1B,CAAC;gBACD,OAAO,MAAM,CAAC,IAAI,EAAE,CAAA;YACtB,CAAC;SACF,CAAC;QACF,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,SAAS,EAAE,KAAK,CAAC,IAAI;QACrB,OAAO,EAAE,IAAI,CAAC,IAAI;KACnB,CAAA;AACH,CAAC,CAAC,CAAA;AAEJ,0GAA0G;AAC1G,MAAM,gBAAgB,GAAG,CAAC,GAAY,EAAuC,EAAE,CAC7E,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,WAAW,CAAC,GAAG,EAAE,WAAW,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC,IAAK,GAAoC,CAAC,OAAO,CAAA"}
@@ -0,0 +1,27 @@
1
+ import * as Announcer from "@wardic/core/Announcer";
2
+ import type * as Clock from "@wardic/core/Clock";
3
+ import * as DefaultTransition from "@wardic/core/DefaultTransition";
4
+ import * as FrameLoop from "@wardic/core/FrameLoop";
5
+ import type * as InputSource from "@wardic/core/InputSource";
6
+ import * as InterpolatorRegistry from "@wardic/core/InterpolatorRegistry";
7
+ import type * as Probe from "@wardic/core/Probe";
8
+ import * as ReducedMotion from "@wardic/core/ReducedMotion";
9
+ import * as RefreshFrameSource from "@wardic/core/RefreshFrameSource";
10
+ import type * as Renderer from "@wardic/core/Renderer";
11
+ import * as Layer from "effect/Layer";
12
+ import { AtomRegistry } from "effect/reactivity";
13
+ /**
14
+ * The full headless environment: deterministic Clock (driven by `frames`), a Refresh frame
15
+ * source that never calls back, recording Renderer, static Probe, silent InputSource, core's real
16
+ * interpolator registry, a fresh AtomRegistry for reading Probe atoms
17
+ * (`viewport`, `prefersReducedMotion`), and a fresh Drag activity, so one test's
18
+ * drag never blocks another's hover or press, and core's Frame loop over that
19
+ * Clock. Provide this in place of `DomLive` to test animation code without a
20
+ * page. It draws nothing on one: an element rendered under it keeps its Starting style, so an
21
+ * app's components are tested with `DomLive` under the test runner's fake timers instead
22
+ * (https://wardic.dev/guides/testing).
23
+ */
24
+ export declare const testLayer: (frames?: ReadonlyArray<number>) => Layer.Layer<Clock.Clock | RefreshFrameSource.RefreshFrameSource | FrameLoop.FrameLoop | Renderer.Renderer | Probe.Probe | InputSource.InputSource | InterpolatorRegistry.InterpolatorRegistry | ReducedMotion.PositionalKeys | DefaultTransition.DefaultTransition | Announcer.Announcer | AtomRegistry.AtomRegistry>;
25
+ /** `testLayer()` with an empty clock — for tests that drive their own frames. It never ticks on its own. */
26
+ export declare const TestLive: Layer.Layer<Announcer.Announcer | AtomRegistry.AtomRegistry | Clock.Clock | DefaultTransition.DefaultTransition | FrameLoop.FrameLoop | InputSource.InputSource | InterpolatorRegistry.InterpolatorRegistry | ReducedMotion.PositionalKeys | Probe.Probe | RefreshFrameSource.RefreshFrameSource | Renderer.Renderer, never, never>;
27
+ //# sourceMappingURL=layer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layer.d.ts","sourceRoot":"","sources":["../src/layer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,wBAAwB,CAAA;AACnD,OAAO,KAAK,KAAK,KAAK,MAAM,oBAAoB,CAAA;AAChD,OAAO,KAAK,iBAAiB,MAAM,gCAAgC,CAAA;AAEnE,OAAO,KAAK,SAAS,MAAM,wBAAwB,CAAA;AACnD,OAAO,KAAK,KAAK,WAAW,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,oBAAoB,MAAM,mCAAmC,CAAA;AAEzE,OAAO,KAAK,KAAK,KAAK,MAAM,oBAAoB,CAAA;AAChD,OAAO,KAAK,aAAa,MAAM,4BAA4B,CAAA;AAC3D,OAAO,KAAK,kBAAkB,MAAM,iCAAiC,CAAA;AACrE,OAAO,KAAK,KAAK,QAAQ,MAAM,uBAAuB,CAAA;AAEtD,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAahD;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,SAAS,YACZ,aAAa,CAAC,MAAM,CAAC,KAC5B,KAAK,CAAC,KAAK,CACV,KAAK,CAAC,KAAK,GACX,kBAAkB,CAAC,kBAAkB,GACrC,SAAS,CAAC,SAAS,GACnB,QAAQ,CAAC,QAAQ,GACjB,KAAK,CAAC,KAAK,GACX,WAAW,CAAC,WAAW,GACvB,oBAAoB,CAAC,oBAAoB,GACzC,aAAa,CAAC,cAAc,GAC5B,iBAAiB,CAAC,iBAAiB,GACnC,SAAS,CAAC,SAAS,GACnB,YAAY,CAAC,YAAY,CAsB1B,CAAA;AAEH,4GAA4G;AAC5G,eAAO,MAAM,QAAQ,qUAAc,CAAA"}
package/dist/layer.js ADDED
@@ -0,0 +1,43 @@
1
+ import * as Announcer from "@wardic/core/Announcer";
2
+ import * as DefaultTransition from "@wardic/core/DefaultTransition";
3
+ import * as DragActivity from "@wardic/core/DragActivity";
4
+ import * as FrameLoop from "@wardic/core/FrameLoop";
5
+ import * as InterpolatorRegistry from "@wardic/core/InterpolatorRegistry";
6
+ import * as PressClaims from "@wardic/core/PressClaims";
7
+ import * as ReducedMotion from "@wardic/core/ReducedMotion";
8
+ import * as RefreshFrameSource from "@wardic/core/RefreshFrameSource";
9
+ import { constUndefined, constVoid } from "effect/Function";
10
+ import * as Layer from "effect/Layer";
11
+ import { AtomRegistry } from "effect/reactivity";
12
+ import * as TestClock from "./clock.js";
13
+ import * as TestInput from "./input.js";
14
+ import * as TestProbe from "./probe.js";
15
+ import * as TestRenderer from "./renderer.js";
16
+ /**
17
+ * The headless Environment's Refresh frame source: it never calls back, as its Clock never ticks
18
+ * on its own, so a flush gate built on it never flushes by itself. A test that fires frames
19
+ * provides `TestClock.pushable()`'s layer over it (ADR-0031, decision 6).
20
+ */
21
+ const stillFrames = { requestFrame: constUndefined, cancelFrame: constVoid };
22
+ /**
23
+ * The full headless environment: deterministic Clock (driven by `frames`), a Refresh frame
24
+ * source that never calls back, recording Renderer, static Probe, silent InputSource, core's real
25
+ * interpolator registry, a fresh AtomRegistry for reading Probe atoms
26
+ * (`viewport`, `prefersReducedMotion`), and a fresh Drag activity, so one test's
27
+ * drag never blocks another's hover or press, and core's Frame loop over that
28
+ * Clock. Provide this in place of `DomLive` to test animation code without a
29
+ * page. It draws nothing on one: an element rendered under it keeps its Starting style, so an
30
+ * app's components are tested with `DomLive` under the test runner's fake timers instead
31
+ * (https://wardic.dev/guides/testing).
32
+ */
33
+ // The Drag activity: ADR-0019. The Frame loop: ADR-0027. The testing guide: #558.
34
+ export const testLayer = (frames = []) => FrameLoop.layer.pipe(Layer.provideMerge(Layer.mergeAll(TestClock.layer(frames), RefreshFrameSource.layer(stillFrames), TestRenderer.layer, TestProbe.layer(), TestInput.layer(), InterpolatorRegistry.layer,
35
+ // The transforms only: the Test Environment names no box keys (ADR-0036).
36
+ ReducedMotion.transformsOnly,
37
+ // No opinion on a key's Default transition: every key on one spring (ADR-0051).
38
+ DefaultTransition.layerOneSpring,
39
+ // No assistive technology to reach headless (ADR-0037).
40
+ Announcer.layerSilent, AtomRegistry.layer, Layer.sync(DragActivity.DragActivity, DragActivity.make), Layer.sync(PressClaims.PressClaims, PressClaims.make))));
41
+ /** `testLayer()` with an empty clock — for tests that drive their own frames. It never ticks on its own. */
42
+ export const TestLive = testLayer();
43
+ //# sourceMappingURL=layer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layer.js","sourceRoot":"","sources":["../src/layer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,wBAAwB,CAAA;AAEnD,OAAO,KAAK,iBAAiB,MAAM,gCAAgC,CAAA;AACnE,OAAO,KAAK,YAAY,MAAM,2BAA2B,CAAA;AACzD,OAAO,KAAK,SAAS,MAAM,wBAAwB,CAAA;AAEnD,OAAO,KAAK,oBAAoB,MAAM,mCAAmC,CAAA;AACzE,OAAO,KAAK,WAAW,MAAM,0BAA0B,CAAA;AAEvD,OAAO,KAAK,aAAa,MAAM,4BAA4B,CAAA;AAC3D,OAAO,KAAK,kBAAkB,MAAM,iCAAiC,CAAA;AAErE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3D,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,KAAK,SAAS,MAAM,YAAY,CAAA;AACvC,OAAO,KAAK,SAAS,MAAM,YAAY,CAAA;AACvC,OAAO,KAAK,SAAS,MAAM,YAAY,CAAA;AACvC,OAAO,KAAK,YAAY,MAAM,eAAe,CAAA;AAE7C;;;;GAIG;AACH,MAAM,WAAW,GAA6B,EAAE,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,SAAS,EAAE,CAAA;AAEtG;;;;;;;;;;GAUG;AACH,kFAAkF;AAClF,MAAM,CAAC,MAAM,SAAS,GAAG,CACvB,MAAM,GAA0B,EAAE,EAalC,EAAE,CACF,SAAS,CAAC,KAAK,CAAC,IAAI,CAClB,KAAK,CAAC,YAAY,CAChB,KAAK,CAAC,QAAQ,CACZ,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,EACvB,kBAAkB,CAAC,KAAK,CAAC,WAAW,CAAC,EACrC,YAAY,CAAC,KAAK,EAClB,SAAS,CAAC,KAAK,EAAE,EACjB,SAAS,CAAC,KAAK,EAAE,EACjB,oBAAoB,CAAC,KAAK;AAC1B,0EAA0E;AAC1E,aAAa,CAAC,cAAc;AAC5B,gFAAgF;AAChF,iBAAiB,CAAC,cAAc;AAChC,wDAAwD;AACxD,SAAS,CAAC,WAAW,EACrB,YAAY,CAAC,KAAK,EAClB,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,YAAY,EAAE,YAAY,CAAC,IAAI,CAAC,EACxD,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW,EAAE,WAAW,CAAC,IAAI,CAAC,CACtD,CACF,CACF,CAAA;AAEH,4GAA4G;AAC5G,MAAM,CAAC,MAAM,QAAQ,GAAG,SAAS,EAAE,CAAA"}
@@ -0,0 +1,46 @@
1
+ import * as Probe from "@wardic/core/Probe";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Layer from "effect/Layer";
4
+ import { AtomRegistry } from "effect/reactivity";
5
+ /** Options for the static test Probe. */
6
+ export interface Options {
7
+ readonly bounds?: Probe.Bounds;
8
+ readonly viewport?: {
9
+ readonly width: number;
10
+ readonly height: number;
11
+ };
12
+ readonly prefersReducedMotion?: boolean;
13
+ /** Whether the page is shown: `true` when left out. */
14
+ readonly pageVisible?: boolean;
15
+ /** Every container's scroll, as `scroll` gives it: a page that doesn't scroll when left out. */
16
+ readonly scroll?: Probe.Scroll;
17
+ /**
18
+ * Script of intersection (in-view) booleans replayed by `observeIntersection`, an **Input feed**
19
+ * that ends after it. Mirrors the InputSource test harness: omitted (or empty) means it
20
+ * emits nothing and ends.
21
+ */
22
+ readonly intersection?: ReadonlyArray<boolean>;
23
+ }
24
+ /**
25
+ * A static Probe: fixed bounds/viewport. `observeIntersection` replays the
26
+ * optional `intersection` script to each subscriber (empty by default, so it
27
+ * emits nothing); `observeResize` stays empty.
28
+ */
29
+ export declare const layer: (options?: Options) => Layer.Layer<Probe.Probe>;
30
+ /**
31
+ * A push-based Probe for tests that need to interleave intersection
32
+ * (in-view/out-of-view) transitions with other effects on their own schedule —
33
+ * mirroring {@link TestInput.pushable}. `observeIntersection(target)` subscribes
34
+ * to a hub, so every subscriber sees the same transitions and only
35
+ * events pushed *after* a subscription is live reach it. `pushIntersection`
36
+ * doesn't yield: a subscriber handles the transition inside the push,
37
+ * as `TestInput.pushable`'s do. `pushScroll` sets the scroll every container reads,
38
+ * through the Environment's registry, as a scroll event would.
39
+ * measure/viewport/prefersReducedMotion/observeResize behave as in {@link layer}.
40
+ */
41
+ export declare const pushable: (options?: Options) => Effect.Effect<{
42
+ readonly layer: Layer.Layer<Probe.Probe>;
43
+ readonly pushIntersection: (inView: boolean) => Effect.Effect<void>;
44
+ readonly pushScroll: (scroll: Probe.Scroll) => Effect.Effect<void, never, AtomRegistry.AtomRegistry>;
45
+ }>;
46
+ //# sourceMappingURL=probe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,oBAAoB,CAAA;AAE3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC,OAAO,EAAQ,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAWtD,yCAAyC;AAEzC,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,MAAM,CAAA;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IACvE,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,CAAA;IACvC,uDAAuD;IACvD,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;IAC9B,gGAAgG;IAChG,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,MAAM,CAAA;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;CAC/C;AAED;;;;GAIG;AACH,eAAO,MAAM,KAAK,aAAa,OAAO,KAAQ,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAMpE,CAAA;AAeD;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,QAAQ,aAAa,OAAO,KAAQ,MAAM,CAAC,MAAM,CAAC;IAC7D,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IACxC,QAAQ,CAAC,gBAAgB,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACnE,QAAQ,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,YAAY,CAAC,YAAY,CAAC,CAAA;CACrG,CAUG,CAAA"}
package/dist/probe.js ADDED
@@ -0,0 +1,60 @@
1
+ import * as Probe from "@wardic/core/Probe";
2
+ import * as Arr from "effect/Array";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Layer from "effect/Layer";
5
+ import * as Stream from "effect/Stream";
6
+ import { Atom, AtomRegistry } from "effect/reactivity";
7
+ import * as Feeds from "./feeds.js";
8
+ const ZERO_BOUNDS = { x: 0, y: 0, width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0 };
9
+ const VIEWPORT = { width: 1024, height: 768 };
10
+ /** A page that doesn't scroll: no range, and progress 1, as Motion reads a container with none. */
11
+ const STILL_SCROLL = { x: { current: 0, length: 0, progress: 1 }, y: { current: 0, length: 0, progress: 1 } };
12
+ /** A scripted in-view boolean as the Probe reports it, with no platform record. */
13
+ const toIntersection = (intersecting) => ({ intersecting });
14
+ /**
15
+ * A static Probe: fixed bounds/viewport. `observeIntersection` replays the
16
+ * optional `intersection` script to each subscriber (empty by default, so it
17
+ * emits nothing); `observeResize` stays empty.
18
+ */
19
+ export const layer = (options = {}) => {
20
+ const { intersection = [], scroll = STILL_SCROLL } = options;
21
+ return Layer.succeed(Probe.Probe, {
22
+ ...fixed(options, Atom.make(scroll)),
23
+ observeIntersection: () => Feeds.script(Arr.map(intersection, toIntersection)),
24
+ });
25
+ };
26
+ /**
27
+ * Both Probes' fixed part: the bounds, the viewport, the preference and the page's visibility, each defaulted, no resizes,
28
+ * and one scroll Atom for every container, target and offset (#234).
29
+ */
30
+ const fixed = ({ bounds = ZERO_BOUNDS, viewport = VIEWPORT, prefersReducedMotion = false, pageVisible = true }, scroll) => ({
31
+ measure: () => Effect.sync(() => bounds),
32
+ viewport: Atom.make(viewport),
33
+ prefersReducedMotion: Atom.make(prefersReducedMotion),
34
+ pageVisible: Atom.make(pageVisible),
35
+ scroll: () => Effect.succeed(scroll),
36
+ observeResize: () => Stream.empty,
37
+ });
38
+ /**
39
+ * A push-based Probe for tests that need to interleave intersection
40
+ * (in-view/out-of-view) transitions with other effects on their own schedule —
41
+ * mirroring {@link TestInput.pushable}. `observeIntersection(target)` subscribes
42
+ * to a hub, so every subscriber sees the same transitions and only
43
+ * events pushed *after* a subscription is live reach it. `pushIntersection`
44
+ * doesn't yield: a subscriber handles the transition inside the push,
45
+ * as `TestInput.pushable`'s do. `pushScroll` sets the scroll every container reads,
46
+ * through the Environment's registry, as a scroll event would.
47
+ * measure/viewport/prefersReducedMotion/observeResize behave as in {@link layer}.
48
+ */
49
+ // The hub: #318. No yield: ADR-0032. Scroll: #234.
50
+ export const pushable = (options = {}) => Effect.sync(() => {
51
+ const intersections = Feeds.hub();
52
+ const { scroll = STILL_SCROLL } = options;
53
+ const scrollAtom = Atom.make(scroll);
54
+ return {
55
+ layer: Layer.succeed(Probe.Probe, { ...fixed(options, scrollAtom), observeIntersection: () => intersections.feed }),
56
+ pushIntersection: (inView) => intersections.push(toIntersection(inView)),
57
+ pushScroll: (next) => AtomRegistry.AtomRegistry.use((registry) => Effect.sync(() => registry.set(scrollAtom, next))),
58
+ };
59
+ });
60
+ //# sourceMappingURL=probe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"probe.js","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,oBAAoB,CAAA;AAC3C,OAAO,KAAK,GAAG,MAAM,cAAc,CAAA;AACnC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACtD,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAEnC,MAAM,WAAW,GAAiB,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAA;AAC3G,MAAM,QAAQ,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAA;AAC7C,mGAAmG;AACnG,MAAM,YAAY,GAAiB,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAA;AAE3H,mFAAmF;AACnF,MAAM,cAAc,GAAG,CAAC,YAAqB,EAAsB,EAAE,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAA;AAoBxF;;;;GAIG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,OAAO,GAAY,EAAE,EAA4B,EAAE;IACvE,MAAM,EAAE,YAAY,GAAG,EAAE,EAAE,MAAM,GAAG,YAAY,EAAE,GAAG,OAAO,CAAA;IAC5D,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE;QAChC,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACpC,mBAAmB,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;KAC/E,CAAC,CAAA;AACJ,CAAC,CAAA;AAED;;;GAGG;AACH,MAAM,KAAK,GAAG,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,QAAQ,GAAG,QAAQ,EAAE,oBAAoB,GAAG,KAAK,EAAE,WAAW,GAAG,IAAI,EAAW,EAAE,MAA+B,EAAE,EAAE,CAAC,CAAC;IAC5J,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC;IACxC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;IAC7B,oBAAoB,EAAE,IAAI,CAAC,IAAI,CAAC,oBAAoB,CAAC;IACrD,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC;IACnC,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;IACpC,aAAa,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK;CAClC,CAAC,CAAA;AAEF;;;;;;;;;;GAUG;AACH,mDAAmD;AACnD,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,OAAO,GAAY,EAAE,EAI3C,EAAE,CACH,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;IACf,MAAM,aAAa,GAAG,KAAK,CAAC,GAAG,EAAsB,CAAA;IACrD,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE,GAAG,OAAO,CAAA;IACzC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACpC,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,GAAG,KAAK,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,mBAAmB,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC;QACnH,gBAAgB,EAAE,CAAC,MAAe,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QACjF,UAAU,EAAE,CAAC,IAAkB,EAAE,EAAE,CAAC,YAAY,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC;KACnI,CAAA;AACH,CAAC,CAAC,CAAA"}
@@ -0,0 +1,32 @@
1
+ import * as Renderer from "@wardic/core/Renderer";
2
+ import * as Layer from "effect/Layer";
3
+ /** A single recorded `project` call. */
4
+ export interface ProjectCall {
5
+ readonly values: Record<string, unknown>;
6
+ }
7
+ /** A single recorded `commit` call. */
8
+ export interface CommitCall {
9
+ readonly target: unknown;
10
+ readonly patch: Renderer.Patch;
11
+ }
12
+ /**
13
+ * A Renderer that records every `project` and `commit` invocation into
14
+ * separate arrays instead of touching an environment — lets a test assert
15
+ * both the pure projection step and the imperative writes that would have
16
+ * been issued.
17
+ *
18
+ * Defaults:
19
+ * - `project` is identity (`(values) => values as Patch`).
20
+ * - `commit` pushes onto `commitCalls`.
21
+ */
22
+ export declare const make: () => {
23
+ readonly service: {
24
+ readonly project: (values: Record<string, unknown>) => Renderer.Patch;
25
+ readonly commit: (target: unknown, patch: Renderer.Patch) => void;
26
+ };
27
+ readonly projectCalls: ReadonlyArray<ProjectCall>;
28
+ readonly commitCalls: ReadonlyArray<CommitCall>;
29
+ };
30
+ /** The {@link DISCARDING} Renderer's Layer. */
31
+ export declare const layer: Layer.Layer<Renderer.Renderer>;
32
+ //# sourceMappingURL=renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,uBAAuB,CAAA;AAEjD,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC,wCAAwC;AACxC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACzC;AAED,uCAAuC;AACvC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAA;CAC/B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,IAAI,QAAO;IACtB,QAAQ,CAAC,OAAO,EAAE;QAChB,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,QAAQ,CAAC,KAAK,CAAA;QACrE,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,KAAK,IAAI,CAAA;KAClE,CAAA;IACD,QAAQ,CAAC,YAAY,EAAE,aAAa,CAAC,WAAW,CAAC,CAAA;IACjD,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,UAAU,CAAC,CAAA;CAiBhD,CAAA;AAaD,+CAA+C;AAC/C,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAgD,CAAA"}
@@ -0,0 +1,43 @@
1
+ import * as Renderer from "@wardic/core/Renderer";
2
+ import { constVoid } from "effect/Function";
3
+ import * as Layer from "effect/Layer";
4
+ /**
5
+ * A Renderer that records every `project` and `commit` invocation into
6
+ * separate arrays instead of touching an environment — lets a test assert
7
+ * both the pure projection step and the imperative writes that would have
8
+ * been issued.
9
+ *
10
+ * Defaults:
11
+ * - `project` is identity (`(values) => values as Patch`).
12
+ * - `commit` pushes onto `commitCalls`.
13
+ */
14
+ export const make = () => {
15
+ const projectCalls = [];
16
+ const commitCalls = [];
17
+ return {
18
+ service: {
19
+ project: (values) => {
20
+ projectCalls.push({ values });
21
+ return values;
22
+ },
23
+ commit: (target, patch) => {
24
+ commitCalls.push({ target, patch });
25
+ },
26
+ },
27
+ projectCalls,
28
+ commitCalls,
29
+ };
30
+ };
31
+ /**
32
+ * A Renderer that draws nothing and keeps nothing: `project` hands its values back as the patch, and
33
+ * `commit` drops it. A test that reads the calls makes its own with {@link make}.
34
+ */
35
+ // #618 (decision 7): this was one recording renderer made at module load, whose calls every test in a
36
+ // file added to and nothing read.
37
+ const DISCARDING = {
38
+ project: (values) => values,
39
+ commit: constVoid,
40
+ };
41
+ /** The {@link DISCARDING} Renderer's Layer. */
42
+ export const layer = Layer.succeed(Renderer.Renderer, DISCARDING);
43
+ //# sourceMappingURL=renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.js","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,uBAAuB,CAAA;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAarC;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,GAOlB,EAAE;IACF,MAAM,YAAY,GAAuB,EAAE,CAAA;IAC3C,MAAM,WAAW,GAAsB,EAAE,CAAA;IACzC,OAAO;QACL,OAAO,EAAE;YACP,OAAO,EAAE,CAAC,MAA+B,EAAkB,EAAE;gBAC3D,YAAY,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAA;gBAC7B,OAAO,MAAwB,CAAA;YACjC,CAAC;YACD,MAAM,EAAE,CAAC,MAAe,EAAE,KAAqB,EAAQ,EAAE;gBACvD,WAAW,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAA;YACrC,CAAC;SACF;QACD,YAAY;QACZ,WAAW;KACZ,CAAA;AACH,CAAC,CAAA;AAED;;;GAGG;AACH,sGAAsG;AACtG,kCAAkC;AAClC,MAAM,UAAU,GAAiC;IAC/C,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAwB;IAC7C,MAAM,EAAE,SAAS;CAClB,CAAA;AAED,+CAA+C;AAC/C,MAAM,CAAC,MAAM,KAAK,GAAmC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "@wardic/test",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0-rc.2",
4
+ "description": "Headless testing Layers for Wardic: a clock that replays the frames you give it, a renderer that records what it's asked to draw, and input you push, so animation code runs without a browser.",
5
+ "keywords": [
6
+ "animation",
7
+ "testing",
8
+ "effect"
9
+ ],
10
+ "homepage": "https://wardic.dev",
11
+ "bugs": "https://github.com/wardiclabs/wardic/issues",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/wardiclabs/wardic.git",
15
+ "directory": "packages/test"
16
+ },
17
+ "license": "MIT",
18
+ "author": {
19
+ "name": "The Wardic contributors",
20
+ "email": "hello@wardic.dev",
21
+ "url": "https://wardic.dev"
22
+ },
23
+ "files": [
24
+ "dist/**/*.js",
25
+ "dist/**/*.d.ts",
26
+ "dist/**/*.map",
27
+ "src",
28
+ "README.md",
29
+ "LICENSE",
30
+ "THIRD_PARTY_NOTICES.md"
31
+ ],
32
+ "type": "module",
33
+ "sideEffects": false,
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "exports": {
38
+ "./package.json": "./package.json",
39
+ ".": {
40
+ "@wardic/source": "./src/index.ts",
41
+ "types": "./dist/index.d.ts",
42
+ "default": "./dist/index.js"
43
+ }
44
+ },
45
+ "scripts": {
46
+ "build": "../../node_modules/typescript-7/bin/tsc --build",
47
+ "typecheck": "../../node_modules/typescript-7/bin/tsc --build",
48
+ "typecheck:tests": "../../node_modules/typescript-7/bin/tsc -p tsconfig.test.json"
49
+ },
50
+ "dependencies": {
51
+ "@wardic/core": "1.0.0-rc.2"
52
+ },
53
+ "peerDependencies": {
54
+ "effect": "^4.1.1"
55
+ }
56
+ }
package/src/clock.ts ADDED
@@ -0,0 +1,136 @@
1
+ import * as Clock from "@wardic/core/Clock"
2
+ import * as RefreshFrameSource from "@wardic/core/RefreshFrameSource"
3
+ import * as Arr from "effect/Array"
4
+ import * as Effect from "effect/Effect"
5
+ import { constVoid } from "effect/Function"
6
+ import * as Layer from "effect/Layer"
7
+ import * as MutableRef from "effect/MutableRef"
8
+ import * as Predicate from "effect/Predicate"
9
+ import * as PubSub from "effect/PubSub"
10
+ import * as Stream from "effect/Stream"
11
+ import * as UndefinedOr from "effect/UndefinedOr"
12
+
13
+ /**
14
+ * A deterministic Clock for frame-level tests. `ticks` replays the supplied `dt`
15
+ * sequence (in seconds) exactly once, so a primitive's per-frame output can be
16
+ * collected with `Stream.runCollect` and asserted. Its `now` is the time those
17
+ * ticks have reached, in ms, from 0: the furthest any subscriber has
18
+ * reached, so two subscribers replaying the same frames don't run it twice as
19
+ * fast. A live Clock (rAF-driven) is `@wardic/dom`'s.
20
+ */
21
+ // `now`: #460.
22
+ export const make = (frames: ReadonlyArray<number> = []): Clock.Clock["Service"] => {
23
+ const time = MutableRef.make(0)
24
+ // Each frame with the time it reaches, in ms from 0 (#618, decision 7: each subscriber added its
25
+ // frames to one sum).
26
+ const reached = Arr.zip(frames, Arr.drop(Arr.scan(frames, 0, (at, dt) => at + dt * 1000), 1))
27
+ return {
28
+ ticks: Stream.map(Stream.fromIterable(reached), ([dt, at]) => {
29
+ MutableRef.update(time, (t) => Math.max(t, at))
30
+ return dt
31
+ }),
32
+ tick: (_t: number) => Effect.sync(() => {}),
33
+ now: () => MutableRef.get(time),
34
+ }
35
+ }
36
+
37
+ /** Clock Layer that emits `frames` (dt values, seconds) then completes. */
38
+ export const layer = (frames: ReadonlyArray<number> = []): Layer.Layer<Clock.Clock> =>
39
+ // A Clock per build: one Layer built twice shares no time (#618).
40
+ Layer.sync(Clock.Clock, () => make(frames))
41
+
42
+ /**
43
+ * Convenience: `n` frames of a fixed `dt` (default 1/60s) — e.g. `frames(30)`
44
+ * for half a second at 60fps.
45
+ */
46
+ export const frames = (count: number, dt = 1 / 60): ReadonlyArray<number> =>
47
+ Array.from({ length: count }, () => dt)
48
+
49
+ /**
50
+ * The Refresh frames `pushable`'s `advance` fires: a `RefreshFrameSource` that keeps the
51
+ * contract's order rules, as `requestAnimationFrame` does (ADR-0031, decision 4). `fire` runs the
52
+ * callbacks requested before it, in request order. One requested while they run waits for the
53
+ * next `fire`, and one cancelled before its turn never runs.
54
+ */
55
+ const makeFrames = () => {
56
+ const pending = new Map<number, (timestampMs: number) => void>()
57
+ const nextHandle = MutableRef.make(1)
58
+ const source: RefreshFrameSource.Shape = {
59
+ requestFrame: (callback) => {
60
+ const handle = MutableRef.getAndIncrement(nextHandle)
61
+ pending.set(handle, callback)
62
+ return handle
63
+ },
64
+ cancelFrame: (handle) => {
65
+ if (Predicate.isNumber(handle)) pending.delete(handle)
66
+ },
67
+ }
68
+ /** Run the callbacks requested so far at timestamp `t` (ms). */
69
+ const fire = (t: number): void =>
70
+ Arr.forEach(Arr.fromIterable(pending.keys()), (handle) =>
71
+ UndefinedOr.match(pending.get(handle), {
72
+ onUndefined: constVoid,
73
+ onDefined: (callback) => {
74
+ pending.delete(handle)
75
+ callback(t)
76
+ },
77
+ }))
78
+ return { source, fire }
79
+ }
80
+
81
+ /**
82
+ * A push-based Clock for tests that need to advance frames *on demand* —
83
+ * interleaved with pointer input — rather than replaying a fixed sequence. The
84
+ * static {@link layer} drains its whole `ticks` stream as fast as the consumer
85
+ * pulls, which makes it impossible to assert "advance a few frames, do
86
+ * something, advance more". This form lets the test `advance` one dt at a time.
87
+ *
88
+ * Backed by a `PubSub` (broadcast, no replay) so a consumer only sees ticks
89
+ * pushed *after* it subscribes — matching a live rAF clock where motion starts
90
+ * from the moment you subscribe. It has no Cold start: its ticks are the advances.
91
+ *
92
+ * Its `layer` also provides the Environment's `RefreshFrameSource`, and one `advance`
93
+ * is one Refresh frame in the web's order: it runs the frames requested so far (a flush gate's
94
+ * flush of a write made outside a tick), in request order, then publishes `dt` (the Clock's
95
+ * tick, at whose end a Frame loop over this Clock calls its `afterTick` hooks: the gate's
96
+ * flush). A frame requested during that `advance` waits for the next one. The frames'
97
+ * timestamps start at 0 and advance by `dt`, and the Clock's `now` is the last one's: a test's
98
+ * advances drive velocities too.
99
+ *
100
+ * It doesn't yield: a subscriber waiting on the ticks handles the tick inside the
101
+ * publish, as every subscriber does inside a real Refresh frame's callback, so a consumer that
102
+ * hops through Effect's scheduler on its way to its sink hasn't got there when `advance`
103
+ * returns, and its test sees it.
104
+ *
105
+ * @example
106
+ * const clock = yield* TestClock.pushable()
107
+ * // … fork a stream that consumes Clock.ticks, providing clock.layer …
108
+ * yield* clock.advance() // one 1/60s frame
109
+ * yield* clock.advance(1 / 30) // a custom dt
110
+ */
111
+ // The RefreshFrameSource: ADR-0031. The gate's flush at the tick's end: ADR-0033. `now`: #460. No
112
+ // yield: ADR-0032.
113
+ export const pushable: () => Effect.Effect<{
114
+ readonly layer: Layer.Layer<Clock.Clock | RefreshFrameSource.RefreshFrameSource>
115
+ readonly advance: (dt?: number) => Effect.Effect<void>
116
+ }> = Effect.fnUntraced(
117
+ function* () {
118
+ const pubsub = yield* PubSub.unbounded<number>()
119
+ const frames = makeFrames()
120
+ const time = MutableRef.make(0)
121
+ return {
122
+ layer: Layer.merge(
123
+ Layer.succeed(Clock.Clock, {
124
+ ticks: Stream.fromPubSub(pubsub),
125
+ tick: (_t: number) => Effect.sync(() => {}),
126
+ now: () => MutableRef.get(time),
127
+ }),
128
+ RefreshFrameSource.layer(frames.source),
129
+ ),
130
+ advance: (dt = 1 / 60) =>
131
+ Effect.sync(() => frames.fire(MutableRef.updateAndGet(time, (t) => t + dt * 1000))).pipe(
132
+ Effect.andThen(PubSub.publish(pubsub, dt)),
133
+ Effect.asVoid,
134
+ ),
135
+ }
136
+ })
package/src/feeds.ts ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The Test Environment's **Input feeds** (#318): a script replayed to each subscriber, and a hub a
3
+ * test pushes into.
4
+ */
5
+ import type * as InputFeed from "@wardic/core/InputFeed"
6
+ import * as Arr from "effect/Array"
7
+ import * as Effect from "effect/Effect"
8
+ import { constVoid } from "effect/Function"
9
+
10
+ /**
11
+ * A fixed script as a feed: each subscriber hears every event as it subscribes, then the end. A
12
+ * Stream over it (`InputFeed.toStream`) emits the script and ends, as `Stream.fromIterable` did; an
13
+ * empty one ends at once. Not shared: each subscriber gets the whole script (tests address one
14
+ * surface, and a consumer may read it twice).
15
+ */
16
+ export const script =
17
+ <A>(events: ReadonlyArray<A>): InputFeed.InputFeed<A> =>
18
+ (emit, end) => {
19
+ Arr.forEach(events, emit)
20
+ end()
21
+ return constVoid
22
+ }
23
+
24
+ /** A live feed a test pushes into, and the push. */
25
+ export interface Hub<A> {
26
+ readonly feed: InputFeed.InputFeed<A>
27
+ readonly push: (event: A) => Effect.Effect<void>
28
+ }
29
+
30
+ /**
31
+ * A live feed: every subscriber hears each event pushed after it subscribed, in the order they
32
+ * subscribed. `push` doesn't yield (ADR-0032): a subscriber handles the event inside the push, as an
33
+ * Environment's consumers handle a real event inside its dispatch.
34
+ */
35
+ export const hub = <A>(): Hub<A> => {
36
+ let subscribers: ReadonlyArray<(event: A) => void> = []
37
+ return {
38
+ feed: (emit) => {
39
+ subscribers = Arr.append(subscribers, emit)
40
+ return () => {
41
+ subscribers = Arr.filter(subscribers, (s) => s !== emit)
42
+ }
43
+ },
44
+ push: (event) => Effect.sync(() => Arr.forEach(subscribers, (emit) => emit(event))),
45
+ }
46
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @wardic/test — headless testing Layers.
3
+ *
4
+ * Provide `TestLive` (or `testLayer(frames)`) in place of `@wardic/dom`'s
5
+ * `DomLive` to run animation code without a page. The Clock is deterministic
6
+ * (replays a `dt` sequence), so primitive output is collectable frame-by-frame.
7
+ * Nothing is drawn on a page: an app's components are tested with `DomLive`
8
+ * under the test runner's fake timers, as https://wardic.dev/guides/testing shows.
9
+ */
10
+ // The testing guide: #558.
11
+ export const VERSION = "1.0.0-rc.2"
12
+
13
+ export * as TestClock from "./clock.ts"
14
+ export * as TestRenderer from "./renderer.ts"
15
+ export * as TestProbe from "./probe.ts"
16
+ export * as TestInput from "./input.ts"
17
+ export { testLayer, TestLive } from "./layer.ts"