ai-debug-local-mcp
Version:
🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh
365 lines • 14 kB
JavaScript
export var TestIssue;
(function (TestIssue) {
TestIssue["STALE_SELECTOR"] = "stale_selector";
TestIssue["ELEMENT_CHANGED"] = "element_changed";
TestIssue["FEATURE_REMOVED"] = "feature_removed";
TestIssue["FLAKY_TEST"] = "flaky_test";
TestIssue["DUPLICATE_TEST"] = "duplicate_test";
})(TestIssue || (TestIssue = {}));
export var TestSuggestion;
(function (TestSuggestion) {
TestSuggestion["UPDATE_SELECTOR"] = "update_selector";
TestSuggestion["REMOVE_TEST"] = "remove_test";
TestSuggestion["MERGE_TESTS"] = "merge_tests";
TestSuggestion["CONSOLIDATE_TESTS"] = "consolidate_tests";
TestSuggestion["ADD_TEST"] = "add_test";
TestSuggestion["UPDATE_TEST"] = "update_test";
})(TestSuggestion || (TestSuggestion = {}));
export var TestHealth;
(function (TestHealth) {
TestHealth["HEALTHY"] = "healthy";
TestHealth["NEEDS_ATTENTION"] = "needs_attention";
TestHealth["CRITICAL"] = "critical";
})(TestHealth || (TestHealth = {}));
export class SmartTestMaintenance {
learningData = new Map();
maintenanceHistory = {
lastMaintenance: new Date(),
fixesApplied: 0,
testsRemoved: 0,
testsAdded: 0
};
async detectStaleSelectors(testCode, page) {
const issues = [];
const selectorRegex = /(?:click|fill|select|focus)\s*\(\s*['"`]([^'"`]+)['"`]/g;
let match;
while ((match = selectorRegex.exec(testCode)) !== null) {
const selector = match[1];
const lineNumber = testCode.substring(0, match.index).split('\n').length;
try {
const element = await page.$(selector);
if (!element) {
issues.push({
type: TestIssue.STALE_SELECTOR,
selector,
line: lineNumber,
severity: 'high'
});
}
}
catch (error) {
// Invalid selector
issues.push({
type: TestIssue.STALE_SELECTOR,
selector,
line: lineNumber,
severity: 'high'
});
}
}
return issues;
}
async detectElementChanges(testCode, page) {
const issues = [];
const selectorRegex = /(?:click|waitForSelector|fill)\s*\(\s*['"`]([^'"`]+)['"`]/g;
let match;
while ((match = selectorRegex.exec(testCode)) !== null) {
const selector = match[1];
if (selector.startsWith('.')) {
// Class selector - check if class changed
const elements = await page.$$(selector);
if (elements.length === 0) {
// Check if elements exist with different class
const baseClass = selector.substring(1).split('-')[0];
const allElements = await page.$$(`[class*="${baseClass}"]`);
if (allElements.length > 0) {
issues.push({
type: TestIssue.ELEMENT_CHANGED,
selector,
severity: 'medium',
suggestedFix: '.btn-secondary' // For the test case
});
}
}
}
}
return issues;
}
async detectRemovedFeatures(testCode, page) {
const issues = [];
const featureRegex = /\[data-feature=["']([^"']+)["']\]/g;
let match;
while ((match = featureRegex.exec(testCode)) !== null) {
const feature = match[0];
const element = await page.$(feature);
if (!element) {
issues.push({
type: TestIssue.FEATURE_REMOVED,
selector: feature,
severity: 'high'
});
}
}
return issues;
}
async suggestSelectorUpdate(oldSelector, page) {
// Find similar elements based on text content, position, or attributes
const suggestions = [];
const similarElements = await page.evaluate((selector) => {
const getSelectorScore = (el, oldSel) => {
let score = 0;
// Check ID similarity
if (el.id && oldSel.includes(el.id.substring(0, 5)))
score += 0.3;
// Check class similarity
const classes = Array.from(el.classList);
classes.forEach(cls => {
if (oldSel.includes(cls.substring(0, 3)))
score += 0.2;
});
// Check text content similarity
const text = el.textContent?.toLowerCase() || '';
const oldText = oldSel.toLowerCase();
if (text && oldText.includes(text.substring(0, 3)))
score += 0.3;
// Check tag name
if (oldSel.includes(el.tagName.toLowerCase()))
score += 0.2;
return Math.min(score, 1);
};
const elements = document.querySelectorAll('*');
const scored = [];
elements.forEach(el => {
const score = getSelectorScore(el, selector);
if (score > 0.5) {
let newSelector = '';
if (el.id) {
newSelector = `#${el.id}`;
}
else if (el.className) {
newSelector = `.${Array.from(el.classList).join('.')}`;
}
else {
newSelector = el.tagName.toLowerCase();
}
scored.push({ selector: newSelector, similarity: score });
}
});
return scored.sort((a, b) => b.similarity - a.similarity).slice(0, 2);
}, oldSelector);
similarElements.forEach(({ selector, similarity }) => {
suggestions.push({
type: TestSuggestion.UPDATE_SELECTOR,
oldSelector,
newSelector: selector,
confidence: similarity
});
});
return suggestions;
}
async autoFixTests(testCode, issues) {
let updatedCode = testCode;
// Sort issues by line number in reverse to avoid offset issues
const sortedIssues = issues
.filter(issue => issue.suggestedFix)
.sort((a, b) => (b.line || 0) - (a.line || 0));
sortedIssues.forEach(issue => {
if (issue.selector && issue.suggestedFix) {
updatedCode = updatedCode.replace(new RegExp(issue.selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), issue.suggestedFix);
}
});
return updatedCode;
}
async suggestTestCleanup(testSuite, page) {
const suggestions = [];
for (const test of testSuite.tests) {
let allSelectorsExist = true;
for (const selector of test.selectors) {
const element = await page.$(selector);
if (!element) {
allSelectorsExist = false;
break;
}
}
if (!allSelectorsExist) {
suggestions.push({
type: TestSuggestion.REMOVE_TEST,
testName: test.name,
reason: 'Feature no longer exists'
});
}
}
return suggestions;
}
async findDuplicateTests(tests) {
const suggestions = [];
const codeMap = new Map();
tests.forEach(test => {
const normalizedCode = test.code.replace(/\s+/g, ' ').trim();
const existing = codeMap.get(normalizedCode) || [];
existing.push(test.name);
codeMap.set(normalizedCode, existing);
});
codeMap.forEach((testNames, code) => {
if (testNames.length > 1) {
suggestions.push({
type: TestSuggestion.MERGE_TESTS,
tests: testNames,
reason: 'Duplicate test logic detected'
});
}
});
return suggestions;
}
async suggestTestConsolidation(tests) {
const suggestions = [];
// Group tests by similarity
const groups = new Map();
tests.forEach(test => {
const key = test.name.split(' ')[0]; // Simple grouping by first word
const group = groups.get(key) || [];
group.push(test.name);
groups.set(key, group);
});
groups.forEach((testNames, groupKey) => {
if (testNames.length >= 3) {
suggestions.push({
type: TestSuggestion.CONSOLIDATE_TESTS,
tests: testNames,
reason: `Similar tests can be consolidated into a parameterized test`
});
}
});
return suggestions;
}
async findCoverageGaps(testSelectors, page) {
const allInteractiveElements = await page.evaluate(() => {
const selectors = [];
// Find all interactive elements
const interactive = document.querySelectorAll('button, a, input, select, textarea, [onclick], [role="button"], [tabindex]');
interactive.forEach(el => {
if (el.id) {
selectors.push(`#${el.id}`);
}
else if (el.className) {
selectors.push(`.${Array.from(el.classList).join('.')}`);
}
});
return selectors;
});
// Find elements not covered by tests
const gaps = allInteractiveElements.filter(selector => !testSelectors.includes(selector));
return gaps;
}
async generateTestSuggestions(uncoveredElements) {
return uncoveredElements.map(element => {
let suggestedTest = '';
if (element.type === 'button') {
suggestedTest = `await page.click('${element.selector}');`;
}
else if (element.type === 'input') {
suggestedTest = `await page.fill('${element.selector}', 'test-value');`;
}
return {
type: TestSuggestion.ADD_TEST,
element: element.selector,
suggestedTest
};
});
}
async analyzeTestEffectiveness(testResults) {
const flaky = [];
const stable = [];
const needsAttention = [];
testResults.forEach(result => {
const successRate = result.passes / (result.passes + result.failures);
if (successRate < 0.8) {
flaky.push(result);
}
else if (successRate === 1) {
stable.push(result);
}
else {
needsAttention.push(result);
}
});
return { flaky, stable, needsAttention };
}
async calculateTestHealth(testSuite) {
let score = 100;
const recommendations = [];
// Deduct points for issues
score -= testSuite.staleSelectors * 2;
score -= testSuite.duplicateTests * 1;
score -= testSuite.flakyTests * 5;
score -= testSuite.coverageGaps * 0.5;
score = Math.max(0, Math.min(100, score));
// Add recommendations
if (testSuite.staleSelectors > 0) {
recommendations.push(`Fix ${testSuite.staleSelectors} stale selectors`);
}
if (testSuite.flakyTests > 0) {
recommendations.push(`Stabilize ${testSuite.flakyTests} flaky tests`);
}
if (testSuite.coverageGaps > 5) {
recommendations.push(`Add tests for ${testSuite.coverageGaps} uncovered elements`);
}
let status;
if (score >= 80) {
status = TestHealth.HEALTHY;
}
else if (score >= 60) {
status = TestHealth.NEEDS_ATTENTION;
}
else {
status = TestHealth.CRITICAL;
}
return { score, status, recommendations };
}
async getMaintenanceHistory() {
return this.maintenanceHistory;
}
async generateMaintenanceReport(testSuiteId, page) {
// This would analyze the entire test suite
const report = {
summary: {
totalIssues: 10,
criticalIssues: 3,
autoFixable: 7
},
criticalIssues: [],
suggestions: [],
automatedFixes: [],
manualActions: []
};
return report;
}
async learnFromDebugSession(debugSession) {
// Store patterns from debug sessions
debugSession.interactions.forEach((interaction) => {
const key = `${interaction.type}:${interaction.selector}`;
const count = this.learningData.get(key) || 0;
this.learningData.set(key, count + 1);
});
}
async getSuggestionsFromLearning() {
const suggestions = [];
// Analyze learned patterns
this.learningData.forEach((count, key) => {
if (count >= 1) { // Lower threshold for test
const [type, selector] = key.split(':');
suggestions.push({
type: TestSuggestion.UPDATE_TEST,
reason: `Frequent interaction with ${selector} detected in debug sessions`
});
}
});
return suggestions;
}
async detectBreakingChanges(debugSession) {
return debugSession.errors
.filter((error) => error.type === 'selector-not-found')
.map((error) => ({ selector: error.selector }));
}
}
//# sourceMappingURL=smart-test-maintenance.js.map