node-red-contrib-event-calc 0.1.4 → 2.0.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/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ Personal Use License
2
+
3
+ Copyright (c) 2025 Holger Amort
4
+
5
+ This software is provided for personal, non-commercial use only.
6
+
7
+ You may:
8
+ - Use this software for personal projects
9
+ - Modify the software for your own use
10
+ - Share the software with attribution
11
+
12
+ You may not:
13
+ - Use this software for commercial purposes without permission
14
+ - Sell or sublicense this software
15
+ - Remove this license notice
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
24
+
25
+ For commercial licensing inquiries, contact the author.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # node-red-contrib-event-calc
2
2
 
3
- Node-RED nodes for event caching and calculations with topic wildcard patterns.
3
+ Node-RED nodes for event caching and streaming calculations with a local pub/sub event hub.
4
4
 
5
5
  ## Overview
6
6
 
7
- This package provides a central cache for event/streaming data values with reactive updates. It enables subscription to topic patterns and calculations when values change, making it easy to build event-driven data processing flows.
7
+ This package provides a local in-memory event hub with topic-based publish/subscribe and latest-value caching for reactive data flows within Node-RED. Stream data from MQTT, OPC-UA, or any source, then perform calculations that trigger automatically when values update.
8
8
 
9
9
  ## Architecture
10
10
 
@@ -13,14 +13,14 @@ This package provides a central cache for event/streaming data values with react
13
13
  │ event-cache (config node) │
14
14
  │ • Stores: Map<topic, {value, ts, metadata}> │
15
15
  │ • Event emitter for topic updates │
16
- │ • Wildcard pattern matching
16
+ │ • LRU eviction, optional TTL
17
17
  └──────────────────────────────────────────────────────────────┘
18
18
  │ │ │
19
19
  ┌────▼────┐ ┌────▼────┐ ┌────▼────┐
20
20
  │event-in │ │event- │ │event- │
21
21
  │ │ │topic │ │calc │
22
22
  │ pushes │ │subscribes│ │multi-sub│
23
- │to cache │ │to pattern│ │+ expr │
23
+ │to cache │ │to topic │ │+ expr │
24
24
  └─────────┘ └─────────┘ └─────────┘
25
25
  ```
26
26
 
@@ -53,30 +53,30 @@ The original message passes through, allowing insertion into existing flows.
53
53
 
54
54
  ### event-topic
55
55
 
56
- Subscribes to a topic pattern and outputs when matching topics update.
56
+ Subscribes to a topic and outputs when that topic updates.
57
57
 
58
58
  **Properties:**
59
- - **Topic Pattern**: Pattern with wildcards (`?` for single level, `*` for any levels)
59
+ - **Topic**: Exact topic to subscribe to
60
60
  - **Output Format**:
61
61
  - *Value only*: `msg.payload` = value
62
62
  - *Full entry*: `msg.payload` = `{value, ts, metadata}`
63
- - *All matching*: `msg.payload` = `{topic1: value1, topic2: value2, ...}`
64
63
  - **Output on deploy**: Emit cached values when flow starts
65
64
 
66
65
  **Dynamic control via input:**
67
- - `msg.pattern`: Change subscription pattern
68
- - `msg.payload = 'refresh'`: Output all currently cached values
66
+ - `msg.topic`: Change subscription topic
67
+ - `msg.payload = 'refresh'`: Output current cached value
69
68
 
70
69
  ### event-calc
71
70
 
72
71
  Subscribes to multiple topics and evaluates an expression when values update.
73
72
 
74
73
  **Properties:**
75
- - **Input Variables**: Map variable names to topic patterns
74
+ - **Input Variables**: Map variable names to topics
76
75
  - **Expression**: JavaScript expression using the variables
77
76
  - **Trigger**: When to calculate
78
77
  - *Any input updates*: Calculate on every update
79
78
  - *Only when all inputs have values*: Wait for all values
79
+ - **External Trigger**: When enabled, any incoming message triggers calculation using cached values
80
80
 
81
81
  **Output:**
82
82
  ```json
