pict-section-inlinedocumentation 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 (51) hide show
  1. package/README.md +107 -0
  2. package/docs/.nojekyll +0 -0
  3. package/docs/README.md +83 -0
  4. package/docs/_cover.md +15 -0
  5. package/docs/_sidebar.md +24 -0
  6. package/docs/_topbar.md +8 -0
  7. package/docs/_version.json +7 -0
  8. package/docs/api-reference.md +185 -0
  9. package/docs/architecture.md +103 -0
  10. package/docs/css/docuserve.css +327 -0
  11. package/docs/embedding-level1-sidebar.md +92 -0
  12. package/docs/embedding-level2-routes.md +86 -0
  13. package/docs/embedding-level3-tooltips.md +97 -0
  14. package/docs/embedding-level4-autogen.md +126 -0
  15. package/docs/index.html +39 -0
  16. package/docs/overview.md +42 -0
  17. package/docs/quickstart.md +95 -0
  18. package/docs/reference.md +73 -0
  19. package/docs/retold-catalog.json +181 -0
  20. package/docs/retold-keyword-index.json +4374 -0
  21. package/example_applications/basic/docs/README.md +40 -0
  22. package/example_applications/basic/docs/_sidebar.md +4 -0
  23. package/example_applications/basic/docs/_topics.json +10 -0
  24. package/example_applications/basic/docs/advanced-topics.md +47 -0
  25. package/example_applications/basic/docs/getting-started.md +70 -0
  26. package/example_applications/basic/index.html +100 -0
  27. package/example_applications/bookshop/.quackage.json +10 -0
  28. package/example_applications/bookshop/Pict-Application-Bookshop-Configuration.json +15 -0
  29. package/example_applications/bookshop/Pict-Application-Bookshop.js +218 -0
  30. package/example_applications/bookshop/data/BookshopData.json +65 -0
  31. package/example_applications/bookshop/data/pict_documentation_topics.json +46 -0
  32. package/example_applications/bookshop/docs/_sidebar.md +6 -0
  33. package/example_applications/bookshop/docs/book-detail.md +21 -0
  34. package/example_applications/bookshop/docs/book-list.md +21 -0
  35. package/example_applications/bookshop/docs/search-filter.md +18 -0
  36. package/example_applications/bookshop/docs/store.md +29 -0
  37. package/example_applications/bookshop/docs/welcome.md +23 -0
  38. package/example_applications/bookshop/html/index.html +236 -0
  39. package/example_applications/bookshop/package.json +34 -0
  40. package/example_applications/bookshop/views/PictView-Bookshop-BookList.js +324 -0
  41. package/example_applications/bookshop/views/PictView-Bookshop-HelpToggle.js +44 -0
  42. package/example_applications/bookshop/views/PictView-Bookshop-Store.js +271 -0
  43. package/package.json +55 -0
  44. package/source/Pict-Section-InlineDocumentation.js +10 -0
  45. package/source/providers/Pict-Provider-InlineDocumentation.js +1995 -0
  46. package/source/views/Pict-View-InlineDocumentation-Content.js +542 -0
  47. package/source/views/Pict-View-InlineDocumentation-Layout.js +206 -0
  48. package/source/views/Pict-View-InlineDocumentation-Nav.js +475 -0
  49. package/source/views/Pict-View-InlineDocumentation-TopicManager.js +1623 -0
  50. package/test/Browser_Integration_tests.js +1449 -0
  51. package/test/Pict-Section-InlineDocumentation_test.js +1285 -0
