@5minds/node-red-contrib-processcube-tools 1.2.0-develop-d19f89-mg68thdf → 1.2.0-develop-59ef22-mg9d9ja5

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.
Files changed (55) hide show
  1. package/.mocharc.json +5 -0
  2. package/package.json +26 -10
  3. package/src/custom-node-template/custom-node-template.html.template +45 -0
  4. package/src/custom-node-template/custom-node-template.ts.template +69 -0
  5. package/src/email-receiver/email-receiver.ts +439 -0
  6. package/src/email-sender/email-sender.ts +210 -0
  7. package/{processcube-html-to-text/processcube-html-to-text.html → src/html-to-text/html-to-text.html} +3 -3
  8. package/src/html-to-text/html-to-text.ts +53 -0
  9. package/src/index.ts +12 -0
  10. package/src/interfaces/EmailReceiverMessage.ts +22 -0
  11. package/src/interfaces/EmailSenderNodeProperties.ts +37 -0
  12. package/src/interfaces/FetchState.ts +9 -0
  13. package/src/interfaces/ImapConnectionConfig.ts +14 -0
  14. package/src/test/framework/advanced-test-patterns.ts +224 -0
  15. package/src/test/framework/generic-node-test-suite.ts +58 -0
  16. package/src/test/framework/index.ts +17 -0
  17. package/src/test/framework/integration-assertions.ts +67 -0
  18. package/src/test/framework/integration-scenario-builder.ts +77 -0
  19. package/src/test/framework/integration-test-runner.ts +101 -0
  20. package/src/test/framework/node-assertions.ts +63 -0
  21. package/src/test/framework/node-test-runner.ts +260 -0
  22. package/src/test/framework/test-scenario-builder.ts +74 -0
  23. package/src/test/framework/types.ts +61 -0
  24. package/src/test/helpers/email-receiver-test-configs.ts +67 -0
  25. package/src/test/helpers/email-receiver-test-flows.ts +16 -0
  26. package/src/test/helpers/email-sender-test-configs.ts +123 -0
  27. package/src/test/helpers/email-sender-test-flows.ts +16 -0
  28. package/src/test/integration/email-receiver.integration.test.ts +41 -0
  29. package/src/test/integration/email-sender.integration.test.ts +129 -0
  30. package/src/test/interfaces/email-data.ts +10 -0
  31. package/src/test/interfaces/email-receiver-config.ts +12 -0
  32. package/src/test/interfaces/email-sender-config.ts +26 -0
  33. package/src/test/interfaces/imap-config.ts +9 -0
  34. package/src/test/interfaces/imap-mailbox.ts +5 -0
  35. package/src/test/interfaces/mail-options.ts +20 -0
  36. package/src/test/interfaces/parsed-email.ts +11 -0
  37. package/src/test/interfaces/send-mail-result.ts +7 -0
  38. package/src/test/mocks/imap-mock.ts +147 -0
  39. package/src/test/mocks/mailparser-mock.ts +82 -0
  40. package/src/test/mocks/nodemailer-mock.ts +118 -0
  41. package/src/test/unit/email-receiver.unit.test.ts +471 -0
  42. package/src/test/unit/email-sender.unit.test.ts +550 -0
  43. package/tsconfig.json +23 -0
  44. package/email-receiver/email-receiver.js +0 -304
  45. package/email-sender/email-sender.js +0 -178
  46. package/examples/.gitkeep +0 -0
  47. package/processcube-html-to-text/processcube-html-to-text.js +0 -22
  48. package/test/helpers/email-receiver.mocks.js +0 -447
  49. package/test/helpers/email-sender.mocks.js +0 -368
  50. package/test/integration/email-receiver.integration.test.js +0 -515
  51. package/test/integration/email-sender.integration.test.js +0 -239
  52. package/test/unit/email-receiver.unit.test.js +0 -304
  53. package/test/unit/email-sender.unit.test.js +0 -570
  54. /package/{email-receiver → src/email-receiver}/email-receiver.html +0 -0
  55. /package/{email-sender → src/email-sender}/email-sender.html +0 -0
