@systemfsoftware/xstate-test 0.0.0-stage → 2.0.0-beta.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,74 @@
1
+ import { a as propertyTest, o as testPaths } from "./propertyTest-BHnBV8ax.js";
2
+ import { ActorLogic } from "@systemfsoftware/xstate";
3
+ import { TestAPI } from "vitest";
4
+ type Source = ActorLogic<any, any, any>;
5
+ /**
6
+ * A Vitest-compatible `it` or `test`: `(name, fn, timeout?) => void`.
7
+ *
8
+ * @experimental
9
+ */
10
+ export type ModelTestBase = (name: string, fn: (context: any) => Promise<void>, timeout?: number) => unknown;
11
+ /**
12
+ * What an expected failure must look like. See `it.model.fails`.
13
+ *
14
+ * @experimental
15
+ */
16
+ export interface ModelTestFailureExpectation {
17
+ /** A substring of, or a pattern matching, the failure message. */
18
+ readonly message?: string | RegExp;
19
+ }
20
+ /** @experimental */
21
+ export interface ModelTestFunction {
22
+ /**
23
+ * Registers a test that runs `propertyTest(source, options)`. Failures are
24
+ * saved under the test file and name, and replayed first on the next run.
25
+ * On failure, the coverage report is printed.
26
+ */
27
+ <TSource extends Source>(name: string, source: TSource, options: Parameters<typeof propertyTest<TSource>>[1], timeout?: number): void;
28
+ /**
29
+ * Registers a test that passes only when `propertyTest(source, options)`
30
+ * fails, and its message matches `expected.message`.
31
+ */
32
+ fails<TSource extends Source>(name: string, source: TSource, options: Parameters<typeof propertyTest<TSource>>[1], expected?: ModelTestFailureExpectation): void;
33
+ }
34
+ /** @experimental */
35
+ export interface PathsTestFunction {
36
+ /** Registers a test that runs `testPaths(source, options)`. */
37
+ <TSource extends Source>(name: string, source: TSource, options?: Parameters<typeof testPaths<TSource>>[1], timeout?: number): void;
38
+ }
39
+ /**
40
+ * The members `withModelTests()` adds to `it` or `test`.
41
+ *
42
+ * @experimental
43
+ */
44
+ export interface ModelTestAPI {
45
+ readonly model: ModelTestFunction;
46
+ readonly paths: PathsTestFunction;
47
+ }
48
+ /**
49
+ * Adds `model` and `paths` to a Vitest `it` or `test`:
50
+ *
51
+ * ```ts
52
+ * import { it as vitestIt } from 'vitest';
53
+ * import { withModelTests } from '@systemfsoftware/xstate-test/vitest';
54
+ *
55
+ * const it = withModelTests(vitestIt);
56
+ * it.model('the cart matches the model', cartMachine, { sut });
57
+ * ```
58
+ *
59
+ * @experimental
60
+ */
61
+ export declare function withModelTests<TBase extends ModelTestBase>(base: TBase): TBase & ModelTestAPI;
62
+ /**
63
+ * Vitest's global `it`, with `it.model` and `it.paths`.
64
+ *
65
+ * @experimental
66
+ */
67
+ export declare const it: TestAPI & ModelTestAPI;
68
+ /**
69
+ * Vitest's global `test`, with `test.model` and `test.paths`.
70
+ *
71
+ * @experimental
72
+ */
73
+ export declare const test: TestAPI & ModelTestAPI;
74
+ export {};
@@ -0,0 +1,139 @@
1
+ import { C as consoleLineLogger, O as testCoverageToJSON, n as propertyTest, r as testPaths, w as formatTestCoverage } from "./propertyTest-5bO0RsUu.mjs";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Cause from "effect/Cause";
4
+ import * as Logger from "effect/Logger";
5
+ // src/vitest.ts
6
+ /**
7
+ * Awaits `value` as an Effect. A value and a thenable are both accepted, and a
8
+ * rejection stays the same error object when the effect is run.
9
+ */
10
+ const awaited = (value) => Effect.promise(() => Promise.resolve(value));
11
+ function getTestKey(context) {
12
+ const names = [context.task.name];
13
+ for (let suite = context.task.suite; suite; suite = suite.suite) if (suite.name.length > 0) names.unshift(suite.name);
14
+ return [context.task.file?.name, ...names].filter(Boolean).join(" > ");
15
+ }
16
+ /**
17
+ * Keys the failure database by the test, unless `failures` is `false`, a
18
+ * custom store, or sets its own `key`.
19
+ */
20
+ function withTestFailures(options, context) {
21
+ const failures = options.failures;
22
+ if (failures === false) return options;
23
+ if (failures !== void 0 && typeof failures === "object" && ("load" in failures || failures.key !== void 0)) return options;
24
+ return {
25
+ ...options,
26
+ failures: {
27
+ ...typeof failures === "object" ? failures : {},
28
+ key: getTestKey(context)
29
+ }
30
+ };
31
+ }
32
+ /** The test timeout for a campaign bounded by `until: { timeMs }`. */
33
+ function getTimeout(options, timeout) {
34
+ if (timeout !== void 0) return timeout;
35
+ const until = options?.until;
36
+ return typeof until === "object" && typeof until.timeMs === "number" ? until.timeMs + 5e3 : void 0;
37
+ }
38
+ function getCoverage(error) {
39
+ return error?.coverage;
40
+ }
41
+ function attachCoverage(context, coverage) {
42
+ if (coverage !== void 0) context.task.meta["xstateTestCoverage"] = testCoverageToJSON(coverage);
43
+ }
44
+ function getMessage(error) {
45
+ return error instanceof Error ? error.message : String(error);
46
+ }
47
+ function matchesExpectation(error, expected) {
48
+ const pattern = expected?.message;
49
+ if (pattern === void 0) return true;
50
+ const message = getMessage(error);
51
+ return typeof pattern === "string" ? message.includes(pattern) : pattern.test(message);
52
+ }
53
+ /**
54
+ * Adds `model` and `paths` to a Vitest `it` or `test`:
55
+ *
56
+ * ```ts
57
+ * import { it as vitestIt } from 'vitest';
58
+ * import { withModelTests } from '@systemfsoftware/xstate-test/vitest';
59
+ *
60
+ * const it = withModelTests(vitestIt);
61
+ * it.model('the cart matches the model', cartMachine, { sut });
62
+ * ```
63
+ *
64
+ * @experimental
65
+ */
66
+ function withModelTests(base) {
67
+ return withModelAPI(() => base);
68
+ }
69
+ /**
70
+ * A callable that forwards everything to the base `it`, plus `model` and
71
+ * `paths`. The base is not modified.
72
+ */
73
+ function withModelAPI(resolve) {
74
+ const api = createModelTestAPI(resolve);
75
+ return new Proxy(function() {}, {
76
+ apply: (_target, thisArg, args) => Reflect.apply(resolve(), thisArg, args),
77
+ get: (_target, property) => property === "model" || property === "paths" ? api[property] : Reflect.get(resolve(), property)
78
+ });
79
+ }
80
+ function createModelTestAPI(getBase) {
81
+ const run = (context, campaign) => Effect.runPromise(Effect.onError(Effect.gen(function* () {
82
+ const { coverage } = yield* awaited(campaign());
83
+ attachCoverage(context, coverage);
84
+ }), (cause) => {
85
+ const coverage = getCoverage(Cause.squash(cause));
86
+ attachCoverage(context, coverage);
87
+ if (coverage !== void 0) Effect.runSync(Effect.log(formatTestCoverage(coverage)).pipe(Effect.provide(Logger.layer([consoleLineLogger]))));
88
+ return Effect.void;
89
+ }));
90
+ const model = ((name, source, options, timeout) => {
91
+ getBase()(name, (context) => run(context, () => propertyTest(source, withTestFailures(options, context))), getTimeout(options, timeout));
92
+ });
93
+ model.fails = (name, source, options, expected) => {
94
+ getBase()(name, (context) => Effect.runPromise(Effect.gen(function* () {
95
+ const outcome = yield* Effect.catchCause(Effect.gen(function* () {
96
+ const { coverage } = yield* awaited(propertyTest(source, withTestFailures(options, context)));
97
+ attachCoverage(context, coverage);
98
+ return { failed: false };
99
+ }), (cause) => {
100
+ const error = Cause.squash(cause);
101
+ attachCoverage(context, getCoverage(error));
102
+ return Effect.succeed({
103
+ failed: true,
104
+ error
105
+ });
106
+ });
107
+ if (!outcome.failed) throw new Error(`Expected "${name}" to fail, but the campaign passed.`);
108
+ if (!matchesExpectation(outcome.error, expected)) throw new Error(`Expected "${name}" to fail with a message matching ${String(expected.message)}, but it failed with:\n${getMessage(outcome.error)}`, { cause: outcome.error });
109
+ })), getTimeout(options, void 0));
110
+ };
111
+ const paths = (name, source, options, timeout) => {
112
+ getBase()(name, (context) => run(context, () => testPaths(source, withTestFailures(options ?? {}, context))), getTimeout(options, timeout));
113
+ };
114
+ return {
115
+ model,
116
+ paths
117
+ };
118
+ }
119
+ /** A global `it` or `test`, resolved when it is first used. */
120
+ function fromGlobal(name) {
121
+ return withModelAPI(() => {
122
+ const base = globalThis[name];
123
+ if (typeof base !== "function") throw new Error(`\`${name}\` from @xstate/test/vitest uses Vitest's global \`${name}\`, which is not defined. Set \`test.globals: true\` in the Vitest config, or wrap Vitest's own: \`withModelTests(${name})\`.`);
124
+ return base;
125
+ });
126
+ }
127
+ /**
128
+ * Vitest's global `it`, with `it.model` and `it.paths`.
129
+ *
130
+ * @experimental
131
+ */
132
+ const it = fromGlobal("it");
133
+ /**
134
+ * Vitest's global `test`, with `test.model` and `test.paths`.
135
+ *
136
+ * @experimental
137
+ */
138
+ const test = fromGlobal("test");
139
+ export { it, test, withModelTests };
package/package.json CHANGED
@@ -1,6 +1,91 @@
1
1
  {
2
2
  "name": "@systemfsoftware/xstate-test",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "license": "MIT",
4
+ "version": "2.0.0-beta.2",
5
+ "author": "Ryan Lee <drdgvhbh@gmail.com>",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/systemfsoftware/xstate.git",
9
+ "directory": "packages/xstate-test"
10
+ },
11
+ "homepage": "https://github.com/systemfsoftware/xstate/tree/main/packages/xstate-test#readme",
12
+ "bugs": "https://github.com/systemfsoftware/xstate/issues",
13
+ "description": "Model-based testing for XState machines — forked under systemfsoftware from statelyai/xstate (@xstate/test at 2146ae26).",
14
+ "type": "module",
15
+ "sideEffects": false,
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "peerDependencies": {
20
+ "@systemfsoftware/xstate": "^6.0.0-beta.65",
21
+ "effect": "^4.1.0",
22
+ "fast-check": "^4",
23
+ "vitest": "^5",
24
+ "zod": "^4.0.0"
25
+ },
26
+ "peerDependenciesMeta": {
27
+ "effect": {
28
+ "optional": true
29
+ },
30
+ "vitest": {
31
+ "optional": true
32
+ },
33
+ "zod": {
34
+ "optional": true
35
+ }
36
+ },
37
+ "devDependencies": {
38
+ "@effect/tsgo": "0.64.1",
39
+ "@microsoft/api-extractor": "7.59.1",
40
+ "@playwright/test": "1.63.0",
41
+ "@systemfsoftware/arethetypeswrong-cli": "file:../../.deps/arethetypeswrong-cli.tgz",
42
+ "@systemfsoftware/tsconfig": "file:../../.deps/tsconfig.tgz",
43
+ "@systemfsoftware/vitest": "file:../../.deps/vitest.tgz",
44
+ "@systemfsoftware/xstate": "^6.0.0-beta.65",
45
+ "@types/node": "24.19.1",
46
+ "effect": "4.1.2",
47
+ "fast-check": "4.9.0",
48
+ "rimraf": "6.1.3",
49
+ "tsdown": "0.23.0",
50
+ "typescript": "7.0.2",
51
+ "vitest": "5.0.3",
52
+ "zod": "4.4.3"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public",
56
+ "provenance": true
57
+ },
58
+ "exports": {
59
+ ".": {
60
+ "types": "./dist/index.d.ts",
61
+ "default": "./dist/index.mjs"
62
+ },
63
+ "./effect-schema": {
64
+ "types": "./dist/effect-schema.d.ts",
65
+ "default": "./dist/effect-schema.mjs"
66
+ },
67
+ "./playwright": {
68
+ "types": "./dist/playwright.d.ts",
69
+ "default": "./dist/playwright.mjs"
70
+ },
71
+ "./schema": {
72
+ "types": "./dist/schema.d.ts",
73
+ "default": "./dist/schema.mjs"
74
+ },
75
+ "./vitest": {
76
+ "types": "./dist/vitest.d.ts",
77
+ "default": "./dist/vitest.mjs"
78
+ },
79
+ "./package.json": "./package.json"
80
+ },
81
+ "scripts": {
82
+ "clean": "rimraf dist",
83
+ "build": "tsdown -l warn && pnpm api:check",
84
+ "typecheck": "tsc -b",
85
+ "test": "vitest run",
86
+ "lint:tsgo": "effect-tsgo diagnostics --project tsconfig.app.json --format ${TSGO_FORMAT:-text} && ../../scripts/tsgo-test-diagnostics.ts",
87
+ "attw": "attw --pack .",
88
+ "api:check": "api-extractor run && api-extractor run --config api-extractor.effect-schema.json && api-extractor run --config api-extractor.playwright.json && api-extractor run --config api-extractor.schema.json && api-extractor run --config api-extractor.vitest.json",
89
+ "api:update": "api-extractor run --local && api-extractor run --local --config api-extractor.effect-schema.json && api-extractor run --local --config api-extractor.playwright.json && api-extractor run --local --config api-extractor.schema.json && api-extractor run --local --config api-extractor.vitest.json"
90
+ }
6
91
  }