@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.
- package/LICENSE +21 -0
- package/README.md +13 -2
- package/THIRD_PARTY_NOTICES.md +145 -0
- package/dist/clock.d.ts +55 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +119 -0
- package/dist/clock.js.map +1 -0
- package/dist/feeds.d.ts +25 -0
- package/dist/feeds.d.ts.map +1 -0
- package/dist/feeds.js +32 -0
- package/dist/feeds.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/input.d.ts +60 -0
- package/dist/input.d.ts.map +1 -0
- package/dist/input.js +90 -0
- package/dist/input.js.map +1 -0
- package/dist/layer.d.ts +27 -0
- package/dist/layer.d.ts.map +1 -0
- package/dist/layer.js +43 -0
- package/dist/layer.js.map +1 -0
- package/dist/probe.d.ts +46 -0
- package/dist/probe.d.ts.map +1 -0
- package/dist/probe.js +60 -0
- package/dist/probe.js.map +1 -0
- package/dist/renderer.d.ts +32 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +43 -0
- package/dist/renderer.js.map +1 -0
- package/package.json +54 -4
- package/src/clock.ts +136 -0
- package/src/feeds.ts +46 -0
- package/src/index.ts +17 -0
- package/src/input.ts +104 -0
- package/src/layer.ts +78 -0
- package/src/probe.ts +87 -0
- package/src/renderer.ts +63 -0
|
@@ -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"}
|
package/dist/layer.d.ts
ADDED
|
@@ -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"}
|
package/dist/probe.d.ts
ADDED
|
@@ -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":"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"}
|
|
@@ -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"}
|
package/dist/renderer.js
ADDED
|
@@ -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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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"
|