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.
Files changed (25) hide show
  1. package/example_applications/custom_login/Custom-Login-Application.js +75 -0
  2. package/example_applications/custom_login/html/index.html +110 -0
  3. package/example_applications/custom_login/package.json +27 -0
  4. package/example_applications/harness_app/Harness-App-Application.js +167 -0
  5. package/example_applications/harness_app/Harness-App-Configuration.json +4 -0
  6. package/example_applications/harness_app/html/index.html +90 -0
  7. package/example_applications/harness_app/package.json +28 -0
  8. package/example_applications/harness_app/providers/PictRouter-HarnessApp.json +24 -0
  9. package/example_applications/harness_app/views/PictView-HarnessApp-Books.js +172 -0
  10. package/example_applications/harness_app/views/PictView-HarnessApp-Dashboard.js +158 -0
  11. package/example_applications/harness_app/views/PictView-HarnessApp-Layout.js +86 -0
  12. package/example_applications/harness_app/views/PictView-HarnessApp-Login.js +58 -0
  13. package/example_applications/harness_app/views/PictView-HarnessApp-TopBar.js +157 -0
  14. package/example_applications/harness_app/views/PictView-HarnessApp-Users.js +188 -0
  15. package/example_applications/oauth_login/OAuth-Login-Application.js +78 -0
  16. package/example_applications/oauth_login/html/index.html +57 -0
  17. package/example_applications/oauth_login/package.json +27 -0
  18. package/example_applications/orator_login/Orator-Login-Application.js +61 -0
  19. package/example_applications/orator_login/html/index.html +51 -0
  20. package/example_applications/orator_login/package.json +27 -0
  21. package/package.json +53 -0
  22. package/source/Pict-Section-Login-DefaultConfiguration.js +265 -0
  23. package/source/Pict-Section-Login.js +533 -0
  24. package/test/Browser_Integration_tests.js +588 -0
  25. 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
+ );