@vttforge/testing 0.1.0 → 0.3.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # @vttforge/testing
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ae8dafb: `withMockFoundry` records sheets and enrichers.
8
+
9
+ Registering a sheet through `registerSystem({ sheets })` goes via `foundry.applications.apps.DocumentSheetConfig`, which the mock did not have — so a consumer's boot test got "Foundry is not available" rather than a result. Enrichers landed on `CONFIG.TextEditor.enrichers` with no way to read them back.
10
+
11
+ Both are now on the handle:
12
+
13
+ ```ts
14
+ const foundry = withMockFoundry();
15
+ registerSystem({ id: 'my-system', sheets: [{ id: 'character', document: 'Actor', sheet: CharacterSheet }] });
16
+ foundry.callHook('init');
17
+
18
+ foundry.sheets.map((s) => s.key); // ['my-system.character']
19
+ ```
20
+
21
+ `key` is the assertion worth writing. Foundry saves it on every document whose owner picked the sheet and builds it from the class name, which a bundler is free to rename between builds — so pinning the key is pinning that the reader's choice survives your next release.
22
+
23
+ `foundry.enrichers` reads back the namespaced id, pattern and `onRender`.
24
+
25
+ ## 0.2.0
26
+
27
+ ### Minor Changes
28
+
29
+ - 6a39f2a: First real release. It was a three-line placeholder.
30
+
31
+ Two entry points, because the two kinds of test run in different places.
32
+
33
+ `@vttforge/testing/vitest` installs the Foundry globals a package reaches for and hands back a handle that records what your code registered — hooks, settings, notifications — so a test can assert on what happened rather than only on what did not throw. Every `@vttforge/core` test built this by hand, differently each time; that is the problem it solves.
34
+
35
+ `@vttforge/testing/quench` registers a batch inside a live world, for what a mock cannot answer: a sheet that really draws, a socket with two clients, a document that round-trips through the database.
36
+
37
+ Also ships ambient declarations for the globals, since a test that mocks Foundry then reads `game.settings` otherwise gets "Cannot find name 'game'".
38
+
39
+ ### Patch Changes
40
+
41
+ - 9305156: The ambient globals now arrive with the import.
42
+
43
+ They shipped as a separate `@vttforge/testing/globals` export, documented for `compilerOptions.types`. That does not work: `types` entries resolve against package roots, not subpath exports, and a consumer following the README got `Cannot find type definition file`.
44
+
45
+ Importing from `@vttforge/testing/vitest` declares them instead — the import a test already writes is the moment it needs them, and there is nothing to configure.
46
+
47
+ Found by using the package from `@vttforge/core`.
48
+ - d015aee: Stop requiring Node 26 to install a browser package.
49
+
50
+ Every package declared `engines.node: ">=26.0.0"`. Four of them — `core`, `styles`, `types` and `dev-module` — compile to ES2022 and run in the browser inside Foundry. They never touch Node, and the floor did nothing except stop anyone on Node 22 LTS from installing the SDK at all.
51
+
52
+ Those four declare no engine now. `@vttforge/testing` drops to `>=22` — its Quench half runs in the browser too. `@vttforge/cli` and `@vttforge/vite-plugin` keep `>=26`, which is what they actually build against.
53
+
3
54
  ## 0.1.0
4
55
 
5
56
  ### Minor Changes
package/README.md CHANGED
@@ -1,11 +1,71 @@
1
1
  # @vttforge/testing
2
2
 
3
- Test helpers and Foundry mocks for VTTForge consumers. Wraps Vitest (unit tests) and Quench (in-Foundry browser tests).
3
+ Helpers for testing Foundry VTT packages.
4
4
 
5
- > **Status:** v0.0.1 placeholder. Implementation lands in v1.0.0.
5
+ Two entry points, because the two kinds of test run in different places.
6
6
 
7
- ## Planned scope (v1.0.0)
7
+ ## `@vttforge/testing/vitest`
8
8
 
