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,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Cloner Puppeteer (Web UI) Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Exercises the data-cloner web interface using headless Chrome via Puppeteer.
|
|
5
|
+
* Requires retold-harness running on HARNESS_PORT and data-cloner on CLONER_PORT.
|
|
6
|
+
*
|
|
7
|
+
* Run via: node test/run-integration-tests.js
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
* @author <steven@velozo.com>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
var Chai = require('chai');
|
|
14
|
+
var Expect = Chai.expect;
|
|
15
|
+
var libFs = require('fs');
|
|
16
|
+
var libPath = require('path');
|
|
17
|
+
|
|
18
|
+
var _DebugDistPath = libPath.resolve(__dirname, '..', 'debug', 'dist');
|
|
19
|
+
|
|
20
|
+
var _ClonerPort = parseInt(process.env.CLONER_PORT, 10) || 9400;
|
|
21
|
+
var _HarnessPort = parseInt(process.env.HARNESS_PORT, 10) || 9403;
|
|
22
|
+
var _ClonerURL = `http://localhost:${_ClonerPort}/clone/`;
|
|
23
|
+
var _HarnessURL = `http://localhost:${_HarnessPort}/1.0/`;
|
|
24
|
+
|
|
25
|
+
// Load the harness schema from disk (retold-harness doesn't serve /Retold/Models)
|
|
26
|
+
var _HarnessSchemaPath = process.env.HARNESS_SCHEMA_PATH || require('path').resolve(__dirname, '..', '..', 'retold-harness', 'source', 'schemas', 'bookstore', 'MeadowModel-Extended.json');
|
|
27
|
+
var _HarnessSchema = JSON.parse(libFs.readFileSync(_HarnessSchemaPath, 'utf8'));
|
|
28
|
+
|
|
29
|
+
var _Browser = null;
|
|
30
|
+
var _Page = null;
|
|
31
|
+
|
|
32
|
+
suite
|
|
33
|
+
(
|
|
34
|
+
'Data Cloner Web UI (Puppeteer)',
|
|
35
|
+
function()
|
|
36
|
+
{
|
|
37
|
+
this.timeout(60000);
|
|
38
|
+
|
|
39
|
+
suiteSetup
|
|
40
|
+
(
|
|
41
|
+
async function()
|
|
42
|
+
{
|
|
43
|
+
this.timeout(30000);
|
|
44
|
+
libFs.mkdirSync(_DebugDistPath, { recursive: true });
|
|
45
|
+
var puppeteer = require('puppeteer');
|
|
46
|
+
_Browser = await puppeteer.launch(
|
|
47
|
+
{
|
|
48
|
+
headless: 'new',
|
|
49
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
50
|
+
});
|
|
51
|
+
_Page = await _Browser.newPage();
|
|
52
|
+
_Page.setDefaultTimeout(30000);
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
suiteTeardown
|
|
57
|
+
(
|
|
58
|
+
async function()
|
|
59
|
+
{
|
|
60
|
+
if (_Browser)
|
|
61
|
+
{
|
|
62
|
+
await _Browser.close();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// ---- Page Load ----
|
|
68
|
+
suite
|
|
69
|
+
(
|
|
70
|
+
'Web UI Load',
|
|
71
|
+
function()
|
|
72
|
+
{
|
|
73
|
+
test
|
|
74
|
+
(
|
|
75
|
+
'Should load the data cloner page',
|
|
76
|
+
async function()
|
|
77
|
+
{
|
|
78
|
+
await _Page.goto(_ClonerURL, { waitUntil: 'networkidle2' });
|
|
79
|
+
var tmpTitle = await _Page.title();
|
|
80
|
+
Expect(tmpTitle).to.equal('Retold Data Cloner');
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
test
|
|
84
|
+
(
|
|
85
|
+
'Should display 7 accordion sections',
|
|
86
|
+
async function()
|
|
87
|
+
{
|
|
88
|
+
// Each section has a step number (1-7)
|
|
89
|
+
var tmpSections = await _Page.$$eval('.accordion-number',
|
|
90
|
+
(els) => els.map((el) => el.textContent.trim()));
|
|
91
|
+
Expect(tmpSections.length).to.equal(7);
|
|
92
|
+
Expect(tmpSections).to.deep.equal(['1', '2', '3', '4', '5', '6', '7']);
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
test
|
|
96
|
+
(
|
|
97
|
+
'Should show live status bar',
|
|
98
|
+
async function()
|
|
99
|
+
{
|
|
100
|
+
// The live status bar should be present (may already be connected from API tests)
|
|
101
|
+
var tmpStatusText = await _Page.$eval('#liveStatusMessage',
|
|
102
|
+
(el) => el.textContent.trim());
|
|
103
|
+
Expect(tmpStatusText).to.be.a('string');
|
|
104
|
+
Expect(tmpStatusText.length).to.be.greaterThan(0);
|
|
105
|
+
|
|
106
|
+
await _Page.screenshot({ path: libPath.join(_DebugDistPath, '01-web-ui-load.png'), fullPage: true });
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// ---- Connection Section ----
|
|
113
|
+
suite
|
|
114
|
+
(
|
|
115
|
+
'Connection Section',
|
|
116
|
+
function()
|
|
117
|
+
{
|
|
118
|
+
test
|
|
119
|
+
(
|
|
120
|
+
'Should connect to SQLite via go button',
|
|
121
|
+
async function()
|
|
122
|
+
{
|
|
123
|
+
this.timeout(15000);
|
|
124
|
+
|
|
125
|
+
// Find and click the "go" link for section 1 (Connection)
|
|
126
|
+
await _Page.evaluate(() =>
|
|
127
|
+
{
|
|
128
|
+
pict.views['DataCloner-Connection'].connectProvider();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Wait for status to update to connected
|
|
132
|
+
await _Page.waitForFunction(() =>
|
|
133
|
+
{
|
|
134
|
+
var el = document.querySelector('.live-status-message');
|
|
135
|
+
return el && el.textContent.indexOf('database connected') < 0 &&
|
|
136
|
+
el.textContent.indexOf('Connected') >= 0 ||
|
|
137
|
+
el.textContent.indexOf('waiting') >= 0;
|
|
138
|
+
}, { timeout: 10000 }).catch(() => {});
|
|
139
|
+
|
|
140
|
+
// Give it a moment to settle
|
|
141
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
142
|
+
|
|
143
|
+
// Verify connection status via API
|
|
144
|
+
var tmpResponse = await _Page.evaluate(async () =>
|
|
145
|
+
{
|
|
146
|
+
var res = await fetch('/clone/connection/status');
|
|
147
|
+
return res.json();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
Expect(tmpResponse.Connected).to.equal(true);
|
|
151
|
+
Expect(tmpResponse.Provider).to.equal('SQLite');
|
|
152
|
+
|
|
153
|
+
await _Page.screenshot({ path: libPath.join(_DebugDistPath, '02-connection.png'), fullPage: true });
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// ---- Session + Schema + Deploy Flow ----
|
|
160
|
+
suite
|
|
161
|
+
(
|
|
162
|
+
'Session, Schema, and Deploy Flow',
|
|
163
|
+
function()
|
|
164
|
+
{
|
|
165
|
+
test
|
|
166
|
+
(
|
|
167
|
+
'Should configure session via UI',
|
|
168
|
+
async function()
|
|
169
|
+
{
|
|
170
|
+
this.timeout(15000);
|
|
171
|
+
|
|
172
|
+
// Set the server URL in the input field
|
|
173
|
+
await _Page.evaluate((pURL) =>
|
|
174
|
+
{
|
|
175
|
+
var tmpInput = document.getElementById('serverURL');
|
|
176
|
+
if (tmpInput)
|
|
177
|
+
{
|
|
178
|
+
tmpInput.value = pURL;
|
|
179
|
+
}
|
|
180
|
+
}, _HarnessURL.replace(/\/1\.0\/$/, ''));
|
|
181
|
+
|
|
182
|
+
// Configure session via the view method
|
|
183
|
+
await _Page.evaluate(() =>
|
|
184
|
+
{
|
|
185
|
+
pict.views['DataCloner-Session'].configureSession();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
189
|
+
|
|
190
|
+
// Verify session configured
|
|
191
|
+
var tmpSession = await _Page.evaluate(async () =>
|
|
192
|
+
{
|
|
193
|
+
var res = await fetch('/clone/session/check');
|
|
194
|
+
return res.json();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
Expect(tmpSession.Configured).to.equal(true);
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
test
|
|
201
|
+
(
|
|
202
|
+
'Should fetch schema via UI',
|
|
203
|
+
async function()
|
|
204
|
+
{
|
|
205
|
+
this.timeout(15000);
|
|
206
|
+
|
|
207
|
+
// Fetch schema (pass directly since retold-harness doesn't serve /Retold/Models)
|
|
208
|
+
await _Page.evaluate(async (pSchema) =>
|
|
209
|
+
{
|
|
210
|
+
await fetch('/clone/schema/fetch',
|
|
211
|
+
{
|
|
212
|
+
method: 'POST',
|
|
213
|
+
headers: { 'Content-Type': 'application/json' },
|
|
214
|
+
body: JSON.stringify({ Schema: pSchema })
|
|
215
|
+
});
|
|
216
|
+
}, _HarnessSchema);
|
|
217
|
+
|
|
218
|
+
// Wait for schema data to settle
|
|
219
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
220
|
+
|
|
221
|
+
// Verify schema fetched
|
|
222
|
+
var tmpSchema = await _Page.evaluate(async () =>
|
|
223
|
+
{
|
|
224
|
+
var res = await fetch('/clone/schema');
|
|
225
|
+
return res.json();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
Expect(tmpSchema.Fetched).to.equal(true);
|
|
229
|
+
Expect(tmpSchema.TableCount).to.be.greaterThan(0);
|
|
230
|
+
Expect(tmpSchema.Tables).to.include('Book');
|
|
231
|
+
Expect(tmpSchema.Tables).to.include('Author');
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
test
|
|
235
|
+
(
|
|
236
|
+
'Should deploy schema via UI',
|
|
237
|
+
async function()
|
|
238
|
+
{
|
|
239
|
+
this.timeout(30000);
|
|
240
|
+
|
|
241
|
+
// Deploy all tables
|
|
242
|
+
await _Page.evaluate(() =>
|
|
243
|
+
{
|
|
244
|
+
pict.views['DataCloner-Deploy'].deploySchema();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Wait for deploy to complete
|
|
248
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
249
|
+
|
|
250
|
+
// Verify sync entities exist
|
|
251
|
+
var tmpStatus = await _Page.evaluate(async () =>
|
|
252
|
+
{
|
|
253
|
+
var res = await fetch('/clone/sync/status');
|
|
254
|
+
return res.json();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
Expect(tmpStatus).to.be.an('object');
|
|
258
|
+
|
|
259
|
+
await _Page.screenshot({ path: libPath.join(_DebugDistPath, '03-session-schema-deploy.png'), fullPage: true });
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// ---- Sync via Web UI ----
|
|
266
|
+
suite
|
|
267
|
+
(
|
|
268
|
+
'Sync via Web UI',
|
|
269
|
+
function()
|
|
270
|
+
{
|
|
271
|
+
this.timeout(120000);
|
|
272
|
+
|
|
273
|
+
test
|
|
274
|
+
(
|
|
275
|
+
'Should start sync and complete successfully',
|
|
276
|
+
async function()
|
|
277
|
+
{
|
|
278
|
+
this.timeout(120000);
|
|
279
|
+
|
|
280
|
+
// Reset to clear any data from prior API tests
|
|
281
|
+
await _Page.evaluate(async () =>
|
|
282
|
+
{
|
|
283
|
+
await fetch('/clone/reset', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
287
|
+
|
|
288
|
+
// Reconfigure session + schema + deploy via API
|
|
289
|
+
await _Page.evaluate(async (pURL) =>
|
|
290
|
+
{
|
|
291
|
+
await fetch('/clone/session/configure',
|
|
292
|
+
{
|
|
293
|
+
method: 'POST',
|
|
294
|
+
headers: { 'Content-Type': 'application/json' },
|
|
295
|
+
body: JSON.stringify({ ServerURL: pURL })
|
|
296
|
+
});
|
|
297
|
+
}, _HarnessURL);
|
|
298
|
+
|
|
299
|
+
await _Page.evaluate(async (pSchema) =>
|
|
300
|
+
{
|
|
301
|
+
await fetch('/clone/schema/fetch',
|
|
302
|
+
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ Schema: pSchema }) });
|
|
303
|
+
}, _HarnessSchema);
|
|
304
|
+
|
|
305
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
306
|
+
|
|
307
|
+
await _Page.evaluate(async () =>
|
|
308
|
+
{
|
|
309
|
+
await fetch('/clone/schema/deploy',
|
|
310
|
+
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ Tables: [] }) });
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
314
|
+
|
|
315
|
+
// Start sync via API (UI checkboxes may not reflect API-configured schema)
|
|
316
|
+
await _Page.evaluate(async () =>
|
|
317
|
+
{
|
|
318
|
+
await fetch('/clone/sync/start',
|
|
319
|
+
{
|
|
320
|
+
method: 'POST',
|
|
321
|
+
headers: { 'Content-Type': 'application/json' },
|
|
322
|
+
body: JSON.stringify({ SyncMode: 'Initial', MaxRecordsPerEntity: 50 })
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Wait for sync to complete (may finish very quickly with 50-record cap)
|
|
327
|
+
await _Page.waitForFunction(async () =>
|
|
328
|
+
{
|
|
329
|
+
try
|
|
330
|
+
{
|
|
331
|
+
var res = await fetch('/clone/sync/status');
|
|
332
|
+
var data = await res.json();
|
|
333
|
+
return data.Running === false && data.Tables && Object.keys(data.Tables).length > 0;
|
|
334
|
+
}
|
|
335
|
+
catch (e)
|
|
336
|
+
{
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
}, { timeout: 100000, polling: 1000 });
|
|
340
|
+
|
|
341
|
+
// Verify sync completed successfully
|
|
342
|
+
var tmpReport = await _Page.evaluate(async () =>
|
|
343
|
+
{
|
|
344
|
+
var res = await fetch('/clone/sync/report');
|
|
345
|
+
return res.json();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
Expect(tmpReport).to.be.an('object');
|
|
349
|
+
Expect(tmpReport.ReportVersion).to.be.a('string');
|
|
350
|
+
Expect(['Success', 'Partial']).to.include(tmpReport.Outcome);
|
|
351
|
+
Expect(tmpReport.Summary.TotalSynced).to.be.greaterThan(0);
|
|
352
|
+
|
|
353
|
+
await _Page.screenshot({ path: libPath.join(_DebugDistPath, '04-sync-complete.png'), fullPage: true });
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
// ---- Live Status Display ----
|
|
360
|
+
suite
|
|
361
|
+
(
|
|
362
|
+
'Live Status Display',
|
|
363
|
+
function()
|
|
364
|
+
{
|
|
365
|
+
this.timeout(120000);
|
|
366
|
+
|
|
367
|
+
test
|
|
368
|
+
(
|
|
369
|
+
'Should show progress information during sync',
|
|
370
|
+
async function()
|
|
371
|
+
{
|
|
372
|
+
this.timeout(120000);
|
|
373
|
+
|
|
374
|
+
// Reset and start a new sync to observe live status
|
|
375
|
+
await _Page.evaluate(async () =>
|
|
376
|
+
{
|
|
377
|
+
await fetch('/clone/reset', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
381
|
+
|
|
382
|
+
// Reconfigure session
|
|
383
|
+
await _Page.evaluate(async (pURL) =>
|
|
384
|
+
{
|
|
385
|
+
await fetch('/clone/session/configure',
|
|
386
|
+
{
|
|
387
|
+
method: 'POST',
|
|
388
|
+
headers: { 'Content-Type': 'application/json' },
|
|
389
|
+
body: JSON.stringify({ ServerURL: pURL })
|
|
390
|
+
});
|
|
391
|
+
}, _HarnessURL);
|
|
392
|
+
|
|
393
|
+
// Fetch + deploy schema (pass directly since retold-harness doesn't serve /Retold/Models)
|
|
394
|
+
await _Page.evaluate(async (pSchema) =>
|
|
395
|
+
{
|
|
396
|
+
await fetch('/clone/schema/fetch',
|
|
397
|
+
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ Schema: pSchema }) });
|
|
398
|
+
}, _HarnessSchema);
|
|
399
|
+
|
|
400
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
401
|
+
|
|
402
|
+
await _Page.evaluate(async () =>
|
|
403
|
+
{
|
|
404
|
+
await fetch('/clone/schema/deploy',
|
|
405
|
+
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ Tables: [] }) });
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
409
|
+
|
|
410
|
+
// Start sync with NO record cap so it takes long enough to observe
|
|
411
|
+
await _Page.evaluate(async () =>
|
|
412
|
+
{
|
|
413
|
+
await fetch('/clone/sync/start',
|
|
414
|
+
{
|
|
415
|
+
method: 'POST',
|
|
416
|
+
headers: { 'Content-Type': 'application/json' },
|
|
417
|
+
body: JSON.stringify({ SyncMode: 'Initial', MaxRecordsPerEntity: 0 })
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Collect live status samples — poll immediately and quickly
|
|
422
|
+
var tmpSamples = [];
|
|
423
|
+
var tmpMaxSamples = 40;
|
|
424
|
+
|
|
425
|
+
for (var i = 0; i < tmpMaxSamples; i++)
|
|
426
|
+
{
|
|
427
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
428
|
+
|
|
429
|
+
var tmpLive = await _Page.evaluate(async () =>
|
|
430
|
+
{
|
|
431
|
+
var res = await fetch('/clone/sync/live-status');
|
|
432
|
+
return res.json();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
tmpSamples.push(tmpLive);
|
|
436
|
+
|
|
437
|
+
// Check if sync finished
|
|
438
|
+
if (tmpLive.Phase !== 'syncing')
|
|
439
|
+
{
|
|
440
|
+
var tmpStatus = await _Page.evaluate(async () =>
|
|
441
|
+
{
|
|
442
|
+
var res = await fetch('/clone/sync/status');
|
|
443
|
+
return res.json();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
if (!tmpStatus.Running)
|
|
447
|
+
{
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Wait for sync to finish
|
|
454
|
+
await _Page.waitForFunction(async () =>
|
|
455
|
+
{
|
|
456
|
+
try
|
|
457
|
+
{
|
|
458
|
+
var res = await fetch('/clone/sync/status');
|
|
459
|
+
var data = await res.json();
|
|
460
|
+
return !data.Running;
|
|
461
|
+
}
|
|
462
|
+
catch (e)
|
|
463
|
+
{
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
}, { timeout: 60000, polling: 2000 }).catch(() => {});
|
|
467
|
+
|
|
468
|
+
// Verify we captured useful live status data
|
|
469
|
+
Expect(tmpSamples.length).to.be.greaterThan(0);
|
|
470
|
+
|
|
471
|
+
var tmpSyncingSamples = tmpSamples.filter((s) => s.Phase === 'syncing');
|
|
472
|
+
|
|
473
|
+
if (tmpSyncingSamples.length > 0)
|
|
474
|
+
{
|
|
475
|
+
// Verify structure of syncing samples
|
|
476
|
+
var tmpSample = tmpSyncingSamples[0];
|
|
477
|
+
Expect(tmpSample).to.have.property('Message');
|
|
478
|
+
Expect(tmpSample).to.have.property('TotalSynced');
|
|
479
|
+
Expect(tmpSample).to.have.property('TotalRecords');
|
|
480
|
+
Expect(tmpSample).to.have.property('PreCountGrandTotal');
|
|
481
|
+
}
|
|
482
|
+
else
|
|
483
|
+
{
|
|
484
|
+
// Sync completed before we could capture a syncing sample —
|
|
485
|
+
// verify completion via the report instead
|
|
486
|
+
var tmpReport = await _Page.evaluate(async () =>
|
|
487
|
+
{
|
|
488
|
+
var res = await fetch('/clone/sync/report');
|
|
489
|
+
return res.json();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
Expect(tmpReport).to.be.an('object');
|
|
493
|
+
Expect(tmpReport.Summary.TotalSynced).to.be.greaterThan(0);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
await _Page.screenshot({ path: libPath.join(_DebugDistPath, '05-live-status.png'), fullPage: true });
|
|
497
|
+
}
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
);
|