@@ -0,0 +1,67 @@
1
+ import { expect } from 'chai';
2
+ import type { IntegrationTestContext } from './integration-test-runner';
3
+
4
+ export class IntegrationAssertions {
5
+ static expectNodeExists(context: IntegrationTestContext, nodeId: string): void {
6
+ expect(context.nodes[nodeId], `Node ${nodeId} should exist`).to.exist;
7
+ }
8
+
9
+ static expectNodeProperty(context: IntegrationTestContext, nodeId: string, property: string, value: any): void {
10
+ this.expectNodeExists(context, nodeId);
11
+ expect(context.nodes[nodeId], `Node ${nodeId} should have property ${property}`).to.have.property(
12
+ property,
13
+ value,
14
+ );
15
+ }
16
+
17
+ static expectMessageReceived(context: IntegrationTestContext, nodeId: string): void {
18
+ const messages = context.messages.filter((m) => m.nodeId === nodeId);
19
+ expect(messages, `Node ${nodeId} should have received at least one message`).to.have.length.greaterThan(0);
20
+ }
21
+
22
+ static expectMessageCount(context: IntegrationTestContext, nodeId: string, count: number): void {
23
+ const messages = context.messages.filter((m) => m.nodeId === nodeId);
24
+ expect(messages, `Node ${nodeId} should have received ${count} messages`).to.have.length(count);
25
+ }
26
+
27
+ static expectMessageContent(context: IntegrationTestContext, nodeId: string, expectedContent: any): void {
28
+ const messages = context.messages.filter((m) => m.nodeId === nodeId);
29
+ expect(messages, `Node ${nodeId} should have received messages`).to.have.length.greaterThan(0);
30
+
31
+ const lastMessage = messages[messages.length - 1].message;
32
+ if (typeof expectedContent === 'object') {
33
+ Object.keys(expectedContent).forEach((key) => {
34
+ expect(lastMessage, `Message should have property ${key}`).to.have.property(key);
35
+ if (expectedContent[key] !== undefined) {
36
+ expect(lastMessage[key], `Message property ${key} should match`).to.deep.equal(
37
+ expectedContent[key],
38
+ );
39
+ }
40
+ });
41
+ } else {
42
+ expect(lastMessage.payload).to.equal(expectedContent);
43
+ }
44
+ }
45
+
46
+ static expectNoMessages(context: IntegrationTestContext, nodeId: string): void {
47
+ const messages = context.messages.filter((m) => m.nodeId === nodeId);
48
+ expect(messages, `Node ${nodeId} should not have received any messages`).to.have.length(0);
49
+ }
50
+
51
+ static expectAllNodesExist(context: IntegrationTestContext, nodeIds: string[]): void {
52
+ nodeIds.forEach((nodeId) => {
53
+ this.expectNodeExists(context, nodeId);
54
+ });
55
+ }
56
+
57
+ static expectMessageOrder(context: IntegrationTestContext, nodeIds: string[]): void {
58
+ const sortedMessages = [...context.messages].sort((a, b) => a.timestamp - b.timestamp);
59
+
60
+ nodeIds.forEach((expectedNodeId, index) => {
61
+ expect(sortedMessages[index], `Message ${index} should be from node ${expectedNodeId}`).to.have.property(
62
+ 'nodeId',
63
+ expectedNodeId,
64
+ );
65
+ });
66
+ }
67
+ }
@@ -0,0 +1,77 @@
1
+ import { expect } from 'chai';
2
+ import type { IntegrationTestScenario } from './integration-test-runner';
3
+ import type { Node } from 'node-red';
4
+
5
+ export class IntegrationScenarioBuilder {
6
+ private scenarios: IntegrationTestScenario[] = [];
7
+
8
+ addScenario(scenario: IntegrationTestScenario): this {
9
+ this.scenarios.push(scenario);
10
+ return this;
11
+ }
12
+
13
+ addLoadingScenario(name: string, flow: any[], nodeId: string): this {
14
+ return this.addScenario({
15
+ name,
16
+ flow,
17
+ nodeId,
18
+ timeout: 2000,
19
+ });
20
+ }
21
+
22
+ addMessageFlowScenario(
23
+ name: string,
24
+ flow: any[],
25
+ sourceNodeId: string,
26
+ input: any,
27
+ expectedMessages: Array<{ nodeId: string; expectedMsg: any }>,
28
+ ): this {
29
+ return this.addScenario({
30
+ name,
31
+ flow,
32
+ nodeId: sourceNodeId,
33
+ input,
34
+ expectedMessages,
35
+ timeout: 3000,
36
+ });
37
+ }
38
+
39
+ addConnectionScenario(name: string, flow: any[], nodeIds: string[]): this {
40
+ return this.addScenario({
41
+ name,
42
+ flow,
43
+ nodeId: nodeIds[0], // Primary node
44
+ setup: (nodes) => {
45
+ // Verify all nodes are connected
46
+ nodeIds.forEach((nodeId) => {
47
+ expect(nodes[nodeId], `Node ${nodeId} should exist in connection test`).to.exist;
48
+ });
49
+ },
50
+ });
51
+ }
52
+
53
+ addLifecycleScenario(
54
+ name: string,
55
+ flow: any[],
56
+ nodeId: string,
57
+ operations: Array<(nodes: Record<string, Node>) => void>,
58
+ ): this {
59
+ return this.addScenario({
60
+ name,
61
+ flow,
62
+ nodeId,
63
+ setup: (nodes) => {
64
+ operations.forEach((operation) => operation(nodes));
65
+ },
66
+ });
67
+ }
68
+
69
+ getScenarios(): IntegrationTestScenario[] {
70
+ return [...this.scenarios];
71
+ }
72
+
73
+ clear(): this {
74
+ this.scenarios = [];
75
+ return this;
76
+ }
77
+ }
@@ -0,0 +1,101 @@
1
+ import type { Node, NodeMessageInFlow } from 'node-red';
2
+ import { NodeTestRunner } from './node-test-runner';
3
+ import type { TestScenario } from './types';
4
+
5
+ export interface IntegrationTestScenario {
6
+ name: string;
7
+ flow: any[];
8
+ nodeId: string;
9
+ input?: any;
10
+ expectedMessages?: Array<{
11
+ nodeId: string;
12
+ expectedMsg: any;
13
+ timeout?: number;
14
+ }>;
15
+ timeout?: number;
16
+ setup?: (nodes: Record<string, Node>) => void;
17
+ cleanup?: () => void;
18
+ }
19
+
20
+ export interface IntegrationTestContext {
21
+ nodes: Record<string, Node>;
22
+ messages: Array<{
23
+ nodeId: string;
24
+ message: NodeMessageInFlow;
25
+ timestamp: number;
26
+ }>;
27
+ errors: any[];
28
+ }
29
+
30
+ export class IntegrationTestRunner {
31
+ static async runIntegrationScenario(
32
+ nodeConstructor: Function,
33
+ scenario: IntegrationTestScenario,
34
+ ): Promise<IntegrationTestContext> {
35
+ console.log(`[SCENARIO START] ${scenario.name}`);
36
+
37
+ const context: IntegrationTestContext = {
38
+ nodes: {},
39
+ messages: [],
40
+ errors: [],
41
+ };
42
+
43
+ const mainNodeConfig = scenario.flow.find((n) => n.id === scenario.nodeId);
44
+
45
+ if (!mainNodeConfig) {
46
+ throw new Error(`Node with id ${scenario.nodeId} not found in flow`);
47
+ }
48
+
49
+ const testScenario: TestScenario = {
50
+ name: scenario.name,
51
+ config: mainNodeConfig,
52
+ input: scenario.input,
53
+ timeout: scenario.timeout || 5000,
54
+ };
55
+
56
+ if (scenario.expectedMessages && scenario.expectedMessages.length > 0) {
57
+ testScenario.expectedOutput = scenario.expectedMessages[0].expectedMsg;
58
+ }
59
+
60
+ const testContext = await NodeTestRunner.runScenario(nodeConstructor, testScenario, {
61
+ sendHandler: function (msg: any) {
62
+ // When send is called with array (multi-output), handle each output
63
+ if (Array.isArray(msg)) {
64
+ msg.forEach((m, index) => {
65
+ if (scenario.expectedMessages && scenario.expectedMessages[index]) {
66
+ context.messages.push({
67
+ nodeId: scenario.expectedMessages[index].nodeId,
68
+ message: m,
69
+ timestamp: Date.now(),
70
+ });
71
+ }
72
+ });
73
+ } else {
74
+ // Single message - use first expected message nodeId if available
75
+ const nodeId =
76
+ scenario.expectedMessages && scenario.expectedMessages[0]
77
+ ? scenario.expectedMessages[0].nodeId
78
+ : scenario.nodeId;
79
+
80
+ context.messages.push({
81
+ nodeId: nodeId,
82
+ message: msg,
83
+ timestamp: Date.now(),
84
+ });
85
+ }
86
+ },
87
+ errorHandler: function (error: any) {
88
+ context.errors.push(error);
89
+ },
90
+ });
91
+
92
+ context.nodes[scenario.nodeId] = testContext.nodeInstance;
93
+
94
+ if (scenario.setup) {
95
+ scenario.setup(context.nodes);
96
+ }
97
+
98
+ console.log(`[SCENARIO COMPLETE] ${scenario.name}`);
99
+ return context;
100
+ }
101
+ }
@@ -0,0 +1,63 @@
1
+ import { expect } from 'chai';
2
+ import type { TestContext } from './types';
3
+
4
+ export class NodeAssertions {
5
+ static expectMessage(context: TestContext, expectedMsg: any): void {
6
+ expect(context.messages, 'No messages were sent').to.have.length.greaterThan(0);
7
+ const lastMessage = context.messages[context.messages.length - 1];
8
+
9
+ if (typeof expectedMsg === 'object' && expectedMsg !== null) {
10
+ Object.keys(expectedMsg).forEach((key) => {
11
+ expect(lastMessage, `Message missing property: ${key}`).to.have.property(key);
12
+ if (expectedMsg[key] !== undefined) {
13
+ expect(lastMessage[key], `Message property ${key} does not match`).to.deep.equal(expectedMsg[key]);
14
+ }
15
+ });
16
+ } else {
17
+ expect(lastMessage, 'Message does not match expected value').to.deep.equal(expectedMsg);
18
+ }
19
+ }
20
+
21
+ static expectError(context: TestContext, expectedError: string | RegExp): void {
22
+ expect(context.errors, 'No errors were recorded').to.have.length.greaterThan(0);
23
+ const error = context.errors[context.errors.length - 1];
24
+ const errorMessage = error?.message || error?.toString() || '';
25
+
26
+ if (typeof expectedError === 'string') {
27
+ expect(errorMessage, 'Error message does not contain expected text').to.include(expectedError);
28
+ } else {
29
+ expect(errorMessage, 'Error message does not match expected pattern').to.match(expectedError);
30
+ }
31
+ }
32
+
33
+ static expectStatus(context: TestContext, expectedStatus: { fill: string; text?: string }): void {
34
+ expect(context.statuses, 'No status updates were recorded').to.have.length.greaterThan(0);
35
+ const status = context.statuses[context.statuses.length - 1];
36
+
37
+ expect(status.fill, 'Status fill color does not match').to.equal(expectedStatus.fill);
38
+ if (expectedStatus.text) {
39
+ expect(status.text, 'Status text does not match').to.include(expectedStatus.text);
40
+ }
41
+ }
42
+
43
+ static expectNodeProperty(context: TestContext, property: string, value: any): void {
44
+ expect(context.nodeInstance, 'Node instance does not exist').to.exist;
45
+ expect(context.nodeInstance, `Node missing property: ${property}`).to.have.property(property, value);
46
+ }
47
+
48
+ static expectNoErrors(context: TestContext): void {
49
+ expect(context.errors, 'Unexpected errors occurred').to.have.length(0);
50
+ }
51
+
52
+ static expectNoMessages(context: TestContext): void {
53
+ expect(context.messages, 'Unexpected messages were sent').to.have.length(0);
54
+ }
55
+
56
+ static expectMessageCount(context: TestContext, count: number): void {
57
+ expect(context.messages, `Expected ${count} messages`).to.have.length(count);
58
+ }
59
+
60
+ static expectStatusCount(context: TestContext, count: number): void {
61
+ expect(context.statuses, `Expected ${count} status updates`).to.have.length(count);
62
+ }
63
+ }
@@ -0,0 +1,260 @@
1
+ import type { NodeAPI } from 'node-red';
2
+ import type { TestScenario, TestContext, MockNodeREDOptions } from './types';
3
+
4
+ // Minimal extension for dependency injection
5
+ export interface DependencyContainer {
6
+ [key: string]: any;
7
+ }
8
+
9
+ export interface EnhancedMockNodeREDOptions extends MockNodeREDOptions {
10
+ dependencies?: DependencyContainer;
11
+ }
12
+
13
+ export class NodeTestRunner {
14
+ private static createMockNodeRED(context: TestContext, options: EnhancedMockNodeREDOptions = {}): any {
15
+ return {
16
+ nodes: {
17
+ createNode: function (node: any, config: any) {
18
+ Object.assign(node, config);
19
+
20
+ // Inject dependencies if available
21
+ if (options.dependencies) {
22
+ Object.keys(options.dependencies).forEach((key) => {
23
+ if (!node[key]) {
24
+ node[key] = options.dependencies![key];
25
+ }
26
+ });
27
+ }
28
+
29
+ // Set up event handlers
30
+ node.on =
31
+ options.onHandler ||
32
+ function (event: string, callback: Function) {
33
+ if (event === 'input') {
34
+ (node as any).inputCallback = callback;
35
+ }
36
+ };
37
+
38
+ // Override node methods to capture calls
39
+ node.send = function (msg: any) {
40
+ context.messages.push(msg);
41
+ if (options.sendHandler) {
42
+ options.sendHandler.call(this, msg);
43
+ }
44
+ };
45
+
46
+ node.error = function (error: any) {
47
+ context.errors.push(error);
48
+ if (options.errorHandler) {
49
+ options.errorHandler.call(this, error);
50
+ }
51
+ };
52
+
53
+ node.status = function (status: any) {
54
+ context.statuses.push(status);
55
+ if (options.statusHandler) {
56
+ options.statusHandler.call(this, status);
57
+ }
58
+ };
59
+
60
+ // Add logging methods
61
+ node.log = function (msg: any) {
62
+ if (options.logHandler) {
63
+ options.logHandler.call(this, msg);
64
+ }
65
+ // Optionally store logs in context
66
+ if (!context.logs) context.logs = [];
67
+ context.logs.push(msg);
68
+ };
69
+
70
+ node.warn = function (msg: any) {
71
+ if (options.warnHandler) {
72
+ options.warnHandler.call(this, msg);
73
+ }
74
+ if (!context.warnings) context.warnings = [];
75
+ context.warnings.push(msg);
76
+ };
77
+
78
+ node.debug = function (msg: any) {
79
+ if (options.debugHandler) {
80
+ options.debugHandler.call(this, msg);
81
+ }
82
+ if (!context.debugs) context.debugs = [];
83
+ context.debugs.push(msg);
84
+ };
85
+
86
+ node.trace = function (msg: any) {
87
+ if (options.traceHandler) {
88
+ options.traceHandler.call(this, msg);
89
+ }
90
+ };
91
+
92
+ return node;
93
+ },
94
+ registerType: function (type: string, constructor: Function) {
95
+ (this as any).lastRegisteredType = type;
96
+ (this as any).lastRegisteredConstructor = constructor;
97
+ },
98
+ },
99
+ util: {
100
+ evaluateNodeProperty: (value: any, type: string, node: any, msg: any): any => {
101
+ switch (type) {
102
+ case 'str':
103
+ return String(value);
104
+ case 'num':
105
+ return Number(value);
106
+ case 'bool':
107
+ return Boolean(value);
108
+ case 'json':
109
+ try {
110
+ return typeof value === 'string' ? JSON.parse(value) : value;
111
+ } catch {
112
+ return value;
113
+ }
114
+ case 'msg':
115
+ const keys = value.split('.');
116
+ let result = msg;
117
+ for (const key of keys) {
118
+ result = result?.[key];
119
+ }
120
+ return result;
121
+ case 'flow':
122
+ case 'global':
123
+ return value;
124
+ default:
125
+ return value;
126
+ }
127
+ },
128
+ },
129
+ };
130
+ }
131
+
132
+ private static createTestContext(options: EnhancedMockNodeREDOptions = {}): TestContext {
133
+ const context: TestContext = {
134
+ mockRED: null,
135
+ nodeInstance: null,
136
+ messages: [],
137
+ errors: [],
138
+ statuses: [],
139
+ };
140
+
141
+ context.mockRED = this.createMockNodeRED(context, options);
142
+ return context;
143
+ }
144
+
145
+ static async runScenario(
146
+ nodeConstructorFn: Function,
147
+ scenario: TestScenario,
148
+ mockOptions: EnhancedMockNodeREDOptions = {},
149
+ ): Promise<TestContext> {
150
+ const context = this.createTestContext(mockOptions);
151
+
152
+ return new Promise((resolve, reject) => {
153
+ let completed = false;
154
+
155
+ const timeout = setTimeout(() => {
156
+ if (!completed) {
157
+ reject(new Error(`Test scenario '${scenario.name}' timed out after ${scenario.timeout || 5000}ms`));
158
+ }
159
+ }, scenario.timeout || 5000);
160
+
161
+ const complete = () => {
162
+ if (completed) return;
163
+ completed = true;
164
+ clearTimeout(timeout);
165
+ // Small delay to catch any additional events
166
+ setTimeout(() => resolve(context), 50);
167
+ };
168
+
169
+ try {
170
+ // Register the node - pass dependencies as second parameter if available
171
+ if (mockOptions.dependencies) {
172
+ nodeConstructorFn(context.mockRED as unknown as NodeAPI, mockOptions.dependencies);
173
+ } else {
174
+ nodeConstructorFn(context.mockRED as unknown as NodeAPI);
175
+ }
176
+
177
+ // Create node instance
178
+ const NodeConstructor = (context.mockRED.nodes as any).lastRegisteredConstructor;
179
+ context.nodeInstance = new NodeConstructor(scenario.config);
180
+
181
+ if ((context.nodeInstance as any).configError) {
182
+ setTimeout(() => {
183
+ if (scenario.expectedError && context.errors.length > 0) {
184
+ complete();
185
+ } else if (scenario.expectedStatus && context.statuses.length > 0) {
186
+ complete();
187
+ }
188
+ }, 100);
189
+ return;
190
+ }
191
+
192
+ // Set up completion detection based on scenario configuration
193
+ const originalSend = context.nodeInstance.send;
194
+ const originalError = context.nodeInstance.error;
195
+ const originalStatus = context.nodeInstance.status;
196
+
197
+ // Override send to detect expected output
198
+ if (scenario.expectedOutput) {
199
+ context.nodeInstance.send = function (msg: any) {
200
+ originalSend.call(this, msg);
201
+ complete();
202
+ };
203
+ }
204
+
205
+ // Override error to detect expected errors
206
+ if (scenario.expectedError) {
207
+ let errorCount = context.errors.length;
208
+ context.nodeInstance.error = function (error: any) {
209
+ originalError.call(this, error);
210
+ // ✅ Only complete on NEW errors (not construction errors we already saw)
211
+ if (context.errors.length > errorCount) {
212
+ errorCount = context.errors.length;
213
+ complete();
214
+ }
215
+ };
216
+ }
217
+
218
+ // Override status to detect completion patterns
219
+ if (scenario.expectedStatus) {
220
+ let statusCount = context.statuses.length;
221
+ context.nodeInstance.status = function (status: any) {
222
+ originalStatus.call(this, status);
223
+
224
+ // ✅ Only check NEW statuses
225
+ if (context.statuses.length > statusCount) {
226
+ statusCount = context.statuses.length;
227
+
228
+ const fillMatches =
229
+ !scenario.expectedStatus!.fill || status.fill === scenario.expectedStatus!.fill;
230
+ const shapeMatches =
231
+ !scenario.expectedStatus!.shape || status.shape === scenario.expectedStatus!.shape;
232
+ const textMatches =
233
+ !scenario.expectedStatus!.text || status.text?.includes(scenario.expectedStatus!.text);
234
+
235
+ if (fillMatches && shapeMatches && textMatches) {
236
+ complete();
237
+ }
238
+ }
239
+ };
240
+ }
241
+
242
+ // If no specific expectations, complete after a short delay
243
+ if (!scenario.expectedOutput && !scenario.expectedError && !scenario.expectedStatus) {
244
+ setTimeout(() => complete(), 100);
245
+ return;
246
+ }
247
+
248
+ // Trigger input if provided
249
+ if (scenario.input && (context.nodeInstance as any).inputCallback) {
250
+ setTimeout(() => {
251
+ (context.nodeInstance as any).inputCallback(scenario.input);
252
+ }, 10);
253
+ }
254
+ } catch (error) {
255
+ clearTimeout(timeout);
256
+ reject(error);
257
+ }
258
+ });
259
+ }
260
+ }
@@ -0,0 +1,74 @@
1
+ import { EnhancedMockNodeREDOptions } from './node-test-runner';
2
+ import type { TestScenario } from './types';
3
+
4
+ export class TestScenarioBuilder {
5
+ private scenarios: TestScenario[] = [];
6
+
7
+ addScenario(scenario: TestScenario): this {
8
+ this.scenarios.push(scenario);
9
+ return this;
10
+ }
11
+
12
+ addValidScenario(
13
+ name: string,
14
+ config: any,
15
+ input?: any,
16
+ expectedOutput?: any,
17
+ mockOptions?: Partial<EnhancedMockNodeREDOptions>,
18
+ ): this {
19
+ return this.addScenario({
20
+ name,
21
+ config,
22
+ input,
23
+ expectedOutput,
24
+ mockOptions,
25
+ });
26
+ }
27
+
28
+ addErrorScenario(
29
+ name: string,
30
+ config: any,
31
+ expectedError: any,
32
+ input?: any,
33
+ mockOptions?: Partial<EnhancedMockNodeREDOptions>,
34
+ ): this {
35
+ this.scenarios.push({
36
+ name,
37
+ config,
38
+ input,
39
+ expectedError,
40
+ mockOptions,
41
+ });
42
+ return this;
43
+ }
44
+
45
+ addStatusScenario(
46
+ name: string,
47
+ config: any,
48
+ expectedStatus: any,
49
+ input?: any,
50
+ mockOptions?: Partial<EnhancedMockNodeREDOptions>, // Accept full mock options
51
+ ): this {
52
+ this.scenarios.push({
53
+ name,
54
+ config,
55
+ input,
56
+ expectedStatus,
57
+ mockOptions,
58
+ });
59
+ return this;
60
+ }
61
+
62
+ addCustomScenario(scenario: Partial<TestScenario> & { name: string; config: any }): this {
63
+ return this.addScenario(scenario as TestScenario);
64
+ }
65
+
66
+ getScenarios(): TestScenario[] {
67
+ return [...this.scenarios]; // Return copy to prevent mutation
68
+ }
69
+
70
+ clear(): this {
71
+ this.scenarios = [];
72
+ return this;
73
+ }
74
+ }