Files
creator-hub/web/tests/ellipsis-tooltip.test.cjs
T

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());
});