2026-07-12 13:00:02 +08:00
<!doctype html>
< html >
< head >
< meta charset = "utf-8" > < meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > components.Nav tests< / title > < script src = "../../ui.js" components = "Nav" > < / script >
< script >
const navBrand = { icon: 'box', label: 'Apigo' }
2026-07-13 22:24:23 +08:00
const navItems = [{ type: 'label', label: 'Main' }, { type: 'button', name: 'home', label: 'Home', icon: 'house' }, { type: 'dropdown', name: 'admin', label: 'Admin', icon: 'shield', items: [{ type: 'label', label: 'Manage' }, { type: 'button', name: 'roles', label: 'Roles', icon: 'person' }, { type: 'switch', name: 'alerts', label: 'Alerts', bind: { alerts: false } }] }, { type: 'fill' }, { type: 'button', name: 'settings', label: 'Settings', icon: 'gear' }, { type: 'button', name: 'logout', label: 'Logout', icon: 'door-open', noselect: true }]
2026-07-12 13:00:02 +08:00
< / script >
< / head >
< body class = "vh-100 overflow-hidden bg-body-tertiary" > < div class = "container-fluid h-100 py-3 d-flex flex-column" >
< header class = "d-flex align-items-center gap-2 mb-3 flex-shrink-0" > < h1 class = "h4 mb-0" > components.Nav< / h1 > < span id = "status" class = "badge text-bg-secondary" > 未测试< / span > < button id = "runAll" class = "btn btn-primary btn-sm ms-auto" type = "button" > 测试全部< / button > < span id = "coverage" class = "small text-body-secondary" > < / span > < / header >
< main class = "flex-fill overflow-auto" > < Nav id = "horizontalNav" $ . state . brand = "navBrand" $ . state . list = "navItems" $ bind = "Hash.nav" > < / Nav > < div class = "mt-3 border" style = "height:220px" > < Nav id = "verticalNav" vertical $ . state . brand = "navBrand" $ . state . list = "navItems" $ bind = "Hash.nav" > < / Nav > < / div > < / main > < pre id = "result" class = "h-25 overflow-auto border rounded bg-body p-2 mt-3 mb-0 flex-shrink-0 small" > < / pre >
< / div > < script >
2026-07-13 22:24:23 +08:00
const cases = ['vertical', 'brand', 'list', 'value', 'Hash.nav', 'openName', 'label', 'button', 'dropdown', 'fill', 'change-event', 'nav-event', 'noselect-nav', 'item-shape']; const errors = []; const originalError = console.error; console.error = (...args) => { errors.push(args.map(String).join(' ')); originalError(...args) }; addEventListener('error', event => errors.push(String(event.message || event.error || event))); addEventListener('unhandledrejection', event => errors.push(String(event.reason || event))); const tick = () => new Promise(resolve => setTimeout(resolve, 60)); const assert = (r, feature, ok, message) => { r.assertions++; if (ok) r.passed++; else { r.failed++; r.failures.push({ feature, message }) } }
2026-07-12 13:00:02 +08:00
window.runTest = async () => { errors.length = 0; const r = { name: 'components.Nav', status: 'running', assertions: 0, passed: 0, failed: 0, failures: [], consoleErrors: [], coverage: { tested: [], total: cases, percent: 0 } }; await tick(); const horizontal = document.querySelector('#horizontalNav'); const vertical = document.querySelector('#verticalNav'); const navLink = (nav, label) => [...nav.querySelectorAll('.nav-link')].find(node => node.textContent.includes(label)); let change; const navEvents = []; horizontal.addEventListener('change', event => { change = event.detail }); horizontal.addEventListener('nav', event => navEvents.push(event.detail)); vertical.addEventListener('nav', event => navEvents.push(event.detail))
assert(r, 'vertical', vertical.vertical === true & & vertical.querySelector('.flex-column'), 'vertical attribute enables vertical navigation')
assert(r, 'brand', horizontal.textContent.includes('Apigo') & & horizontal.querySelector('.bi-box') & & vertical.textContent.includes('Apigo'), 'brand renders the same icon and label in both layouts')
assert(r, 'list', horizontal.querySelectorAll('.nav-link, .navbar-text').length >= 3, 'list renders navigation items')
assert(r, 'label', horizontal.textContent.includes('Main'), 'label item renders non-clickable text')
assert(r, 'fill', horizontal.querySelector('.flex-fill'), 'fill item renders a horizontal spacer')
navLink(horizontal, 'Home').click(); await tick(); assert(r, 'button', horizontal.state.value === 'home', 'button item selects its name'); assert(r, 'value', change === 'home', 'value is selected item name and change detail'); assert(r, 'Hash.nav', Hash.nav === 'home' & & location.hash.includes('nav=%22home%22') & & vertical.state.value === 'home', '$bind writes Hash.nav and synchronizes the same data across layouts'); assert(r, 'change-event', change === 'home', 'change event exposes selected item name')
navLink(vertical, 'Admin').click(); await tick(); assert(r, 'dropdown', vertical.textContent.includes('Manage') & & vertical.textContent.includes('Roles'), 'dropdown renders documented items field'); assert(r, 'openName', vertical.state.openName === 'admin', 'vertical dropdown stores its open name')
navLink(vertical, 'Roles').click(); await tick(); assert(r, 'item-shape', vertical.state.value === 'roles' & & navLink(vertical, 'Roles').classList.contains('active'), 'item shape name and items select an active subitem')
assert(r, 'nav-event', navEvents.some(detail => detail.item?.name === 'home') & & navEvents.some(detail => detail.item?.name === 'admin' & & detail.open === true), 'nav event exposes item and dropdown open detail')
2026-07-13 22:24:23 +08:00
const selectedBeforeLogout = horizontal.state.value; navLink(horizontal, 'Logout').click(); await tick(); assert(r, 'noselect-nav', horizontal.state.value === selectedBeforeLogout & & navEvents.some(detail => detail.name === 'logout' & & detail.selected === false), 'noselect action keeps selection and still dispatches nav')
2026-07-12 13:00:02 +08:00
r.coverage.tested = [...cases]; r.coverage.percent = 100; r.consoleErrors = [...errors]; r.status = r.failed || r.consoleErrors.length ? 'failed' : 'passed'; document.querySelector('#status').className = 'badge text-bg-' + (r.status === 'passed' ? 'success' : 'danger'); document.querySelector('#status').textContent = r.status === 'passed' ? '通过' : '失败'; document.querySelector('#coverage').textContent = '覆盖 100%'; document.querySelector('#result').textContent = JSON.stringify(r, null, 2); return r }; document.querySelector('#runAll').onclick = window.runTest
< / script > < / body > < / html >