import assert from 'node:assert/strict' import test from 'node:test' import { readFileSync } from 'node:fs' import { parse } from '@vue/compiler-sfc' import ts from 'typescript' import { computed, ref, watch } from 'vue' const source = parse( readFileSync(new URL('../src/views/m/pl/plan-cert/CertDetail.vue', import.meta.url), 'utf8') ).descriptor.scriptSetup!.content // Execute production navigation declarations and lifecycle registrations. Only geometry, // time and animation scheduling are controlled: no browser or mirrored scroll algorithm. function harness() { const start = source.indexOf(' const detailTabs =') const code = source.slice(start, source.indexOf(' const showFeedback =', start)) const mounts: Array<() => void> = [], unmounts: Array<() => void> = [] const listeners = new Map void>(), frames = new Map void>() const timers = new Map void }>() const scrolls: any[] = [] let timerId = 0 let frameId = 0, now = 1000, headerHeight = 66 const state: Record = { disposed: false, recordInvalidated: false } const plan = ref({ id: 'A' }), loading = ref(false), props = { planId: 'A' } const tops: Record = { 'cert-sec-base': 122, 'cert-sec-transport': 700, 'cert-sec-attachments': 1300 } const viewport = { scrollY: 0, innerHeight: 600, addEventListener: (name: string, fn: () => void) => listeners.set(name, fn), removeEventListener: (name: string, fn: () => void) => { assert.equal(listeners.get(name), fn) listeners.delete(name) }, requestAnimationFrame: (fn: () => void) => { frames.set(++frameId, fn) return frameId }, cancelAnimationFrame: (id: number) => frames.delete(id), setTimeout: (callback: () => void, delay: number) => { timers.set(++timerId, { due: now + delay, callback }) return timerId }, clearTimeout: (id: number) => timers.delete(id) } const document = { scrollingElement: { scrollHeight: 1600 }, querySelector: () => ({ getBoundingClientRect: () => ({ height: headerHeight }) }), getElementById: (id: string) => id in tops ? { getBoundingClientRect: () => ({ top: tops[id] - viewport.scrollY }), scrollIntoView: (options: any) => scrolls.push({ id, options }) } : null } const bindings = { ref, computed, watch, stops: ref({ deliveries: [] }), plan, props, loading, window: viewport, document, Date: { now: () => now }, onMounted: (fn: () => void) => mounts.push(fn), onUnmounted: (fn: () => void) => unmounts.push(fn) } const execute = new Function( 'state', ...Object.keys(bindings), `with (state) { ${ts.transpile(code, { target: ts.ScriptTarget.ESNext })} return { activeTab, stickyOffset, onAnchorTab, sync: typeof syncActiveTabFromScroll === 'function' ? syncActiveTabFromScroll : () => {}, margin: () => typeof scrollMargin !== 'undefined' ? scrollMargin.value : undefined } }` ) const nav = execute(state, ...Object.values(bindings)) return { ...nav, state, plan, props, loading, viewport, document, tops, scrolls, listeners, frames, timers, mount: () => mounts.forEach((fn) => fn()), unmount: () => unmounts.forEach((fn) => fn()), tick: (ms: number) => { now += ms for (const [id, timer] of [...timers]) { if (timer.due > now) continue timers.delete(id) timer.callback() } }, resize: (height: number) => { headerHeight = height listeners.get('resize')?.() }, flushFrame: () => { const pending = [...frames.values()] frames.clear() pending.forEach((fn) => fn()) } } } test('manual scroll selects transport and Home restores base', () => { const h = harness() h.mount() h.viewport.scrollY = 650 h.sync() assert.equal(h.activeTab.value, 'transport') h.viewport.scrollY = 0 h.sync() assert.equal(h.activeTab.value, 'base') }) test('bottom selects visible short final section', () => { const h = harness() h.mount() h.viewport.scrollY = 1000 h.sync() assert.equal(h.activeTab.value, 'attachments') }) for (const missing of [false, true]) test(missing ? 'missing last section uses top probe' : 'short unscrolled page keeps base', () => { const h = harness() h.mount() if (missing) { delete h.tops['cert-sec-attachments'] h.viewport.scrollY = 1000 } else { h.document.scrollingElement.scrollHeight = 500 h.tops['cert-sec-transport'] = 250 h.tops['cert-sec-attachments'] = 400 } h.sync() assert.equal(h.activeTab.value, missing ? 'transport' : 'base') }) test('final section below viewport does not win bottom override', () => { const h = harness() h.mount() h.viewport.scrollY = 1000 h.tops['cert-sec-attachments'] = 1800 h.sync() assert.equal(h.activeTab.value, 'transport') }) test('anchor click locks selected tab for 700ms and uses smooth positioning', () => { const h = harness() h.mount() h.onAnchorTab({ name: 'attachments' }) assert.equal(h.activeTab.value, 'attachments') assert.deepEqual(h.scrolls, [ { id: 'cert-sec-attachments', options: { behavior: 'smooth', block: 'start' } } ]) h.tick(699) h.sync() assert.equal(h.activeTab.value, 'attachments') h.tick(1) h.sync() assert.equal(h.activeTab.value, 'base') }) for (const invalid of ['disposed', 'recordInvalidated', 'loading', 'mismatch', 'empty']) test(`${invalid} record never reads stale anchors or starts scrolling`, () => { const h = harness() h.mount() if (invalid === 'loading') h.loading.value = true else if (invalid === 'mismatch') h.props.planId = 'B' else if (invalid === 'empty') h.plan.value = null else h.state[invalid] = true h.document.getElementById = () => { assert.fail('stale DOM must not be read') } h.viewport.scrollY = 1000 h.sync() h.onAnchorTab({ name: 'attachments' }) assert.equal(h.activeTab.value, 'base') assert.deepEqual(h.scrolls, []) }) test('unknown or absent anchor cannot change active tab', () => { const h = harness() h.mount() h.onAnchorTab({ name: 'missing' }) delete h.tops['cert-sec-attachments'] h.onAnchorTab({ name: 'attachments' }) assert.equal(h.activeTab.value, 'base') assert.deepEqual(h.scrolls, []) }) test('mount measures header; resize recomputes header + nav + gap', () => { const h = harness() h.mount() assert.equal(h.stickyOffset.value, 66) assert.equal(h.margin(), 122) h.resize(78.3) assert.equal(h.stickyOffset.value, 79) assert.equal(h.margin(), 135) }) test('scroll lifecycle coalesces frames and removes listeners and pending frame on unmount', () => { const h = harness() h.mount() assert.ok(h.listeners.has('scroll')) h.viewport.scrollY = 650 h.listeners.get('scroll')!() h.listeners.get('scroll')!() assert.equal(h.frames.size, 1) h.flushFrame() assert.equal(h.activeTab.value, 'transport') h.listeners.get('scroll')!() h.unmount() assert.equal(h.frames.size, 0) assert.equal(h.listeners.size, 0) }) test('Home during click lock resyncs once lock expires without another scroll event', () => { const h = harness() h.mount() h.onAnchorTab({ name: 'attachments' }) h.viewport.scrollY = 1000 h.tick(400) h.viewport.scrollY = 0 h.listeners.get('scroll')!() h.flushFrame() assert.equal(h.activeTab.value, 'attachments') h.tick(300) assert.equal(h.activeTab.value, 'base') assert.equal(h.timers.size, 0) }) test('consecutive anchor clicks replace the previous lock-expiry timer', () => { const h = harness() h.mount() h.onAnchorTab({ name: 'attachments' }) h.tick(400) h.onAnchorTab({ name: 'transport' }) assert.equal(h.timers.size, 1) h.tick(300) assert.equal(h.activeTab.value, 'transport') assert.equal(h.timers.size, 1) h.tick(400) assert.equal(h.activeTab.value, 'base') assert.equal(h.timers.size, 0) }) test('unmount cancels pending lock-expiry timer', () => { const h = harness() h.mount() h.onAnchorTab({ name: 'attachments' }) assert.equal(h.timers.size, 1) h.unmount() assert.equal(h.timers.size, 0) h.document.getElementById = () => { assert.fail('unmounted timer must not read DOM') } h.tick(700) assert.equal(h.activeTab.value, 'attachments') }) test('lock expiry after record invalidation does not read stale sections', () => { const h = harness() h.mount() h.onAnchorTab({ name: 'attachments' }) assert.equal(h.timers.size, 1) h.state.recordInvalidated = true h.document.getElementById = () => { assert.fail('invalidated timer must not read DOM') } h.tick(700) assert.equal(h.activeTab.value, 'attachments') assert.equal(h.timers.size, 0) })