@decohere/testing 0.0.1-dev.119
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/README.md +34 -0
- package/commonjs/src/flush-promises.js +16 -0
- package/commonjs/src/glob-with-memfs.js +22 -0
- package/commonjs/src/index.js +22 -0
- package/commonjs/src/memfs.js +52 -0
- package/commonjs/src/mock-logger.js +36 -0
- package/commonjs/src/spy-on-constructor.js +27 -0
- package/es6/src/flush-promises.js +6 -0
- package/es6/src/glob-with-memfs.js +20 -0
- package/es6/src/index.js +5 -0
- package/es6/src/memfs.js +25 -0
- package/es6/src/mock-logger.js +26 -0
- package/es6/src/spy-on-constructor.js +31 -0
- package/package.json +33 -0
- package/types/src/flush-promises.d.ts +1 -0
- package/types/src/glob-with-memfs.d.ts +3 -0
- package/types/src/index.d.ts +5 -0
- package/types/src/memfs.d.ts +5 -0
- package/types/src/mock-logger.d.ts +13 -0
- package/types/src/spy-on-constructor.d.ts +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
Vitest helpers for testing Decohere code ([ADR 0015](../../docs/adr/0015-core-devkit-testing.md)). Only for tests, never imported by library or app code.
|
|
4
|
+
|
|
5
|
+
| Export | What it does |
|
|
6
|
+
|---|---|
|
|
7
|
+
| `resetMemfs`, `seedMemfs`, `createTestFile`, `readFile`, `fileExists` | An in-memory file system ([memfs](https://github.com/streamich/memfs)) for tests that mock `node:fs` |
|
|
8
|
+
| `withMemfsFsForGlob` | Wraps the real `glob` module so its file access goes through memfs: real matching, in-memory files |
|
|
9
|
+
| `createMockLogger` | A scribe-shaped logger whose methods are `vi.fn()` spies |
|
|
10
|
+
| `spyOnConstructor` | Spies on a class constructor without losing `design:paramtypes`, so crucible can still inject it ([how-to](../../docs/testing/how-tos/vitest-constructor-spy-with-di.md)) |
|
|
11
|
+
| `flushPromises` | Waits until pending promise callbacks have run |
|
|
12
|
+
|
|
13
|
+
This repo routes file access to memfs in a Vitest setup file ([`test/fs.setup.ts`](../../test/fs.setup.ts)), with `__mocks__/fs.ts` re-exporting memfs:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// vitest setup file
|
|
17
|
+
vi.mock("node:fs");
|
|
18
|
+
vi.mock("node:fs/promises");
|
|
19
|
+
vi.mock("glob", async () => {
|
|
20
|
+
const actual = await vi.importActual<typeof import("glob")>("glob");
|
|
21
|
+
const { withMemfsFsForGlob } = await import("@decohere/testing");
|
|
22
|
+
return withMemfsFsForGlob(actual);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// a test
|
|
26
|
+
import { resetMemfs, seedMemfs } from "@decohere/testing";
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
resetMemfs();
|
|
30
|
+
seedMemfs({ "/app/config/config.yaml": "name: api\n" });
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`vitest` and `memfs` should be peer dependencies; the dependency sync can't declare peers yet ([#683](https://github.com/typesmiths/decohere/issues/683)).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "flushPromises", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return flushPromises;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
var scheduler = typeof setImmediate === "function" ? setImmediate : setTimeout;
|
|
12
|
+
function flushPromises() {
|
|
13
|
+
return new Promise((resolve)=>{
|
|
14
|
+
scheduler(resolve);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "withMemfsFsForGlob", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return withMemfsFsForGlob;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _memfs = require("memfs");
|
|
12
|
+
function withMemfsFsForGlob(actual) {
|
|
13
|
+
// Preserve all module exports, overriding only `glob` to force `{ fs: memfs }`.
|
|
14
|
+
const glob = (pattern, options)=>actual.glob(pattern, {
|
|
15
|
+
...options,
|
|
16
|
+
fs: _memfs.fs
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
...actual,
|
|
20
|
+
glob
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
_export_star(require("./flush-promises.js"), exports);
|
|
6
|
+
_export_star(require("./glob-with-memfs.js"), exports);
|
|
7
|
+
_export_star(require("./memfs.js"), exports);
|
|
8
|
+
_export_star(require("./mock-logger.js"), exports);
|
|
9
|
+
_export_star(require("./spy-on-constructor.js"), exports);
|
|
10
|
+
function _export_star(from, to) {
|
|
11
|
+
Object.keys(from).forEach(function(k) {
|
|
12
|
+
if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
|
|
13
|
+
Object.defineProperty(to, k, {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
get: function() {
|
|
16
|
+
return from[k];
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
return from;
|
|
22
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
get createTestFile () {
|
|
13
|
+
return createTestFile;
|
|
14
|
+
},
|
|
15
|
+
get fileExists () {
|
|
16
|
+
return fileExists;
|
|
17
|
+
},
|
|
18
|
+
get readFile () {
|
|
19
|
+
return readFile;
|
|
20
|
+
},
|
|
21
|
+
get resetMemfs () {
|
|
22
|
+
return resetMemfs;
|
|
23
|
+
},
|
|
24
|
+
get seedMemfs () {
|
|
25
|
+
return seedMemfs;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const _memfs = require("memfs");
|
|
29
|
+
function resetMemfs() {
|
|
30
|
+
_memfs.vol.reset();
|
|
31
|
+
}
|
|
32
|
+
function seedMemfs(files) {
|
|
33
|
+
_memfs.vol.fromJSON(files);
|
|
34
|
+
}
|
|
35
|
+
function createTestFile(filePath, content) {
|
|
36
|
+
_memfs.fs.mkdirSync(filePath.substring(0, filePath.lastIndexOf("/")), {
|
|
37
|
+
recursive: true
|
|
38
|
+
});
|
|
39
|
+
_memfs.fs.writeFileSync(filePath, content);
|
|
40
|
+
}
|
|
41
|
+
async function readFile(filePath) {
|
|
42
|
+
const response = await _memfs.fs.promises.readFile(filePath, "utf8");
|
|
43
|
+
return response;
|
|
44
|
+
}
|
|
45
|
+
function fileExists(filePath) {
|
|
46
|
+
try {
|
|
47
|
+
_memfs.vol.statSync(filePath);
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "createMockLogger", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return createMockLogger;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _vitest = require("vitest");
|
|
12
|
+
const DefaultLogLevels = {
|
|
13
|
+
fatal: 60,
|
|
14
|
+
error: 50,
|
|
15
|
+
warn: 40,
|
|
16
|
+
info: 30,
|
|
17
|
+
debug: 20,
|
|
18
|
+
trace: 10
|
|
19
|
+
};
|
|
20
|
+
function createMockLogger() {
|
|
21
|
+
return {
|
|
22
|
+
error: _vitest.vi.fn(),
|
|
23
|
+
info: _vitest.vi.fn(),
|
|
24
|
+
warn: _vitest.vi.fn(),
|
|
25
|
+
debug: _vitest.vi.fn(),
|
|
26
|
+
trace: _vitest.vi.fn(),
|
|
27
|
+
fatal: _vitest.vi.fn(),
|
|
28
|
+
isLevelEnabled: _vitest.vi.fn().mockReturnValue(true),
|
|
29
|
+
get currentLevel () {
|
|
30
|
+
return "info";
|
|
31
|
+
},
|
|
32
|
+
get logLevels () {
|
|
33
|
+
return DefaultLogLevels;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "spyOnConstructor", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return spyOnConstructor;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _vitest = require("vitest");
|
|
12
|
+
function spyOnConstructor(Target) {
|
|
13
|
+
const spy = _vitest.vi.fn();
|
|
14
|
+
const ProxiedTarget = new Proxy(Target, {
|
|
15
|
+
construct (Ctor, args) {
|
|
16
|
+
spy(...args);
|
|
17
|
+
return Reflect.construct(Ctor, args);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
for (const key of Reflect.getMetadataKeys(Target)){
|
|
21
|
+
Reflect.defineMetadata(key, Reflect.getMetadata(key, Target), ProxiedTarget);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
spy,
|
|
25
|
+
Proxy: ProxiedTarget
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { fs } from "memfs";
|
|
2
|
+
// Wraps a real `glob` module so all file-system access is routed through memfs.
|
|
3
|
+
//
|
|
4
|
+
// Why this exists:
|
|
5
|
+
// - Integration tests often need real glob matching semantics.
|
|
6
|
+
// - Vitest `vi.mock` factories are hoisted, so directly importing and patching
|
|
7
|
+
// `glob` inside helper modules can trigger initialization-order issues.
|
|
8
|
+
// - Passing the already-loaded `actual` module keeps the helper pure and
|
|
9
|
+
// hoist-safe while still allowing us to inject memfs.
|
|
10
|
+
export function withMemfsFsForGlob(actual) {
|
|
11
|
+
// Preserve all module exports, overriding only `glob` to force `{ fs: memfs }`.
|
|
12
|
+
const glob = (pattern, options)=>actual.glob(pattern, {
|
|
13
|
+
...options,
|
|
14
|
+
fs
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
glob
|
|
19
|
+
};
|
|
20
|
+
}
|
package/es6/src/index.js
ADDED
package/es6/src/memfs.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { fs, vol } from "memfs";
|
|
2
|
+
export function resetMemfs() {
|
|
3
|
+
vol.reset();
|
|
4
|
+
}
|
|
5
|
+
export function seedMemfs(files) {
|
|
6
|
+
vol.fromJSON(files);
|
|
7
|
+
}
|
|
8
|
+
export function createTestFile(filePath, content) {
|
|
9
|
+
fs.mkdirSync(filePath.substring(0, filePath.lastIndexOf("/")), {
|
|
10
|
+
recursive: true
|
|
11
|
+
});
|
|
12
|
+
fs.writeFileSync(filePath, content);
|
|
13
|
+
}
|
|
14
|
+
export async function readFile(filePath) {
|
|
15
|
+
const response = await fs.promises.readFile(filePath, "utf8");
|
|
16
|
+
return response;
|
|
17
|
+
}
|
|
18
|
+
export function fileExists(filePath) {
|
|
19
|
+
try {
|
|
20
|
+
vol.statSync(filePath);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
const DefaultLogLevels = {
|
|
3
|
+
fatal: 60,
|
|
4
|
+
error: 50,
|
|
5
|
+
warn: 40,
|
|
6
|
+
info: 30,
|
|
7
|
+
debug: 20,
|
|
8
|
+
trace: 10
|
|
9
|
+
};
|
|
10
|
+
export function createMockLogger() {
|
|
11
|
+
return {
|
|
12
|
+
error: vi.fn(),
|
|
13
|
+
info: vi.fn(),
|
|
14
|
+
warn: vi.fn(),
|
|
15
|
+
debug: vi.fn(),
|
|
16
|
+
trace: vi.fn(),
|
|
17
|
+
fatal: vi.fn(),
|
|
18
|
+
isLevelEnabled: vi.fn().mockReturnValue(true),
|
|
19
|
+
get currentLevel () {
|
|
20
|
+
return "info";
|
|
21
|
+
},
|
|
22
|
+
get logLevels () {
|
|
23
|
+
return DefaultLogLevels;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a Proxy around a class constructor that intercepts `new` calls,
|
|
4
|
+
* records arguments via a spy, and delegates to the real constructor.
|
|
5
|
+
*
|
|
6
|
+
* Use this to assert what was injected into a class by a DI container
|
|
7
|
+
* (e.g. Container) without changing actual behavior.
|
|
8
|
+
*
|
|
9
|
+
* The proxy copies all `reflect-metadata` entries from the original class
|
|
10
|
+
* so that DI metadata (`design:paramtypes`) remains intact.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const { spy, Proxy: SpiedService } = spyOnConstructor(MyService);
|
|
14
|
+
* // use SpiedService in place of MyService when wiring the container
|
|
15
|
+
* expect(spy).toHaveBeenCalledWith(expect.any(ExpectedDependency));
|
|
16
|
+
*/ export function spyOnConstructor(Target) {
|
|
17
|
+
const spy = vi.fn();
|
|
18
|
+
const ProxiedTarget = new Proxy(Target, {
|
|
19
|
+
construct (Ctor, args) {
|
|
20
|
+
spy(...args);
|
|
21
|
+
return Reflect.construct(Ctor, args);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
for (const key of Reflect.getMetadataKeys(Target)){
|
|
25
|
+
Reflect.defineMetadata(key, Reflect.getMetadata(key, Target), ProxiedTarget);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
spy,
|
|
29
|
+
Proxy: ProxiedTarget
|
|
30
|
+
};
|
|
31
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@decohere/testing",
|
|
3
|
+
"version": "0.0.1-dev.119",
|
|
4
|
+
"description": "Test helpers for Decohere code under Vitest: memfs file systems, a mock logger, constructor spies that keep DI metadata.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"typescript",
|
|
7
|
+
"testing",
|
|
8
|
+
"vitest",
|
|
9
|
+
"memfs",
|
|
10
|
+
"decohere"
|
|
11
|
+
],
|
|
12
|
+
"license": "ISC",
|
|
13
|
+
"author": "Typesmiths",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "index.js",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./types/src/index.d.ts",
|
|
19
|
+
"import": "./es6/src/index.js",
|
|
20
|
+
"require": "./commonjs/src/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@decohere/core": "0.0.1-dev.119"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"memfs": "^4.17.0",
|
|
31
|
+
"vitest": "^3.2.6"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function flushPromises(): Promise<unknown>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function resetMemfs(): void;
|
|
2
|
+
export declare function seedMemfs(files: Record<string, string>): void;
|
|
3
|
+
export declare function createTestFile(filePath: string, content: string | Buffer): void;
|
|
4
|
+
export declare function readFile(filePath: string): Promise<string>;
|
|
5
|
+
export declare function fileExists(filePath: string): boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type LoggerMethod = (msg: string | number | object, ...args: unknown[]) => void;
|
|
2
|
+
export type TestLogger = {
|
|
3
|
+
fatal: LoggerMethod;
|
|
4
|
+
error: LoggerMethod;
|
|
5
|
+
warn: LoggerMethod;
|
|
6
|
+
info: LoggerMethod;
|
|
7
|
+
debug: LoggerMethod;
|
|
8
|
+
trace: LoggerMethod;
|
|
9
|
+
isLevelEnabled(level: string): boolean;
|
|
10
|
+
readonly currentLevel: string;
|
|
11
|
+
readonly logLevels: Record<string, number>;
|
|
12
|
+
};
|
|
13
|
+
export declare function createMockLogger(): TestLogger;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
import type { Constructable } from "@decohere/core/reflection";
|
|
3
|
+
/**
|
|
4
|
+
* Creates a Proxy around a class constructor that intercepts `new` calls,
|
|
5
|
+
* records arguments via a spy, and delegates to the real constructor.
|
|
6
|
+
*
|
|
7
|
+
* Use this to assert what was injected into a class by a DI container
|
|
8
|
+
* (e.g. Container) without changing actual behavior.
|
|
9
|
+
*
|
|
10
|
+
* The proxy copies all `reflect-metadata` entries from the original class
|
|
11
|
+
* so that DI metadata (`design:paramtypes`) remains intact.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const { spy, Proxy: SpiedService } = spyOnConstructor(MyService);
|
|
15
|
+
* // use SpiedService in place of MyService when wiring the container
|
|
16
|
+
* expect(spy).toHaveBeenCalledWith(expect.any(ExpectedDependency));
|
|
17
|
+
*/
|
|
18
|
+
export declare function spyOnConstructor<T extends Constructable>(Target: T): {
|
|
19
|
+
spy: ReturnType<typeof vi.fn>;
|
|
20
|
+
Proxy: T;
|
|
21
|
+
};
|