import { readFileSync, readdirSync } from 'node:fs' import { extname, join, relative } from 'node:path' import { describe, expect, it } from 'vitest' const modulesRoot = join(process.cwd(), 'src/modules') function vueFiles(directory: string): string[] { return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { const path = join(directory, entry.name) if (entry.isDirectory()) return vueFiles(path) return extname(entry.name) === '.vue' ? [path] : [] }) } function isPermanentlyReadOnly(tag: string): boolean { return ( /\s(?:disabled|readonly)(?=\s|\/>|>)/.test(tag) || /\s(?:disabled|readonly)=(?:""|'')/.test(tag) || /\s(?::|v-bind:)(?:disabled|readonly)=(?:"true"|'true')/.test(tag) ) } function hasMeaningfulPlaceholder(tag: string): boolean { const value = tag.match(/\s(?::|v-bind:)?placeholder\s*=\s*(["'])(.*?)\1/)?.[2] return Boolean(value?.trim()) } describe('正式业务模块输入提示合同', () => { it('所有可编辑 el-input 与 el-input-number 都提供 placeholder', () => { const violations: string[] = [] for (const file of vueFiles(modulesRoot)) { const source = readFileSync(file, 'utf8').replace(//g, (comment) => comment.replace(/[^\n]/g, ' '), ) const openingTag = /])*>/g for (const match of source.matchAll(openingTag)) { const tag = match[0] if (isPermanentlyReadOnly(tag)) continue if (hasMeaningfulPlaceholder(tag)) continue const line = source.slice(0, match.index).split('\n').length violations.push( `${relative(modulesRoot, file).replaceAll('\\', '/')}:${line} ${tag.replace(/\s+/g, ' ')}`, ) } } expect(violations, `缺少 placeholder:\n${violations.join('\n')}`).toEqual([]) }) })