const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const ts = require('typescript'); const vm = require('node:vm'); const sourceRoot = path.join(__dirname, '../src'); const cases = [ 'components/accounts/AccountManagementList.tsx', 'pages/accounts/$id/index.tsx', 'pages/accounts/$id/edit.tsx', 'pages/accounts/monitoring/$id/index.tsx', 'components/creator/WorksGallery.tsx', 'pages/gateways/index.tsx', 'pages/network-exits/index.tsx', 'pages/network-exits/$id/index.tsx', 'pages/accounts/import.tsx', ]; function attribute(opening, name) { return opening.attributes.properties.find( (prop) => ts.isJsxAttribute(prop) && prop.name.getText() === name, ); } function truncatedElements(file) { const source = ts.createSourceFile( file, fs.readFileSync(path.join(sourceRoot, file), 'utf8'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX, ); const elements = []; function visit(node) { if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) { const ellipsis = attribute(node, 'ellipsis'); if (ellipsis) elements.push({ source, opening: node, ellipsis }); } ts.forEachChild(node, visit); } visit(source); return elements; } for (const file of cases) { test(`${file}: every truncated field exposes its full content on hover`, () => { const elements = truncatedElements(file); assert.ok(elements.length > 0, 'The page must contain truncated fields'); for (const { source, opening, ellipsis } of elements) { const tag = opening.tagName.getText(source); if (tag === 'Typography.Link') { // Link only accepts boolean ellipsis, so its full URL needs a surrounding Tooltip. const wrapper = opening.parent.parent; assert.ok(ts.isJsxElement(wrapper), 'Truncated links need a tooltip wrapper'); assert.equal(wrapper.openingElement.tagName.getText(source), 'Tooltip'); const title = attribute(wrapper.openingElement, 'title'); assert.ok(title?.initializer && ts.isJsxExpression(title.initializer)); assert.equal(title.initializer.expression.getText(source), 'job.share_url'); assert.equal(attribute(opening, 'href').initializer.expression.getText(source), 'job.share_url'); assert.equal(attribute(opening, 'target').initializer.text, '_blank'); } else { assert.ok(ellipsis.initializer && ts.isJsxExpression(ellipsis.initializer), `${tag} needs tooltip configuration`); const config = ellipsis.initializer.expression; assert.ok(ts.isObjectLiteralExpression(config), `${tag} needs tooltip configuration`); const tooltip = config.properties.find( (prop) => ts.isPropertyAssignment(prop) && prop.name.getText(source) === 'tooltip', ); assert.ok(tooltip, `${tag} must show full content when truncated`); assert.equal(tooltip.initializer.kind, ts.SyntaxKind.TrueKeyword, `${tag} must use its complete original content`); } } }); } test('works cards preserve their two-line title and description limits', () => { const elements = truncatedElements('components/creator/WorksGallery.tsx'); const rows = elements.map(({ ellipsis }) => { const config = ellipsis.initializer.expression; return config.properties.find((prop) => prop.name.getText() === 'rows').initializer.text; }); assert.deepEqual(rows, ['2', '2']); }); function renderField(file, index, bindings) { const { source, opening } = truncatedElements(file).at(index); const { outputText } = ts.transpileModule( `const field = (${opening.parent.getText(source)});`, { compilerOptions: { jsx: ts.JsxEmit.ReactJSX, module: ts.ModuleKind.CommonJS } }, ); return vm.runInNewContext(`${outputText}\nfield;`, { require, exports: {}, Typography: { Text: 'span', Paragraph: 'p', Title: 'h5' }, ...bindings, }); } for (const [label, env, expected] of [ ['long name', { name: '完整运行环境名称'.repeat(30), alias: '别名' }, '完整运行环境名称'.repeat(30)], ['alias', { name: '', alias: '完整环境别名'.repeat(30) }, '完整环境别名'.repeat(30)], ['empty environment name', {}, '—'], ]) { test(`account environment hover preserves ${label}`, () => { const field = renderField('components/accounts/AccountManagementList.tsx', -1, { env }); assert.equal(field.props.children, expected); assert.equal(field.props.ellipsis.tooltip, true); }); } for (const visible of [false, true]) { test(`gateway token hover respects visible=${visible}`, () => { const token = 'complete-gateway-token'.repeat(20); const field = renderField('pages/gateways/index.tsx', 1, { token, visible }); assert.equal(field.props.children, visible ? token : '••••••••••••'); assert.equal(field.props.ellipsis.tooltip, true); }); } for (const [index, key] of [[0, 'title'], [1, 'body']]) { test(`work ${key} hover preserves the full multi-line content`, () => { const content = '完整内容第一行\n完整内容第二行\n'.repeat(30); const field = renderField('components/creator/WorksGallery.tsx', index, { work: { [key]: content } }); assert.equal(field.props.children, content); assert.equal(field.props.ellipsis.tooltip, true); assert.equal(field.props.ellipsis.rows, 2); }); } test('the regression checklist includes every ellipsis field in frontend source', () => { const files = fs.readdirSync(sourceRoot, { recursive: true }) .filter((file) => file.endsWith('.tsx') && truncatedElements(file).length > 0) .sort(); assert.deepEqual(files, [...cases].sort()); });