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,151 @@ 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,
3542 $,
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
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
4054}
4155
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
56+ export const notFoundElementFactory = ( _selector : string , index ?: number , parent : WebdriverIO . Browser | WebdriverIO . Element = browser ) : WebdriverIO . Element => {
57+ const partialElement = {
58+ selector : _selector ,
59+ index,
60+ $,
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
91+ }
92+
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+ return elementArray
133+ }
134+
135+ export function chainableElementArrayFactory ( selector : string , length : number ) {
136+ const elementArray = elementArrayFactory ( selector , length )
137+
138+ // Wdio framework does return a Promise-wrapped element, so we need to mimic this behavior
139+ const chainablePromiseArray = Promise . resolve ( elementArray ) as unknown as ChainablePromiseArray
140+
141+ // Ensure `'getElements' in chainableElements` is false while allowing to use `await chainableElement.getElements()`
142+ const runtimeChainablePromiseArray = new Proxy ( chainablePromiseArray , {
143+ get ( target , prop ) {
144+ if ( typeof prop === 'string' && / ^ \d + $ / . test ( prop ) ) {
145+ // Simulate index out of bounds error when asking for an element outside the array length
146+ const index = parseInt ( prop , 10 )
147+ if ( index >= length ) {
148+ const error = new Error ( `Index out of bounds! $$(${ selector } ) returned only ${ length } elements.` )
149+ return new Proxy ( Promise . resolve ( ) , {
150+ get ( target , prop ) {
151+ if ( prop === 'then' ) {
152+ return ( resolve : any , reject : any ) => reject ( error )
153+ }
154+ return ( ) => Promise . reject ( error )
155+ }
156+ } )
157+ }
158+ }
159+ if ( elementArray && prop in elementArray ) {
160+ return elementArray [ prop as keyof WebdriverIO . ElementArray ]
161+ }
162+ const value = Reflect . get ( target , prop )
163+ return typeof value === 'function' ? value . bind ( target ) : value
164+ }
165+ } )
166+
167+ return runtimeChainablePromiseArray
63168}
64169
65170export const browser = {
@@ -71,4 +176,3 @@ export const browser = {
71176 getTitle : vi . spyOn ( { getTitle : async ( ) => 'Example Domain' } , 'getTitle' ) ,
72177 call ( fn : Function ) { return fn ( ) } ,
73178} satisfies Partial < WebdriverIO . Browser > as unknown as WebdriverIO . Browser
74-
0 commit comments