2026-09-03 19:25:53 +08:00
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import { readdirSync, readFileSync } from 'node:fs'
|
|
|
|
|
import path from 'node:path'
|
|
|
|
|
import test from 'node:test'
|
|
|
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
|
|
|
|
|
|
const sourceRoot = path.dirname(fileURLToPath(import.meta.url))
|
|
|
|
|
|
|
|
|
|
function collectVueFiles(directory) {
|
|
|
|
|
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
|
|
|
const target = path.join(directory, entry.name)
|
|
|
|
|
return entry.isDirectory() ? collectVueFiles(target) : entry.name.endsWith('.vue') ? [target] : []
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function lineNumber(source, offset) {
|
|
|
|
|
return source.slice(0, offset).split('\n').length
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hasLiteralAttribute(tag, name) {
|
|
|
|
|
return new RegExp(`\\s${name}(?:\\s|=|/?>)`).test(tag)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isEditableNativeTextInput(tag) {
|
|
|
|
|
if (hasLiteralAttribute(tag, 'disabled') || hasLiteralAttribute(tag, 'readonly')) return false
|
|
|
|
|
const type = tag.match(/\stype\s*=\s*["']([^"']+)["']/)?.[1]?.toLowerCase() ?? 'text'
|
|
|
|
|
return ['text', 'search', 'email', 'tel', 'url', 'password'].includes(type)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('all editable text inputs provide a user-facing placeholder', () => {
|
|
|
|
|
const violations = []
|
|
|
|
|
|
|
|
|
|
for (const file of collectVueFiles(sourceRoot)) {
|
|
|
|
|
const source = readFileSync(file, 'utf8')
|
|
|
|
|
const template = source.match(/<template(?:\s[^>]*)?>([\s\S]*)<\/template>/)?.[1] ?? ''
|
|
|
|
|
const withoutComments = template.replace(/<!--[\s\S]*?-->/g, '')
|
|
|
|
|
const attributeContent = String.raw`(?:[^>"']|"[^"]*"|'[^']*')*`
|
2026-09-03 19:51:29 +08:00
|
|
|
const tagPattern = new RegExp(`<el-input(?:-number)?\\b${attributeContent}/?>|<(?:input|textarea)\\b${attributeContent}/?>`, 'g')
|
2026-09-03 19:25:53 +08:00
|
|
|
|
|
|
|
|
for (const match of withoutComments.matchAll(tagPattern)) {
|
|
|
|
|
const tag = match[0]
|
|
|
|
|
const isElementInput = tag.startsWith('<el-input')
|
|
|
|
|
const editable = isElementInput
|
|
|
|
|
? !hasLiteralAttribute(tag, 'disabled') && !hasLiteralAttribute(tag, 'readonly')
|
|
|
|
|
: isEditableNativeTextInput(tag)
|
|
|
|
|
|
|
|
|
|
if (editable && !/\s:?(?:placeholder)\s*=/.test(tag)) {
|
|
|
|
|
violations.push(`${path.relative(sourceRoot, file)}:${lineNumber(withoutComments, match.index ?? 0)} ${tag.replace(/\s+/g, ' ')}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(violations, [], `Editable text inputs without placeholders:\n${violations.join('\n')}`)
|
|
|
|
|
})
|