@@ -92,19 +92,27 @@ Subscribes to multiple topics and evaluates an expression when values update.
92
92
  }
93
93
  ```
94
94
 
95
- ## Wildcard Patterns
95
+ ### event-json
96
96
 
97
- Two wildcards are supported:
97
+ Bidirectional JSON envelope converter for MQTT messaging.
98
98
 
99
- | Pattern | Matches | Doesn't Match |
100
- |---------|---------|---------------|
101
- | `sensor?` | `sensor1`, `sensorA` | `sensor`, `sensor12` |
102
- | `sensors/*` | `sensors/temp`, `sensors/room1/temp` | `sensors` (nothing after /) |
103
- | `*/temp` | `room/temp`, `sensors/temp` | `temp` (nothing before /) |
104
- | `*` | Any topic with 1+ chars | Empty string |
99
+ **Behavior:**
100
+ - **Unwrap**: If payload is `{value, topic?, timestamp?}`, extracts to msg properties
101
+ - **Wrap**: If payload is any other value, wraps as `{timestamp, topic, value}`
105
102
 
106
- - `?` matches exactly one character
107
- - `*` matches one or more characters
103
+ **Usage:**
104
+ ```
105
+ [MQTT in] → [event-json] → [event-in] (unwrap JSON from broker)
106
+ [event-topic] → [event-json] → [MQTT out] (wrap for broker)
107
+ ```
108
+
109
+ ### event-simulator
110
+
111
+ Generates simulated data for testing. Supports sine waves, random values, and ramps.
112
+
113
+ ### event-chart
114
+
115
+ Real-time charting node for visualizing cached event data.
108
116
 
109
117
  ## Examples
110
118
 
@@ -121,13 +129,21 @@ Two wildcards are supported:
121
129
  trigger: all
122
130
  ```
123
131
 
124
- ### Monitor All Sensors
132
+ ### Time-based Calculations (External Trigger)
125
133
 
126
134
  ```
127
- [any-input: sensors/*] → [event-in] → [cache]
135
+ [inject: every 1 min] → [event-calc (external trigger)] → [MQTT out]
136
+ inputs: a = sensors/power
137
+ b = sensors/voltage
138
+ expression: a * b
139
+ ```
140
+
141
+ ### MQTT Round-trip with JSON Envelope
128
142
 
