@@ -5,44 +5,6 @@ import type { BrowserRPC } from '../client'
5
5
6
6
// this file should not import anything directly, only types
7
7
8
- function convertElementToXPath ( element : Element ) {
9
- if ( ! element || ! ( element instanceof Element ) ) {
10
- throw new Error (
11
- `Expected DOM element to be an instance of Element, received ${ typeof element } ` ,
12
- )
13
- }
14
-
15
- return getPathTo ( element )
16
- }
17
-
18
- function getPathTo ( element : Element ) : string {
19
- if ( element . id !== '' ) {
20
- return `id("${ element . id } ")`
21
- }
22
-
23
- if ( ! element . parentNode || element === document . documentElement ) {
24
- return element . tagName
25
- }
26
-
27
- let ix = 0
28
- const siblings = element . parentNode . childNodes
29
- for ( let i = 0 ; i < siblings . length ; i ++ ) {
30
- const sibling = siblings [ i ]
31
- if ( sibling === element ) {
32
- return `${ getPathTo ( element . parentNode as Element ) } /${ element . tagName } [${
33
- ix + 1
34
- } ]`
35
- }
36
- if (
37
- sibling . nodeType === 1
38
- && ( sibling as Element ) . tagName === element . tagName
39
- ) {
40
- ix ++
41
- }
42
- }
43
- return 'invalid xpath'
44
- }
45
-
46
8
// @ts -expect-error not typed global
47
9
const state = ( ) : WorkerGlobalState => __vitest_worker__
48
10
// @ts -expect-error not typed global
@@ -60,37 +22,99 @@ function triggerCommand<T>(command: string, ...args: any[]) {
60
22
61
23
const provider = runner ( ) . provider
62
24
25
+ function convertElementToCssSelector ( element : Element ) {
26
+ if ( ! element || ! ( element instanceof Element ) ) {
27
+ throw new Error (
28
+ `Expected DOM element to be an instance of Element, received ${ typeof element } ` ,
29
+ )
30
+ }
31
+
32
+ return getUniqueCssSelector ( element )
33
+ }
34
+
35
+ function getUniqueCssSelector ( el : Element ) {
36
+ const path = [ ]
37
+ let parent : null | ParentNode
38
+ let hasShadowRoot = false
39
+ // eslint-disable-next-line no-cond-assign
40
+ while ( parent = getParent ( el ) ) {
41
+ if ( ( parent as Element ) . shadowRoot ) {
42
+ hasShadowRoot = true
43
+ }
44
+
45
+ const tag = el . tagName
46
+ if ( el . id ) {
47
+ path . push ( `#${ el . id } ` )
48
+ }
49
+ else if ( ! el . nextElementSibling && ! el . previousElementSibling ) {
50
+ path . push ( tag )
51
+ }
52
+ else {
53
+ let index = 0
54
+ let sameTagSiblings = 0
55
+ let elementIndex = 0
56
+
57
+ for ( const sibling of parent . children ) {
58
+ index ++
59
+ if ( sibling . tagName === tag ) {
60
+ sameTagSiblings ++
61
+ }
62
+ if ( sibling === el ) {
63
+ elementIndex = index
64
+ }
65
+ }
66
+
67
+ if ( sameTagSiblings > 1 ) {
68
+ path . push ( `${ tag } :nth-child(${ elementIndex } )` )
69
+ }
70
+ else {
71
+ path . push ( tag )
72
+ }
73
+ }
74
+ el = parent as Element
75
+ } ;
76
+ return `${ provider === 'webdriverio' && hasShadowRoot ? '>>>' : '' } ${ path . reverse ( ) . join ( ' > ' ) } ` . toLowerCase ( )
77
+ }
78
+
79
+ function getParent ( el : Element ) {
80
+ const parent = el . parentNode
81
+ if ( parent instanceof ShadowRoot ) {
82
+ return parent . host
83
+ }
84
+ return parent
85
+ }
86
+
63
87
export const userEvent : UserEvent = {
64
88
// TODO: actually setup userEvent with config options
65
89
setup ( ) {
66
90
return userEvent
67
91
} ,
68
92
click ( element : Element , options : UserEventClickOptions = { } ) {
69
- const xpath = convertElementToXPath ( element )
70
- return triggerCommand ( '__vitest_click' , xpath , options )
93
+ const css = convertElementToCssSelector ( element )
94
+ return triggerCommand ( '__vitest_click' , css , options )
71
95
} ,
72
96
dblClick ( element : Element , options : UserEventClickOptions = { } ) {
73
- const xpath = convertElementToXPath ( element )
74
- return triggerCommand ( '__vitest_dblClick' , xpath , options )
97
+ const css = convertElementToCssSelector ( element )
98
+ return triggerCommand ( '__vitest_dblClick' , css , options )
75
99
} ,
76
100
tripleClick ( element : Element , options : UserEventClickOptions = { } ) {
77
- const xpath = convertElementToXPath ( element )
78
- return triggerCommand ( '__vitest_tripleClick' , xpath , options )
101
+ const css = convertElementToCssSelector ( element )
102
+ return triggerCommand ( '__vitest_tripleClick' , css , options )
79
103
} ,
80
104
selectOptions ( element , value ) {
81
105
const values = provider === 'webdriverio'
82
106
? getWebdriverioSelectOptions ( element , value )
83
107
: getSimpleSelectOptions ( element , value )
84
- const xpath = convertElementToXPath ( element )
85
- return triggerCommand ( '__vitest_selectOptions' , xpath , values )
108
+ const css = convertElementToCssSelector ( element )
109
+ return triggerCommand ( '__vitest_selectOptions' , css , values )
86
110
} ,
87
111
type ( element : Element , text : string , options : UserEventTypeOptions = { } ) {
88
- const xpath = convertElementToXPath ( element )
89
- return triggerCommand ( '__vitest_type' , xpath , text , options )
112
+ const css = convertElementToCssSelector ( element )
113
+ return triggerCommand ( '__vitest_type' , css , text , options )
90
114
} ,
91
115
clear ( element : Element ) {
92
- const xpath = convertElementToXPath ( element )
93
- return triggerCommand ( '__vitest_clear' , xpath )
116
+ const css = convertElementToCssSelector ( element )
117
+ return triggerCommand ( '__vitest_clear' , css )
94
118
} ,
95
119
tab ( options : UserEventTabOptions = { } ) {
96
120
return triggerCommand ( '__vitest_tab' , options )
@@ -99,23 +123,23 @@ export const userEvent: UserEvent = {
99
123
return triggerCommand ( '__vitest_keyboard' , text )
100
124
} ,
101
125
hover ( element : Element ) {
102
- const xpath = convertElementToXPath ( element )
103
- return triggerCommand ( '__vitest_hover' , xpath )
126
+ const css = convertElementToCssSelector ( element )
127
+ return triggerCommand ( '__vitest_hover' , css )
104
128
} ,
105
129
unhover ( element : Element ) {
106
- const xpath = convertElementToXPath ( element . ownerDocument . body )
107
- return triggerCommand ( '__vitest_hover' , xpath )
130
+ const css = convertElementToCssSelector ( element . ownerDocument . body )
131
+ return triggerCommand ( '__vitest_hover' , css )
108
132
} ,
109
133
110
134
// non userEvent events, but still useful
111
135
fill ( element : Element , text : string , options ) {
112
- const xpath = convertElementToXPath ( element )
113
- return triggerCommand ( '__vitest_fill' , xpath , text , options )
136
+ const css = convertElementToCssSelector ( element )
137
+ return triggerCommand ( '__vitest_fill' , css , text , options )
114
138
} ,
115
139
dragAndDrop ( source : Element , target : Element , options = { } ) {
116
- const sourceXpath = convertElementToXPath ( source )
117
- const targetXpath = convertElementToXPath ( target )
118
- return triggerCommand ( '__vitest_dragAndDrop' , sourceXpath , targetXpath , options )
140
+ const sourceCss = convertElementToCssSelector ( source )
141
+ const targetCss = convertElementToCssSelector ( target )
142
+ return triggerCommand ( '__vitest_dragAndDrop' , sourceCss , targetCss , options )
119
143
} ,
120
144
}
121
145
@@ -137,7 +161,7 @@ function getWebdriverioSelectOptions(element: Element, value: string | string[]
137
161
if ( typeof optionValue !== 'string' ) {
138
162
const index = options . indexOf ( optionValue as HTMLOptionElement )
139
163
if ( index === - 1 ) {
140
- throw new Error ( `The element ${ convertElementToXPath ( optionValue ) } was not found in the "select" options.` )
164
+ throw new Error ( `The element ${ convertElementToCssSelector ( optionValue ) } was not found in the "select" options.` )
141
165
}
142
166
143
167
return [ { index } ]
@@ -162,7 +186,7 @@ function getWebdriverioSelectOptions(element: Element, value: string | string[]
162
186
function getSimpleSelectOptions ( element : Element , value : string | string [ ] | HTMLElement [ ] | HTMLElement ) {
163
187
return ( Array . isArray ( value ) ? value : [ value ] ) . map ( ( v ) => {
164
188
if ( typeof v !== 'string' ) {
165
- return { element : convertElementToXPath ( v ) }
189
+ return { element : convertElementToCssSelector ( v ) }
166
190
}
167
191
return v
168
192
} )
@@ -220,7 +244,7 @@ export const page: BrowserPage = {
220
244
return triggerCommand ( '__vitest_screenshot' , name , {
221
245
...options ,
222
246
element : options . element
223
- ? convertElementToXPath ( options . element )
247
+ ? convertElementToCssSelector ( options . element )
224
248
: undefined ,
225
249
} )
226
250
} ,
0 commit comments