import assert from 'node:assert/strict' import { resolve } from 'node:path' import tsParser from '@typescript-eslint/parser' import { Linter } from 'eslint' import { it } from 'vitest' import rule from './require-title-for-truncated-text.js' import './fixtures/truncation.module.css' const plugin = { rules: { 'require-title-for-truncated-text': rule, }, } function verifyAndFix(code, filename = 'test.tsx') { const linter = new Linter({ cwd: resolve('..') }) return linter.verifyAndFix( code, [ { files: ['**/*.tsx'], languageOptions: { parser: tsParser, parserOptions: { ecmaFeatures: { jsx: true }, ecmaVersion: 'latest', sourceType: 'module', }, }, plugins: { dify: plugin }, rules: { 'dify/require-title-for-truncated-text': 'warn' }, }, ], { filename }, ) } it('accepts an explicit title and line-clamp-none', () => { const result = verifyAndFix(` const name = 'Dify' export const Example = () => <> {name} {name} `) assert.equal(result.messages.length, 0) assert.equal(result.fixed, false) }) it('accepts Dify UI TooltipTrigger children and render elements', () => { const result = verifyAndFix(` import { TooltipTrigger as Trigger } from '@langgenius/dify-ui/tooltip' const name = 'Dify' export const Example = () => <> {name} {name}} /> `) assert.equal(result.messages.length, 0) assert.equal(result.fixed, false) }) it('reports static classes, class helpers, constants, and inline styles without fixing', () => { const code = ` const truncatedClassName = cn('min-w-0', condition && 'md:truncate') export const Example = ({ itemName, label }) => <> {itemName}

{label}

Description
` const result = verifyAndFix(code) assert.equal(result.fixed, false) assert.equal(result.messages.length, 3) assert.ok(result.messages.every((message) => message.severity === 1)) assert.equal(result.output, code) }) it('resolves truncation variables from the active lexical scope', () => { const shadowedParameterCode = ` const className = 'truncate' const style = { textOverflow: 'ellipsis' } export const Example = ({ className, style, label }) => <> {label} {label} ` const shadowedParameterResult = verifyAndFix(shadowedParameterCode) assert.equal(shadowedParameterResult.fixed, false) assert.equal(shadowedParameterResult.messages.length, 0) assert.equal(shadowedParameterResult.output, shadowedParameterCode) const nestedBindingResult = verifyAndFix(` const className = 'regular' const style = { color: 'red' } export const Example = ({ label }) => { const className = 'truncate' const style = { textOverflow: 'ellipsis' } return <> {label} {label} } `) assert.equal(nestedBindingResult.fixed, false) assert.equal(nestedBindingResult.messages.length, 2) assert.ok(nestedBindingResult.messages.every((message) => message.severity === 1)) }) it('reports an empty title without replacing it', () => { const code = ` export const Example = ({ name }) => {name} ` const result = verifyAndFix(code) assert.equal(result.fixed, false) assert.equal(result.messages.length, 1) assert.equal(result.messages[0].severity, 1) assert.equal(result.messages[0].messageId, 'emptyTitle') assert.equal(result.output, code) }) it('reports custom components without fixing them', () => { const code = ` export const Example = ({ name }) => ( {name} ) ` const result = verifyAndFix(code) assert.equal(result.fixed, false) assert.equal(result.messages.length, 1) assert.equal(result.messages[0].severity, 1) assert.equal(result.output, code) }) it('ignores expressions that may execute user code', () => { const code = ` export const Example = ({ item }) => { let index = 0 return <> {item.name} {getLabel()} {index++} } ` const result = verifyAndFix(code) assert.equal(result.fixed, false) assert.equal(result.messages.length, 0) assert.equal(result.output, code) }) it('reports a single nested text child or a text-bearing prop without fixing', () => { const result = verifyAndFix(` export const Example = ({ name, content }) => <>
{name}
`) assert.equal(result.fixed, false) assert.equal(result.messages.length, 2) assert.ok(result.messages.every((message) => message.severity === 1)) }) it('checks Dify UI single text but ignores multiple text children', () => { const filename = resolve('../packages/dify-ui/src/example.tsx') const result = verifyAndFix( ` export const Example = ({ primary, secondary }) => <> {primary} {primary}{secondary}
{primary} {secondary}
`, filename, ) assert.equal(result.fixed, false) assert.equal(result.messages.length, 1) assert.equal(result.messages[0].severity, 1) }) it('does not count non-text conditional children as additional text', () => { const result = verifyAndFix(` export const Example = ({ primary, showOverlay }) => ( {primary} {showOverlay && } ) `) assert.equal(result.fixed, false) assert.equal(result.messages.length, 1) assert.equal(result.messages[0].messageId, 'missingTitle') }) it('resolves truncation classes imported from CSS modules', () => { const filename = resolve('plugins/eslint/rules/fixtures/example.tsx') const result = verifyAndFix( ` import styles from './truncation.module.css' export const Example = ({ name }) => <> {name} {name} {name} {name} {name} {name} export const Shadowed = ({ styles, name }) => ( {name} ) `, filename, ) assert.equal(result.fixed, false) assert.equal(result.messages.length, 2) assert.ok(result.messages.every((message) => message.severity === 1)) }) it('reports dynamic truncation class and style expressions without fixing', () => { const code = ` const regularClassName = 'regular' const dynamicClassNames = ['regular'] const classMap = { regular: true } const regularStyle = { color: 'red' } const styles = [regularStyle] const tag = strings => strings[0] export const Example = ({ condition, name }) => <> {name} {name} {name} {name} {name} {name} {name} {name} {name} {name} {name} {name} {name} {name} ` const result = verifyAndFix(code) assert.equal(result.fixed, false) assert.equal(result.messages.length, 11) assert.ok(result.messages.every((message) => message.messageId === 'missingTitle')) assert.equal(result.output, code) }) it('reports statically empty title forms without replacing them', () => { const code = ` export const Example = ({ name }) => <> {name} {name} {name} {name} {name} ` const result = verifyAndFix(code) assert.equal(result.fixed, false) assert.equal(result.messages.length, 5) assert.ok(result.messages.every((message) => message.messageId === 'emptyTitle')) assert.equal(result.output, code) }) it('reports safe literal content but accepts a titled single child', () => { const result = verifyAndFix(` export const Example = ({ name }) => <> {42} {\`Dify\`} {name}
{name}
`) assert.equal(result.fixed, false) assert.equal(result.messages.length, 3) assert.deepEqual( result.messages.map((message) => message.messageId), ['missingTitle', 'missingTitle', 'emptyTitle'], ) }) it('accepts referenced TooltipTrigger render candidates', () => { const result = verifyAndFix(` import { TooltipTrigger as Trigger } from '@langgenius/dify-ui/tooltip' export const Example = ({ condition, name }) => { const primaryTrigger = {name} const fallbackTrigger = {name} return } `) assert.equal(result.fixed, false) assert.equal(result.messages.length, 0) })