9
- - Pre-built Foundry global mocks (`game`, `Hooks`, `CONFIG`, `Roll`, `ui.notifications`).
10
- - Quench test harness with VTTForge sheet helpers.
11
- - Snapshot helpers for `system.json` / `template.json` regression tests.
9
+ Runs in CI against mocked globals. Covers everything up to the moment a window
10
+ renders: data models, settings, hook registration, migrations, document
11
+ updates.
12
+
13
+ ```ts
14
+ import { createMockActor, withMockFoundry } from '@vttforge/testing/vitest';
15
+
16
+ const foundry = withMockFoundry();
17
+ registerMyModule();
18
+
19
+ foundry.callHook('init');
20
+ expect(foundry.settings[0].key).toBe('cacheSize');
21
+
22
+ foundry.restore();
23
+ ```
24
+
25
+ `withMockFoundry` installs `foundry`, `game`, `CONFIG`, `Hooks`, `ui` and
26
+ `CONST`, and hands back a handle that records what your code registered — hooks,
27
+ settings, notifications — so a test can assert on what happened rather than only
28
+ on what did not throw. `restore()` puts every global back, including deleting the
29
+ ones that never existed.
30
+
31
+ The mock documents behave like real ones where it counts: `update` merges rather
32
+ than replacing, and dotted paths expand. Both matter — a mock that replaces lets
33
+ a test pass while the real thing drops every sibling key.
34
+
35
+ ### Naming the globals
36
+
37
+ A test that reads `game.settings` would otherwise get "Cannot find name
38
+ 'game'". Importing from this entry declares them, so there is nothing to
39
+ configure — the import a test already writes is what brings them.
40
+
41
+ ## `@vttforge/testing/quench`
42
+
43
+ Runs inside a live world, for what a mock cannot answer: a sheet that really
44
+ draws, a socket with two clients, a document that round-trips through the
45
+ database.
46
+
47
+ ```ts
48
+ import { registerBatch } from '@vttforge/testing/quench';
49
+
50
+ registerBatch('my-module.sheets', ({ describe, it, assert }) => {
51
+ describe('character sheet', () => {
52
+ it('renders', async () => {
53
+ const actor = await Actor.create({ name: 'T', type: 'character' });
54
+ await actor.sheet.render(true);
55
+ assert.ok(actor.sheet.rendered);
56
+ await actor.delete();
57
+ });
58
+ });
59
+ });
60
+ ```
61
+
62
+ Safe to call at module scope: it waits for `quenchReady` rather than assuming
63
+ Quench has loaded, which is the mistake that makes a batch silently never
64
+ appear. Outside Foundry it is a no-op, so a file holding both kinds of test can
65
+ still be imported by the vitest run.
66
+
67
+ ## Where the line sits
68
+
69
+ Anything before `_renderHTML` is testable in Vitest. Real rendering is Quench's
70
+ half. Reaching for a mock past that line produces tests that pass and tell you
71
+ nothing.
@@ -0,0 +1,151 @@
1
+ //#region src/vitest/mock-foundry.d.ts
2
+ /**
3
+ * A Foundry runtime, faked well enough to import a package and construct
4
+ * its classes.
5
+ *
6
+ * Every `@vttforge/core` test built this by hand before this existed, and no
7
+ * two built it quite the same way. That is the problem: the globals a package
8
+ * needs are not obvious, they are not documented anywhere, and you discover
9
+ * them one `ReferenceError` at a time.
10
+ *
11
+ * What this covers is the boundary the SDK actually sits on — everything up
12
+ * to the moment a window renders. `_renderHTML` and beyond needs a real
13
+ * browser and a real Foundry; that is what the Quench half is for.
14
+ */
15
+ /** A Foundry document, as much of one as a unit test needs. */
16
+ interface MockDocument {
17
+ id: string;
18
+ name: string;
19
+ type: string;
20
+ system: Record<string, any>;
21
+ flags: Record<string, Record<string, unknown>>;
22
+ getFlag(scope: string, key: string): unknown;
23
+ setFlag(scope: string, key: string, value: unknown): Promise<MockDocument>;
24
+ unsetFlag(scope: string, key: string): Promise<MockDocument>;
25
+ update(delta: Record<string, unknown>): Promise<MockDocument>;
26
+ /** Every update this document received, in order. */
27
+ readonly updates: ReadonlyArray<Record<string, unknown>>;
28
+ }
29
+ interface MockDocumentOptions {
30
+ id?: string;
31
+ name?: string;
32
+ type?: string;
33
+ system?: Record<string, any>;
34
+ flags?: Record<string, Record<string, unknown>>;
35
+ }
36
+ /** An Actor for a test. Its `updates` record what the code under test wrote. */
37
+ declare function createMockActor(options?: MockDocumentOptions): MockDocument;
38
+ /** An Item for a test. */
39
+ declare function createMockItem(options?: MockDocumentOptions): MockDocument;
40
+ //#endregion
41
+ //#region src/vitest/with-mock-foundry.d.ts
42
+ /**
43
+ * Install the Foundry globals a package needs, and take them away again.
44
+ *
45
+ * The globals are the awkward part of testing anything built on Foundry:
46
+ * `foundry`, `game`, `CONFIG`, `Hooks`, `ui`, `CONST`. A package touches some
47
+ * subset, the subset is not written down, and you find it one
48
+ * `ReferenceError` at a time.
49
+ *
50
+ * Everything here is a plain object or a recording stub, so a test can assert
51
+ * on what was registered rather than only on what did not throw.
52
+ */
53
+ /** A hook registration the code under test made. */
54
+ interface RecordedHook {
55
+ event: string;
56
+ once: boolean;
57
+ fn: (...args: unknown[]) => unknown;
58
+ }
59
+ /** A setting the code under test registered. */
60
+ interface RecordedSetting {
61
+ namespace: string;
62
+ key: string;
63
+ config: Record<string, unknown>;
64
+ }
65
+ /** One `DocumentSheetConfig.registerSheet`, as VTTForge makes it. */
66
+ interface RecordedSheet {
67
+ /** The key Foundry persists: `<package id>.<sheet id>`. */
68
+ readonly key: string;
69
+ /** The package that registered it. */
70
+ readonly scope: string;
71
+ /** The sheet id — the class name VTTForge pinned. */
72
+ readonly id: string;
73
+ readonly sheetClass: unknown;
74
+ readonly documentClass: unknown;
75
+ readonly options: Record<string, unknown>;
76
+ }
77
+ /** One entry pushed onto `CONFIG.TextEditor.enrichers`. */
78
+ interface RecordedEnricher {
79
+ /** The namespaced id: `<package id>.<enricher id>`. */
80
+ readonly id: string;
81
+ readonly pattern: RegExp;
82
+ readonly onRender?: unknown;
83
+ }
84
+ interface MockFoundry {
85
+ /** Every `Hooks.on` and `Hooks.once`, in order. */
86
+ readonly hooks: ReadonlyArray<RecordedHook>;
87
+ /** Every `game.settings.register`, in order. */
88
+ readonly settings: ReadonlyArray<RecordedSetting>;
89
+ /** Every notification raised, by severity. */
90
+ readonly notifications: ReadonlyArray<{
91
+ level: 'info' | 'warn' | 'error';
92
+ message: string;
93
+ }>;
94
+ /**
95
+ * Every sheet registered, in order.
96
+ *
97
+ * `key` is the thing worth asserting: Foundry saves it on each document
98
+ * using the sheet, so a test that pins the key is a test that the reader's
99
+ * choice survives your next build.
100
+ */
101
+ readonly sheets: ReadonlyArray<RecordedSheet>;
102
+ /**
103
+ * Every text enricher registered, in order.
104
+ *
105
+ * `id` is namespaced, which is what stops a common name from colliding with
106
+ * another package — and what makes `onRender` fire at all.
107
+ */
108
+ readonly enrichers: ReadonlyArray<RecordedEnricher>;
109
+ /** Fire a hook the way Foundry would, for the listeners registered so far. */
110
+ callHook(event: string, ...args: unknown[]): unknown[];
111
+ /** Read back a registered setting's current value. */
112
+ getSetting(namespace: string, key: string): unknown;
113
+ /** Put every global back the way it was. */
114
+ restore(): void;
115
+ }
116
+ interface MockFoundryOptions {
117
+ /** The current user. Defaults to a GM, since most module code checks. */
118
+ user?: {
119
+ id?: string;
120
+ isGM?: boolean;
121
+ name?: string;
122
+ };
123
+ /** Extra `foundry.*` members, merged over the defaults. */
124
+ foundry?: Record<string, unknown>;
125
+ /** Extra `game.*` members, merged over the defaults. */
126
+ game?: Record<string, unknown>;
127
+ }
128
+ /**
129
+ * Stand up the globals, run the callback, tear them down.
130
+ *
131
+ * ```ts
132
+ * const foundry = withMockFoundry();
133
+ * registerMyModule();
134
+ * expect(foundry.hooks.map((h) => h.event)).toContain('init');
135
+ * foundry.restore();
136
+ * ```
137
+ */
138
+ declare function withMockFoundry(options?: MockFoundryOptions): MockFoundry;
139
+ //#endregion
140
+ //#region src/vitest/index.d.ts
141
+ declare global {
142
+ const foundry: any;
143
+ const game: any;
144
+ const CONFIG: any;
145
+ const Hooks: any;
146
+ const ui: any;
147
+ const CONST: any;
148
+ }
149
+ //#endregion
150
+ export { RecordedSheet as a, createMockActor as c, RecordedSetting as i, createMockItem as l, RecordedEnricher as n, withMockFoundry as o, RecordedHook as r, MockDocument as s, MockFoundry as t };
151
+ //# sourceMappingURL=index-CcUzNUCI.d.mts.map
@@ -0,0 +1,57 @@
1
+ //#region src/quench/register-batch.d.ts
2
+ /**
3
+ * Registering a Quench batch, for the tests that need a real Foundry.
4
+ *
5
+ * The vitest half covers everything up to the moment a window renders. Past
6
+ * that — a sheet that actually draws, a socket with two clients, a document
7
+ * that really round-trips through the database — needs the real thing, and
8
+ * Quench is how the community runs those from inside a running world.
9
+ *
10
+ * The whole ceremony is one `quenchReady` hook and a registration call, which
11
+ * everyone writes again and gets subtly wrong: register too early and Quench
12
+ * is not there, too late and the batch is missed.
13
+ */
14
+ /** What Quench hands a batch to describe its cases. */
15
+ interface QuenchContext {
16
+ describe(title: string, fn: () => void): void;
17
+ it(title: string, fn: () => unknown): void;
18
+ before(fn: () => unknown): void;
19
+ after(fn: () => unknown): void;
20
+ beforeEach(fn: () => unknown): void;
21
+ afterEach(fn: () => unknown): void;
22
+ assert: any;
23
+ expect: any;
24
+ }
25
+ interface BatchOptions {
26
+ /** Shown in the Quench window. Defaults to the batch id. */
27
+ displayName?: string;
28
+ /**
29
+ * Groups the batch in the Quench UI. Use your package id so a user running
30
+ * everything can tell whose failures are whose.
31
+ */
32
+ snapBaseline?: boolean;
33
+ }
34
+ /**
35
+ * Register a batch once Quench is ready.
36
+ *
37
+ * Safe to call at module scope — it waits for the hook rather than assuming
38
+ * Quench has loaded, which is the mistake that makes a batch silently never
39
+ * appear.
40
+ *
41
+ * ```ts
42
+ * registerBatch('my-module.sheets', ({ describe, it, assert }) => {
43
+ * describe('character sheet', () => {
44
+ * it('renders', async () => {
45
+ * const actor = await Actor.create({ name: 'T', type: 'character' });
46
+ * await actor.sheet.render(true);
47
+ * assert.ok(actor.sheet.rendered);
48
+ * await actor.delete();
49
+ * });
50
+ * });
51
+ * });
52
+ * ```
53
+ */
54
+ declare function registerBatch(id: string, fn: (context: QuenchContext) => void, options?: BatchOptions): void;
55
+ //#endregion
56
+ export { QuenchContext as n, registerBatch as r, BatchOptions as t };
57
+ //# sourceMappingURL=index-DLgaf8hZ.d.mts.map
package/dist/index.d.mts CHANGED
@@ -1,8 +1,3 @@
1
- //#region src/index.d.ts
2
- /**
3
- * @vttforge/testing placeholder. Implementation lands in v1.0.0.
4
- */
5
- declare const VTTFORGE_TESTING_VERSION = "0.0.1";
6
- //#endregion
7
- export { VTTFORGE_TESTING_VERSION };
8
- //# sourceMappingURL=index.d.mts.map
1
+ import { n as QuenchContext, r as registerBatch, t as BatchOptions } from "./index-DLgaf8hZ.mjs";
2
+ import { a as RecordedSheet, c as createMockActor, i as RecordedSetting, l as createMockItem, n as RecordedEnricher, o as withMockFoundry, r as RecordedHook, s as MockDocument, t as MockFoundry } from "./index-CcUzNUCI.mjs";
3
+ export { type BatchOptions, type MockDocument, type MockFoundry, type QuenchContext, type RecordedEnricher, type RecordedHook, type RecordedSetting, type RecordedSheet, createMockActor, createMockItem, registerBatch, withMockFoundry };
package/dist/index.mjs CHANGED
@@ -1,9 +1,3 @@
1
- //#region src/index.ts
2
- /**
3
- * @vttforge/testing placeholder. Implementation lands in v1.0.0.
4
- */
5
- const VTTFORGE_TESTING_VERSION = "0.0.1";
6
- //#endregion
7
- export { VTTFORGE_TESTING_VERSION };
8
-
9
- //# sourceMappingURL=index.mjs.map
1
+ import { t as registerBatch } from "./quench-CdVnhmcA.mjs";
2
+ import { n as createMockActor, r as createMockItem, t as withMockFoundry } from "./vitest-D0qlXJeT.mjs";
3
+ export { createMockActor, createMockItem, registerBatch, withMockFoundry };
@@ -0,0 +1,2 @@
1
+ import { n as QuenchContext, r as registerBatch, t as BatchOptions } from "../index-DLgaf8hZ.mjs";
2
+ export { type BatchOptions, type QuenchContext, registerBatch };
@@ -0,0 +1,2 @@
1
+ import { t as registerBatch } from "../quench-CdVnhmcA.mjs";
2
+ export { registerBatch };
@@ -0,0 +1,35 @@
1
+ //#region src/quench/register-batch.ts
2
+ /**
3
+ * Register a batch once Quench is ready.
4
+ *
5
+ * Safe to call at module scope — it waits for the hook rather than assuming
6
+ * Quench has loaded, which is the mistake that makes a batch silently never
7
+ * appear.
8
+ *
9
+ * ```ts
10
+ * registerBatch('my-module.sheets', ({ describe, it, assert }) => {
11
+ * describe('character sheet', () => {
12
+ * it('renders', async () => {
13
+ * const actor = await Actor.create({ name: 'T', type: 'character' });
14
+ * await actor.sheet.render(true);
15
+ * assert.ok(actor.sheet.rendered);
16
+ * await actor.delete();
17
+ * });
18
+ * });
19
+ * });
20
+ * ```
21
+ */
22
+ function registerBatch(id, fn, options = {}) {
23
+ const hooks = globalThis.Hooks;
24
+ if (!hooks?.once) return;
25
+ hooks.once("quenchReady", (quench) => {
26
+ quench.registerBatch(id, fn, {
27
+ displayName: options.displayName ?? id,
28
+ ...options
29
+ });
30
+ });
31
+ }
32
+ //#endregion
33
+ export { registerBatch as t };
34
+
35
+ //# sourceMappingURL=quench-CdVnhmcA.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quench-CdVnhmcA.mjs","names":[],"sources":["../src/quench/register-batch.ts"],"sourcesContent":["/**\n * Registering a Quench batch, for the tests that need a real Foundry.\n *\n * The vitest half covers everything up to the moment a window renders. Past\n * that — a sheet that actually draws, a socket with two clients, a document\n * that really round-trips through the database — needs the real thing, and\n * Quench is how the community runs those from inside a running world.\n *\n * The whole ceremony is one `quenchReady` hook and a registration call, which\n * everyone writes again and gets subtly wrong: register too early and Quench\n * is not there, too late and the batch is missed.\n */\n\n/** What Quench hands a batch to describe its cases. */\nexport interface QuenchContext {\n describe(title: string, fn: () => void): void;\n it(title: string, fn: () => unknown): void;\n before(fn: () => unknown): void;\n after(fn: () => unknown): void;\n beforeEach(fn: () => unknown): void;\n afterEach(fn: () => unknown): void;\n // biome-ignore lint/suspicious/noExplicitAny: Quench bundles chai, whose assert surface is its own\n assert: any;\n // biome-ignore lint/suspicious/noExplicitAny: chai's expect, likewise\n expect: any;\n}\n\nexport interface BatchOptions {\n /** Shown in the Quench window. Defaults to the batch id. */\n displayName?: string;\n /**\n * Groups the batch in the Quench UI. Use your package id so a user running\n * everything can tell whose failures are whose.\n */\n snapBaseline?: boolean;\n}\n\ninterface QuenchApi {\n registerBatch(\n id: string,\n fn: (context: QuenchContext) => void,\n options?: Record<string, unknown>,\n ): void;\n}\n\n/**\n * Register a batch once Quench is ready.\n *\n * Safe to call at module scope — it waits for the hook rather than assuming\n * Quench has loaded, which is the mistake that makes a batch silently never\n * appear.\n *\n * ```ts\n * registerBatch('my-module.sheets', ({ describe, it, assert }) => {\n * describe('character sheet', () => {\n * it('renders', async () => {\n * const actor = await Actor.create({ name: 'T', type: 'character' });\n * await actor.sheet.render(true);\n * assert.ok(actor.sheet.rendered);\n * await actor.delete();\n * });\n * });\n * });\n * ```\n */\nexport function registerBatch(\n id: string,\n fn: (context: QuenchContext) => void,\n options: BatchOptions = {},\n): void {\n const hooks = (globalThis as Record<string, unknown>).Hooks as\n | { once(event: string, cb: (quench: QuenchApi) => void): void }\n | undefined;\n\n if (!hooks?.once) {\n // No Foundry here at all. Quench batches only mean anything inside a\n // running world, so this is a no-op rather than an error — it lets a file\n // holding both kinds of test be imported by the vitest run.\n return;\n }\n\n hooks.once('quenchReady', (quench) => {\n quench.registerBatch(id, fn, { displayName: options.displayName ?? id, ...options });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAiEA,SAAgB,cACd,IACA,IACA,UAAwB,CAAC,GACnB;CACN,MAAM,QAAS,WAAuC;CAItD,IAAI,CAAC,OAAO,MAIV;CAGF,MAAM,KAAK,gBAAgB,WAAW;EACpC,OAAO,cAAc,IAAI,IAAI;GAAE,aAAa,QAAQ,eAAe;GAAI,GAAG;EAAQ,CAAC;CACrF,CAAC;AACH"}
@@ -0,0 +1,2 @@
1
+ import { a as RecordedSheet, c as createMockActor, i as RecordedSetting, l as createMockItem, n as RecordedEnricher, o as withMockFoundry, r as RecordedHook, s as MockDocument, t as MockFoundry } from "../index-CcUzNUCI.mjs";
2
+ export { type MockDocument, type MockFoundry, type RecordedEnricher, type RecordedHook, type RecordedSetting, type RecordedSheet, createMockActor, createMockItem, withMockFoundry };
@@ -0,0 +1,2 @@
1
+ import { n as createMockActor, r as createMockItem, t as withMockFoundry } from "../vitest-D0qlXJeT.mjs";
2
+ export { createMockActor, createMockItem, withMockFoundry };
@@ -0,0 +1,307 @@
1
+ //#region src/vitest/mock-foundry.ts
2
+ let nextId = 0;
3
+ /** `{'system.hp.value': 3}` → `{system: {hp: {value: 3}}}`. */
4
+ function expand(flat) {
5
+ const out = {};
6
+ for (const [path, value] of Object.entries(flat)) {
7
+ const parts = path.split(".");
8
+ const last = parts.pop();
9
+ if (!last) continue;
10
+ let target = out;
11
+ for (const part of parts) {
12
+ target[part] ??= {};
13
+ target = target[part];
14
+ }
15
+ target[last] = value;
16
+ }
17
+ return out;
18
+ }
19
+ /** Deep-merge, the way a document update behaves. Arrays replace wholesale. */
20
+ function merge(target, source) {
21
+ for (const [key, value] of Object.entries(source)) if (value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype) {
22
+ if (typeof target[key] !== "object" || target[key] === null) target[key] = {};
23
+ merge(target[key], value);
24
+ } else target[key] = value;
25
+ }
26
+ function createMockDocument(kind, options = {}) {
27
+ nextId += 1;
28
+ const updates = [];
29
+ const doc = {
30
+ id: options.id ?? `${kind}${String(nextId).padStart(12, "0")}`,
31
+ name: options.name ?? `Test ${kind}`,
32
+ type: options.type ?? "base",
33
+ system: options.system ?? {},
34
+ flags: options.flags ?? {},
35
+ updates,
36
+ getFlag: (scope, key) => doc.flags[scope]?.[key],
37
+ setFlag: async (scope, key, value) => {
38
+ doc.flags[scope] ??= {};
39
+ doc.flags[scope][key] = value;
40
+ return doc;
41
+ },
42
+ unsetFlag: async (scope, key) => {
43
+ delete doc.flags[scope]?.[key];
44
+ return doc;
45
+ },
46
+ /**
47
+ * Records the delta and merges it in.
48
+ *
49
+ * Merges, not replaces — which is what Foundry does and what a mock has
50
+ * to match. Assigning `{system: {hp: {value: 4}}}` wholesale drops
51
+ * `hp.max`, and a test written against that passes while the real thing
52
+ * loses data.
53
+ *
54
+ * Dotted keys are expanded first, because that is how updates arrive.
55
+ */
56
+ update: async (delta) => {
57
+ updates.push(delta);
58
+ merge(doc, expand(delta));
59
+ return doc;
60
+ }
61
+ };
62
+ return doc;
63
+ }
64
+ /** An Actor for a test. Its `updates` record what the code under test wrote. */
65
+ function createMockActor(options = {}) {
66
+ return createMockDocument("Actor", {
67
+ type: "character",
68
+ ...options
69
+ });
70
+ }
71
+ /** An Item for a test. */
72
+ function createMockItem(options = {}) {
73
+ return createMockDocument("Item", {
74
+ type: "base",
75
+ ...options
76
+ });
77
+ }
78
+ //#endregion
79
+ //#region src/vitest/with-mock-foundry.ts
80
+ const GLOBALS = [
81
+ "foundry",
82
+ "game",
83
+ "CONFIG",
84
+ "Hooks",
85
+ "ui",
86
+ "CONST"
87
+ ];
88
+ /**
89
+ * Flatten and expand, the way `foundry.utils` does.
90
+ *
91
+ * Provided because SDK code calls them on the global, not because a test
92
+ * needs them: leaving them out means every consumer stubs them again.
93
+ */
94
+ function flattenObject(obj, prefix = "") {
95
+ const flat = {};
96
+ for (const [key, value] of Object.entries(obj)) {
97
+ const path = prefix ? `${prefix}.${key}` : key;
98
+ if (value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype && Object.keys(value).length > 0) Object.assign(flat, flattenObject(value, path));
99
+ else flat[path] = value;
100
+ }
101
+ return flat;
102
+ }
103
+ function expandObject(flat) {
104
+ const out = {};
105
+ for (const [path, value] of Object.entries(flat)) {
106
+ const parts = path.split(".");
107
+ const last = parts.pop();
108
+ if (!last) continue;
109
+ let target = out;
110
+ for (const part of parts) {
111
+ target[part] ??= {};
112
+ target = target[part];
113
+ }
114
+ target[last] = value;
115
+ }
116
+ return out;
117
+ }
118
+ /**
119
+ * Stand up the globals, run the callback, tear them down.
120
+ *
121
+ * ```ts
122
+ * const foundry = withMockFoundry();
123
+ * registerMyModule();
124
+ * expect(foundry.hooks.map((h) => h.event)).toContain('init');
125
+ * foundry.restore();
126
+ * ```
127
+ */
128
+ function withMockFoundry(options = {}) {
129
+ const saved = /* @__PURE__ */ new Map();
130
+ const scope = globalThis;
131
+ for (const name of GLOBALS) saved.set(name, scope[name]);
132
+ const hooks = [];
133
+ const settings = [];
134
+ const values = /* @__PURE__ */ new Map();
135
+ const notifications = [];
136
+ const Hooks = {
137
+ on: (event, fn) => {
138
+ hooks.push({
139
+ event,
140
+ once: false,
141
+ fn
142
+ });
143
+ return hooks.length;
144
+ },
145
+ once: (event, fn) => {
146
+ hooks.push({
147
+ event,
148
+ once: true,
149
+ fn
150
+ });
151
+ return hooks.length;
152
+ },
153
+ off: () => {},
154
+ call: (event, ...args) => callHook(event, ...args).every((r) => r !== false),
155
+ callAll: (event, ...args) => {
156
+ callHook(event, ...args);
157
+ return true;
158
+ }
159
+ };
160
+ function callHook(event, ...args) {
161
+ return hooks.filter((h) => h.event === event).map((h) => h.fn(...args));
162
+ }
163
+ const user = {
164
+ id: "user000000000001",
165
+ isGM: true,
166
+ name: "Gamemaster",
167
+ ...options.user
168
+ };
169
+ const sheets = [];
170
+ const enrichers = [];
171
+ scope.Hooks = Hooks;
172
+ scope.CONST = { DOCUMENT_OWNERSHIP_LEVELS: {
173
+ NONE: 0,
174
+ LIMITED: 1,
175
+ OBSERVER: 2,
176
+ OWNER: 3
177
+ } };
178
+ scope.CONFIG = {
179
+ Actor: {
180
+ dataModels: {},
181
+ sheetClasses: {}
182
+ },
183
+ Item: {
184
+ dataModels: {},
185
+ sheetClasses: {}
186
+ },
187
+ TextEditor: { enrichers },
188
+ Combat: {},
189
+ ActiveEffect: {},
190
+ statusEffects: [],
191
+ debug: { hooks: false }
192
+ };
193
+ scope.ui = { notifications: {
194
+ info: (m) => notifications.push({
195
+ level: "info",
196
+ message: m
197
+ }),
198
+ warn: (m) => notifications.push({
199
+ level: "warn",
200
+ message: m
201
+ }),
202
+ error: (m) => notifications.push({
203
+ level: "error",
204
+ message: m
205
+ })
206
+ } };
207
+ scope.foundry = {
208
+ utils: {
209
+ flattenObject,
210
+ expandObject,
211
+ mergeObject: (a, b) => ({
212
+ ...a,
213
+ ...b
214
+ }),
215
+ deepClone: (v) => structuredClone(v),
216
+ randomID: () => Math.random().toString(36).slice(2, 18),
217
+ isNewerVersion: (a, b) => a.localeCompare(b, void 0, { numeric: true }) > 0
218
+ },
219
+ abstract: {
220
+ TypeDataModel: class {},
221
+ DataModel: class {},
222
+ Document: class {}
223
+ },
224
+ data: { fields: {} },
225
+ applications: {
226
+ api: {
227
+ ApplicationV2: class {},
228
+ HandlebarsApplicationMixin: (b) => b
229
+ },
230
+ sheets: {
231
+ ActorSheetV2: class {},
232
+ ItemSheetV2: class {}
233
+ },
234
+ apps: { DocumentSheetConfig: {
235
+ registerSheet(documentClass, scope, sheetClass, sheetOptions = {}) {
236
+ sheets.push({
237
+ key: `${scope}.${sheetClass.name}`,
238
+ scope,
239
+ id: sheetClass.name,
240
+ sheetClass,
241
+ documentClass,
242
+ options: sheetOptions
243
+ });
244
+ },
245
+ unregisterSheet: () => {}
246
+ } },
247
+ ux: {},
248
+ instances: /* @__PURE__ */ new Map()
249
+ },
250
+ documents: { collections: {
251
+ Actors: {
252
+ registerSheet: () => {},
253
+ unregisterSheet: () => {}
254
+ },
255
+ Items: {
256
+ registerSheet: () => {},
257
+ unregisterSheet: () => {}
258
+ }
259
+ } },
260
+ ...options.foundry
261
+ };
262
+ scope.game = {
263
+ user,
264
+ userId: user.id,
265
+ ready: true,
266
+ modules: /* @__PURE__ */ new Map(),
267
+ actors: [],
268
+ items: [],
269
+ i18n: {
270
+ localize: (key) => key,
271
+ format: (key, data) => `${key} ${JSON.stringify(data)}`
272
+ },
273
+ settings: {
274
+ register: (namespace, key, config) => {
275
+ settings.push({
276
+ namespace,
277
+ key,
278
+ config
279
+ });
280
+ values.set(`${namespace}.${key}`, config.default);
281
+ },
282
+ get: (namespace, key) => values.get(`${namespace}.${key}`),
283
+ set: async (namespace, key, value) => {
284
+ values.set(`${namespace}.${key}`, value);
285
+ return value;
286
+ }
287
+ },
288
+ ...options.game
289
+ };
290
+ return {
291
+ hooks,
292
+ settings,
293
+ notifications,
294
+ sheets,
295
+ enrichers,
296
+ callHook,
297
+ getSetting: (namespace, key) => values.get(`${namespace}.${key}`),
298
+ restore() {
299
+ for (const [name, value] of saved) if (value === void 0) delete scope[name];
300
+ else scope[name] = value;
301
+ }
302
+ };
303
+ }
304
+ //#endregion
305
+ export { createMockActor as n, createMockItem as r, withMockFoundry as t };
306
+
307
+ //# sourceMappingURL=vitest-D0qlXJeT.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vitest-D0qlXJeT.mjs","names":[],"sources":["../src/vitest/mock-foundry.ts","../src/vitest/with-mock-foundry.ts"],"sourcesContent":["/**\n * A Foundry runtime, faked well enough to import a package and construct\n * its classes.\n *\n * Every `@vttforge/core` test built this by hand before this existed, and no\n * two built it quite the same way. That is the problem: the globals a package\n * needs are not obvious, they are not documented anywhere, and you discover\n * them one `ReferenceError` at a time.\n *\n * What this covers is the boundary the SDK actually sits on — everything up\n * to the moment a window renders. `_renderHTML` and beyond needs a real\n * browser and a real Foundry; that is what the Quench half is for.\n */\n\n/** A Foundry document, as much of one as a unit test needs. */\nexport interface MockDocument {\n id: string;\n name: string;\n type: string;\n // biome-ignore lint/suspicious/noExplicitAny: a document's system data is whatever its schema says\n system: Record<string, any>;\n flags: Record<string, Record<string, unknown>>;\n getFlag(scope: string, key: string): unknown;\n setFlag(scope: string, key: string, value: unknown): Promise<MockDocument>;\n unsetFlag(scope: string, key: string): Promise<MockDocument>;\n update(delta: Record<string, unknown>): Promise<MockDocument>;\n /** Every update this document received, in order. */\n readonly updates: ReadonlyArray<Record<string, unknown>>;\n}\n\ninterface MockDocumentOptions {\n id?: string;\n name?: string;\n type?: string;\n // biome-ignore lint/suspicious/noExplicitAny: mirrors MockDocument.system\n system?: Record<string, any>;\n flags?: Record<string, Record<string, unknown>>;\n}\n\nlet nextId = 0;\n\n/** `{'system.hp.value': 3}` → `{system: {hp: {value: 3}}}`. */\nfunction expand(flat: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [path, value] of Object.entries(flat)) {\n const parts = path.split('.');\n const last = parts.pop();\n if (!last) continue;\n let target = out;\n for (const part of parts) {\n target[part] ??= {};\n target = target[part] as Record<string, unknown>;\n }\n target[last] = value;\n }\n return out;\n}\n\n/** Deep-merge, the way a document update behaves. Arrays replace wholesale. */\nfunction merge(target: Record<string, unknown>, source: Record<string, unknown>): void {\n for (const [key, value] of Object.entries(source)) {\n const isPlain =\n value !== null &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n Object.getPrototypeOf(value) === Object.prototype;\n if (isPlain) {\n if (typeof target[key] !== 'object' || target[key] === null) target[key] = {};\n merge(target[key] as Record<string, unknown>, value as Record<string, unknown>);\n } else {\n target[key] = value;\n }\n }\n}\n\nfunction createMockDocument(kind: string, options: MockDocumentOptions = {}): MockDocument {\n nextId += 1;\n const updates: Record<string, unknown>[] = [];\n\n const doc: MockDocument = {\n id: options.id ?? `${kind}${String(nextId).padStart(12, '0')}`,\n name: options.name ?? `Test ${kind}`,\n type: options.type ?? 'base',\n system: options.system ?? {},\n flags: options.flags ?? {},\n updates,\n\n getFlag: (scope, key) => doc.flags[scope]?.[key],\n\n setFlag: async (scope, key, value) => {\n doc.flags[scope] ??= {};\n doc.flags[scope][key] = value;\n return doc;\n },\n\n unsetFlag: async (scope, key) => {\n delete doc.flags[scope]?.[key];\n return doc;\n },\n\n /**\n * Records the delta and merges it in.\n *\n * Merges, not replaces — which is what Foundry does and what a mock has\n * to match. Assigning `{system: {hp: {value: 4}}}` wholesale drops\n * `hp.max`, and a test written against that passes while the real thing\n * loses data.\n *\n * Dotted keys are expanded first, because that is how updates arrive.\n */\n update: async (delta) => {\n updates.push(delta);\n merge(doc as unknown as Record<string, unknown>, expand(delta));\n return doc;\n },\n };\n\n return doc;\n}\n\n/** An Actor for a test. Its `updates` record what the code under test wrote. */\nexport function createMockActor(options: MockDocumentOptions = {}): MockDocument {\n return createMockDocument('Actor', { type: 'character', ...options });\n}\n\n/** An Item for a test. */\nexport function createMockItem(options: MockDocumentOptions = {}): MockDocument {\n return createMockDocument('Item', { type: 'base', ...options });\n}\n","/**\n * Install the Foundry globals a package needs, and take them away again.\n *\n * The globals are the awkward part of testing anything built on Foundry:\n * `foundry`, `game`, `CONFIG`, `Hooks`, `ui`, `CONST`. A package touches some\n * subset, the subset is not written down, and you find it one\n * `ReferenceError` at a time.\n *\n * Everything here is a plain object or a recording stub, so a test can assert\n * on what was registered rather than only on what did not throw.\n */\n\n/** A hook registration the code under test made. */\nexport interface RecordedHook {\n event: string;\n once: boolean;\n fn: (...args: unknown[]) => unknown;\n}\n\n/** A setting the code under test registered. */\nexport interface RecordedSetting {\n namespace: string;\n key: string;\n config: Record<string, unknown>;\n}\n\n/** One `DocumentSheetConfig.registerSheet`, as VTTForge makes it. */\nexport interface RecordedSheet {\n /** The key Foundry persists: `<package id>.<sheet id>`. */\n readonly key: string;\n /** The package that registered it. */\n readonly scope: string;\n /** The sheet id — the class name VTTForge pinned. */\n readonly id: string;\n readonly sheetClass: unknown;\n readonly documentClass: unknown;\n readonly options: Record<string, unknown>;\n}\n\n/** One entry pushed onto `CONFIG.TextEditor.enrichers`. */\nexport interface RecordedEnricher {\n /** The namespaced id: `<package id>.<enricher id>`. */\n readonly id: string;\n readonly pattern: RegExp;\n readonly onRender?: unknown;\n}\n\nexport interface MockFoundry {\n /** Every `Hooks.on` and `Hooks.once`, in order. */\n readonly hooks: ReadonlyArray<RecordedHook>;\n /** Every `game.settings.register`, in order. */\n readonly settings: ReadonlyArray<RecordedSetting>;\n /** Every notification raised, by severity. */\n readonly notifications: ReadonlyArray<{ level: 'info' | 'warn' | 'error'; message: string }>;\n /**\n * Every sheet registered, in order.\n *\n * `key` is the thing worth asserting: Foundry saves it on each document\n * using the sheet, so a test that pins the key is a test that the reader's\n * choice survives your next build.\n */\n readonly sheets: ReadonlyArray<RecordedSheet>;\n /**\n * Every text enricher registered, in order.\n *\n * `id` is namespaced, which is what stops a common name from colliding with\n * another package — and what makes `onRender` fire at all.\n */\n readonly enrichers: ReadonlyArray<RecordedEnricher>;\n /** Fire a hook the way Foundry would, for the listeners registered so far. */\n callHook(event: string, ...args: unknown[]): unknown[];\n /** Read back a registered setting's current value. */\n getSetting(namespace: string, key: string): unknown;\n /** Put every global back the way it was. */\n restore(): void;\n}\n\ninterface MockFoundryOptions {\n /** The current user. Defaults to a GM, since most module code checks. */\n user?: { id?: string; isGM?: boolean; name?: string };\n /** Extra `foundry.*` members, merged over the defaults. */\n foundry?: Record<string, unknown>;\n /** Extra `game.*` members, merged over the defaults. */\n game?: Record<string, unknown>;\n}\n\nconst GLOBALS = ['foundry', 'game', 'CONFIG', 'Hooks', 'ui', 'CONST'] as const;\n\n/**\n * Flatten and expand, the way `foundry.utils` does.\n *\n * Provided because SDK code calls them on the global, not because a test\n * needs them: leaving them out means every consumer stubs them again.\n */\nfunction flattenObject(obj: Record<string, unknown>, prefix = ''): Record<string, unknown> {\n const flat: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n const path = prefix ? `${prefix}.${key}` : key;\n const isPlain =\n value !== null &&\n typeof value === 'object' &&\n Object.getPrototypeOf(value) === Object.prototype;\n if (isPlain && Object.keys(value as object).length > 0) {\n Object.assign(flat, flattenObject(value as Record<string, unknown>, path));\n } else {\n flat[path] = value;\n }\n }\n return flat;\n}\n\nfunction expandObject(flat: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [path, value] of Object.entries(flat)) {\n const parts = path.split('.');\n const last = parts.pop();\n if (!last) continue;\n let target = out;\n for (const part of parts) {\n target[part] ??= {};\n target = target[part] as Record<string, unknown>;\n }\n target[last] = value;\n }\n return out;\n}\n\n/**\n * Stand up the globals, run the callback, tear them down.\n *\n * ```ts\n * const foundry = withMockFoundry();\n * registerMyModule();\n * expect(foundry.hooks.map((h) => h.event)).toContain('init');\n * foundry.restore();\n * ```\n */\nexport function withMockFoundry(options: MockFoundryOptions = {}): MockFoundry {\n const saved = new Map<string, unknown>();\n const scope = globalThis as Record<string, unknown>;\n for (const name of GLOBALS) saved.set(name, scope[name]);\n\n const hooks: RecordedHook[] = [];\n const settings: RecordedSetting[] = [];\n const values = new Map<string, unknown>();\n const notifications: { level: 'info' | 'warn' | 'error'; message: string }[] = [];\n\n const Hooks = {\n on: (event: string, fn: (...a: unknown[]) => unknown) => {\n hooks.push({ event, once: false, fn });\n return hooks.length;\n },\n once: (event: string, fn: (...a: unknown[]) => unknown) => {\n hooks.push({ event, once: true, fn });\n return hooks.length;\n },\n off: () => {},\n call: (event: string, ...args: unknown[]) => callHook(event, ...args).every((r) => r !== false),\n callAll: (event: string, ...args: unknown[]) => {\n callHook(event, ...args);\n return true;\n },\n };\n\n function callHook(event: string, ...args: unknown[]): unknown[] {\n return hooks.filter((h) => h.event === event).map((h) => h.fn(...args));\n }\n\n const user = { id: 'user000000000001', isGM: true, name: 'Gamemaster', ...options.user };\n\n const sheets: RecordedSheet[] = [];\n const enrichers: RecordedEnricher[] = [];\n\n scope.Hooks = Hooks;\n scope.CONST = { DOCUMENT_OWNERSHIP_LEVELS: { NONE: 0, LIMITED: 1, OBSERVER: 2, OWNER: 3 } };\n scope.CONFIG = {\n Actor: { dataModels: {}, sheetClasses: {} },\n Item: { dataModels: {}, sheetClasses: {} },\n TextEditor: { enrichers },\n Combat: {},\n ActiveEffect: {},\n statusEffects: [],\n debug: { hooks: false },\n };\n scope.ui = {\n notifications: {\n info: (m: string) => notifications.push({ level: 'info', message: m }),\n warn: (m: string) => notifications.push({ level: 'warn', message: m }),\n error: (m: string) => notifications.push({ level: 'error', message: m }),\n },\n };\n scope.foundry = {\n utils: {\n flattenObject,\n expandObject,\n mergeObject: (a: Record<string, unknown>, b: Record<string, unknown>) => ({ ...a, ...b }),\n deepClone: <T>(v: T): T => structuredClone(v),\n randomID: () => Math.random().toString(36).slice(2, 18),\n isNewerVersion: (a: string, b: string) =>\n a.localeCompare(b, undefined, { numeric: true }) > 0,\n },\n abstract: { TypeDataModel: class {}, DataModel: class {}, Document: class {} },\n data: { fields: {} },\n applications: {\n api: { ApplicationV2: class {}, HandlebarsApplicationMixin: (b: unknown) => b },\n sheets: { ActorSheetV2: class {}, ItemSheetV2: class {} },\n apps: {\n // Where `registerSystem({ sheets })` and `registerModule({ sheets })`\n // register. Foundry builds the key from the class name, so this\n // records the same key it would persist.\n DocumentSheetConfig: {\n registerSheet(\n documentClass: unknown,\n scope: string,\n sheetClass: { name: string },\n sheetOptions: Record<string, unknown> = {},\n ) {\n sheets.push({\n key: `${scope}.${sheetClass.name}`,\n scope,\n id: sheetClass.name,\n sheetClass,\n documentClass,\n options: sheetOptions,\n });\n },\n unregisterSheet: () => {},\n },\n },\n ux: {},\n instances: new Map(),\n },\n documents: {\n collections: {\n Actors: { registerSheet: () => {}, unregisterSheet: () => {} },\n Items: { registerSheet: () => {}, unregisterSheet: () => {} },\n },\n },\n ...options.foundry,\n };\n scope.game = {\n user,\n userId: user.id,\n ready: true,\n modules: new Map(),\n actors: [],\n items: [],\n i18n: {\n localize: (key: string) => key,\n format: (key: string, data: Record<string, unknown>) => `${key} ${JSON.stringify(data)}`,\n },\n settings: {\n register: (namespace: string, key: string, config: Record<string, unknown>) => {\n settings.push({ namespace, key, config });\n values.set(`${namespace}.${key}`, config.default);\n },\n get: (namespace: string, key: string) => values.get(`${namespace}.${key}`),\n set: async (namespace: string, key: string, value: unknown) => {\n values.set(`${namespace}.${key}`, value);\n return value;\n },\n },\n ...options.game,\n };\n\n return {\n hooks,\n settings,\n notifications,\n sheets,\n enrichers,\n callHook,\n getSetting: (namespace, key) => values.get(`${namespace}.${key}`),\n restore() {\n for (const [name, value] of saved) {\n if (value === undefined) delete scope[name];\n else scope[name] = value;\n }\n },\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vttforge/testing",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Quench-compatible test helpers and Foundry mocks for VTTForge systems and modules.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,6 +22,14 @@
22
22
  "types": "./dist/index.d.mts",
23
23
  "import": "./dist/index.mjs"
24
24
  },
25
+ "./vitest": {
26
+ "types": "./dist/vitest/index.d.mts",
27
+ "import": "./dist/vitest/index.mjs"
28
+ },
29
+ "./quench": {
30
+ "types": "./dist/quench/index.d.mts",
31
+ "import": "./dist/quench/index.mjs"
32
+ },
25
33
  "./package.json": "./package.json"
26
34
  },
27
35
  "main": "./dist/index.mjs",
@@ -42,7 +50,7 @@
42
50
  "vitest": "^4.1.11"
43
51
  },
44
52
  "engines": {
45
- "node": ">=26.0.0"
53
+ "node": ">=22.0.0"
46
54
  },
47
55
  "publishConfig": {
48
56
  "access": "public",
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @vttforge/testing — placeholder. Implementation lands in v1.0.0.\n */\nexport const VTTFORGE_TESTING_VERSION = '0.0.1';\n"],"mappings":";;;;AAGA,MAAa,2BAA2B"}