@vttforge/testing 0.3.1 → 0.3.3
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 +12 -0
- package/dist/{index-CcUzNUCI.d.mts → index-DB9kK9Rn.d.mts} +22 -11
- package/dist/{index-DLgaf8hZ.d.mts → index-G_T1z-KX.d.mts} +4 -4
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -2
- package/dist/quench/index.d.mts +1 -1
- package/dist/quench/index.mjs +1 -1
- package/dist/{quench-CdVnhmcA.mjs → quench-C6E-Sh9r.mjs} +2 -2
- package/dist/quench-C6E-Sh9r.mjs.map +1 -0
- package/dist/vitest/index.d.mts +2 -2
- package/dist/vitest/index.mjs +1 -1
- package/dist/{vitest-D0qlXJeT.mjs → vitest-C9NdKAgo.mjs} +2 -2
- package/dist/vitest-C9NdKAgo.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/quench-CdVnhmcA.mjs.map +0 -1
- package/dist/vitest-D0qlXJeT.mjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @vttforge/testing
|
|
2
2
|
|
|
3
|
+
## 0.3.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8a7ade5: Declare the Foundry globals with `var` so a consumer's own declarations can coexist. A package that adds `@vttforge/testing` to a typechecked project already declares `foundry`, `game`, `CONFIG`, `Hooks` and `ui` for its own source, and two `declare global` blocks only merge when both use `var`. With `const` on either side `tsc` stopped with `TS2451: Cannot redeclare block-scoped variable`, which is what a real consumer hit the first time it tried to typecheck its tests. The scaffolding templates are corrected the same way.
|
|
8
|
+
|
|
9
|
+
## 0.3.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 9b8ca8c: Export the option and config interfaces that public functions already took: `MockDocumentOptions` and `MockFoundryOptions` from `@vttforge/testing`, `ActorConfig` and `ItemConfig` from `@vttforge/core`. They were reachable through the functions but could not be named.
|
|
14
|
+
|
|
3
15
|
## 0.3.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* needs are not obvious, they are not documented anywhere, and you discover
|
|
9
9
|
* them one `ReferenceError` at a time.
|
|
10
10
|
*
|
|
11
|
-
* What this covers is the boundary the SDK actually sits on
|
|
11
|
+
* What this covers is the boundary the SDK actually sits on: everything up
|
|
12
12
|
* to the moment a window renders. `_renderHTML` and beyond needs a real
|
|
13
13
|
* browser and a real Foundry; that is what the Quench half is for.
|
|
14
14
|
*/
|
|
@@ -68,7 +68,7 @@ interface RecordedSheet {
|
|
|
68
68
|
readonly key: string;
|
|
69
69
|
/** The package that registered it. */
|
|
70
70
|
readonly scope: string;
|
|
71
|
-
/** The sheet id
|
|
71
|
+
/** The sheet id, the class name VTTForge pinned. */
|
|
72
72
|
readonly id: string;
|
|
73
73
|
readonly sheetClass: unknown;
|
|
74
74
|
readonly documentClass: unknown;
|
|
@@ -103,7 +103,7 @@ interface MockFoundry {
|
|
|
103
103
|
* Every text enricher registered, in order.
|
|
104
104
|
*
|
|
105
105
|
* `id` is namespaced, which is what stops a common name from colliding with
|
|
106
|
-
* another package
|
|
106
|
+
* another package, and what makes `onRender` fire at all.
|
|
107
107
|
*/
|
|
108
108
|
readonly enrichers: ReadonlyArray<RecordedEnricher>;
|
|
109
109
|
/** Fire a hook the way Foundry would, for the listeners registered so far. */
|
|
@@ -138,14 +138,25 @@ interface MockFoundryOptions {
|
|
|
138
138
|
declare function withMockFoundry(options?: MockFoundryOptions): MockFoundry;
|
|
139
139
|
//#endregion
|
|
140
140
|
//#region src/vitest/index.d.ts
|
|
141
|
+
/**
|
|
142
|
+
* `var`, not `const`, and that is the whole point.
|
|
143
|
+
*
|
|
144
|
+
* A package that consumes this already declares these globals for its own
|
|
145
|
+
* source, because the SDK does not ship a Foundry runtime type. Two
|
|
146
|
+
* `declare global` blocks naming the same thing have to merge, and only
|
|
147
|
+
* `var` merges: `const` is block-scoped, so a second one is a redeclaration
|
|
148
|
+
* and `tsc` stops with TS2451. Consumers hit that the moment they add the
|
|
149
|
+
* test helpers to a typechecked project, which is the moment they most want
|
|
150
|
+
* them.
|
|
151
|
+
*/
|
|
141
152
|
declare global {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
153
|
+
var foundry: any;
|
|
154
|
+
var game: any;
|
|
155
|
+
var CONFIG: any;
|
|
156
|
+
var Hooks: any;
|
|
157
|
+
var ui: any;
|
|
158
|
+
var CONST: any;
|
|
148
159
|
}
|
|
149
160
|
//#endregion
|
|
150
|
-
export {
|
|
151
|
-
//# sourceMappingURL=index-
|
|
161
|
+
export { RecordedSetting as a, MockDocument as c, createMockItem as d, RecordedHook as i, MockDocumentOptions as l, MockFoundryOptions as n, RecordedSheet as o, RecordedEnricher as r, withMockFoundry as s, MockFoundry as t, createMockActor as u };
|
|
162
|
+
//# sourceMappingURL=index-DB9kK9Rn.d.mts.map
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Registering a Quench batch, for the tests that need a real Foundry.
|
|
4
4
|
*
|
|
5
5
|
* The vitest half covers everything up to the moment a window renders. Past
|
|
6
|
-
* that
|
|
7
|
-
* that really round-trips through the database
|
|
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
8
|
* Quench is how the community runs those from inside a running world.
|
|
9
9
|
*
|
|
10
10
|
* The whole ceremony is one `quenchReady` hook and a registration call, which
|
|
@@ -34,7 +34,7 @@ interface BatchOptions {
|
|
|
34
34
|
/**
|
|
35
35
|
* Register a batch once Quench is ready.
|
|
36
36
|
*
|
|
37
|
-
* Safe to call at module scope
|
|
37
|
+
* Safe to call at module scope: it waits for the hook rather than assuming
|
|
38
38
|
* Quench has loaded, which is the mistake that makes a batch silently never
|
|
39
39
|
* appear.
|
|
40
40
|
*
|
|
@@ -54,4 +54,4 @@ interface BatchOptions {
|
|
|
54
54
|
declare function registerBatch(id: string, fn: (context: QuenchContext) => void, options?: BatchOptions): void;
|
|
55
55
|
//#endregion
|
|
56
56
|
export { QuenchContext as n, registerBatch as r, BatchOptions as t };
|
|
57
|
-
//# sourceMappingURL=index-
|
|
57
|
+
//# sourceMappingURL=index-G_T1z-KX.d.mts.map
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { n as QuenchContext, r as registerBatch, t as BatchOptions } from "./index-
|
|
2
|
-
import { a as
|
|
3
|
-
export { type BatchOptions, type MockDocument, type MockFoundry, type QuenchContext, type RecordedEnricher, type RecordedHook, type RecordedSetting, type RecordedSheet, createMockActor, createMockItem, registerBatch, withMockFoundry };
|
|
1
|
+
import { n as QuenchContext, r as registerBatch, t as BatchOptions } from "./index-G_T1z-KX.mjs";
|
|
2
|
+
import { a as RecordedSetting, c as MockDocument, d as createMockItem, i as RecordedHook, l as MockDocumentOptions, n as MockFoundryOptions, o as RecordedSheet, r as RecordedEnricher, s as withMockFoundry, t as MockFoundry, u as createMockActor } from "./index-DB9kK9Rn.mjs";
|
|
3
|
+
export { type BatchOptions, type MockDocument, type MockDocumentOptions, type MockFoundry, type MockFoundryOptions, type QuenchContext, type RecordedEnricher, type RecordedHook, type RecordedSetting, type RecordedSheet, createMockActor, createMockItem, registerBatch, withMockFoundry };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as registerBatch } from "./quench-
|
|
2
|
-
import { n as createMockActor, r as createMockItem, t as withMockFoundry } from "./vitest-
|
|
1
|
+
import { t as registerBatch } from "./quench-C6E-Sh9r.mjs";
|
|
2
|
+
import { n as createMockActor, r as createMockItem, t as withMockFoundry } from "./vitest-C9NdKAgo.mjs";
|
|
3
3
|
export { createMockActor, createMockItem, registerBatch, withMockFoundry };
|
package/dist/quench/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as QuenchContext, r as registerBatch, t as BatchOptions } from "../index-
|
|
1
|
+
import { n as QuenchContext, r as registerBatch, t as BatchOptions } from "../index-G_T1z-KX.mjs";
|
|
2
2
|
export { type BatchOptions, type QuenchContext, registerBatch };
|
package/dist/quench/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as registerBatch } from "../quench-
|
|
1
|
+
import { t as registerBatch } from "../quench-C6E-Sh9r.mjs";
|
|
2
2
|
export { registerBatch };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Register a batch once Quench is ready.
|
|
4
4
|
*
|
|
5
|
-
* Safe to call at module scope
|
|
5
|
+
* Safe to call at module scope: it waits for the hook rather than assuming
|
|
6
6
|
* Quench has loaded, which is the mistake that makes a batch silently never
|
|
7
7
|
* appear.
|
|
8
8
|
*
|
|
@@ -32,4 +32,4 @@ function registerBatch(id, fn, options = {}) {
|
|
|
32
32
|
//#endregion
|
|
33
33
|
export { registerBatch as t };
|
|
34
34
|
|
|
35
|
-
//# sourceMappingURL=quench-
|
|
35
|
+
//# sourceMappingURL=quench-C6E-Sh9r.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quench-C6E-Sh9r.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"}
|
package/dist/vitest/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { type MockDocument, type MockFoundry, type RecordedEnricher, type RecordedHook, type RecordedSetting, type RecordedSheet, createMockActor, createMockItem, withMockFoundry };
|
|
1
|
+
import { a as RecordedSetting, c as MockDocument, d as createMockItem, i as RecordedHook, l as MockDocumentOptions, n as MockFoundryOptions, o as RecordedSheet, r as RecordedEnricher, s as withMockFoundry, t as MockFoundry, u as createMockActor } from "../index-DB9kK9Rn.mjs";
|
|
2
|
+
export { type MockDocument, type MockDocumentOptions, type MockFoundry, type MockFoundryOptions, type RecordedEnricher, type RecordedHook, type RecordedSetting, type RecordedSheet, createMockActor, createMockItem, withMockFoundry };
|
package/dist/vitest/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as createMockActor, r as createMockItem, t as withMockFoundry } from "../vitest-
|
|
1
|
+
import { n as createMockActor, r as createMockItem, t as withMockFoundry } from "../vitest-C9NdKAgo.mjs";
|
|
2
2
|
export { createMockActor, createMockItem, withMockFoundry };
|
|
@@ -46,7 +46,7 @@ function createMockDocument(kind, options = {}) {
|
|
|
46
46
|
/**
|
|
47
47
|
* Records the delta and merges it in.
|
|
48
48
|
*
|
|
49
|
-
* Merges, not replaces
|
|
49
|
+
* Merges, not replaces, which is what Foundry does and what a mock has
|
|
50
50
|
* to match. Assigning `{system: {hp: {value: 4}}}` wholesale drops
|
|
51
51
|
* `hp.max`, and a test written against that passes while the real thing
|
|
52
52
|
* loses data.
|
|
@@ -304,4 +304,4 @@ function withMockFoundry(options = {}) {
|
|
|
304
304
|
//#endregion
|
|
305
305
|
export { createMockActor as n, createMockItem as r, withMockFoundry as t };
|
|
306
306
|
|
|
307
|
-
//# sourceMappingURL=vitest-
|
|
307
|
+
//# sourceMappingURL=vitest-C9NdKAgo.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vitest-C9NdKAgo.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\nexport interface 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\nexport interface 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 +0,0 @@
|
|
|
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"}
|
|
@@ -1 +0,0 @@
|
|
|
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":";AAuCA,IAAI,SAAS;;AAGb,SAAS,OAAO,MAAwD;CACtE,MAAM,MAA+B,CAAC;CACtC,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,IAAI,GAAG;EAChD,MAAM,QAAQ,KAAK,MAAM,GAAG;EAC5B,MAAM,OAAO,MAAM,IAAI;EACvB,IAAI,CAAC,MAAM;EACX,IAAI,SAAS;EACb,KAAK,MAAM,QAAQ,OAAO;GACxB,OAAO,UAAU,CAAC;GAClB,SAAS,OAAO;EAClB;EACA,OAAO,QAAQ;CACjB;CACA,OAAO;AACT;;AAGA,SAAS,MAAM,QAAiC,QAAuC;CACrF,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAM9C,IAJE,UAAU,QACV,OAAO,UAAU,YACjB,CAAC,MAAM,QAAQ,KAAK,KACpB,OAAO,eAAe,KAAK,MAAM,OAAO,WAC7B;EACX,IAAI,OAAO,OAAO,SAAS,YAAY,OAAO,SAAS,MAAM,OAAO,OAAO,CAAC;EAC5E,MAAM,OAAO,MAAiC,KAAgC;CAChF,OACE,OAAO,OAAO;AAGpB;AAEA,SAAS,mBAAmB,MAAc,UAA+B,CAAC,GAAiB;CACzF,UAAU;CACV,MAAM,UAAqC,CAAC;CAE5C,MAAM,MAAoB;EACxB,IAAI,QAAQ,MAAM,GAAG,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,IAAI,GAAG;EAC3D,MAAM,QAAQ,QAAQ,QAAQ;EAC9B,MAAM,QAAQ,QAAQ;EACtB,QAAQ,QAAQ,UAAU,CAAC;EAC3B,OAAO,QAAQ,SAAS,CAAC;EACzB;EAEA,UAAU,OAAO,QAAQ,IAAI,MAAM,MAAM,GAAG;EAE5C,SAAS,OAAO,OAAO,KAAK,UAAU;GACpC,IAAI,MAAM,WAAW,CAAC;GACtB,IAAI,MAAM,MAAM,CAAC,OAAO;GACxB,OAAO;EACT;EAEA,WAAW,OAAO,OAAO,QAAQ;GAC/B,OAAO,IAAI,MAAM,MAAM,GAAG;GAC1B,OAAO;EACT;;;;;;;;;;;EAYA,QAAQ,OAAO,UAAU;GACvB,QAAQ,KAAK,KAAK;GAClB,MAAM,KAA2C,OAAO,KAAK,CAAC;GAC9D,OAAO;EACT;CACF;CAEA,OAAO;AACT;;AAGA,SAAgB,gBAAgB,UAA+B,CAAC,GAAiB;CAC/E,OAAO,mBAAmB,SAAS;EAAE,MAAM;EAAa,GAAG;CAAQ,CAAC;AACtE;;AAGA,SAAgB,eAAe,UAA+B,CAAC,GAAiB;CAC9E,OAAO,mBAAmB,QAAQ;EAAE,MAAM;EAAQ,GAAG;CAAQ,CAAC;AAChE;;;AC1CA,MAAM,UAAU;CAAC;CAAW;CAAQ;CAAU;CAAS;CAAM;AAAO;;;;;;;AAQpE,SAAS,cAAc,KAA8B,SAAS,IAA6B;CACzF,MAAM,OAAgC,CAAC;CACvC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,GAAG,GAAG;EAC9C,MAAM,OAAO,SAAS,GAAG,OAAO,GAAG,QAAQ;EAK3C,IAHE,UAAU,QACV,OAAO,UAAU,YACjB,OAAO,eAAe,KAAK,MAAM,OAAO,aAC3B,OAAO,KAAK,KAAe,CAAC,CAAC,SAAS,GACnD,OAAO,OAAO,MAAM,cAAc,OAAkC,IAAI,CAAC;OAEzE,KAAK,QAAQ;CAEjB;CACA,OAAO;AACT;AAEA,SAAS,aAAa,MAAwD;CAC5E,MAAM,MAA+B,CAAC;CACtC,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,IAAI,GAAG;EAChD,MAAM,QAAQ,KAAK,MAAM,GAAG;EAC5B,MAAM,OAAO,MAAM,IAAI;EACvB,IAAI,CAAC,MAAM;EACX,IAAI,SAAS;EACb,KAAK,MAAM,QAAQ,OAAO;GACxB,OAAO,UAAU,CAAC;GAClB,SAAS,OAAO;EAClB;EACA,OAAO,QAAQ;CACjB;CACA,OAAO;AACT;;;;;;;;;;;AAYA,SAAgB,gBAAgB,UAA8B,CAAC,GAAgB;CAC7E,MAAM,wBAAQ,IAAI,IAAqB;CACvC,MAAM,QAAQ;CACd,KAAK,MAAM,QAAQ,SAAS,MAAM,IAAI,MAAM,MAAM,KAAK;CAEvD,MAAM,QAAwB,CAAC;CAC/B,MAAM,WAA8B,CAAC;CACrC,MAAM,yBAAS,IAAI,IAAqB;CACxC,MAAM,gBAAyE,CAAC;CAEhF,MAAM,QAAQ;EACZ,KAAK,OAAe,OAAqC;GACvD,MAAM,KAAK;IAAE;IAAO,MAAM;IAAO;GAAG,CAAC;GACrC,OAAO,MAAM;EACf;EACA,OAAO,OAAe,OAAqC;GACzD,MAAM,KAAK;IAAE;IAAO,MAAM;IAAM;GAAG,CAAC;GACpC,OAAO,MAAM;EACf;EACA,WAAW,CAAC;EACZ,OAAO,OAAe,GAAG,SAAoB,SAAS,OAAO,GAAG,IAAI,CAAC,CAAC,OAAO,MAAM,MAAM,KAAK;EAC9F,UAAU,OAAe,GAAG,SAAoB;GAC9C,SAAS,OAAO,GAAG,IAAI;GACvB,OAAO;EACT;CACF;CAEA,SAAS,SAAS,OAAe,GAAG,MAA4B;EAC9D,OAAO,MAAM,QAAQ,MAAM,EAAE,UAAU,KAAK,CAAC,CAAC,KAAK,MAAM,EAAE,GAAG,GAAG,IAAI,CAAC;CACxE;CAEA,MAAM,OAAO;EAAE,IAAI;EAAoB,MAAM;EAAM,MAAM;EAAc,GAAG,QAAQ;CAAK;CAEvF,MAAM,SAA0B,CAAC;CACjC,MAAM,YAAgC,CAAC;CAEvC,MAAM,QAAQ;CACd,MAAM,QAAQ,EAAE,2BAA2B;EAAE,MAAM;EAAG,SAAS;EAAG,UAAU;EAAG,OAAO;CAAE,EAAE;CAC1F,MAAM,SAAS;EACb,OAAO;GAAE,YAAY,CAAC;GAAG,cAAc,CAAC;EAAE;EAC1C,MAAM;GAAE,YAAY,CAAC;GAAG,cAAc,CAAC;EAAE;EACzC,YAAY,EAAE,UAAU;EACxB,QAAQ,CAAC;EACT,cAAc,CAAC;EACf,eAAe,CAAC;EAChB,OAAO,EAAE,OAAO,MAAM;CACxB;CACA,MAAM,KAAK,EACT,eAAe;EACb,OAAO,MAAc,cAAc,KAAK;GAAE,OAAO;GAAQ,SAAS;EAAE,CAAC;EACrE,OAAO,MAAc,cAAc,KAAK;GAAE,OAAO;GAAQ,SAAS;EAAE,CAAC;EACrE,QAAQ,MAAc,cAAc,KAAK;GAAE,OAAO;GAAS,SAAS;EAAE,CAAC;CACzE,EACF;CACA,MAAM,UAAU;EACd,OAAO;GACL;GACA;GACA,cAAc,GAA4B,OAAgC;IAAE,GAAG;IAAG,GAAG;GAAE;GACvF,YAAe,MAAY,gBAAgB,CAAC;GAC5C,gBAAgB,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,EAAE;GACtD,iBAAiB,GAAW,MAC1B,EAAE,cAAc,GAAG,KAAA,GAAW,EAAE,SAAS,KAAK,CAAC,IAAI;EACvD;EACA,UAAU;GAAE,eAAe,MAAM,CAAC;GAAG,WAAW,MAAM,CAAC;GAAG,UAAU,MAAM,CAAC;EAAE;EAC7E,MAAM,EAAE,QAAQ,CAAC,EAAE;EACnB,cAAc;GACZ,KAAK;IAAE,eAAe,MAAM,CAAC;IAAG,6BAA6B,MAAe;GAAE;GAC9E,QAAQ;IAAE,cAAc,MAAM,CAAC;IAAG,aAAa,MAAM,CAAC;GAAE;GACxD,MAAM,EAIJ,qBAAqB;IACnB,cACE,eACA,OACA,YACA,eAAwC,CAAC,GACzC;KACA,OAAO,KAAK;MACV,KAAK,GAAG,MAAM,GAAG,WAAW;MAC5B;MACA,IAAI,WAAW;MACf;MACA;MACA,SAAS;KACX,CAAC;IACH;IACA,uBAAuB,CAAC;GAC1B,EACF;GACA,IAAI,CAAC;GACL,2BAAW,IAAI,IAAI;EACrB;EACA,WAAW,EACT,aAAa;GACX,QAAQ;IAAE,qBAAqB,CAAC;IAAG,uBAAuB,CAAC;GAAE;GAC7D,OAAO;IAAE,qBAAqB,CAAC;IAAG,uBAAuB,CAAC;GAAE;EAC9D,EACF;EACA,GAAG,QAAQ;CACb;CACA,MAAM,OAAO;EACX;EACA,QAAQ,KAAK;EACb,OAAO;EACP,yBAAS,IAAI,IAAI;EACjB,QAAQ,CAAC;EACT,OAAO,CAAC;EACR,MAAM;GACJ,WAAW,QAAgB;GAC3B,SAAS,KAAa,SAAkC,GAAG,IAAI,GAAG,KAAK,UAAU,IAAI;EACvF;EACA,UAAU;GACR,WAAW,WAAmB,KAAa,WAAoC;IAC7E,SAAS,KAAK;KAAE;KAAW;KAAK;IAAO,CAAC;IACxC,OAAO,IAAI,GAAG,UAAU,GAAG,OAAO,OAAO,OAAO;GAClD;GACA,MAAM,WAAmB,QAAgB,OAAO,IAAI,GAAG,UAAU,GAAG,KAAK;GACzE,KAAK,OAAO,WAAmB,KAAa,UAAmB;IAC7D,OAAO,IAAI,GAAG,UAAU,GAAG,OAAO,KAAK;IACvC,OAAO;GACT;EACF;EACA,GAAG,QAAQ;CACb;CAEA,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA,aAAa,WAAW,QAAQ,OAAO,IAAI,GAAG,UAAU,GAAG,KAAK;EAChE,UAAU;GACR,KAAK,MAAM,CAAC,MAAM,UAAU,OAC1B,IAAI,UAAU,KAAA,GAAW,OAAO,MAAM;QACjC,MAAM,QAAQ;EAEvB;CACF;AACF"}
|