Skip to main content
Glama
locator-engine.js15.2 kB
"use strict"; /** * Locator Engine - Core locator generation logic for LocatorLabs MCP * * @author Naveen AutomationLabs * @license MIT * @date 2025 * @see https://github.com/naveenanimation20/locatorlabs-mcp */ Object.defineProperty(exports, "__esModule", { value: true }); exports.LocatorEngine = void 0; class LocatorEngine { browserManager; constructor(browserManager) { this.browserManager = browserManager; } async getLocators(url, elementDescription) { const page = await this.browserManager.navigateTo(url); const elements = await this.findMatchingElements(page, elementDescription); if (elements.length === 0) { return { element: elementDescription, locators: [], recommended: "No matching elements found", }; } const element = elements[0]; const locators = this.generateLocators(element); const ranked = this.rankLocators(locators); return { element: elementDescription, locators: ranked, recommended: ranked[0]?.locator || "No locator found", }; } async analyzePage(url, elementTypes) { const page = await this.browserManager.navigateTo(url); const allElements = await this.getAllInteractiveElements(page); let filtered = allElements; if (elementTypes && elementTypes.length > 0) { filtered = allElements.filter((el) => elementTypes.some((t) => el.tagName.toLowerCase().includes(t) || el.type?.includes(t))); } const results = filtered.map((el) => { const locators = this.generateLocators(el); const ranked = this.rankLocators(locators); return { element: this.describeElement(el), type: el.tagName.toLowerCase(), locators: ranked.slice(0, 3), recommended: ranked[0]?.locator || "N/A", }; }); return { url, totalElements: results.length, elements: results, }; } async generatePageObject(url, className, language) { const page = await this.browserManager.navigateTo(url); const elements = await this.getAllInteractiveElements(page); const pageElements = elements.map((el) => { const locators = this.generateLocators(el); const ranked = this.rankLocators(locators); return { name: this.generatePropertyName(el), locator: ranked[0]?.locator || `locator('${el.selector}')`, element: el, }; }); switch (language) { case "python": return this.generatePythonPOM(className, pageElements, url); case "javascript": return this.generateJavaScriptPOM(className, pageElements, url); default: return this.generateTypeScriptPOM(className, pageElements, url); } } async validateLocator(url, locatorStr) { const page = await this.browserManager.navigateTo(url); try { const locator = this.parseLocatorString(page, locatorStr); const count = await locator.count(); const suggestions = []; if (count === 0) { suggestions.push("Element not found. Check if the page has loaded completely."); suggestions.push("Verify the locator syntax is correct."); } else if (count > 1) { suggestions.push(`Found ${count} elements. Consider adding more specificity.`); suggestions.push("Try using getByRole with name option for uniqueness."); } return { isValid: count > 0, matchCount: count, isUnique: count === 1, suggestions, }; } catch (e) { return { isValid: false, matchCount: 0, isUnique: false, suggestions: [`Invalid locator syntax: ${e instanceof Error ? e.message : String(e)}`], }; } } async findMatchingElements(page, description) { const keywords = description.toLowerCase().split(/\s+/); const allElements = await this.getAllInteractiveElements(page); return allElements.filter((el) => { const searchText = [ el.id, el.name, el.className, el.text, el.placeholder, el.ariaLabel, el.type, el.title, el.tagName ].filter(Boolean).join(" ").toLowerCase(); return keywords.some((kw) => searchText.includes(kw)); }); } async getAllInteractiveElements(page) { return await page.evaluate(() => { const interactiveSelectors = [ "button", "a", "input", "select", "textarea", "[role='button']", "[role='link']", "[role='textbox']", "[role='checkbox']", "[role='radio']", "[role='combobox']", "[onclick]", "[tabindex]" ]; const elements = []; const seen = new Set(); interactiveSelectors.forEach((sel) => { document.querySelectorAll(sel).forEach((el) => { if (seen.has(el)) return; seen.add(el); const rect = el.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) return; const htmlEl = el; const inputEl = el; elements.push({ tagName: el.tagName.toLowerCase(), id: el.id || undefined, name: inputEl.name || undefined, className: el.className || undefined, type: inputEl.type || undefined, placeholder: inputEl.placeholder || undefined, text: htmlEl.innerText?.trim().substring(0, 50) || undefined, ariaLabel: el.getAttribute("aria-label") || undefined, role: el.getAttribute("role") || undefined, testId: el.getAttribute("data-testid") || el.getAttribute("data-test-id") || undefined, href: el.href || undefined, title: el.getAttribute("title") || undefined, value: inputEl.value || undefined, xpath: getXPath(el), selector: getUniqueSelector(el), }); }); }); function getXPath(el) { if (el.id) return `//*[@id="${el.id}"]`; const parts = []; let current = el; while (current && current.nodeType === Node.ELEMENT_NODE) { let index = 1; let sibling = current.previousElementSibling; while (sibling) { if (sibling.tagName === current.tagName) index++; sibling = sibling.previousElementSibling; } parts.unshift(`${current.tagName.toLowerCase()}[${index}]`); current = current.parentElement; } return "/" + parts.join("/"); } function getUniqueSelector(el) { if (el.id) return `#${el.id}`; const tag = el.tagName.toLowerCase(); const classes = Array.from(el.classList).slice(0, 2).join("."); return classes ? `${tag}.${classes}` : tag; } return elements; }); } generateLocators(el) { const locators = []; // Role-based (highest priority for Playwright) if (el.role || this.inferRole(el)) { const role = el.role || this.inferRole(el); const name = el.text || el.ariaLabel || el.title; if (name) { locators.push({ type: "role", locator: `getByRole('${role}', { name: '${this.escape(name)}' })`, reliability: 95, description: "Playwright recommended - accessible and stable", }); } } // Test ID (if exists) if (el.testId) { locators.push({ type: "testId", locator: `getByTestId('${el.testId}')`, reliability: 98, description: "Best choice - explicitly set for testing", }); } // Placeholder (for inputs) if (el.placeholder) { locators.push({ type: "placeholder", locator: `getByPlaceholder('${this.escape(el.placeholder)}')`, reliability: 85, description: "Good for form inputs", }); } // Label (for form elements) if (el.ariaLabel) { locators.push({ type: "label", locator: `getByLabel('${this.escape(el.ariaLabel)}')`, reliability: 88, description: "Accessible label-based locator", }); } // Text-based if (el.text && el.tagName !== "input") { locators.push({ type: "text", locator: `getByText('${this.escape(el.text)}')`, reliability: 75, description: "Text content based - may break if text changes", }); } // ID-based if (el.id) { locators.push({ type: "id", locator: `locator('#${el.id}')`, reliability: 90, description: "ID selector - stable if ID is meaningful", }); } // CSS selector locators.push({ type: "css", locator: `locator('${el.selector}')`, reliability: 60, description: "CSS selector - may be brittle", }); // XPath (lowest priority) locators.push({ type: "xpath", locator: `locator('${el.xpath}')`, reliability: 40, description: "XPath - avoid unless necessary", }); return locators; } inferRole(el) { const tag = el.tagName.toLowerCase(); const type = el.type?.toLowerCase(); if (tag === "button" || type === "submit" || type === "button") return "button"; if (tag === "a") return "link"; if (tag === "input" && type === "checkbox") return "checkbox"; if (tag === "input" && type === "radio") return "radio"; if (tag === "input" || tag === "textarea") return "textbox"; if (tag === "select") return "combobox"; if (tag === "img") return "img"; return null; } rankLocators(locators) { return locators.sort((a, b) => b.reliability - a.reliability); } describeElement(el) { const parts = []; if (el.text) parts.push(`"${el.text}"`); if (el.placeholder) parts.push(`placeholder: "${el.placeholder}"`); if (el.type) parts.push(`type: ${el.type}`); parts.push(`<${el.tagName}>`); return parts.join(" ") || el.tagName; } generatePropertyName(el) { let name = el.id || el.name || el.text || el.placeholder || el.ariaLabel || el.tagName; name = name.replace(/[^a-zA-Z0-9]/g, " ").trim(); const words = name.split(/\s+/).slice(0, 3); return words.map((w, i) => i === 0 ? w.toLowerCase() : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(""); } escape(str) { return str.replace(/'/g, "\\'").substring(0, 50); } parseLocatorString(page, str) { if (str.startsWith("getByRole")) { const match = str.match(/getByRole\('(\w+)'(?:,\s*\{\s*name:\s*'([^']+)'\s*\})?\)/); if (match) return page.getByRole(match[1], match[2] ? { name: match[2] } : undefined); } if (str.startsWith("getByText")) { const match = str.match(/getByText\('([^']+)'\)/); if (match) return page.getByText(match[1]); } if (str.startsWith("getByTestId")) { const match = str.match(/getByTestId\('([^']+)'\)/); if (match) return page.getByTestId(match[1]); } if (str.startsWith("getByPlaceholder")) { const match = str.match(/getByPlaceholder\('([^']+)'\)/); if (match) return page.getByPlaceholder(match[1]); } if (str.startsWith("locator")) { const match = str.match(/locator\('([^']+)'\)/); if (match) return page.locator(match[1]); } return page.locator(str); } generateTypeScriptPOM(className, elements, url) { const props = elements.map((e) => ` readonly ${e.name}: Locator;`).join("\n"); const inits = elements.map((e) => ` this.${e.name} = page.${e.locator};`).join("\n"); return `import { Page, Locator } from '@playwright/test'; /** * Page Object Model for: ${url} * Generated by LocatorLabs MCP */ export class ${className} { readonly page: Page; ${props} constructor(page: Page) { this.page = page; ${inits} } async navigate() { await this.page.goto('${url}'); } } `; } generateJavaScriptPOM(className, elements, url) { const inits = elements.map((e) => ` this.${e.name} = page.${e.locator};`).join("\n"); return `/** * Page Object Model for: ${url} * Generated by LocatorLabs MCP */ class ${className} { constructor(page) { this.page = page; ${inits} } async navigate() { await this.page.goto('${url}'); } } module.exports = { ${className} }; `; } generatePythonPOM(className, elements, url) { const props = elements.map((e) => { const pyLocator = e.locator .replace(/getByRole/g, "get_by_role") .replace(/getByText/g, "get_by_text") .replace(/getByTestId/g, "get_by_test_id") .replace(/getByPlaceholder/g, "get_by_placeholder"); return ` self.${this.toSnakeCase(e.name)} = page.${pyLocator}`; }).join("\n"); return `""" Page Object Model for: ${url} Generated by LocatorLabs MCP """ from playwright.sync_api import Page class ${className}: def __init__(self, page: Page): self.page = page ${props} def navigate(self): self.page.goto('${url}') `; } toSnakeCase(str) { return str.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, ""); } } exports.LocatorEngine = LocatorEngine; //# sourceMappingURL=locator-engine.js.map

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/naveenanimation20/locatorlabs-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server