Skip to content

Commit

Permalink
fix(browser): Improve unique CSS selector generation (#6243)
Browse files Browse the repository at this point in the history
Co-authored-by: Zack Voase <zvoase@netflix.com>
  • Loading branch information
zacharyvoase and zacharyvoase authored Jul 30, 2024
1 parent 073a50c commit e7acd0c
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 5 deletions.
40 changes: 35 additions & 5 deletions packages/browser/src/client/tester/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,36 @@ function convertElementToCssSelector(element: Element) {
return getUniqueCssSelector(element)
}

function escapeIdForCSSSelector(id: string) {
return id
.split('')
.map((char) => {
const code = char.charCodeAt(0)

if (char === ' ' || char === '#' || char === '.' || char === ':' || char === '[' || char === ']' || char === '>' || char === '+' || char === '~' || char === '\\') {
// Escape common special characters with backslashes
return `\\${char}`
}
else if (code >= 0x10000) {
// Unicode escape for characters outside the BMP
return `\\${code.toString(16).toUpperCase().padStart(6, '0')} `
}
else if (code < 0x20 || code === 0x7F) {
// Non-printable ASCII characters (0x00-0x1F and 0x7F) are escaped
return `\\${code.toString(16).toUpperCase().padStart(2, '0')} `
}
else if (code >= 0x80) {
// Non-ASCII characters (0x80 and above) are escaped
return `\\${code.toString(16).toUpperCase().padStart(2, '0')} `
}
else {
// Allowable characters are used directly
return char
}
})
.join('')
}

function getUniqueCssSelector(el: Element) {
const path = []
let parent: null | ParentNode
Expand All @@ -44,10 +74,10 @@ function getUniqueCssSelector(el: Element) {

const tag = el.tagName
if (el.id) {
path.push(`#${el.id}`)
path.push(`#${escapeIdForCSSSelector(el.id)}`)
}
else if (!el.nextElementSibling && !el.previousElementSibling) {
path.push(tag)
path.push(tag.toLowerCase())
}
else {
let index = 0
Expand All @@ -65,15 +95,15 @@ function getUniqueCssSelector(el: Element) {
}

if (sameTagSiblings > 1) {
path.push(`${tag}:nth-child(${elementIndex})`)
path.push(`${tag.toLowerCase()}:nth-child(${elementIndex})`)
}
else {
path.push(tag)
path.push(tag.toLowerCase())
}
}
el = parent as Element
};
return `${provider === 'webdriverio' && hasShadowRoot ? '>>>' : ''}${path.reverse().join(' > ')}`.toLowerCase()
return `${provider === 'webdriverio' && hasShadowRoot ? '>>>' : ''}${path.reverse().join(' > ')}`
}

function getParent(el: Element) {
Expand Down
21 changes: 21 additions & 0 deletions test/browser/test/userEvent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,27 @@ describe('userEvent.click', () => {

expect(onClick).toHaveBeenCalled()
})

test('clicks a button with complex HTML ID', async () => {
const container = document.createElement('div')
// This is similar to unique IDs generated by React's useId()
container.id = ':r3:'
const button = document.createElement('button')
// Use uppercase and special characters
button.id = 'A:Button'
button.textContent = 'Click me'
container.appendChild(button)
document.body.appendChild(container)

const onClick = vi.fn()
const dblClick = vi.fn()
button.addEventListener('click', onClick)

await userEvent.click(button)

expect(onClick).toHaveBeenCalled()
expect(dblClick).not.toHaveBeenCalled()
})
})

describe('userEvent.dblClick', () => {
Expand Down

0 comments on commit e7acd0c

Please sign in to comment.