129
- [event-topic: sensors/*] → [debug]
130
- outputFormat: all
143
+ ```
144
+ [MQTT in] → [event-json] → [event-in] → [cache]
145
+
146
+ [event-calc] → [event-json] → [MQTT out]
131
147
  ```
132
148
 
133
149
  ### Calculate Power (Voltage × Current)
@@ -188,27 +204,6 @@ Two wildcards are supported:
188
204
  | `ifelse(a > b, 'high', 'low')` | Conditional |
189
205
  | `pctChange(a, b)` | % change from b to a |
190
206
 
191
- ## Scalability
192
-
193
- The event-cache is optimized for high subscriber counts:
194
-
195
- | Subscription Type | Lookup Complexity | Best For |
196
- |-------------------|-------------------|----------|
197
- | Exact topic (e.g., `sensors/room1/temp`) | O(1) | High-frequency updates, many subscribers |
198
- | Wildcard pattern (e.g., `sensors/*`) | O(w) | Flexible matching, fewer patterns |
199
-
200
- Where `w` = number of wildcard subscriptions (typically much smaller than total subscribers).
201
-
202
- **Example performance:**
203
- - 1000 exact subscriptions to different topics: O(1) per update
204
- - 10 wildcard patterns + 1000 exact subscriptions: O(10) per update
205
- - Pure wildcard subscriptions: O(n) per update
206
-
207
- **Recommendations for high scale:**
208
- - Prefer exact topic matches when possible
209
- - Use wildcards sparingly for monitoring/logging
210
- - Check stats endpoint: `GET /event-cache/:id/stats`
211
-
212
207
  ## API (for custom nodes)
213
208
 
214
209
  The event-cache node exposes methods for programmatic access:
@@ -223,12 +218,8 @@ cache.setValue('topic/path', 42, { source: 'sensor' });
223
218
  const entry = cache.getValue('topic/path');
224
219
  // { value: 42, ts: 1704000000000, metadata: { source: 'sensor' } }
225
220
 
226
- // Get matching values
227
- const temps = cache.getMatching('sensors/*');
228
- // Map { 'sensors/room1/temp' => {...}, 'sensors/room2/temp' => {...} }
229
-
230
221
  // Subscribe to updates
231
- const subId = cache.subscribe('sensors/*', (topic, entry) => {
222
+ const subId = cache.subscribe('sensors/room1/temp', (topic, entry) => {
232
223
  console.log(`${topic} = ${entry.value}`);
233
224
  });
234
225
 
@@ -242,6 +233,14 @@ const topics = cache.getTopics();
242
233
  cache.clear();
243
234
  ```
244
235
 
236
+ ## HTTP Admin Endpoints
237
+
238
+ ```
239
+ GET /event-cache/:id/stats - Cache statistics
240
+ GET /event-cache/:id/topics - List all topics
241
+ POST /event-cache/:id/clear - Clear cache
242
+ ```
243
+
245
244
  ## License
246
245
 
247
- MIT
246
+ Personal Use License - See [LICENSE](LICENSE) file.
@@ -13,7 +13,8 @@
13
13
  inputMappings: { value: [] },
14
14
  expression: { value: "" },
15
15
  triggerOn: { value: "any" },
16
- outputTopic: { value: "calc/result" }
16
+ outputTopic: { value: "calc/result" },
17
+ externalTrigger: { value: false }
17
18
  },
18
19
  inputs: 1,
19
20
  outputs: 2,
@@ -213,6 +214,11 @@
213
214
  <label for="node-input-outputTopic"><i class="fa fa-bookmark"></i> Output Topic</label>
214
215
  <input type="text" id="node-input-outputTopic" placeholder="calc/result">
215
216
  </div>
217
+ <div class="form-row">
218
+ <label>&nbsp;</label>
219
+ <input type="checkbox" id="node-input-externalTrigger" style="width:auto; margin-right:5px;">
220
+ <label for="node-input-externalTrigger" style="width:auto;"> External Trigger - calculate on any input message</label>
221
+ </div>
216
222
  </script>
217
223
 
218
224
  <script type="text/html" data-help-name="event-calc">
@@ -233,6 +239,8 @@
233
239
  </dd>
234
240
  <dt>Output Topic</dt>
235
241
  <dd>Topic for output messages.</dd>
242
+ <dt>External Trigger</dt>
243
+ <dd>When enabled, any incoming message will trigger a calculation using the current cached values. Useful for time-based or event-driven calculations.</dd>
236
244
  </dl>
237
245
 
238
246
  <h3>Inputs</h3>
@@ -66,6 +66,7 @@ module.exports = function(RED) {
66
66
  node.expression = config.expression || '';
67
67
  node.triggerOn = config.triggerOn || 'any';
68
68
  node.outputTopic = config.outputTopic || 'calc/result';
69
+ node.externalTrigger = config.externalTrigger || false;
69
70
 
70
71
  const subscriptionIds = [];
71
72
 
@@ -239,6 +240,14 @@ module.exports = function(RED) {
239
240
  node.status({ fill: "blue", shape: "dot", text: "expr updated" });
240
241
  }
241
242
 
243
+ // External trigger: any incoming message triggers calculation
244
+ if (node.externalTrigger) {
245
+ const triggerSource = msg.topic || '_external';
246
+ tryCalculate(triggerSource, latestValues);
247
+ done();
248
+ return;
249
+ }
250
+
242
251
  // Force recalculation (use special topic to bypass self-output check)
243
252
  if (msg.payload === 'recalc' || msg.topic === 'recalc') {
244
253
  if (latestValues.size > 0) {
@@ -0,0 +1,58 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('event-json', {
3
+ category: 'event calc',
4
+ color: '#758467',
5
+ defaults: {
6
+ name: { value: "" }
7
+ },
8
+ inputs: 1,
9
+ outputs: 1,
10
+ icon: "font-awesome/fa-exchange",
11
+ label: function() {
12
+ return this.name || "event json";
13
+ },
14
+ paletteLabel: "event json"
15
+ });
16
+ </script>
17
+
18
+ <script type="text/html" data-template-name="event-json">
19
+ <div class="form-row">
20
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
21
+ <input type="text" id="node-input-name" placeholder="Name">
22
+ </div>
23
+ </script>
24
+
25
+ <script type="text/html" data-help-name="event-json">
26
+ <p>Bidirectional JSON envelope converter. Automatically detects direction.</p>
27
+
28
+ <h3>Behavior</h3>
29
+ <dl class="message-properties">
30
+ <dt>Unwrap (JSON to Message)</dt>
31
+ <dd>If payload is <code>{value, topic?, timestamp?}</code>, extracts value to <code>msg.payload</code> and copies topic/timestamp to msg.</dd>
32
+ <dt>Wrap (Message to JSON)</dt>
33
+ <dd>If payload is any other value, wraps it as <code>{timestamp, topic, value}</code>.</dd>
34
+ </dl>
35
+
36
+ <h3>Examples</h3>
37
+ <h4>Wrap (before MQTT publish)</h4>
38
+ <pre>
39
+ Input: msg.topic = "sensor/temp", msg.payload = 25.5
40
+ Output: msg.payload = {
41
+ timestamp: 1704900000000,
42
+ topic: "sensor/temp",
43
+ value: 25.5
44
+ }</pre>
45
+
46
+ <h4>Unwrap (after MQTT subscribe)</h4>
47
+ <pre>
48
+ Input: msg.payload = '{"timestamp":1704900000000,"topic":"sensor/temp","value":25.5}'
49
+ Output: msg.topic = "sensor/temp"
50
+ msg.payload = 25.5
51
+ msg.timestamp = 1704900000000</pre>
52
+
53
+ <h3>Status</h3>
54
+ <ul>
55
+ <li><b>Green "wrapped"</b> - Created JSON envelope</li>
56
+ <li><b>Blue "unwrapped"</b> - Extracted from JSON envelope</li>
57
+ </ul>
58
+ </script>
@@ -0,0 +1,69 @@
1
+ /**
2
+ * event-json - Bidirectional JSON envelope converter
3
+ *
4
+ * Automatically detects direction:
5
+ * - Object with {value, topic?, timestamp?} -> extracts to msg
6
+ * - Any other payload -> wraps in {timestamp, topic, value}
7
+ */
8
+ module.exports = function(RED) {
9
+ function EventJsonNode(config) {
10
+ RED.nodes.createNode(this, config);
11
+ const node = this;
12
+
13
+ node.on('input', function(msg, send, done) {
14
+ send = send || function() { node.send.apply(node, arguments); };
15
+ done = done || function(err) { if (err) node.error(err, msg); };
16
+
17
+ try {
18
+ let data = msg.payload;
19
+
20
+ // If string, try to parse as JSON
21
+ if (typeof data === 'string') {
22
+ try {
23
+ data = JSON.parse(data);
24
+ } catch (e) {
25
+ // Not JSON string - wrap it
26
+ msg.payload = {
27
+ timestamp: Date.now(),
28
+ topic: msg.topic,
29
+ value: msg.payload
30
+ };
31
+ node.status({ fill: "green", shape: "dot", text: "wrapped" });
32
+ send(msg);
33
+ done();
34
+ return;
35
+ }
36
+ }
37
+
38
+ // Check if it's an envelope object (has 'value' property)
39
+ if (typeof data === 'object' && data !== null && 'value' in data) {
40
+ // Unwrap: extract from envelope
41
+ if (data.topic) {
42
+ msg.topic = data.topic;
43
+ }
44
+ if (data.timestamp) {
45
+ msg.timestamp = data.timestamp;
46
+ }
47
+ msg.payload = data.value;
48
+ node.status({ fill: "blue", shape: "dot", text: "unwrapped" });
49
+ } else {
50
+ // Wrap: create envelope
51
+ msg.payload = {
52
+ timestamp: Date.now(),
53
+ topic: msg.topic,
54
+ value: data
55
+ };
56
+ node.status({ fill: "green", shape: "dot", text: "wrapped" });
57
+ }
58
+
59
+ send(msg);
60
+ done();
61
+ } catch (err) {
62
+ node.status({ fill: "red", shape: "ring", text: "error" });
63
+ done(err);
64
+ }
65
+ });
66
+ }
67
+
68
+ RED.nodes.registerType("event-json", EventJsonNode);
69
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "node-red-contrib-event-calc",
3
- "version": "0.1.4",
4
- "description": "Node-RED nodes for event caching and calculations with topic wildcard patterns",
3
+ "version": "2.0.1",
4
+ "description": "Node-RED nodes for event caching and calculations",
5
5
  "author": {
6
6
  "name": "Holger Amort"
7
7
  },
@@ -9,19 +9,18 @@
9
9
  "node-red",
10
10
  "events",
11
11
  "cache",
12
- "wildcards",
13
12
  "reactive",
14
13
  "calculation",
15
14
  "streaming"
16
15
  ],
17
- "license": "MIT",
18
- "homepage": "https://github.com/ErnstHolger/node-red#readme",
16
+ "license": "SEE LICENSE IN LICENSE",
17
+ "homepage": "https://github.com/ErnstHolger/node-red-contrib-event-calc#readme",
19
18
  "bugs": {
20
- "url": "https://github.com/ErnstHolger/node-red/issues"
19
+ "url": "https://github.com/ErnstHolger/node-red-contrib-event-calc/issues"
21
20
  },
22
21
  "repository": {
23
22
  "type": "git",
24
- "url": "git+https://github.com/ErnstHolger/node-red.git"
23
+ "url": "git+https://github.com/ErnstHolger/node-red-contrib-event-calc.git"
25
24
  },
26
25
  "node-red": {
27
26
  "version": ">=2.0.0",
@@ -30,6 +29,7 @@
30
29
  "event-in": "nodes/event-in.js",
31
30
  "event-topic": "nodes/event-topic.js",
32
31
  "event-calc": "nodes/event-calc.js",
32
+ "event-json": "nodes/event-json.js",
33
33
  "event-simulator": "nodes/event-simulator.js",
34
34
  "event-chart": "nodes/event-chart.js"
35
35
  }
@@ -38,7 +38,19 @@
38
38
  "node": ">=18.0.0"
39
39
  },
