hub-dashboard-ia-mobile.test.mjs
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6
docs: record AIP-b SD-21 land (KN #308)
Human
12 days ago
| 1 | /** |
| 2 | * Seven-tier contracts for HUB-DASH-IA-d (mobile bottom nav + How-to copy sync). |
| 3 | * Ground truth: docs/reviews/2026-07-29-hub-dashboard-ia.md (expert item 20 + copy/BV). |
| 4 | */ |
| 5 | import { describe, it } from 'node:test'; |
| 6 | import assert from 'node:assert/strict'; |
| 7 | import fs from 'node:fs'; |
| 8 | import path from 'node:path'; |
| 9 | import { fileURLToPath } from 'node:url'; |
| 10 | import { |
| 11 | hubMobileBottomNavOrder, |
| 12 | hubMobileMoreActions, |
| 13 | hubPrimaryRailOrder, |
| 14 | hubCriticalDataTabs, |
| 15 | clampProposedBadgeCount, |
| 16 | formatProposedBadgeText, |
| 17 | shouldPulseReviewBadge, |
| 18 | hubShellLabelForTab, |
| 19 | } from '../web/hub/hub-shell-ia.mjs'; |
| 20 | |
| 21 | const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); |
| 22 | const hubHtml = fs.readFileSync(path.join(root, 'web/hub/index.html'), 'utf8'); |
| 23 | const hubJs = fs.readFileSync(path.join(root, 'web/hub/hub.js'), 'utf8'); |
| 24 | const hubCss = fs.readFileSync(path.join(root, 'web/hub/hub.css'), 'utf8'); |
| 25 | const onboarding = fs.readFileSync(path.join(root, 'web/hub/onboarding-wizard.mjs'), 'utf8'); |
| 26 | |
| 27 | describe('HUB-DASH-IA-d unit', () => { |
| 28 | it('mobile bottom nav order is Vault | Review | History | More', () => { |
| 29 | assert.deepEqual(hubMobileBottomNavOrder(), ['Vault', 'Review', 'History', 'More']); |
| 30 | assert.deepEqual(hubPrimaryRailOrder(), ['Vault', 'Review', 'History']); |
| 31 | assert.equal(hubShellLabelForTab('suggested'), 'Review'); |
| 32 | }); |
| 33 | |
| 34 | it('More sheet lists Import/Connect/Settings/Help (Insights relocated for mobile)', () => { |
| 35 | const more = hubMobileMoreActions(); |
| 36 | assert.ok(more.includes('Import')); |
| 37 | assert.ok(more.includes('Connect')); |
| 38 | assert.ok(more.includes('Settings')); |
| 39 | assert.ok(more.includes('Help')); |
| 40 | assert.ok(more.includes('Insights')); |
| 41 | assert.deepEqual(more, ['Insights', 'Import', 'Connect', 'Settings', 'Help']); |
| 42 | }); |
| 43 | }); |
| 44 | |
| 45 | describe('HUB-DASH-IA-d integration (source wiring)', () => { |
| 46 | it('bottom Review uses data-tab=suggested; History opens History mode; More actions share secondary runner', () => { |
| 47 | assert.match(hubHtml, /id="hub-bottom-review"[^>]*data-tab="suggested"/); |
| 48 | assert.match(hubHtml, /id="hub-bottom-vault"[^>]*data-tab="notes"/); |
| 49 | assert.match(hubHtml, /id="hub-bottom-history"[^>]*data-hub-mode="history"/); |
| 50 | assert.match(hubJs, /hub-bottom-history[\s\S]{0,200}openHistoryMode\s*\(/); |
| 51 | assert.match(hubJs, /function\s+runHubSecondaryAction\s*\(/); |
| 52 | assert.match(hubJs, /data-hub-more-action[\s\S]{0,300}runHubSecondaryAction/); |
| 53 | assert.match( |
| 54 | hubJs, |
| 55 | /key\s*===\s*['"]connect['"][\s\S]{0,200}openSettingsIntegrationsTab\s*\(/, |
| 56 | ); |
| 57 | }); |
| 58 | |
| 59 | it('badge updates include hub-bottom-review-badge with rail/header', () => { |
| 60 | assert.match( |
| 61 | hubJs, |
| 62 | /\[['"]hub-review-badge['"],\s*['"]hub-header-review-badge['"],\s*['"]hub-bottom-review-badge['"]\]/, |
| 63 | ); |
| 64 | assert.match(hubHtml, /id="hub-bottom-review-badge"/); |
| 65 | }); |
| 66 | }); |
| 67 | |
| 68 | describe('HUB-DASH-IA-d e2e static contract', () => { |
| 69 | it('bottom nav markup order Vault → Review → History → More', () => { |
| 70 | const nav = hubHtml.match(/id="hub-bottom-nav"[\s\S]*?<\/nav>/); |
| 71 | assert.ok(nav, 'hub-bottom-nav present'); |
| 72 | const chunk = nav[0]; |
| 73 | const vault = chunk.indexOf('>Vault<'); |
| 74 | const review = chunk.indexOf('>Review'); |
| 75 | const history = chunk.indexOf('>History<'); |
| 76 | const more = chunk.indexOf('>More<'); |
| 77 | assert.ok(vault >= 0 && review > vault && history > review && more > history); |
| 78 | }); |
| 79 | |
| 80 | it('More sheet has Import, Connect, Settings, Help actions', () => { |
| 81 | assert.match(hubHtml, /id="hub-more-sheet"/); |
| 82 | for (const action of ['import', 'connect', 'settings', 'help']) { |
| 83 | assert.match(hubHtml, new RegExp('data-hub-more-action="' + action + '"')); |
| 84 | } |
| 85 | }); |
| 86 | |
| 87 | it('How-to / onboarding copy uses Review glossary and History segments (no Suggested queue label)', () => { |
| 88 | assert.match(onboarding, /Proposals and the Review queue/); |
| 89 | assert.match(onboarding, /mobile bottom nav/); |
| 90 | assert.match(hubHtml, /History → Discarded/); |
| 91 | assert.match(hubHtml, /History → Activity/); |
| 92 | assert.match(hubHtml, /Vault browse \+ Insights/); |
| 93 | assert.doesNotMatch(hubHtml, /Discarded tab:/); |
| 94 | assert.doesNotMatch(hubHtml, /Activity tab:/); |
| 95 | assert.doesNotMatch(hubHtml, />Suggested</); |
| 96 | assert.doesNotMatch(onboarding, /Suggested queue/); |
| 97 | assert.doesNotMatch(onboarding, /the Suggested/); |
| 98 | }); |
| 99 | }); |
| 100 | |
| 101 | describe('HUB-DASH-IA-d stress', () => { |
| 102 | it('bottom badge helpers safe for proposed count 0–100', () => { |
| 103 | for (let i = 0; i <= 100; i++) { |
| 104 | assert.equal(clampProposedBadgeCount(i), i); |
| 105 | const t = formatProposedBadgeText(i); |
| 106 | if (i === 0) assert.equal(t, ''); |
| 107 | else assert.equal(t, String(i)); |
| 108 | } |
| 109 | assert.equal(shouldPulseReviewBadge(0, 1), true); |
| 110 | }); |
| 111 | }); |
| 112 | |
| 113 | describe('HUB-DASH-IA-d data-integrity', () => { |
| 114 | it('critical data-tab values unchanged; bottom nav does not rename hooks', () => { |
| 115 | assert.deepEqual(hubCriticalDataTabs(), ['notes', 'suggested', 'activity', 'problem']); |
| 116 | for (const tab of hubCriticalDataTabs()) { |
| 117 | assert.match(hubHtml, new RegExp('data-tab="' + tab + '"')); |
| 118 | assert.match(hubHtml, new RegExp('id="tab-' + tab + '"')); |
| 119 | } |
| 120 | for (const id of [ |
| 121 | 'btn-header-suggested', |
| 122 | 'proposals-suggested', |
| 123 | 'btn-import', |
| 124 | 'btn-settings', |
| 125 | 'btn-how-to-use', |
| 126 | 'modal-import', |
| 127 | 'modal-settings', |
| 128 | 'modal-how-to-use', |
| 129 | 'modal-onboarding', |
| 130 | 'hub-rail-connect', |
| 131 | 'hub-bottom-nav', |
| 132 | ]) { |
| 133 | assert.match(hubHtml, new RegExp('id="' + id + '"')); |
| 134 | } |
| 135 | }); |
| 136 | }); |
| 137 | |
| 138 | describe('HUB-DASH-IA-d performance (mobile chrome disclosure)', () => { |
| 139 | it('CSS hides left rail and shows fixed bottom nav at ≤768px', () => { |
| 140 | assert.match(hubCss, /@media\s*\(\s*max-width:\s*768px\s*\)/); |
| 141 | assert.match(hubCss, /\.hub-rail\s*\{\s*display:\s*none/); |
| 142 | assert.match(hubCss, /\.hub-bottom-nav\s*\{[^}]*display:\s*flex/s); |
| 143 | assert.match(hubCss, /\.hub-bottom-nav\s*\{[^}]*position:\s*fixed/s); |
| 144 | assert.match(hubCss, /\.hub-bottom-nav\s*\{\s*display:\s*none/); |
| 145 | }); |
| 146 | }); |
| 147 | |
| 148 | describe('HUB-DASH-IA-d security', () => { |
| 149 | it('More actions use text labels; badge textContent; approve/discard contracts unchanged', () => { |
| 150 | assert.match(hubJs, /badge\.textContent\s*=/); |
| 151 | assert.doesNotMatch(hubJs, /hub-bottom-review-badge[\s\S]{0,80}innerHTML\s*=/); |
| 152 | assert.doesNotMatch(hubJs, /data-hub-more-action[\s\S]{0,120}innerHTML\s*=/); |
| 153 | assert.match(hubJs, /\/api\/v1\/proposals\/['"]\s*\+\s*encodeURIComponent\(id\)\s*\+\s*['"]\/approve/); |
| 154 | assert.match(hubJs, /\/api\/v1\/proposals\/['"]\s*\+\s*encodeURIComponent\(id\)\s*\+\s*['"]\/discard/); |
| 155 | }); |
| 156 | }); |
File History
1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6
docs: record AIP-b SD-21 land (KN #308)
Human
12 days ago