-
Notifications
You must be signed in to change notification settings - Fork 3.6k
New issue
Have a question about this project?Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of serviceand privacy statement.We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature] Add these cypress like locators #23718
Comments
Related request#16155 |
Alsoparent(),in addition toparents() |
also scrolling
|
Please, this feature will be great |
It will be wonderful |
this would be game changer! |
This feature would be greatly appreciated as these days frameworks like |
This will be great to have (moving from Testcafe to Playwright and sometimes it is harder to go bottom-up to find an element which appears several times in the page and it has dynamic ids and no other element that can be used). For example: editing an element in the page (there are many) and the only thing that makes the difference between them is some specific text set by me which is the last element in the DOM. |
Adding parent(), parents(), child(), children(), sibling() would be so good. It would allow us to traverse the DOM with native Playwright functionality. |
It will be great to have this feature! |
It's almost 1 year since this feature task is open. Will it be taken into consideration soon? Thank you! |
This would make my testing much easier! |
Think it's a little crazy how these things were not built-in to begin with. |
While not available out of the box, I believe itmightbe possible to polyfill the missing functionality byadding custom selectors.I do agree it would be nice to have it out-of-the-box, but we don't have that yet… If my time permits, I might give it a shot. |
A sibling locator would be a gamechanger. |
This will reduce my locator params length sooooo much. We need these, especially the |
This is my take at tackling this issue. Below is a quick example and the source code follows. I don't have the capacity to maintain a FOSS library hence providing the source directly. If someone wants to take this code (given that credit will be given) and convert it to a library, by all means! I've briefly tested the code and it seems okay but it is possible there are bugs in it… How-to example
test('Sample',async({page})=>{
constlocator=page.getByRole('heading');
constquery=find(locator);// will try 5 times by default
constquery2=find(locator,99);// will try 99 before throwing an error
query.children();// get all children of locator (@return Locator[])
query.parent();// get immediate parent of the locator (@return Locator)
query.parent.until.class('');// keep iterating "up" until parent matches target class (without leading dot!) (@return Locator)
query.parent.until.id('');// keep iterating "up" until parent matches target id (without leading #!) (@return Locator)
query.parent.until.tag('');// keep iterating "up" until parent matches target tag (@return Locator)
query.self();// points to itself, just for completeness sake (@return Locator)
query.sibling.all();// get all siblings of locator, excludes self by default (@return Locator[])
query.sibling.next();// get the next sibling (@return Locator)
query.sibling.next.all();// get all next siblings (@return Locator[])
query.sibling.next.until.class('');// keep iterating "forward" until sibling matches target class (without leading dot!) (@return Locator)
query.sibling.next.until.id('');// keep iterating "forward" until sibling matches target id (without leading #!) (@return Locator)
query.sibling.next.until.tag('');// keep iterating "forward" until sibling matches target id (@return Locator)
query.sibling.prev();// get previous sibling of locator (@return Locator)
query.sibling.prev.all();// get all previous siblings (@return Locator[])
query.sibling.prev.until.class('');// keep iterating "backwards" until sibling matches target class (without leading dot!) (@return Locator)
query.sibling.prev.until.id('');// keep iterating "backwards" until sibling matches target id (without leading #!) (@return Locator)
query.sibling.prev.until.tag;// keep iterating "backwards" until sibling matches target tag (@return Locator)
}); find.ts
import{xpath}from'./xpath';
import{iterator}from'./iterator';
importtype{Locator}from'@playwright/test';
/**
* Inpspired by Cypress queries
* @param source Starting locator
* @param tries How many times to iterate before throwing an error (default: `5`)
* @link https://docs.cypress.io/api/table-of-contents#Queries
*/
exportfunctionfind(source:Locator,tries:number=5){
constlocator=augmentLocator(source);
construn=iterator(locator,tries);
const{query}=xpath;
constsanitize={
/**
* Remove any non Alpha betic characters from the given string
*/
tag:(input:string):string=>input.replaceAll(/[^A-z]/g,''),
};
constfindSelf=()=>locator.xpath(query.self);
constfindChilden=()=>locator.xpath(query.children).all();
constfindRoot=()=>locator.xpath(query.root);
constfindParent=Object.assign(()=>locator.xpath(query.parent),{
until:{
/**
* @param parent Parent ID (full match & case sensitive)
*/
id:(parent:string):Promise<Locator>=>{
returnrun(query.parent,(el,id)=>el.id===id,parent);
},
/**
* @param parent CSS class (full match & case sensitive!)
*/
class:(parent:string):Promise<Locator>=>{
returnrun(query.parent,(el,css)=>el.classList.contains(css),parent);
},
/**
* @param parent Tag for which we will search, e.g. `<span>` or `img
*/
tag:(parent:string):Promise<Locator>=>{
consttag=sanitize.tag(parent).toUpperCase();
returnrun(query.parent,(el,tag)=>el.tagName===tag,tag);
},
},
});
constsibling={
next:locator.xpath(query.sibling.next),
prev:locator.xpath(query.sibling.previous),
};
constfindSibling={
/**
* @param includingSelf Should the source [locator](https://playwright.dev/docs/api/class-locator) be included or not? (default `false`)
*/
all:async(includingSelf:boolean=false)=>{
constprev=awaitsibling.prev.all();
constnext=awaitsibling.next.all();
if(!includingSelf)return[...prev,...next];
return[...prev,locator,...next];
},
next:Object.assign(()=>sibling.next.first(),{
all:()=>sibling.next.all(),
until:{
id:(id:string)=>{
returnrun(query.sibling.next,(el,id)=>el.id===id,id);
},
/**
* @param css Target css class (case sensitive & full match!)
*/
class:(css:string):Promise<Locator>=>{
returnrun(query.sibling.next,(el,css)=>el.classList.contains(css),css);
},
/**
* @param tag Tag for which we will search, e.g. `<span>` or `img
*/
tag:(tag:string):Promise<Locator>=>{
consttargetTag=sanitize.tag(tag).toUpperCase();
returnrun(query.sibling.next,(el,tag)=>el.tagName===tag,targetTag);
},
},
}),
prev:Object.assign(()=>sibling.prev.last(),{
all:()=>sibling.prev.all(),
until:{
id:(id:string)=>{
returnrun(query.sibling.previous,(el,id)=>el.id===id,id);
},
class:(css:string)=>{
returnrun(query.sibling.previous,(el,css)=>el.classList.contains(css),css);
},
tag:(tag:string)=>{
consttargetTag=sanitize.tag(tag).toUpperCase();
returnrun(query.sibling.previous,(el,tag)=>el.tagName===tag,targetTag);
},
},
}),
};
return{
self:findSelf,
/**
* Implementing root xpath locator in Playwright is problematic
* because unless the locator is node type 'document', query
* will *always* prepend a leading dot making it relative
* @link https://github /microsoft/playwright/blob/e1e6c287226e4503e04b1b704ba370b9177a6209/packages/playwright-core/src/server/injected/xpathSelectorEngine.ts#L21-L22
*/
// root: findRoot,
children:findChilden,
parent:findParent,
sibling:findSibling,
};
}
interfaceAugmentedLocatorextendsLocator{
/**
* Execute XPath query
* @param query XPath query
*/
xpath:(query:string)=>Locator;
}
/**
* Add method `xpath` to [locator](https://playwright.dev/docs/api/class-locator)
* @param locator
*/
constaugmentLocator=(locator:Locator):AugmentedLocator=>{
returnObject.assign(locator,{
xpath:(query:string)=>{
if(!query.startsWith('xpath='))query='xpath='+query;
returnlocator.locator(query);
},
});
}; iterator.ts
importtype{ElementHandle,JSHandle,Locator}from'@playwright/test';
importtype{Serializable}from'child_process';
/**
* This function "iterates" (do-while loop) until the given predicate is fullfilled or the number of allowed tries is reached and an error is thrown
* @param locator Source locator
* @param tries How many times to iterate before throwing an error
*/
exportfunctioniterator(locator:Locator,tries:number=5){
/**
* "Iterate" (do-while loop) until the `evaluateFn` either returns `true` or the allowed number of `tries` is reached and an error is thrown
* @param selector XPath selector which would allow recursive looping, e.g. `parent::node()` or `following-sibling::node()`
* @param evaluateFn A predicate function which receives the current element as its first argument and the custom supplied argument as its second argument. Since this function is run *inside* whatever browser Playwright is using, it will *not* inherit scope, which is why we need to supply the custom argument manually (if one is required)
* @param argument Custom argument we supply to the predicate function (optional, defaults to `undefined`). Has to be [a serializable object](https://developer.mozilla.org/en-US/docs/Glossary/Serializable_object) so live DOM nodes are no-go. See [this](https://stackoverflow /a/69183016/4343719) StackOverflow answer (even though it's for Puppeteer the same principle still applies to Playwright).
*/
returnasync<EextendsElement=Element,AextendsArgument=Argument>(
selector:string,
evaluateFn:((element:E,argument:A)=>Promise<boolean>)|((element:E,argument:A)=>boolean),
argument:A=undefinedasA
):Promise<Locator>=>{
constquery=makeXPathQuery(selector);
lettargetLocator:Locator=locator;
letcount:number=0;
do{
targetLocator=targetLocator.locator(query).first();
constresult=awaittargetLocator.evaluate(evaluateFn,argument);
if(result)returntargetLocator;
}while(count!==tries);
thrownewError('Internal error!',);
};
}
constmakeXPathQuery=(selector:string):string=>{
returnselector.startsWith('xpath=')?selector:'xpath='+selector;
};
typeElement=SVGElement|HTMLElement;
typeArgument=Serializable|JSHandle|ElementHandle|undefined; xpath.ts
constqueries={
root:'/',
self:'self::node()',
parent:'parent::node()',
children:'self::node()/child::node()',
sibling:{
next:'self::node()/following-sibling::node()',
previous:'self::node()/preceding-sibling::node()',
},
}asconst;
exportconstxpath={
query:queries,
}; |
I’ve been using Cypress for a long time now and have enjoyed it quite a lot. A colleague asked that I try Playwright, and I must say WOW!, just WOW!
I wouldn’t say I’ve fallen in love with Playwright yet as I’m still experimenting with it at the moment however I’m liking it already.
I think there a a few things Cypress did to make testing easy especially their locator commands. I think Playwright might just win me over completely if they add similar Cypress commands like: contain(), parents(), parentsUntil(), sibling(), next(), prev(), children(), within(), etc.
The text was updated successfully, but these errors were encountered: