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' >
@@ -21,11 +21,17 @@ const getElementMethods = () => ({
2121 getComputedLabel : vi . spyOn ( { getComputedLabel : async ( ) => 'Computed Label' } , 'getComputedLabel' ) ,
2222 getComputedRole : vi . spyOn ( { getComputedRole : async ( ) => 'Computed Role' } , 'getComputedRole' ) ,
2323 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' ) ,
2426 getSize : vi . spyOn ( { getSize : async ( prop ?: 'width' | 'height' ) => {
2527 if ( prop === 'width' ) { return 100 }
2628 if ( prop === 'height' ) { return 50 }
2729 return { width : 100 , height : 50 } satisfies Size
28- } } , 'getSize' ) as unknown as WebdriverIO . Element [ 'getSize' ] ,
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
3137export const elementFactory = ( _selector : string , index ?: number , parent : WebdriverIO . Browser | WebdriverIO . Element = browser ) : WebdriverIO . Element => {
@@ -40,9 +46,50 @@ export const elementFactory = (_selector: string, index?: number, parent: Webdri
4046
4147 const element = partialElement as unknown as WebdriverIO . Element
4248 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+
4353 return element
4454}
4555
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+
4693const $ = vi . fn ( ( _selector : string ) => {
4794 const element = elementFactory ( _selector )
4895
@@ -106,6 +153,30 @@ export function chainableElementArrayFactory(selector: string, length: number) {
106153 // Ensure `'getElements' in chainableElements` is false while allowing to use `await chainableElement.getElements()`
107154 const runtimeChainablePromiseArray = new Proxy ( chainablePromiseArray , {
108155 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+ }
109180 if ( elementArray && prop in elementArray ) {
110181 return elementArray [ prop as keyof WebdriverIO . ElementArray ]
111182 }
@@ -126,4 +197,3 @@ export const browser = {
126197 getTitle : vi . spyOn ( { getTitle : async ( ) => 'Example Domain' } , 'getTitle' ) ,
127198 call ( fn : Function ) { return fn ( ) } ,
128199} satisfies Partial < WebdriverIO . Browser > as unknown as WebdriverIO . Browser
129-
0 commit comments