pict-section-login 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/example_applications/custom_login/Custom-Login-Application.js +75 -0
- package/example_applications/custom_login/html/index.html +110 -0
- package/example_applications/custom_login/package.json +27 -0
- package/example_applications/harness_app/Harness-App-Application.js +167 -0
- package/example_applications/harness_app/Harness-App-Configuration.json +4 -0
- package/example_applications/harness_app/html/index.html +90 -0
- package/example_applications/harness_app/package.json +28 -0
- package/example_applications/harness_app/providers/PictRouter-HarnessApp.json +24 -0
- package/example_applications/harness_app/views/PictView-HarnessApp-Books.js +172 -0
- package/example_applications/harness_app/views/PictView-HarnessApp-Dashboard.js +158 -0
- package/example_applications/harness_app/views/PictView-HarnessApp-Layout.js +86 -0
- package/example_applications/harness_app/views/PictView-HarnessApp-Login.js +58 -0
- package/example_applications/harness_app/views/PictView-HarnessApp-TopBar.js +157 -0
- package/example_applications/harness_app/views/PictView-HarnessApp-Users.js +188 -0
- package/example_applications/oauth_login/OAuth-Login-Application.js +78 -0
- package/example_applications/oauth_login/html/index.html +57 -0
- package/example_applications/oauth_login/package.json +27 -0
- package/example_applications/orator_login/Orator-Login-Application.js +61 -0
- package/example_applications/orator_login/html/index.html +51 -0
- package/example_applications/orator_login/package.json +27 -0
- package/package.json +53 -0
- package/source/Pict-Section-Login-DefaultConfiguration.js +265 -0
- package/source/Pict-Section-Login.js +533 -0
- package/test/Browser_Integration_tests.js +588 -0
- package/test/Pict-Section-Login_tests.js +593 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless browser integration tests for pict-section-login.
|
|
3
|
+
*
|
|
4
|
+
* Verifies the login UI works in a real browser environment:
|
|
5
|
+
* 1) The login form renders with expected DOM elements
|
|
6
|
+
* 2) Invalid credentials display an error message
|
|
7
|
+
* 3) Valid credentials show the logged-in status area
|
|
8
|
+
* 4) Logout returns to the login form
|
|
9
|
+
*
|
|
10
|
+
* Uses a mock window.fetch to simulate authentication endpoints without
|
|
11
|
+
* needing a running backend.
|
|
12
|
+
*
|
|
13
|
+
* Requires: npm run build (quackage) to have been run first so dist/ exists.
|
|
14
|
+
*
|
|
15
|
+
* @license MIT
|
|
16
|
+
* @author Steven Velozo <steven@velozo.com>
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const Chai = require('chai');
|
|
20
|
+
const Expect = Chai.expect;
|
|
21
|
+
|
|
22
|
+
const libHTTP = require('http');
|
|
23
|
+
const libFS = require('fs');
|
|
24
|
+
const libPath = require('path');
|
|
25
|
+
|
|
26
|
+
const _PackageRoot = libPath.resolve(__dirname, '..');
|
|
27
|
+
const _DistDir = libPath.join(_PackageRoot, 'dist');
|
|
28
|
+
const _PictDistDir = libPath.join(_PackageRoot, 'node_modules', 'pict', 'dist');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a simple HTTP server that serves the static files needed
|
|
32
|
+
* for the browser test page.
|
|
33
|
+
*
|
|
34
|
+
* @param {function} fCallback - Callback with (pError, pServer, pPort)
|
|
35
|
+
*/
|
|
36
|
+
function startTestServer(fCallback)
|
|
37
|
+
{
|
|
38
|
+
let tmpMimeTypes =
|
|
39
|
+
{
|
|
40
|
+
'.html': 'text/html',
|
|
41
|
+
'.js': 'application/javascript',
|
|
42
|
+
'.map': 'application/json'
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let tmpServer = libHTTP.createServer(
|
|
46
|
+
(pRequest, pResponse) =>
|
|
47
|
+
{
|
|
48
|
+
let tmpUrl = pRequest.url;
|
|
49
|
+
|
|
50
|
+
// Route: / -> test page (generated inline)
|
|
51
|
+
if (tmpUrl === '/' || tmpUrl === '/index.html')
|
|
52
|
+
{
|
|
53
|
+
pResponse.writeHead(200, { 'Content-Type': 'text/html' });
|
|
54
|
+
pResponse.end(generateTestHTML());
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Route: /pict.js -> from node_modules/pict/dist/
|
|
59
|
+
if (tmpUrl === '/pict.js')
|
|
60
|
+
{
|
|
61
|
+
serveFile(libPath.join(_PictDistDir, 'pict.js'), pResponse, tmpMimeTypes);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Route: /pict-section-login.* -> from dist/
|
|
66
|
+
if (tmpUrl.startsWith('/pict-section-login'))
|
|
67
|
+
{
|
|
68
|
+
serveFile(libPath.join(_DistDir, tmpUrl.slice(1)), pResponse, tmpMimeTypes);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
pResponse.writeHead(404);
|
|
73
|
+
pResponse.end('Not Found');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Listen on a random available port
|
|
77
|
+
tmpServer.listen(0, '127.0.0.1',
|
|
78
|
+
() =>
|
|
79
|
+
{
|
|
80
|
+
let tmpPort = tmpServer.address().port;
|
|
81
|
+
return fCallback(null, tmpServer, tmpPort);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Serve a static file from the filesystem.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} pFilePath - Absolute path to file
|
|
89
|
+
* @param {object} pResponse - HTTP response object
|
|
90
|
+
* @param {object} pMimeTypes - Extension -> MIME type map
|
|
91
|
+
*/
|
|
92
|
+
function serveFile(pFilePath, pResponse, pMimeTypes)
|
|
93
|
+
{
|
|
94
|
+
if (!libFS.existsSync(pFilePath))
|
|
95
|
+
{
|
|
96
|
+
pResponse.writeHead(404);
|
|
97
|
+
pResponse.end('File not found: ' + pFilePath);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let tmpExt = libPath.extname(pFilePath);
|
|
102
|
+
let tmpContentType = pMimeTypes[tmpExt] || 'application/octet-stream';
|
|
103
|
+
|
|
104
|
+
let tmpContent = libFS.readFileSync(pFilePath);
|
|
105
|
+
pResponse.writeHead(200, { 'Content-Type': tmpContentType });
|
|
106
|
+
pResponse.end(tmpContent);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Generate the test HTML page that runs in the browser.
|
|
111
|
+
*
|
|
112
|
+
* Loads pict.js (creates global Pict) and pict-section-login.js (creates
|
|
113
|
+
* global PictSectionLogin). Mocks window.fetch to simulate authentication
|
|
114
|
+
* endpoints. Tests exercise form rendering, login, and logout flows.
|
|
115
|
+
* Results are stored on window.__TEST_RESULTS__.
|
|
116
|
+
*
|
|
117
|
+
* @returns {string} HTML content
|
|
118
|
+
*/
|
|
119
|
+
function generateTestHTML()
|
|
120
|
+
{
|
|
121
|
+
return `<!DOCTYPE html>
|
|
122
|
+
<html>
|
|
123
|
+
<head><title>Pict-Section-Login Browser Tests</title></head>
|
|
124
|
+
<body>
|
|
125
|
+
<h1>Pict-Section-Login Browser Integration Tests</h1>
|
|
126
|
+
<pre id="output">Running tests...</pre>
|
|
127
|
+
<div id="Pict-Login-Container"></div>
|
|
128
|
+
|
|
129
|
+
<!-- Load pict (creates global Pict) -->
|
|
130
|
+
<script src="/pict.js"></script>
|
|
131
|
+
|
|
132
|
+
<!-- Load pict-section-login (creates global PictSectionLogin) -->
|
|
133
|
+
<script src="/pict-section-login.js"></script>
|
|
134
|
+
|
|
135
|
+
<script>
|
|
136
|
+
// ===== Mock fetch =====
|
|
137
|
+
// Intercept fetch calls to simulate authentication endpoints.
|
|
138
|
+
// Accepts demo/demo as valid credentials.
|
|
139
|
+
var _OriginalFetch = window.fetch;
|
|
140
|
+
window.fetch = function(pUrl, pOptions)
|
|
141
|
+
{
|
|
142
|
+
// POST /1.0/Authenticate
|
|
143
|
+
if (pUrl === '/1.0/Authenticate' && pOptions && pOptions.method === 'POST')
|
|
144
|
+
{
|
|
145
|
+
var body = JSON.parse(pOptions.body);
|
|
146
|
+
if (body.UserName === 'demo' && body.Password === 'demo')
|
|
147
|
+
{
|
|
148
|
+
return Promise.resolve(
|
|
149
|
+
{
|
|
150
|
+
ok: true,
|
|
151
|
+
json: function()
|
|
152
|
+
{
|
|
153
|
+
return Promise.resolve(
|
|
154
|
+
{
|
|
155
|
+
LoggedIn: true,
|
|
156
|
+
UserID: 42,
|
|
157
|
+
UserRecord:
|
|
158
|
+
{
|
|
159
|
+
IDUser: 42,
|
|
160
|
+
LoginID: 'demo',
|
|
161
|
+
FullName: 'Demo User',
|
|
162
|
+
Email: 'demo@example.com'
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
else
|
|
169
|
+
{
|
|
170
|
+
return Promise.resolve(
|
|
171
|
+
{
|
|
172
|
+
ok: true,
|
|
173
|
+
json: function()
|
|
174
|
+
{
|
|
175
|
+
return Promise.resolve(
|
|
176
|
+
{
|
|
177
|
+
LoggedIn: false,
|
|
178
|
+
Error: 'Invalid credentials.'
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// GET /1.0/Deauthenticate
|
|
186
|
+
if (pUrl === '/1.0/Deauthenticate')
|
|
187
|
+
{
|
|
188
|
+
return Promise.resolve(
|
|
189
|
+
{
|
|
190
|
+
ok: true,
|
|
191
|
+
json: function()
|
|
192
|
+
{
|
|
193
|
+
return Promise.resolve({ LoggedIn: false });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// GET /1.0/CheckSession
|
|
199
|
+
if (pUrl === '/1.0/CheckSession')
|
|
200
|
+
{
|
|
201
|
+
return Promise.resolve(
|
|
202
|
+
{
|
|
203
|
+
ok: true,
|
|
204
|
+
json: function()
|
|
205
|
+
{
|
|
206
|
+
return Promise.resolve({ LoggedIn: false });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Fall through to real fetch for anything else
|
|
212
|
+
return _OriginalFetch.apply(window, arguments);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// ===== Test Runner =====
|
|
216
|
+
(async function runTests()
|
|
217
|
+
{
|
|
218
|
+
var results = [];
|
|
219
|
+
var output = document.getElementById('output');
|
|
220
|
+
|
|
221
|
+
function addResult(pName, pPassed, pError)
|
|
222
|
+
{
|
|
223
|
+
results.push({ name: pName, passed: pPassed, error: pError || null });
|
|
224
|
+
output.textContent += '\\n' + (pPassed ? 'PASS' : 'FAIL') + ': ' + pName;
|
|
225
|
+
if (pError)
|
|
226
|
+
{
|
|
227
|
+
output.textContent += ' (' + pError + ')';
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Helper: wait for a condition to become true (polling)
|
|
232
|
+
function waitFor(pConditionFn, pTimeoutMs)
|
|
233
|
+
{
|
|
234
|
+
var timeout = pTimeoutMs || 5000;
|
|
235
|
+
var start = Date.now();
|
|
236
|
+
return new Promise(function(resolve, reject)
|
|
237
|
+
{
|
|
238
|
+
function check()
|
|
239
|
+
{
|
|
240
|
+
if (pConditionFn())
|
|
241
|
+
{
|
|
242
|
+
return resolve();
|
|
243
|
+
}
|
|
244
|
+
if (Date.now() - start > timeout)
|
|
245
|
+
{
|
|
246
|
+
return reject(new Error('waitFor timed out after ' + timeout + 'ms'));
|
|
247
|
+
}
|
|
248
|
+
setTimeout(check, 50);
|
|
249
|
+
}
|
|
250
|
+
check();
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
try
|
|
255
|
+
{
|
|
256
|
+
// ---- Test 1: Pict global is available ----
|
|
257
|
+
addResult(
|
|
258
|
+
'Pict global available',
|
|
259
|
+
typeof Pict !== 'undefined' && typeof Pict === 'function'
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// ---- Test 2: PictSectionLogin global is available ----
|
|
263
|
+
addResult(
|
|
264
|
+
'PictSectionLogin global available',
|
|
265
|
+
typeof PictSectionLogin !== 'undefined' && typeof PictSectionLogin === 'function'
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// ---- Create a Pict instance and register the login view ----
|
|
269
|
+
var tmpPict = new Pict(
|
|
270
|
+
{
|
|
271
|
+
Product: 'LoginTest',
|
|
272
|
+
ProductVersion: '1.0.0'
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Register and add the login view
|
|
276
|
+
tmpPict.addView('PictSectionLogin',
|
|
277
|
+
{
|
|
278
|
+
CheckSessionOnLoad: false,
|
|
279
|
+
RenderOnLoad: false
|
|
280
|
+
}, PictSectionLogin);
|
|
281
|
+
|
|
282
|
+
var tmpLoginView = tmpPict.views['PictSectionLogin'];
|
|
283
|
+
|
|
284
|
+
// Render the login view into the container
|
|
285
|
+
tmpLoginView.render();
|
|
286
|
+
|
|
287
|
+
// ---- Test 3: Login form renders ----
|
|
288
|
+
var tmpForm = document.querySelector('#pict-login-form');
|
|
289
|
+
addResult(
|
|
290
|
+
'Login form renders',
|
|
291
|
+
tmpForm !== null,
|
|
292
|
+
tmpForm === null ? 'could not find #pict-login-form' : null
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// ---- Test 4: Username and password inputs exist ----
|
|
296
|
+
var tmpUsernameInput = document.querySelector('#pict-login-username');
|
|
297
|
+
var tmpPasswordInput = document.querySelector('#pict-login-password');
|
|
298
|
+
addResult(
|
|
299
|
+
'Username and password inputs exist',
|
|
300
|
+
tmpUsernameInput !== null && tmpPasswordInput !== null,
|
|
301
|
+
tmpUsernameInput === null ? 'missing #pict-login-username'
|
|
302
|
+
: (tmpPasswordInput === null ? 'missing #pict-login-password' : null)
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// ---- Test 5: Error area hidden initially ----
|
|
306
|
+
var tmpErrorEl = document.querySelector('#pict-login-error');
|
|
307
|
+
addResult(
|
|
308
|
+
'Error area hidden initially',
|
|
309
|
+
tmpErrorEl !== null && tmpErrorEl.style.display === 'none',
|
|
310
|
+
tmpErrorEl === null ? 'missing #pict-login-error'
|
|
311
|
+
: ('display is: ' + tmpErrorEl.style.display)
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// ---- Test 6: Invalid login shows error ----
|
|
315
|
+
// Fill in bad credentials and submit the form
|
|
316
|
+
tmpUsernameInput.value = 'baduser';
|
|
317
|
+
tmpPasswordInput.value = 'badpass';
|
|
318
|
+
tmpForm.dispatchEvent(new Event('submit', { cancelable: true }));
|
|
319
|
+
|
|
320
|
+
// Wait for the error to appear (fetch mock resolves asynchronously)
|
|
321
|
+
await waitFor(function()
|
|
322
|
+
{
|
|
323
|
+
var el = document.querySelector('#pict-login-error');
|
|
324
|
+
return el && el.style.display === 'block' && el.textContent.length > 0;
|
|
325
|
+
}, 5000);
|
|
326
|
+
|
|
327
|
+
var tmpErrorAfterBadLogin = document.querySelector('#pict-login-error');
|
|
328
|
+
addResult(
|
|
329
|
+
'Invalid login shows error',
|
|
330
|
+
tmpErrorAfterBadLogin !== null
|
|
331
|
+
&& tmpErrorAfterBadLogin.style.display === 'block'
|
|
332
|
+
&& tmpErrorAfterBadLogin.textContent.indexOf('Invalid') >= 0,
|
|
333
|
+
tmpErrorAfterBadLogin ? 'error text: ' + tmpErrorAfterBadLogin.textContent : 'error element missing'
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
// ---- Test 7: Valid login shows status area ----
|
|
337
|
+
tmpUsernameInput = document.querySelector('#pict-login-username');
|
|
338
|
+
tmpPasswordInput = document.querySelector('#pict-login-password');
|
|
339
|
+
tmpUsernameInput.value = 'demo';
|
|
340
|
+
tmpPasswordInput.value = 'demo';
|
|
341
|
+
tmpForm = document.querySelector('#pict-login-form');
|
|
342
|
+
tmpForm.dispatchEvent(new Event('submit', { cancelable: true }));
|
|
343
|
+
|
|
344
|
+
// Wait for the status area to become visible
|
|
345
|
+
await waitFor(function()
|
|
346
|
+
{
|
|
347
|
+
var el = document.querySelector('#pict-login-status-area');
|
|
348
|
+
return el && el.style.display === 'block';
|
|
349
|
+
}, 5000);
|
|
350
|
+
|
|
351
|
+
var tmpStatusArea = document.querySelector('#pict-login-status-area');
|
|
352
|
+
var tmpDisplayName = document.querySelector('#pict-login-display-name');
|
|
353
|
+
addResult(
|
|
354
|
+
'Valid login shows status area with user info',
|
|
355
|
+
tmpStatusArea !== null
|
|
356
|
+
&& tmpStatusArea.style.display === 'block'
|
|
357
|
+
&& tmpDisplayName !== null
|
|
358
|
+
&& tmpDisplayName.textContent === 'Demo User',
|
|
359
|
+
tmpDisplayName ? 'display name: ' + tmpDisplayName.textContent : 'status area or display name missing'
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// ---- Test 8: Logout returns to form ----
|
|
363
|
+
var tmpLogoutBtn = document.querySelector('#pict-login-logout');
|
|
364
|
+
if (tmpLogoutBtn)
|
|
365
|
+
{
|
|
366
|
+
tmpLogoutBtn.click();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Wait for the form area to reappear
|
|
370
|
+
await waitFor(function()
|
|
371
|
+
{
|
|
372
|
+
var el = document.querySelector('#pict-login-form-area');
|
|
373
|
+
return el && el.style.display === 'block';
|
|
374
|
+
}, 5000);
|
|
375
|
+
|
|
376
|
+
var tmpFormArea = document.querySelector('#pict-login-form-area');
|
|
377
|
+
var tmpStatusAfterLogout = document.querySelector('#pict-login-status-area');
|
|
378
|
+
addResult(
|
|
379
|
+
'Logout returns to form',
|
|
380
|
+
tmpFormArea !== null
|
|
381
|
+
&& tmpFormArea.style.display === 'block'
|
|
382
|
+
&& tmpStatusAfterLogout !== null
|
|
383
|
+
&& tmpStatusAfterLogout.style.display === 'none',
|
|
384
|
+
tmpFormArea ? 'form display: ' + tmpFormArea.style.display + ', status display: '
|
|
385
|
+
+ (tmpStatusAfterLogout ? tmpStatusAfterLogout.style.display : 'missing') : 'form area missing'
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
catch (pError)
|
|
389
|
+
{
|
|
390
|
+
addResult('unexpected error', false, pError.message || String(pError));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Store final results for puppeteer to read
|
|
394
|
+
window.__TEST_RESULTS__ = results;
|
|
395
|
+
window.__TESTS_COMPLETE__ = true;
|
|
396
|
+
|
|
397
|
+
output.textContent += '\\n\\nDone: '
|
|
398
|
+
+ results.filter(function(r) { return r.passed; }).length + '/'
|
|
399
|
+
+ results.length + ' passed';
|
|
400
|
+
})();
|
|
401
|
+
</script>
|
|
402
|
+
</body>
|
|
403
|
+
</html>`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
suite
|
|
407
|
+
(
|
|
408
|
+
'Browser-Integration',
|
|
409
|
+
function()
|
|
410
|
+
{
|
|
411
|
+
// Browser tests need extra time for puppeteer startup
|
|
412
|
+
this.timeout(60000);
|
|
413
|
+
|
|
414
|
+
let _Server;
|
|
415
|
+
let _Port;
|
|
416
|
+
let _Browser;
|
|
417
|
+
let _Puppeteer;
|
|
418
|
+
|
|
419
|
+
suiteSetup
|
|
420
|
+
(
|
|
421
|
+
function(fDone)
|
|
422
|
+
{
|
|
423
|
+
// Verify dist/ exists
|
|
424
|
+
if (!libFS.existsSync(libPath.join(_DistDir, 'pict-section-login.js')))
|
|
425
|
+
{
|
|
426
|
+
return fDone(new Error(
|
|
427
|
+
'dist/pict-section-login.js not found. Run "npm run build" first.'
|
|
428
|
+
));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Verify pict dist exists
|
|
432
|
+
if (!libFS.existsSync(libPath.join(_PictDistDir, 'pict.js')))
|
|
433
|
+
{
|
|
434
|
+
return fDone(new Error(
|
|
435
|
+
'node_modules/pict/dist/pict.js not found. Run "npm install" first.'
|
|
436
|
+
));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Start the test server
|
|
440
|
+
startTestServer(
|
|
441
|
+
function(pError, pServer, pPort)
|
|
442
|
+
{
|
|
443
|
+
if (pError)
|
|
444
|
+
{
|
|
445
|
+
return fDone(pError);
|
|
446
|
+
}
|
|
447
|
+
_Server = pServer;
|
|
448
|
+
_Port = pPort;
|
|
449
|
+
|
|
450
|
+
// Load puppeteer
|
|
451
|
+
try
|
|
452
|
+
{
|
|
453
|
+
_Puppeteer = require('puppeteer');
|
|
454
|
+
}
|
|
455
|
+
catch (pRequireError)
|
|
456
|
+
{
|
|
457
|
+
_Server.close();
|
|
458
|
+
return fDone(new Error(
|
|
459
|
+
'puppeteer is not installed. Run "npm install" to install it as a devDependency.'
|
|
460
|
+
));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return fDone();
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
suiteTeardown
|
|
469
|
+
(
|
|
470
|
+
function(fDone)
|
|
471
|
+
{
|
|
472
|
+
let tmpCleanupSteps = [];
|
|
473
|
+
|
|
474
|
+
if (_Browser)
|
|
475
|
+
{
|
|
476
|
+
tmpCleanupSteps.push(_Browser.close().catch(() => {}));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
Promise.all(tmpCleanupSteps).then(
|
|
480
|
+
function()
|
|
481
|
+
{
|
|
482
|
+
if (_Server)
|
|
483
|
+
{
|
|
484
|
+
_Server.close(fDone);
|
|
485
|
+
}
|
|
486
|
+
else
|
|
487
|
+
{
|
|
488
|
+
fDone();
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
test
|
|
495
|
+
(
|
|
496
|
+
'All browser tests pass in headless Chrome',
|
|
497
|
+
function(fDone)
|
|
498
|
+
{
|
|
499
|
+
_Puppeteer.launch(
|
|
500
|
+
{
|
|
501
|
+
headless: true,
|
|
502
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
503
|
+
})
|
|
504
|
+
.then(
|
|
505
|
+
function(pBrowser)
|
|
506
|
+
{
|
|
507
|
+
_Browser = pBrowser;
|
|
508
|
+
return _Browser.newPage();
|
|
509
|
+
})
|
|
510
|
+
.then(
|
|
511
|
+
function(pPage)
|
|
512
|
+
{
|
|
513
|
+
// Capture console output for debugging
|
|
514
|
+
pPage.on('console',
|
|
515
|
+
function(pMessage)
|
|
516
|
+
{
|
|
517
|
+
if (pMessage.type() === 'error')
|
|
518
|
+
{
|
|
519
|
+
console.log(' [browser error]', pMessage.text());
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
pPage.on('pageerror',
|
|
524
|
+
function(pError)
|
|
525
|
+
{
|
|
526
|
+
console.log(' [browser page error]', pError.message);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
return pPage.goto(`http://127.0.0.1:${_Port}/`, { waitUntil: 'networkidle0', timeout: 30000 })
|
|
530
|
+
.then(function() { return pPage; });
|
|
531
|
+
})
|
|
532
|
+
.then(
|
|
533
|
+
function(pPage)
|
|
534
|
+
{
|
|
535
|
+
// Wait for tests to complete
|
|
536
|
+
return pPage.waitForFunction(
|
|
537
|
+
'window.__TESTS_COMPLETE__ === true',
|
|
538
|
+
{ timeout: 30000 }
|
|
539
|
+
).then(function() { return pPage; });
|
|
540
|
+
})
|
|
541
|
+
.then(
|
|
542
|
+
function(pPage)
|
|
543
|
+
{
|
|
544
|
+
// Read results
|
|
545
|
+
return pPage.evaluate(function()
|
|
546
|
+
{
|
|
547
|
+
return window.__TEST_RESULTS__;
|
|
548
|
+
});
|
|
549
|
+
})
|
|
550
|
+
.then(
|
|
551
|
+
function(pResults)
|
|
552
|
+
{
|
|
553
|
+
// Assert each test passed
|
|
554
|
+
Expect(pResults).to.be.an('array');
|
|
555
|
+
Expect(pResults.length).to.be.above(0);
|
|
556
|
+
|
|
557
|
+
let tmpFailures = [];
|
|
558
|
+
|
|
559
|
+
for (let i = 0; i < pResults.length; i++)
|
|
560
|
+
{
|
|
561
|
+
let tmpResult = pResults[i];
|
|
562
|
+
if (!tmpResult.passed)
|
|
563
|
+
{
|
|
564
|
+
tmpFailures.push(
|
|
565
|
+
tmpResult.name + (tmpResult.error ? ': ' + tmpResult.error : '')
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (tmpFailures.length > 0)
|
|
571
|
+
{
|
|
572
|
+
Expect.fail(
|
|
573
|
+
'Browser tests failed:\n - ' + tmpFailures.join('\n - ')
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
console.log(` ${pResults.length} browser sub-tests passed`);
|
|
578
|
+
fDone();
|
|
579
|
+
})
|
|
580
|
+
.catch(
|
|
581
|
+
function(pError)
|
|
582
|
+
{
|
|
583
|
+
fDone(pError);
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
);
|