@mudah-cli/testing 0.8.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mudah Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # @mudah-cli/testing
2
+
3
+ In-process test helpers for Mudah apps: `TestApp` for commands, `TestTui` for full-screen
4
+ widgets, plus FS/network/plugin mocks and snapshot assertions. No PTY.
5
+
6
+ Part of [Mudah](https://github.com/thesimonharms/mudah) — an ergonomic, animation-rich CLI
7
+ framework.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install -D @mudah-cli/testing
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { TestApp, TestTui } from '@mudah-cli/testing';
19
+
20
+ // Commands — real kernel, real providers, captured streams
21
+ const app = await TestApp.create({ cwd: appRoot });
22
+ const result = await app.dispatch(['welcome', 'Mudah']);
23
+ result.exit(0).outContains('Hello, Mudah!');
24
+
25
+ // Full-screen widgets — mount, drive, snapshot
26
+ const tui = TestTui.mount(screen.root, { cols: 80, rows: 24 });
27
+ tui.send('down').send('enter');
28
+ expect(tui.snapshot()).toContain('prod');
29
+ ```
30
+
31
+ Also ships `FsMock` / `mockFs`, `NetworkMock`, `MockPluginRegistry`, `diffSnapshots` /
32
+ `diffTrees`, and `assertFast` for perf-budget assertions.
33
+
34
+ ## Links
35
+
36
+ - [Full documentation](https://github.com/thesimonharms/mudah#readme)
37
+ - [License: MIT](LICENSE)
@@ -0,0 +1,35 @@
1
+ /**
2
+ * In-memory virtual filesystem for testing.
3
+ * No disk I/O — everything stays in memory.
4
+ *
5
+ * ```ts
6
+ * const fs = new FsMock();
7
+ * fs.write('/etc/config.json', '{}');
8
+ * const text = fs.read('/etc/config.json');
9
+ * expect(text).toBe('{}');
10
+ * ```
11
+ */
12
+ export declare class FsMock {
13
+ private root;
14
+ write(path: string, content: string): void;
15
+ read(path: string): string | undefined;
16
+ exists(path: string): boolean;
17
+ isDir(path: string): boolean;
18
+ readdir(path: string): string[];
19
+ readdirRecursive(path: string): string[];
20
+ rm(path: string): boolean;
21
+ mkdir(path: string): void;
22
+ private resolve;
23
+ }
24
+ /**
25
+ * Global mock helpers: intercept `readFileSync` / `writeFileSync` calls
26
+ * and redirect them to a FsMock instance.
27
+ *
28
+ * ```ts
29
+ * const mock = new FsMock();
30
+ * const restore = mockFs(mock);
31
+ * // Now code using fs.readFileSync reads from `mock`
32
+ * restore(); // Back to real fs
33
+ * ```
34
+ */
35
+ export declare function mockFs(mock: FsMock): () => void;
@@ -0,0 +1,145 @@
1
+ import { createRequire } from 'node:module';
2
+ const require = createRequire(import.meta.url);
3
+ const fs = require('node:fs');
4
+ /**
5
+ * In-memory virtual filesystem for testing.
6
+ * No disk I/O — everything stays in memory.
7
+ *
8
+ * ```ts
9
+ * const fs = new FsMock();
10
+ * fs.write('/etc/config.json', '{}');
11
+ * const text = fs.read('/etc/config.json');
12
+ * expect(text).toBe('{}');
13
+ * ```
14
+ */
15
+ export class FsMock {
16
+ root = { type: 'dir', children: new Map() };
17
+ write(path, content) {
18
+ const parts = path.split('/').filter(Boolean);
19
+ let node = this.root;
20
+ for (let i = 0; i < parts.length - 1; i++) {
21
+ if (!node.children)
22
+ node.children = new Map();
23
+ let child = node.children.get(parts[i]);
24
+ if (!child) {
25
+ child = { type: 'dir', children: new Map() };
26
+ node.children.set(parts[i], child);
27
+ }
28
+ node = child;
29
+ }
30
+ if (!node.children)
31
+ node.children = new Map();
32
+ node.children.set(parts[parts.length - 1], { type: 'file', content });
33
+ }
34
+ read(path) {
35
+ const node = this.resolve(path);
36
+ return node?.type === 'file' ? node.content : undefined;
37
+ }
38
+ exists(path) {
39
+ return this.resolve(path) !== undefined;
40
+ }
41
+ isDir(path) {
42
+ return this.resolve(path)?.type === 'dir';
43
+ }
44
+ readdir(path) {
45
+ const node = this.resolve(path);
46
+ return node?.type === 'dir' && node.children ? [...node.children.keys()].sort() : [];
47
+ }
48
+ readdirRecursive(path) {
49
+ const out = [];
50
+ const walk = (node, prefix) => {
51
+ if (!node.children)
52
+ return;
53
+ for (const [name, child] of node.children) {
54
+ const fullPath = prefix === '' ? name : `${prefix}/${name}`;
55
+ out.push(fullPath);
56
+ if (child.type === 'dir')
57
+ walk(child, fullPath);
58
+ }
59
+ };
60
+ const dir = this.resolve(path);
61
+ if (dir?.type === 'dir')
62
+ walk(dir, '');
63
+ return out.sort();
64
+ }
65
+ rm(path) {
66
+ const parts = path.split('/').filter(Boolean);
67
+ let node = this.root;
68
+ for (let i = 0; i < parts.length - 1; i++) {
69
+ const child = node.children?.get(parts[i]);
70
+ if (!child)
71
+ return false;
72
+ node = child;
73
+ }
74
+ return node.children?.delete(parts[parts.length - 1]) ?? false;
75
+ }
76
+ mkdir(path) {
77
+ const parts = path.split('/').filter(Boolean);
78
+ let node = this.root;
79
+ for (const part of parts) {
80
+ if (!node.children)
81
+ node.children = new Map();
82
+ let child = node.children.get(part);
83
+ if (!child) {
84
+ child = { type: 'dir', children: new Map() };
85
+ node.children.set(part, child);
86
+ }
87
+ node = child;
88
+ }
89
+ }
90
+ resolve(path) {
91
+ const parts = path.split('/').filter(Boolean);
92
+ let node = this.root;
93
+ for (const part of parts) {
94
+ const next = node.children?.get(part);
95
+ if (!next)
96
+ return undefined;
97
+ node = next;
98
+ }
99
+ return node;
100
+ }
101
+ }
102
+ /**
103
+ * Global mock helpers: intercept `readFileSync` / `writeFileSync` calls
104
+ * and redirect them to a FsMock instance.
105
+ *
106
+ * ```ts
107
+ * const mock = new FsMock();
108
+ * const restore = mockFs(mock);
109
+ * // Now code using fs.readFileSync reads from `mock`
110
+ * restore(); // Back to real fs
111
+ * ```
112
+ */
113
+ export function mockFs(mock) {
114
+ const previousRead = fs.readFileSync;
115
+ const previousWrite = fs.writeFileSync;
116
+ const previousExists = fs.existsSync;
117
+ fs.readFileSync = ((path, encoding) => {
118
+ const key = String(path);
119
+ const text = mock.read(key) ?? mock.read(`/${key}`);
120
+ if (text === undefined) {
121
+ const err = new Error(`ENOENT: no such file or directory, open '${key}'`);
122
+ err.code = 'ENOENT';
123
+ throw err;
124
+ }
125
+ const enc = encoding === 'utf8' ||
126
+ encoding === 'utf-8' ||
127
+ (typeof encoding === 'object' &&
128
+ encoding !== null &&
129
+ encoding.encoding === 'utf8');
130
+ return enc ? text : Buffer.from(text);
131
+ });
132
+ fs.writeFileSync = ((path, data) => {
133
+ mock.write(String(path), typeof data === 'string' ? data : Buffer.from(data).toString('utf8'));
134
+ });
135
+ fs.existsSync = ((path) => {
136
+ const key = String(path);
137
+ return mock.exists(key) || mock.exists(`/${key}`);
138
+ });
139
+ return () => {
140
+ fs.readFileSync = previousRead;
141
+ fs.writeFileSync = previousWrite;
142
+ fs.existsSync = previousExists;
143
+ };
144
+ }
145
+ //# sourceMappingURL=fs-mock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs-mock.js","sourceRoot":"","sources":["../src/fs-mock.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,8 @@
1
- export { TestApp, TestResult, type TestAppOptions } from './test-app.js';
2
- export { TestTui, type TestTuiOptions } from './test-tui.js';
1
+ export { type ReplayHandle, type SessionAction, SessionRecorder } from '@mudah-cli/tui';
2
+ export { FsMock, mockFs } from './fs-mock.js';
3
+ export { NetworkMock, type NetworkMockResponse } from './network-mock.js';
4
+ export { createMockPlugin, type MockPlugin, type MockPluginOptions, MockPluginRegistry, } from './plugin-mock.js';
5
+ export { assertHasColor, assertLacksColor, type ColorExpectation } from './snapshot-assert.js';
6
+ export { TestApp, type TestAppOptions, TestResult } from './test-app.js';
7
+ export { assertFast, TestTui, type TestTuiAction, type TestTuiMeasure, type TestTuiOptions, } from './test-tui.js';
8
+ export { diffSnapshots, diffTrees } from './visual-diff.js';
package/dist/index.js CHANGED
@@ -1,3 +1,9 @@
1
+ export { SessionRecorder } from '@mudah-cli/tui';
2
+ export { FsMock, mockFs } from './fs-mock.js';
3
+ export { NetworkMock } from './network-mock.js';
4
+ export { createMockPlugin, MockPluginRegistry, } from './plugin-mock.js';
5
+ export { assertHasColor, assertLacksColor } from './snapshot-assert.js';
1
6
  export { TestApp, TestResult } from './test-app.js';
2
- export { TestTui } from './test-tui.js';
7
+ export { assertFast, TestTui, } from './test-tui.js';
8
+ export { diffSnapshots, diffTrees } from './visual-diff.js';
3
9
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,UAAU,EAAuB,MAAM,eAAe,CAAC;AACzE,OAAO,EAAE,OAAO,EAAuB,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyC,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACxF,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,WAAW,EAA4B,MAAM,mBAAmB,CAAC;AAC1E,OAAO,EACL,gBAAgB,EAGhB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAyB,MAAM,sBAAsB,CAAC;AAC/F,OAAO,EAAE,OAAO,EAAuB,UAAU,EAAE,MAAM,eAAe,CAAC;AACzE,OAAO,EACL,UAAU,EACV,OAAO,GAIR,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * In-memory fetch double for tests. No sockets — handlers return canned
3
+ * JSON or text by URL string or RegExp.
4
+ *
5
+ * ```ts
6
+ * const net = new NetworkMock();
7
+ * net.on('https://example.com/a.json', { ok: true });
8
+ * const body = await (await net.fetch('https://example.com/a.json')).json();
9
+ * expect(net.calls).toEqual(['https://example.com/a.json']);
10
+ * ```
11
+ */
12
+ export interface NetworkMockResponse {
13
+ status?: number;
14
+ body?: unknown;
15
+ headers?: Record<string, string>;
16
+ }
17
+ export declare class NetworkMock {
18
+ private readonly routes;
19
+ readonly calls: string[];
20
+ on(url: string | RegExp, body: unknown, status?: number): this;
21
+ reply(url: string | RegExp, response: NetworkMockResponse): this;
22
+ /** `fetch` implementation to inject into code under test. */
23
+ fetch: typeof fetch;
24
+ }
@@ -0,0 +1,31 @@
1
+ export class NetworkMock {
2
+ routes = [];
3
+ calls = [];
4
+ on(url, body, status = 200) {
5
+ this.routes.push({ match: url, response: { status, body } });
6
+ return this;
7
+ }
8
+ reply(url, response) {
9
+ this.routes.push({ match: url, response });
10
+ return this;
11
+ }
12
+ /** `fetch` implementation to inject into code under test. */
13
+ fetch = async (input, _init) => {
14
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
15
+ this.calls.push(url);
16
+ const route = this.routes.find((entry) => typeof entry.match === 'string' ? entry.match === url : entry.match.test(url));
17
+ if (!route) {
18
+ return new Response('not found', { status: 404, headers: { 'content-type': 'text/plain' } });
19
+ }
20
+ const status = route.response.status ?? 200;
21
+ const body = route.response.body;
22
+ if (typeof body === 'string' || body instanceof Uint8Array) {
23
+ return new Response(body, { status, headers: route.response.headers });
24
+ }
25
+ return new Response(JSON.stringify(body ?? null), {
26
+ status,
27
+ headers: { 'content-type': 'application/json', ...route.response.headers },
28
+ });
29
+ };
30
+ }
31
+ //# sourceMappingURL=network-mock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-mock.js","sourceRoot":"","sources":["../src/network-mock.ts"],"names":[],"mappings":"AAiBA,MAAM,OAAO,WAAW;IACL,MAAM,GAAqE,EAAE,CAAC;IACtF,KAAK,GAAa,EAAE,CAAC;IAE9B,EAAE,CAAC,GAAoB,EAAE,IAAa,EAAE,MAAM,GAAG,GAAG;QAClD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;QAC7D,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,GAAoB,EAAE,QAA6B;QACvD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC3C,OAAO,IAAI,CAAC;IACd,CAAC;IAED,6DAA6D;IAC7D,KAAK,GAAiB,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;QAC3C,MAAM,GAAG,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,YAAY,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC;QAC9F,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACrB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CACvC,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,KAAK,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAC9E,CAAC;QACF,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,IAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,EAAE,CAAC,CAAC;QAC/F,CAAC;QACD,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,IAAI,GAAG,CAAC;QAC5C,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;QACjC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,YAAY,UAAU,EAAE,CAAC;YAC3D,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;QACzE,CAAC;QACD,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,IAAI,CAAC,EAAE;YAChD,MAAM;YACN,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,GAAG,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE;SAC3E,CAAC,CAAC;IACL,CAAC,CAAC;CACH"}
@@ -0,0 +1,47 @@
1
+ import type { CommandClass, CommandModule, PluginDiscoveryOptions, PluginInfo, ProviderClass } from '@mudah-cli/core';
2
+ export interface MockPluginOptions {
3
+ name: string;
4
+ providers?: readonly ProviderClass[];
5
+ commands?: readonly CommandModule[] | readonly CommandClass[];
6
+ keywords?: readonly string[];
7
+ }
8
+ /**
9
+ * A {@link PluginInfo}-shaped plugin plus the extras the mock registry
10
+ * needs to serve `resolve` / `readPackage` / `importModule`.
11
+ */
12
+ export interface MockPlugin extends PluginInfo {
13
+ readonly keywords: readonly string[];
14
+ /** Module object returned by the injectable `importModule`. */
15
+ readonly module: Record<string, unknown>;
16
+ }
17
+ /**
18
+ * Build a plugin fixture that `discoverPlugins` / `loadPlugin` can load
19
+ * through {@link MockPluginRegistry.asDiscoveryOptions}.
20
+ */
21
+ export declare function createMockPlugin(options: MockPluginOptions): MockPlugin;
22
+ /**
23
+ * In-memory plugin graph for kernel tests. Supplies the three injectables
24
+ * on {@link PluginDiscoveryOptions} so discovery never touches disk.
25
+ *
26
+ * ```ts
27
+ * const registry = new MockPluginRegistry();
28
+ * registry.register(createMockPlugin({ name: 'demo-plugin', providers: [DemoProvider] }));
29
+ * const plugins = await discoverPlugins('/app', registry.asDiscoveryOptions());
30
+ * ```
31
+ */
32
+ export declare class MockPluginRegistry {
33
+ private readonly plugins;
34
+ register(plugin: MockPlugin | MockPluginOptions): this;
35
+ list(): MockPlugin[];
36
+ clear(): void;
37
+ /**
38
+ * Injectable discovery options matching `packages/core/src/plugins.ts`
39
+ * {@link PluginDiscoveryOptions}: `resolve`, `readPackage`, `importModule`.
40
+ */
41
+ asDiscoveryOptions(): PluginDiscoveryOptions;
42
+ private resolve;
43
+ private readPackage;
44
+ private importModule;
45
+ private pluginForManifest;
46
+ private pluginNameFromUrl;
47
+ }
@@ -0,0 +1,109 @@
1
+ import { join } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ /**
4
+ * Build a plugin fixture that `discoverPlugins` / `loadPlugin` can load
5
+ * through {@link MockPluginRegistry.asDiscoveryOptions}.
6
+ */
7
+ export function createMockPlugin(options) {
8
+ const providers = options.providers ?? [];
9
+ const commandClasses = (options.commands ?? []).map((entry) => isCommandModule(entry) ? entry.default : entry);
10
+ const commands = commandClasses.map((ctor) => ({ default: ctor }));
11
+ const keywords = options.keywords ?? ['mudah-plugin'];
12
+ const module = {};
13
+ if (providers.length > 0)
14
+ module['providers'] = [...providers];
15
+ if (commandClasses.length > 0)
16
+ module['commands'] = [...commandClasses];
17
+ for (const provider of providers) {
18
+ if (provider.name)
19
+ module[provider.name] = provider;
20
+ }
21
+ return { name: options.name, providers, commands, keywords, module };
22
+ }
23
+ /**
24
+ * In-memory plugin graph for kernel tests. Supplies the three injectables
25
+ * on {@link PluginDiscoveryOptions} so discovery never touches disk.
26
+ *
27
+ * ```ts
28
+ * const registry = new MockPluginRegistry();
29
+ * registry.register(createMockPlugin({ name: 'demo-plugin', providers: [DemoProvider] }));
30
+ * const plugins = await discoverPlugins('/app', registry.asDiscoveryOptions());
31
+ * ```
32
+ */
33
+ export class MockPluginRegistry {
34
+ plugins = new Map();
35
+ register(plugin) {
36
+ const mock = isMockPlugin(plugin) ? plugin : createMockPlugin(plugin);
37
+ this.plugins.set(mock.name, mock);
38
+ return this;
39
+ }
40
+ list() {
41
+ return [...this.plugins.values()];
42
+ }
43
+ clear() {
44
+ this.plugins.clear();
45
+ }
46
+ /**
47
+ * Injectable discovery options matching `packages/core/src/plugins.ts`
48
+ * {@link PluginDiscoveryOptions}: `resolve`, `readPackage`, `importModule`.
49
+ */
50
+ asDiscoveryOptions() {
51
+ return {
52
+ resolve: (name, from) => this.resolve(name, from),
53
+ readPackage: (path) => this.readPackage(path),
54
+ importModule: (url) => this.importModule(url),
55
+ };
56
+ }
57
+ resolve(name, from) {
58
+ return pathToFileURL(join(from, 'node_modules', name, 'index.js')).href;
59
+ }
60
+ async readPackage(path) {
61
+ const normalized = path.replace(/\\/g, '/');
62
+ const plugin = this.pluginForManifest(normalized);
63
+ if (plugin !== undefined) {
64
+ return {
65
+ name: plugin.name,
66
+ main: 'index.js',
67
+ keywords: [...plugin.keywords],
68
+ };
69
+ }
70
+ if (normalized.endsWith('/package.json') && !normalized.includes('/node_modules/')) {
71
+ return {
72
+ name: 'mock-host',
73
+ dependencies: Object.fromEntries([...this.plugins.keys()].map((name) => [name, '0.0.0'])),
74
+ };
75
+ }
76
+ throw new Error(`[testing] no mock manifest at ${path}`);
77
+ }
78
+ async importModule(url) {
79
+ const name = this.pluginNameFromUrl(url);
80
+ const plugin = name === undefined ? undefined : this.plugins.get(name);
81
+ if (plugin === undefined)
82
+ throw new Error(`[testing] cannot import ${url}`);
83
+ return { ...plugin.module };
84
+ }
85
+ pluginForManifest(normalizedPath) {
86
+ for (const plugin of this.plugins.values()) {
87
+ if (normalizedPath.includes(`/node_modules/${plugin.name}/package.json`))
88
+ return plugin;
89
+ }
90
+ return undefined;
91
+ }
92
+ pluginNameFromUrl(url) {
93
+ const path = url.startsWith('file:') ? new URL(url).pathname : url;
94
+ const normalized = path.replace(/\\/g, '/');
95
+ for (const name of this.plugins.keys()) {
96
+ if (normalized.includes(`/node_modules/${name}/`))
97
+ return name;
98
+ }
99
+ const match = /\/node_modules\/((?:@[^/]+\/)?[^/]+)\//.exec(normalized);
100
+ return match?.[1];
101
+ }
102
+ }
103
+ function isCommandModule(value) {
104
+ return typeof value === 'object' && value !== null && 'default' in value;
105
+ }
106
+ function isMockPlugin(value) {
107
+ return 'module' in value && 'keywords' in value && 'providers' in value && 'commands' in value;
108
+ }
109
+ //# sourceMappingURL=plugin-mock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-mock.js","sourceRoot":"","sources":["../src/plugin-mock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ export interface ColorExpectation {
2
+ /** Plain-text fragment to find. */
3
+ text: string;
4
+ /** Hex color (case-insensitive) expected on at least one matched cell. */
5
+ hex: string;
6
+ }
7
+ /**
8
+ * Assert that `text` contains a fragment painted with the given hex color.
9
+ * Throws on failure with a diff-style message.
10
+ */
11
+ export declare function assertHasColor(text: string, expectation: ColorExpectation): void;
12
+ /**
13
+ * Assert that `text` does NOT contain a fragment painted with the given hex
14
+ * color (i.e. the fragment is present but in a different color, or absent
15
+ * entirely).
16
+ */
17
+ export declare function assertLacksColor(text: string, expectation: ColorExpectation): void;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Color assertions for TUI snapshots.
3
+ *
4
+ * These are plain functions, not vitest matchers, so they don't force a
5
+ * vitest dependency on the testing package. The user's tests can wrap them
6
+ * in `expect(() => assertHasColor(...)).not.toThrow()` or similar.
7
+ */
8
+ import { stripAnsi } from '@mudah-cli/ui';
9
+ const ANSI_SGR_FOREGROUND = /\x1b\[(?:38;2;(\d+);(\d+);(\d+)|38;5;(\d+)|3[0-8])m/g;
10
+ function hexFromRgb(r, g, b) {
11
+ return `#${[r, g, b].map((n) => Number(n).toString(16).padStart(2, '0')).join('')}`;
12
+ }
13
+ /** Return runs of (text, optional hex color) covering `text` in order. */
14
+ function styledRuns(text) {
15
+ const runs = [];
16
+ let last = 0;
17
+ let current = { text: '' };
18
+ for (const match of text.matchAll(ANSI_SGR_FOREGROUND)) {
19
+ // Close out whatever came before this escape.
20
+ const start = match.index ?? 0;
21
+ current.text += text.slice(last, start);
22
+ if (current.text.length > 0)
23
+ runs.push(current);
24
+ // Open a new run carrying the color from this escape.
25
+ const r = match[1];
26
+ const g = match[2];
27
+ const b = match[3];
28
+ current = r && g && b ? { text: '', hex: hexFromRgb(r, g, b) } : { text: '' };
29
+ last = start + match[0].length;
30
+ }
31
+ current.text += text.slice(last);
32
+ if (current.text.length > 0)
33
+ runs.push(current);
34
+ return runs;
35
+ }
36
+ function hexToRgb(hex) {
37
+ const v = hex.replace('#', '');
38
+ const full = v.length === 3
39
+ ? v
40
+ .split('')
41
+ .map((c) => c + c)
42
+ .join('')
43
+ : v;
44
+ const n = parseInt(full, 16);
45
+ return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
46
+ }
47
+ /**
48
+ * Assert that `text` contains a fragment painted with the given hex color.
49
+ * Throws on failure with a diff-style message.
50
+ */
51
+ export function assertHasColor(text, expectation) {
52
+ const { text: fragment, hex } = expectation;
53
+ const plain = stripAnsi(text);
54
+ const at = plain.indexOf(fragment);
55
+ if (at < 0) {
56
+ throw new Error(`[testing] Snapshot does not contain "${fragment}".\n--- snapshot ---\n${text}`);
57
+ }
58
+ // Walk the styled runs and find a run whose text overlaps [at, at+fragment.length].
59
+ const end = at + fragment.length;
60
+ let cursor = 0;
61
+ let found;
62
+ for (const run of styledRuns(text)) {
63
+ const runStart = cursor;
64
+ const runEnd = cursor + stripAnsi(run.text).length;
65
+ if (run.hex !== undefined && runStart < end && runEnd > at) {
66
+ found = run.hex;
67
+ break;
68
+ }
69
+ cursor = runEnd;
70
+ }
71
+ if (found === undefined) {
72
+ throw new Error(`[testing] "${fragment}" appears in the snapshot but is not painted with a hex color (the cell uses the default style).\n--- snapshot ---\n${text}`);
73
+ }
74
+ const [r1, g1, b1] = hexToRgb(found);
75
+ const [r2, g2, b2] = hexToRgb(hex);
76
+ if (r1 !== r2 || g1 !== g2 || b1 !== b2) {
77
+ throw new Error(`[testing] "${fragment}" is painted with ${found} but expected ${hex}.\n--- snapshot ---\n${text}`);
78
+ }
79
+ }
80
+ /**
81
+ * Assert that `text` does NOT contain a fragment painted with the given hex
82
+ * color (i.e. the fragment is present but in a different color, or absent
83
+ * entirely).
84
+ */
85
+ export function assertLacksColor(text, expectation) {
86
+ const { text: fragment } = expectation;
87
+ if (!stripAnsi(text).includes(fragment))
88
+ return;
89
+ try {
90
+ assertHasColor(text, expectation);
91
+ }
92
+ catch {
93
+ return;
94
+ }
95
+ throw new Error(`[testing] "${fragment}" appears painted with ${expectation.hex} when it should not.`);
96
+ }
97
+ //# sourceMappingURL=snapshot-assert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-assert.js","sourceRoot":"","sources":["../src/snapshot-assert.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
+ import { type CommandModule } from '@mudah-cli/console';
1
2
  import { Application } from '@mudah-cli/core';
2
3
  import { Output } from '@mudah-cli/ui';
3
- import { type CommandModule } from '@mudah-cli/console';
4
4
  export interface TestAppOptions {
5
5
  /** App root containing `mudah.json`. */
6
6
  cwd: string;
package/dist/test-app.js CHANGED
@@ -1,7 +1,7 @@
1
+ import { ConsoleKernel, renderError } from '@mudah-cli/console';
1
2
  import { Application } from '@mudah-cli/core';
2
3
  import { detectCapabilities } from '@mudah-cli/terminal';
3
4
  import { Output, resolveTheme } from '@mudah-cli/ui';
4
- import { ConsoleKernel, renderError } from '@mudah-cli/console';
5
5
  /**
6
6
  * An in-process test harness around a real Mudah application.
7
7
  *