@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 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.9.1",
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
+ };
@@ -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>>>;