ultravisor 1.0.2 → 1.0.4
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/.claude/launch.json +11 -0
- package/.claude/ultravisor-dev-config.json +3 -0
- package/.ultravisor.json +426 -0
- package/docs/README.md +63 -0
- package/package.json +12 -8
- package/source/Ultravisor.cjs +22 -3
- package/source/cli/Ultravisor-CLIProgram.cjs +35 -23
- package/source/cli/commands/Ultravisor-Command-SingleOperation.cjs +29 -18
- package/source/cli/commands/Ultravisor-Command-SingleTask.cjs +62 -19
- package/source/cli/commands/Ultravisor-Command-UpdateTask.cjs +27 -15
- package/source/config/Ultravisor-Default-Command-Configuration.cjs +5 -3
- package/source/services/Ultravisor-ExecutionEngine.cjs +1039 -0
- package/source/services/Ultravisor-ExecutionManifest.cjs +399 -0
- package/source/services/Ultravisor-Hypervisor-State.cjs +270 -97
- package/source/services/Ultravisor-Hypervisor.cjs +38 -83
- package/source/services/Ultravisor-StateManager.cjs +241 -0
- package/source/services/Ultravisor-TaskTypeRegistry.cjs +143 -0
- package/source/services/tasks/Ultravisor-TaskType-Base.cjs +105 -0
- package/source/services/tasks/control/Ultravisor-TaskType-IfConditional.cjs +148 -0
- package/source/services/tasks/control/Ultravisor-TaskType-LaunchOperation.cjs +187 -0
- package/source/services/tasks/control/Ultravisor-TaskType-SplitExecute.cjs +184 -0
- package/source/services/tasks/data/Ultravisor-TaskType-ReplaceString.cjs +82 -0
- package/source/services/tasks/data/Ultravisor-TaskType-SetValues.cjs +81 -0
- package/source/services/tasks/data/Ultravisor-TaskType-StringAppender.cjs +101 -0
- package/source/services/tasks/file-io/Ultravisor-TaskType-ReadFile.cjs +103 -0
- package/source/services/tasks/file-io/Ultravisor-TaskType-WriteFile.cjs +117 -0
- package/source/services/tasks/interaction/Ultravisor-TaskType-ErrorMessage.cjs +54 -0
- package/source/services/tasks/interaction/Ultravisor-TaskType-ValueInput.cjs +62 -0
- package/source/web_server/Ultravisor-API-Server.cjs +237 -124
- package/test/Ultravisor_browser_tests.js +2226 -0
- package/test/Ultravisor_tests.js +1143 -5830
- package/webinterface/css/ultravisor.css +23 -0
- package/webinterface/package.json +6 -3
- package/webinterface/source/Pict-Application-Ultravisor.js +93 -73
- package/webinterface/source/cards/FlowCard-CSVTransform.js +43 -0
- package/webinterface/source/cards/FlowCard-Command.js +86 -0
- package/webinterface/source/cards/FlowCard-ComprehensionIntersect.js +40 -0
- package/webinterface/source/cards/FlowCard-Conditional.js +87 -0
- package/webinterface/source/cards/FlowCard-CopyFile.js +55 -0
- package/webinterface/source/cards/FlowCard-End.js +29 -0
- package/webinterface/source/cards/FlowCard-GetJSON.js +55 -0
- package/webinterface/source/cards/FlowCard-GetText.js +54 -0
- package/webinterface/source/cards/FlowCard-Histogram.js +176 -0
- package/webinterface/source/cards/FlowCard-LaunchOperation.js +82 -0
- package/webinterface/source/cards/FlowCard-ListFiles.js +55 -0
- package/webinterface/source/cards/FlowCard-MeadowCount.js +44 -0
- package/webinterface/source/cards/FlowCard-MeadowCreate.js +44 -0
- package/webinterface/source/cards/FlowCard-MeadowDelete.js +45 -0
- package/webinterface/source/cards/FlowCard-MeadowRead.js +46 -0
- package/webinterface/source/cards/FlowCard-MeadowReads.js +46 -0
- package/webinterface/source/cards/FlowCard-MeadowUpdate.js +44 -0
- package/webinterface/source/cards/FlowCard-ParseCSV.js +85 -0
- package/webinterface/source/cards/FlowCard-ReadJSON.js +54 -0
- package/webinterface/source/cards/FlowCard-ReadText.js +54 -0
- package/webinterface/source/cards/FlowCard-RestRequest.js +59 -0
- package/webinterface/source/cards/FlowCard-SendJSON.js +57 -0
- package/webinterface/source/cards/FlowCard-Solver.js +77 -0
- package/webinterface/source/cards/FlowCard-Start.js +29 -0
- package/webinterface/source/cards/FlowCard-TemplateString.js +77 -0
- package/webinterface/source/cards/FlowCard-WriteJSON.js +54 -0
- package/webinterface/source/cards/FlowCard-WriteText.js +54 -0
- package/webinterface/source/data/ExampleFlow-CSVPipeline.js +231 -0
- package/webinterface/source/data/ExampleFlow-FileProcessor.js +315 -0
- package/webinterface/source/data/ExampleFlow-MeadowPipeline.js +328 -0
- package/webinterface/source/providers/PictRouter-Ultravisor-Configuration.json +8 -8
- package/webinterface/source/views/PictView-Ultravisor-Dashboard.js +6 -6
- package/webinterface/source/views/PictView-Ultravisor-FlowEditor.js +436 -0
- package/webinterface/source/views/PictView-Ultravisor-ManifestList.js +45 -43
- package/webinterface/source/views/PictView-Ultravisor-OperationEdit.js +34 -89
- package/webinterface/source/views/PictView-Ultravisor-OperationList.js +128 -13
- package/webinterface/source/views/PictView-Ultravisor-PendingInput.js +314 -0
- package/webinterface/source/views/PictView-Ultravisor-Schedule.js +18 -53
- package/webinterface/source/views/PictView-Ultravisor-TimingView.js +27 -14
- package/webinterface/source/views/PictView-Ultravisor-TopBar.js +2 -1
- package/.babelrc +0 -6
- package/.browserslistrc +0 -1
- package/.browserslistrc-BACKUP +0 -1
- package/.gulpfile-quackage-config.json +0 -7
- package/.gulpfile-quackage.js +0 -2
- package/debug/Harness.js +0 -5
- package/source/services/Ultravisor-Operation-Manifest.cjs +0 -160
- package/source/services/Ultravisor-Operation.cjs +0 -200
- package/source/services/Ultravisor-Task.cjs +0 -349
- package/source/services/events/Ultravisor-Hypervisor-Event-Solver.cjs +0 -11
- package/source/services/tasks/Ultravisor-Task-Base.cjs +0 -264
- package/source/services/tasks/Ultravisor-Task-CollectValues.cjs +0 -188
- package/source/services/tasks/Ultravisor-Task-Command.cjs +0 -65
- package/source/services/tasks/Ultravisor-Task-CommandEach.cjs +0 -190
- package/source/services/tasks/Ultravisor-Task-Conditional.cjs +0 -104
- package/source/services/tasks/Ultravisor-Task-DateWindow.cjs +0 -72
- package/source/services/tasks/Ultravisor-Task-GeneratePagedOperation.cjs +0 -336
- package/source/services/tasks/Ultravisor-Task-LaunchOperation.cjs +0 -143
- package/source/services/tasks/Ultravisor-Task-LaunchTask.cjs +0 -146
- package/source/services/tasks/Ultravisor-Task-LineMatch.cjs +0 -158
- package/source/services/tasks/Ultravisor-Task-Request.cjs +0 -56
- package/source/services/tasks/Ultravisor-Task-Solver.cjs +0 -89
- package/source/services/tasks/Ultravisor-Task-TemplateString.cjs +0 -93
- package/source/services/tasks/rest/Ultravisor-Task-GetBinary.cjs +0 -127
- package/source/services/tasks/rest/Ultravisor-Task-GetJSON.cjs +0 -119
- package/source/services/tasks/rest/Ultravisor-Task-GetText.cjs +0 -109
- package/source/services/tasks/rest/Ultravisor-Task-GetXML.cjs +0 -112
- package/source/services/tasks/rest/Ultravisor-Task-RestRequest.cjs +0 -499
- package/source/services/tasks/rest/Ultravisor-Task-SendJSON.cjs +0 -150
- package/source/services/tasks/stagingfiles/Ultravisor-Task-CopyFile.cjs +0 -110
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ListFiles.cjs +0 -89
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadBinary.cjs +0 -87
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadJSON.cjs +0 -67
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadText.cjs +0 -66
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadXML.cjs +0 -69
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteBinary.cjs +0 -95
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteJSON.cjs +0 -96
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteText.cjs +0 -99
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteXML.cjs +0 -102
- package/webinterface/.babelrc +0 -6
- package/webinterface/.browserslistrc +0 -1
- package/webinterface/.browserslistrc-BACKUP +0 -1
- package/webinterface/.gulpfile-quackage-config.json +0 -7
- package/webinterface/.gulpfile-quackage.js +0 -2
- package/webinterface/source/views/PictView-Ultravisor-TaskEdit.js +0 -220
- package/webinterface/source/views/PictView-Ultravisor-TaskList.js +0 -248
- /package/docs/{cover.md → _cover.md} +0 -0
|
@@ -0,0 +1,2226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ultravisor -- Headless Browser Tests
|
|
3
|
+
*
|
|
4
|
+
* Exercises each web interface view in a headless Chromium browser,
|
|
5
|
+
* generates numbered screenshots to debug/dist/automated_test_output/.
|
|
6
|
+
*
|
|
7
|
+
* Creates and executes multiple operations via the REST API to exercise
|
|
8
|
+
* all task types and workflow patterns:
|
|
9
|
+
* - Simple file copy (read-file → write-file)
|
|
10
|
+
* - Conditional branching (set-values → if-conditional → write-file)
|
|
11
|
+
* - Error handling (read-file error → error-message)
|
|
12
|
+
* - Looping pipeline (read → split → replace → append → write)
|
|
13
|
+
* - Sub-operation composition (launch-operation)
|
|
14
|
+
* - State template transforms
|
|
15
|
+
*
|
|
16
|
+
* Requires: puppeteer (dev dependency)
|
|
17
|
+
* Requires: webinterface/dist/ to be pre-built (npx quack build)
|
|
18
|
+
*/
|
|
19
|
+
const libAssert = require('assert');
|
|
20
|
+
const libPath = require('path');
|
|
21
|
+
const libFs = require('fs');
|
|
22
|
+
const libPuppeteer = require('puppeteer');
|
|
23
|
+
const libPict = require('pict');
|
|
24
|
+
|
|
25
|
+
// Services
|
|
26
|
+
const libServiceHypervisor = require('../source/services/Ultravisor-Hypervisor.cjs');
|
|
27
|
+
const libServiceHypervisorState = require('../source/services/Ultravisor-Hypervisor-State.cjs');
|
|
28
|
+
const libServiceHypervisorEventBase = require('../source/services/Ultravisor-Hypervisor-Event-Base.cjs');
|
|
29
|
+
const libServiceHypervisorEventCron = require('../source/services/events/Ultravisor-Hypervisor-Event-Cron.cjs');
|
|
30
|
+
const libServiceTaskTypeRegistry = require('../source/services/Ultravisor-TaskTypeRegistry.cjs');
|
|
31
|
+
const libServiceStateManager = require('../source/services/Ultravisor-StateManager.cjs');
|
|
32
|
+
const libServiceExecutionEngine = require('../source/services/Ultravisor-ExecutionEngine.cjs');
|
|
33
|
+
const libServiceExecutionManifest = require('../source/services/Ultravisor-ExecutionManifest.cjs');
|
|
34
|
+
const libWebServerAPIServer = require('../source/web_server/Ultravisor-API-Server.cjs');
|
|
35
|
+
|
|
36
|
+
// ── Module-scope state ──────────────────────────────────
|
|
37
|
+
let _Browser = null;
|
|
38
|
+
let _Page = null;
|
|
39
|
+
let _Fable = null;
|
|
40
|
+
let _BaseURL = '';
|
|
41
|
+
let _ScreenshotDir = '';
|
|
42
|
+
let _ScreenshotIndex = 0;
|
|
43
|
+
let _ConsoleErrors = [];
|
|
44
|
+
let _StagingDir = '';
|
|
45
|
+
let _TestResults =
|
|
46
|
+
{
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
totalTests: 0,
|
|
49
|
+
passed: 0,
|
|
50
|
+
failed: 0,
|
|
51
|
+
screenshots: [],
|
|
52
|
+
duration: ''
|
|
53
|
+
};
|
|
54
|
+
let _StartTime = Date.now();
|
|
55
|
+
|
|
56
|
+
// ── Helpers ─────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function takeScreenshot(pName)
|
|
59
|
+
{
|
|
60
|
+
_ScreenshotIndex++;
|
|
61
|
+
let tmpPadded = String(_ScreenshotIndex).padStart(2, '0');
|
|
62
|
+
let tmpFilename = tmpPadded + '-' + pName + '.png';
|
|
63
|
+
_TestResults.screenshots.push(tmpFilename);
|
|
64
|
+
return _Page.screenshot(
|
|
65
|
+
{
|
|
66
|
+
path: libPath.join(_ScreenshotDir, tmpFilename),
|
|
67
|
+
fullPage: false
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function settle(pMs)
|
|
72
|
+
{
|
|
73
|
+
await _Page.evaluate((pDelay) => new Promise((r) => setTimeout(r, pDelay)), pMs || 500);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function waitForAppReady()
|
|
77
|
+
{
|
|
78
|
+
await _Page.waitForSelector('#Ultravisor-Application-Container', { timeout: 15000 });
|
|
79
|
+
try
|
|
80
|
+
{
|
|
81
|
+
await _Page.waitForFunction(
|
|
82
|
+
() =>
|
|
83
|
+
{
|
|
84
|
+
return (typeof(window.Pict) !== 'undefined') || (typeof(window._Pict) !== 'undefined');
|
|
85
|
+
},
|
|
86
|
+
{ timeout: 10000 }
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
catch (pErr)
|
|
90
|
+
{
|
|
91
|
+
console.log(' [Warning] Pict global not found after 10s; proceeding without it.');
|
|
92
|
+
}
|
|
93
|
+
await settle(2000);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function navigateToRoute(pRoute)
|
|
97
|
+
{
|
|
98
|
+
await _Page.evaluate((pHash) =>
|
|
99
|
+
{
|
|
100
|
+
window.location.hash = pHash;
|
|
101
|
+
}, pRoute);
|
|
102
|
+
await settle(800);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create an operation via the API and return its response.
|
|
107
|
+
*/
|
|
108
|
+
async function apiCreateOperation(pOperationData)
|
|
109
|
+
{
|
|
110
|
+
return _Page.evaluate(async (pBaseURL, pData) =>
|
|
111
|
+
{
|
|
112
|
+
let tmpRes = await fetch(pBaseURL + '/Operation',
|
|
113
|
+
{
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: JSON.stringify(pData)
|
|
117
|
+
});
|
|
118
|
+
return tmpRes.json();
|
|
119
|
+
}, _BaseURL, pOperationData);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Execute an operation via the API and return the execution result.
|
|
124
|
+
*/
|
|
125
|
+
async function apiExecuteOperation(pHash)
|
|
126
|
+
{
|
|
127
|
+
return _Page.evaluate(async (pBaseURL, pHash) =>
|
|
128
|
+
{
|
|
129
|
+
let tmpRes = await fetch(pBaseURL + '/Operation/' + pHash + '/Execute');
|
|
130
|
+
return { status: tmpRes.status, body: await tmpRes.json() };
|
|
131
|
+
}, _BaseURL, pHash);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Generic API GET helper.
|
|
136
|
+
*/
|
|
137
|
+
async function apiGet(pPath)
|
|
138
|
+
{
|
|
139
|
+
return _Page.evaluate(async (pBaseURL, pPath) =>
|
|
140
|
+
{
|
|
141
|
+
let tmpRes = await fetch(pBaseURL + pPath);
|
|
142
|
+
return { status: tmpRes.status, body: await tmpRes.json() };
|
|
143
|
+
}, _BaseURL, pPath);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Generic API POST helper.
|
|
148
|
+
*/
|
|
149
|
+
async function apiPost(pPath, pBody)
|
|
150
|
+
{
|
|
151
|
+
return _Page.evaluate(async (pBaseURL, pPath, pBody) =>
|
|
152
|
+
{
|
|
153
|
+
let tmpRes = await fetch(pBaseURL + pPath,
|
|
154
|
+
{
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: { 'Content-Type': 'application/json' },
|
|
157
|
+
body: JSON.stringify(pBody)
|
|
158
|
+
});
|
|
159
|
+
return { status: tmpRes.status, body: await tmpRes.json() };
|
|
160
|
+
}, _BaseURL, pPath, pBody);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Generic API DELETE helper.
|
|
165
|
+
*/
|
|
166
|
+
async function apiDelete(pPath)
|
|
167
|
+
{
|
|
168
|
+
return _Page.evaluate(async (pBaseURL, pPath) =>
|
|
169
|
+
{
|
|
170
|
+
let tmpRes = await fetch(pBaseURL + pPath, { method: 'DELETE' });
|
|
171
|
+
return { status: tmpRes.status, body: await tmpRes.json() };
|
|
172
|
+
}, _BaseURL, pPath);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Test Suite ──────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
suite
|
|
178
|
+
(
|
|
179
|
+
'Ultravisor Browser Tests',
|
|
180
|
+
function ()
|
|
181
|
+
{
|
|
182
|
+
this.timeout(300000);
|
|
183
|
+
|
|
184
|
+
suiteSetup
|
|
185
|
+
(
|
|
186
|
+
function (fDone)
|
|
187
|
+
{
|
|
188
|
+
this.timeout(60000);
|
|
189
|
+
|
|
190
|
+
_ScreenshotDir = libPath.join(__dirname, '..', 'debug', 'dist', 'automated_test_output');
|
|
191
|
+
_StagingDir = libPath.resolve(__dirname, '..', '.test_staging_browser');
|
|
192
|
+
|
|
193
|
+
// Clean and create output directory
|
|
194
|
+
if (libFs.existsSync(_ScreenshotDir))
|
|
195
|
+
{
|
|
196
|
+
libFs.rmSync(_ScreenshotDir, { recursive: true, force: true });
|
|
197
|
+
}
|
|
198
|
+
libFs.mkdirSync(_ScreenshotDir, { recursive: true });
|
|
199
|
+
|
|
200
|
+
// Ensure staging directory exists
|
|
201
|
+
libFs.mkdirSync(_StagingDir, { recursive: true });
|
|
202
|
+
|
|
203
|
+
// Verify webinterface dist exists
|
|
204
|
+
let tmpWebInterfacePath = libPath.resolve(__dirname, '..', 'webinterface', 'dist');
|
|
205
|
+
if (!libFs.existsSync(libPath.join(tmpWebInterfacePath, 'index.html')))
|
|
206
|
+
{
|
|
207
|
+
return fDone(new Error('webinterface/dist/index.html not found. Run "npx quack build" first.'));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Create Pict instance with random port
|
|
211
|
+
let tmpPort = 10000 + Math.floor(Math.random() * 50000);
|
|
212
|
+
_Fable = new libPict(
|
|
213
|
+
{
|
|
214
|
+
Product: 'Ultravisor-BrowserTest',
|
|
215
|
+
APIServerPort: tmpPort,
|
|
216
|
+
LogLevel: 5
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
_Fable.ProgramConfiguration =
|
|
220
|
+
{
|
|
221
|
+
UltravisorWebInterfacePath: tmpWebInterfacePath,
|
|
222
|
+
UltravisorStagingRoot: _StagingDir
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
if (typeof(_Fable.gatherProgramConfiguration) !== 'function')
|
|
226
|
+
{
|
|
227
|
+
_Fable.gatherProgramConfiguration = function ()
|
|
228
|
+
{
|
|
229
|
+
return {
|
|
230
|
+
GatherPhases:
|
|
231
|
+
[
|
|
232
|
+
{ Phase: 'Default Program Configuration' },
|
|
233
|
+
{ Phase: 'Test Configuration', Path: libPath.join(_StagingDir, '.ultravisor.json') }
|
|
234
|
+
],
|
|
235
|
+
ConfigurationOutcome: {}
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Register all services
|
|
241
|
+
_Fable.addAndInstantiateServiceTypeIfNotExists('UltravisorHypervisor', libServiceHypervisor);
|
|
242
|
+
_Fable.addAndInstantiateServiceTypeIfNotExists('UltravisorHypervisorState', libServiceHypervisorState);
|
|
243
|
+
_Fable.addAndInstantiateServiceTypeIfNotExists('UltravisorHypervisorEventBase', libServiceHypervisorEventBase);
|
|
244
|
+
_Fable.addAndInstantiateServiceTypeIfNotExists('UltravisorHypervisorEventCron', libServiceHypervisorEventCron);
|
|
245
|
+
_Fable.addAndInstantiateServiceTypeIfNotExists('UltravisorTaskTypeRegistry', libServiceTaskTypeRegistry);
|
|
246
|
+
_Fable.addAndInstantiateServiceTypeIfNotExists('UltravisorStateManager', libServiceStateManager);
|
|
247
|
+
_Fable.addAndInstantiateServiceTypeIfNotExists('UltravisorExecutionEngine', libServiceExecutionEngine);
|
|
248
|
+
_Fable.addAndInstantiateServiceTypeIfNotExists('UltravisorExecutionManifest', libServiceExecutionManifest);
|
|
249
|
+
|
|
250
|
+
let tmpRegistry = Object.values(_Fable.servicesMap['UltravisorTaskTypeRegistry'])[0];
|
|
251
|
+
if (tmpRegistry)
|
|
252
|
+
{
|
|
253
|
+
tmpRegistry.registerBuiltInTaskTypes();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Register and start API server
|
|
257
|
+
_Fable.addAndInstantiateServiceTypeIfNotExists('UltravisorAPIServer', libWebServerAPIServer);
|
|
258
|
+
let tmpAPIServer = Object.values(_Fable.servicesMap['UltravisorAPIServer'])[0];
|
|
259
|
+
|
|
260
|
+
tmpAPIServer.start(function (pError)
|
|
261
|
+
{
|
|
262
|
+
if (pError)
|
|
263
|
+
{
|
|
264
|
+
return fDone(pError);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
_BaseURL = 'http://localhost:' + tmpPort;
|
|
268
|
+
|
|
269
|
+
libPuppeteer.launch(
|
|
270
|
+
{
|
|
271
|
+
headless: true,
|
|
272
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
273
|
+
}).then(function (pBrowser)
|
|
274
|
+
{
|
|
275
|
+
_Browser = pBrowser;
|
|
276
|
+
return _Browser.newPage();
|
|
277
|
+
}).then(function (pPage)
|
|
278
|
+
{
|
|
279
|
+
_Page = pPage;
|
|
280
|
+
return _Page.setViewport({ width: 1280, height: 800 });
|
|
281
|
+
}).then(function ()
|
|
282
|
+
{
|
|
283
|
+
_Page.on('console', function (pMsg)
|
|
284
|
+
{
|
|
285
|
+
if (pMsg.type() === 'error')
|
|
286
|
+
{
|
|
287
|
+
console.log(' [Browser Console Error]', pMsg.text());
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
_Page.on('pageerror', function (pError)
|
|
291
|
+
{
|
|
292
|
+
_ConsoleErrors.push(pError.message);
|
|
293
|
+
console.log(' [Browser Error]', pError.message);
|
|
294
|
+
});
|
|
295
|
+
_Page.on('requestfailed', function (pRequest)
|
|
296
|
+
{
|
|
297
|
+
console.log(' [Request Failed]', pRequest.url(), pRequest.failure().errorText);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
return _Page.goto(_BaseURL + '/index.html', { waitUntil: 'networkidle2', timeout: 30000 });
|
|
301
|
+
}).then(function ()
|
|
302
|
+
{
|
|
303
|
+
return waitForAppReady();
|
|
304
|
+
}).then(function ()
|
|
305
|
+
{
|
|
306
|
+
fDone();
|
|
307
|
+
}).catch(function (pErr)
|
|
308
|
+
{
|
|
309
|
+
fDone(pErr);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
suiteTeardown
|
|
316
|
+
(
|
|
317
|
+
function (fDone)
|
|
318
|
+
{
|
|
319
|
+
this.timeout(15000);
|
|
320
|
+
|
|
321
|
+
_TestResults.failed = _TestResults.totalTests - _TestResults.passed;
|
|
322
|
+
_TestResults.duration = ((Date.now() - _StartTime) / 1000).toFixed(1) + 's';
|
|
323
|
+
|
|
324
|
+
try
|
|
325
|
+
{
|
|
326
|
+
libFs.writeFileSync(
|
|
327
|
+
libPath.join(_ScreenshotDir, 'test-results.json'),
|
|
328
|
+
JSON.stringify(_TestResults, null, '\t'),
|
|
329
|
+
'utf8'
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
catch (pErr)
|
|
333
|
+
{
|
|
334
|
+
console.log(' [Warning] Failed to write test-results.json:', pErr.message);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let tmpClosePromise = Promise.resolve();
|
|
338
|
+
|
|
339
|
+
if (_Browser)
|
|
340
|
+
{
|
|
341
|
+
tmpClosePromise = _Browser.close();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
tmpClosePromise.then(function ()
|
|
345
|
+
{
|
|
346
|
+
if (_Fable)
|
|
347
|
+
{
|
|
348
|
+
let tmpAPIServer = Object.values(_Fable.servicesMap['UltravisorAPIServer'] || {})[0];
|
|
349
|
+
if (tmpAPIServer && tmpAPIServer._Orator)
|
|
350
|
+
{
|
|
351
|
+
return tmpAPIServer._Orator.stopService(function ()
|
|
352
|
+
{
|
|
353
|
+
if (libFs.existsSync(_StagingDir))
|
|
354
|
+
{
|
|
355
|
+
libFs.rmSync(_StagingDir, { recursive: true, force: true });
|
|
356
|
+
}
|
|
357
|
+
return fDone();
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return fDone();
|
|
362
|
+
}).catch(function ()
|
|
363
|
+
{
|
|
364
|
+
return fDone();
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
setup(function ()
|
|
370
|
+
{
|
|
371
|
+
_TestResults.totalTests++;
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// ════════════════════════════════════════════════
|
|
375
|
+
// Application Load
|
|
376
|
+
// ════════════════════════════════════════════════
|
|
377
|
+
suite
|
|
378
|
+
(
|
|
379
|
+
'Application Load',
|
|
380
|
+
function ()
|
|
381
|
+
{
|
|
382
|
+
test
|
|
383
|
+
(
|
|
384
|
+
'app loads and container exists',
|
|
385
|
+
async function ()
|
|
386
|
+
{
|
|
387
|
+
this.timeout(15000);
|
|
388
|
+
|
|
389
|
+
let tmpContainerExists = await _Page.evaluate(
|
|
390
|
+
() => !!document.getElementById('Ultravisor-Application-Container')
|
|
391
|
+
);
|
|
392
|
+
libAssert.ok(tmpContainerExists, 'Application container should exist');
|
|
393
|
+
|
|
394
|
+
let tmpPictExists = await _Page.evaluate(
|
|
395
|
+
() => typeof(window._Pict) !== 'undefined'
|
|
396
|
+
);
|
|
397
|
+
libAssert.ok(tmpPictExists, 'window._Pict should be defined');
|
|
398
|
+
|
|
399
|
+
await takeScreenshot('app-loads');
|
|
400
|
+
_TestResults.passed++;
|
|
401
|
+
}
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// ════════════════════════════════════════════════
|
|
407
|
+
// View Navigation
|
|
408
|
+
// ════════════════════════════════════════════════
|
|
409
|
+
suite
|
|
410
|
+
(
|
|
411
|
+
'View Navigation',
|
|
412
|
+
function ()
|
|
413
|
+
{
|
|
414
|
+
test
|
|
415
|
+
(
|
|
416
|
+
'Dashboard view renders',
|
|
417
|
+
async function ()
|
|
418
|
+
{
|
|
419
|
+
this.timeout(15000);
|
|
420
|
+
await navigateToRoute('#/Home');
|
|
421
|
+
await takeScreenshot('dashboard');
|
|
422
|
+
_TestResults.passed++;
|
|
423
|
+
}
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
test
|
|
427
|
+
(
|
|
428
|
+
'Operation List view renders',
|
|
429
|
+
async function ()
|
|
430
|
+
{
|
|
431
|
+
this.timeout(15000);
|
|
432
|
+
await navigateToRoute('#/Operations');
|
|
433
|
+
await takeScreenshot('operation-list');
|
|
434
|
+
_TestResults.passed++;
|
|
435
|
+
}
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
test
|
|
439
|
+
(
|
|
440
|
+
'Operation Edit view renders',
|
|
441
|
+
async function ()
|
|
442
|
+
{
|
|
443
|
+
this.timeout(15000);
|
|
444
|
+
await navigateToRoute('#/OperationEdit');
|
|
445
|
+
await takeScreenshot('operation-edit');
|
|
446
|
+
_TestResults.passed++;
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
test
|
|
451
|
+
(
|
|
452
|
+
'Schedule view renders',
|
|
453
|
+
async function ()
|
|
454
|
+
{
|
|
455
|
+
this.timeout(15000);
|
|
456
|
+
await navigateToRoute('#/Schedule');
|
|
457
|
+
await takeScreenshot('schedule');
|
|
458
|
+
_TestResults.passed++;
|
|
459
|
+
}
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
test
|
|
463
|
+
(
|
|
464
|
+
'Manifest List view renders',
|
|
465
|
+
async function ()
|
|
466
|
+
{
|
|
467
|
+
this.timeout(15000);
|
|
468
|
+
await navigateToRoute('#/Manifests');
|
|
469
|
+
await takeScreenshot('manifest-list');
|
|
470
|
+
_TestResults.passed++;
|
|
471
|
+
}
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
test
|
|
475
|
+
(
|
|
476
|
+
'Timing view renders',
|
|
477
|
+
async function ()
|
|
478
|
+
{
|
|
479
|
+
this.timeout(15000);
|
|
480
|
+
await navigateToRoute('#/Timing');
|
|
481
|
+
await takeScreenshot('timing');
|
|
482
|
+
_TestResults.passed++;
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
test
|
|
487
|
+
(
|
|
488
|
+
'Flow Editor view renders',
|
|
489
|
+
async function ()
|
|
490
|
+
{
|
|
491
|
+
this.timeout(15000);
|
|
492
|
+
await navigateToRoute('#/FlowEditor');
|
|
493
|
+
await takeScreenshot('flow-editor');
|
|
494
|
+
_TestResults.passed++;
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
// ════════════════════════════════════════════════
|
|
501
|
+
// Navigation Cycle
|
|
502
|
+
// ════════════════════════════════════════════════
|
|
503
|
+
suite
|
|
504
|
+
(
|
|
505
|
+
'Navigation Cycle',
|
|
506
|
+
function ()
|
|
507
|
+
{
|
|
508
|
+
test
|
|
509
|
+
(
|
|
510
|
+
'navigates through all views sequentially',
|
|
511
|
+
async function ()
|
|
512
|
+
{
|
|
513
|
+
this.timeout(30000);
|
|
514
|
+
|
|
515
|
+
let tmpRoutes =
|
|
516
|
+
[
|
|
517
|
+
'#/Home',
|
|
518
|
+
'#/Operations', '#/OperationEdit',
|
|
519
|
+
'#/Schedule', '#/Manifests',
|
|
520
|
+
'#/Timing', '#/FlowEditor'
|
|
521
|
+
];
|
|
522
|
+
|
|
523
|
+
for (let i = 0; i < tmpRoutes.length; i++)
|
|
524
|
+
{
|
|
525
|
+
await navigateToRoute(tmpRoutes[i]);
|
|
526
|
+
await takeScreenshot('nav-cycle-' + tmpRoutes[i].replace('#/', '').toLowerCase());
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
_TestResults.passed++;
|
|
530
|
+
}
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
// ════════════════════════════════════════════════
|
|
536
|
+
// NodeTemplate CRUD API
|
|
537
|
+
// ════════════════════════════════════════════════
|
|
538
|
+
suite
|
|
539
|
+
(
|
|
540
|
+
'NodeTemplate CRUD API',
|
|
541
|
+
function ()
|
|
542
|
+
{
|
|
543
|
+
let _CreatedTemplateHash = '';
|
|
544
|
+
|
|
545
|
+
test
|
|
546
|
+
(
|
|
547
|
+
'create a node template via POST /NodeTemplate',
|
|
548
|
+
async function ()
|
|
549
|
+
{
|
|
550
|
+
this.timeout(10000);
|
|
551
|
+
|
|
552
|
+
let tmpResponse = await _Page.evaluate(async (pBaseURL) =>
|
|
553
|
+
{
|
|
554
|
+
let tmpRes = await fetch(pBaseURL + '/NodeTemplate',
|
|
555
|
+
{
|
|
556
|
+
method: 'POST',
|
|
557
|
+
headers: { 'Content-Type': 'application/json' },
|
|
558
|
+
body: JSON.stringify({
|
|
559
|
+
Type: 'read-file',
|
|
560
|
+
Name: 'Test ReadFile Template',
|
|
561
|
+
Settings: { FilePath: '/tmp/test.txt', Encoding: 'utf8' }
|
|
562
|
+
})
|
|
563
|
+
});
|
|
564
|
+
return tmpRes.json();
|
|
565
|
+
}, _BaseURL);
|
|
566
|
+
|
|
567
|
+
libAssert.ok(tmpResponse.Hash, 'Template should have a Hash');
|
|
568
|
+
libAssert.ok(tmpResponse.Hash.startsWith('TMPL-'), 'Hash should start with TMPL-');
|
|
569
|
+
libAssert.strictEqual(tmpResponse.Type, 'read-file', 'Type should match');
|
|
570
|
+
|
|
571
|
+
_CreatedTemplateHash = tmpResponse.Hash;
|
|
572
|
+
console.log(' Created template:', _CreatedTemplateHash);
|
|
573
|
+
|
|
574
|
+
_TestResults.passed++;
|
|
575
|
+
}
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
test
|
|
579
|
+
(
|
|
580
|
+
'list node templates via GET /NodeTemplate',
|
|
581
|
+
async function ()
|
|
582
|
+
{
|
|
583
|
+
this.timeout(10000);
|
|
584
|
+
|
|
585
|
+
let tmpResponse = (await apiGet('/NodeTemplate')).body;
|
|
586
|
+
|
|
587
|
+
libAssert.ok(Array.isArray(tmpResponse), 'Should return an array');
|
|
588
|
+
libAssert.ok(tmpResponse.length >= 1, 'Should have at least one template');
|
|
589
|
+
|
|
590
|
+
let tmpFound = tmpResponse.find(function (pT) { return pT.Hash === _CreatedTemplateHash; });
|
|
591
|
+
libAssert.ok(tmpFound, 'Created template should appear in list');
|
|
592
|
+
|
|
593
|
+
_TestResults.passed++;
|
|
594
|
+
}
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
test
|
|
598
|
+
(
|
|
599
|
+
'get a node template by hash via GET /NodeTemplate/:Hash',
|
|
600
|
+
async function ()
|
|
601
|
+
{
|
|
602
|
+
this.timeout(10000);
|
|
603
|
+
|
|
604
|
+
let tmpResponse = (await apiGet('/NodeTemplate/' + _CreatedTemplateHash)).body;
|
|
605
|
+
|
|
606
|
+
libAssert.strictEqual(tmpResponse.Hash, _CreatedTemplateHash, 'Hash should match');
|
|
607
|
+
libAssert.strictEqual(tmpResponse.Name, 'Test ReadFile Template', 'Name should match');
|
|
608
|
+
libAssert.deepStrictEqual(tmpResponse.Settings, { FilePath: '/tmp/test.txt', Encoding: 'utf8' }, 'Settings should match');
|
|
609
|
+
|
|
610
|
+
_TestResults.passed++;
|
|
611
|
+
}
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
test
|
|
615
|
+
(
|
|
616
|
+
'update a node template via PUT /NodeTemplate/:Hash',
|
|
617
|
+
async function ()
|
|
618
|
+
{
|
|
619
|
+
this.timeout(10000);
|
|
620
|
+
|
|
621
|
+
let tmpResponse = await _Page.evaluate(async (pBaseURL, pHash) =>
|
|
622
|
+
{
|
|
623
|
+
let tmpRes = await fetch(pBaseURL + '/NodeTemplate/' + pHash,
|
|
624
|
+
{
|
|
625
|
+
method: 'PUT',
|
|
626
|
+
headers: { 'Content-Type': 'application/json' },
|
|
627
|
+
body: JSON.stringify({
|
|
628
|
+
Name: 'Updated ReadFile Template',
|
|
629
|
+
Settings: { FilePath: '/tmp/updated.txt', Encoding: 'utf8' }
|
|
630
|
+
})
|
|
631
|
+
});
|
|
632
|
+
return tmpRes.json();
|
|
633
|
+
}, _BaseURL, _CreatedTemplateHash);
|
|
634
|
+
|
|
635
|
+
libAssert.strictEqual(tmpResponse.Name, 'Updated ReadFile Template', 'Name should be updated');
|
|
636
|
+
|
|
637
|
+
_TestResults.passed++;
|
|
638
|
+
}
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
test
|
|
642
|
+
(
|
|
643
|
+
'delete a node template via DELETE /NodeTemplate/:Hash',
|
|
644
|
+
async function ()
|
|
645
|
+
{
|
|
646
|
+
this.timeout(10000);
|
|
647
|
+
|
|
648
|
+
let tmpResponse = (await apiDelete('/NodeTemplate/' + _CreatedTemplateHash)).body;
|
|
649
|
+
libAssert.strictEqual(tmpResponse.Status, 'Deleted', 'Should confirm deletion');
|
|
650
|
+
|
|
651
|
+
let tmpGetResponse = await apiGet('/NodeTemplate/' + _CreatedTemplateHash);
|
|
652
|
+
libAssert.strictEqual(tmpGetResponse.status, 404, 'Should return 404 after deletion');
|
|
653
|
+
|
|
654
|
+
_TestResults.passed++;
|
|
655
|
+
}
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
// ════════════════════════════════════════════════
|
|
661
|
+
// Task Type Registry
|
|
662
|
+
// ════════════════════════════════════════════════
|
|
663
|
+
suite
|
|
664
|
+
(
|
|
665
|
+
'Task Type Registry',
|
|
666
|
+
function ()
|
|
667
|
+
{
|
|
668
|
+
test
|
|
669
|
+
(
|
|
670
|
+
'list task types via GET /TaskType',
|
|
671
|
+
async function ()
|
|
672
|
+
{
|
|
673
|
+
this.timeout(10000);
|
|
674
|
+
|
|
675
|
+
let tmpResponse = (await apiGet('/TaskType')).body;
|
|
676
|
+
|
|
677
|
+
libAssert.ok(Array.isArray(tmpResponse), 'Should return an array');
|
|
678
|
+
libAssert.ok(tmpResponse.length >= 10, 'Should have at least 10 built-in task types (got ' + tmpResponse.length + ')');
|
|
679
|
+
|
|
680
|
+
// Verify every expected task type is registered
|
|
681
|
+
let tmpExpected = ['read-file', 'write-file', 'set-values', 'replace-string',
|
|
682
|
+
'string-appender', 'if-conditional', 'split-execute', 'launch-operation',
|
|
683
|
+
'value-input', 'error-message'];
|
|
684
|
+
|
|
685
|
+
for (let i = 0; i < tmpExpected.length; i++)
|
|
686
|
+
{
|
|
687
|
+
let tmpFound = tmpResponse.find(function (pT) { return pT.Hash === tmpExpected[i]; });
|
|
688
|
+
libAssert.ok(tmpFound, tmpExpected[i] + ' task type should be registered');
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
console.log(' Task types:', tmpResponse.map(function (pT) { return pT.Hash; }).join(', '));
|
|
692
|
+
|
|
693
|
+
_TestResults.passed++;
|
|
694
|
+
}
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
// ════════════════════════════════════════════════
|
|
700
|
+
// Operation CRUD API
|
|
701
|
+
// ════════════════════════════════════════════════
|
|
702
|
+
suite
|
|
703
|
+
(
|
|
704
|
+
'Operation CRUD API',
|
|
705
|
+
function ()
|
|
706
|
+
{
|
|
707
|
+
let _CreatedOpHash = '';
|
|
708
|
+
|
|
709
|
+
test
|
|
710
|
+
(
|
|
711
|
+
'create an operation via POST /Operation',
|
|
712
|
+
async function ()
|
|
713
|
+
{
|
|
714
|
+
this.timeout(10000);
|
|
715
|
+
|
|
716
|
+
let tmpResponse = await apiCreateOperation({
|
|
717
|
+
Name: 'CRUD Test Operation',
|
|
718
|
+
Description: 'Created for CRUD API testing',
|
|
719
|
+
Graph: {
|
|
720
|
+
Nodes: [
|
|
721
|
+
{ Hash: 'n-start', Type: 'start', X: 0, Y: 0 },
|
|
722
|
+
{ Hash: 'n-end', Type: 'end', X: 200, Y: 0 }
|
|
723
|
+
],
|
|
724
|
+
Connections: [
|
|
725
|
+
{
|
|
726
|
+
Hash: 'c1', ConnectionType: 'Event',
|
|
727
|
+
SourceNodeHash: 'n-start', SourcePortHash: 'n-start-eo-Start',
|
|
728
|
+
TargetNodeHash: 'n-end', TargetPortHash: 'n-end-ei-End'
|
|
729
|
+
}
|
|
730
|
+
],
|
|
731
|
+
ViewState: {}
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
libAssert.ok(tmpResponse.Hash, 'Operation should have a Hash');
|
|
736
|
+
libAssert.ok(tmpResponse.Hash.startsWith('OPR-'), 'Hash should start with OPR-');
|
|
737
|
+
libAssert.strictEqual(tmpResponse.Name, 'CRUD Test Operation', 'Name should match');
|
|
738
|
+
|
|
739
|
+
_CreatedOpHash = tmpResponse.Hash;
|
|
740
|
+
console.log(' Created operation:', _CreatedOpHash);
|
|
741
|
+
|
|
742
|
+
_TestResults.passed++;
|
|
743
|
+
}
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
test
|
|
747
|
+
(
|
|
748
|
+
'list operations via GET /Operation',
|
|
749
|
+
async function ()
|
|
750
|
+
{
|
|
751
|
+
this.timeout(10000);
|
|
752
|
+
|
|
753
|
+
let tmpResponse = (await apiGet('/Operation')).body;
|
|
754
|
+
|
|
755
|
+
libAssert.ok(Array.isArray(tmpResponse), 'Should return an array');
|
|
756
|
+
libAssert.ok(tmpResponse.length >= 1, 'Should have at least one operation');
|
|
757
|
+
|
|
758
|
+
let tmpFound = tmpResponse.find(function (pOp) { return pOp.Hash === _CreatedOpHash; });
|
|
759
|
+
libAssert.ok(tmpFound, 'Created operation should appear in list');
|
|
760
|
+
|
|
761
|
+
_TestResults.passed++;
|
|
762
|
+
}
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
test
|
|
766
|
+
(
|
|
767
|
+
'get an operation by hash via GET /Operation/:Hash',
|
|
768
|
+
async function ()
|
|
769
|
+
{
|
|
770
|
+
this.timeout(10000);
|
|
771
|
+
|
|
772
|
+
let tmpResponse = (await apiGet('/Operation/' + _CreatedOpHash)).body;
|
|
773
|
+
|
|
774
|
+
libAssert.strictEqual(tmpResponse.Hash, _CreatedOpHash, 'Hash should match');
|
|
775
|
+
libAssert.strictEqual(tmpResponse.Name, 'CRUD Test Operation', 'Name should match');
|
|
776
|
+
libAssert.ok(tmpResponse.Graph, 'Should have a Graph');
|
|
777
|
+
libAssert.strictEqual(tmpResponse.Graph.Nodes.length, 2, 'Graph should have 2 nodes');
|
|
778
|
+
|
|
779
|
+
_TestResults.passed++;
|
|
780
|
+
}
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
test
|
|
784
|
+
(
|
|
785
|
+
'execute a trivial operation via GET /Operation/:Hash/Execute',
|
|
786
|
+
async function ()
|
|
787
|
+
{
|
|
788
|
+
this.timeout(15000);
|
|
789
|
+
|
|
790
|
+
let tmpResult = await apiExecuteOperation(_CreatedOpHash);
|
|
791
|
+
|
|
792
|
+
libAssert.strictEqual(tmpResult.status, 200, 'Should return 200');
|
|
793
|
+
libAssert.strictEqual(tmpResult.body.Status, 'Complete', 'Trivial start→end should complete');
|
|
794
|
+
|
|
795
|
+
console.log(' Trivial execution:', tmpResult.body.Status,
|
|
796
|
+
'| Log:', tmpResult.body.Log ? tmpResult.body.Log.length : 0, 'entries');
|
|
797
|
+
|
|
798
|
+
_TestResults.passed++;
|
|
799
|
+
}
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
test
|
|
803
|
+
(
|
|
804
|
+
'update an operation via PUT /Operation/:Hash',
|
|
805
|
+
async function ()
|
|
806
|
+
{
|
|
807
|
+
this.timeout(10000);
|
|
808
|
+
|
|
809
|
+
let tmpResponse = await _Page.evaluate(async (pBaseURL, pHash) =>
|
|
810
|
+
{
|
|
811
|
+
let tmpRes = await fetch(pBaseURL + '/Operation/' + pHash,
|
|
812
|
+
{
|
|
813
|
+
method: 'PUT',
|
|
814
|
+
headers: { 'Content-Type': 'application/json' },
|
|
815
|
+
body: JSON.stringify({ Name: 'Updated CRUD Test' })
|
|
816
|
+
});
|
|
817
|
+
return tmpRes.json();
|
|
818
|
+
}, _BaseURL, _CreatedOpHash);
|
|
819
|
+
|
|
820
|
+
libAssert.strictEqual(tmpResponse.Name, 'Updated CRUD Test', 'Name should be updated');
|
|
821
|
+
|
|
822
|
+
_TestResults.passed++;
|
|
823
|
+
}
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
test
|
|
827
|
+
(
|
|
828
|
+
'delete an operation via DELETE /Operation/:Hash',
|
|
829
|
+
async function ()
|
|
830
|
+
{
|
|
831
|
+
this.timeout(10000);
|
|
832
|
+
|
|
833
|
+
let tmpResponse = (await apiDelete('/Operation/' + _CreatedOpHash)).body;
|
|
834
|
+
libAssert.strictEqual(tmpResponse.Status, 'Deleted', 'Should confirm deletion');
|
|
835
|
+
|
|
836
|
+
let tmpGetResponse = await apiGet('/Operation/' + _CreatedOpHash);
|
|
837
|
+
libAssert.strictEqual(tmpGetResponse.status, 404, 'Should return 404 after deletion');
|
|
838
|
+
|
|
839
|
+
_TestResults.passed++;
|
|
840
|
+
}
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
// ════════════════════════════════════════════════
|
|
846
|
+
// Workflow 1: Simple File Copy (read-file → write-file)
|
|
847
|
+
// ════════════════════════════════════════════════
|
|
848
|
+
suite
|
|
849
|
+
(
|
|
850
|
+
'Workflow: Simple File Copy',
|
|
851
|
+
function ()
|
|
852
|
+
{
|
|
853
|
+
let _OpHash = '';
|
|
854
|
+
let _InputPath = '';
|
|
855
|
+
|
|
856
|
+
test
|
|
857
|
+
(
|
|
858
|
+
'create input file and save operation',
|
|
859
|
+
async function ()
|
|
860
|
+
{
|
|
861
|
+
this.timeout(15000);
|
|
862
|
+
|
|
863
|
+
let tmpTestDir = libPath.join(_StagingDir, 'file_copy_test');
|
|
864
|
+
libFs.mkdirSync(tmpTestDir, { recursive: true });
|
|
865
|
+
|
|
866
|
+
_InputPath = libPath.join(tmpTestDir, 'source.txt');
|
|
867
|
+
libFs.writeFileSync(_InputPath, 'This is the source file content.\nLine two.\n', 'utf8');
|
|
868
|
+
|
|
869
|
+
let tmpResponse = await apiCreateOperation({
|
|
870
|
+
Name: 'File Copy Test',
|
|
871
|
+
Description: 'Reads a file and writes its content to another file.',
|
|
872
|
+
Graph: {
|
|
873
|
+
Nodes: [
|
|
874
|
+
{ Hash: 'n-start', Type: 'start', X: 0, Y: 0 },
|
|
875
|
+
{
|
|
876
|
+
Hash: 'n-read', Type: 'read-file',
|
|
877
|
+
Settings: { FilePath: _InputPath, Encoding: 'utf8' },
|
|
878
|
+
Ports: [], X: 200, Y: 0
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
Hash: 'n-write', Type: 'write-file',
|
|
882
|
+
Settings: { FilePath: libPath.join(tmpTestDir, 'copy.txt'), Encoding: 'utf8' },
|
|
883
|
+
Ports: [], X: 400, Y: 0
|
|
884
|
+
},
|
|
885
|
+
{ Hash: 'n-end', Type: 'end', X: 600, Y: 0 }
|
|
886
|
+
],
|
|
887
|
+
Connections: [
|
|
888
|
+
{ Hash: 'c1', ConnectionType: 'Event', SourceNodeHash: 'n-start', SourcePortHash: 'n-start-eo-Start', TargetNodeHash: 'n-read', TargetPortHash: 'n-read-ei-BeginRead' },
|
|
889
|
+
{ Hash: 'c2', ConnectionType: 'Event', SourceNodeHash: 'n-read', SourcePortHash: 'n-read-eo-ReadComplete', TargetNodeHash: 'n-write', TargetPortHash: 'n-write-ei-BeginWrite' },
|
|
890
|
+
{ Hash: 'c3', ConnectionType: 'State', SourceNodeHash: 'n-read', SourcePortHash: 'n-read-so-FileContent', TargetNodeHash: 'n-write', TargetPortHash: 'n-write-si-Content' },
|
|
891
|
+
{ Hash: 'c4', ConnectionType: 'Event', SourceNodeHash: 'n-write', SourcePortHash: 'n-write-eo-WriteComplete', TargetNodeHash: 'n-end', TargetPortHash: 'n-end-ei-End' }
|
|
892
|
+
],
|
|
893
|
+
ViewState: {}
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
_OpHash = tmpResponse.Hash;
|
|
898
|
+
libAssert.ok(_OpHash, 'Operation should be created');
|
|
899
|
+
console.log(' File Copy op:', _OpHash);
|
|
900
|
+
|
|
901
|
+
_TestResults.passed++;
|
|
902
|
+
}
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
test
|
|
906
|
+
(
|
|
907
|
+
'execute file copy operation and verify output',
|
|
908
|
+
async function ()
|
|
909
|
+
{
|
|
910
|
+
this.timeout(15000);
|
|
911
|
+
|
|
912
|
+
let tmpResult = await apiExecuteOperation(_OpHash);
|
|
913
|
+
|
|
914
|
+
libAssert.strictEqual(tmpResult.body.Status, 'Complete', 'File copy should complete');
|
|
915
|
+
libAssert.ok(tmpResult.body.TaskOutputs['n-read'], 'Should have read-file outputs');
|
|
916
|
+
libAssert.ok(tmpResult.body.TaskOutputs['n-read'].BytesRead > 0, 'Should have read bytes');
|
|
917
|
+
libAssert.ok(tmpResult.body.TaskOutputs['n-write'], 'Should have write-file outputs');
|
|
918
|
+
libAssert.ok(tmpResult.body.TaskOutputs['n-write'].BytesWritten > 0, 'Should have written bytes');
|
|
919
|
+
|
|
920
|
+
console.log(' Read', tmpResult.body.TaskOutputs['n-read'].BytesRead, 'bytes, wrote',
|
|
921
|
+
tmpResult.body.TaskOutputs['n-write'].BytesWritten, 'bytes');
|
|
922
|
+
|
|
923
|
+
_TestResults.passed++;
|
|
924
|
+
}
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
);
|
|
928
|
+
|
|
929
|
+
// ════════════════════════════════════════════════
|
|
930
|
+
// Workflow 2: State Template Transform
|
|
931
|
+
// (read-file → write-file with template prepend)
|
|
932
|
+
// ════════════════════════════════════════════════
|
|
933
|
+
suite
|
|
934
|
+
(
|
|
935
|
+
'Workflow: State Template Transform',
|
|
936
|
+
function ()
|
|
937
|
+
{
|
|
938
|
+
let _OpHash = '';
|
|
939
|
+
let _InputPath = '';
|
|
940
|
+
let _OutputPath = '';
|
|
941
|
+
|
|
942
|
+
test
|
|
943
|
+
(
|
|
944
|
+
'create and execute template transform operation',
|
|
945
|
+
async function ()
|
|
946
|
+
{
|
|
947
|
+
this.timeout(15000);
|
|
948
|
+
|
|
949
|
+
let tmpTestDir = libPath.join(_StagingDir, 'template_test');
|
|
950
|
+
libFs.mkdirSync(tmpTestDir, { recursive: true });
|
|
951
|
+
|
|
952
|
+
_InputPath = libPath.join(tmpTestDir, 'input.txt');
|
|
953
|
+
_OutputPath = libPath.join(tmpTestDir, 'output.txt');
|
|
954
|
+
libFs.writeFileSync(_InputPath, 'Original content here.', 'utf8');
|
|
955
|
+
|
|
956
|
+
let tmpResponse = await apiCreateOperation({
|
|
957
|
+
Name: 'Template Transform',
|
|
958
|
+
Description: 'Reads a file and prepends a header via state template.',
|
|
959
|
+
Graph: {
|
|
960
|
+
Nodes: [
|
|
961
|
+
{ Hash: 'n-start', Type: 'start', X: 0, Y: 0 },
|
|
962
|
+
{
|
|
963
|
+
Hash: 'n-read', Type: 'read-file',
|
|
964
|
+
Settings: { FilePath: _InputPath, Encoding: 'utf8' },
|
|
965
|
+
Ports: [], X: 200, Y: 0
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
Hash: 'n-write', Type: 'write-file',
|
|
969
|
+
Settings: { FilePath: _OutputPath, Encoding: 'utf8' },
|
|
970
|
+
Ports: [], X: 400, Y: 0
|
|
971
|
+
},
|
|
972
|
+
{ Hash: 'n-end', Type: 'end', X: 600, Y: 0 }
|
|
973
|
+
],
|
|
974
|
+
Connections: [
|
|
975
|
+
{ Hash: 'c1', ConnectionType: 'Event', SourceNodeHash: 'n-start', SourcePortHash: 'n-start-eo-Start', TargetNodeHash: 'n-read', TargetPortHash: 'n-read-ei-BeginRead' },
|
|
976
|
+
{ Hash: 'c2', ConnectionType: 'Event', SourceNodeHash: 'n-read', SourcePortHash: 'n-read-eo-ReadComplete', TargetNodeHash: 'n-write', TargetPortHash: 'n-write-ei-BeginWrite' },
|
|
977
|
+
// State connection WITH template: prepend "HEADER: " to the file content
|
|
978
|
+
{
|
|
979
|
+
Hash: 'c3', ConnectionType: 'State',
|
|
980
|
+
SourceNodeHash: 'n-read', SourcePortHash: 'n-read-so-FileContent',
|
|
981
|
+
TargetNodeHash: 'n-write', TargetPortHash: 'n-write-si-Content',
|
|
982
|
+
Data: { Template: 'HEADER: {~D:Record.Value~}' }
|
|
983
|
+
},
|
|
984
|
+
{ Hash: 'c4', ConnectionType: 'Event', SourceNodeHash: 'n-write', SourcePortHash: 'n-write-eo-WriteComplete', TargetNodeHash: 'n-end', TargetPortHash: 'n-end-ei-End' }
|
|
985
|
+
],
|
|
986
|
+
ViewState: {}
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
_OpHash = tmpResponse.Hash;
|
|
991
|
+
let tmpResult = await apiExecuteOperation(_OpHash);
|
|
992
|
+
|
|
993
|
+
libAssert.strictEqual(tmpResult.body.Status, 'Complete', 'Template transform should complete');
|
|
994
|
+
|
|
995
|
+
// Verify the output file has the header prepended
|
|
996
|
+
libAssert.ok(libFs.existsSync(_OutputPath), 'Output file should exist');
|
|
997
|
+
let tmpContent = libFs.readFileSync(_OutputPath, 'utf8');
|
|
998
|
+
|
|
999
|
+
libAssert.ok(tmpContent.startsWith('HEADER: '), 'Output should start with HEADER: prefix');
|
|
1000
|
+
libAssert.ok(tmpContent.includes('Original content here.'), 'Output should contain original content');
|
|
1001
|
+
console.log(' Template output:', JSON.stringify(tmpContent));
|
|
1002
|
+
|
|
1003
|
+
_TestResults.passed++;
|
|
1004
|
+
}
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
);
|
|
1008
|
+
|
|
1009
|
+
// ════════════════════════════════════════════════
|
|
1010
|
+
// Workflow 3: Conditional Branching
|
|
1011
|
+
// (set-values → if-conditional → true/false write paths)
|
|
1012
|
+
// ════════════════════════════════════════════════
|
|
1013
|
+
suite
|
|
1014
|
+
(
|
|
1015
|
+
'Workflow: Conditional Branching',
|
|
1016
|
+
function ()
|
|
1017
|
+
{
|
|
1018
|
+
test
|
|
1019
|
+
(
|
|
1020
|
+
'execute true-branch when condition matches',
|
|
1021
|
+
async function ()
|
|
1022
|
+
{
|
|
1023
|
+
this.timeout(15000);
|
|
1024
|
+
|
|
1025
|
+
let tmpTestDir = libPath.join(_StagingDir, 'branch_true_test');
|
|
1026
|
+
libFs.mkdirSync(tmpTestDir, { recursive: true });
|
|
1027
|
+
let tmpTruePath = libPath.join(tmpTestDir, 'true_result.txt');
|
|
1028
|
+
let tmpFalsePath = libPath.join(tmpTestDir, 'false_result.txt');
|
|
1029
|
+
|
|
1030
|
+
let tmpResponse = await apiCreateOperation({
|
|
1031
|
+
Name: 'Branch True Test',
|
|
1032
|
+
Description: 'SetValues sets status=active, IfConditional branches to True.',
|
|
1033
|
+
Graph: {
|
|
1034
|
+
Nodes: [
|
|
1035
|
+
{ Hash: 'n-start', Type: 'start', X: 0, Y: 0 },
|
|
1036
|
+
{
|
|
1037
|
+
Hash: 'n-set', Type: 'set-values',
|
|
1038
|
+
Settings: { Mappings: [{ Address: 'Operation.Status', Value: 'active' }] },
|
|
1039
|
+
Ports: [], X: 200, Y: 0
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
Hash: 'n-if', Type: 'if-conditional',
|
|
1043
|
+
Settings: { DataAddress: 'Operation.Status', CompareValue: 'active', Operator: '==' },
|
|
1044
|
+
Ports: [], X: 400, Y: 0
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
Hash: 'n-write-t', Type: 'write-file',
|
|
1048
|
+
Settings: { FilePath: tmpTruePath, Content: 'Condition was TRUE', Encoding: 'utf8' },
|
|
1049
|
+
Ports: [], X: 600, Y: -100
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
Hash: 'n-write-f', Type: 'write-file',
|
|
1053
|
+
Settings: { FilePath: tmpFalsePath, Content: 'Condition was FALSE', Encoding: 'utf8' },
|
|
1054
|
+
Ports: [], X: 600, Y: 100
|
|
1055
|
+
},
|
|
1056
|
+
{ Hash: 'n-end', Type: 'end', X: 800, Y: 0 }
|
|
1057
|
+
],
|
|
1058
|
+
Connections: [
|
|
1059
|
+
{ Hash: 'c1', ConnectionType: 'Event', SourceNodeHash: 'n-start', SourcePortHash: 'n-start-eo-Start', TargetNodeHash: 'n-set', TargetPortHash: 'n-set-ei-Execute' },
|
|
1060
|
+
{ Hash: 'c2', ConnectionType: 'Event', SourceNodeHash: 'n-set', SourcePortHash: 'n-set-eo-Complete', TargetNodeHash: 'n-if', TargetPortHash: 'n-if-ei-Evaluate' },
|
|
1061
|
+
{ Hash: 'c3', ConnectionType: 'Event', SourceNodeHash: 'n-if', SourcePortHash: 'n-if-eo-True', TargetNodeHash: 'n-write-t', TargetPortHash: 'n-write-t-ei-BeginWrite' },
|
|
1062
|
+
{ Hash: 'c4', ConnectionType: 'Event', SourceNodeHash: 'n-if', SourcePortHash: 'n-if-eo-False', TargetNodeHash: 'n-write-f', TargetPortHash: 'n-write-f-ei-BeginWrite' },
|
|
1063
|
+
{ Hash: 'c5', ConnectionType: 'Event', SourceNodeHash: 'n-write-t', SourcePortHash: 'n-write-t-eo-WriteComplete', TargetNodeHash: 'n-end', TargetPortHash: 'n-end-ei-End' },
|
|
1064
|
+
{ Hash: 'c6', ConnectionType: 'Event', SourceNodeHash: 'n-write-f', SourcePortHash: 'n-write-f-eo-WriteComplete', TargetNodeHash: 'n-end', TargetPortHash: 'n-end-ei-End' }
|
|
1065
|
+
],
|
|
1066
|
+
ViewState: {}
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
let tmpResult = await apiExecuteOperation(tmpResponse.Hash);
|
|
1071
|
+
|
|
1072
|
+
libAssert.strictEqual(tmpResult.body.Status, 'Complete', 'Branch should complete');
|
|
1073
|
+
libAssert.ok(libFs.existsSync(tmpTruePath), 'True branch file should exist');
|
|
1074
|
+
libAssert.ok(!libFs.existsSync(tmpFalsePath), 'False branch file should NOT exist');
|
|
1075
|
+
|
|
1076
|
+
let tmpContent = libFs.readFileSync(tmpTruePath, 'utf8');
|
|
1077
|
+
libAssert.strictEqual(tmpContent, 'Condition was TRUE', 'True branch content should match');
|
|
1078
|
+
|
|
1079
|
+
console.log(' Branch result: TRUE path taken as expected');
|
|
1080
|
+
|
|
1081
|
+
_TestResults.passed++;
|
|
1082
|
+
}
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
test
|
|
1086
|
+
(
|
|
1087
|
+
'execute false-branch when condition does not match',
|
|
1088
|
+
async function ()
|
|
1089
|
+
{
|
|
1090
|
+
this.timeout(15000);
|
|
1091
|
+
|
|
1092
|
+
let tmpTestDir = libPath.join(_StagingDir, 'branch_false_test');
|
|
1093
|
+
libFs.mkdirSync(tmpTestDir, { recursive: true });
|
|
1094
|
+
let tmpTruePath = libPath.join(tmpTestDir, 'true_result.txt');
|
|
1095
|
+
let tmpFalsePath = libPath.join(tmpTestDir, 'false_result.txt');
|
|
1096
|
+
|
|
1097
|
+
let tmpResponse = await apiCreateOperation({
|
|
1098
|
+
Name: 'Branch False Test',
|
|
1099
|
+
Description: 'SetValues sets status=inactive, IfConditional branches to False.',
|
|
1100
|
+
Graph: {
|
|
1101
|
+
Nodes: [
|
|
1102
|
+
{ Hash: 'n-start', Type: 'start', X: 0, Y: 0 },
|
|
1103
|
+
{
|
|
1104
|
+
Hash: 'n-set', Type: 'set-values',
|
|
1105
|
+
Settings: { Mappings: [{ Address: 'Operation.Status', Value: 'inactive' }] },
|
|
1106
|
+
Ports: [], X: 200, Y: 0
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
Hash: 'n-if', Type: 'if-conditional',
|
|
1110
|
+
Settings: { DataAddress: 'Operation.Status', CompareValue: 'active', Operator: '==' },
|
|
1111
|
+
Ports: [], X: 400, Y: 0
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
Hash: 'n-write-t', Type: 'write-file',
|
|
1115
|
+
Settings: { FilePath: tmpTruePath, Content: 'TRUE', Encoding: 'utf8' },
|
|
1116
|
+
Ports: [], X: 600, Y: -100
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
Hash: 'n-write-f', Type: 'write-file',
|
|
1120
|
+
Settings: { FilePath: tmpFalsePath, Content: 'FALSE', Encoding: 'utf8' },
|
|
1121
|
+
Ports: [], X: 600, Y: 100
|
|
1122
|
+
},
|
|
1123
|
+
{ Hash: 'n-end', Type: 'end', X: 800, Y: 0 }
|
|
1124
|
+
],
|
|
1125
|
+
Connections: [
|
|
1126
|
+
{ Hash: 'c1', ConnectionType: 'Event', SourceNodeHash: 'n-start', SourcePortHash: 'n-start-eo-Start', TargetNodeHash: 'n-set', TargetPortHash: 'n-set-ei-Execute' },
|
|
1127
|
+
{ Hash: 'c2', ConnectionType: 'Event', SourceNodeHash: 'n-set', SourcePortHash: 'n-set-eo-Complete', TargetNodeHash: 'n-if', TargetPortHash: 'n-if-ei-Evaluate' },
|
|
1128
|
+
{ Hash: 'c3', ConnectionType: 'Event', SourceNodeHash: 'n-if', SourcePortHash: 'n-if-eo-True', TargetNodeHash: 'n-write-t', TargetPortHash: 'n-write-t-ei-BeginWrite' },
|
|
1129
|
+
{ Hash: 'c4', ConnectionType: 'Event', SourceNodeHash: 'n-if', SourcePortHash: 'n-if-eo-False', TargetNodeHash: 'n-write-f', TargetPortHash: 'n-write-f-ei-BeginWrite' },
|
|
1130
|
+
{ Hash: 'c5', ConnectionType: 'Event', SourceNodeHash: 'n-write-t', SourcePortHash: 'n-write-t-eo-WriteComplete', TargetNodeHash: 'n-end', TargetPortHash: 'n-end-ei-End' },
|
|
1131
|
+
{ Hash: 'c6', ConnectionType: 'Event', SourceNodeHash: 'n-write-f', SourcePortHash: 'n-write-f-eo-WriteComplete', TargetNodeHash: 'n-end', TargetPortHash: 'n-end-ei-End' }
|
|
1132
|
+
],
|
|
1133
|
+
ViewState: {}
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
let tmpResult = await apiExecuteOperation(tmpResponse.Hash);
|
|
1138
|
+
|
|
1139
|
+
libAssert.strictEqual(tmpResult.body.Status, 'Complete', 'Branch should complete');
|
|
1140
|
+
libAssert.ok(!libFs.existsSync(tmpTruePath), 'True branch file should NOT exist');
|
|
1141
|
+
libAssert.ok(libFs.existsSync(tmpFalsePath), 'False branch file should exist');
|
|
1142
|
+
|
|
1143
|
+
let tmpContent = libFs.readFileSync(tmpFalsePath, 'utf8');
|
|
1144
|
+
libAssert.strictEqual(tmpContent, 'FALSE', 'False branch content should match');
|
|
1145
|
+
|
|
1146
|
+
console.log(' Branch result: FALSE path taken as expected');
|
|
1147
|
+
|
|
1148
|
+
_TestResults.passed++;
|
|
1149
|
+
}
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
);
|
|
1153
|
+
|
|
1154
|
+
// ════════════════════════════════════════════════
|
|
1155
|
+
// Workflow 4: Error Handling
|
|
1156
|
+
// (read-file error → error-message)
|
|
1157
|
+
// ════════════════════════════════════════════════
|
|
1158
|
+
suite
|
|
1159
|
+
(
|
|
1160
|
+
'Workflow: Error Handling',
|
|
1161
|
+
function ()
|
|
1162
|
+
{
|
|
1163
|
+
test
|
|
1164
|
+
(
|
|
1165
|
+
'read-file error triggers error-message node',
|
|
1166
|
+
async function ()
|
|
1167
|
+
{
|
|
1168
|
+
this.timeout(15000);
|
|
1169
|
+
|
|
1170
|
+
let tmpTestDir = libPath.join(_StagingDir, 'error_test');
|
|
1171
|
+
libFs.mkdirSync(tmpTestDir, { recursive: true });
|
|
1172
|
+
let tmpFallbackPath = libPath.join(tmpTestDir, 'fallback.txt');
|
|
1173
|
+
|
|
1174
|
+
let tmpResponse = await apiCreateOperation({
|
|
1175
|
+
Name: 'Error Handling Test',
|
|
1176
|
+
Description: 'ReadFile with bad path fires Error, handled by ErrorMessage + fallback write.',
|
|
1177
|
+
Graph: {
|
|
1178
|
+
Nodes: [
|
|
1179
|
+
{ Hash: 'n-start', Type: 'start', X: 0, Y: 0 },
|
|
1180
|
+
{
|
|
1181
|
+
Hash: 'n-read', Type: 'read-file',
|
|
1182
|
+
Settings: { FilePath: '/nonexistent/path/missing.txt', Encoding: 'utf8' },
|
|
1183
|
+
Ports: [], X: 200, Y: 0
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
Hash: 'n-err', Type: 'error-message',
|
|
1187
|
+
Settings: { MessageTemplate: 'File read failed — using fallback.' },
|
|
1188
|
+
Ports: [], X: 400, Y: 100
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
Hash: 'n-fallback', Type: 'write-file',
|
|
1192
|
+
Settings: { FilePath: tmpFallbackPath, Content: 'Fallback content after error', Encoding: 'utf8' },
|
|
1193
|
+
Ports: [], X: 600, Y: 100
|
|
1194
|
+
},
|
|
1195
|
+
{ Hash: 'n-end', Type: 'end', X: 800, Y: 0 }
|
|
1196
|
+
],
|
|
1197
|
+
Connections: [
|
|
1198
|
+
{ Hash: 'c1', ConnectionType: 'Event', SourceNodeHash: 'n-start', SourcePortHash: 'n-start-eo-Start', TargetNodeHash: 'n-read', TargetPortHash: 'n-read-ei-BeginRead' },
|
|
1199
|
+
// Error path: read error → error-message
|
|
1200
|
+
{ Hash: 'c2', ConnectionType: 'Event', SourceNodeHash: 'n-read', SourcePortHash: 'n-read-eo-Error', TargetNodeHash: 'n-err', TargetPortHash: 'n-err-ei-Trigger' },
|
|
1201
|
+
// After error message → write fallback
|
|
1202
|
+
{ Hash: 'c3', ConnectionType: 'Event', SourceNodeHash: 'n-err', SourcePortHash: 'n-err-eo-Complete', TargetNodeHash: 'n-fallback', TargetPortHash: 'n-fallback-ei-BeginWrite' },
|
|
1203
|
+
{ Hash: 'c4', ConnectionType: 'Event', SourceNodeHash: 'n-fallback', SourcePortHash: 'n-fallback-eo-WriteComplete', TargetNodeHash: 'n-end', TargetPortHash: 'n-end-ei-End' }
|
|
1204
|
+
],
|
|
1205
|
+
ViewState: {}
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
let tmpResult = await apiExecuteOperation(tmpResponse.Hash);
|
|
1210
|
+
|
|
1211
|
+
libAssert.strictEqual(tmpResult.body.Status, 'Complete', 'Error-handled operation should complete');
|
|
1212
|
+
|
|
1213
|
+
// Verify fallback was written
|
|
1214
|
+
libAssert.ok(libFs.existsSync(tmpFallbackPath), 'Fallback file should be written');
|
|
1215
|
+
let tmpContent = libFs.readFileSync(tmpFallbackPath, 'utf8');
|
|
1216
|
+
libAssert.strictEqual(tmpContent, 'Fallback content after error', 'Fallback content should match');
|
|
1217
|
+
|
|
1218
|
+
// Verify the error was logged
|
|
1219
|
+
let tmpLogs = tmpResult.body.Log || [];
|
|
1220
|
+
let tmpHasErrorLog = tmpLogs.some(function (pL) { return pL.includes('File read failed'); });
|
|
1221
|
+
libAssert.ok(tmpHasErrorLog, 'Error message should appear in logs');
|
|
1222
|
+
|
|
1223
|
+
console.log(' Error handling: fallback written, error logged');
|
|
1224
|
+
|
|
1225
|
+
_TestResults.passed++;
|
|
1226
|
+
}
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
);
|
|
1230
|
+
|
|
1231
|
+
// ════════════════════════════════════════════════
|
|
1232
|
+
// Workflow 5: Multi-Value SetValues
|
|
1233
|
+
// (set-values with multiple addresses → if-conditional with contains)
|
|
1234
|
+
// ════════════════════════════════════════════════
|
|
1235
|
+
suite
|
|
1236
|
+
(
|
|
1237
|
+
'Workflow: Multi-Value SetValues with Contains Operator',
|
|
1238
|
+
function ()
|
|
1239
|
+
{
|
|
1240
|
+
test
|
|
1241
|
+
(
|
|
1242
|
+
'set multiple values and use contains operator in conditional',
|
|
1243
|
+
async function ()
|
|
1244
|
+
{
|
|
1245
|
+
this.timeout(15000);
|
|
1246
|
+
|
|
1247
|
+
let tmpTestDir = libPath.join(_StagingDir, 'setvals_test');
|
|
1248
|
+
libFs.mkdirSync(tmpTestDir, { recursive: true });
|
|
1249
|
+
let tmpResultPath = libPath.join(tmpTestDir, 'result.txt');
|
|
1250
|
+
|
|
1251
|
+
let tmpResponse = await apiCreateOperation({
|
|
1252
|
+
Name: 'Multi SetValues Test',
|
|
1253
|
+
Description: 'Sets multiple operation values, then uses contains to check.',
|
|
1254
|
+
Graph: {
|
|
1255
|
+
Nodes: [
|
|
1256
|
+
{ Hash: 'n-start', Type: 'start', X: 0, Y: 0 },
|
|
1257
|
+
{
|
|
1258
|
+
Hash: 'n-set', Type: 'set-values',
|
|
1259
|
+
Settings: {
|
|
1260
|
+
Mappings: [
|
|
1261
|
+
{ Address: 'Operation.Greeting', Value: 'Hello World from Ultravisor' },
|
|
1262
|
+
{ Address: 'Operation.Counter', Value: 42 },
|
|
1263
|
+
{ Address: 'Global.AppVersion', Value: '2.0' }
|
|
1264
|
+
]
|
|
1265
|
+
},
|
|
1266
|
+
Ports: [], X: 200, Y: 0
|
|
1267
|
+
},
|
|
1268
|
+
{
|
|
1269
|
+
Hash: 'n-if', Type: 'if-conditional',
|
|
1270
|
+
Settings: { DataAddress: 'Operation.Greeting', CompareValue: 'Ultravisor', Operator: 'contains' },
|
|
1271
|
+
Ports: [], X: 400, Y: 0
|
|
1272
|
+
},
|
|
1273
|
+
{
|
|
1274
|
+
Hash: 'n-write', Type: 'write-file',
|
|
1275
|
+
Settings: { FilePath: tmpResultPath, Content: 'Contains matched', Encoding: 'utf8' },
|
|
1276
|
+
Ports: [], X: 600, Y: 0
|
|
1277
|
+
},
|
|
1278
|
+
{ Hash: 'n-end', Type: 'end', X: 800, Y: 0 }
|
|
1279
|
+
],
|
|
1280
|
+
Connections: [
|
|
1281
|
+
{ Hash: 'c1', ConnectionType: 'Event', SourceNodeHash: 'n-start', SourcePortHash: 'n-start-eo-Start', TargetNodeHash: 'n-set', TargetPortHash: 'n-set-ei-Execute' },
|
|
1282
|
+
{ Hash: 'c2', ConnectionType: 'Event', SourceNodeHash: 'n-set', SourcePortHash: 'n-set-eo-Complete', TargetNodeHash: 'n-if', TargetPortHash: 'n-if-ei-Evaluate' },
|
|
1283
|
+
{ Hash: 'c3', ConnectionType: 'Event', SourceNodeHash: 'n-if', SourcePortHash: 'n-if-eo-True', TargetNodeHash: 'n-write', TargetPortHash: 'n-write-ei-BeginWrite' },
|
|
1284
|
+
{ Hash: 'c4', ConnectionType: 'Event', SourceNodeHash: 'n-write', SourcePortHash: 'n-write-eo-WriteComplete', TargetNodeHash: 'n-end', TargetPortHash: 'n-end-ei-End' }
|
|
1285
|
+
],
|
|
1286
|
+
ViewState: {}
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
let tmpResult = await apiExecuteOperation(tmpResponse.Hash);
|
|
1291
|
+
|
|
1292
|
+
libAssert.strictEqual(tmpResult.body.Status, 'Complete', 'Multi-value operation should complete');
|
|
1293
|
+
libAssert.ok(libFs.existsSync(tmpResultPath), 'Result file should exist (contains matched)');
|
|
1294
|
+
|
|
1295
|
+
let tmpContent = libFs.readFileSync(tmpResultPath, 'utf8');
|
|
1296
|
+
libAssert.strictEqual(tmpContent, 'Contains matched', 'Contains operator should work');
|
|
1297
|
+
|
|
1298
|
+
console.log(' Multi-SetValues with contains: passed');
|
|
1299
|
+
|
|
1300
|
+
_TestResults.passed++;
|
|
1301
|
+
}
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
);
|
|
1305
|
+
|
|
1306
|
+
// ════════════════════════════════════════════════
|
|
1307
|
+
// Workflow 6: String Replace Pipeline
|
|
1308
|
+
// (read → replace → write via API)
|
|
1309
|
+
// ════════════════════════════════════════════════
|
|
1310
|
+
suite
|
|
1311
|
+
(
|
|
1312
|
+
'Workflow: String Replace Pipeline',
|
|
1313
|
+
function ()
|
|
1314
|
+
{
|
|
1315
|
+
test
|
|
1316
|
+
(
|
|
1317
|
+
'replace all occurrences in a file',
|
|
1318
|
+
async function ()
|
|
1319
|
+
{
|
|
1320
|
+
this.timeout(15000);
|
|
1321
|
+
|
|
1322
|
+
let tmpTestDir = libPath.join(_StagingDir, 'replace_test');
|
|
1323
|
+
libFs.mkdirSync(tmpTestDir, { recursive: true });
|
|
1324
|
+
let tmpInputPath = libPath.join(tmpTestDir, 'input.txt');
|
|
1325
|
+
let tmpOutputPath = libPath.join(tmpTestDir, 'output.txt');
|
|
1326
|
+
libFs.writeFileSync(tmpInputPath, 'The quick brown fox jumps over the lazy fox. Fox is clever.', 'utf8');
|
|
1327
|
+
|
|
1328
|
+
let tmpResponse = await apiCreateOperation({
|
|
1329
|
+
Name: 'Replace Pipeline',
|
|
1330
|
+
Description: 'Reads file, replaces fox→cat, writes output.',
|
|
1331
|
+
Graph: {
|
|
1332
|
+
Nodes: [
|
|
1333
|
+
{ Hash: 'n-start', Type: 'start', X: 0, Y: 0 },
|
|
1334
|
+
{
|
|
1335
|
+
Hash: 'n-read', Type: 'read-file',
|
|
1336
|
+
Settings: { FilePath: tmpInputPath, Encoding: 'utf8' },
|
|
1337
|
+
Ports: [], X: 200, Y: 0
|
|
1338
|
+
},
|
|
1339
|
+
{
|
|
1340
|
+
Hash: 'n-replace', Type: 'replace-string',
|
|
1341
|
+
Settings: { SearchString: 'fox', ReplaceString: 'cat' },
|
|
1342
|
+
Ports: [], X: 400, Y: 0
|
|
1343
|
+
},
|
|
1344
|
+
{
|
|
1345
|
+
Hash: 'n-write', Type: 'write-file',
|
|
1346
|
+
Settings: { FilePath: tmpOutputPath, Encoding: 'utf8' },
|
|
1347
|
+
Ports: [], X: 600, Y: 0
|
|
1348
|
+
},
|
|
1349
|
+
{ Hash: 'n-end', Type: 'end', X: 800, Y: 0 }
|
|
1350
|
+
],
|
|
1351
|
+
Connections: [
|
|
1352
|
+
{ Hash: 'c1', ConnectionType: 'Event', SourceNodeHash: 'n-start', SourcePortHash: 'n-start-eo-Start', TargetNodeHash: 'n-read', TargetPortHash: 'n-read-ei-BeginRead' },
|
|
1353
|
+
{ Hash: 'c2', ConnectionType: 'Event', SourceNodeHash: 'n-read', SourcePortHash: 'n-read-eo-ReadComplete', TargetNodeHash: 'n-replace', TargetPortHash: 'n-replace-ei-Replace' },
|
|
1354
|
+
{ Hash: 'c3', ConnectionType: 'State', SourceNodeHash: 'n-read', SourcePortHash: 'n-read-so-FileContent', TargetNodeHash: 'n-replace', TargetPortHash: 'n-replace-si-InputString' },
|
|
1355
|
+
{ Hash: 'c4', ConnectionType: 'Event', SourceNodeHash: 'n-replace', SourcePortHash: 'n-replace-eo-ReplaceComplete', TargetNodeHash: 'n-write', TargetPortHash: 'n-write-ei-BeginWrite' },
|
|
1356
|
+
{ Hash: 'c5', ConnectionType: 'State', SourceNodeHash: 'n-replace', SourcePortHash: 'n-replace-so-ReplacedString', TargetNodeHash: 'n-write', TargetPortHash: 'n-write-si-Content' },
|
|
1357
|
+
{ Hash: 'c6', ConnectionType: 'Event', SourceNodeHash: 'n-write', SourcePortHash: 'n-write-eo-WriteComplete', TargetNodeHash: 'n-end', TargetPortHash: 'n-end-ei-End' }
|
|
1358
|
+
],
|
|
1359
|
+
ViewState: {}
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
let tmpResult = await apiExecuteOperation(tmpResponse.Hash);
|
|
1364
|
+
|
|
1365
|
+
libAssert.strictEqual(tmpResult.body.Status, 'Complete', 'Replace pipeline should complete');
|
|
1366
|
+
|
|
1367
|
+
let tmpOutput = libFs.readFileSync(tmpOutputPath, 'utf8');
|
|
1368
|
+
libAssert.ok(tmpOutput.includes('cat'), 'Output should contain "cat"');
|
|
1369
|
+
libAssert.ok(!tmpOutput.includes('fox'), 'Output should not contain "fox"');
|
|
1370
|
+
libAssert.ok(tmpOutput.includes('The quick brown cat'), 'First replacement should match');
|
|
1371
|
+
|
|
1372
|
+
// Note: Fox (capital) won't be replaced since replace-string is case-sensitive
|
|
1373
|
+
console.log(' Replace output:', JSON.stringify(tmpOutput));
|
|
1374
|
+
|
|
1375
|
+
_TestResults.passed++;
|
|
1376
|
+
}
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
);
|
|
1380
|
+
|
|
1381
|
+
// ════════════════════════════════════════════════
|
|
1382
|
+
// Workflow 7: Looping Pipeline (via Flow Editor)
|
|
1383
|
+
// (read → split → replace → append → write)
|
|
1384
|
+
// ════════════════════════════════════════════════
|
|
1385
|
+
suite
|
|
1386
|
+
(
|
|
1387
|
+
'Workflow: Looping Pipeline via Flow Editor',
|
|
1388
|
+
function ()
|
|
1389
|
+
{
|
|
1390
|
+
let _WorkflowInputPath = '';
|
|
1391
|
+
let _WorkflowOutputPath = '';
|
|
1392
|
+
let _SavedOperationHash = '';
|
|
1393
|
+
|
|
1394
|
+
test
|
|
1395
|
+
(
|
|
1396
|
+
'create test input file for workflow',
|
|
1397
|
+
function ()
|
|
1398
|
+
{
|
|
1399
|
+
this.timeout(5000);
|
|
1400
|
+
|
|
1401
|
+
let tmpStagingDir = libPath.join(_StagingDir, 'workflow_test');
|
|
1402
|
+
libFs.mkdirSync(tmpStagingDir, { recursive: true });
|
|
1403
|
+
|
|
1404
|
+
_WorkflowInputPath = libPath.join(tmpStagingDir, 'input.txt');
|
|
1405
|
+
_WorkflowOutputPath = libPath.join(tmpStagingDir, 'output.txt');
|
|
1406
|
+
|
|
1407
|
+
libFs.writeFileSync(_WorkflowInputPath,
|
|
1408
|
+
'Hello John Smith\nJohn went to the store\nMary and John had lunch\nNo match here\n',
|
|
1409
|
+
'utf8');
|
|
1410
|
+
|
|
1411
|
+
libAssert.ok(libFs.existsSync(_WorkflowInputPath), 'Input file should exist');
|
|
1412
|
+
_TestResults.passed++;
|
|
1413
|
+
}
|
|
1414
|
+
);
|
|
1415
|
+
|
|
1416
|
+
test
|
|
1417
|
+
(
|
|
1418
|
+
'navigate to flow editor and build workflow',
|
|
1419
|
+
async function ()
|
|
1420
|
+
{
|
|
1421
|
+
this.timeout(30000);
|
|
1422
|
+
|
|
1423
|
+
await _Page.evaluate(() =>
|
|
1424
|
+
{
|
|
1425
|
+
if (window._Pict && window._Pict.PictApplication)
|
|
1426
|
+
{
|
|
1427
|
+
window._Pict.PictApplication.navigateTo('/FlowEditor');
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
await settle(1500);
|
|
1431
|
+
|
|
1432
|
+
await takeScreenshot('workflow-editor-empty');
|
|
1433
|
+
|
|
1434
|
+
let tmpFlowData = await _Page.evaluate((pInputPath, pOutputPath) =>
|
|
1435
|
+
{
|
|
1436
|
+
let tmpGraph =
|
|
1437
|
+
{
|
|
1438
|
+
Nodes:
|
|
1439
|
+
[
|
|
1440
|
+
{
|
|
1441
|
+
Hash: 'wf-start', Type: 'start',
|
|
1442
|
+
X: 50, Y: 200, Width: 140, Height: 80,
|
|
1443
|
+
Title: 'Start',
|
|
1444
|
+
Ports: [{ Hash: 'wf-start-eo-Start', Direction: 'output', Side: 'right', Label: 'Out' }],
|
|
1445
|
+
Settings: {}
|
|
1446
|
+
},
|
|
1447
|
+
{
|
|
1448
|
+
Hash: 'wf-read', Type: 'read-file',
|
|
1449
|
+
X: 260, Y: 180, Width: 200, Height: 100,
|
|
1450
|
+
Title: 'Load Input File',
|
|
1451
|
+
Ports: [
|
|
1452
|
+
{ Hash: 'wf-read-ei-BeginRead', Direction: 'input', Side: 'left-bottom', Label: 'BeginRead' },
|
|
1453
|
+
{ Hash: 'wf-read-eo-ReadComplete', Direction: 'output', Side: 'right', Label: 'ReadComplete' },
|
|
1454
|
+
{ Hash: 'wf-read-so-FileContent', Direction: 'output', Side: 'right-top', Label: 'FileContent' },
|
|
1455
|
+
{ Hash: 'wf-read-eo-Error', Direction: 'output', Side: 'bottom', Label: 'Error' }
|
|
1456
|
+
],
|
|
1457
|
+
Settings: { FilePath: pInputPath, Encoding: 'utf8' }
|
|
1458
|
+
},
|
|
1459
|
+
{
|
|
1460
|
+
Hash: 'wf-split', Type: 'split-execute',
|
|
1461
|
+
X: 540, Y: 160, Width: 240, Height: 120,
|
|
1462
|
+
Title: 'Split Lines',
|
|
1463
|
+
Ports: [
|
|
1464
|
+
{ Hash: 'wf-split-ei-PerformSplit', Direction: 'input', Side: 'left-bottom', Label: 'PerformSplit' },
|
|
1465
|
+
{ Hash: 'wf-split-ei-StepComplete', Direction: 'input', Side: 'left-bottom', Label: 'StepComplete' },
|
|
1466
|
+
{ Hash: 'wf-split-si-InputString', Direction: 'input', Side: 'left-top', Label: 'InputString' },
|
|
1467
|
+
{ Hash: 'wf-split-eo-TokenDataSent', Direction: 'output', Side: 'right', Label: 'TokenDataSent' },
|
|
1468
|
+
{ Hash: 'wf-split-so-CurrentToken', Direction: 'output', Side: 'right-top', Label: 'CurrentToken' },
|
|
1469
|
+
{ Hash: 'wf-split-eo-CompletedAllSubtasks', Direction: 'output', Side: 'right-bottom', Label: 'CompletedAllSubtasks' }
|
|
1470
|
+
],
|
|
1471
|
+
Settings: {}
|
|
1472
|
+
},
|
|
1473
|
+
{
|
|
1474
|
+
Hash: 'wf-replace', Type: 'replace-string',
|
|
1475
|
+
X: 860, Y: 160, Width: 220, Height: 100,
|
|
1476
|
+
Title: 'Replace John with Jane',
|
|
1477
|
+
Ports: [
|
|
1478
|
+
{ Hash: 'wf-replace-si-InputString', Direction: 'input', Side: 'left-top', Label: 'InputString' },
|
|
1479
|
+
{ Hash: 'wf-replace-ei-Replace', Direction: 'input', Side: 'left-bottom', Label: 'Replace' },
|
|
1480
|
+
{ Hash: 'wf-replace-eo-ReplaceComplete', Direction: 'output', Side: 'right', Label: 'ReplaceComplete' },
|
|
1481
|
+
{ Hash: 'wf-replace-so-ReplacedString', Direction: 'output', Side: 'right-top', Label: 'ReplacedString' }
|
|
1482
|
+
],
|
|
1483
|
+
Settings: { SearchString: 'John', ReplaceString: 'Jane' }
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
Hash: 'wf-append', Type: 'string-appender',
|
|
1487
|
+
X: 1160, Y: 160, Width: 220, Height: 100,
|
|
1488
|
+
Title: 'Append Line',
|
|
1489
|
+
Ports: [
|
|
1490
|
+
{ Hash: 'wf-append-ei-Append', Direction: 'input', Side: 'left-bottom', Label: 'Append' },
|
|
1491
|
+
{ Hash: 'wf-append-si-InputString', Direction: 'input', Side: 'left-top', Label: 'InputString' },
|
|
1492
|
+
{ Hash: 'wf-append-eo-Completed', Direction: 'output', Side: 'right', Label: 'Completed' },
|
|
1493
|
+
{ Hash: 'wf-append-so-AppendedString', Direction: 'output', Side: 'right-top', Label: 'AppendedString' }
|
|
1494
|
+
],
|
|
1495
|
+
Settings: { OutputAddress: 'Operation.OutputFileContents', AppendNewline: true }
|
|
1496
|
+
},
|
|
1497
|
+
{
|
|
1498
|
+
Hash: 'wf-write', Type: 'write-file',
|
|
1499
|
+
X: 860, Y: 380, Width: 220, Height: 80,
|
|
1500
|
+
Title: 'Save Output File',
|
|
1501
|
+
Ports: [
|
|
1502
|
+
{ Hash: 'wf-write-si-Content', Direction: 'input', Side: 'left-top', Label: 'Content' },
|
|
1503
|
+
{ Hash: 'wf-write-ei-BeginWrite', Direction: 'input', Side: 'left-bottom', Label: 'BeginWrite' },
|
|
1504
|
+
{ Hash: 'wf-write-eo-WriteComplete', Direction: 'output', Side: 'right', Label: 'WriteComplete' },
|
|
1505
|
+
{ Hash: 'wf-write-eo-Error', Direction: 'output', Side: 'bottom', Label: 'Error' }
|
|
1506
|
+
],
|
|
1507
|
+
Settings: { FilePath: pOutputPath, Encoding: 'utf8' }
|
|
1508
|
+
},
|
|
1509
|
+
{
|
|
1510
|
+
Hash: 'wf-end', Type: 'end',
|
|
1511
|
+
X: 1160, Y: 380, Width: 140, Height: 80,
|
|
1512
|
+
Title: 'End',
|
|
1513
|
+
Ports: [{ Hash: 'wf-end-ei-In', Direction: 'input', Side: 'left-bottom', Label: 'In' }],
|
|
1514
|
+
Settings: {}
|
|
1515
|
+
}
|
|
1516
|
+
],
|
|
1517
|
+
Connections:
|
|
1518
|
+
[
|
|
1519
|
+
{ Hash: 'wf-ev1', ConnectionType: 'Event', SourceNodeHash: 'wf-start', SourcePortHash: 'wf-start-eo-Start', TargetNodeHash: 'wf-read', TargetPortHash: 'wf-read-ei-BeginRead', Data: {} },
|
|
1520
|
+
{ Hash: 'wf-ev2', ConnectionType: 'Event', SourceNodeHash: 'wf-read', SourcePortHash: 'wf-read-eo-ReadComplete', TargetNodeHash: 'wf-split', TargetPortHash: 'wf-split-ei-PerformSplit', Data: {} },
|
|
1521
|
+
{ Hash: 'wf-ev3', ConnectionType: 'Event', SourceNodeHash: 'wf-split', SourcePortHash: 'wf-split-eo-TokenDataSent', TargetNodeHash: 'wf-replace', TargetPortHash: 'wf-replace-ei-Replace', Data: {} },
|
|
1522
|
+
{ Hash: 'wf-ev4', ConnectionType: 'Event', SourceNodeHash: 'wf-replace', SourcePortHash: 'wf-replace-eo-ReplaceComplete', TargetNodeHash: 'wf-append', TargetPortHash: 'wf-append-ei-Append', Data: {} },
|
|
1523
|
+
{ Hash: 'wf-ev5', ConnectionType: 'Event', SourceNodeHash: 'wf-append', SourcePortHash: 'wf-append-eo-Completed', TargetNodeHash: 'wf-split', TargetPortHash: 'wf-split-ei-StepComplete', Data: {} },
|
|
1524
|
+
{ Hash: 'wf-ev6', ConnectionType: 'Event', SourceNodeHash: 'wf-split', SourcePortHash: 'wf-split-eo-CompletedAllSubtasks', TargetNodeHash: 'wf-write', TargetPortHash: 'wf-write-ei-BeginWrite', Data: {} },
|
|
1525
|
+
{ Hash: 'wf-ev7', ConnectionType: 'Event', SourceNodeHash: 'wf-write', SourcePortHash: 'wf-write-eo-WriteComplete', TargetNodeHash: 'wf-end', TargetPortHash: 'wf-end-ei-In', Data: {} },
|
|
1526
|
+
{ Hash: 'wf-st1', ConnectionType: 'State', SourceNodeHash: 'wf-read', SourcePortHash: 'wf-read-so-FileContent', TargetNodeHash: 'wf-split', TargetPortHash: 'wf-split-si-InputString', Data: {} },
|
|
1527
|
+
{ Hash: 'wf-st2', ConnectionType: 'State', SourceNodeHash: 'wf-split', SourcePortHash: 'wf-split-so-CurrentToken', TargetNodeHash: 'wf-replace', TargetPortHash: 'wf-replace-si-InputString', Data: {} },
|
|
1528
|
+
{ Hash: 'wf-st3', ConnectionType: 'State', SourceNodeHash: 'wf-replace', SourcePortHash: 'wf-replace-so-ReplacedString', TargetNodeHash: 'wf-append', TargetPortHash: 'wf-append-si-InputString', Data: {} },
|
|
1529
|
+
{ Hash: 'wf-st4', ConnectionType: 'State', SourceNodeHash: 'wf-append', SourcePortHash: 'wf-append-so-AppendedString', TargetNodeHash: 'wf-write', TargetPortHash: 'wf-write-si-Content', Data: {} }
|
|
1530
|
+
],
|
|
1531
|
+
ViewState: { PanX: 0, PanY: 0, Zoom: 1, SelectedNodeHash: null, SelectedConnectionHash: null }
|
|
1532
|
+
};
|
|
1533
|
+
|
|
1534
|
+
let tmpPict = window._Pict;
|
|
1535
|
+
let tmpFlowEditor = tmpPict.views['Ultravisor-FlowEditor'];
|
|
1536
|
+
if (tmpFlowEditor && tmpFlowEditor._FlowView)
|
|
1537
|
+
{
|
|
1538
|
+
tmpPict.AppData.Ultravisor.Flows.Current = JSON.parse(JSON.stringify(tmpGraph));
|
|
1539
|
+
tmpFlowEditor._FlowView.setFlowData(tmpPict.AppData.Ultravisor.Flows.Current);
|
|
1540
|
+
return { success: true, nodeCount: tmpGraph.Nodes.length, connectionCount: tmpGraph.Connections.length };
|
|
1541
|
+
}
|
|
1542
|
+
return { success: false, error: 'Flow view not initialized' };
|
|
1543
|
+
}, _WorkflowInputPath, _WorkflowOutputPath);
|
|
1544
|
+
|
|
1545
|
+
libAssert.ok(tmpFlowData.success, 'Flow data should be injected: ' + JSON.stringify(tmpFlowData));
|
|
1546
|
+
libAssert.strictEqual(tmpFlowData.nodeCount, 7, 'Should have 7 nodes');
|
|
1547
|
+
libAssert.strictEqual(tmpFlowData.connectionCount, 11, 'Should have 11 connections');
|
|
1548
|
+
|
|
1549
|
+
await settle(500);
|
|
1550
|
+
await takeScreenshot('workflow-editor-built');
|
|
1551
|
+
_TestResults.passed++;
|
|
1552
|
+
}
|
|
1553
|
+
);
|
|
1554
|
+
|
|
1555
|
+
test
|
|
1556
|
+
(
|
|
1557
|
+
'zoom to fit and verify port rendering',
|
|
1558
|
+
async function ()
|
|
1559
|
+
{
|
|
1560
|
+
this.timeout(15000);
|
|
1561
|
+
|
|
1562
|
+
await _Page.evaluate(() =>
|
|
1563
|
+
{
|
|
1564
|
+
let tmpPict = window._Pict;
|
|
1565
|
+
let tmpFlowEditor = tmpPict.views['Ultravisor-FlowEditor'];
|
|
1566
|
+
if (tmpFlowEditor && tmpFlowEditor._FlowView && tmpFlowEditor._FlowView._ViewportManager)
|
|
1567
|
+
{
|
|
1568
|
+
tmpFlowEditor._FlowView._ViewportManager.zoomToFit();
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
await settle(500);
|
|
1572
|
+
|
|
1573
|
+
let tmpPortInfo = await _Page.evaluate(() =>
|
|
1574
|
+
{
|
|
1575
|
+
let tmpPorts = document.querySelectorAll('.pict-flow-port');
|
|
1576
|
+
let tmpByType = {};
|
|
1577
|
+
tmpPorts.forEach((pEl) =>
|
|
1578
|
+
{
|
|
1579
|
+
let tmpType = pEl.getAttribute('data-port-type') || pEl.getAttribute('data-port-direction') || 'unknown';
|
|
1580
|
+
if (!tmpByType[tmpType]) tmpByType[tmpType] = 0;
|
|
1581
|
+
tmpByType[tmpType]++;
|
|
1582
|
+
});
|
|
1583
|
+
return { total: tmpPorts.length, byType: tmpByType };
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
console.log(' Port breakdown:', JSON.stringify(tmpPortInfo.byType));
|
|
1587
|
+
libAssert.ok(tmpPortInfo.total > 0, 'Should render ports on the SVG');
|
|
1588
|
+
|
|
1589
|
+
await takeScreenshot('workflow-zoomed');
|
|
1590
|
+
_TestResults.passed++;
|
|
1591
|
+
}
|
|
1592
|
+
);
|
|
1593
|
+
|
|
1594
|
+
test
|
|
1595
|
+
(
|
|
1596
|
+
'save the operation via the UI',
|
|
1597
|
+
async function ()
|
|
1598
|
+
{
|
|
1599
|
+
this.timeout(15000);
|
|
1600
|
+
|
|
1601
|
+
await _Page.evaluate(() =>
|
|
1602
|
+
{
|
|
1603
|
+
let tmpNameEl = document.getElementById('Ultravisor-FlowEditor-Name');
|
|
1604
|
+
if (tmpNameEl) tmpNameEl.value = 'Workflow Test - Line Replace';
|
|
1605
|
+
let tmpDescEl = document.getElementById('Ultravisor-FlowEditor-Description');
|
|
1606
|
+
if (tmpDescEl) tmpDescEl.value = 'Reads a file, replaces John with Jane per-line, writes output.';
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
await _Page.evaluate(() =>
|
|
1610
|
+
{
|
|
1611
|
+
window._lastAlert = null;
|
|
1612
|
+
window.alert = function (pMsg) { window._lastAlert = pMsg; };
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
await _Page.evaluate(() =>
|
|
1616
|
+
{
|
|
1617
|
+
let tmpPict = window._Pict;
|
|
1618
|
+
tmpPict.views['Ultravisor-FlowEditor'].saveOperation();
|
|
1619
|
+
});
|
|
1620
|
+
await settle(2000);
|
|
1621
|
+
|
|
1622
|
+
let tmpSaveResult = await _Page.evaluate(() =>
|
|
1623
|
+
{
|
|
1624
|
+
let tmpHashEl = document.getElementById('Ultravisor-FlowEditor-HashDisplay');
|
|
1625
|
+
return {
|
|
1626
|
+
alertMessage: window._lastAlert,
|
|
1627
|
+
operationHash: tmpHashEl ? tmpHashEl.textContent : ''
|
|
1628
|
+
};
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
console.log(' Save result:', tmpSaveResult.alertMessage, '| Hash:', tmpSaveResult.operationHash);
|
|
1632
|
+
libAssert.ok(tmpSaveResult.alertMessage && tmpSaveResult.alertMessage.includes('saved'), 'Should show saved alert');
|
|
1633
|
+
libAssert.ok(tmpSaveResult.operationHash.length > 0, 'Should have an operation hash');
|
|
1634
|
+
|
|
1635
|
+
_SavedOperationHash = tmpSaveResult.operationHash;
|
|
1636
|
+
|
|
1637
|
+
await takeScreenshot('workflow-saved');
|
|
1638
|
+
_TestResults.passed++;
|
|
1639
|
+
}
|
|
1640
|
+
);
|
|
1641
|
+
|
|
1642
|
+
test
|
|
1643
|
+
(
|
|
1644
|
+
'execute the looping pipeline via the API',
|
|
1645
|
+
async function ()
|
|
1646
|
+
{
|
|
1647
|
+
this.timeout(30000);
|
|
1648
|
+
|
|
1649
|
+
libAssert.ok(_SavedOperationHash, 'Must have a saved operation hash');
|
|
1650
|
+
|
|
1651
|
+
let tmpResult = await apiExecuteOperation(_SavedOperationHash);
|
|
1652
|
+
|
|
1653
|
+
libAssert.strictEqual(tmpResult.body.Status, 'Complete', 'Execution should complete successfully');
|
|
1654
|
+
console.log(' Execution status:', tmpResult.body.Status, '| Log entries:', (tmpResult.body.Log || []).length);
|
|
1655
|
+
|
|
1656
|
+
_TestResults.passed++;
|
|
1657
|
+
}
|
|
1658
|
+
);
|
|
1659
|
+
|
|
1660
|
+
test
|
|
1661
|
+
(
|
|
1662
|
+
'verify the output file has correct content',
|
|
1663
|
+
function ()
|
|
1664
|
+
{
|
|
1665
|
+
this.timeout(5000);
|
|
1666
|
+
|
|
1667
|
+
libAssert.ok(libFs.existsSync(_WorkflowOutputPath),
|
|
1668
|
+
'Output file should exist at: ' + _WorkflowOutputPath);
|
|
1669
|
+
|
|
1670
|
+
let tmpOutputContent = libFs.readFileSync(_WorkflowOutputPath, 'utf8');
|
|
1671
|
+
console.log(' Output content:', JSON.stringify(tmpOutputContent));
|
|
1672
|
+
|
|
1673
|
+
libAssert.ok(tmpOutputContent.includes('Jane'), 'Output should contain "Jane"');
|
|
1674
|
+
libAssert.ok(!tmpOutputContent.includes('John'), 'Output should not contain "John"');
|
|
1675
|
+
libAssert.ok(tmpOutputContent.includes('Hello Jane Smith'), 'First line correct');
|
|
1676
|
+
libAssert.ok(tmpOutputContent.includes('Jane went to the store'), 'Second line correct');
|
|
1677
|
+
libAssert.ok(tmpOutputContent.includes('Mary and Jane had lunch'), 'Third line correct');
|
|
1678
|
+
libAssert.ok(tmpOutputContent.includes('No match here'), 'Fourth line unchanged');
|
|
1679
|
+
|
|
1680
|
+
_TestResults.passed++;
|
|
1681
|
+
}
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
);
|
|
1685
|
+
|
|
1686
|
+
// ════════════════════════════════════════════════
|
|
1687
|
+
// Workflow 8: Sub-Operation (launch-operation)
|
|
1688
|
+
// ════════════════════════════════════════════════
|
|
1689
|
+
suite
|
|
1690
|
+
(
|
|
1691
|
+
'Workflow: Sub-Operation via launch-operation',
|
|
1692
|
+
function ()
|
|
1693
|
+
{
|
|
1694
|
+
let _ChildOpHash = '';
|
|
1695
|
+
|
|
1696
|
+
test
|
|
1697
|
+
(
|
|
1698
|
+
'create a child operation that writes a file',
|
|
1699
|
+
async function ()
|
|
1700
|
+
{
|
|
1701
|
+
this.timeout(15000);
|
|
1702
|
+
|
|
1703
|
+
let tmpTestDir = libPath.join(_StagingDir, 'subop_test');
|
|
1704
|
+
libFs.mkdirSync(tmpTestDir, { recursive: true });
|
|
1705
|
+
let tmpChildOutputPath = libPath.join(tmpTestDir, 'child_output.txt');
|
|
1706
|
+
|
|
1707
|
+
let tmpResponse = await apiCreateOperation({
|
|
1708
|
+
Hash: 'CHILD-OP-001',
|
|
1709
|
+
Name: 'Child Operation',
|
|
1710
|
+
Description: 'Simple child op that writes a marker file.',
|
|
1711
|
+
Graph: {
|
|
1712
|
+
Nodes: [
|
|
1713
|
+
{ Hash: 'n-start', Type: 'start', X: 0, Y: 0 },
|
|
1714
|
+
{
|
|
1715
|
+
Hash: 'n-write', Type: 'write-file',
|
|
1716
|
+
Settings: { FilePath: tmpChildOutputPath, Content: 'Written by child operation', Encoding: 'utf8' },
|
|
1717
|
+
Ports: [], X: 200, Y: 0
|
|
1718
|
+
},
|
|
1719
|
+
{ Hash: 'n-end', Type: 'end', X: 400, Y: 0 }
|
|
1720
|
+
],
|
|
1721
|
+
Connections: [
|
|
1722
|
+
{ Hash: 'c1', ConnectionType: 'Event', SourceNodeHash: 'n-start', SourcePortHash: 'n-start-eo-Start', TargetNodeHash: 'n-write', TargetPortHash: 'n-write-ei-BeginWrite' },
|
|
1723
|
+
{ Hash: 'c2', ConnectionType: 'Event', SourceNodeHash: 'n-write', SourcePortHash: 'n-write-eo-WriteComplete', TargetNodeHash: 'n-end', TargetPortHash: 'n-end-ei-End' }
|
|
1724
|
+
],
|
|
1725
|
+
ViewState: {}
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
_ChildOpHash = tmpResponse.Hash;
|
|
1730
|
+
libAssert.ok(_ChildOpHash, 'Child operation should be created');
|
|
1731
|
+
console.log(' Child operation:', _ChildOpHash);
|
|
1732
|
+
|
|
1733
|
+
_TestResults.passed++;
|
|
1734
|
+
}
|
|
1735
|
+
);
|
|
1736
|
+
|
|
1737
|
+
test
|
|
1738
|
+
(
|
|
1739
|
+
'create and execute parent operation that launches the child',
|
|
1740
|
+
async function ()
|
|
1741
|
+
{
|
|
1742
|
+
this.timeout(15000);
|
|
1743
|
+
|
|
1744
|
+
let tmpTestDir = libPath.join(_StagingDir, 'subop_test');
|
|
1745
|
+
let tmpParentOutputPath = libPath.join(tmpTestDir, 'parent_output.txt');
|
|
1746
|
+
|
|
1747
|
+
let tmpResponse = await apiCreateOperation({
|
|
1748
|
+
Name: 'Parent Operation',
|
|
1749
|
+
Description: 'Launches child operation then writes its own marker.',
|
|
1750
|
+
Graph: {
|
|
1751
|
+
Nodes: [
|
|
1752
|
+
{ Hash: 'n-start', Type: 'start', X: 0, Y: 0 },
|
|
1753
|
+
{
|
|
1754
|
+
Hash: 'n-launch', Type: 'launch-operation',
|
|
1755
|
+
Settings: { OperationHash: _ChildOpHash },
|
|
1756
|
+
Ports: [], X: 200, Y: 0
|
|
1757
|
+
},
|
|
1758
|
+
{
|
|
1759
|
+
Hash: 'n-write', Type: 'write-file',
|
|
1760
|
+
Settings: { FilePath: tmpParentOutputPath, Content: 'Parent completed after child', Encoding: 'utf8' },
|
|
1761
|
+
Ports: [], X: 400, Y: 0
|
|
1762
|
+
},
|
|
1763
|
+
{ Hash: 'n-end', Type: 'end', X: 600, Y: 0 }
|
|
1764
|
+
],
|
|
1765
|
+
Connections: [
|
|
1766
|
+
{ Hash: 'c1', ConnectionType: 'Event', SourceNodeHash: 'n-start', SourcePortHash: 'n-start-eo-Start', TargetNodeHash: 'n-launch', TargetPortHash: 'n-launch-ei-Launch' },
|
|
1767
|
+
{ Hash: 'c2', ConnectionType: 'Event', SourceNodeHash: 'n-launch', SourcePortHash: 'n-launch-eo-Completed', TargetNodeHash: 'n-write', TargetPortHash: 'n-write-ei-BeginWrite' },
|
|
1768
|
+
{ Hash: 'c3', ConnectionType: 'Event', SourceNodeHash: 'n-write', SourcePortHash: 'n-write-eo-WriteComplete', TargetNodeHash: 'n-end', TargetPortHash: 'n-end-ei-End' }
|
|
1769
|
+
],
|
|
1770
|
+
ViewState: {}
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
let tmpResult = await apiExecuteOperation(tmpResponse.Hash);
|
|
1775
|
+
|
|
1776
|
+
libAssert.strictEqual(tmpResult.body.Status, 'Complete', 'Parent operation should complete');
|
|
1777
|
+
|
|
1778
|
+
// Verify the launch-operation outputs
|
|
1779
|
+
let tmpLaunchOutputs = tmpResult.body.TaskOutputs['n-launch'];
|
|
1780
|
+
libAssert.ok(tmpLaunchOutputs, 'Should have launch-operation outputs');
|
|
1781
|
+
libAssert.strictEqual(tmpLaunchOutputs.Status, 'Complete', 'Child status should be Complete');
|
|
1782
|
+
libAssert.ok(tmpLaunchOutputs.ElapsedMs >= 0, 'Should track child elapsed time');
|
|
1783
|
+
|
|
1784
|
+
// Verify both files were written
|
|
1785
|
+
let tmpChildOutputPath = libPath.join(tmpTestDir, 'child_output.txt');
|
|
1786
|
+
libAssert.ok(libFs.existsSync(tmpChildOutputPath), 'Child output should exist');
|
|
1787
|
+
libAssert.ok(libFs.existsSync(tmpParentOutputPath), 'Parent output should exist');
|
|
1788
|
+
|
|
1789
|
+
let tmpChildContent = libFs.readFileSync(tmpChildOutputPath, 'utf8');
|
|
1790
|
+
let tmpParentContent = libFs.readFileSync(tmpParentOutputPath, 'utf8');
|
|
1791
|
+
libAssert.strictEqual(tmpChildContent, 'Written by child operation', 'Child content matches');
|
|
1792
|
+
libAssert.strictEqual(tmpParentContent, 'Parent completed after child', 'Parent content matches');
|
|
1793
|
+
|
|
1794
|
+
console.log(' Sub-operation: child and parent both completed');
|
|
1795
|
+
console.log(' Child elapsed:', tmpLaunchOutputs.ElapsedMs, 'ms');
|
|
1796
|
+
|
|
1797
|
+
_TestResults.passed++;
|
|
1798
|
+
}
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
);
|
|
1802
|
+
|
|
1803
|
+
// ════════════════════════════════════════════════
|
|
1804
|
+
// Schedule API
|
|
1805
|
+
// ════════════════════════════════════════════════
|
|
1806
|
+
suite
|
|
1807
|
+
(
|
|
1808
|
+
'Schedule API',
|
|
1809
|
+
function ()
|
|
1810
|
+
{
|
|
1811
|
+
let _ScheduledOpHash = '';
|
|
1812
|
+
let _ScheduleEntryGUID = '';
|
|
1813
|
+
|
|
1814
|
+
test
|
|
1815
|
+
(
|
|
1816
|
+
'create an operation for scheduling',
|
|
1817
|
+
async function ()
|
|
1818
|
+
{
|
|
1819
|
+
this.timeout(10000);
|
|
1820
|
+
|
|
1821
|
+
let tmpResponse = await apiCreateOperation({
|
|
1822
|
+
Name: 'Schedulable Operation',
|
|
1823
|
+
Description: 'A simple operation for schedule testing.',
|
|
1824
|
+
Graph: {
|
|
1825
|
+
Nodes: [
|
|
1826
|
+
{ Hash: 'n-start', Type: 'start', X: 0, Y: 0 },
|
|
1827
|
+
{ Hash: 'n-end', Type: 'end', X: 200, Y: 0 }
|
|
1828
|
+
],
|
|
1829
|
+
Connections: [
|
|
1830
|
+
{
|
|
1831
|
+
Hash: 'c1', ConnectionType: 'Event',
|
|
1832
|
+
SourceNodeHash: 'n-start', SourcePortHash: 'n-start-eo-Start',
|
|
1833
|
+
TargetNodeHash: 'n-end', TargetPortHash: 'n-end-ei-End'
|
|
1834
|
+
}
|
|
1835
|
+
],
|
|
1836
|
+
ViewState: {}
|
|
1837
|
+
}
|
|
1838
|
+
});
|
|
1839
|
+
|
|
1840
|
+
_ScheduledOpHash = tmpResponse.Hash;
|
|
1841
|
+
libAssert.ok(_ScheduledOpHash, 'Schedulable operation created');
|
|
1842
|
+
|
|
1843
|
+
_TestResults.passed++;
|
|
1844
|
+
}
|
|
1845
|
+
);
|
|
1846
|
+
|
|
1847
|
+
test
|
|
1848
|
+
(
|
|
1849
|
+
'schedule an operation via POST /Schedule/Operation',
|
|
1850
|
+
async function ()
|
|
1851
|
+
{
|
|
1852
|
+
this.timeout(10000);
|
|
1853
|
+
|
|
1854
|
+
let tmpResponse = await apiPost('/Schedule/Operation', {
|
|
1855
|
+
Hash: _ScheduledOpHash,
|
|
1856
|
+
ScheduleType: 'cron',
|
|
1857
|
+
Parameters: '0 */6 * * *'
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
libAssert.strictEqual(tmpResponse.status, 200, 'Schedule should succeed');
|
|
1861
|
+
libAssert.ok(tmpResponse.body.GUID, 'Schedule entry should have a GUID');
|
|
1862
|
+
libAssert.strictEqual(tmpResponse.body.TargetHash, _ScheduledOpHash, 'TargetHash should match');
|
|
1863
|
+
libAssert.strictEqual(tmpResponse.body.TargetType, 'Operation', 'TargetType should be Operation');
|
|
1864
|
+
|
|
1865
|
+
_ScheduleEntryGUID = tmpResponse.body.GUID;
|
|
1866
|
+
console.log(' Schedule entry:', _ScheduleEntryGUID);
|
|
1867
|
+
|
|
1868
|
+
_TestResults.passed++;
|
|
1869
|
+
}
|
|
1870
|
+
);
|
|
1871
|
+
|
|
1872
|
+
test
|
|
1873
|
+
(
|
|
1874
|
+
'list schedule via GET /Schedule',
|
|
1875
|
+
async function ()
|
|
1876
|
+
{
|
|
1877
|
+
this.timeout(10000);
|
|
1878
|
+
|
|
1879
|
+
let tmpResponse = await apiGet('/Schedule');
|
|
1880
|
+
|
|
1881
|
+
libAssert.ok(Array.isArray(tmpResponse.body), 'Should return an array');
|
|
1882
|
+
libAssert.ok(tmpResponse.body.length >= 1, 'Should have at least one entry');
|
|
1883
|
+
|
|
1884
|
+
let tmpEntry = tmpResponse.body.find(function (pE) { return pE.GUID === _ScheduleEntryGUID; });
|
|
1885
|
+
libAssert.ok(tmpEntry, 'Our schedule entry should appear in list');
|
|
1886
|
+
|
|
1887
|
+
_TestResults.passed++;
|
|
1888
|
+
}
|
|
1889
|
+
);
|
|
1890
|
+
|
|
1891
|
+
test
|
|
1892
|
+
(
|
|
1893
|
+
'remove schedule entry via DELETE /Schedule/:GUID',
|
|
1894
|
+
async function ()
|
|
1895
|
+
{
|
|
1896
|
+
this.timeout(10000);
|
|
1897
|
+
|
|
1898
|
+
let tmpResponse = await apiDelete('/Schedule/' + _ScheduleEntryGUID);
|
|
1899
|
+
|
|
1900
|
+
libAssert.strictEqual(tmpResponse.body.Status, 'Deleted', 'Should confirm deletion');
|
|
1901
|
+
|
|
1902
|
+
// Verify it is gone
|
|
1903
|
+
let tmpListResponse = await apiGet('/Schedule');
|
|
1904
|
+
let tmpEntry = tmpListResponse.body.find(function (pE) { return pE.GUID === _ScheduleEntryGUID; });
|
|
1905
|
+
libAssert.ok(!tmpEntry, 'Entry should be removed from schedule');
|
|
1906
|
+
|
|
1907
|
+
_TestResults.passed++;
|
|
1908
|
+
}
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
);
|
|
1912
|
+
|
|
1913
|
+
// ════════════════════════════════════════════════
|
|
1914
|
+
// Manifest API
|
|
1915
|
+
// ════════════════════════════════════════════════
|
|
1916
|
+
suite
|
|
1917
|
+
(
|
|
1918
|
+
'Manifest API',
|
|
1919
|
+
function ()
|
|
1920
|
+
{
|
|
1921
|
+
test
|
|
1922
|
+
(
|
|
1923
|
+
'list execution manifests via GET /Manifest',
|
|
1924
|
+
async function ()
|
|
1925
|
+
{
|
|
1926
|
+
this.timeout(10000);
|
|
1927
|
+
|
|
1928
|
+
let tmpResponse = await apiGet('/Manifest');
|
|
1929
|
+
|
|
1930
|
+
libAssert.strictEqual(tmpResponse.status, 200, 'Should return 200');
|
|
1931
|
+
libAssert.ok(Array.isArray(tmpResponse.body), 'Should return an array');
|
|
1932
|
+
|
|
1933
|
+
// We should have manifests from previous executions
|
|
1934
|
+
console.log(' Manifests:', tmpResponse.body.length, 'run(s) recorded');
|
|
1935
|
+
if (tmpResponse.body.length > 0)
|
|
1936
|
+
{
|
|
1937
|
+
let tmpFirst = tmpResponse.body[0];
|
|
1938
|
+
console.log(' First manifest:', tmpFirst.Hash || tmpFirst.OperationHash, '| Status:', tmpFirst.Status);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
_TestResults.passed++;
|
|
1942
|
+
}
|
|
1943
|
+
);
|
|
1944
|
+
}
|
|
1945
|
+
);
|
|
1946
|
+
|
|
1947
|
+
// ════════════════════════════════════════════════
|
|
1948
|
+
// Status & Package API
|
|
1949
|
+
// ════════════════════════════════════════════════
|
|
1950
|
+
suite
|
|
1951
|
+
(
|
|
1952
|
+
'Status and Package API',
|
|
1953
|
+
function ()
|
|
1954
|
+
{
|
|
1955
|
+
test
|
|
1956
|
+
(
|
|
1957
|
+
'get server status via GET /status',
|
|
1958
|
+
async function ()
|
|
1959
|
+
{
|
|
1960
|
+
this.timeout(10000);
|
|
1961
|
+
|
|
1962
|
+
let tmpResponse = await apiGet('/status');
|
|
1963
|
+
|
|
1964
|
+
libAssert.strictEqual(tmpResponse.status, 200, 'Should return 200');
|
|
1965
|
+
libAssert.strictEqual(tmpResponse.body.Status, 'Running', 'Server should be running');
|
|
1966
|
+
libAssert.ok(typeof tmpResponse.body.ScheduleEntries === 'number', 'ScheduleEntries should be a number');
|
|
1967
|
+
|
|
1968
|
+
console.log(' Server status:', tmpResponse.body.Status,
|
|
1969
|
+
'| Schedule entries:', tmpResponse.body.ScheduleEntries,
|
|
1970
|
+
'| Schedule running:', tmpResponse.body.ScheduleRunning);
|
|
1971
|
+
|
|
1972
|
+
_TestResults.passed++;
|
|
1973
|
+
}
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
);
|
|
1977
|
+
|
|
1978
|
+
// ════════════════════════════════════════════════
|
|
1979
|
+
// Dashboard Data After Workflows
|
|
1980
|
+
// ════════════════════════════════════════════════
|
|
1981
|
+
suite
|
|
1982
|
+
(
|
|
1983
|
+
'Dashboard After Workflows',
|
|
1984
|
+
function ()
|
|
1985
|
+
{
|
|
1986
|
+
test
|
|
1987
|
+
(
|
|
1988
|
+
'dashboard shows updated counts',
|
|
1989
|
+
async function ()
|
|
1990
|
+
{
|
|
1991
|
+
this.timeout(15000);
|
|
1992
|
+
|
|
1993
|
+
await navigateToRoute('#/Home');
|
|
1994
|
+
await settle(2000);
|
|
1995
|
+
|
|
1996
|
+
await takeScreenshot('dashboard-after-workflows');
|
|
1997
|
+
|
|
1998
|
+
_TestResults.passed++;
|
|
1999
|
+
}
|
|
2000
|
+
);
|
|
2001
|
+
|
|
2002
|
+
test
|
|
2003
|
+
(
|
|
2004
|
+
'operations list shows created operations',
|
|
2005
|
+
async function ()
|
|
2006
|
+
{
|
|
2007
|
+
this.timeout(15000);
|
|
2008
|
+
|
|
2009
|
+
await navigateToRoute('#/Operations');
|
|
2010
|
+
await settle(1500);
|
|
2011
|
+
|
|
2012
|
+
let tmpOpCount = await _Page.evaluate(() =>
|
|
2013
|
+
{
|
|
2014
|
+
let tmpRows = document.querySelectorAll('.ultravisor-operation-table tbody tr');
|
|
2015
|
+
return tmpRows ? tmpRows.length : 0;
|
|
2016
|
+
});
|
|
2017
|
+
|
|
2018
|
+
console.log(' Operations visible in list:', tmpOpCount);
|
|
2019
|
+
libAssert.ok(tmpOpCount >= 1, 'Should show at least one operation in the list');
|
|
2020
|
+
|
|
2021
|
+
await takeScreenshot('operations-list-populated');
|
|
2022
|
+
|
|
2023
|
+
_TestResults.passed++;
|
|
2024
|
+
}
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
);
|
|
2028
|
+
|
|
2029
|
+
// ════════════════════════════════════════════════
|
|
2030
|
+
// Pending Input API
|
|
2031
|
+
// ════════════════════════════════════════════════
|
|
2032
|
+
suite
|
|
2033
|
+
(
|
|
2034
|
+
'Pending Input API',
|
|
2035
|
+
function ()
|
|
2036
|
+
{
|
|
2037
|
+
let _PendingOpHash = '';
|
|
2038
|
+
let _PendingRunHash = '';
|
|
2039
|
+
|
|
2040
|
+
test
|
|
2041
|
+
(
|
|
2042
|
+
'create operation with value-input node',
|
|
2043
|
+
async function ()
|
|
2044
|
+
{
|
|
2045
|
+
this.timeout(15000);
|
|
2046
|
+
|
|
2047
|
+
let tmpResponse = await apiCreateOperation({
|
|
2048
|
+
Name: 'Pending Input Test',
|
|
2049
|
+
Description: 'Tests value-input pause and resume via PendingInput API.',
|
|
2050
|
+
Graph: {
|
|
2051
|
+
Nodes: [
|
|
2052
|
+
{ Hash: 'pi-start', Type: 'start', X: 0, Y: 0 },
|
|
2053
|
+
{
|
|
2054
|
+
Hash: 'pi-input', Type: 'value-input',
|
|
2055
|
+
Settings: { PromptMessage: 'Enter test value', OutputAddress: 'Operation.TestValue' },
|
|
2056
|
+
Ports: [], X: 200, Y: 0
|
|
2057
|
+
},
|
|
2058
|
+
{ Hash: 'pi-end', Type: 'end', X: 400, Y: 0 }
|
|
2059
|
+
],
|
|
2060
|
+
Connections: [
|
|
2061
|
+
{ Hash: 'pi-c1', ConnectionType: 'Event', SourceNodeHash: 'pi-start', SourcePortHash: 'pi-start-eo-Start', TargetNodeHash: 'pi-input', TargetPortHash: 'pi-input-ei-RequestInput' },
|
|
2062
|
+
{ Hash: 'pi-c2', ConnectionType: 'Event', SourceNodeHash: 'pi-input', SourcePortHash: 'pi-input-eo-ValueInputComplete', TargetNodeHash: 'pi-end', TargetPortHash: 'pi-end-ei-End' }
|
|
2063
|
+
],
|
|
2064
|
+
ViewState: {}
|
|
2065
|
+
}
|
|
2066
|
+
});
|
|
2067
|
+
|
|
2068
|
+
_PendingOpHash = tmpResponse.Hash;
|
|
2069
|
+
libAssert.ok(_PendingOpHash, 'Operation should be created');
|
|
2070
|
+
console.log(' Pending Input op:', _PendingOpHash);
|
|
2071
|
+
|
|
2072
|
+
_TestResults.passed++;
|
|
2073
|
+
}
|
|
2074
|
+
);
|
|
2075
|
+
|
|
2076
|
+
test
|
|
2077
|
+
(
|
|
2078
|
+
'execute operation — should pause at value-input',
|
|
2079
|
+
async function ()
|
|
2080
|
+
{
|
|
2081
|
+
this.timeout(15000);
|
|
2082
|
+
|
|
2083
|
+
let tmpResult = await apiExecuteOperation(_PendingOpHash);
|
|
2084
|
+
libAssert.strictEqual(tmpResult.body.Status, 'WaitingForInput', 'Should be waiting for input');
|
|
2085
|
+
libAssert.ok(tmpResult.body.WaitingTasks, 'Should have WaitingTasks');
|
|
2086
|
+
libAssert.ok(tmpResult.body.WaitingTasks['pi-input'], 'Should be waiting on pi-input node');
|
|
2087
|
+
libAssert.strictEqual(tmpResult.body.WaitingTasks['pi-input'].PromptMessage, 'Enter test value', 'PromptMessage should match');
|
|
2088
|
+
|
|
2089
|
+
_PendingRunHash = tmpResult.body.Hash;
|
|
2090
|
+
console.log(' Run paused:', _PendingRunHash, '| Prompt:', tmpResult.body.WaitingTasks['pi-input'].PromptMessage);
|
|
2091
|
+
|
|
2092
|
+
_TestResults.passed++;
|
|
2093
|
+
}
|
|
2094
|
+
);
|
|
2095
|
+
|
|
2096
|
+
test
|
|
2097
|
+
(
|
|
2098
|
+
'GET /PendingInput lists the paused run',
|
|
2099
|
+
async function ()
|
|
2100
|
+
{
|
|
2101
|
+
this.timeout(10000);
|
|
2102
|
+
|
|
2103
|
+
let tmpResponse = await apiGet('/PendingInput');
|
|
2104
|
+
libAssert.strictEqual(tmpResponse.status, 200, 'Should return 200');
|
|
2105
|
+
libAssert.ok(Array.isArray(tmpResponse.body), 'Should return an array');
|
|
2106
|
+
libAssert.ok(tmpResponse.body.length >= 1, 'Should have at least one pending input');
|
|
2107
|
+
|
|
2108
|
+
let tmpFound = tmpResponse.body.find(function (pItem) { return pItem.RunHash === _PendingRunHash; });
|
|
2109
|
+
libAssert.ok(tmpFound, 'Should find our paused run');
|
|
2110
|
+
libAssert.strictEqual(tmpFound.OperationHash, _PendingOpHash, 'OperationHash should match');
|
|
2111
|
+
libAssert.ok(tmpFound.WaitingTasks['pi-input'], 'Should show pi-input as waiting');
|
|
2112
|
+
libAssert.strictEqual(tmpFound.WaitingTasks['pi-input'].PromptMessage, 'Enter test value', 'PromptMessage should match');
|
|
2113
|
+
|
|
2114
|
+
console.log(' Pending inputs found:', tmpResponse.body.length);
|
|
2115
|
+
|
|
2116
|
+
_TestResults.passed++;
|
|
2117
|
+
}
|
|
2118
|
+
);
|
|
2119
|
+
|
|
2120
|
+
test
|
|
2121
|
+
(
|
|
2122
|
+
'Pending Input view shows waiting operations',
|
|
2123
|
+
async function ()
|
|
2124
|
+
{
|
|
2125
|
+
this.timeout(15000);
|
|
2126
|
+
|
|
2127
|
+
await navigateToRoute('#/PendingInput');
|
|
2128
|
+
await settle(2000);
|
|
2129
|
+
|
|
2130
|
+
let tmpCardCount = await _Page.evaluate(() =>
|
|
2131
|
+
{
|
|
2132
|
+
let tmpCards = document.querySelectorAll('.ultravisor-pendinginput-card');
|
|
2133
|
+
return tmpCards ? tmpCards.length : 0;
|
|
2134
|
+
});
|
|
2135
|
+
|
|
2136
|
+
libAssert.ok(tmpCardCount >= 1, 'Should show at least one pending input card');
|
|
2137
|
+
console.log(' Pending input cards visible:', tmpCardCount);
|
|
2138
|
+
|
|
2139
|
+
await takeScreenshot('pending-input-view');
|
|
2140
|
+
|
|
2141
|
+
_TestResults.passed++;
|
|
2142
|
+
}
|
|
2143
|
+
);
|
|
2144
|
+
|
|
2145
|
+
test
|
|
2146
|
+
(
|
|
2147
|
+
'POST /PendingInput/:RunHash submits value and resumes',
|
|
2148
|
+
async function ()
|
|
2149
|
+
{
|
|
2150
|
+
this.timeout(15000);
|
|
2151
|
+
|
|
2152
|
+
let tmpResponse = await apiPost('/PendingInput/' + _PendingRunHash, { NodeHash: 'pi-input', Value: 'hello-test-value' });
|
|
2153
|
+
|
|
2154
|
+
libAssert.strictEqual(tmpResponse.status, 200, 'Should return 200');
|
|
2155
|
+
libAssert.strictEqual(tmpResponse.body.Status, 'Complete', 'Operation should complete after input');
|
|
2156
|
+
libAssert.ok(tmpResponse.body.TaskOutputs['pi-input'], 'Should have pi-input outputs');
|
|
2157
|
+
libAssert.strictEqual(tmpResponse.body.TaskOutputs['pi-input'].InputValue, 'hello-test-value', 'InputValue should match');
|
|
2158
|
+
|
|
2159
|
+
console.log(' Resumed run status:', tmpResponse.body.Status);
|
|
2160
|
+
|
|
2161
|
+
_TestResults.passed++;
|
|
2162
|
+
}
|
|
2163
|
+
);
|
|
2164
|
+
|
|
2165
|
+
test
|
|
2166
|
+
(
|
|
2167
|
+
'GET /PendingInput is now empty for that run',
|
|
2168
|
+
async function ()
|
|
2169
|
+
{
|
|
2170
|
+
this.timeout(10000);
|
|
2171
|
+
|
|
2172
|
+
let tmpResponse = await apiGet('/PendingInput');
|
|
2173
|
+
libAssert.strictEqual(tmpResponse.status, 200, 'Should return 200');
|
|
2174
|
+
|
|
2175
|
+
let tmpFound = tmpResponse.body.find(function (pItem) { return pItem.RunHash === _PendingRunHash; });
|
|
2176
|
+
libAssert.ok(!tmpFound, 'Completed run should no longer appear in pending inputs');
|
|
2177
|
+
|
|
2178
|
+
console.log(' Remaining pending inputs:', tmpResponse.body.length);
|
|
2179
|
+
|
|
2180
|
+
_TestResults.passed++;
|
|
2181
|
+
}
|
|
2182
|
+
);
|
|
2183
|
+
}
|
|
2184
|
+
);
|
|
2185
|
+
|
|
2186
|
+
// ════════════════════════════════════════════════
|
|
2187
|
+
// Console Errors
|
|
2188
|
+
// ════════════════════════════════════════════════
|
|
2189
|
+
suite
|
|
2190
|
+
(
|
|
2191
|
+
'Console Errors',
|
|
2192
|
+
function ()
|
|
2193
|
+
{
|
|
2194
|
+
test
|
|
2195
|
+
(
|
|
2196
|
+
'no critical console errors during all tests',
|
|
2197
|
+
function ()
|
|
2198
|
+
{
|
|
2199
|
+
let tmpCriticalErrors = _ConsoleErrors.filter(function (pMsg)
|
|
2200
|
+
{
|
|
2201
|
+
if (pMsg.includes('fetch') || pMsg.includes('Failed to load')
|
|
2202
|
+
|| pMsg.includes('NetworkError') || pMsg.includes('net::')
|
|
2203
|
+
|| pMsg.includes('TypeError: Cannot read properties of undefined'))
|
|
2204
|
+
{
|
|
2205
|
+
return false;
|
|
2206
|
+
}
|
|
2207
|
+
return true;
|
|
2208
|
+
});
|
|
2209
|
+
|
|
2210
|
+
if (tmpCriticalErrors.length > 0)
|
|
2211
|
+
{
|
|
2212
|
+
console.log(' Critical browser errors found:');
|
|
2213
|
+
tmpCriticalErrors.forEach(function (pErr)
|
|
2214
|
+
{
|
|
2215
|
+
console.log(' -', pErr);
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
console.log(` Browser errors: ${_ConsoleErrors.length} total, ${tmpCriticalErrors.length} critical`);
|
|
2220
|
+
_TestResults.passed++;
|
|
2221
|
+
}
|
|
2222
|
+
);
|
|
2223
|
+
}
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
);
|