Skip to content

Commit e7acd0c

Browse files
authored
fix(browser): Improve unique CSS selector generation (#6243)
Co-authored-by: Zack Voase <[email protected]>
1 parent 073a50c commit e7acd0c

File tree

2 files changed

+56
-5
lines changed

2 files changed

+56
-5
lines changed

packages/browser/src/client/tester/context.ts

+35-5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,36 @@ function convertElementToCssSelector(element: Element) {
3232
return getUniqueCssSelector(element)
3333
}
3434

35+
function escapeIdForCSSSelector(id: string) {
36+
return id
37+
.split('')
38+
.map((char) => {
39+
const code = char.charCodeAt(0)
40+
41+
if (char === ' ' || char === '#' || char === '.' || char === ':' || char === '[' || char === ']' || char === '>' || char === '+' || char === '~' || char === '\\') {
42+
// Escape common special characters with backslashes
43+
return `\\${char}`
44+
}
45+
else if (code >= 0x10000) {
46+
// Unicode escape for characters outside the BMP
47+
return `\\${code.toString(16).toUpperCase().padStart(6, '0')} `
48+
}
49+
else if (code < 0x20 || code === 0x7F) {
50+
// Non-printable ASCII characters (0x00-0x1F and 0x7F) are escaped
51+
return `\\${code.toString(16).toUpperCase().padStart(2, '0')} `
52+
}
53+
else if (code >= 0x80) {
54+
// Non-ASCII characters (0x80 and above) are escaped
55+
return `\\${code.toString(16).toUpperCase().padStart(2, '0')} `
56+
}
57+
else {
58+
// Allowable characters are used directly
59+
return char
60+
}
61+
})
62+
.join('')
63+
}
64+
3565
function getUniqueCssSelector(el: Element) {
3666
const path = []
3767
let parent: null | ParentNode
@@ -44,10 +74,10 @@ function getUniqueCssSelector(el: Element) {
4474

4575
const tag = el.tagName
4676
if (el.id) {
47-
path.push(`#${el.id}`)
77+
path.push(`#${escapeIdForCSSSelector(el.id)}`)
4878
}
4979
else if (!el.nextElementSibling && !el.previousElementSibling) {
50-
path.push(tag)
80+
path.push(tag.toLowerCase())
5181
}
5282
else {
5383
let index = 0
@@ -65,15 +95,15 @@ function getUniqueCssSelector(el: Element) {
6595
}
6696

6797
if (sameTagSiblings > 1) {
68-
path.push(`${tag}:nth-child(${elementIndex})`)
98+
path.push(`${tag.toLowerCase()}:nth-child(${elementIndex})`)
6999
}
70100
else {
71-
path.push(tag)
101+
path.push(tag.toLowerCase())
72102
}
73103
}
74104
el = parent as Element
75105
};
76-
return `${provider === 'webdriverio' && hasShadowRoot ? '>>>' : ''}${path.reverse().join(' > ')}`.toLowerCase()
106+
return `${provider === 'webdriverio' && hasShadowRoot ? '>>>' : ''}${path.reverse().join(' > ')}`
77107
}
78108

79109
function getParent(el: Element) {

test/browser/test/userEvent.test.ts

+21
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,27 @@ describe('userEvent.click', () => {
7070

7171
expect(onClick).toHaveBeenCalled()
7272
})
73+
74+
test('clicks a button with complex HTML ID', async () => {
75+
const container = document.createElement('div')
76+
// This is similar to unique IDs generated by React's useId()
77+
container.id = ':r3:'
78+
const button = document.createElement('button')
79+
// Use uppercase and special characters
80+
button.id = 'A:Button'
81+
button.textContent = 'Click me'
82+
container.appendChild(button)
83+
document.body.appendChild(container)
84+
85+
const onClick = vi.fn()
86+
const dblClick = vi.fn()
87+
button.addEventListener('click', onClick)
88+
89+
await userEvent.click(button)
90+
91+
expect(onClick).toHaveBeenCalled()
92+
expect(dblClick).not.toHaveBeenCalled()
93+
})
7394
})
7495

7596
describe('userEvent.dblClick', () => {

0 commit comments

Comments
 (0)