Closed GrayedFox closed 1 year ago
Posting this custom command here as a workaround - if you want to have static selector classes but still need to base the selection based on a dynamic parent element, this custom command does the trick:
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Cypress {
interface Chainable {
/**
* Custom command that wraps a child element found within a parent
* @param
* @example cy.getChild(parent, child);
*/
getChild: typeof getChild;
}
}
function getChild(
parent: Cypress.Chainable,
child: Cypress.Chainable
): Cypress.Chainable {
let childWithin: Cypress.Chainable;
return parent
.within(() => {
child.then(($el) => {
childWithin = $el;
});
})
.then(() => {
return cy.wrap(childWithin);
});
}
Cypress.Commands.add('getChild', getChild);
Pass in either a parent Selector or query (i.e. cy.getChild(cy.get('parent-selector', DecoratedSelector))
and the custom command will yield a chainable interface so you can use Mocha and Chai assertions straight off the top of it.
@GrayedFox I guess the only way to accomplish 'dynamic' parents with static selectors is by so-called using mixins. Basically, you would need a function that takes a parent selector and returns a new class where static selectors are initialized with the proper parent selector. This solution is clunky though and probably your's solution with getChild
is cleaner.
Here is a little helper of my own which let you contextualize a selector holders withing a parent :
export const ForParent = <T>(parent: Selector, type: { new(): T; }): T => {
const instance = new type();
let childHolder: any = {};
const contextualizedObject: any = {...instance};
Object.getOwnPropertyNames(Object.getPrototypeOf(instance))
.filter(key => key !== 'constructor')
.forEach(key => {
// @ts-ignore
const selector = instance[key] as Selector;
if (!selector) {
return;
}
Object.defineProperty(contextualizedObject, key, {
// Create a new getter for the property
get: function () {
return parent
.within(() => selector.then(($el: any) => childHolder[key] = $el))
.then(() => cy.wrap(childHolder[key]))
}
})
})
return contextualizedObject ;
}
Usage :
const mySelectors= ForParent(parentSelector , SelectorHolders);
mySelectors.MyTitle.should('have.text', 'lol);
With this definitions :
export class SelectorHolders{
@By.Attribute('my-title')
MyTitle: Selector;
@By.Attribute('my-subtitle')
MySubTitle: Selector;
}
@jonathanantoine that's awesome! Thank you for sharing!
Right now I have a class that uses selector methods as property decorators, which is fine for static values, but I would like to pass in a Selector at runtime so that I can create or return a class instance with different parents.
I would like to be able to do something like this and pass in a string as a value to the class constructor at runtime:
So that I can then do something like this:
Typescript throws an error due to me using the decorator function as a standard function:
Type '(host: Host, property: string) => any' is not assignable to type 'Selector'.ts(2322)
I can get around the error by casting to unknown and then to Selector, but will that work as expected? Is there an easier way to achieve what I am trying to do here? From what I understand decorators do nothing until runtime - but maybe I should be using the method as a method decorator or class decorator instead?
Pretty new to this functionality in TypeScript, been reading this but am still learning: https://www.digitalocean.com/community/tutorials/how-to-use-decorators-in-typescript#creating-method-decorators