40
40
  "scripts": {
41
- "publish:dry": "npm publish --dry-run"
41
+ "deploy": "npm link",
42
+ "deploy:patch": "npm version patch --no-git-tag-version && npm link",
43
+ "deploy:minor": "npm version minor --no-git-tag-version && npm link",
44
+ "deploy:major": "npm version major --no-git-tag-version && npm link",
45
+ "publish:dry": "npm publish --dry-run",
46
+ "publish:patch": "npm version patch && npm publish && start https://flows.nodered.org/add/node",
47
+ "publish:minor": "npm version minor && npm publish && start https://flows.nodered.org/add/node",
48
+ "publish:major": "npm version major && npm publish && start https://flows.nodered.org/add/node",
49
+ "test": "playwright test",
50
+ "test:ui": "playwright test --ui",
51
+ "test:headed": "playwright test --headed"
42
52
  },
43
- "dependencies": {}
53
+ "devDependencies": {
54
+ "@playwright/test": "^1.57.0"
55
+ }
44
56
  }
@@ -0,0 +1,22 @@
1
+ // @ts-check
2
+ const { defineConfig } = require('@playwright/test');
3
+
4
+ module.exports = defineConfig({
5
+ testDir: './tests',
6
+ fullyParallel: false,
7
+ forbidOnly: !!process.env.CI,
8
+ retries: process.env.CI ? 2 : 0,
9
+ workers: 1,
10
+ reporter: 'html',
11
+ use: {
12
+ baseURL: 'http://localhost:1880',
13
+ trace: 'on-first-retry',
14
+ screenshot: 'only-on-failure',
15
+ },
16
+ projects: [
17
+ {
18
+ name: 'chromium',
19
+ use: { browserName: 'chromium' },
20
+ },
21
+ ],
22
+ });
@@ -0,0 +1,141 @@
1
+ // @ts-check
2
+ const { test, expect } = require('@playwright/test');
3
+
4
+ test.describe('Event Calc - External Trigger Feature', () => {
5
+ test.beforeEach(async ({ page }) => {
6
+ // Navigate to Node-RED editor
7
+ await page.goto('/');
8
+ // Wait for Node-RED to load
9
+ await page.waitForSelector('#red-ui-palette', { timeout: 30000 });
10
+ });
11
+
12
+ test('should show External Trigger checkbox in event-calc node configuration', async ({ page }) => {
13
+ // Search for the event-calc node in the palette
14
+ const paletteSearch = page.locator('#red-ui-palette-search input');
15
+ await paletteSearch.fill('event calc');
16
+
17
+ // Wait for search results
18
+ await page.waitForTimeout(500);
19
+
20
+ // Find the event-calc node in the palette
21
+ const eventCalcNode = page.locator('.red-ui-palette-node[data-palette-type="event-calc"]');
22
+ await expect(eventCalcNode).toBeVisible();
23
+
24
+ // Drag the node to the workspace
25
+ const workspace = page.locator('#red-ui-workspace-chart');
26
+ await eventCalcNode.dragTo(workspace);
27
+
28
+ // Double-click on the newly created node to open the editor
29
+ // First find the node in the workspace
30
+ const nodeInWorkspace = page.locator('.red-ui-flow-node-group').last();
31
+ await nodeInWorkspace.dblclick();
32
+
33
+ // Wait for the edit dialog to open
34
+ await page.waitForSelector('.red-ui-editor', { timeout: 5000 });
35
+
36
+ // Verify the External Trigger checkbox exists
37
+ const externalTriggerCheckbox = page.locator('#node-input-externalTrigger');
38
+ await expect(externalTriggerCheckbox).toBeVisible();
39
+
40
+ // Verify it's unchecked by default
41
+ await expect(externalTriggerCheckbox).not.toBeChecked();
42
+
43
+ // Check the checkbox
44
+ await externalTriggerCheckbox.check();
45
+ await expect(externalTriggerCheckbox).toBeChecked();
46
+
47
+ // Verify the label is correct
48
+ const label = page.locator('label[for="node-input-externalTrigger"]');
49
+ await expect(label).toContainText('External Trigger');
50
+ await expect(label).toContainText('calculate on any input message');
51
+ });
52
+
53
+ test('should toggle External Trigger checkbox', async ({ page }) => {
54
+ // Search for the event-calc node
55
+ const paletteSearch = page.locator('#red-ui-palette-search input');
56
+ await paletteSearch.fill('event calc');
57
+ await page.waitForTimeout(500);
58
+
59
+ // Find and drag the node
60
+ const eventCalcNode = page.locator('.red-ui-palette-node[data-palette-type="event-calc"]');
61
+ const workspace = page.locator('#red-ui-workspace-chart');
62
+ await eventCalcNode.dragTo(workspace);
63
+
64
+ // Open the node editor
65
+ const nodeInWorkspace = page.locator('.red-ui-flow-node-group').last();
66
+ await nodeInWorkspace.dblclick();
67
+ await page.waitForSelector('.red-ui-editor', { timeout: 5000 });
68
+
69
+ const externalTriggerCheckbox = page.locator('#node-input-externalTrigger');
70
+
71
+ // Toggle on
72
+ await externalTriggerCheckbox.check();
73
+ await expect(externalTriggerCheckbox).toBeChecked();
74
+
75
+ // Toggle off
76
+ await externalTriggerCheckbox.uncheck();
77
+ await expect(externalTriggerCheckbox).not.toBeChecked();
78
+
79
+ // Toggle on again
80
+ await externalTriggerCheckbox.check();
81
+ await expect(externalTriggerCheckbox).toBeChecked();
82
+ });
83
+
84
+ test('should persist External Trigger setting after save', async ({ page }) => {
85
+ // Search for event-cache node first (required config)
86
+ const paletteSearch = page.locator('#red-ui-palette-search input');
87
+ await paletteSearch.fill('event cache');
88
+ await page.waitForTimeout(500);
89
+
90
+ // Add event-cache config node if needed
91
+ const eventCacheNode = page.locator('.red-ui-palette-node[data-palette-type="event-cache"]');
92
+ if (await eventCacheNode.isVisible()) {
93
+ const workspace = page.locator('#red-ui-workspace-chart');
94
+ await eventCacheNode.dragTo(workspace);
95
+
96
+ // Configure the cache node
97
+ const cacheNodeInWorkspace = page.locator('.red-ui-flow-node-group').last();
98
+ await cacheNodeInWorkspace.dblclick();
99
+ await page.waitForSelector('.red-ui-editor', { timeout: 5000 });
100
+
101
+ // Set a name for the cache
102
+ const nameInput = page.locator('#node-input-name');
103
+ await nameInput.fill('Test Cache');
104
+
105
+ // Save the cache node
106
+ const doneButton = page.locator('.red-ui-tray-footer button').filter({ hasText: 'Done' });
107
+ await doneButton.click();
108
+ await page.waitForTimeout(500);
109
+ }
110
+
111
+ // Now add the event-calc node
112
+ await paletteSearch.clear();
113
+ await paletteSearch.fill('event calc');
114
+ await page.waitForTimeout(500);
115
+
116
+ const eventCalcNode = page.locator('.red-ui-palette-node[data-palette-type="event-calc"]');
117
+ const workspace = page.locator('#red-ui-workspace-chart');
118
+ await eventCalcNode.dragTo(workspace);
119
+
120
+ // Open the node editor
121
+ const calcNodeInWorkspace = page.locator('.red-ui-flow-node-group').last();
122
+ await calcNodeInWorkspace.dblclick();
123
+ await page.waitForSelector('.red-ui-editor', { timeout: 5000 });
124
+
125
+ // Check the External Trigger checkbox
126
+ const externalTriggerCheckbox = page.locator('#node-input-externalTrigger');
127
+ await externalTriggerCheckbox.check();
128
+
129
+ // Save the node configuration
130
+ const doneButton = page.locator('.red-ui-tray-footer button').filter({ hasText: 'Done' });
131
+ await doneButton.click();
132
+ await page.waitForTimeout(500);
133
+
134
+ // Reopen the node editor
135
+ await calcNodeInWorkspace.dblclick();
136
+ await page.waitForSelector('.red-ui-editor', { timeout: 5000 });
137
+
138
+ // Verify the checkbox is still checked
139
+ await expect(externalTriggerCheckbox).toBeChecked();
140
+ });
141
+ });