33 * This file exist for better typed mock implementation, so that we can follow wdio/globals API updates more easily.
44 */
55import { vi } from 'vitest'
6- import type { ChainablePromiseArray , ChainablePromiseElement } from 'webdriverio'
6+ import type { ChainablePromiseArray , ChainablePromiseElement , ParsedCSSValue } from 'webdriverio'
77
88import type { RectReturn } from '@wdio/protocols'
99export type Size = Pick < RectReturn , 'width' | 'height' >
@@ -20,46 +20,172 @@ const getElementMethods = () => ({
2020 getHTML : vi . spyOn ( { getHTML : async ( ) => { return '<Html/>' } } , 'getHTML' ) ,
2121 getComputedLabel : vi . spyOn ( { getComputedLabel : async ( ) => 'Computed Label' } , 'getComputedLabel' ) ,
2222 getComputedRole : vi . spyOn ( { getComputedRole : async ( ) => 'Computed Role' } , 'getComputedRole' ) ,
23+ getAttribute : vi . spyOn ( { getAttribute : async ( _attr : string ) => 'some attribute' } , 'getAttribute' ) ,
24+ getCSSProperty : vi . spyOn ( { getCSSProperty : async ( _prop : string , _pseudo ?: string ) =>
25+ ( { value : 'colorValue' , parsed : { } } satisfies ParsedCSSValue ) } , 'getCSSProperty' ) ,
2326 getSize : vi . spyOn ( { getSize : async ( prop ?: 'width' | 'height' ) => {
2427 if ( prop === 'width' ) { return 100 }
2528 if ( prop === 'height' ) { return 50 }
2629 return { width : 100 , height : 50 } satisfies Size
27- } } , 'getSize' ) as unknown as WebdriverIO . Element [ 'getSize' ] ,
28- getAttribute : vi . spyOn ( { getAttribute : async ( _attr : string ) => 'some attribute' } , 'getAttribute' ) ,
30+ } } ,
31+ // Force wrong size & number typing, fixed by https://github.com/webdriverio/webdriverio/pull/15003
32+ 'getSize' ) as unknown as WebdriverIO . Element [ 'getSize' ] ,
33+ $,
34+ $$,
2935} satisfies Partial < WebdriverIO . Element > )
3036
31- function $ ( _selector : string ) {
32- const element = {
37+ export const elementFactory = ( _selector : string , index ?: number , parent : WebdriverIO . Browser | WebdriverIO . Element = browser ) : WebdriverIO . Element => {
38+ const partialElement = {
3339 selector : _selector ,
3440 ...getElementMethods ( ) ,
41+ index,
42+ $,
43+ $$,
44+ parent
45+ } satisfies Partial < WebdriverIO . Element >
46+
47+ const element = partialElement as unknown as WebdriverIO . Element
48+ element . getElement = vi . fn ( ) . mockResolvedValue ( element )
49+
50+ // Note: an element found has element.elementId while a not found has element.error
51+ element . elementId = `${ _selector } ${ index ? '-' + index : '' } `
52+
53+ return element
54+ }
55+
56+ export const notFoundElementFactory = ( _selector : string , index ?: number , parent : WebdriverIO . Browser | WebdriverIO . Element = browser ) : WebdriverIO . Element => {
57+ const partialElement = {
58+ selector : _selector ,
59+ index,
3560 $,
36- $$
37- } satisfies Partial < WebdriverIO . Element > as unknown as WebdriverIO . Element
38- element . getElement = async ( ) => Promise . resolve ( element )
39- return element as unknown as ChainablePromiseElement
61+ $$,
62+ isExisting : vi . fn ( ) . mockResolvedValue ( false ) ,
63+ parent
64+ } satisfies Partial < WebdriverIO . Element >
65+
66+ const element = partialElement as unknown as WebdriverIO . Element
67+
68+ // Note: an element found has element.elementId while a not found has element.error
69+ const elementId = `${ _selector } ${ index ? '-' + index : '' } `
70+ const error = ( functionName : string ) => new Error ( `Can't call ${ functionName } on element with selector ${ elementId } because element wasn't found` )
71+
72+ // Mimic element not found by throwing error on any method call beisde isExisting
73+ const notFoundElement = new Proxy ( element , {
74+ get ( target , prop ) {
75+ if ( prop in element ) {
76+ const value = element [ prop as keyof WebdriverIO . Element ]
77+ return value
78+ }
79+ if ( [ 'then' , 'catch' , 'toStringTag' ] . includes ( prop as string ) || typeof prop === 'symbol' ) {
80+ const value = Reflect . get ( target , prop )
81+ return typeof value === 'function' ? value . bind ( target ) : value
82+ }
83+ element . error = error ( prop as string )
84+ return ( ) => { throw element . error }
85+ }
86+ } )
87+
88+ element . getElement = vi . fn ( ) . mockResolvedValue ( notFoundElement )
89+
90+ return notFoundElement
4091}
4192
42- function $$ ( selector : string ) {
43- const length = ( this ) ?. _length || 2
44- const elements = Array ( length ) . fill ( null ) . map ( ( _ , index ) => {
45- const element = {
46- selector,
47- index,
48- ...getElementMethods ( ) ,
49- $,
50- $$
51- } satisfies Partial < WebdriverIO . Element > as unknown as WebdriverIO . Element
52- element . getElement = async ( ) => Promise . resolve ( element )
53- return element
54- } ) satisfies WebdriverIO . Element [ ] as unknown as WebdriverIO . ElementArray
55-
56- elements . foundWith = '$$'
57- elements . props = [ ]
58- elements . props . length = length
59- elements . selector = selector
60- elements . getElements = async ( ) => elements
61- elements . length = length
62- return elements as unknown as ChainablePromiseArray
93+ const $ = vi . fn ( ( _selector : string ) => {
94+ const element = elementFactory ( _selector )
95+
96+ // Wdio framework does return a Promise-wrapped element, so we need to mimic this behavior
97+ const chainablePromiseElement = Promise . resolve ( element ) as unknown as ChainablePromiseElement
98+
99+ // Ensure `'getElement' in chainableElement` is false while allowing to use `await chainableElement.getElement()`
100+ const runtimeChainableElement = new Proxy ( chainablePromiseElement , {
101+ get ( target , prop ) {
102+ if ( prop in element ) {
103+ return element [ prop as keyof WebdriverIO . Element ]
104+ }
105+ const value = Reflect . get ( target , prop )
106+ return typeof value === 'function' ? value . bind ( target ) : value
107+ }
108+ } )
109+ return runtimeChainableElement
110+ } )
111+
112+ const $$ = vi . fn ( ( selector : string ) => {
113+ const length = ( this as any ) ?. _length || 2
114+ return chainableElementArrayFactory ( selector , length )
115+ } )
116+
117+ export function elementArrayFactory ( selector : string , length ?: number ) : WebdriverIO . ElementArray {
118+ const elements : WebdriverIO . Element [ ] = Array ( length ) . fill ( null ) . map ( ( _ , index ) => elementFactory ( selector , index ) )
119+
120+ const elementArray = elements as unknown as WebdriverIO . ElementArray
121+
122+ elementArray . foundWith = '$$'
123+ elementArray . props = [ ]
124+ elementArray . selector = selector
125+ elementArray . getElements = vi . fn ( ) . mockResolvedValue ( elementArray )
126+ elementArray . filter = async < T > ( fn : ( element : WebdriverIO . Element , index : number , array : T [ ] ) => boolean | Promise < boolean > ) => {
127+ const results = await Promise . all ( elements . map ( ( el , i ) => fn ( el , i , elements as unknown as T [ ] ) ) )
128+ return Array . prototype . filter . call ( elements , ( _ , i ) => results [ i ] )
129+ }
130+ elementArray . parent = browser
131+
132+ // TODO Verify if we need to implement other array methods
133+ // [Symbol.iterator]: array[Symbol.iterator].bind(array)
134+ // filter: vi.fn().mockReturnThis(),
135+ // map: vi.fn().mockReturnThis(),
136+ // find: vi.fn().mockReturnThis(),
137+ // forEach: vi.fn(),
138+ // some: vi.fn(),
139+ // every: vi.fn(),
140+ // slice: vi.fn().mockReturnThis(),
141+ // toArray: vi.fn().mockReturnThis(),
142+ // getElements: vi.fn().mockResolvedValue(array)
143+
144+ return elementArray
145+ }
146+
147+ export function chainableElementArrayFactory ( selector : string , length : number ) {
148+ const elementArray = elementArrayFactory ( selector , length )
149+
150+ // Wdio framework does return a Promise-wrapped element, so we need to mimic this behavior
151+ const chainablePromiseArray = Promise . resolve ( elementArray ) as unknown as ChainablePromiseArray
152+
153+ // Ensure `'getElements' in chainableElements` is false while allowing to use `await chainableElement.getElements()`
154+ const runtimeChainablePromiseArray = new Proxy ( chainablePromiseArray , {
155+ get ( target , prop ) {
156+ if ( typeof prop === 'string' && / ^ \d + $ / . test ( prop ) ) {
157+ const index = parseInt ( prop , 10 )
158+ if ( index >= length ) {
159+ const error = new Error ( `Index out of bounds! $$(${ selector } ) returned only ${ length } elements.` )
160+ return new Proxy ( Promise . resolve ( ) , {
161+ get ( target , prop ) {
162+ if ( prop === 'then' ) {
163+ return ( resolve : any , reject : any ) => reject ( error )
164+ }
165+ // Allow resolving methods like 'catch', 'finally' normally from the promise if needed,
166+ // but usually we want any interaction to fail?
167+ // Actually, standard promise methods might be accessed.
168+ // But the user requirements says: `$$('foo')[3].getText()` should return a promise (that rejects).
169+
170+ // If accessing a property that exists on Promise (like catch, finally, Symbol.toStringTag), maybe we should be careful.
171+ // However, the test expects `el` (the proxy) to be a Promise instance.
172+ // And `el.getText()` to return a promise.
173+
174+ // If I return a function that returns a rejected promise for everything else:
175+ return ( ) => Promise . reject ( error )
176+ }
177+ } )
178+ }
179+ }
180+ if ( elementArray && prop in elementArray ) {
181+ return elementArray [ prop as keyof WebdriverIO . ElementArray ]
182+ }
183+ const value = Reflect . get ( target , prop )
184+ return typeof value === 'function' ? value . bind ( target ) : value
185+ }
186+ } )
187+
188+ return runtimeChainablePromiseArray
63189}
64190
65191export const browser = {
@@ -71,4 +197,3 @@ export const browser = {
71197 getTitle : vi . spyOn ( { getTitle : async ( ) => 'Example Domain' } , 'getTitle' ) ,
72198 call ( fn : Function ) { return fn ( ) } ,
73199} satisfies Partial < WebdriverIO . Browser > as unknown as WebdriverIO . Browser
74-
0 commit comments