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