@bonsae/nrg 0.9.1 → 0.10.1
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/package.json +5 -1
- package/server/index.cjs +2 -0
- package/test/index.js +273 -0
- package/types/test.d.ts +39 -0
package/README.md
CHANGED
|
@@ -155,6 +155,9 @@ src/
|
|
|
155
155
|
│ │ └── index.ts # registerTypes, exports
|
|
156
156
|
│ ├── constants.ts
|
|
157
157
|
│ └── validator.ts # AJV-based validation
|
|
158
|
+
├── test/ # Test utilities for consumers
|
|
159
|
+
│ ├── index.ts # createNode, receive, close, reset
|
|
160
|
+
│ └── mocks.ts # RED and Node-RED node mocks
|
|
158
161
|
├── vite/ # Build tooling
|
|
159
162
|
│ ├── plugin.ts # Vite plugin factory
|
|
160
163
|
│ ├── plugins/ # Dev server, build orchestration
|
|
@@ -167,6 +170,37 @@ src/
|
|
|
167
170
|
└── server.json
|
|
168
171
|
```
|
|
169
172
|
|
|
173
|
+
## Testing
|
|
174
|
+
|
|
175
|
+
Test your nodes' server-side logic with `@bonsae/nrg/test`:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
pnpm add -D vitest
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// tests/my-node.test.ts
|
|
183
|
+
import { describe, it, expect } from "vitest";
|
|
184
|
+
import { createNode } from "@bonsae/nrg/test";
|
|
185
|
+
import MyNode from "../src/server/nodes/my-node";
|
|
186
|
+
|
|
187
|
+
describe("my-node", () => {
|
|
188
|
+
it("should process messages", async () => {
|
|
189
|
+
const { node } = await createNode(MyNode, {
|
|
190
|
+
config: { greeting: "hello" },
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await node.receive({ payload: "world" });
|
|
194
|
+
|
|
195
|
+
expect(node.sent(0)).toEqual([{ payload: "hello world" }]);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
npx vitest run
|
|
202
|
+
```
|
|
203
|
+
|
|
170
204
|
## Development
|
|
171
205
|
|
|
172
206
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bonsae/nrg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "NRG framework — build Node-RED nodes with Vue 3, TypeScript, and JSON Schema",
|
|
5
5
|
"author": "Allan Oricil <allanoricil@duck.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"types": "./types/vite.d.ts",
|
|
34
34
|
"default": "./vite/index.js"
|
|
35
35
|
},
|
|
36
|
+
"./test": {
|
|
37
|
+
"types": "./types/test.d.ts",
|
|
38
|
+
"default": "./test/index.js"
|
|
39
|
+
},
|
|
36
40
|
"./tsconfig/base.json": "./tsconfig/base.json",
|
|
37
41
|
"./tsconfig/client.json": "./tsconfig/client.json",
|
|
38
42
|
"./tsconfig/server.json": "./tsconfig/server.json"
|
package/server/index.cjs
CHANGED
|
@@ -39,6 +39,7 @@ __export(index_exports, {
|
|
|
39
39
|
defineIONode: () => defineIONode,
|
|
40
40
|
defineModule: () => defineModule,
|
|
41
41
|
defineSchema: () => defineSchema,
|
|
42
|
+
initValidator: () => initValidator,
|
|
42
43
|
registerType: () => registerType,
|
|
43
44
|
registerTypes: () => registerTypes
|
|
44
45
|
});
|
|
@@ -1043,6 +1044,7 @@ function defineModule(definition) {
|
|
|
1043
1044
|
defineIONode,
|
|
1044
1045
|
defineModule,
|
|
1045
1046
|
defineSchema,
|
|
1047
|
+
initValidator,
|
|
1046
1048
|
registerType,
|
|
1047
1049
|
registerTypes
|
|
1048
1050
|
});
|
package/test/index.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// src/test/index.ts
|
|
2
|
+
import { vi as vi2 } from "vitest";
|
|
3
|
+
|
|
4
|
+
// src/test/mocks.ts
|
|
5
|
+
import { vi } from "vitest";
|
|
6
|
+
function createMockRED(options = {}) {
|
|
7
|
+
const { nodes = {}, settings = {} } = options;
|
|
8
|
+
return {
|
|
9
|
+
log: {
|
|
10
|
+
info: vi.fn(),
|
|
11
|
+
warn: vi.fn(),
|
|
12
|
+
error: vi.fn(),
|
|
13
|
+
debug: vi.fn()
|
|
14
|
+
},
|
|
15
|
+
nodes: {
|
|
16
|
+
getNode: vi.fn((id) => nodes[id]),
|
|
17
|
+
registerType: vi.fn(),
|
|
18
|
+
createNode: vi.fn()
|
|
19
|
+
},
|
|
20
|
+
httpAdmin: {
|
|
21
|
+
get: vi.fn(),
|
|
22
|
+
post: vi.fn(),
|
|
23
|
+
put: vi.fn(),
|
|
24
|
+
delete: vi.fn(),
|
|
25
|
+
use: vi.fn()
|
|
26
|
+
},
|
|
27
|
+
settings: { ...settings },
|
|
28
|
+
_: vi.fn((key, subs) => {
|
|
29
|
+
if (!subs) return key;
|
|
30
|
+
return Object.entries(subs).reduce(
|
|
31
|
+
(str, [k, v]) => str.replace(`__${k}__`, v),
|
|
32
|
+
key
|
|
33
|
+
);
|
|
34
|
+
}),
|
|
35
|
+
util: {
|
|
36
|
+
evaluateNodeProperty: vi.fn(
|
|
37
|
+
(value, type, _node, msg, callback) => {
|
|
38
|
+
try {
|
|
39
|
+
let result;
|
|
40
|
+
switch (type) {
|
|
41
|
+
case "str":
|
|
42
|
+
result = String(value);
|
|
43
|
+
break;
|
|
44
|
+
case "num":
|
|
45
|
+
result = Number(value);
|
|
46
|
+
break;
|
|
47
|
+
case "bool":
|
|
48
|
+
result = value === "true" || value === true;
|
|
49
|
+
break;
|
|
50
|
+
case "json":
|
|
51
|
+
result = typeof value === "string" ? JSON.parse(value) : value;
|
|
52
|
+
break;
|
|
53
|
+
case "msg":
|
|
54
|
+
result = msg ? getProperty(msg, value) : void 0;
|
|
55
|
+
break;
|
|
56
|
+
case "date":
|
|
57
|
+
result = Date.now();
|
|
58
|
+
break;
|
|
59
|
+
case "bin":
|
|
60
|
+
result = Buffer.from(value ?? "");
|
|
61
|
+
break;
|
|
62
|
+
case "re":
|
|
63
|
+
result = new RegExp(value);
|
|
64
|
+
break;
|
|
65
|
+
case "jsonata":
|
|
66
|
+
case "flow":
|
|
67
|
+
case "global":
|
|
68
|
+
case "env":
|
|
69
|
+
case "cred":
|
|
70
|
+
result = void 0;
|
|
71
|
+
break;
|
|
72
|
+
case "node":
|
|
73
|
+
result = value;
|
|
74
|
+
break;
|
|
75
|
+
default:
|
|
76
|
+
result = value;
|
|
77
|
+
}
|
|
78
|
+
callback(null, result);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
callback(err, void 0);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
},
|
|
85
|
+
events: {
|
|
86
|
+
on: vi.fn(),
|
|
87
|
+
emit: vi.fn()
|
|
88
|
+
},
|
|
89
|
+
hooks: {
|
|
90
|
+
add: vi.fn(),
|
|
91
|
+
remove: vi.fn()
|
|
92
|
+
},
|
|
93
|
+
version: vi.fn(() => "0.0.0-test")
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function getProperty(obj, path) {
|
|
97
|
+
return path.split(".").reduce((acc, key) => acc?.[key], obj);
|
|
98
|
+
}
|
|
99
|
+
function createContextStore() {
|
|
100
|
+
const store = {};
|
|
101
|
+
return {
|
|
102
|
+
get: vi.fn(
|
|
103
|
+
(key, _store, cb) => cb(null, store[key])
|
|
104
|
+
),
|
|
105
|
+
set: vi.fn(
|
|
106
|
+
(key, value, _store, cb) => {
|
|
107
|
+
store[key] = value;
|
|
108
|
+
cb(null);
|
|
109
|
+
}
|
|
110
|
+
),
|
|
111
|
+
keys: vi.fn(
|
|
112
|
+
(_store, cb) => cb(null, Object.keys(store))
|
|
113
|
+
)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function createMockNodeRedNode(options = {}) {
|
|
117
|
+
const nodeCtx = createContextStore();
|
|
118
|
+
const flowCtx = createContextStore();
|
|
119
|
+
const globalCtx = createContextStore();
|
|
120
|
+
const context = {
|
|
121
|
+
...nodeCtx,
|
|
122
|
+
flow: flowCtx,
|
|
123
|
+
global: globalCtx
|
|
124
|
+
};
|
|
125
|
+
return {
|
|
126
|
+
id: options.id ?? `test-${Math.random().toString(36).slice(2, 10)}`,
|
|
127
|
+
type: options.type ?? "test-node",
|
|
128
|
+
name: options.name ?? "",
|
|
129
|
+
z: options.z ?? "flow-1",
|
|
130
|
+
x: 100,
|
|
131
|
+
y: 200,
|
|
132
|
+
g: void 0,
|
|
133
|
+
wires: options.wires ?? [[]],
|
|
134
|
+
credentials: options.credentials ?? {},
|
|
135
|
+
log: vi.fn(),
|
|
136
|
+
warn: vi.fn(),
|
|
137
|
+
error: vi.fn(),
|
|
138
|
+
on: vi.fn(),
|
|
139
|
+
send: vi.fn(),
|
|
140
|
+
status: vi.fn(),
|
|
141
|
+
updateWires: vi.fn(),
|
|
142
|
+
receive: vi.fn(),
|
|
143
|
+
context: vi.fn(() => context),
|
|
144
|
+
...options
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/test/index.ts
|
|
149
|
+
import { initValidator } from "@bonsae/nrg/server";
|
|
150
|
+
function buildConfig(NodeClass, userConfig = {}) {
|
|
151
|
+
const defaults = {};
|
|
152
|
+
if (NodeClass.configSchema?.properties) {
|
|
153
|
+
for (const [key, prop] of Object.entries(
|
|
154
|
+
NodeClass.configSchema.properties
|
|
155
|
+
)) {
|
|
156
|
+
if (prop.default !== void 0) {
|
|
157
|
+
defaults[key] = prop.default;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { ...defaults, ...userConfig };
|
|
162
|
+
}
|
|
163
|
+
function buildNodeRedNodes(configNodes) {
|
|
164
|
+
const nodes = {};
|
|
165
|
+
for (const [id, value] of Object.entries(configNodes)) {
|
|
166
|
+
if (value && typeof value === "object" && "id" in value) {
|
|
167
|
+
nodes[id] = { _node: value };
|
|
168
|
+
} else {
|
|
169
|
+
nodes[id] = value;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return nodes;
|
|
173
|
+
}
|
|
174
|
+
function attachHelpers(node, nodeRedNode) {
|
|
175
|
+
const sentMessages = [];
|
|
176
|
+
const statusCalls = [];
|
|
177
|
+
nodeRedNode.send.mockImplementation((msg) => {
|
|
178
|
+
sentMessages.push(msg);
|
|
179
|
+
});
|
|
180
|
+
nodeRedNode.status.mockImplementation((status) => {
|
|
181
|
+
statusCalls.push(status);
|
|
182
|
+
});
|
|
183
|
+
const nodeRef = node;
|
|
184
|
+
const helpers = {
|
|
185
|
+
async receive(msg) {
|
|
186
|
+
const sendFn = vi2.fn((outMsg) => {
|
|
187
|
+
nodeRedNode.send(outMsg);
|
|
188
|
+
});
|
|
189
|
+
await nodeRef._input(msg, sendFn);
|
|
190
|
+
},
|
|
191
|
+
async close(removed = false) {
|
|
192
|
+
await nodeRef._closed(removed);
|
|
193
|
+
},
|
|
194
|
+
reset() {
|
|
195
|
+
sentMessages.length = 0;
|
|
196
|
+
statusCalls.length = 0;
|
|
197
|
+
nodeRedNode.log.mockClear();
|
|
198
|
+
nodeRedNode.warn.mockClear();
|
|
199
|
+
nodeRedNode.error.mockClear();
|
|
200
|
+
},
|
|
201
|
+
sent(port) {
|
|
202
|
+
if (port === void 0) return [...sentMessages];
|
|
203
|
+
return sentMessages.map(
|
|
204
|
+
(msg) => Array.isArray(msg) ? msg[port] : port === 0 ? msg : void 0
|
|
205
|
+
).filter((msg) => msg != null);
|
|
206
|
+
},
|
|
207
|
+
statuses() {
|
|
208
|
+
return [...statusCalls];
|
|
209
|
+
},
|
|
210
|
+
logged(level) {
|
|
211
|
+
if (level) {
|
|
212
|
+
return nodeRedNode[level === "info" ? "log" : level].mock.calls.map(
|
|
213
|
+
(c) => c[0]
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return [
|
|
217
|
+
...nodeRedNode.log.mock.calls.map((c) => c[0]),
|
|
218
|
+
...nodeRedNode.warn.mock.calls.map((c) => c[0]),
|
|
219
|
+
...nodeRedNode.error.mock.calls.map((c) => c[0])
|
|
220
|
+
];
|
|
221
|
+
},
|
|
222
|
+
warned() {
|
|
223
|
+
return nodeRedNode.warn.mock.calls.map((c) => c[0]);
|
|
224
|
+
},
|
|
225
|
+
errored() {
|
|
226
|
+
return nodeRedNode.error.mock.calls.map((c) => c[0]);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
return Object.assign(node, helpers);
|
|
230
|
+
}
|
|
231
|
+
function isConfigNode(NodeClass) {
|
|
232
|
+
return NodeClass.category === "config";
|
|
233
|
+
}
|
|
234
|
+
async function createNode(NodeClass, options = {}) {
|
|
235
|
+
const {
|
|
236
|
+
config: userConfig = {},
|
|
237
|
+
credentials = {},
|
|
238
|
+
configNodes = {},
|
|
239
|
+
settings = {},
|
|
240
|
+
overrides: overrideOpts = {}
|
|
241
|
+
} = options;
|
|
242
|
+
const redNodes = buildNodeRedNodes(configNodes);
|
|
243
|
+
const RED = createMockRED({ nodes: redNodes, settings });
|
|
244
|
+
initValidator(RED);
|
|
245
|
+
const configDefaults = {
|
|
246
|
+
id: overrideOpts.id ?? `test-${Math.random().toString(36).slice(2, 10)}`,
|
|
247
|
+
type: NodeClass.type
|
|
248
|
+
};
|
|
249
|
+
if (isConfigNode(NodeClass)) {
|
|
250
|
+
configDefaults._users = [];
|
|
251
|
+
}
|
|
252
|
+
const config = buildConfig(NodeClass, {
|
|
253
|
+
...configDefaults,
|
|
254
|
+
...userConfig
|
|
255
|
+
});
|
|
256
|
+
const nodeRedNode = createMockNodeRedNode({
|
|
257
|
+
id: config.id,
|
|
258
|
+
type: NodeClass.type,
|
|
259
|
+
name: config.name ?? "",
|
|
260
|
+
credentials,
|
|
261
|
+
...overrideOpts
|
|
262
|
+
});
|
|
263
|
+
await Promise.resolve(
|
|
264
|
+
NodeClass._registered?.(RED) ?? NodeClass.registered?.(RED)
|
|
265
|
+
);
|
|
266
|
+
const node = new NodeClass(RED, nodeRedNode, config, credentials);
|
|
267
|
+
const augmented = attachHelpers(node, nodeRedNode);
|
|
268
|
+
await Promise.resolve(augmented.created?.());
|
|
269
|
+
return { node: augmented, RED };
|
|
270
|
+
}
|
|
271
|
+
export {
|
|
272
|
+
createNode
|
|
273
|
+
};
|
package/types/test.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
|
|
2
|
+
export interface CreateNodeOptions {
|
|
3
|
+
config?: Record<string, any>;
|
|
4
|
+
credentials?: Record<string, any>;
|
|
5
|
+
configNodes?: Record<string, any>;
|
|
6
|
+
settings?: Record<string, any>;
|
|
7
|
+
overrides?: Record<string, any>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type ExtractInput<T> = T extends { input(msg: infer I): any } ? I : any;
|
|
11
|
+
type ExtractOutput<T> = T extends { send(msg: infer O): any } ? O : any;
|
|
12
|
+
|
|
13
|
+
export interface TestNodeHelpers<TInput = any, TOutput = any> {
|
|
14
|
+
receive(msg: TInput): Promise<void>;
|
|
15
|
+
close(removed?: boolean): Promise<void>;
|
|
16
|
+
reset(): void;
|
|
17
|
+
sent(): TOutput[];
|
|
18
|
+
sent(port: number): any[];
|
|
19
|
+
statuses(): any[];
|
|
20
|
+
logged(level?: "info" | "warn" | "error" | "debug"): string[];
|
|
21
|
+
warned(): string[];
|
|
22
|
+
errored(): string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CreateNodeResult<T> {
|
|
26
|
+
node: T & TestNodeHelpers<ExtractInput<T>, ExtractOutput<T>>;
|
|
27
|
+
RED: any;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface NodeClass {
|
|
31
|
+
readonly type: string;
|
|
32
|
+
readonly category?: string;
|
|
33
|
+
readonly configSchema?: any;
|
|
34
|
+
registered?(RED: any): void | Promise<void>;
|
|
35
|
+
_registered?(RED: any): void | Promise<void>;
|
|
36
|
+
new (...args: any[]): any;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export declare function createNode<T extends NodeClass>(NodeClass: T, options?: CreateNodeOptions): Promise<CreateNodeResult<InstanceType<T>>>;
|