const assert = require('node:assert/strict'); const { readFileSync } = require('node:fs'); const { test } = require('node:test'); const vm = require('node:vm'); const ts = require('typescript'); // 页面纵向滚动 + sticky 表头吸顶 + 横向滚动 + 固定列 + 分页。 const files = [ ['src/components/accounts/MonitoringAccountList.tsx', 1100], ['src/components/accounts/AccountManagementList.tsx', 'max-content'], ['src/pages/accounts/import.tsx', 1000], ['src/pages/network-exits/index.tsx', 1100], ['src/pages/gateways/index.tsx', 960], ]; for (const [file, width] of files) { const source = ts.createSourceFile( file, readFileSync(`${__dirname}/../${file}`, 'utf8'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX, ); const code = source.getFullText(); const tables = []; function visit(node) { if ((ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) && node.tagName.getText(source) === 'Table') tables.push(node); ts.forEachChild(node, visit); } visit(source); test(`${file} 保留表头吸顶、分页和固定列`, () => { assert.equal(tables.length, 1); const props = tables[0].attributes.properties; const sticky = props.find(prop => prop.name?.getText(source) === 'sticky'); assert.ok(sticky?.initializer && ts.isJsxExpression(sticky.initializer), '表头吸顶必须预留顶部栏高度'); assert.equal(sticky.initializer.expression.getText(source), 'sticky'); assert.match(code, /const sticky = useTableSticky\(\);/); const pagination = props.find(prop => prop.name?.getText(source) === 'pagination'); assert.ok(pagination?.initializer && ts.isJsxExpression(pagination.initializer)); const config = pagination.initializer.expression; assert.ok( (ts.isIdentifier(config) && code.includes(`const ${config.text} = useTablePagination()`)) || (ts.isCallExpression(config) && config.expression.getText(source) === 'tablePagination') || (ts.isObjectLiteralExpression(config) && config.properties.some(prop => ts.isSpreadAssignment(prop) && prop.expression.getText(source) === 'tablePagination()', )), '分页必须使用或扩展统一分页配置', ); assert.doesNotMatch(code, /pagination=\{false\}/); assert.match(code, /\.\.\.fixedLeft { const scroll = tables[0].attributes.properties.find(prop => prop.name?.getText(source) === 'scroll'); assert.ok(scroll?.initializer && ts.isJsxExpression(scroll.initializer)); const expression = scroll.initializer.expression.getText(source); for (const horizontal of [false, true]) { for (const height of [480, 1080]) { for (const rowCount of [0, 1, 20, 100]) { const result = vm.runInNewContext(`(${expression})`, { horizontal, vertical: rowCount * 48 + 120 >= height - 300 || horizontal, tableScroll: (x, y) => ({ x, y }), }); assert.equal(Object.hasOwn(result, 'y'), false, `不应配置 scroll.y(行数 ${rowCount},高度 ${height})`); assert.equal(result.x, width === 'max-content' ? width : horizontal ? width : undefined); } } } assert.doesNotMatch(code, /window\.innerHeight|useOverflowGrid|tableScroll/, '不再保留按高度和行数启用表内滚动的旧路径'); if (width !== 'max-content') assert.match(code, /!Grid\.useBreakpoint\(\)\.xl/, '横向断点保持 xl'); }); } const tableFile = `${__dirname}/../src/utils/table.ts`; const tableCode = ts.transpileModule(readFileSync(tableFile, 'utf8'), { fileName: tableFile, compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, }).outputText; const tableModule = { exports: {} }; const routeContext = {}, proProvider = {}; let layout = {}, token = {}; vm.runInNewContext(tableCode, { exports: tableModule.exports, require: id => { if (id === '@ant-design/pro-components') return { RouteContext: routeContext, ProProvider: proProvider }; if (id === 'react') return { useContext: context => context === routeContext ? layout : { token } }; if (id === '@/hooks/usePageState') return {}; throw new Error(`Unexpected import: ${id}`); } }, { filename: tableFile }); const helpers = tableModule.exports; test('mix 布局默认固定顶部栏,表头须避让;自定义高度随布局更新', () => { for (const height of [56, 72, 96]) { for (const isMobile of [false, true]) { layout = { hasHeader: true, layout: 'mix', fixedHeader: false, isMobile }; token = { layout: { header: { heightLayoutHeader: height } } }; assert.equal(helpers.useTableSticky().offsetHeader, height); } } }); test('遵循 ProLayout 默认顶部栏高度', () => { layout = { hasHeader: true, layout: 'mix' }; token = {}; assert.equal(helpers.useTableSticky().offsetHeader, 56); }); test('其他布局只在存在固定顶部栏时预留高度', () => { token = { layout: { header: { heightLayoutHeader: 72 } } }; for (const kind of ['top', 'side']) { for (const hasHeader of [false, true]) { for (const fixedHeader of [false, true]) { layout = { layout: kind, hasHeader, fixedHeader }; assert.equal(helpers.useTableSticky().offsetHeader, hasHeader && fixedHeader ? 72 : 0); } } } layout = { layout: 'mix', hasHeader: false, fixedHeader: true }; assert.equal(helpers.useTableSticky().offsetHeader, 0); }); test('分页仍为每页 20 条,单页隐藏分页条', () => { assert.equal(helpers.TABLE_PAGE_SIZE, 20); const pagination = helpers.tablePagination(); assert.equal(pagination.pageSize, 20); assert.equal(pagination.hideOnSinglePage, true); }); test('固定列保留原配置且不修改输入', () => { const column = { title: '账号', width: 240 }; for (const [helper, side] of [[helpers.fixedLeft, 'left'], [helpers.fixedRight, 'right']]) { const fixed = helper(column); assert.equal(fixed.fixed, side); assert.equal(fixed.title, column.title); assert.equal(fixed.width, column.width); assert.notEqual(fixed, column); assert.equal(Object.hasOwn(column, 'fixed'), false); } }); test('移除纵向滚动判断与无必要的滚动包装函数', () => { assert.equal(helpers.useOverflowGrid, undefined); assert.equal(helpers.tableScroll, undefined); });