gen-testid 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +14 -0
- package/.idea/gen-testid.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/.idea/workspace.xml +96 -0
- package/README.md +524 -0
- package/bin/cli.js +9 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +87 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +95 -0
- package/dist/core/GenTestId.d.ts +12 -0
- package/dist/core/GenTestId.js +127 -0
- package/dist/core/semantic-generator.d.ts +18 -0
- package/dist/core/semantic-generator.js +142 -0
- package/dist/core/strategies.d.ts +24 -0
- package/dist/core/strategies.js +73 -0
- package/dist/core/types.d.ts +28 -0
- package/dist/core/types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/shared/element-counter.d.ts +4 -0
- package/dist/shared/element-counter.js +11 -0
- package/dist/shared/parser.d.ts +11 -0
- package/dist/shared/parser.js +132 -0
- package/package.json +24 -0
- package/src/cli/index.ts +102 -0
- package/src/cli/init.ts +114 -0
- package/src/core/GenTestId.ts +169 -0
- package/src/core/semantic-generator.ts +164 -0
- package/src/core/strategies.ts +91 -0
- package/src/core/types.ts +33 -0
- package/src/index.ts +2 -0
- package/src/shared/element-counter.ts +10 -0
- package/src/shared/parser.ts +174 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// src/shared/parser.ts
|
|
2
|
+
import { parse, compileTemplate } from '@vue/compiler-sfc';
|
|
3
|
+
const NODE_TYPES = {
|
|
4
|
+
ELEMENT: 1,
|
|
5
|
+
ATTRIBUTE: 6,
|
|
6
|
+
DIRECTIVE: 7
|
|
7
|
+
};
|
|
8
|
+
const IGNORED_TAGS = new Set([
|
|
9
|
+
'template', 'slot', 'component', 'keep-alive',
|
|
10
|
+
'transition', 'transition-group'
|
|
11
|
+
]);
|
|
12
|
+
/**
|
|
13
|
+
* Надёжно находит позицию для вставки атрибута в тег
|
|
14
|
+
*/
|
|
15
|
+
function findInsertPosition(tagSource, isSelfClosing) {
|
|
16
|
+
if (isSelfClosing) {
|
|
17
|
+
// Для <Comp ... /> — вставляем перед />
|
|
18
|
+
const closingIndex = tagSource.lastIndexOf('/>');
|
|
19
|
+
return { offset: closingIndex, prefix: '' };
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
// Для <Comp ...> — вставляем сразу после имени тега
|
|
23
|
+
// Находим конец имени тега: <TagName[пробел|атрибут|>|\n]
|
|
24
|
+
const tagNameMatch = tagSource.match(/^<([a-zA-Z][a-zA-Z0-9-]*)/);
|
|
25
|
+
if (!tagNameMatch)
|
|
26
|
+
return { offset: 1, prefix: '' }; // fallback после <
|
|
27
|
+
const afterTagName = tagSource.slice(tagNameMatch[0].length);
|
|
28
|
+
// Если сразу после имени тега идёт > — вставляем перед ним
|
|
29
|
+
if (afterTagName.trimStart().startsWith('>')) {
|
|
30
|
+
const gtIndex = tagSource.indexOf('>', tagNameMatch[0].length);
|
|
31
|
+
return { offset: gtIndex, prefix: '' };
|
|
32
|
+
}
|
|
33
|
+
// Иначе вставляем после имени тега (до первого пробела/атрибута)
|
|
34
|
+
return { offset: tagNameMatch[0].length, prefix: ' ' };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function parseVueFile(content, filePath, genTestId) {
|
|
38
|
+
const result = {
|
|
39
|
+
modifiedContent: content,
|
|
40
|
+
injectedCount: 0,
|
|
41
|
+
elements: []
|
|
42
|
+
};
|
|
43
|
+
try {
|
|
44
|
+
const { descriptor, errors } = parse(content, { filename: filePath });
|
|
45
|
+
if (errors?.length > 0) {
|
|
46
|
+
console.warn(`⚠️ Ошибки парсинга SFC ${filePath}:`, errors.map(e => e.message || String(e)));
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
if (!descriptor.template) {
|
|
50
|
+
console.log(` ⚪ Нет секции <template> в ${filePath}`);
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
const templateSource = descriptor.template.content;
|
|
54
|
+
const templateStartOffset = descriptor.template.loc.start.offset;
|
|
55
|
+
const { ast } = compileTemplate({
|
|
56
|
+
source: templateSource,
|
|
57
|
+
filename: filePath,
|
|
58
|
+
id: 'gen-testid-injector'
|
|
59
|
+
});
|
|
60
|
+
if (!ast?.children)
|
|
61
|
+
return result;
|
|
62
|
+
const injections = [];
|
|
63
|
+
function walk(node) {
|
|
64
|
+
if (node.type !== NODE_TYPES.ELEMENT)
|
|
65
|
+
return;
|
|
66
|
+
const tag = node.tag;
|
|
67
|
+
if (!tag || IGNORED_TAGS.has(tag.toLowerCase())) {
|
|
68
|
+
processChildren(node);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const hasDataTestId = node.props?.some((prop) => {
|
|
72
|
+
if (prop.type === NODE_TYPES.ATTRIBUTE) {
|
|
73
|
+
return prop.name === 'data-testid';
|
|
74
|
+
}
|
|
75
|
+
if (prop.type === NODE_TYPES.DIRECTIVE && prop.name === 'bind' && prop.arg) {
|
|
76
|
+
return prop.arg?.content === 'data-testid';
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
});
|
|
80
|
+
if (!hasDataTestId) {
|
|
81
|
+
const startOffset = templateStartOffset + node.loc.start.offset;
|
|
82
|
+
const endOffset = templateStartOffset + node.loc.end.offset;
|
|
83
|
+
const lineNumber = content.slice(0, startOffset).split('\n').length;
|
|
84
|
+
const fullTagSource = content.slice(startOffset, endOffset);
|
|
85
|
+
const isSelfClosing = fullTagSource.trimEnd().endsWith('/>');
|
|
86
|
+
const testId = genTestId.generateTestId({
|
|
87
|
+
componentName: tag,
|
|
88
|
+
filePath,
|
|
89
|
+
lineNumber,
|
|
90
|
+
metadata: {
|
|
91
|
+
elementType: `vue:${tag}`,
|
|
92
|
+
textContent: tag,
|
|
93
|
+
attributes: {}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
// 🔥 Надёжное определение позиции вставки
|
|
97
|
+
const { offset: relativeOffset, prefix } = findInsertPosition(fullTagSource, isSelfClosing);
|
|
98
|
+
const insertOffset = startOffset + relativeOffset;
|
|
99
|
+
const insertText = ` ${prefix}data-testid="${testId}"`;
|
|
100
|
+
injections.push({ offset: insertOffset, insertText, testId, tag, lineNumber });
|
|
101
|
+
result.elements.push({ testId, lineNumber, elementType: `vue:${tag}` });
|
|
102
|
+
result.injectedCount++;
|
|
103
|
+
console.log(` ✅ ${tag} (строка ${lineNumber}): ${testId}`);
|
|
104
|
+
}
|
|
105
|
+
processChildren(node);
|
|
106
|
+
}
|
|
107
|
+
function processChildren(node) {
|
|
108
|
+
if (node.children && Array.isArray(node.children)) {
|
|
109
|
+
for (const child of node.children) {
|
|
110
|
+
walk(child);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const child of ast.children) {
|
|
115
|
+
walk(child);
|
|
116
|
+
}
|
|
117
|
+
// Применяем инъекции от конца к началу
|
|
118
|
+
if (injections.length > 0) {
|
|
119
|
+
injections.sort((a, b) => b.offset - a.offset);
|
|
120
|
+
let modified = content;
|
|
121
|
+
for (const inj of injections) {
|
|
122
|
+
modified = modified.slice(0, inj.offset) + inj.insertText + modified.slice(inj.offset);
|
|
123
|
+
}
|
|
124
|
+
result.modifiedContent = modified;
|
|
125
|
+
}
|
|
126
|
+
console.log(` 📊 Всего добавлено testid: ${result.injectedCount}`);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.warn(`⚠️ Ошибка при парсинге Vue файла ${filePath}:`, error);
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gen-testid",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Автоматическая простановка data-testid в Vue проектах",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"gen-testid": "./bin/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@vue/compiler-sfc": "^3.5.32",
|
|
13
|
+
"commander": "^11.1.0",
|
|
14
|
+
"fast-glob": "^3.3.2"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^20.8.0",
|
|
18
|
+
"typescript": "^5.2.0"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"dev": "tsc --watch"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { GenTestId } from '../core/GenTestId.js';
|
|
5
|
+
import { initCommand } from './init.js';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('gen-testid')
|
|
13
|
+
.description('Автоматическая простановка data-testid в Vue проектах')
|
|
14
|
+
.version('1.0.1');
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.command('init')
|
|
18
|
+
.description('Инициализация конфигурации')
|
|
19
|
+
.action(initCommand);
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.command('inject [files...]')
|
|
23
|
+
.description('Проставить testid в указанные файлы')
|
|
24
|
+
.option('-c, --config <path>', 'путь к конфигурации')
|
|
25
|
+
.option('-s, --strategy <name>', 'стратегия именования')
|
|
26
|
+
.option('-p, --prefix <prefix>', 'префикс для testid')
|
|
27
|
+
.option('-d, --dry-run', 'пробный запуск без изменений')
|
|
28
|
+
.action(async (files, options) => {
|
|
29
|
+
console.log('🔍 Инжекция testid...\n');
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const config = loadConfig(options.config);
|
|
33
|
+
|
|
34
|
+
const genTestId = new GenTestId({
|
|
35
|
+
...config,
|
|
36
|
+
strategy: options.strategy || config.strategy,
|
|
37
|
+
prefix: options.prefix || config.prefix
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (files.length === 0) {
|
|
41
|
+
console.log('📁 Сканирование всех Vue файлов...');
|
|
42
|
+
const results = await genTestId.processAllFiles();
|
|
43
|
+
printResults(results, options.dryRun);
|
|
44
|
+
} else {
|
|
45
|
+
console.log(`📁 Обработка ${files.length} файлов...`);
|
|
46
|
+
const results = [];
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
const fullPath = path.resolve(process.cwd(), file);
|
|
49
|
+
const result = await genTestId.processFile(fullPath);
|
|
50
|
+
results.push(result);
|
|
51
|
+
}
|
|
52
|
+
printResults(results, options.dryRun);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('\n❌ Ошибка:', error);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
program.parse(process.argv);
|
|
62
|
+
|
|
63
|
+
function loadConfig(configPath?: string): any {
|
|
64
|
+
if (configPath) {
|
|
65
|
+
const fullPath = path.resolve(process.cwd(), configPath);
|
|
66
|
+
if (fs.existsSync(fullPath)) {
|
|
67
|
+
return JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rcPath = path.resolve(process.cwd(), '.gentestidrc');
|
|
72
|
+
if (fs.existsSync(rcPath)) {
|
|
73
|
+
return JSON.parse(fs.readFileSync(rcPath, 'utf-8'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function printResults(results: any[], dryRun?: boolean) {
|
|
80
|
+
const total = results.reduce((sum, r) => sum + r.injectedCount, 0);
|
|
81
|
+
const filesWithChanges = results.filter(r => r.injectedCount > 0).length;
|
|
82
|
+
|
|
83
|
+
console.log(`\n${dryRun ? '📋' : '✅'} Результаты:`);
|
|
84
|
+
console.log(` Всего файлов: ${results.length}`);
|
|
85
|
+
console.log(` Файлов с изменениями: ${filesWithChanges}`);
|
|
86
|
+
console.log(` ${dryRun ? 'Будет добавлено' : 'Добавлено'} testid: ${total}`);
|
|
87
|
+
|
|
88
|
+
if (total > 0) {
|
|
89
|
+
console.log('\n📝 Детали:');
|
|
90
|
+
results
|
|
91
|
+
.filter(r => r.injectedCount > 0)
|
|
92
|
+
.slice(0, 5)
|
|
93
|
+
.forEach(r => {
|
|
94
|
+
const relativePath = path.relative(process.cwd(), r.filePath);
|
|
95
|
+
console.log(` 📁 ${relativePath}: +${r.injectedCount} testid`);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (results.filter(r => r.injectedCount > 0).length > 5) {
|
|
99
|
+
console.log(` ... и ещё ${results.filter(r => r.injectedCount > 0).length - 5} файлов`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/cli/init.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// src/cli/init.ts
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as readline from 'readline';
|
|
5
|
+
|
|
6
|
+
export async function initCommand() {
|
|
7
|
+
console.log('🚀 Инициализация gen-testid для Vue...\n');
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
console.log('📋 Доступные стратегии:');
|
|
11
|
+
console.log(' 1. hierarchical (default) - иерархические ID (test__Component__elem-1)');
|
|
12
|
+
console.log(' 2. stable-uuid - стабильные UUID');
|
|
13
|
+
console.log(' 3. minimal - простые номера');
|
|
14
|
+
console.log(' 4. semantic - умные ID через Ollama (бесплатно, локально)\n');
|
|
15
|
+
|
|
16
|
+
const strategyChoice = await askQuestion('Выберите стратегию (1-4) [1]: ');
|
|
17
|
+
|
|
18
|
+
let strategy = 'hierarchical';
|
|
19
|
+
let semanticModel = '';
|
|
20
|
+
let ollamaUrl = '';
|
|
21
|
+
|
|
22
|
+
switch (strategyChoice) {
|
|
23
|
+
case '2':
|
|
24
|
+
strategy = 'stable-uuid';
|
|
25
|
+
break;
|
|
26
|
+
case '3':
|
|
27
|
+
strategy = 'minimal';
|
|
28
|
+
break;
|
|
29
|
+
case '4':
|
|
30
|
+
strategy = 'semantic';
|
|
31
|
+
console.log('\n🤖 Настройка семантической генерации через Ollama:');
|
|
32
|
+
console.log(' Требуется Ollama');
|
|
33
|
+
console.log(' Установка: brew install ollama && ollama pull llama3.2:3b');
|
|
34
|
+
console.log(' Запуск: ollama serve\n');
|
|
35
|
+
|
|
36
|
+
const model = await askQuestion('Модель (llama3.2:3b/mistral/gemma:2b) [llama3.2:3b]: ');
|
|
37
|
+
semanticModel = model || 'llama3.2:3b';
|
|
38
|
+
|
|
39
|
+
const url = await askQuestion('URL Ollama [http://localhost:11434]: ');
|
|
40
|
+
ollamaUrl = url || 'http://localhost:11434';
|
|
41
|
+
|
|
42
|
+
console.log('\n💡 Совет: Запустите "ollama serve" в отдельном терминале');
|
|
43
|
+
break;
|
|
44
|
+
default:
|
|
45
|
+
strategy = 'hierarchical';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const prefix = await askQuestion('Префикс для testid [default: test]: ');
|
|
49
|
+
|
|
50
|
+
const config = {
|
|
51
|
+
strategy,
|
|
52
|
+
prefix: prefix || 'test',
|
|
53
|
+
semanticModel: semanticModel,
|
|
54
|
+
ollamaUrl: ollamaUrl,
|
|
55
|
+
includeFiles: ['src/**/*.vue'],
|
|
56
|
+
excludeFiles: ['**/*.test.*', '**/node_modules/**', '**/dist/**']
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const configPath = path.join(process.cwd(), '.gentestidrc');
|
|
60
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
61
|
+
console.log(`\n✅ Конфигурация сохранена в ${configPath}`);
|
|
62
|
+
|
|
63
|
+
if (strategy === 'semantic') {
|
|
64
|
+
console.log('\n🤖 Семантическая генерация активирована!');
|
|
65
|
+
console.log(' При первом запуске ID будут иерархическими,');
|
|
66
|
+
console.log(' а Ollama в фоне будет генерировать умные ID.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await updatePackageJson();
|
|
70
|
+
|
|
71
|
+
console.log('\n📝 Следующие шаги:');
|
|
72
|
+
console.log(' 1. Запустите Ollama: ollama serve');
|
|
73
|
+
console.log(' 2. Запустите "npx gen-testid inject" для простановки testid');
|
|
74
|
+
console.log(' 3. Готово! 🎉\n');
|
|
75
|
+
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('❌ Ошибка при инициализации:', error);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function updatePackageJson() {
|
|
83
|
+
try {
|
|
84
|
+
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
85
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
|
|
86
|
+
|
|
87
|
+
if (!pkg.scripts) {
|
|
88
|
+
pkg.scripts = {};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!pkg.scripts['testids']) {
|
|
92
|
+
pkg.scripts['testids'] = 'gen-testid inject';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2));
|
|
96
|
+
console.log('✅ Скрипты добавлены в package.json');
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.log('⚠️ Не удалось обновить package.json');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function askQuestion(question: string): Promise<string> {
|
|
103
|
+
const rl = readline.createInterface({
|
|
104
|
+
input: process.stdin,
|
|
105
|
+
output: process.stdout
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return new Promise(resolve => {
|
|
109
|
+
rl.question(question, answer => {
|
|
110
|
+
rl.close();
|
|
111
|
+
resolve(answer);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// src/core/GenTestId.ts
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import fg from 'fast-glob';
|
|
5
|
+
import {
|
|
6
|
+
GenTestIdConfig,
|
|
7
|
+
ElementContext,
|
|
8
|
+
NamingStrategy,
|
|
9
|
+
InjectionResult
|
|
10
|
+
} from './types.js';
|
|
11
|
+
import {
|
|
12
|
+
HierarchicalStrategy,
|
|
13
|
+
StableUuidStrategy,
|
|
14
|
+
MinimalStrategy,
|
|
15
|
+
SemanticStrategy
|
|
16
|
+
} from './strategies.js';
|
|
17
|
+
import { ElementCounter } from '../shared/element-counter.js';
|
|
18
|
+
import { parseVueFile } from '../shared/parser.js';
|
|
19
|
+
|
|
20
|
+
export class GenTestId {
|
|
21
|
+
public config: Required<GenTestIdConfig>;
|
|
22
|
+
private strategy: NamingStrategy;
|
|
23
|
+
private counter: ElementCounter;
|
|
24
|
+
|
|
25
|
+
constructor(config: GenTestIdConfig = {}) {
|
|
26
|
+
this.config = {
|
|
27
|
+
prefix: 'test',
|
|
28
|
+
strategy: 'hierarchical',
|
|
29
|
+
semanticModel: '',
|
|
30
|
+
ollamaUrl: 'http://localhost:11434',
|
|
31
|
+
excludeFiles: ['**/*.test.*', '**/*.spec.*', '**/node_modules/**', '**/dist/**'],
|
|
32
|
+
includeFiles: ['src/**/*.vue'],
|
|
33
|
+
...config
|
|
34
|
+
} as Required<GenTestIdConfig>;
|
|
35
|
+
|
|
36
|
+
this.counter = new ElementCounter();
|
|
37
|
+
this.strategy = this.initStrategy();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private initStrategy(): NamingStrategy {
|
|
41
|
+
switch (this.config.strategy) {
|
|
42
|
+
case 'hierarchical':
|
|
43
|
+
return new HierarchicalStrategy(this.config.prefix);
|
|
44
|
+
case 'stable-uuid':
|
|
45
|
+
return new StableUuidStrategy(this.config.prefix);
|
|
46
|
+
case 'minimal':
|
|
47
|
+
return new MinimalStrategy(this.config.prefix);
|
|
48
|
+
case 'semantic':
|
|
49
|
+
return new SemanticStrategy(
|
|
50
|
+
this.config.prefix,
|
|
51
|
+
this.config.semanticModel,
|
|
52
|
+
this.config.ollamaUrl
|
|
53
|
+
);
|
|
54
|
+
default:
|
|
55
|
+
return new HierarchicalStrategy(this.config.prefix);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public generateTestId(context: Omit<ElementContext, 'occurrence'>): string {
|
|
60
|
+
const key = `${context.filePath}:${context.componentName}:${context.lineNumber || 'unknown'}`;
|
|
61
|
+
const occurrence = this.counter.increment(key);
|
|
62
|
+
|
|
63
|
+
const fullContext: ElementContext = {
|
|
64
|
+
...context,
|
|
65
|
+
occurrence,
|
|
66
|
+
lineNumber: context.lineNumber || 0
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return this.strategy.generateId(fullContext);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public async processFile(filePath: string): Promise<InjectionResult> {
|
|
73
|
+
try {
|
|
74
|
+
if (this.shouldExcludeFile(filePath)) {
|
|
75
|
+
return { filePath, injectedCount: 0, elements: [] };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let content = await fs.readFile(filePath, 'utf-8');
|
|
79
|
+
|
|
80
|
+
//Добавляем testid
|
|
81
|
+
let parseResult: { injectedCount: number; modifiedContent: string; elements: any[] } = {
|
|
82
|
+
injectedCount: 0,
|
|
83
|
+
modifiedContent: content,
|
|
84
|
+
elements: []
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (filePath.endsWith('.vue')) {
|
|
88
|
+
parseResult = await parseVueFile(content, filePath, this);
|
|
89
|
+
if (parseResult.injectedCount > 0) {
|
|
90
|
+
content = parseResult.modifiedContent;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
//Save
|
|
95
|
+
if (parseResult.injectedCount > 0) {
|
|
96
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
filePath,
|
|
101
|
+
injectedCount: parseResult.injectedCount,
|
|
102
|
+
elements: parseResult.elements
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error(`❌ Error processing ${filePath}:`, error);
|
|
107
|
+
return { filePath, injectedCount: 0, elements: [] };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private shouldExcludeFile(filePath: string): boolean {
|
|
112
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
113
|
+
|
|
114
|
+
for (const pattern of this.config.excludeFiles) {
|
|
115
|
+
if (pattern.includes('*')) {
|
|
116
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
117
|
+
if (regex.test(relativePath)) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
} else if (relativePath.includes(pattern)) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public async processAllFiles(): Promise<InjectionResult[]> {
|
|
129
|
+
console.log('🔍 Поиск файлов...');
|
|
130
|
+
|
|
131
|
+
const files: string[] = [];
|
|
132
|
+
|
|
133
|
+
for (const pattern of this.config.includeFiles) {
|
|
134
|
+
try {
|
|
135
|
+
const matches = await fg(pattern, {
|
|
136
|
+
ignore: this.config.excludeFiles,
|
|
137
|
+
absolute: true,
|
|
138
|
+
onlyFiles: true
|
|
139
|
+
});
|
|
140
|
+
files.push(...matches);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.warn(`⚠️ Ошибка при поиске по паттерну ${pattern}:`, error);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(`📁 Найдено файлов: ${files.length}\n`);
|
|
147
|
+
|
|
148
|
+
const results: InjectionResult[] = [];
|
|
149
|
+
let totalTestIds = 0;
|
|
150
|
+
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
console.log(`📄 Обработка: ${path.basename(file)}`);
|
|
153
|
+
const result = await this.processFile(file);
|
|
154
|
+
results.push(result);
|
|
155
|
+
|
|
156
|
+
totalTestIds += result.injectedCount;
|
|
157
|
+
|
|
158
|
+
if (result.injectedCount > 0) {
|
|
159
|
+
console.log(` ✅ Добавлено testid: ${result.injectedCount}\n`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (totalTestIds > 0) {
|
|
164
|
+
console.log(`\n🏷️ Всего добавлено testid: ${totalTestIds}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return results;
|
|
168
|
+
}
|
|
169
|
+
}
|