137 lines
5.6 KiB
JavaScript
137 lines
5.6 KiB
JavaScript
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());
|
|
});
|