retold-data-service 2.0.16 → 2.0.18
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 +2 -2
- package/.quackage.json +19 -0
- package/package.json +13 -6
- package/source/services/data-cloner/DataCloner-Command-Sync.js +83 -50
- package/source/services/data-cloner/DataCloner-Command-WebUI.js +27 -10
- package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +281 -4
- package/source/services/data-cloner/pict-app/Pict-Application-DataCloner-Configuration.json +9 -0
- package/source/services/data-cloner/pict-app/Pict-Application-DataCloner.js +102 -0
- package/source/services/data-cloner/pict-app/Pict-DataCloner-Bundle.js +6 -0
- package/source/services/data-cloner/pict-app/providers/Pict-Provider-DataCloner.js +998 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Connection.js +407 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Deploy.js +126 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Export.js +483 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Layout.js +390 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Schema.js +241 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Session.js +268 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Sync.js +575 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-ViewData.js +176 -0
- package/source/services/data-cloner/web/data-cloner.js +7952 -0
- package/source/services/data-cloner/web/data-cloner.js.map +1 -0
- package/source/services/data-cloner/web/data-cloner.min.js +2 -0
- package/source/services/data-cloner/web/data-cloner.min.js.map +1 -0
- package/source/services/data-cloner/web/index.html +17 -0
- package/test/DataCloner-Integration_tests.js +1205 -0
- package/test/DataCloner-Puppeteer_tests.js +502 -0
- package/test/integration-report.json +311 -0
- package/test/run-integration-tests.js +501 -0
- package/source/services/data-cloner/data-cloner-web.html +0 -2706
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Data Cloner Integration Test Runner
|
|
4
|
+
*
|
|
5
|
+
* Starts retold-harness and data-cloner servers, runs integration tests,
|
|
6
|
+
* collects timing data, and generates a summary report.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node test/run-integration-tests.js [--skip-puppeteer] [--engines sqlite,mysql]
|
|
10
|
+
*
|
|
11
|
+
* @license MIT
|
|
12
|
+
* @author <steven@velozo.com>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const libChildProcess = require('child_process');
|
|
16
|
+
const libFs = require('fs');
|
|
17
|
+
const libPath = require('path');
|
|
18
|
+
const libOs = require('os');
|
|
19
|
+
const libHttp = require('http');
|
|
20
|
+
|
|
21
|
+
// ---- Configuration ----
|
|
22
|
+
const _HarnessPort = 9403;
|
|
23
|
+
const _ClonerPort = 9400;
|
|
24
|
+
const _HarnessBaseURL = `http://localhost:${_HarnessPort}`;
|
|
25
|
+
const _ClonerBaseURL = `http://localhost:${_ClonerPort}`;
|
|
26
|
+
const _RetoldRoot = libPath.resolve(__dirname, '..', '..', '..', '..');
|
|
27
|
+
const _HarnessDir = libPath.resolve(_RetoldRoot, 'modules', 'meadow', 'retold-harness');
|
|
28
|
+
const _ClonerDir = libPath.resolve(__dirname, '..');
|
|
29
|
+
const _HealthTimeout = 30000;
|
|
30
|
+
const _OverallTimeout = 600000;
|
|
31
|
+
|
|
32
|
+
// ---- Parse CLI args ----
|
|
33
|
+
let _SkipPuppeteer = process.argv.includes('--skip-puppeteer');
|
|
34
|
+
let _EngineArg = process.argv.find((a) => a.startsWith('--engines='));
|
|
35
|
+
let _RequestedEngines = _EngineArg ? _EngineArg.split('=')[1].split(',') : ['sqlite'];
|
|
36
|
+
|
|
37
|
+
// ---- State ----
|
|
38
|
+
let _HarnessProcess = null;
|
|
39
|
+
let _ClonerProcess = null;
|
|
40
|
+
let _TempDir = null;
|
|
41
|
+
let _Report = {
|
|
42
|
+
timestamp: new Date().toISOString(),
|
|
43
|
+
duration_ms: 0,
|
|
44
|
+
summary: { total: 0, passed: 0, failed: 0, skipped: 0 },
|
|
45
|
+
storage_engines: {},
|
|
46
|
+
suites: [],
|
|
47
|
+
puppeteer: { available: false, suites: [] }
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ---- Helpers ----
|
|
51
|
+
|
|
52
|
+
function fLog(pMessage)
|
|
53
|
+
{
|
|
54
|
+
let tmpTime = new Date().toISOString().substring(11, 19);
|
|
55
|
+
console.log(`[${tmpTime}] ${pMessage}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fHttpGet(pURL, fCallback)
|
|
59
|
+
{
|
|
60
|
+
let tmpReq = libHttp.get(pURL,
|
|
61
|
+
(pRes) =>
|
|
62
|
+
{
|
|
63
|
+
let tmpChunks = [];
|
|
64
|
+
pRes.on('data', (pChunk) => tmpChunks.push(pChunk));
|
|
65
|
+
pRes.on('end', () =>
|
|
66
|
+
{
|
|
67
|
+
try
|
|
68
|
+
{
|
|
69
|
+
let tmpData = JSON.parse(Buffer.concat(tmpChunks).toString());
|
|
70
|
+
return fCallback(null, tmpData);
|
|
71
|
+
}
|
|
72
|
+
catch (pParseError)
|
|
73
|
+
{
|
|
74
|
+
return fCallback(null, Buffer.concat(tmpChunks).toString());
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
tmpReq.on('error', (pError) => fCallback(pError));
|
|
79
|
+
tmpReq.setTimeout(5000, () => { tmpReq.destroy(); fCallback(new Error('timeout')); });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function fWaitForHealth(pURL, pLabel, fCallback)
|
|
83
|
+
{
|
|
84
|
+
let tmpStart = Date.now();
|
|
85
|
+
|
|
86
|
+
let fPoll = () =>
|
|
87
|
+
{
|
|
88
|
+
if (Date.now() - tmpStart > _HealthTimeout)
|
|
89
|
+
{
|
|
90
|
+
return fCallback(new Error(`${pLabel} did not become healthy within ${_HealthTimeout / 1000}s`));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fHttpGet(pURL,
|
|
94
|
+
(pError) =>
|
|
95
|
+
{
|
|
96
|
+
if (!pError)
|
|
97
|
+
{
|
|
98
|
+
return fCallback();
|
|
99
|
+
}
|
|
100
|
+
setTimeout(fPoll, 500);
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
setTimeout(fPoll, 1000);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function fKillProcess(pProcess, pLabel)
|
|
108
|
+
{
|
|
109
|
+
if (!pProcess || pProcess.exitCode !== null)
|
|
110
|
+
{
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fLog(`Stopping ${pLabel}...`);
|
|
115
|
+
pProcess.kill('SIGTERM');
|
|
116
|
+
|
|
117
|
+
setTimeout(() =>
|
|
118
|
+
{
|
|
119
|
+
if (pProcess.exitCode === null)
|
|
120
|
+
{
|
|
121
|
+
fLog(`Force-killing ${pLabel}...`);
|
|
122
|
+
pProcess.kill('SIGKILL');
|
|
123
|
+
}
|
|
124
|
+
}, 5000);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function fCleanup()
|
|
128
|
+
{
|
|
129
|
+
fKillProcess(_HarnessProcess, 'retold-harness');
|
|
130
|
+
fKillProcess(_ClonerProcess, 'data-cloner');
|
|
131
|
+
|
|
132
|
+
if (_TempDir && libFs.existsSync(_TempDir))
|
|
133
|
+
{
|
|
134
|
+
try
|
|
135
|
+
{
|
|
136
|
+
libFs.rmSync(_TempDir, { recursive: true, force: true });
|
|
137
|
+
fLog(`Cleaned up temp dir: ${_TempDir}`);
|
|
138
|
+
}
|
|
139
|
+
catch (pErr)
|
|
140
|
+
{
|
|
141
|
+
fLog(`Warning: could not clean temp dir: ${pErr.message}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function fFormatDuration(pMs)
|
|
147
|
+
{
|
|
148
|
+
if (pMs < 1000) return `${pMs}ms`;
|
|
149
|
+
let tmpSec = (pMs / 1000).toFixed(1);
|
|
150
|
+
if (pMs < 60000) return `${tmpSec}s`;
|
|
151
|
+
let tmpMin = Math.floor(pMs / 60000);
|
|
152
|
+
tmpSec = ((pMs % 60000) / 1000).toFixed(0);
|
|
153
|
+
return `${tmpMin}m ${tmpSec}s`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function fPrintReport()
|
|
157
|
+
{
|
|
158
|
+
console.log('\n' + '='.repeat(72));
|
|
159
|
+
console.log(' DATA CLONER INTEGRATION TEST REPORT');
|
|
160
|
+
console.log('='.repeat(72));
|
|
161
|
+
console.log(` Timestamp: ${_Report.timestamp}`);
|
|
162
|
+
console.log(` Duration: ${fFormatDuration(_Report.duration_ms)}`);
|
|
163
|
+
console.log(` Total: ${_Report.summary.total}`);
|
|
164
|
+
console.log(` Passed: ${_Report.summary.passed}`);
|
|
165
|
+
console.log(` Failed: ${_Report.summary.failed}`);
|
|
166
|
+
console.log(` Skipped: ${_Report.summary.skipped}`);
|
|
167
|
+
console.log('-'.repeat(72));
|
|
168
|
+
|
|
169
|
+
// Storage engines
|
|
170
|
+
console.log('\n Storage Engines:');
|
|
171
|
+
let tmpEngines = Object.keys(_Report.storage_engines);
|
|
172
|
+
for (let i = 0; i < tmpEngines.length; i++)
|
|
173
|
+
{
|
|
174
|
+
let tmpName = tmpEngines[i];
|
|
175
|
+
let tmpEngine = _Report.storage_engines[tmpName];
|
|
176
|
+
if (tmpEngine.status === 'pass')
|
|
177
|
+
{
|
|
178
|
+
console.log(` ${tmpName.padEnd(14)} PASS ${fFormatDuration(tmpEngine.sync_duration_ms).padStart(10)} ${tmpEngine.records_synced} records / ${tmpEngine.tables_synced} tables`);
|
|
179
|
+
}
|
|
180
|
+
else if (tmpEngine.status === 'fail')
|
|
181
|
+
{
|
|
182
|
+
console.log(` ${tmpName.padEnd(14)} FAIL ${tmpEngine.error || 'Unknown error'}`);
|
|
183
|
+
}
|
|
184
|
+
else
|
|
185
|
+
{
|
|
186
|
+
console.log(` ${tmpName.padEnd(14)} SKIP ${tmpEngine.reason || ''}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Suites
|
|
191
|
+
console.log('\n Test Suites:');
|
|
192
|
+
for (let i = 0; i < _Report.suites.length; i++)
|
|
193
|
+
{
|
|
194
|
+
let tmpSuite = _Report.suites[i];
|
|
195
|
+
let tmpPassCount = tmpSuite.tests.filter((t) => t.status === 'pass').length;
|
|
196
|
+
let tmpFailCount = tmpSuite.tests.filter((t) => t.status === 'fail').length;
|
|
197
|
+
let tmpStatus = tmpFailCount > 0 ? 'FAIL' : 'PASS';
|
|
198
|
+
console.log(` ${tmpStatus === 'FAIL' ? 'X' : '+'} ${tmpSuite.name.padEnd(35)} ${tmpPassCount}/${tmpSuite.tests.length} passed ${fFormatDuration(tmpSuite.duration_ms)}`);
|
|
199
|
+
|
|
200
|
+
// Show failed tests
|
|
201
|
+
for (let j = 0; j < tmpSuite.tests.length; j++)
|
|
202
|
+
{
|
|
203
|
+
if (tmpSuite.tests[j].status === 'fail')
|
|
204
|
+
{
|
|
205
|
+
console.log(` FAIL: ${tmpSuite.tests[j].name}`);
|
|
206
|
+
if (tmpSuite.tests[j].error)
|
|
207
|
+
{
|
|
208
|
+
console.log(` ${tmpSuite.tests[j].error.substring(0, 120)}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Puppeteer
|
|
215
|
+
if (_Report.puppeteer.available && _Report.puppeteer.suites.length > 0)
|
|
216
|
+
{
|
|
217
|
+
console.log('\n Puppeteer Tests:');
|
|
218
|
+
for (let i = 0; i < _Report.puppeteer.suites.length; i++)
|
|
219
|
+
{
|
|
220
|
+
let tmpSuite = _Report.puppeteer.suites[i];
|
|
221
|
+
let tmpPassCount = tmpSuite.tests.filter((t) => t.status === 'pass').length;
|
|
222
|
+
let tmpFailCount = tmpSuite.tests.filter((t) => t.status === 'fail').length;
|
|
223
|
+
let tmpStatus = tmpFailCount > 0 ? 'FAIL' : 'PASS';
|
|
224
|
+
console.log(` ${tmpStatus === 'FAIL' ? 'X' : '+'} ${tmpSuite.name.padEnd(35)} ${tmpPassCount}/${tmpSuite.tests.length} passed ${fFormatDuration(tmpSuite.duration_ms)}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.log('\n' + '='.repeat(72) + '\n');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---- Main ----
|
|
232
|
+
|
|
233
|
+
let tmpOverallStart = Date.now();
|
|
234
|
+
|
|
235
|
+
// Create temp directory for SQLite
|
|
236
|
+
_TempDir = libPath.join(libOs.tmpdir(), `retold-cloner-test-${Date.now()}`);
|
|
237
|
+
libFs.mkdirSync(libPath.join(_TempDir, 'data'), { recursive: true });
|
|
238
|
+
fLog(`Temp directory: ${_TempDir}`);
|
|
239
|
+
|
|
240
|
+
// Handle cleanup on exit
|
|
241
|
+
process.on('SIGINT', () => { fCleanup(); process.exit(1); });
|
|
242
|
+
process.on('SIGTERM', () => { fCleanup(); process.exit(1); });
|
|
243
|
+
process.on('uncaughtException', (pErr) => { console.error('Uncaught:', pErr); fCleanup(); process.exit(1); });
|
|
244
|
+
|
|
245
|
+
// Step 1: Start retold-harness
|
|
246
|
+
fLog(`Starting retold-harness on port ${_HarnessPort}...`);
|
|
247
|
+
_HarnessProcess = libChildProcess.spawn('node',
|
|
248
|
+
['source/Retold-Harness.js'],
|
|
249
|
+
{
|
|
250
|
+
cwd: _HarnessDir,
|
|
251
|
+
env: Object.assign({}, process.env, { PORT: String(_HarnessPort) }),
|
|
252
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
_HarnessProcess.stdout.on('data', (pData) =>
|
|
256
|
+
{
|
|
257
|
+
let tmpLines = pData.toString().trim().split('\n');
|
|
258
|
+
for (let i = 0; i < tmpLines.length; i++)
|
|
259
|
+
{
|
|
260
|
+
if (tmpLines[i].indexOf('error') > -1 || tmpLines[i].indexOf('Error') > -1)
|
|
261
|
+
{
|
|
262
|
+
fLog(`[harness] ${tmpLines[i]}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
_HarnessProcess.stderr.on('data', (pData) => fLog(`[harness stderr] ${pData.toString().trim()}`));
|
|
267
|
+
_HarnessProcess.on('exit', (pCode) => fLog(`retold-harness exited with code ${pCode}`));
|
|
268
|
+
|
|
269
|
+
// Step 2: Start data-cloner
|
|
270
|
+
fLog(`Starting data-cloner on port ${_ClonerPort}...`);
|
|
271
|
+
_ClonerProcess = libChildProcess.spawn('node',
|
|
272
|
+
[libPath.join(_ClonerDir, 'bin', 'retold-data-service-clone.js'), '--port', String(_ClonerPort)],
|
|
273
|
+
{
|
|
274
|
+
cwd: _TempDir,
|
|
275
|
+
env: Object.assign({}, process.env),
|
|
276
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
_ClonerProcess.stdout.on('data', (pData) =>
|
|
280
|
+
{
|
|
281
|
+
let tmpLines = pData.toString().trim().split('\n');
|
|
282
|
+
for (let i = 0; i < tmpLines.length; i++)
|
|
283
|
+
{
|
|
284
|
+
if (tmpLines[i].indexOf('error') > -1 || tmpLines[i].indexOf('Error') > -1)
|
|
285
|
+
{
|
|
286
|
+
fLog(`[cloner] ${tmpLines[i]}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
_ClonerProcess.stderr.on('data', (pData) => fLog(`[cloner stderr] ${pData.toString().trim()}`));
|
|
291
|
+
_ClonerProcess.on('exit', (pCode) => fLog(`data-cloner exited with code ${pCode}`));
|
|
292
|
+
|
|
293
|
+
// Step 3: Wait for both servers to be healthy
|
|
294
|
+
fLog('Waiting for servers to become healthy...');
|
|
295
|
+
fWaitForHealth(`${_HarnessBaseURL}/1.0/Books/Count`, 'retold-harness',
|
|
296
|
+
(pHarnessError) =>
|
|
297
|
+
{
|
|
298
|
+
if (pHarnessError)
|
|
299
|
+
{
|
|
300
|
+
console.error(pHarnessError.message);
|
|
301
|
+
fCleanup();
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
fLog('retold-harness is healthy.');
|
|
305
|
+
|
|
306
|
+
fWaitForHealth(`${_ClonerBaseURL}/clone/sync/status`, 'data-cloner',
|
|
307
|
+
(pClonerError) =>
|
|
308
|
+
{
|
|
309
|
+
if (pClonerError)
|
|
310
|
+
{
|
|
311
|
+
console.error(pClonerError.message);
|
|
312
|
+
fCleanup();
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
fLog('data-cloner is healthy.');
|
|
316
|
+
fLog('Both servers healthy. Running tests...\n');
|
|
317
|
+
|
|
318
|
+
// Step 4: Run Mocha programmatically
|
|
319
|
+
let Mocha = require('mocha');
|
|
320
|
+
let tmpMocha = new Mocha(
|
|
321
|
+
{
|
|
322
|
+
ui: 'tdd',
|
|
323
|
+
timeout: 120000,
|
|
324
|
+
slow: 5000,
|
|
325
|
+
reporter: 'spec'
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Set env vars for tests
|
|
329
|
+
process.env.CLONER_PORT = String(_ClonerPort);
|
|
330
|
+
process.env.HARNESS_PORT = String(_HarnessPort);
|
|
331
|
+
process.env.CLONER_TEMP_DIR = _TempDir;
|
|
332
|
+
process.env.REQUESTED_ENGINES = _RequestedEngines.join(',');
|
|
333
|
+
process.env.HARNESS_SCHEMA_PATH = libPath.join(_HarnessDir, 'source', 'schemas', 'bookstore', 'MeadowModel-Extended.json');
|
|
334
|
+
|
|
335
|
+
tmpMocha.addFile(libPath.join(__dirname, 'DataCloner-Integration_tests.js'));
|
|
336
|
+
|
|
337
|
+
// Collect results
|
|
338
|
+
let tmpSuiteMap = {};
|
|
339
|
+
let tmpCurrentSuite = null;
|
|
340
|
+
|
|
341
|
+
let tmpRunner = tmpMocha.run(
|
|
342
|
+
(pFailures) =>
|
|
343
|
+
{
|
|
344
|
+
// Finalize suite data
|
|
345
|
+
let tmpSuiteNames = Object.keys(tmpSuiteMap);
|
|
346
|
+
for (let i = 0; i < tmpSuiteNames.length; i++)
|
|
347
|
+
{
|
|
348
|
+
let tmpS = tmpSuiteMap[tmpSuiteNames[i]];
|
|
349
|
+
tmpS.duration_ms = tmpS.tests.reduce((a, t) => a + t.duration_ms, 0);
|
|
350
|
+
_Report.suites.push(tmpS);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
_Report.summary.total = tmpRunner.stats.tests;
|
|
354
|
+
_Report.summary.passed = tmpRunner.stats.passes;
|
|
355
|
+
_Report.summary.failed = tmpRunner.stats.failures;
|
|
356
|
+
_Report.summary.skipped = tmpRunner.stats.pending || 0;
|
|
357
|
+
|
|
358
|
+
// Step 5: Optionally run Puppeteer tests
|
|
359
|
+
let fFinish = () =>
|
|
360
|
+
{
|
|
361
|
+
_Report.duration_ms = Date.now() - tmpOverallStart;
|
|
362
|
+
|
|
363
|
+
// Write report
|
|
364
|
+
let tmpReportPath = libPath.join(__dirname, 'integration-report.json');
|
|
365
|
+
try
|
|
366
|
+
{
|
|
367
|
+
libFs.writeFileSync(tmpReportPath, JSON.stringify(_Report, null, '\t'), 'utf8');
|
|
368
|
+
fLog(`Report written to ${tmpReportPath}`);
|
|
369
|
+
}
|
|
370
|
+
catch (pWriteErr)
|
|
371
|
+
{
|
|
372
|
+
fLog(`Warning: could not write report: ${pWriteErr.message}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
fPrintReport();
|
|
376
|
+
fCleanup();
|
|
377
|
+
process.exit(pFailures > 0 ? 1 : 0);
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
if (!_SkipPuppeteer)
|
|
381
|
+
{
|
|
382
|
+
try
|
|
383
|
+
{
|
|
384
|
+
require.resolve('puppeteer');
|
|
385
|
+
_Report.puppeteer.available = true;
|
|
386
|
+
fLog('\nRunning Puppeteer tests...');
|
|
387
|
+
|
|
388
|
+
let tmpPuppeteerMocha = new Mocha(
|
|
389
|
+
{
|
|
390
|
+
ui: 'tdd',
|
|
391
|
+
timeout: 60000,
|
|
392
|
+
slow: 10000,
|
|
393
|
+
reporter: 'spec'
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
let tmpPuppeteerTestPath = libPath.join(__dirname, 'DataCloner-Puppeteer_tests.js');
|
|
397
|
+
if (libFs.existsSync(tmpPuppeteerTestPath))
|
|
398
|
+
{
|
|
399
|
+
tmpPuppeteerMocha.addFile(tmpPuppeteerTestPath);
|
|
400
|
+
|
|
401
|
+
let tmpPuppeteerSuiteMap = {};
|
|
402
|
+
let tmpPRunner = tmpPuppeteerMocha.run(
|
|
403
|
+
(pPuppeteerFailures) =>
|
|
404
|
+
{
|
|
405
|
+
let tmpPSuiteNames = Object.keys(tmpPuppeteerSuiteMap);
|
|
406
|
+
for (let i = 0; i < tmpPSuiteNames.length; i++)
|
|
407
|
+
{
|
|
408
|
+
let tmpPS = tmpPuppeteerSuiteMap[tmpPSuiteNames[i]];
|
|
409
|
+
tmpPS.duration_ms = tmpPS.tests.reduce((a, t) => a + t.duration_ms, 0);
|
|
410
|
+
_Report.puppeteer.suites.push(tmpPS);
|
|
411
|
+
}
|
|
412
|
+
_Report.summary.total += tmpPRunner.stats.tests;
|
|
413
|
+
_Report.summary.passed += tmpPRunner.stats.passes;
|
|
414
|
+
_Report.summary.failed += tmpPRunner.stats.failures;
|
|
415
|
+
fFinish();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
tmpPRunner.on('suite', (pSuite) =>
|
|
419
|
+
{
|
|
420
|
+
if (pSuite.title && pSuite.title !== '')
|
|
421
|
+
{
|
|
422
|
+
tmpPuppeteerSuiteMap[pSuite.title] = { name: pSuite.title, tests: [], duration_ms: 0 };
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
tmpPRunner.on('pass', (pTest) =>
|
|
426
|
+
{
|
|
427
|
+
let tmpSuiteName = pTest.parent ? pTest.parent.title : 'Unknown';
|
|
428
|
+
if (tmpPuppeteerSuiteMap[tmpSuiteName])
|
|
429
|
+
{
|
|
430
|
+
tmpPuppeteerSuiteMap[tmpSuiteName].tests.push({ name: pTest.title, status: 'pass', duration_ms: pTest.duration || 0 });
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
tmpPRunner.on('fail', (pTest, pErr) =>
|
|
434
|
+
{
|
|
435
|
+
let tmpSuiteName = pTest.parent ? pTest.parent.title : 'Unknown';
|
|
436
|
+
if (tmpPuppeteerSuiteMap[tmpSuiteName])
|
|
437
|
+
{
|
|
438
|
+
tmpPuppeteerSuiteMap[tmpSuiteName].tests.push({ name: pTest.title, status: 'fail', duration_ms: pTest.duration || 0, error: pErr.message });
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
else
|
|
443
|
+
{
|
|
444
|
+
fLog('Puppeteer test file not found, skipping.');
|
|
445
|
+
fFinish();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch (pResolveErr)
|
|
449
|
+
{
|
|
450
|
+
fLog('Puppeteer not installed, skipping browser tests. Install with: npm install --save-dev puppeteer');
|
|
451
|
+
fFinish();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
else
|
|
455
|
+
{
|
|
456
|
+
fLog('Skipping Puppeteer tests (--skip-puppeteer).');
|
|
457
|
+
fFinish();
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Collect per-test timing
|
|
462
|
+
tmpRunner.on('suite', (pSuite) =>
|
|
463
|
+
{
|
|
464
|
+
if (pSuite.title && pSuite.title !== '')
|
|
465
|
+
{
|
|
466
|
+
tmpCurrentSuite = pSuite.title;
|
|
467
|
+
if (!tmpSuiteMap[tmpCurrentSuite])
|
|
468
|
+
{
|
|
469
|
+
tmpSuiteMap[tmpCurrentSuite] = { name: tmpCurrentSuite, tests: [], duration_ms: 0 };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
tmpRunner.on('pass', (pTest) =>
|
|
474
|
+
{
|
|
475
|
+
let tmpSuiteName = pTest.parent ? pTest.parent.title : tmpCurrentSuite || 'Unknown';
|
|
476
|
+
if (!tmpSuiteMap[tmpSuiteName])
|
|
477
|
+
{
|
|
478
|
+
tmpSuiteMap[tmpSuiteName] = { name: tmpSuiteName, tests: [], duration_ms: 0 };
|
|
479
|
+
}
|
|
480
|
+
tmpSuiteMap[tmpSuiteName].tests.push({ name: pTest.title, status: 'pass', duration_ms: pTest.duration || 0 });
|
|
481
|
+
});
|
|
482
|
+
tmpRunner.on('fail', (pTest, pErr) =>
|
|
483
|
+
{
|
|
484
|
+
let tmpSuiteName = pTest.parent ? pTest.parent.title : tmpCurrentSuite || 'Unknown';
|
|
485
|
+
if (!tmpSuiteMap[tmpSuiteName])
|
|
486
|
+
{
|
|
487
|
+
tmpSuiteMap[tmpSuiteName] = { name: tmpSuiteName, tests: [], duration_ms: 0 };
|
|
488
|
+
}
|
|
489
|
+
tmpSuiteMap[tmpSuiteName].tests.push({ name: pTest.title, status: 'fail', duration_ms: pTest.duration || 0, error: pErr.message });
|
|
490
|
+
});
|
|
491
|
+
tmpRunner.on('pending', (pTest) =>
|
|
492
|
+
{
|
|
493
|
+
let tmpSuiteName = pTest.parent ? pTest.parent.title : tmpCurrentSuite || 'Unknown';
|
|
494
|
+
if (!tmpSuiteMap[tmpSuiteName])
|
|
495
|
+
{
|
|
496
|
+
tmpSuiteMap[tmpSuiteName] = { name: tmpSuiteName, tests: [], duration_ms: 0 };
|
|
497
|
+
}
|
|
498
|
+
tmpSuiteMap[tmpSuiteName].tests.push({ name: pTest.title, status: 'skip', duration_ms: 0 });
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
});
|