import * as path from 'path' import { Node, Project } from 'ts-morph' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { HardcodedStringDetector, hasCJK, hasEnglishUIText, isInCodeContext, isNonUIString, shouldSkipNode } from '../check-hardcoded-strings' function createTestProject() { return new Project({ skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: true, compilerOptions: { jsx: 2 } // React JSX }) } function findStringLiteral(project: Project, code: string, targetString: string): Node | undefined { const sourceFile = project.createSourceFile('test.tsx', code, { overwrite: true }) let found: Node | undefined sourceFile.forEachDescendant((node) => { if (Node.isStringLiteral(node) && node.getLiteralValue() === targetString) { found = node } }) return found } function findTemplateLiteral(project: Project, code: string): Node | undefined { const sourceFile = project.createSourceFile('test.tsx', code, { overwrite: true }) let found: Node | undefined sourceFile.forEachDescendant((node) => { if (Node.isNoSubstitutionTemplateLiteral(node) || Node.isTemplateExpression(node)) { found = node } }) return found } // Mock fs module vi.mock('fs') describe('check-hardcoded-strings', () => { const mockSrcDir = '/mock/src/renderer/src' beforeEach(() => { vi.resetAllMocks() }) afterEach(() => { vi.restoreAllMocks() }) describe('hasCJK', () => { it('should detect Chinese characters', () => { expect(hasCJK('测试文本')).toBe(true) expect(hasCJK('Hello 世界')).toBe(true) expect(hasCJK('中文')).toBe(true) }) it('should detect Japanese characters', () => { expect(hasCJK('こんにちは')).toBe(true) // Hiragana expect(hasCJK('カタカナ')).toBe(true) // Katakana expect(hasCJK('日本語')).toBe(true) // Kanji }) it('should detect Korean characters', () => { expect(hasCJK('한국어')).toBe(true) // Hangul expect(hasCJK('안녕하세요')).toBe(true) }) it('should return false for non-CJK text', () => { expect(hasCJK('Hello World')).toBe(false) expect(hasCJK('12345')).toBe(false) expect(hasCJK('')).toBe(false) }) }) describe('hasEnglishUIText', () => { it('should detect English UI text patterns', () => { expect(hasEnglishUIText('Create New File')).toBe(true) expect(hasEnglishUIText('Save As')).toBe(true) expect(hasEnglishUIText('Open Project')).toBe(true) }) it('should reject single words', () => { expect(hasEnglishUIText('Save')).toBe(false) expect(hasEnglishUIText('Cancel')).toBe(false) }) it('should reject lowercase text', () => { expect(hasEnglishUIText('create new file')).toBe(false) expect(hasEnglishUIText('save as')).toBe(false) }) it('should reject too long phrases', () => { expect(hasEnglishUIText('This Is A Very Long Phrase With Many Words')).toBe(false) }) }) describe('isNonUIString', () => { it('should identify empty strings', () => { expect(isNonUIString('')).toBe(true) }) it('should identify pure numbers', () => { expect(isNonUIString('123')).toBe(true) expect(isNonUIString('0')).toBe(true) expect(isNonUIString('999')).toBe(true) }) it('should not mark regular UI text as non-UI', () => { expect(isNonUIString('Hello World')).toBe(false) expect(isNonUIString('Save')).toBe(false) expect(isNonUIString('确认')).toBe(false) expect(isNonUIString('请输入内容')).toBe(false) expect(isNonUIString('-')).toBe(false) // Even short strings may be UI in specific contexts }) it('should not filter technical strings (now handled by AST context)', () => { // With AST-based detection, these are no longer filtered // because we only check specific UI contexts where they rarely appear expect(isNonUIString('./path/to/file')).toBe(false) expect(isNonUIString('https://example.com')).toBe(false) expect(isNonUIString('#fff')).toBe(false) expect(isNonUIString('snake_case_id')).toBe(false) }) }) describe('File filtering', () => { const IGNORED_DIRS = ['__tests__', 'node_modules', 'i18n', 'locales', 'types', 'assets'] const IGNORED_FILES = ['*.test.ts', '*.test.tsx', '*.d.ts'] const mockShouldSkipFile = (filePath: string): boolean => { const relativePath = filePath.replace(mockSrcDir + '/', '') if (IGNORED_DIRS.some((dir) => relativePath.includes(dir))) { return true } const fileName = path.basename(filePath) if ( IGNORED_FILES.some((pattern) => { const regex = new RegExp(pattern.replace('*', '.*')) return regex.test(fileName) }) ) { return true } return false } it('should skip test files', () => { expect(mockShouldSkipFile(`${mockSrcDir}/components/Button.test.tsx`)).toBe(true) expect(mockShouldSkipFile(`${mockSrcDir}/utils/helper.test.ts`)).toBe(true) }) it('should skip type definition files', () => { expect(mockShouldSkipFile(`${mockSrcDir}/types/index.d.ts`)).toBe(true) }) it('should skip i18n/locales directories', () => { expect(mockShouldSkipFile(`${mockSrcDir}/i18n/locales/en-us.json`)).toBe(true) expect(mockShouldSkipFile(`${mockSrcDir}/locales/zh-cn.json`)).toBe(true) }) it('should skip __tests__ directories', () => { expect(mockShouldSkipFile(`${mockSrcDir}/components/__tests__/Button.test.tsx`)).toBe(true) }) it('should NOT skip regular component files', () => { expect(mockShouldSkipFile(`${mockSrcDir}/components/Button.tsx`)).toBe(false) expect(mockShouldSkipFile(`${mockSrcDir}/pages/Home.tsx`)).toBe(false) }) it('should NOT skip regular TypeScript files', () => { expect(mockShouldSkipFile(`${mockSrcDir}/utils/helper.ts`)).toBe(false) }) }) describe('HardcodedStringDetector', () => { // These are integration tests that would require actual files // For unit testing, we test the exported utility functions instead it('should be instantiable', () => { const detector = new HardcodedStringDetector() expect(detector).toBeDefined() }) }) describe('Legacy pattern compatibility (regex patterns for reference)', () => { // Keep legacy pattern tests for backward compatibility reference const CHINESE_PATTERNS = [ { regex: />([^<]*[\u4e00-\u9fff][^<]*) { const testLine = '测试文本' const matches = testLine.match(CHINESE_PATTERNS[0].regex) expect(matches).not.toBeNull() }) it('should detect Chinese characters in placeholder attribute (regex)', () => { const testLine = 'placeholder="请输入内容"' const matches = testLine.match(CHINESE_PATTERNS[1].regex) expect(matches).not.toBeNull() }) it('should detect Chinese characters in title attribute (regex)', () => { const testLine = 'title="提示信息"' const matches = testLine.match(CHINESE_PATTERNS[1].regex) expect(matches).not.toBeNull() }) }) describe('shouldSkipNode', () => { let project: Project beforeEach(() => { project = createTestProject() }) it('should skip import declarations', () => { const node = findStringLiteral(project, `import { foo } from 'some-module'`, 'some-module') expect(node).toBeDefined() expect(shouldSkipNode(node!)).toBe(true) }) it('should skip export declarations', () => { const node = findStringLiteral(project, `export { foo } from 'some-module'`, 'some-module') expect(node).toBeDefined() expect(shouldSkipNode(node!)).toBe(true) }) it('should skip logger calls', () => { const node = findStringLiteral(project, `logger.info('测试日志')`, '测试日志') expect(node).toBeDefined() expect(shouldSkipNode(node!)).toBe(true) }) it('should skip console calls', () => { const node = findStringLiteral(project, `console.log('测试日志')`, '测试日志') expect(node).toBeDefined() expect(shouldSkipNode(node!)).toBe(true) }) it('should skip t() translation function calls', () => { const node = findStringLiteral(project, `t('common.save')`, 'common.save') expect(node).toBeDefined() expect(shouldSkipNode(node!)).toBe(true) }) it('should skip type alias declarations', () => { const node = findStringLiteral(project, `type Status = '成功' | '失败'`, '成功') expect(node).toBeDefined() expect(shouldSkipNode(node!)).toBe(true) }) it('should skip interface declarations', () => { const node = findStringLiteral(project, `interface Foo { status: '成功' }`, '成功') expect(node).toBeDefined() expect(shouldSkipNode(node!)).toBe(true) }) it('should skip enum members', () => { const node = findStringLiteral(project, `enum Status { Success = '成功' }`, '成功') expect(node).toBeDefined() expect(shouldSkipNode(node!)).toBe(true) }) it('should skip language/locale variable declarations', () => { const node = findStringLiteral(project, `const languageOptions = ['中文', 'English']`, '中文') expect(node).toBeDefined() expect(shouldSkipNode(node!)).toBe(true) }) it('should NOT skip regular string literals', () => { const node = findStringLiteral(project, `const message = '测试消息'`, '测试消息') expect(node).toBeDefined() expect(shouldSkipNode(node!)).toBe(false) }) }) describe('isInCodeContext', () => { let project: Project beforeEach(() => { project = createTestProject() }) it('should detect tagged template expressions with css tag', () => { const node = findTemplateLiteral(project, 'const style = css`color: red;`') expect(node).toBeDefined() expect(isInCodeContext(node!)).toBe(true) }) it('should detect tagged template expressions with styled tag', () => { const node = findTemplateLiteral(project, 'const Button = styled.button`padding: 10px;`') expect(node).toBeDefined() expect(isInCodeContext(node!)).toBe(true) }) it('should detect CSS variable names', () => { const node = findStringLiteral(project, `const customStyle = 'color: blue'`, 'color: blue') expect(node).toBeDefined() expect(isInCodeContext(node!)).toBe(true) }) it('should detect code variable names', () => { const node = findStringLiteral(project, `const pythonCode = 'print("hello")'`, 'print("hello")') expect(node).toBeDefined() expect(isInCodeContext(node!)).toBe(true) }) it('should detect CSS property assignments', () => { const node = findStringLiteral(project, `const obj = { style: 'color: red' }`, 'color: red') expect(node).toBeDefined() expect(isInCodeContext(node!)).toBe(true) }) it('should detect code property assignments', () => { const node = findStringLiteral(project, `const obj = { script: 'console.log(1)' }`, 'console.log(1)') expect(node).toBeDefined() expect(isInCodeContext(node!)).toBe(true) }) it('should detect JSX style attributes', () => { const node = findStringLiteral(project, `
`, 'color: red') expect(node).toBeDefined() expect(isInCodeContext(node!)).toBe(true) }) it('should detect executeJavaScript calls', () => { const node = findStringLiteral(project, `webview.executeJavaScript('document.title')`, 'document.title') expect(node).toBeDefined() expect(isInCodeContext(node!)).toBe(true) }) it('should detect executeJavaScript with string concatenation', () => { const node = findStringLiteral(project, `webview.executeJavaScript('var x = ' + value + ';')`, 'var x = ') expect(node).toBeDefined() expect(isInCodeContext(node!)).toBe(true) }) it('should NOT detect regular strings', () => { const node = findStringLiteral(project, `const message = '普通消息'`, '普通消息') expect(node).toBeDefined() expect(isInCodeContext(node!)).toBe(false) }) }) })