@@ -0,0 +1,1449 @@
1
+ /**
2
+ * Headless browser integration tests for pict-section-inlinedocumentation.
3
+ *
4
+ * Verifies the inline documentation bookshop example works in a real browser:
5
+ * 1) The bookshop loads and renders the book catalog
6
+ * 2) The help panel opens via button and F1 keyboard shortcut
7
+ * 3) Browsing different screens loads the correct contextual help
8
+ * 4) Editing existing documentation (edit, modify, save, cancel)
9
+ * 5) Creating a new documentation topic at runtime
10
+ * 6) Binding a documentation entry to a route
11
+ * 7) Multiple topics matching a route — navigation between them
12
+ *
13
+ * Screenshots and logs are saved to dist/test-artifacts/ after each key stage.
14
+ *
15
+ * Serves the pre-built bookshop example dist/ folder from a local HTTP server.
16
+ *
17
+ * Requires:
18
+ * - cd example_applications/bookshop && npx quack build && npx quack copy
19
+ * - npm install (puppeteer must be available)
20
+ *
21
+ * @license MIT
22
+ * @author Steven Velozo <steven@velozo.com>
23
+ */
24
+
25
+ const Chai = require('chai');
26
+ const Expect = Chai.expect;
27
+
28
+ const libHTTP = require('http');
29
+ const libFS = require('fs');
30
+ const libPath = require('path');
31
+
32
+ const _PackageRoot = libPath.resolve(__dirname, '..');
33
+ const _BookshopDistDir = libPath.join(_PackageRoot, 'example_applications', 'bookshop', 'dist');
34
+ const _ArtifactsDir = libPath.join(_PackageRoot, 'dist', 'test-artifacts');
35
+
36
+ /**
37
+ * Ensure the test artifacts directory exists.
38
+ */
39
+ function ensureArtifactsDir()
40
+ {
41
+ if (!libFS.existsSync(libPath.join(_PackageRoot, 'dist')))
42
+ {
43
+ libFS.mkdirSync(libPath.join(_PackageRoot, 'dist'));
44
+ }
45
+ if (!libFS.existsSync(_ArtifactsDir))
46
+ {
47
+ libFS.mkdirSync(_ArtifactsDir);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Create a simple HTTP server that serves the bookshop dist/ folder.
53
+ *
54
+ * @param {function} fCallback - Callback with (pError, pServer, pPort)
55
+ */
56
+ function startTestServer(fCallback)
57
+ {
58
+ let tmpMimeTypes =
59
+ {
60
+ '.html': 'text/html',
61
+ '.js': 'application/javascript',
62
+ '.json': 'application/json',
63
+ '.md': 'text/markdown',
64
+ '.map': 'application/json',
65
+ '.css': 'text/css'
66
+ };
67
+
68
+ let tmpServer = libHTTP.createServer(
69
+ (pRequest, pResponse) =>
70
+ {
71
+ let tmpUrl = pRequest.url;
72
+
73
+ // Strip query strings
74
+ let tmpQueryIndex = tmpUrl.indexOf('?');
75
+ if (tmpQueryIndex >= 0)
76
+ {
77
+ tmpUrl = tmpUrl.substring(0, tmpQueryIndex);
78
+ }
79
+
80
+ // Default to index.html
81
+ if (tmpUrl === '/')
82
+ {
83
+ tmpUrl = '/index.html';
84
+ }
85
+
86
+ // Serve from dist/
87
+ let tmpFilePath = libPath.join(_BookshopDistDir, tmpUrl);
88
+
89
+ if (!libFS.existsSync(tmpFilePath))
90
+ {
91
+ pResponse.writeHead(404);
92
+ pResponse.end('Not Found: ' + tmpUrl);
93
+ return;
94
+ }
95
+
96
+ let tmpStat = libFS.statSync(tmpFilePath);
97
+ if (tmpStat.isDirectory())
98
+ {
99
+ tmpFilePath = libPath.join(tmpFilePath, 'index.html');
100
+ if (!libFS.existsSync(tmpFilePath))
101
+ {
102
+ pResponse.writeHead(404);
103
+ pResponse.end('Not Found: ' + tmpUrl);
104
+ return;
105
+ }
106
+ }
107
+
108
+ let tmpExt = libPath.extname(tmpFilePath);
109
+ let tmpContentType = tmpMimeTypes[tmpExt] || 'application/octet-stream';
110
+
111
+ let tmpContent = libFS.readFileSync(tmpFilePath);
112
+ pResponse.writeHead(200, { 'Content-Type': tmpContentType });
113
+ pResponse.end(tmpContent);
114
+ });
115
+
116
+ tmpServer.listen(0, '127.0.0.1',
117
+ () =>
118
+ {
119
+ let tmpPort = tmpServer.address().port;
120
+ return fCallback(null, tmpServer, tmpPort);
121
+ });
122
+ }
123
+
124
+
125
+ suite
126
+ (
127
+ 'Browser-Integration',
128
+ function()
129
+ {
130
+ // Browser tests need extra time for puppeteer startup and page operations
131
+ this.timeout(60000);
132
+
133
+ let _Server;
134
+ let _Port;
135
+ let _Browser;
136
+ let _Page;
137
+ let _Puppeteer;
138
+
139
+ // Collected console log entries across the entire run
140
+ let _ConsoleLog = [];
141
+ // Track screenshots taken
142
+ let _ScreenshotsTaken = [];
143
+ // Test results for the summary log
144
+ let _TestResults = [];
145
+ // Run start time
146
+ let _RunStartTime;
147
+
148
+ /**
149
+ * Take a screenshot and save it to the artifacts dir.
150
+ *
151
+ * @param {string} pName - Descriptive name (used in filename)
152
+ * @returns {Promise}
153
+ */
154
+ function captureScreenshot(pName)
155
+ {
156
+ let tmpFilename = pName.replace(/[^a-zA-Z0-9_-]/g, '_') + '.png';
157
+ let tmpPath = libPath.join(_ArtifactsDir, tmpFilename);
158
+
159
+ return _Page.screenshot({ path: tmpPath, fullPage: true })
160
+ .then(function()
161
+ {
162
+ _ScreenshotsTaken.push({ name: pName, file: tmpFilename, timestamp: new Date().toISOString() });
163
+ })
164
+ .catch(function(pError)
165
+ {
166
+ // Screenshot failures should not break the test — log and move on
167
+ _ScreenshotsTaken.push({ name: pName, file: tmpFilename, timestamp: new Date().toISOString(), error: pError.message });
168
+ console.log(' [screenshot warning] ' + pName + ': ' + pError.message);
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Capture a state log entry — collects page state and appdata at this moment.
174
+ *
175
+ * @param {string} pStageName - Descriptive stage name
176
+ * @returns {Promise<Object>} The captured state
177
+ */
178
+ function captureStateLog(pStageName)
179
+ {
180
+ return _Page.evaluate(function()
181
+ {
182
+ var state = window._Pict && window._Pict.AppData && window._Pict.AppData.InlineDocumentation
183
+ ? window._Pict.AppData.InlineDocumentation : {};
184
+ var appState = window._Pict && window._Pict.AppData && window._Pict.AppData.Bookshop
185
+ ? window._Pict.AppData.Bookshop : {};
186
+ var provider = window._Pict && window._Pict.providers
187
+ ? window._Pict.providers['Pict-InlineDocumentation'] : null;
188
+
189
+ return {
190
+ CurrentPath: state.CurrentPath || '',
191
+ CurrentRoute: state.CurrentRoute || '',
192
+ Topic: state.Topic || null,
193
+ EditEnabled: !!state.EditEnabled,
194
+ Editing: !!state.Editing,
195
+ EditingPath: state.EditingPath || '',
196
+ CurrentView: appState.CurrentView || '',
197
+ CurrentBook: appState.CurrentBook ? appState.CurrentBook.Title : null,
198
+ HelpVisible: !!appState.HelpVisible,
199
+ TopicCount: state.Topics ? Object.keys(state.Topics).length : 0,
200
+ CacheCount: provider ? Object.keys(provider._ContentCache).length : 0,
201
+ PanelVisible: document.getElementById('Bookshop-Help-Panel')
202
+ ? document.getElementById('Bookshop-Help-Panel').classList.contains('visible')
203
+ : false
204
+ };
205
+ })
206
+ .then(function(pState)
207
+ {
208
+ pState._stage = pStageName;
209
+ pState._timestamp = new Date().toISOString();
210
+ _ConsoleLog.push(pState);
211
+ return pState;
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Write all collected logs and the summary to disk.
217
+ */
218
+ function writeSummaryLog()
219
+ {
220
+ let tmpSummary =
221
+ {
222
+ runDate: _RunStartTime,
223
+ runDurationMs: Date.now() - new Date(_RunStartTime).getTime(),
224
+ testsRun: _TestResults.length,
225
+ testsPassed: _TestResults.filter(function(r) { return r.passed; }).length,
226
+ testsFailed: _TestResults.filter(function(r) { return !r.passed; }).length,
227
+ tests: _TestResults,
228
+ screenshots: _ScreenshotsTaken,
229
+ stateLog: _ConsoleLog
230
+ };
231
+
232
+ let tmpLogPath = libPath.join(_ArtifactsDir, 'test-run-summary.json');
233
+ libFS.writeFileSync(tmpLogPath, JSON.stringify(tmpSummary, null, '\t'));
234
+
235
+ // Also write a human-readable log
236
+ let tmpLines = [];
237
+ tmpLines.push('=== Browser Integration Test Run ===');
238
+ tmpLines.push('Date: ' + tmpSummary.runDate);
239
+ tmpLines.push('Duration: ' + tmpSummary.runDurationMs + 'ms');
240
+ tmpLines.push('Tests: ' + tmpSummary.testsPassed + '/' + tmpSummary.testsRun + ' passed');
241
+ tmpLines.push('');
242
+ tmpLines.push('--- Test Results ---');
243
+ for (let i = 0; i < _TestResults.length; i++)
244
+ {
245
+ let tmpR = _TestResults[i];
246
+ tmpLines.push((tmpR.passed ? 'PASS' : 'FAIL') + ': ' + tmpR.name + (tmpR.error ? ' (' + tmpR.error + ')' : ''));
247
+ }
248
+ tmpLines.push('');
249
+ tmpLines.push('--- Screenshots ---');
250
+ for (let i = 0; i < _ScreenshotsTaken.length; i++)
251
+ {
252
+ let tmpS = _ScreenshotsTaken[i];
253
+ tmpLines.push('[' + tmpS.timestamp + '] ' + tmpS.name + ' -> ' + tmpS.file);
254
+ }
255
+ tmpLines.push('');
256
+ tmpLines.push('--- State Log ---');
257
+ for (let i = 0; i < _ConsoleLog.length; i++)
258
+ {
259
+ let tmpEntry = _ConsoleLog[i];
260
+ tmpLines.push('[' + tmpEntry._timestamp + '] ' + tmpEntry._stage);
261
+ tmpLines.push(' View=' + tmpEntry.CurrentView + ' Path=' + tmpEntry.CurrentPath + ' Route=' + tmpEntry.CurrentRoute);
262
+ tmpLines.push(' Topic=' + tmpEntry.Topic + ' Editing=' + tmpEntry.Editing + ' PanelVisible=' + tmpEntry.PanelVisible);
263
+ }
264
+ tmpLines.push('');
265
+
266
+ let tmpTextPath = libPath.join(_ArtifactsDir, 'test-run-summary.log');
267
+ libFS.writeFileSync(tmpTextPath, tmpLines.join('\n'));
268
+ }
269
+
270
+ /**
271
+ * Record a test result.
272
+ *
273
+ * @param {string} pName - Test name
274
+ * @param {boolean} pPassed - Whether it passed
275
+ * @param {string} [pError] - Error message if failed
276
+ */
277
+ function recordTestResult(pName, pPassed, pError)
278
+ {
279
+ _TestResults.push({ name: pName, passed: pPassed, error: pError || null, timestamp: new Date().toISOString() });
280
+ }
281
+
282
+
283
+ suiteSetup
284
+ (
285
+ function(fDone)
286
+ {
287
+ _RunStartTime = new Date().toISOString();
288
+
289
+ // Ensure artifacts dir
290
+ ensureArtifactsDir();
291
+
292
+ // Verify dist/ exists
293
+ if (!libFS.existsSync(libPath.join(_BookshopDistDir, 'index.html')))
294
+ {
295
+ return fDone(new Error(
296
+ 'Bookshop dist/index.html not found. Run "cd example_applications/bookshop && npx quack build && npx quack copy" first.'
297
+ ));
298
+ }
299
+
300
+ if (!libFS.existsSync(libPath.join(_BookshopDistDir, 'bookshop_example.js')))
301
+ {
302
+ return fDone(new Error(
303
+ 'Bookshop dist/bookshop_example.js not found. Run "cd example_applications/bookshop && npx quack build" first.'
304
+ ));
305
+ }
306
+
307
+ // Start the test server
308
+ startTestServer(
309
+ function(pError, pServer, pPort)
310
+ {
311
+ if (pError)
312
+ {
313
+ return fDone(pError);
314
+ }
315
+ _Server = pServer;
316
+ _Port = pPort;
317
+
318
+ // Load puppeteer
319
+ try
320
+ {
321
+ _Puppeteer = require('puppeteer');
322
+ }
323
+ catch (pRequireError)
324
+ {
325
+ _Server.close();
326
+ return fDone(new Error(
327
+ 'puppeteer is not installed. Run "npm install" to install it as a devDependency.'
328
+ ));
329
+ }
330
+
331
+ // Launch browser
332
+ _Puppeteer.launch(
333
+ {
334
+ headless: true,
335
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
336
+ protocolTimeout: 60000
337
+ })
338
+ .then(
339
+ function(pBrowser)
340
+ {
341
+ _Browser = pBrowser;
342
+ return _Browser.newPage();
343
+ })
344
+ .then(
345
+ function(pPage)
346
+ {
347
+ _Page = pPage;
348
+
349
+ // Set a reasonable viewport
350
+ return _Page.setViewport({ width: 1400, height: 900 });
351
+ })
352
+ .then(
353
+ function()
354
+ {
355
+ // Capture all console output
356
+ _Page.on('console',
357
+ function(pMessage)
358
+ {
359
+ if (pMessage.type() === 'error')
360
+ {
361
+ console.log(' [browser error]', pMessage.text());
362
+ }
363
+ });
364
+
365
+ _Page.on('pageerror',
366
+ function(pError)
367
+ {
368
+ console.log(' [browser page error]', pError.message);
369
+ });
370
+
371
+ return fDone();
372
+ })
373
+ .catch(
374
+ function(pError)
375
+ {
376
+ _Server.close();
377
+ return fDone(pError);
378
+ });
379
+ });
380
+ }
381
+ );
382
+
383
+ suiteTeardown
384
+ (
385
+ function(fDone)
386
+ {
387
+ // Write all logs and summary
388
+ try
389
+ {
390
+ writeSummaryLog();
391
+ console.log(' Artifacts saved to: dist/test-artifacts/');
392
+ console.log(' Screenshots: ' + _ScreenshotsTaken.length);
393
+ }
394
+ catch (pWriteError)
395
+ {
396
+ console.log(' Warning: could not write test artifacts:', pWriteError.message);
397
+ }
398
+
399
+ let tmpCleanupSteps = [];
400
+
401
+ if (_Browser)
402
+ {
403
+ tmpCleanupSteps.push(_Browser.close().catch(function() {}));
404
+ }
405
+
406
+ Promise.all(tmpCleanupSteps).then(
407
+ function()
408
+ {
409
+ if (_Server)
410
+ {
411
+ _Server.close(fDone);
412
+ }
413
+ else
414
+ {
415
+ fDone();
416
+ }
417
+ });
418
+ }
419
+ );
420
+
421
+
422
+ // ====================================================================
423
+ // Test 1: Application loads and renders the book catalog
424
+ // ====================================================================
425
+ test
426
+ (
427
+ 'Bookshop loads and renders the book catalog',
428
+ function(fDone)
429
+ {
430
+ _Page.goto(`http://127.0.0.1:${_Port}/`, { waitUntil: 'networkidle0', timeout: 15000 })
431
+ .then(function()
432
+ {
433
+ return _Page.waitForSelector('.bookshop-book-card', { timeout: 10000 });
434
+ })
435
+ .then(function()
436
+ {
437
+ return captureScreenshot('01-book-catalog-loaded');
438
+ })
439
+ .then(function()
440
+ {
441
+ return captureStateLog('01-book-catalog-loaded');
442
+ })
443
+ .then(function()
444
+ {
445
+ return _Page.evaluate(function()
446
+ {
447
+ var cards = document.querySelectorAll('.bookshop-book-card');
448
+ var title = document.querySelector('.bookshop-section-title');
449
+ return {
450
+ cardCount: cards.length,
451
+ titleText: title ? title.textContent : null,
452
+ hasPict: typeof window._Pict !== 'undefined',
453
+ hasProvider: !!(window._Pict && window._Pict.providers && window._Pict.providers['Pict-InlineDocumentation'])
454
+ };
455
+ });
456
+ })
457
+ .then(function(pResult)
458
+ {
459
+ Expect(pResult.cardCount).to.be.above(0, 'Should have at least one book card');
460
+ Expect(pResult.titleText).to.equal('Book Catalog');
461
+ Expect(pResult.hasPict).to.be.true;
462
+ Expect(pResult.hasProvider).to.be.true;
463
+ recordTestResult('Bookshop loads and renders the book catalog', true);
464
+ fDone();
465
+ })
466
+ .catch(function(pError) { recordTestResult('Bookshop loads and renders the book catalog', false, pError.message); fDone(pError); });
467
+ }
468
+ );
469
+
470
+
471
+ // ====================================================================
472
+ // Test 2: Help panel opens via header button and shows content
473
+ // ====================================================================
474
+ test
475
+ (
476
+ 'Help panel opens via header button and shows route-matched content',
477
+ function(fDone)
478
+ {
479
+ _Page.evaluate(function()
480
+ {
481
+ var panel = document.getElementById('Bookshop-Help-Panel');
482
+ return panel ? panel.classList.contains('visible') : false;
483
+ })
484
+ .then(function(pWasVisible)
485
+ {
486
+ if (pWasVisible)
487
+ {
488
+ return _Page.evaluate(function() { document.getElementById('Bookshop-Help-CloseBtn').click(); });
489
+ }
490
+ })
491
+ .then(function()
492
+ {
493
+ return _Page.click('#Bookshop-Header-HelpBtn');
494
+ })
495
+ .then(function()
496
+ {
497
+ return _Page.waitForFunction(
498
+ 'document.getElementById("Bookshop-Help-Panel") && document.getElementById("Bookshop-Help-Panel").classList.contains("visible")',
499
+ { timeout: 5000 }
500
+ );
501
+ })
502
+ .then(function()
503
+ {
504
+ return captureScreenshot('02-help-panel-open-book-list');
505
+ })
506
+ .then(function()
507
+ {
508
+ return captureStateLog('02-help-panel-open-book-list');
509
+ })
510
+ .then(function()
511
+ {
512
+ return _Page.evaluate(function()
513
+ {
514
+ var state = window._Pict.AppData.InlineDocumentation;
515
+ var body = document.getElementById('InlineDoc-Content-Body');
516
+ return {
517
+ panelVisible: document.getElementById('Bookshop-Help-Panel').classList.contains('visible'),
518
+ currentPath: state.CurrentPath,
519
+ currentRoute: state.CurrentRoute,
520
+ topic: state.Topic,
521
+ bodyHasContent: body ? body.innerHTML.length > 50 : false
522
+ };
523
+ });
524
+ })
525
+ .then(function(pResult)
526
+ {
527
+ Expect(pResult.panelVisible).to.be.true;
528
+ Expect(pResult.currentPath).to.equal('book-list.md', 'Should load book-list.md for /books route');
529
+ Expect(pResult.topic).to.equal('BOOKSHOP-BOOKLIST');
530
+ Expect(pResult.bodyHasContent).to.be.true;
531
+ recordTestResult('Help panel opens via header button and shows route-matched content', true);
532
+ fDone();
533
+ })
534
+ .catch(function(pError) { recordTestResult('Help panel opens via header button and shows route-matched content', false, pError.message); fDone(pError); });
535
+ }
536
+ );
537
+
538
+
539
+ // ====================================================================
540
+ // Test 3: Navigate to store page and help updates via route
541
+ // ====================================================================
542
+ test
543
+ (
544
+ 'Navigate to store page and help updates to store topic via route matching',
545
+ function(fDone)
546
+ {
547
+ _Page.evaluate(function()
548
+ {
549
+ var panel = document.getElementById('Bookshop-Help-Panel');
550
+ if (panel && panel.classList.contains('visible'))
551
+ {
552
+ window._Pict.PictApplication.toggleHelp();
553
+ }
554
+ })
555
+ .then(function()
556
+ {
557
+ return _Page.evaluate(function() { document.querySelector('.bookshop-book-card').click(); });
558
+ })
559
+ .then(function()
560
+ {
561
+ return _Page.waitForSelector('.bookshop-store-detail', { timeout: 5000 });
562
+ })
563
+ .then(function()
564
+ {
565
+ return captureScreenshot('03-store-page-navigated');
566
+ })
567
+ .then(function()
568
+ {
569
+ return captureStateLog('03-store-page-navigated');
570
+ })
571
+ .then(function()
572
+ {
573
+ return _Page.evaluate(function()
574
+ {
575
+ var state = window._Pict.AppData.InlineDocumentation;
576
+ var appState = window._Pict.AppData.Bookshop;
577
+ return {
578
+ currentView: appState.CurrentView,
579
+ currentBook: appState.CurrentBook ? appState.CurrentBook.Title : null,
580
+ currentRoute: state.CurrentRoute,
581
+ topic: state.Topic,
582
+ currentPath: state.CurrentPath
583
+ };
584
+ });
585
+ })
586
+ .then(function(pResult)
587
+ {
588
+ Expect(pResult.currentView).to.equal('Store');
589
+ Expect(pResult.currentBook).to.be.a('string');
590
+ Expect(pResult.currentRoute).to.match(/^\/books\/store\/\d+$/);
591
+ Expect(pResult.topic).to.equal('BOOKSHOP-STORE');
592
+ Expect(pResult.currentPath).to.equal('store.md');
593
+ recordTestResult('Navigate to store page and help updates to store topic via route matching', true);
594
+ fDone();
595
+ })
596
+ .catch(function(pError) { recordTestResult('Navigate to store page and help updates to store topic via route matching', false, pError.message); fDone(pError); });
597
+ }
598
+ );
599
+
600
+
601
+ // ====================================================================
602
+ // Test 4: Open help on store page via ? button
603
+ // ====================================================================
604
+ test
605
+ (
606
+ 'Help button on store page opens contextual help for the store',
607
+ function(fDone)
608
+ {
609
+ _Page.evaluate(function() { document.getElementById('Bookshop-Help-Store').click(); })
610
+ .then(function()
611
+ {
612
+ return _Page.waitForFunction(
613
+ 'document.getElementById("Bookshop-Help-Panel") && document.getElementById("Bookshop-Help-Panel").classList.contains("visible")',
614
+ { timeout: 5000 }
615
+ );
616
+ })
617
+ .then(function()
618
+ {
619
+ return captureScreenshot('04-store-help-via-question-mark');
620
+ })
621
+ .then(function()
622
+ {
623
+ return captureStateLog('04-store-help-via-question-mark');
624
+ })
625
+ .then(function()
626
+ {
627
+ return _Page.evaluate(function()
628
+ {
629
+ var state = window._Pict.AppData.InlineDocumentation;
630
+ var body = document.getElementById('InlineDoc-Content-Body');
631
+ return {
632
+ panelVisible: document.getElementById('Bookshop-Help-Panel').classList.contains('visible'),
633
+ topic: state.Topic,
634
+ bodyHtml: body ? body.innerHTML.substring(0, 200) : ''
635
+ };
636
+ });
637
+ })
638
+ .then(function(pResult)
639
+ {
640
+ Expect(pResult.panelVisible).to.be.true;
641
+ Expect(pResult.topic).to.equal('BOOKSHOP-STORE');
642
+ Expect(pResult.bodyHtml.length).to.be.above(0);
643
+ recordTestResult('Help button on store page opens contextual help for the store', true);
644
+ fDone();
645
+ })
646
+ .catch(function(pError) { recordTestResult('Help button on store page opens contextual help for the store', false, pError.message); fDone(pError); });
647
+ }
648
+ );
649
+
650
+
651
+ // ====================================================================
652
+ // Test 5: Edit existing documentation — toggle to edit, modify, save
653
+ // ====================================================================
654
+ test
655
+ (
656
+ 'Edit mode: toggle into edit, modify content, and save',
657
+ function(fDone)
658
+ {
659
+ _Page.evaluate(function()
660
+ {
661
+ var state = window._Pict.AppData.InlineDocumentation;
662
+ return {
663
+ editEnabled: state.EditEnabled,
664
+ editing: state.Editing,
665
+ currentPath: state.CurrentPath
666
+ };
667
+ })
668
+ .then(function(pState)
669
+ {
670
+ Expect(pState.editEnabled).to.be.true;
671
+ Expect(pState.editing).to.be.false;
672
+
673
+ return _Page.evaluate(function() { document.getElementById('InlineDoc-Edit-Toggle').click(); });
674
+ })
675
+ .then(function()
676
+ {
677
+ return _Page.waitForSelector('#InlineDoc-Editor-Container', { timeout: 5000 });
678
+ })
679
+ .then(function()
680
+ {
681
+ return captureScreenshot('05a-edit-mode-textarea-open');
682
+ })
683
+ .then(function()
684
+ {
685
+ return captureStateLog('05a-edit-mode-textarea-open');
686
+ })
687
+ .then(function()
688
+ {
689
+ return _Page.evaluate(function()
690
+ {
691
+ var state = window._Pict.AppData.InlineDocumentation;
692
+ var editorContainer = document.getElementById('InlineDoc-Editor-Container');
693
+ var saveBtn = document.getElementById('InlineDoc-Edit-Save');
694
+ var cancelBtn = document.getElementById('InlineDoc-Edit-Cancel');
695
+ var editBtn = document.getElementById('InlineDoc-Edit-Toggle');
696
+ return {
697
+ editing: state.Editing,
698
+ editingPath: state.EditingPath,
699
+ hasEditor: !!editorContainer,
700
+ contentLength: (state.EditorSegments && state.EditorSegments.length > 0) ? (state.EditorSegments[0].Content || '').length : 0,
701
+ saveBtnVisible: saveBtn ? saveBtn.style.display !== 'none' : false,
702
+ cancelBtnVisible: cancelBtn ? cancelBtn.style.display !== 'none' : false,
703
+ editBtnHidden: editBtn ? editBtn.style.display === 'none' : false
704
+ };
705
+ });
706
+ })
707
+ .then(function(pResult)
708
+ {
709
+ Expect(pResult.editing).to.be.true;
710
+ Expect(pResult.editingPath).to.equal('store.md');
711
+ Expect(pResult.hasEditor).to.be.true;
712
+ Expect(pResult.contentLength).to.be.above(0, 'Editor should contain markdown');
713
+ Expect(pResult.saveBtnVisible).to.be.true;
714
+ Expect(pResult.cancelBtnVisible).to.be.true;
715
+ Expect(pResult.editBtnHidden).to.be.true;
716
+
717
+ return _Page.evaluate(function()
718
+ {
719
+ var editorView = window._Pict.views['InlineDoc-MarkdownEditor'];
720
+ var newContent = '# Edited Store Help\n\nThis was edited by the browser test.\n\n## Browser Test Section\n\nNew content added.';
721
+ if (editorView && typeof editorView.setSegmentContent === 'function')
722
+ {
723
+ editorView.setSegmentContent(0, newContent);
724
+ }
725
+ window._Pict.AppData.InlineDocumentation.EditorSegments[0].Content = newContent;
726
+ return newContent.length;
727
+ });
728
+ })
729
+ .then(function(pNewLength)
730
+ {
731
+ Expect(pNewLength).to.be.above(0);
732
+
733
+ return _Page.evaluate(function() { document.getElementById('InlineDoc-Edit-Save').click(); });
734
+ })
735
+ .then(function()
736
+ {
737
+ return _Page.waitForFunction(
738
+ '!document.getElementById("InlineDoc-Editor-Container")',
739
+ { timeout: 5000 }
740
+ );
741
+ })
742
+ .then(function()
743
+ {
744
+ return captureScreenshot('05b-edit-mode-saved');
745
+ })
746
+ .then(function()
747
+ {
748
+ return captureStateLog('05b-edit-mode-saved');
749
+ })
750
+ .then(function()
751
+ {
752
+ return _Page.evaluate(function()
753
+ {
754
+ var state = window._Pict.AppData.InlineDocumentation;
755
+ var body = document.getElementById('InlineDoc-Content-Body');
756
+ var provider = window._Pict.providers['Pict-InlineDocumentation'];
757
+ var cache = provider._ContentCache['docs/store.md'];
758
+ return {
759
+ editing: state.Editing,
760
+ bodyContainsEdited: body ? body.innerHTML.indexOf('Edited Store Help') >= 0 : false,
761
+ bodyContainsNewSection: body ? body.innerHTML.indexOf('Browser Test Section') >= 0 : false,
762
+ cacheUpdated: cache ? cache.markdown.indexOf('Edited Store Help') >= 0 : false
763
+ };
764
+ });
765
+ })
766
+ .then(function(pResult)
767
+ {
768
+ Expect(pResult.editing).to.be.false;
769
+ Expect(pResult.bodyContainsEdited).to.be.true;
770
+ Expect(pResult.bodyContainsNewSection).to.be.true;
771
+ Expect(pResult.cacheUpdated).to.be.true;
772
+ recordTestResult('Edit mode: toggle into edit, modify content, and save', true);
773
+ fDone();
774
+ })
775
+ .catch(function(pError) { recordTestResult('Edit mode: toggle into edit, modify content, and save', false, pError.message); fDone(pError); });
776
+ }
777
+ );
778
+
779
+
780
+ // ====================================================================
781
+ // Test 6: Edit and Cancel — content should revert
782
+ // ====================================================================
783
+ test
784
+ (
785
+ 'Edit mode: cancel reverts to original rendered content',
786
+ function(fDone)
787
+ {
788
+ _Page.evaluate(function() { document.getElementById('InlineDoc-Edit-Toggle').click(); })
789
+ .then(function()
790
+ {
791
+ return _Page.waitForSelector('#InlineDoc-Editor-Container', { timeout: 5000 });
792
+ })
793
+ .then(function()
794
+ {
795
+ return _Page.evaluate(function()
796
+ {
797
+ var editorView = window._Pict.views['InlineDoc-MarkdownEditor'];
798
+ var newContent = '# CANCELLED CHANGES\n\nThis should not appear.';
799
+ if (editorView && typeof editorView.setSegmentContent === 'function')
800
+ {
801
+ editorView.setSegmentContent(0, newContent);
802
+ }
803
+ window._Pict.AppData.InlineDocumentation.EditorSegments[0].Content = newContent;
804
+ });
805
+ })
806
+ .then(function()
807
+ {
808
+ return captureScreenshot('06a-edit-mode-before-cancel');
809
+ })
810
+ .then(function()
811
+ {
812
+ return _Page.evaluate(function() { document.getElementById('InlineDoc-Edit-Cancel').click(); });
813
+ })
814
+ .then(function()
815
+ {
816
+ return _Page.waitForFunction(
817
+ '!document.getElementById("InlineDoc-Editor-Container")',
818
+ { timeout: 5000 }
819
+ );
820
+ })
821
+ .then(function()
822
+ {
823
+ return captureScreenshot('06b-edit-mode-after-cancel');
824
+ })
825
+ .then(function()
826
+ {
827
+ return captureStateLog('06-edit-cancel');
828
+ })
829
+ .then(function()
830
+ {
831
+ return _Page.evaluate(function()
832
+ {
833
+ var state = window._Pict.AppData.InlineDocumentation;
834
+ var body = document.getElementById('InlineDoc-Content-Body');
835
+ return {
836
+ editing: state.Editing,
837
+ bodyDoesNotContainCancelled: body ? body.innerHTML.indexOf('CANCELLED CHANGES') < 0 : true,
838
+ bodyContainsSavedContent: body ? body.innerHTML.indexOf('Edited Store Help') >= 0 : false
839
+ };
840
+ });
841
+ })
842
+ .then(function(pResult)
843
+ {
844
+ Expect(pResult.editing).to.be.false;
845
+ Expect(pResult.bodyDoesNotContainCancelled).to.be.true;
846
+ Expect(pResult.bodyContainsSavedContent).to.be.true;
847
+ recordTestResult('Edit mode: cancel reverts to original rendered content', true);
848
+ fDone();
849
+ })
850
+ .catch(function(pError) { recordTestResult('Edit mode: cancel reverts to original rendered content', false, pError.message); fDone(pError); });
851
+ }
852
+ );
853
+
854
+
855
+ // ====================================================================
856
+ // Test 7: Navigate back to book list and verify help context switches
857
+ // ====================================================================
858
+ test
859
+ (
860
+ 'Navigate back to book list and help context switches to book list topic',
861
+ function(fDone)
862
+ {
863
+ _Page.evaluate(function()
864
+ {
865
+ var appState = window._Pict.AppData.Bookshop;
866
+ if (appState.CurrentView !== 'Store')
867
+ {
868
+ window._Pict.PictApplication.showBook(1);
869
+ }
870
+ var panel = document.getElementById('Bookshop-Help-Panel');
871
+ if (panel && panel.classList.contains('visible'))
872
+ {
873
+ window._Pict.PictApplication.toggleHelp();
874
+ }
875
+ })
876
+ .then(function()
877
+ {
878
+ return _Page.waitForSelector('#Bookshop-Store-Back', { timeout: 5000 });
879
+ })
880
+ .then(function()
881
+ {
882
+ return _Page.evaluate(function() { document.getElementById('Bookshop-Store-Back').click(); });
883
+ })
884
+ .then(function()
885
+ {
886
+ return _Page.waitForSelector('.bookshop-book-card', { timeout: 5000 });
887
+ })
888
+ .then(function()
889
+ {
890
+ return captureScreenshot('07-navigated-back-to-book-list');
891
+ })
892
+ .then(function()
893
+ {
894
+ return captureStateLog('07-navigated-back-to-book-list');
895
+ })
896
+ .then(function()
897
+ {
898
+ return _Page.evaluate(function()
899
+ {
900
+ var state = window._Pict.AppData.InlineDocumentation;
901
+ return {
902
+ currentRoute: state.CurrentRoute,
903
+ topic: state.Topic,
904
+ currentPath: state.CurrentPath
905
+ };
906
+ });
907
+ })
908
+ .then(function(pResult)
909
+ {
910
+ Expect(pResult.currentRoute).to.equal('/books');
911
+ Expect(pResult.topic).to.equal('BOOKSHOP-BOOKLIST');
912
+ Expect(pResult.currentPath).to.equal('book-list.md');
913
+ recordTestResult('Navigate back to book list and help context switches to book list topic', true);
914
+ fDone();
915
+ })
916
+ .catch(function(pError) { recordTestResult('Navigate back to book list and help context switches to book list topic', false, pError.message); fDone(pError); });
917
+ }
918
+ );
919
+
920
+
921
+ // ====================================================================
922
+ // Test 8: F1 keyboard shortcut toggles help
923
+ // ====================================================================
924
+ test
925
+ (
926
+ 'F1 keyboard shortcut toggles help panel',
927
+ function(fDone)
928
+ {
929
+ _Page.evaluate(function()
930
+ {
931
+ var panel = document.getElementById('Bookshop-Help-Panel');
932
+ if (panel && panel.classList.contains('visible'))
933
+ {
934
+ window._Pict.PictApplication.toggleHelp();
935
+ }
936
+ return !document.getElementById('Bookshop-Help-Panel').classList.contains('visible');
937
+ })
938
+ .then(function(pIsClosed)
939
+ {
940
+ Expect(pIsClosed).to.be.true;
941
+
942
+ return _Page.keyboard.press('F1');
943
+ })
944
+ .then(function()
945
+ {
946
+ return _Page.waitForFunction(
947
+ 'document.getElementById("Bookshop-Help-Panel").classList.contains("visible")',
948
+ { timeout: 5000 }
949
+ );
950
+ })
951
+ .then(function()
952
+ {
953
+ return captureScreenshot('08a-f1-help-opened');
954
+ })
955
+ .then(function()
956
+ {
957
+ return _Page.keyboard.press('F1');
958
+ })
959
+ .then(function()
960
+ {
961
+ return _Page.waitForFunction(
962
+ '!document.getElementById("Bookshop-Help-Panel").classList.contains("visible")',
963
+ { timeout: 5000 }
964
+ );
965
+ })
966
+ .then(function()
967
+ {
968
+ return captureScreenshot('08b-f1-help-closed');
969
+ })
970
+ .then(function()
971
+ {
972
+ return captureStateLog('08-f1-toggle');
973
+ })
974
+ .then(function()
975
+ {
976
+ return _Page.evaluate(function()
977
+ {
978
+ return !document.getElementById('Bookshop-Help-Panel').classList.contains('visible');
979
+ });
980
+ })
981
+ .then(function(pIsClosed)
982
+ {
983
+ Expect(pIsClosed).to.be.true;
984
+ recordTestResult('F1 keyboard shortcut toggles help panel', true);
985
+ fDone();
986
+ })
987
+ .catch(function(pError) { recordTestResult('F1 keyboard shortcut toggles help panel', false, pError.message); fDone(pError); });
988
+ }
989
+ );
990
+
991
+
992
+ // ====================================================================
993
+ // Test 9: Create a new documentation topic at runtime (not route-bound)
994
+ // ====================================================================
995
+ test
996
+ (
997
+ 'Create a new topic at runtime that is not bound to any route',
998
+ function(fDone)
999
+ {
1000
+ _Page.evaluate(function()
1001
+ {
1002
+ var provider = window._Pict.providers['Pict-InlineDocumentation'];
1003
+
1004
+ provider.addTopic('CUSTOM-HELP',
1005
+ {
1006
+ TopicHelpFilePath: 'custom-help.md',
1007
+ TopicTitle: 'Custom Runtime Help'
1008
+ });
1009
+
1010
+ var url = 'docs/custom-help.md';
1011
+ var markdown = '# Custom Help Topic\\n\\nThis documentation was created at runtime.\\n\\n## Not Route Bound\\n\\nThis topic has no route association.';
1012
+ var html = provider._ContentProvider.parseMarkdown(markdown);
1013
+ provider._ContentCache[url] = { html: html, markdown: markdown };
1014
+
1015
+ provider.loadTopicDocument('CUSTOM-HELP');
1016
+
1017
+ var state = window._Pict.AppData.InlineDocumentation;
1018
+ return {
1019
+ topicExists: !!state.Topics['CUSTOM-HELP'],
1020
+ topicTitle: state.Topics['CUSTOM-HELP'] ? state.Topics['CUSTOM-HELP'].TopicTitle : null,
1021
+ topicHasRoutes: !!(state.Topics['CUSTOM-HELP'] && state.Topics['CUSTOM-HELP'].Routes),
1022
+ activeTopic: state.Topic,
1023
+ currentPath: state.CurrentPath
1024
+ };
1025
+ })
1026
+ .then(function(pResult)
1027
+ {
1028
+ Expect(pResult.topicExists).to.be.true;
1029
+ Expect(pResult.topicTitle).to.equal('Custom Runtime Help');
1030
+ Expect(pResult.topicHasRoutes).to.be.false;
1031
+ Expect(pResult.activeTopic).to.equal('CUSTOM-HELP');
1032
+ Expect(pResult.currentPath).to.equal('custom-help.md');
1033
+
1034
+ return _Page.evaluate(function()
1035
+ {
1036
+ var panel = document.getElementById('Bookshop-Help-Panel');
1037
+ if (!panel.classList.contains('visible'))
1038
+ {
1039
+ window._Pict.PictApplication.toggleHelp();
1040
+ }
1041
+ });
1042
+ })
1043
+ .then(function()
1044
+ {
1045
+ return _Page.waitForFunction(
1046
+ 'document.getElementById("Bookshop-Help-Panel").classList.contains("visible")',
1047
+ { timeout: 3000 }
1048
+ );
1049
+ })
1050
+ .then(function()
1051
+ {
1052
+ return captureScreenshot('09-runtime-topic-created');
1053
+ })
1054
+ .then(function()
1055
+ {
1056
+ return captureStateLog('09-runtime-topic-created');
1057
+ })
1058
+ .then(function()
1059
+ {
1060
+ return _Page.evaluate(function()
1061
+ {
1062
+ var body = document.getElementById('InlineDoc-Content-Body');
1063
+ return {
1064
+ hasCustomContent: body ? body.innerHTML.indexOf('Custom Help Topic') >= 0 : false,
1065
+ hasNotRouteBound: body ? body.innerHTML.indexOf('Not Route Bound') >= 0 : false
1066
+ };
1067
+ });
1068
+ })
1069
+ .then(function(pResult)
1070
+ {
1071
+ Expect(pResult.hasCustomContent).to.be.true;
1072
+ Expect(pResult.hasNotRouteBound).to.be.true;
1073
+
1074
+ return _Page.evaluate(function()
1075
+ {
1076
+ var provider = window._Pict.providers['Pict-InlineDocumentation'];
1077
+ var match = provider.resolveHelpForRoute('/custom');
1078
+ return match;
1079
+ });
1080
+ })
1081
+ .then(function(pMatch)
1082
+ {
1083
+ Expect(pMatch).to.be.null;
1084
+ recordTestResult('Create a new topic at runtime that is not bound to any route', true);
1085
+ fDone();
1086
+ })
1087
+ .catch(function(pError) { recordTestResult('Create a new topic at runtime that is not bound to any route', false, pError.message); fDone(pError); });
1088
+ }
1089
+ );
1090
+
1091
+
1092
+ // ====================================================================
1093
+ // Test 10: Bind a documentation entry to a route
1094
+ // ====================================================================
1095
+ test
1096
+ (
1097
+ 'Bind an existing topic to a route and verify it resolves',
1098
+ function(fDone)
1099
+ {
1100
+ _Page.evaluate(function()
1101
+ {
1102
+ var provider = window._Pict.providers['Pict-InlineDocumentation'];
1103
+
1104
+ provider.addRouteToTopic('CUSTOM-HELP', '/custom');
1105
+ provider.addRouteToTopic('CUSTOM-HELP', '/custom/*');
1106
+
1107
+ var state = window._Pict.AppData.InlineDocumentation;
1108
+ var topic = state.Topics['CUSTOM-HELP'];
1109
+
1110
+ return {
1111
+ hasRoutes: Array.isArray(topic.Routes),
1112
+ routeCount: topic.Routes ? topic.Routes.length : 0,
1113
+ routes: topic.Routes || [],
1114
+ resolvedExact: provider.resolveHelpForRoute('/custom'),
1115
+ resolvedWildcard: provider.resolveHelpForRoute('/custom/sub/page'),
1116
+ resolvedUnrelated: provider.resolveHelpForRoute('/unrelated')
1117
+ };
1118
+ })
1119
+ .then(function(pResult)
1120
+ {
1121
+ Expect(pResult.hasRoutes).to.be.true;
1122
+ Expect(pResult.routeCount).to.equal(2);
1123
+ Expect(pResult.routes).to.include('/custom');
1124
+ Expect(pResult.routes).to.include('/custom/*');
1125
+ Expect(pResult.resolvedExact).to.equal('CUSTOM-HELP');
1126
+ Expect(pResult.resolvedWildcard).to.equal('CUSTOM-HELP');
1127
+ Expect(pResult.resolvedUnrelated).to.be.null;
1128
+ recordTestResult('Bind an existing topic to a route and verify it resolves', true);
1129
+ fDone();
1130
+ })
1131
+ .catch(function(pError) { recordTestResult('Bind an existing topic to a route and verify it resolves', false, pError.message); fDone(pError); });
1132
+ }
1133
+ );
1134
+
1135
+
1136
+ // ====================================================================
1137
+ // Test 11: Multiple topics match the same route — getTopicsForRoute
1138
+ // ====================================================================
1139
+ test
1140
+ (
1141
+ 'Multiple topics matching one route, user can navigate between them',
1142
+ function(fDone)
1143
+ {
1144
+ _Page.evaluate(function()
1145
+ {
1146
+ var provider = window._Pict.providers['Pict-InlineDocumentation'];
1147
+
1148
+ provider.addTopic('BOOKSHOP-FAQ',
1149
+ {
1150
+ TopicHelpFilePath: 'faq.md',
1151
+ TopicTitle: 'Frequently Asked Questions',
1152
+ Routes: ['/books', '/books/*']
1153
+ });
1154
+
1155
+ var markdown = '# Bookshop FAQ\\n\\nFrequently asked questions about the bookshop.\\n\\n## How do I buy a book?\\n\\nClick on a book card and then click Add to Cart.';
1156
+ var html = provider._ContentProvider.parseMarkdown(markdown);
1157
+ provider._ContentCache['docs/faq.md'] = { html: html, markdown: markdown };
1158
+
1159
+ var matches = provider.getTopicsForRoute('/books');
1160
+
1161
+ return {
1162
+ matchCount: matches.length,
1163
+ matchCodes: matches.map(function(m) { return m.TopicCode; }),
1164
+ firstMatch: matches.length > 0 ? matches[0].TopicCode : null,
1165
+ allHavePatterns: matches.every(function(m) { return m.Pattern && m.MatchLength > 0; })
1166
+ };
1167
+ })
1168
+ .then(function(pResult)
1169
+ {
1170
+ Expect(pResult.matchCount).to.be.at.least(2, 'Should have at least two topics matching /books');
1171
+ Expect(pResult.matchCodes).to.include('BOOKSHOP-BOOKLIST');
1172
+ Expect(pResult.matchCodes).to.include('BOOKSHOP-FAQ');
1173
+ Expect(pResult.allHavePatterns).to.be.true;
1174
+
1175
+ return _Page.evaluate(function()
1176
+ {
1177
+ var provider = window._Pict.providers['Pict-InlineDocumentation'];
1178
+ provider.loadTopicDocument('BOOKSHOP-BOOKLIST');
1179
+ return true;
1180
+ });
1181
+ })
1182
+ .then(function()
1183
+ {
1184
+ return _Page.waitForFunction(
1185
+ 'document.getElementById("InlineDoc-Content-Body") && document.getElementById("InlineDoc-Content-Body").innerHTML.indexOf("Book Catalog") >= 0',
1186
+ { timeout: 5000 }
1187
+ );
1188
+ })
1189
+ .then(function()
1190
+ {
1191
+ return captureScreenshot('11a-multi-match-booklist-topic');
1192
+ })
1193
+ .then(function()
1194
+ {
1195
+ return _Page.evaluate(function()
1196
+ {
1197
+ var provider = window._Pict.providers['Pict-InlineDocumentation'];
1198
+ provider.loadTopicDocument('BOOKSHOP-FAQ');
1199
+ return true;
1200
+ });
1201
+ })
1202
+ .then(function()
1203
+ {
1204
+ return _Page.waitForFunction(
1205
+ 'document.getElementById("InlineDoc-Content-Body") && document.getElementById("InlineDoc-Content-Body").innerHTML.indexOf("Bookshop FAQ") >= 0',
1206
+ { timeout: 5000 }
1207
+ );
1208
+ })
1209
+ .then(function()
1210
+ {
1211
+ return captureScreenshot('11b-multi-match-faq-topic');
1212
+ })
1213
+ .then(function()
1214
+ {
1215
+ return captureStateLog('11-multi-match-navigate');
1216
+ })
1217
+ .then(function()
1218
+ {
1219
+ return _Page.evaluate(function()
1220
+ {
1221
+ var state = window._Pict.AppData.InlineDocumentation;
1222
+ var body = document.getElementById('InlineDoc-Content-Body');
1223
+ return {
1224
+ topic: state.Topic,
1225
+ currentPath: state.CurrentPath,
1226
+ hasFaqContent: body ? body.innerHTML.indexOf('Bookshop FAQ') >= 0 : false,
1227
+ hasHowToBuy: body ? body.innerHTML.indexOf('How do I buy a book') >= 0 : false
1228
+ };
1229
+ });
1230
+ })
1231
+ .then(function(pResult)
1232
+ {
1233
+ Expect(pResult.topic).to.equal('BOOKSHOP-FAQ');
1234
+ Expect(pResult.currentPath).to.equal('faq.md');
1235
+ Expect(pResult.hasFaqContent).to.be.true;
1236
+ Expect(pResult.hasHowToBuy).to.be.true;
1237
+
1238
+ // Switch back to confirm toggle works
1239
+ return _Page.evaluate(function()
1240
+ {
1241
+ var provider = window._Pict.providers['Pict-InlineDocumentation'];
1242
+ provider.loadTopicDocument('BOOKSHOP-BOOKLIST');
1243
+ return true;
1244
+ });
1245
+ })
1246
+ .then(function()
1247
+ {
1248
+ return _Page.waitForFunction(
1249
+ 'document.getElementById("InlineDoc-Content-Body") && document.getElementById("InlineDoc-Content-Body").innerHTML.indexOf("Book Catalog") >= 0',
1250
+ { timeout: 5000 }
1251
+ );
1252
+ })
1253
+ .then(function()
1254
+ {
1255
+ return captureScreenshot('11c-multi-match-back-to-booklist');
1256
+ })
1257
+ .then(function()
1258
+ {
1259
+ return _Page.evaluate(function()
1260
+ {
1261
+ var state = window._Pict.AppData.InlineDocumentation;
1262
+ return { topic: state.Topic, currentPath: state.CurrentPath };
1263
+ });
1264
+ })
1265
+ .then(function(pResult)
1266
+ {
1267
+ Expect(pResult.topic).to.equal('BOOKSHOP-BOOKLIST');
1268
+ Expect(pResult.currentPath).to.equal('book-list.md');
1269
+ recordTestResult('Multiple topics matching one route, user can navigate between them', true);
1270
+ fDone();
1271
+ })
1272
+ .catch(function(pError) { recordTestResult('Multiple topics matching one route, user can navigate between them', false, pError.message); fDone(pError); });
1273
+ }
1274
+ );
1275
+
1276
+
1277
+ // ====================================================================
1278
+ // Test 12: getTopicsForRoute on wildcard-heavy routes
1279
+ // ====================================================================
1280
+ test
1281
+ (
1282
+ 'getTopicsForRoute returns sorted matches with wildcards',
1283
+ function(fDone)
1284
+ {
1285
+ _Page.evaluate(function()
1286
+ {
1287
+ var provider = window._Pict.providers['Pict-InlineDocumentation'];
1288
+
1289
+ var matches = provider.getTopicsForRoute('/books/store/5');
1290
+ return {
1291
+ matchCount: matches.length,
1292
+ matchCodes: matches.map(function(m) { return m.TopicCode; }),
1293
+ firstMatch: matches.length > 0 ? matches[0].TopicCode : null,
1294
+ firstMatchLength: matches.length > 0 ? matches[0].MatchLength : 0,
1295
+ lastMatchLength: matches.length > 0 ? matches[matches.length - 1].MatchLength : 0
1296
+ };
1297
+ })
1298
+ .then(function(pResult)
1299
+ {
1300
+ Expect(pResult.matchCount).to.be.at.least(2, 'Should match BOOKSHOP-STORE and BOOKSHOP-FAQ');
1301
+ Expect(pResult.matchCodes).to.include('BOOKSHOP-STORE');
1302
+ Expect(pResult.matchCodes).to.include('BOOKSHOP-FAQ');
1303
+ Expect(pResult.firstMatch).to.equal('BOOKSHOP-STORE', 'BOOKSHOP-STORE should be first (longer prefix)');
1304
+ Expect(pResult.firstMatchLength).to.be.at.least(pResult.lastMatchLength, 'Results should be sorted by match length desc');
1305
+ recordTestResult('getTopicsForRoute returns sorted matches with wildcards', true);
1306
+ fDone();
1307
+ })
1308
+ .catch(function(pError) { recordTestResult('getTopicsForRoute returns sorted matches with wildcards', false, pError.message); fDone(pError); });
1309
+ }
1310
+ );
1311
+
1312
+
1313
+ // ====================================================================
1314
+ // Test 13: Edit the newly created runtime topic
1315
+ // ====================================================================
1316
+ test
1317
+ (
1318
+ 'Edit the runtime-created topic content',
1319
+ function(fDone)
1320
+ {
1321
+ _Page.evaluate(function()
1322
+ {
1323
+ var provider = window._Pict.providers['Pict-InlineDocumentation'];
1324
+ provider.loadTopicDocument('CUSTOM-HELP');
1325
+ var panel = document.getElementById('Bookshop-Help-Panel');
1326
+ if (!panel.classList.contains('visible'))
1327
+ {
1328
+ window._Pict.PictApplication.toggleHelp();
1329
+ }
1330
+ return true;
1331
+ })
1332
+ .then(function()
1333
+ {
1334
+ return _Page.waitForFunction(
1335
+ 'document.getElementById("InlineDoc-Content-Body") && document.getElementById("InlineDoc-Content-Body").innerHTML.indexOf("Custom Help Topic") >= 0',
1336
+ { timeout: 5000 }
1337
+ );
1338
+ })
1339
+ .then(function()
1340
+ {
1341
+ return _Page.evaluate(function() { document.getElementById('InlineDoc-Edit-Toggle').click(); });
1342
+ })
1343
+ .then(function()
1344
+ {
1345
+ return _Page.waitForSelector('#InlineDoc-Editor-Container', { timeout: 5000 });
1346
+ })
1347
+ .then(function()
1348
+ {
1349
+ return captureScreenshot('13a-edit-runtime-topic');
1350
+ })
1351
+ .then(function()
1352
+ {
1353
+ return _Page.evaluate(function()
1354
+ {
1355
+ var editorView = window._Pict.views['InlineDoc-MarkdownEditor'];
1356
+ var newContent = '# Updated Custom Help\n\nThis topic was edited after being created at runtime.\n\n## Now With Route Binding\n\nBound to /custom.';
1357
+ if (editorView && typeof editorView.setSegmentContent === 'function')
1358
+ {
1359
+ editorView.setSegmentContent(0, newContent);
1360
+ }
1361
+ window._Pict.AppData.InlineDocumentation.EditorSegments[0].Content = newContent;
1362
+ });
1363
+ })
1364
+ .then(function()
1365
+ {
1366
+ return _Page.evaluate(function() { document.getElementById('InlineDoc-Edit-Save').click(); });
1367
+ })
1368
+ .then(function()
1369
+ {
1370
+ return _Page.waitForFunction(
1371
+ '!document.getElementById("InlineDoc-Editor-Container")',
1372
+ { timeout: 5000 }
1373
+ );
1374
+ })
1375
+ .then(function()
1376
+ {
1377
+ return captureScreenshot('13b-edit-runtime-topic-saved');
1378
+ })
1379
+ .then(function()
1380
+ {
1381
+ return captureStateLog('13-edit-runtime-topic');
1382
+ })
1383
+ .then(function()
1384
+ {
1385
+ return _Page.evaluate(function()
1386
+ {
1387
+ var body = document.getElementById('InlineDoc-Content-Body');
1388
+ var provider = window._Pict.providers['Pict-InlineDocumentation'];
1389
+ var cache = provider._ContentCache['docs/custom-help.md'];
1390
+ return {
1391
+ hasUpdatedTitle: body ? body.innerHTML.indexOf('Updated Custom Help') >= 0 : false,
1392
+ cacheMarkdownUpdated: cache ? cache.markdown.indexOf('Updated Custom Help') >= 0 : false
1393
+ };
1394
+ });
1395
+ })
1396
+ .then(function(pResult)
1397
+ {
1398
+ Expect(pResult.hasUpdatedTitle).to.be.true;
1399
+ Expect(pResult.cacheMarkdownUpdated).to.be.true;
1400
+ recordTestResult('Edit the runtime-created topic content', true);
1401
+ fDone();
1402
+ })
1403
+ .catch(function(pError) { recordTestResult('Edit the runtime-created topic content', false, pError.message); fDone(pError); });
1404
+ }
1405
+ );
1406
+
1407
+
1408
+ // ====================================================================
1409
+ // Test 14: Verify edit disabled hides edit toolbar
1410
+ // ====================================================================
1411
+ test
1412
+ (
1413
+ 'Disabling edit mode hides the edit toolbar',
1414
+ function(fDone)
1415
+ {
1416
+ _Page.evaluate(function()
1417
+ {
1418
+ var provider = window._Pict.providers['Pict-InlineDocumentation'];
1419
+
1420
+ provider.setEditEnabled(false);
1421
+
1422
+ var toolbar = document.getElementById('InlineDoc-Edit-Toolbar');
1423
+ var isHidden = toolbar ? !toolbar.classList.contains('visible') : true;
1424
+
1425
+ provider.setEditEnabled(true);
1426
+
1427
+ return { toolbarHiddenWhenDisabled: isHidden };
1428
+ })
1429
+ .then(function(pResult)
1430
+ {
1431
+ Expect(pResult.toolbarHiddenWhenDisabled).to.be.true;
1432
+ recordTestResult('Disabling edit mode hides the edit toolbar', true);
1433
+
1434
+ // Final screenshot
1435
+ return captureScreenshot('14-final-state');
1436
+ })
1437
+ .then(function()
1438
+ {
1439
+ return captureStateLog('14-final-state');
1440
+ })
1441
+ .then(function()
1442
+ {
1443
+ fDone();
1444
+ })
1445
+ .catch(function(pError) { recordTestResult('Disabling edit mode hides the edit toolbar', false, pError.message); fDone(pError); });
1446
+ }
1447
+ );
1448
+ }
1449
+ );