// 探针:真实载荷(GAS 699 条同码)下的分页正确性 —— 逐页读数必须等于 domain 行读数。 import { readFileSync } from 'node:fs' import { registerHooks } from 'node:module' import { parse, compileScript, compileTemplate } from '@vue/compiler-sfc' import { createRenderer, h, ref, nextTick } from '@vue/runtime-core' const root = new URL('../../../../../src/modules/cockpit/', import.meta.url) registerHooks({ load(url, context, nextLoad) { if (url.endsWith('.json')) return { format: 'module', shortCircuit: true, source: `export default ${readFileSync(new URL(url), 'utf8')}` } if (url.endsWith('.vue')) { const { descriptor } = parse(readFileSync(new URL(url), 'utf8'), { filename: new URL(url).pathname }) const script = compileScript(descriptor, { id: 'probe', genDefaultAs: '__sfc__' }) const template = compileTemplate({ source: descriptor.template.content, filename: new URL(url).pathname, id: 'probe', compilerOptions: { bindingMetadata: script.bindings }, }) return { format: 'module', shortCircuit: true, source: `${script.content}\n${template.code}\n__sfc__.render = render; export default __sfc__;` } } return nextLoad(url, context) }, }) const nodeOps = { createElement: (tag) => ({ tag, props: {}, children: [], parent: null, text: '', style: {}, addEventListener() {}, removeEventListener() {} }), createText: (text) => ({ tag: '#text', text, children: [], parent: null, props: {}, style: {}, addEventListener() {}, removeEventListener() {} }), createComment: (text) => ({ tag: '#comment', text, children: [], parent: null, props: {}, style: {}, addEventListener() {}, removeEventListener() {} }), setText: (n, t) => { n.text = t }, setElementText: (n, t) => { n.text = t; n.children = [] }, insert: (child, parent, anchor) => { if (child.parent) child.parent.children = child.parent.children.filter((c) => c !== child) child.parent = parent if (!parent) return if (anchor) { const i = parent.children.indexOf(anchor); parent.children.splice(i < 0 ? parent.children.length : i, 0, child) } else parent.children.push(child) }, remove: (c) => { if (c.parent) c.parent.children = c.parent.children.filter((x) => x !== c); c.parent = null }, parentNode: (n) => n.parent, nextSibling: (n) => (n.parent ? n.parent.children[n.parent.children.indexOf(n) + 1] || null : null), querySelector: () => null, setScopeId: () => {}, patchProp: (el, k, _p, next) => { el.props[k] = next }, } const { createApp } = createRenderer(nodeOps) const { default: PanelBlocks } = await import(new URL('components/PanelBlocks.vue', root)) const { operationPanel, toOperatingInformation } = await import(new URL('domain/infrastructurePanels.js', root)) const { section } = await import(new URL('domain/sectionState.js', root)) function findAll(node, predicate, acc = []) { if (!node) return acc if (predicate(node)) acc.push(node) for (const c of node.children || []) findAll(c, predicate, acc) return acc } function textOf(node) { if (!node) return '' // 元素若被 setElementText 直接落文本(Vue 对纯文本元素走这条路),文本在 node.text 上。 const own = node.tag === '#text' ? node.text : (typeof node.text === 'string' ? node.text : '') + (typeof node.props?.text === 'string' ? node.props.text : '') return own + (node.children || []).map(textOf).join('') } const raw = JSON.parse(readFileSync(new URL('../t51/oi-GAS.json', import.meta.url), 'utf8')).data const data = toOperatingInformation(raw) const view = operationPanel(data, section('success', { data, loaded: true }), { scope: 'all' }) const rows = view.blocks[1].rows console.log('row keys distinct:', new Set(rows.map((r) => r.key)).size, '/', rows.length) const viewRef = ref(view) const app = createApp({ render: () => h(PanelBlocks, { view: viewRef.value }) }) app.config.warnHandler = () => {} const container = nodeOps.createElement('#root') app.mount(container) function rendered() { return findAll(container, (n) => n.props && 'data-item' in n.props).map((card) => ({ key: card.props['data-item'], title: textOf(findAll(card, (n) => n.props && 'class' in n.props && String(n.props['class']).includes('panel-item__title'))[0]), meta: textOf(findAll(card, (n) => n.props && 'class' in n.props && String(n.props['class']).includes('panel-item__meta'))[0]), })) } let bad = 0 let pagesChecked = 0 for (let page = 1; page <= 70; page += 1) { const btn = findAll(container, (n) => String(n.props?.['data-page-number']) === String(page))[0] if (!btn) { console.log('page button missing', page); bad += 1; continue } btn.props.onClick() await nextTick() const got = rendered() const want = rows.slice((page - 1) * 10, page * 10) pagesChecked += 1 if (got.length !== want.length) { console.log(`page ${page}: rendered ${got.length} want ${want.length}`); bad += 1; continue } for (let i = 0; i < want.length; i += 1) { if (got[i].key !== want[i].key || got[i].meta !== want[i].meta.join(' · ') || got[i].title !== want[i].title) { console.log(`page ${page} row ${i} mismatch:\n got ${JSON.stringify(got[i])}\n want ${JSON.stringify({ key: want[i].key, title: want[i].title, meta: want[i].meta.join(' · ') })}`) bad += 1 break } } } console.log(`pages checked: ${pagesChecked}, mismatches: ${bad}`) // 计数行 + 到边禁用 const state = findAll(container, (n) => n.props && 'data-page-state' in n.props)[0] console.log('state on last page:', textOf(state)) const prev = findAll(container, (n) => 'data-page-prev' in (n.props || {}))[0] const next = findAll(container, (n) => 'data-page-next' in (n.props || {}))[0] console.log('prev disabled:', prev?.props.disabled, 'next disabled:', next?.props.disabled)