/** * 票 59 离线闸门:统一分页批次一 —— 共享分页基建(pagination.js + PagerNav.vue)与设备详情三清单。 * * 五组断言(都不启浏览器、不联网,遵循 t46 成例): * P. 单源:PAGE_SIZE=10;equipmentDetailView 的 pagedRows/HISTORY_PAGE_SIZE 是**兼容再导出** * (同一函数引用),组件里不再有两份翻页算术。 * W. 页码窗口纯函数:≤7 全列;>7 收缩「1 … c−1 c c+1 … Y」恒含首末与省略号;非法输入不抛错。 * J. 跳页夹回纯函数:非数字/0/超 pageCount 一律夹回 1..pageCount,不报错(规范「跳页」节)。 * S. PagerNav 静态守卫:一页不渲染、到边禁用保占位、aria-current="page"、 * 跳页 aria-label/placeholder、**唯一 @page 出口**(不新增第二套页码状态)、断言面 data-* 齐全。 * C. 消费方守卫:EquipmentDetailSections 三清单(预警/报警/工单)都走 pagedOf 窗口行 + * 共享 PagerNav;组件零第二份翻页算术;既有 data-pager/data-page-prev/next/state 面迁移保留。 * K. SFC 真实编译:两份 .vue 用 @vue/compiler-sfc 编译过(t41 教训:拼错的绑定与坏语法只有加载层判得出)。 * * 守卫有牙(变异见 verify/t59-mutations.mjs): * 窗口全列阈值收缩/省略号丢失/夹回失效/单页画控件/第二套出口/aria 丢失/组件重写算术/兼容导出改名 → 对应断言转红。 * * 运行:node .scratch/cockpit-real-data-wayfinding/verify/t59-shared-pager.mjs */ import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { parse, compileScript, compileTemplate } from '@vue/compiler-sfc' import { PAGE_ELLIPSIS, PAGE_SIZE, clampPage, pageWindow, pagedRows as pagedRowsCore } from '../../../src/modules/cockpit/domain/pagination.js' import { HISTORY_PAGE_SIZE, pagedRows as pagedRowsCompat } from '../../../src/modules/cockpit/domain/equipmentDetailView.js' const read = (rel) => readFileSync(new URL(`../../../src/modules/cockpit/${rel}`, import.meta.url), 'utf8') const stripComments = (text) => text.replace(//g, '').replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1') let passed = 0 let failed = 0 function check(name, condition, message) { if (condition) { passed += 1 console.log(`PASS ${name}`) } else { failed += 1 console.error(`FAIL ${name}: ${message}`) } } // ================================================================ P. 单源与兼容导出 { // P1 页大小单源 = 10(条文 1);旧名 HISTORY_PAGE_SIZE 与 PAGE_SIZE 同值。 check('P1-page-size-ten', PAGE_SIZE === 10 && HISTORY_PAGE_SIZE === PAGE_SIZE && PAGE_ELLIPSIS === '…', `PAGE_SIZE=${PAGE_SIZE} HISTORY=${HISTORY_PAGE_SIZE}`) // P2 兼容是**同一函数引用**,不是复制粘贴的第二份实现(条文 11 的判据形状)。 check('P2-compat-export-is-same-function', pagedRowsCompat === pagedRowsCore, 'equipmentDetailView.pagedRows 必须是 pagination.pagedRows 的再导出(同一引用)') // P3 旧模块里不许再留有本地实现体(复制粘贴升格=将来必漂移)。 const viewCode = read('domain/equipmentDetailView.js') check('P3-no-second-implementation', !/function pagedRows\(/.test(viewCode) && /from '\.\/pagination\.js'/.test(viewCode), 'equipmentDetailView 只再导出,不再自己实现 pagedRows') // P4 脏输入不抛错(非数组、NaN 页码、非法页大小全部兜底)。 const dirty = pagedRowsCore(null, NaN) check('P4-dirty-input-safe', dirty.page === 1 && dirty.pageCount === 1 && dirty.rows.length === 0, JSON.stringify(dirty)) // P5 越界页码夹回最后一页(条文 6:数据变少导致的越界由分页函数夹回,不画空表)。 const many = pagedRowsCore(Array.from({ length: 23 }, (_, index) => ({ code: `W${index}` })), 99) check('P5-out-of-range-clamps-to-last', many.page === 3 && many.rows.length === 3 && many.pageCount === 3, JSON.stringify({ page: many.page, n: many.rows.length })) } // ================================================================ W. 页码窗口(条文 2) { // W1 少于 7 页全列,不带省略号。 check('W1-five-pages-full', pageWindow(3, 5).join(',') === '1,2,3,4,5', pageWindow(3, 5).join(',')) // W2 恰好 7 页仍全列(阈值边界)。 check('W2-seven-pages-still-full', pageWindow(4, 7).join(',') === '1,2,3,4,5,6,7', pageWindow(4, 7).join(',')) // W3 8 页起步即收缩:恒含首末 + 当前±1 + 省略号。 check('W3-eight-pages-shrinks', pageWindow(5, 8).join(',') === '1,…,4,5,6,…,8', pageWindow(5, 8).join(',')) // W4 中段窗口:10 页看第 5 页 →「1 … 4 5 6 … 10」。 check('W4-middle-window', pageWindow(5, 10).join(',') === '1,…,4,5,6,…,10', pageWindow(5, 10).join(',')) // W5 贴左:当前页邻到首页时左侧不出现假省略号(1,2,3 … 10)。 check('W5-left-edge-no-fake-ellipsis', pageWindow(2, 10).join(',') === '1,2,3,…,10', pageWindow(2, 10).join(',')) // W6 贴右:对称(1 … 8,9,10)。 check('W6-right-edge', pageWindow(9, 10).join(',') === '1,…,8,9,10', pageWindow(9, 10).join(',')) // W7 恒含首末页(条文 2「恒含首末」),任意当前页成立。 const alwaysFirstLast = [1, 3, 5, 8, 15].every((c) => { const items = pageWindow(c, 20) return items[0] === 1 && items[items.length - 1] === 20 && items.includes(Math.min(c, 20)) }) check('W7-first-last-current-always', alwaysFirstLast, '每个窗口都要含首页、末页与当前页') // W8 数字升序且无重复(省略号除外)——渲染层据此直接出按钮。 const nums = pageWindow(6, 12).filter((item) => item !== PAGE_ELLIPSIS) check('W8-ascending-unique', nums.every((n, i) => i === 0 || n > nums[i - 1]), nums.join(',')) // W9 非法输入不抛错:脏页码/脏总数/超界页码都回合法窗口。 let threw = false try { const zero = pageWindow(0, 0) const junk = pageWindow('x', undefined) const over = pageWindow(999, 10) threw = !(zero.length === 1 && junk.length === 1 && over[over.length - 1] === 10 && over.includes(10)) } catch { threw = true } check('W9-illegal-input-no-throw', !threw, '脏输入必须按最小安全形状回,不许抛') } // ================================================================ J. 跳页夹回(规范「跳页」节) { // J1 合法页照收。 check('J1-valid-page', clampPage(5, 10) === 5 && clampPage('7', 10) === 7, `${clampPage(5, 10)}/${clampPage('7', 10)}`) // J2 0/负数夹到 1,超 pageCount 夹到 pageCount(不报错、不越界)。 check('J2-clamp-to-range', clampPage(0, 10) === 1 && clampPage(-4, 10) === 1 && clampPage(99, 10) === 10 && clampPage('100', 3) === 3, `${clampPage(0, 10)}/${clampPage(99, 10)}`) // J3 非数字回 1,**绝不回 NaN**(NaN 会污染页码状态并把控件画成「第 NaN 页」)。 check('J3-non-numeric-clamps-to-one', clampPage('abc', 10) === 1 && clampPage('', 10) === 1 && clampPage(null, 10) === 1 && clampPage(undefined, 10) === 1, `abc→${clampPage('abc', 10)}`) // J4 小数截断、空白容错(输入框里粘进来的值)。 check('J4-truncate-and-trim', clampPage(2.7, 10) === 2 && clampPage(' 4 ', 10) === 4, `${clampPage(2.7, 10)}/${clampPage(' 4 ', 10)}`) // J5 pageCount 本身脏时兜底为 1(宁可不跳,不跳进非法页)。 check('J5-dirty-pagecount-safe', clampPage(5, 0) === 1 && clampPage(5, NaN) === 1, `pageCount=0→${clampPage(5, 0)}`) } // ================================================================ S. PagerNav 静态守卫 { const pager = stripComments(read('components/PagerNav.vue')) // S1 总页数 ≤1 整块不渲染(条文 4 + 跳页节第 4 条),门控在 visible 计算属性。 check( 'S1-one-page-renders-nothing', /const visible = computed\(\(\) => props\.pageCount > 1\)/.test(pager) && /