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,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { GenTestId } from '../core/GenTestId.js';
|
|
4
|
+
import { initCommand } from './init.js';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name('gen-testid')
|
|
10
|
+
.description('Автоматическая простановка data-testid в Vue проектах')
|
|
11
|
+
.version('1.0.1');
|
|
12
|
+
program
|
|
13
|
+
.command('init')
|
|
14
|
+
.description('Инициализация конфигурации')
|
|
15
|
+
.action(initCommand);
|
|
16
|
+
program
|
|
17
|
+
.command('inject [files...]')
|
|
18
|
+
.description('Проставить testid в указанные файлы')
|
|
19
|
+
.option('-c, --config <path>', 'путь к конфигурации')
|
|
20
|
+
.option('-s, --strategy <name>', 'стратегия именования')
|
|
21
|
+
.option('-p, --prefix <prefix>', 'префикс для testid')
|
|
22
|
+
.option('-d, --dry-run', 'пробный запуск без изменений')
|
|
23
|
+
.action(async (files, options) => {
|
|
24
|
+
console.log('🔍 Инжекция testid...\n');
|
|
25
|
+
try {
|
|
26
|
+
const config = loadConfig(options.config);
|
|
27
|
+
const genTestId = new GenTestId({
|
|
28
|
+
...config,
|
|
29
|
+
strategy: options.strategy || config.strategy,
|
|
30
|
+
prefix: options.prefix || config.prefix
|
|
31
|
+
});
|
|
32
|
+
if (files.length === 0) {
|
|
33
|
+
console.log('📁 Сканирование всех Vue файлов...');
|
|
34
|
+
const results = await genTestId.processAllFiles();
|
|
35
|
+
printResults(results, options.dryRun);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.log(`📁 Обработка ${files.length} файлов...`);
|
|
39
|
+
const results = [];
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
const fullPath = path.resolve(process.cwd(), file);
|
|
42
|
+
const result = await genTestId.processFile(fullPath);
|
|
43
|
+
results.push(result);
|
|
44
|
+
}
|
|
45
|
+
printResults(results, options.dryRun);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error('\n❌ Ошибка:', error);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
program.parse(process.argv);
|
|
54
|
+
function loadConfig(configPath) {
|
|
55
|
+
if (configPath) {
|
|
56
|
+
const fullPath = path.resolve(process.cwd(), configPath);
|
|
57
|
+
if (fs.existsSync(fullPath)) {
|
|
58
|
+
return JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const rcPath = path.resolve(process.cwd(), '.gentestidrc');
|
|
62
|
+
if (fs.existsSync(rcPath)) {
|
|
63
|
+
return JSON.parse(fs.readFileSync(rcPath, 'utf-8'));
|
|
64
|
+
}
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
function printResults(results, dryRun) {
|
|
68
|
+
const total = results.reduce((sum, r) => sum + r.injectedCount, 0);
|
|
69
|
+
const filesWithChanges = results.filter(r => r.injectedCount > 0).length;
|
|
70
|
+
console.log(`\n${dryRun ? '📋' : '✅'} Результаты:`);
|
|
71
|
+
console.log(` Всего файлов: ${results.length}`);
|
|
72
|
+
console.log(` Файлов с изменениями: ${filesWithChanges}`);
|
|
73
|
+
console.log(` ${dryRun ? 'Будет добавлено' : 'Добавлено'} testid: ${total}`);
|
|
74
|
+
if (total > 0) {
|
|
75
|
+
console.log('\n📝 Детали:');
|
|
76
|
+
results
|
|
77
|
+
.filter(r => r.injectedCount > 0)
|
|
78
|
+
.slice(0, 5)
|
|
79
|
+
.forEach(r => {
|
|
80
|
+
const relativePath = path.relative(process.cwd(), r.filePath);
|
|
81
|
+
console.log(` 📁 ${relativePath}: +${r.injectedCount} testid`);
|
|
82
|
+
});
|
|
83
|
+
if (results.filter(r => r.injectedCount > 0).length > 5) {
|
|
84
|
+
console.log(` ... и ещё ${results.filter(r => r.injectedCount > 0).length - 5} файлов`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initCommand(): Promise<void>;
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
export async function initCommand() {
|
|
6
|
+
console.log('🚀 Инициализация gen-testid для Vue...\n');
|
|
7
|
+
try {
|
|
8
|
+
console.log('📋 Доступные стратегии:');
|
|
9
|
+
console.log(' 1. hierarchical (default) - иерархические ID (test__Component__elem-1)');
|
|
10
|
+
console.log(' 2. stable-uuid - стабильные UUID');
|
|
11
|
+
console.log(' 3. minimal - простые номера');
|
|
12
|
+
console.log(' 4. semantic - умные ID через Ollama (бесплатно, локально)\n');
|
|
13
|
+
const strategyChoice = await askQuestion('Выберите стратегию (1-4) [1]: ');
|
|
14
|
+
let strategy = 'hierarchical';
|
|
15
|
+
let semanticModel = '';
|
|
16
|
+
let ollamaUrl = '';
|
|
17
|
+
switch (strategyChoice) {
|
|
18
|
+
case '2':
|
|
19
|
+
strategy = 'stable-uuid';
|
|
20
|
+
break;
|
|
21
|
+
case '3':
|
|
22
|
+
strategy = 'minimal';
|
|
23
|
+
break;
|
|
24
|
+
case '4':
|
|
25
|
+
strategy = 'semantic';
|
|
26
|
+
console.log('\n🤖 Настройка семантической генерации через Ollama:');
|
|
27
|
+
console.log(' Требуется Ollama');
|
|
28
|
+
console.log(' Установка: brew install ollama && ollama pull llama3.2:3b');
|
|
29
|
+
console.log(' Запуск: ollama serve\n');
|
|
30
|
+
const model = await askQuestion('Модель (llama3.2:3b/mistral/gemma:2b) [llama3.2:3b]: ');
|
|
31
|
+
semanticModel = model || 'llama3.2:3b';
|
|
32
|
+
const url = await askQuestion('URL Ollama [http://localhost:11434]: ');
|
|
33
|
+
ollamaUrl = url || 'http://localhost:11434';
|
|
34
|
+
console.log('\n💡 Совет: Запустите "ollama serve" в отдельном терминале');
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
strategy = 'hierarchical';
|
|
38
|
+
}
|
|
39
|
+
const prefix = await askQuestion('Префикс для testid [default: test]: ');
|
|
40
|
+
const config = {
|
|
41
|
+
strategy,
|
|
42
|
+
prefix: prefix || 'test',
|
|
43
|
+
semanticModel: semanticModel,
|
|
44
|
+
ollamaUrl: ollamaUrl,
|
|
45
|
+
includeFiles: ['src/**/*.vue'],
|
|
46
|
+
excludeFiles: ['**/*.test.*', '**/node_modules/**', '**/dist/**']
|
|
47
|
+
};
|
|
48
|
+
const configPath = path.join(process.cwd(), '.gentestidrc');
|
|
49
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
50
|
+
console.log(`\n✅ Конфигурация сохранена в ${configPath}`);
|
|
51
|
+
if (strategy === 'semantic') {
|
|
52
|
+
console.log('\n🤖 Семантическая генерация активирована!');
|
|
53
|
+
console.log(' При первом запуске ID будут иерархическими,');
|
|
54
|
+
console.log(' а Ollama в фоне будет генерировать умные ID.');
|
|
55
|
+
}
|
|
56
|
+
await updatePackageJson();
|
|
57
|
+
console.log('\n📝 Следующие шаги:');
|
|
58
|
+
console.log(' 1. Запустите Ollama: ollama serve');
|
|
59
|
+
console.log(' 2. Запустите "npx gen-testid inject" для простановки testid');
|
|
60
|
+
console.log(' 3. Готово! 🎉\n');
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.error('❌ Ошибка при инициализации:', error);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function updatePackageJson() {
|
|
68
|
+
try {
|
|
69
|
+
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
70
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
|
|
71
|
+
if (!pkg.scripts) {
|
|
72
|
+
pkg.scripts = {};
|
|
73
|
+
}
|
|
74
|
+
if (!pkg.scripts['testids']) {
|
|
75
|
+
pkg.scripts['testids'] = 'gen-testid inject';
|
|
76
|
+
}
|
|
77
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2));
|
|
78
|
+
console.log('✅ Скрипты добавлены в package.json');
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.log('⚠️ Не удалось обновить package.json');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function askQuestion(question) {
|
|
85
|
+
const rl = readline.createInterface({
|
|
86
|
+
input: process.stdin,
|
|
87
|
+
output: process.stdout
|
|
88
|
+
});
|
|
89
|
+
return new Promise(resolve => {
|
|
90
|
+
rl.question(question, answer => {
|
|
91
|
+
rl.close();
|
|
92
|
+
resolve(answer);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { GenTestIdConfig, ElementContext, InjectionResult } from './types.js';
|
|
2
|
+
export declare class GenTestId {
|
|
3
|
+
config: Required<GenTestIdConfig>;
|
|
4
|
+
private strategy;
|
|
5
|
+
private counter;
|
|
6
|
+
constructor(config?: GenTestIdConfig);
|
|
7
|
+
private initStrategy;
|
|
8
|
+
generateTestId(context: Omit<ElementContext, 'occurrence'>): string;
|
|
9
|
+
processFile(filePath: string): Promise<InjectionResult>;
|
|
10
|
+
private shouldExcludeFile;
|
|
11
|
+
processAllFiles(): Promise<InjectionResult[]>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
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 { HierarchicalStrategy, StableUuidStrategy, MinimalStrategy, SemanticStrategy } from './strategies.js';
|
|
6
|
+
import { ElementCounter } from '../shared/element-counter.js';
|
|
7
|
+
import { parseVueFile } from '../shared/parser.js';
|
|
8
|
+
export class GenTestId {
|
|
9
|
+
constructor(config = {}) {
|
|
10
|
+
this.config = {
|
|
11
|
+
prefix: 'test',
|
|
12
|
+
strategy: 'hierarchical',
|
|
13
|
+
semanticModel: '',
|
|
14
|
+
ollamaUrl: 'http://localhost:11434',
|
|
15
|
+
excludeFiles: ['**/*.test.*', '**/*.spec.*', '**/node_modules/**', '**/dist/**'],
|
|
16
|
+
includeFiles: ['src/**/*.vue'],
|
|
17
|
+
...config
|
|
18
|
+
};
|
|
19
|
+
this.counter = new ElementCounter();
|
|
20
|
+
this.strategy = this.initStrategy();
|
|
21
|
+
}
|
|
22
|
+
initStrategy() {
|
|
23
|
+
switch (this.config.strategy) {
|
|
24
|
+
case 'hierarchical':
|
|
25
|
+
return new HierarchicalStrategy(this.config.prefix);
|
|
26
|
+
case 'stable-uuid':
|
|
27
|
+
return new StableUuidStrategy(this.config.prefix);
|
|
28
|
+
case 'minimal':
|
|
29
|
+
return new MinimalStrategy(this.config.prefix);
|
|
30
|
+
case 'semantic':
|
|
31
|
+
return new SemanticStrategy(this.config.prefix, this.config.semanticModel, this.config.ollamaUrl);
|
|
32
|
+
default:
|
|
33
|
+
return new HierarchicalStrategy(this.config.prefix);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
generateTestId(context) {
|
|
37
|
+
const key = `${context.filePath}:${context.componentName}:${context.lineNumber || 'unknown'}`;
|
|
38
|
+
const occurrence = this.counter.increment(key);
|
|
39
|
+
const fullContext = {
|
|
40
|
+
...context,
|
|
41
|
+
occurrence,
|
|
42
|
+
lineNumber: context.lineNumber || 0
|
|
43
|
+
};
|
|
44
|
+
return this.strategy.generateId(fullContext);
|
|
45
|
+
}
|
|
46
|
+
async processFile(filePath) {
|
|
47
|
+
try {
|
|
48
|
+
if (this.shouldExcludeFile(filePath)) {
|
|
49
|
+
return { filePath, injectedCount: 0, elements: [] };
|
|
50
|
+
}
|
|
51
|
+
let content = await fs.readFile(filePath, 'utf-8');
|
|
52
|
+
//Добавляем testid
|
|
53
|
+
let parseResult = {
|
|
54
|
+
injectedCount: 0,
|
|
55
|
+
modifiedContent: content,
|
|
56
|
+
elements: []
|
|
57
|
+
};
|
|
58
|
+
if (filePath.endsWith('.vue')) {
|
|
59
|
+
parseResult = await parseVueFile(content, filePath, this);
|
|
60
|
+
if (parseResult.injectedCount > 0) {
|
|
61
|
+
content = parseResult.modifiedContent;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//Save
|
|
65
|
+
if (parseResult.injectedCount > 0) {
|
|
66
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
filePath,
|
|
70
|
+
injectedCount: parseResult.injectedCount,
|
|
71
|
+
elements: parseResult.elements
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.error(`❌ Error processing ${filePath}:`, error);
|
|
76
|
+
return { filePath, injectedCount: 0, elements: [] };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
shouldExcludeFile(filePath) {
|
|
80
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
81
|
+
for (const pattern of this.config.excludeFiles) {
|
|
82
|
+
if (pattern.includes('*')) {
|
|
83
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
84
|
+
if (regex.test(relativePath)) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (relativePath.includes(pattern)) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
async processAllFiles() {
|
|
95
|
+
console.log('🔍 Поиск файлов...');
|
|
96
|
+
const files = [];
|
|
97
|
+
for (const pattern of this.config.includeFiles) {
|
|
98
|
+
try {
|
|
99
|
+
const matches = await fg(pattern, {
|
|
100
|
+
ignore: this.config.excludeFiles,
|
|
101
|
+
absolute: true,
|
|
102
|
+
onlyFiles: true
|
|
103
|
+
});
|
|
104
|
+
files.push(...matches);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.warn(`⚠️ Ошибка при поиске по паттерну ${pattern}:`, error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
console.log(`📁 Найдено файлов: ${files.length}\n`);
|
|
111
|
+
const results = [];
|
|
112
|
+
let totalTestIds = 0;
|
|
113
|
+
for (const file of files) {
|
|
114
|
+
console.log(`📄 Обработка: ${path.basename(file)}`);
|
|
115
|
+
const result = await this.processFile(file);
|
|
116
|
+
results.push(result);
|
|
117
|
+
totalTestIds += result.injectedCount;
|
|
118
|
+
if (result.injectedCount > 0) {
|
|
119
|
+
console.log(` ✅ Добавлено testid: ${result.injectedCount}\n`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (totalTestIds > 0) {
|
|
123
|
+
console.log(`\n🏷️ Всего добавлено testid: ${totalTestIds}`);
|
|
124
|
+
}
|
|
125
|
+
return results;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ElementContext } from './types.js';
|
|
2
|
+
export interface SemanticGeneratorConfig {
|
|
3
|
+
model?: string;
|
|
4
|
+
ollamaUrl?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class SemanticGenerator {
|
|
7
|
+
private model;
|
|
8
|
+
private ollamaUrl;
|
|
9
|
+
private cache;
|
|
10
|
+
private idCounter;
|
|
11
|
+
private isAvailableCache;
|
|
12
|
+
constructor(config?: SemanticGeneratorConfig);
|
|
13
|
+
private checkAvailability;
|
|
14
|
+
generateSemanticIdSync(context: ElementContext): string | null;
|
|
15
|
+
private buildPrompt;
|
|
16
|
+
private generateWithOllamaSync;
|
|
17
|
+
private cleanId;
|
|
18
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
export class SemanticGenerator {
|
|
3
|
+
constructor(config = {}) {
|
|
4
|
+
this.cache = new Map();
|
|
5
|
+
this.idCounter = new Map();
|
|
6
|
+
this.isAvailableCache = null;
|
|
7
|
+
this.model = config.model || 'llama3.2:3b';
|
|
8
|
+
this.ollamaUrl = config.ollamaUrl || 'http://localhost:11434';
|
|
9
|
+
this.checkAvailability();
|
|
10
|
+
}
|
|
11
|
+
checkAvailability() {
|
|
12
|
+
try {
|
|
13
|
+
const result = execSync(`curl -s -o /dev/null -w "%{http_code}" ${this.ollamaUrl}/api/tags`, {
|
|
14
|
+
encoding: 'utf-8',
|
|
15
|
+
timeout: 2000
|
|
16
|
+
});
|
|
17
|
+
this.isAvailableCache = result === '200';
|
|
18
|
+
if (this.isAvailableCache) {
|
|
19
|
+
console.log('✅ Ollama доступен');
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.log('❌ Ollama не отвечает');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
this.isAvailableCache = false;
|
|
27
|
+
console.log('❌ Ollama не доступен');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
generateSemanticIdSync(context) {
|
|
31
|
+
if (!this.isAvailableCache) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const cacheKey = `${context.filePath}:${context.componentName}:${context.lineNumber}`;
|
|
35
|
+
if (this.cache.has(cacheKey)) {
|
|
36
|
+
return this.cache.get(cacheKey);
|
|
37
|
+
}
|
|
38
|
+
const prompt = this.buildPrompt(context);
|
|
39
|
+
try {
|
|
40
|
+
const semanticId = this.generateWithOllamaSync(prompt);
|
|
41
|
+
if (semanticId && semanticId.length > 0 && semanticId !== 'id') {
|
|
42
|
+
let cleanedId = this.cleanId(semanticId);
|
|
43
|
+
//Проверяем дубликаты внутри файла одного
|
|
44
|
+
const fileKey = context.filePath;
|
|
45
|
+
const duplicateKey = `${fileKey}:${cleanedId}`;
|
|
46
|
+
const count = this.idCounter.get(duplicateKey) || 0;
|
|
47
|
+
if (count > 0) {
|
|
48
|
+
cleanedId = `${cleanedId}-${count + 1}`;
|
|
49
|
+
}
|
|
50
|
+
this.idCounter.set(duplicateKey, count + 1);
|
|
51
|
+
this.cache.set(cacheKey, cleanedId);
|
|
52
|
+
return cleanedId;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
//Ошибка - игнорируем
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
buildPrompt(context) {
|
|
61
|
+
const tagName = context.metadata?.elementType?.split(':')[1] || context.componentName;
|
|
62
|
+
const textContent = context.metadata?.textContent || '';
|
|
63
|
+
const placeholder = context.metadata?.attributes?.placeholder || '';
|
|
64
|
+
const inputType = context.metadata?.attributes?.type || '';
|
|
65
|
+
const labelText = context.metadata?.attributes?.label || '';
|
|
66
|
+
const isCustomComponent = context.metadata?.isCustomComponent || false;
|
|
67
|
+
let elementDescription = '';
|
|
68
|
+
if (isCustomComponent) {
|
|
69
|
+
// Для кастомных компонентов используем label или имя компонента
|
|
70
|
+
const componentName = tagName.replace(/Field$/, '').toLowerCase();
|
|
71
|
+
elementDescription = labelText ? `"${labelText}" ${componentName}` : `${componentName} component`;
|
|
72
|
+
}
|
|
73
|
+
else if (tagName === 'button') {
|
|
74
|
+
const buttonText = textContent || 'button';
|
|
75
|
+
elementDescription = `"${buttonText}" button`;
|
|
76
|
+
}
|
|
77
|
+
else if (tagName === 'input') {
|
|
78
|
+
if (inputType === 'email')
|
|
79
|
+
elementDescription = 'email input';
|
|
80
|
+
else if (inputType === 'password')
|
|
81
|
+
elementDescription = 'password input';
|
|
82
|
+
else if (inputType === 'checkbox')
|
|
83
|
+
elementDescription = 'checkbox';
|
|
84
|
+
else if (inputType === 'submit')
|
|
85
|
+
elementDescription = 'submit button';
|
|
86
|
+
else if (placeholder)
|
|
87
|
+
elementDescription = `"${placeholder}" input`;
|
|
88
|
+
else
|
|
89
|
+
elementDescription = 'input field';
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
elementDescription = tagName;
|
|
93
|
+
}
|
|
94
|
+
return `Generate ONE short semantic test ID for: ${elementDescription}
|
|
95
|
+
${textContent ? `Text: "${textContent}"` : ''}
|
|
96
|
+
${placeholder ? `Placeholder: "${placeholder}"` : ''}
|
|
97
|
+
${labelText ? `Label: "${labelText}"` : ''}
|
|
98
|
+
|
|
99
|
+
Return ONLY the ID (lowercase, hyphens, max 25 chars).
|
|
100
|
+
Examples: login-button, email-input, password-field, user-name-input, submit-form
|
|
101
|
+
|
|
102
|
+
ID:`;
|
|
103
|
+
}
|
|
104
|
+
generateWithOllamaSync(prompt) {
|
|
105
|
+
try {
|
|
106
|
+
const escapedPrompt = JSON.stringify(prompt);
|
|
107
|
+
const command = `curl -s -X POST ${this.ollamaUrl}/api/generate \
|
|
108
|
+
-H "Content-Type: application/json" \
|
|
109
|
+
-d '{"model":"${this.model}","prompt":${escapedPrompt},"stream":false,"options":{"temperature":0.2,"max_tokens":25}}'`;
|
|
110
|
+
const result = execSync(command, {
|
|
111
|
+
encoding: 'utf-8',
|
|
112
|
+
timeout: 5000,
|
|
113
|
+
maxBuffer: 1024 * 1024
|
|
114
|
+
});
|
|
115
|
+
const data = JSON.parse(result);
|
|
116
|
+
let response = data.response?.trim() || null;
|
|
117
|
+
if (response) {
|
|
118
|
+
response = response.toLowerCase()
|
|
119
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
120
|
+
.replace(/-+/g, '-')
|
|
121
|
+
.replace(/^-|-$/g, '');
|
|
122
|
+
}
|
|
123
|
+
return response;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
cleanId(id) {
|
|
130
|
+
if (!id)
|
|
131
|
+
return '';
|
|
132
|
+
let cleaned = id
|
|
133
|
+
.toLowerCase()
|
|
134
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
135
|
+
.replace(/-+/g, '-')
|
|
136
|
+
.replace(/^-|-$/g, '');
|
|
137
|
+
if (cleaned.length > 35) {
|
|
138
|
+
cleaned = cleaned.substring(0, 35);
|
|
139
|
+
}
|
|
140
|
+
return cleaned;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NamingStrategy, ElementContext } from './types.js';
|
|
2
|
+
export declare class HierarchicalStrategy implements NamingStrategy {
|
|
3
|
+
private prefix;
|
|
4
|
+
constructor(prefix?: string);
|
|
5
|
+
generateId(context: ElementContext): string;
|
|
6
|
+
}
|
|
7
|
+
export declare class StableUuidStrategy implements NamingStrategy {
|
|
8
|
+
private prefix;
|
|
9
|
+
private cache;
|
|
10
|
+
constructor(prefix?: string);
|
|
11
|
+
generateId(context: ElementContext): string;
|
|
12
|
+
}
|
|
13
|
+
export declare class MinimalStrategy implements NamingStrategy {
|
|
14
|
+
private prefix;
|
|
15
|
+
private globalCounter;
|
|
16
|
+
constructor(prefix?: string);
|
|
17
|
+
generateId(): string;
|
|
18
|
+
}
|
|
19
|
+
export declare class SemanticStrategy implements NamingStrategy {
|
|
20
|
+
private generator;
|
|
21
|
+
private fallbackStrategy;
|
|
22
|
+
constructor(prefix?: string, model?: string, ollamaUrl?: string);
|
|
23
|
+
generateId(context: ElementContext): string;
|
|
24
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { SemanticGenerator } from './semantic-generator.js';
|
|
3
|
+
export class HierarchicalStrategy {
|
|
4
|
+
constructor(prefix = 'test') {
|
|
5
|
+
this.prefix = prefix;
|
|
6
|
+
}
|
|
7
|
+
generateId(context) {
|
|
8
|
+
const { componentName, occurrence } = context;
|
|
9
|
+
const fileName = context.filePath
|
|
10
|
+
.split('/')
|
|
11
|
+
.pop()
|
|
12
|
+
?.replace(/\.[^/.]+$/, '') || '';
|
|
13
|
+
const lineNumber = context.lineNumber || 0;
|
|
14
|
+
const tagType = context.metadata?.elementType?.split(':')[1] || componentName;
|
|
15
|
+
const parts = [
|
|
16
|
+
this.prefix,
|
|
17
|
+
fileName,
|
|
18
|
+
tagType,
|
|
19
|
+
`line-${lineNumber}`,
|
|
20
|
+
`elem-${occurrence}`
|
|
21
|
+
].filter(Boolean);
|
|
22
|
+
return parts.join('__').toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class StableUuidStrategy {
|
|
26
|
+
constructor(prefix = 'test') {
|
|
27
|
+
this.prefix = prefix;
|
|
28
|
+
this.cache = new Map();
|
|
29
|
+
}
|
|
30
|
+
generateId(context) {
|
|
31
|
+
const key = `${context.filePath}:${context.componentName}:${context.lineNumber}:${context.occurrence}`;
|
|
32
|
+
if (!this.cache.has(key)) {
|
|
33
|
+
const hash = createHash('md5').update(key).digest('hex');
|
|
34
|
+
const uuid = [
|
|
35
|
+
hash.substring(0, 8),
|
|
36
|
+
hash.substring(8, 12),
|
|
37
|
+
hash.substring(12, 16),
|
|
38
|
+
hash.substring(16, 20),
|
|
39
|
+
hash.substring(20, 32)
|
|
40
|
+
].join('-');
|
|
41
|
+
this.cache.set(key, `${this.prefix}-${uuid}`);
|
|
42
|
+
}
|
|
43
|
+
return this.cache.get(key);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export class MinimalStrategy {
|
|
47
|
+
constructor(prefix = 'test') {
|
|
48
|
+
this.prefix = prefix;
|
|
49
|
+
this.globalCounter = 0;
|
|
50
|
+
}
|
|
51
|
+
generateId() {
|
|
52
|
+
return `${this.prefix}-${++this.globalCounter}`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export class SemanticStrategy {
|
|
56
|
+
constructor(prefix = 'test', model, ollamaUrl) {
|
|
57
|
+
console.log('🤖 Инициализация семантической стратегии...');
|
|
58
|
+
this.generator = new SemanticGenerator({
|
|
59
|
+
model: model || 'llama3.2:3b',
|
|
60
|
+
ollamaUrl: ollamaUrl || 'http://localhost:11434'
|
|
61
|
+
});
|
|
62
|
+
this.fallbackStrategy = new HierarchicalStrategy(prefix);
|
|
63
|
+
}
|
|
64
|
+
generateId(context) {
|
|
65
|
+
// Пробуем получить семантический ID
|
|
66
|
+
const semanticId = this.generator.generateSemanticIdSync(context);
|
|
67
|
+
if (semanticId && semanticId.length > 0) {
|
|
68
|
+
return semanticId;
|
|
69
|
+
}
|
|
70
|
+
// Fallback
|
|
71
|
+
return this.fallbackStrategy.generateId(context);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type Strategy = 'hierarchical' | 'stable-uuid' | 'minimal' | 'semantic';
|
|
2
|
+
export interface GenTestIdConfig {
|
|
3
|
+
prefix?: string;
|
|
4
|
+
strategy?: Strategy;
|
|
5
|
+
semanticModel?: string;
|
|
6
|
+
ollamaUrl?: string;
|
|
7
|
+
excludeFiles?: string[];
|
|
8
|
+
includeFiles?: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface ElementContext {
|
|
11
|
+
componentName: string;
|
|
12
|
+
filePath: string;
|
|
13
|
+
lineNumber?: number;
|
|
14
|
+
occurrence: number;
|
|
15
|
+
metadata?: Record<string, any>;
|
|
16
|
+
}
|
|
17
|
+
export interface NamingStrategy {
|
|
18
|
+
generateId(context: ElementContext): string;
|
|
19
|
+
}
|
|
20
|
+
export interface InjectionResult {
|
|
21
|
+
filePath: string;
|
|
22
|
+
injectedCount: number;
|
|
23
|
+
elements: Array<{
|
|
24
|
+
testId: string;
|
|
25
|
+
lineNumber: number;
|
|
26
|
+
elementType: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GenTestId } from './core/GenTestId.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { GenTestId } from '../core/GenTestId.js';
|
|
2
|
+
export interface ParseResult {
|
|
3
|
+
modifiedContent: string;
|
|
4
|
+
injectedCount: number;
|
|
5
|
+
elements: Array<{
|
|
6
|
+
testId: string;
|
|
7
|
+
lineNumber: number;
|
|
8
|
+
elementType: string;
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
11
|
+
export declare function parseVueFile(content: string, filePath: string, genTestId: GenTestId): Promise<ParseResult>;
|