Skip to main content
Glama
browserBaseSelectionHints.test.ts7.41 kB
import { describe, expect, test } from '@jest/globals'; import { BrowserToolBase } from '../tools/browser/base.js'; import type { ToolResponse } from '../tools/common/types.js'; class ExposedBrowserToolBase extends BrowserToolBase { constructor() { super({}); } // Required abstract method (not used in these tests) async execute(): Promise<ToolResponse> { throw new Error('Not implemented for tests'); } public hint(selector: string, totalCount: number): string { return this.buildNthSelectorHint(selector, totalCount); } public selectionInfo(selector: string, elementIndex: number, totalCount: number, preferredVisible = true): string { return this.formatElementSelectionInfo(selector, elementIndex, totalCount, preferredVisible); } public normalize(selector: string): string { return this.normalizeSelector(selector); } public sanitize(msg: string): string { // @ts-ignore accessing protected for test via wrapper return this.sanitizeSelectorEngineMessage(msg); } } describe('BrowserToolBase selection hints', () => { const tool = new ExposedBrowserToolBase(); test('buildNthSelectorHint provides copy-ready examples', () => { const hint = tool.hint('text=Add Recipe', 3); expect(hint).toContain('text=Add Recipe >> nth=0'); expect(hint).toContain('text=Add Recipe >> nth=2'); }); test('buildNthSelectorHint skips selectors already using nth syntax', () => { const hint = tool.hint('text=Add Recipe >> nth=1', 4); expect(hint).toBe(''); }); test('formatElementSelectionInfo appends nth hint and duplicate warning', () => { const info = tool.selectionInfo('testid:submit', 0, 2); expect(info).toContain('using element 1'); expect(info).toContain('>> nth=0'); expect(info).toContain('Test IDs should be unique'); }); test('formatElementSelectionInfo omits extras when only one element matches', () => { const info = tool.selectionInfo('text=Unique Button', 0, 1); expect(info).toBe(''); }); }); describe('BrowserToolBase selector normalization', () => { const tool = new ExposedBrowserToolBase(); describe('testid shortcuts', () => { test('converts testid: shortcut', () => { expect(tool.normalize('testid:submit-button')).toBe('[data-testid="submit-button"]'); }); test('supports combined selector after testid shortcut', () => { expect(tool.normalize('testid:chat-buttons button:first-child')).toBe( '[data-testid="chat-buttons"] button:first-child', ); }); test('supports combined selector with newline after testid shortcut', () => { const input = 'testid:chat-buttons\n button:first-child'; const expected = '[data-testid="chat-buttons"]\n button:first-child'; expect(tool.normalize(input)).toBe(expected); }); test('converts data-test: shortcut', () => { expect(tool.normalize('data-test:login-form')).toBe('[data-test="login-form"]'); }); test('converts data-cy: shortcut', () => { expect(tool.normalize('data-cy:username')).toBe('[data-cy="username"]'); }); }); describe('escape character normalization', () => { test('preserves single backslash before brackets', () => { expect(tool.normalize('.top-\\[36px\\]')).toBe('.top-\\[36px\\]'); }); test('collapses double backslashes before brackets to single', () => { expect(tool.normalize('.top-\\\\[36px\\\\]')).toBe('.top-\\[36px\\]'); }); test('preserves single backslash before colon', () => { expect(tool.normalize('.dark\\:bg-gray-700')).toBe('.dark\\:bg-gray-700'); }); test('collapses multiple backslashes before colon to single', () => { expect(tool.normalize('.flex-1.border-b.dark\\\\:border-gray-700')).toBe('.flex-1.border-b.dark\\:border-gray-700'); }); test('collapses extra escapes and preserves required ones', () => { expect(tool.normalize('.sticky.top-\\\\[36px\\\\].z-30')).toBe('.sticky.top-\\[36px\\].z-30'); }); test('preserves escapes for multiple colons and bracket values', () => { expect(tool.normalize('.dark\\:hover\\:bg-\\[#333\\]')).toBe('.dark\\:hover\\:bg-\\[#333\\]'); }); test('collapses triple or more backslashes to single', () => { expect(tool.normalize('.class-\\\\\\[value\\\\\\]')).toBe('.class-\\[value\\]'); }); }); describe('pass through unaffected selectors', () => { test('passes through regular class selectors', () => { expect(tool.normalize('.my-class')).toBe('.my-class'); }); test('passes through ID selectors', () => { expect(tool.normalize('#my-id')).toBe('#my-id'); }); test('passes through attribute selectors', () => { expect(tool.normalize('[data-value="test"]')).toBe('[data-value="test"]'); }); test('passes through text selectors', () => { expect(tool.normalize('text=Click me')).toBe('text=Click me'); }); test('passes through complex unescaped selectors', () => { expect(tool.normalize('.dark:bg-gray-700.hover:bg-blue-500')).toBe('.dark:bg-gray-700.hover:bg-blue-500'); }); test('passes through selectors with actual bracket values (already valid CSS)', () => { expect(tool.normalize('.top-[36px]')).toBe('.top-[36px]'); }); }); describe('edge cases', () => { test('handles empty selector', () => { expect(tool.normalize('')).toBe(''); }); test('handles selector with only backslashes', () => { // Backslashes are only removed when followed by [ ] or : // Standalone backslashes pass through unchanged (edge case, invalid selector) expect(tool.normalize('\\\\')).toBe('\\\\'); }); test('handles mixed testid shortcut with escaped characters', () => { // Testid shortcuts are processed first, so escapes in the value are preserved expect(tool.normalize('testid:my-button')).toBe('[data-testid="my-button"]'); }); }); describe('ID selector handling', () => { test('keeps simple IDs unchanged', () => { expect(tool.normalize('#simple-id')).toBe('#simple-id'); }); test('switches ID with special chars to id= engine', () => { expect(tool.normalize('#radix-\\:rc\\:-content-123')).toBe('id=radix-:rc:-content-123'); }); test('does not switch when ID is part of a compound selector', () => { expect(tool.normalize('#radix-\\:rc\\:-content-123 .child')).toBe('#radix-\\:rc\\:-content-123 .child'); }); test('preserves Tailwind width class bracket escapes in compound class selector', () => { const input = '.flex.min-w-\\[280px\\].max-w-\\[480px\\]'; expect(tool.normalize(input)).toBe(input); }); }); describe('error sanitization', () => { const tool = new ExposedBrowserToolBase(); test('removes stack frames and keeps concise message', () => { const raw = "locator.count: SyntaxError: Failed to execute 'querySelectorAll' on 'Document': '#bad:selector' is not a valid selector.\n at query (<anonymous>:4989:41)\n at <anonymous>:4999:7\n at SelectorEvaluatorImpl._cached (<anonymous>:4776:20)"; const sanitized = tool.sanitize(raw); expect(sanitized).toContain("is not a valid selector"); expect(sanitized).not.toContain("at query ("); expect(sanitized).not.toContain("<anonymous>:"); }); }); });

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/antonzherdev/mcp-web-inspector'

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