claude-playwright
Version:
Seamless integration between Claude Code and Playwright MCP for efficient browser automation and testing
308 lines (307 loc) • 9.92 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/core/smart-normalizer.ts
var smart_normalizer_exports = {};
__export(smart_normalizer_exports, {
SmartNormalizer: () => SmartNormalizer
});
module.exports = __toCommonJS(smart_normalizer_exports);
var import_crypto = __toESM(require("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 = import_crypto.default.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;
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
SmartNormalizer
});
//# sourceMappingURL=smart-normalizer.cjs.map
;