UNPKG

claude-playwright

Version:

Seamless integration between Claude Code and Playwright MCP for efficient browser automation and testing

273 lines 8.23 kB
// src/core/smart-normalizer.ts import crypto from "crypto"; var SmartNormalizer = class { constructor() { this.POSITION_KEYWORDS = [ "before", "after", "first", "last", "next", "previous", "above", "below", "top", "bottom", "left", "right" ]; this.RELATION_KEYWORDS = [ "in", "of", "from", "to", "with", "by", "for" ]; this.STOP_WORDS = [ "the", "a", "an", "and", "or", "but", "at", "on" ]; this.ACTION_SYNONYMS = { "click": ["click", "press", "tap", "hit", "select", "choose"], "type": ["type", "enter", "input", "fill", "write"], "navigate": ["go", "navigate", "open", "visit", "load"], "hover": ["hover", "mouseover", "move"] }; } normalize(input) { const original = input.trim(); const features = this.extractFeatures(original); let text = original.toLowerCase(); const quotedStrings = []; text = text.replace(/(["'])((?:(?!\1)[^\\]|\\.)*)(\1)/g, (match, quote, content) => { quotedStrings.push(content); return `QUOTED_${quotedStrings.length - 1}`; }); const positions = this.extractPositions(text); text = this.normalizeActions(text); text = this.removeCommonPatterns(text); const words = text.split(/\s+/).filter((word) => word.length > 0); const tokens = []; const preserved = []; for (let i = 0; i < words.length; i++) { const word = words[i]; if (this.POSITION_KEYWORDS.includes(word)) { preserved.push({ word, position: i, context: words[i + 1] || null }); } else if (!this.STOP_WORDS.includes(word) && !this.RELATION_KEYWORDS.includes(word) && !["button", "field", "element"].includes(word)) { tokens.push(word); } } tokens.sort(); let normalized = tokens.join(" "); if (preserved.length > 0) { const posInfo = preserved.map( (p) => `${p.word}${p.context ? "-" + p.context : ""}` ).join(","); normalized += ` _pos:${posInfo}`; } if (quotedStrings.length > 0) { normalized += ` _quoted:${quotedStrings.join(",")}`; } const hash = crypto.createHash("md5").update(normalized).digest("hex"); return { normalized, tokens, positions, features, hash }; } extractFeatures(input) { const text = input.toLowerCase(); return { hasId: /#[\w-]+/.test(input), hasClass: /\.[\w-]+/.test(input), hasQuoted: /"[^"]+"|'[^']+'/.test(input), numbers: input.match(/\d+/g) || [], positions: this.extractPositions(text), attributes: this.extractAttributes(input), wordCount: input.split(/\s+/).length, hasImperative: /^(click|press|tap|select|enter|type|fill)/i.test(input), casePattern: this.detectCasePattern(input), isNavigation: /^(go|navigate|open|visit)/i.test(input), isFormAction: /(submit|enter|fill|type|input)/i.test(input), hasDataTestId: /data-test|testid|data-cy/i.test(input) }; } extractPositions(text) { const positions = []; const words = text.split(/\s+/); for (let i = 0; i < words.length; i++) { const word = words[i]; if (this.POSITION_KEYWORDS.includes(word)) { positions.push({ word, position: i, context: words[i + 1] || words[i - 1] || void 0 }); } } return positions; } extractAttributes(input) { const attributes = []; const patterns = [ /\[([^\]]+)\]/g, // [attribute=value] /data-[\w-]+/g, // data-testid /aria-[\w-]+/g, // aria-label /role="[\w-]+"/g, // role="button" /type="[\w-]+"/g, // type="submit" /placeholder="[^"]+"/g // placeholder="text" ]; for (const pattern of patterns) { const matches = input.match(pattern); if (matches) { attributes.push(...matches); } } return attributes; } detectCasePattern(input) { const hasLower = /[a-z]/.test(input); const hasUpper = /[A-Z]/.test(input); if (!hasLower && hasUpper) return "upper"; if (hasLower && !hasUpper) return "lower"; const words = input.split(/\s+/); const isTitleCase = words.every( (word) => /^[A-Z][a-z]*$/.test(word) || /^[a-z]+$/.test(word) ); return isTitleCase ? "title" : "mixed"; } normalizeActions(text) { for (const [canonical, synonyms] of Object.entries(this.ACTION_SYNONYMS)) { for (const synonym of synonyms) { const regex = new RegExp(`\\b${synonym}\\b`, "g"); text = text.replace(regex, canonical); } } return text; } removeCommonPatterns(text) { text = text.replace(/^(click|press|tap)(\s+on)?(\s+the)?/i, "click"); text = text.replace(/\s+(button|element|field)$/i, ""); text = text.replace(/button\s+/i, ""); text = text.replace(/\b(the|a|an)\b/g, ""); text = text.replace(/[^\w\s#._-]/g, " "); text = text.replace(/\s+/g, " ").trim(); return text; } // Utility methods for similarity calculateSimilarity(result1, result2) { const set1 = new Set(result1.tokens); const set2 = new Set(result2.tokens); const intersection = new Set([...set1].filter((x) => set2.has(x))); const union = /* @__PURE__ */ new Set([...set1, ...set2]); let similarity = intersection.size / union.size; const quoted1 = result1.normalized.match(/_quoted:([^_]*)/)?.[1] || ""; const quoted2 = result2.normalized.match(/_quoted:([^_]*)/)?.[1] || ""; if (quoted1 === quoted2 && quoted1.length > 0) { similarity += 0.2; } const pos1 = result1.normalized.match(/_pos:([^_]*)/)?.[1] || ""; const pos2 = result2.normalized.match(/_pos:([^_]*)/)?.[1] || ""; if (pos1 !== pos2 && (pos1.length > 0 || pos2.length > 0)) { similarity -= 0.3; } return Math.max(0, Math.min(1, similarity)); } // Fuzzy matching for typo tolerance damerauLevenshtein(a, b) { const da = {}; const maxdist = a.length + b.length; const H = []; H[-1] = []; H[-1][-1] = maxdist; for (let i = 0; i <= a.length; i++) { H[i] = []; H[i][-1] = maxdist; H[i][0] = i; } for (let j = 0; j <= b.length; j++) { H[-1][j] = maxdist; H[0][j] = j; } for (let i = 1; i <= a.length; i++) { let db = 0; for (let j = 1; j <= b.length; j++) { const k = da[b[j - 1]] || 0; const l = db; let cost = 1; if (a[i - 1] === b[j - 1]) { cost = 0; db = j; } H[i][j] = Math.min( H[i - 1][j] + 1, // insertion H[i][j - 1] + 1, // deletion H[i - 1][j - 1] + cost, // substitution H[k - 1][l - 1] + (i - k - 1) + 1 + (j - l - 1) // transposition ); } da[a[i - 1]] = i; } return H[a.length][b.length]; } // Create fuzzy variations for learning generateVariations(input) { const variations = [input]; const normalized = this.normalize(input); variations.push(input.toLowerCase()); variations.push(input.replace(/\s+/g, " ").trim()); variations.push(input.replace(/^(click|press)\s+/i, "tap ")); variations.push(input.replace(/\s+button$/i, "")); if (normalized.tokens.length <= 4) { const permutations = this.generateTokenPermutations(normalized.tokens); variations.push(...permutations.slice(0, 3)); } return [...new Set(variations)]; } generateTokenPermutations(tokens) { if (tokens.length <= 1) return tokens; if (tokens.length > 4) return []; const result = []; const permute = (arr, start = 0) => { if (start === arr.length) { result.push(arr.join(" ")); return; } for (let i = start; i < arr.length; i++) { [arr[start], arr[i]] = [arr[i], arr[start]]; permute(arr, start + 1); [arr[start], arr[i]] = [arr[i], arr[start]]; } }; permute([...tokens]); return result; } }; export { SmartNormalizer }; //# sourceMappingURL=smart-normalizer.js.map