microsoft / playwright

Playwright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.
https://playwright.dev
Apache License 2.0
66.84k stars 3.67k forks source link

[Feature] Add these cypress like locators #23718

Open RantiBaba opened 1 year ago

RantiBaba commented 1 year ago

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.

yury-s commented 1 year ago

Related request https://github.com/microsoft/playwright/issues/16155

harmin-parra commented 1 year ago

Also parent(), in addition to parents()

harmin-parra commented 1 year ago

also scrolling

scrollTo('bottom')
scrollTo('topRight', { duration: 2000 })
scrollTo('center', { easing: 'linear' })
scrollTo(250, 250)
scrollTo('75%', '25%')
lucasdonato commented 11 months ago

Please, this feature will be great

WeberLucas commented 11 months ago

It will be wonderful

joaoh9 commented 10 months ago

this would be game changer!

fasatrix commented 8 months ago

This feature would be greatly appreciated as these days frameworks like Mui are making testing really challenging because of the agnostic way of creating selectors that are rarely unique (I have used xpath as fallback plan however very ugly solution)

marinamarin91 commented 7 months ago

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.

cctui-dev commented 6 months ago

Adding parent(), parents(), child(), children(), sibling() would be so good. It would allow us to traverse the DOM with native Playwright functionality. Working on a multi-language site, I need to either flood my page with test ids, rely on css selectors or xpath.

bogdanbotin commented 6 months ago

It will be great to have this feature !

marinamarin91 commented 6 months ago

It's almost 1 year since this feature task is open. Will it be taken into consideration soon? Thank you!

sindresteen commented 6 months ago

This would make my testing much easier!

basickarl commented 5 months ago

Think it's a little crazy how these things were not built-in to begin with.

alexkuc commented 5 months ago

While not available out of the box, I believe it might be possible to polyfill the missing functionality by adding 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.

nissanthen commented 5 months ago

A sibling locator would be a gamechanger.

lamlamla commented 4 months ago

This will reduce my locator params length sooooo much. We need these, especially the parent() method

alexkuc commented 4 months ago

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

```typescript test('Sample', async ({ page }) => { const locator = page.getByRole('heading'); const query = find(locator); // will try 5 times by default const query2 = 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

```typescript import { xpath } from './xpath'; import { iterator } from './iterator'; import type { 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 */ export function find(source: Locator, tries: number = 5) { const locator = augmentLocator(source); const run = iterator(locator, tries); const { query } = xpath; const sanitize = { /** * Remove any non alphabetic characters from the given string */ tag: (input: string): string => input.replaceAll(/[^A-z]/g, ''), }; const findSelf = () => locator.xpath(query.self); const findChilden = () => locator.xpath(query.children).all(); const findRoot = () => locator.xpath(query.root); const findParent = Object.assign(() => locator.xpath(query.parent), { until: { /** * @param parent Parent ID (full match & case sensitive) */ id: (parent: string): Promise => { return run(query.parent, (el, id) => el.id === id, parent); }, /** * @param parent CSS class (full match & case sensitive!) */ class: (parent: string): Promise => { return run(query.parent, (el, css) => el.classList.contains(css), parent); }, /** * @param parent Tag for which we will search, e.g. `` or `img */ tag: (parent: string): Promise => { const tag = sanitize.tag(parent).toUpperCase(); return run(query.parent, (el, tag) => el.tagName === tag, tag); }, }, }); const sibling = { next: locator.xpath(query.sibling.next), prev: locator.xpath(query.sibling.previous), }; const findSibling = { /** * @param includingSelf Should the source [locator](https://playwright.dev/docs/api/class-locator) be included or not? (default `false`) */ all: async (includingSelf: boolean = false) => { const prev = await sibling.prev.all(); const next = await sibling.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) => { return run(query.sibling.next, (el, id) => el.id === id, id); }, /** * @param css Target css class (case sensitive & full match!) */ class: (css: string): Promise => { return run(query.sibling.next, (el, css) => el.classList.contains(css), css); }, /** * @param tag Tag for which we will search, e.g. `` or `img */ tag: (tag: string): Promise => { const targetTag = sanitize.tag(tag).toUpperCase(); return run(query.sibling.next, (el, tag) => el.tagName === tag, targetTag); }, }, }), prev: Object.assign(() => sibling.prev.last(), { all: () => sibling.prev.all(), until: { id: (id: string) => { return run(query.sibling.previous, (el, id) => el.id === id, id); }, class: (css: string) => { return run(query.sibling.previous, (el, css) => el.classList.contains(css), css); }, tag: (tag: string) => { const targetTag = sanitize.tag(tag).toUpperCase(); return run(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.com/microsoft/playwright/blob/e1e6c287226e4503e04b1b704ba370b9177a6209/packages/playwright-core/src/server/injected/xpathSelectorEngine.ts#L21-L22 */ // root: findRoot, children: findChilden, parent: findParent, sibling: findSibling, }; } interface AugmentedLocator extends Locator { /** * 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 */ const augmentLocator = (locator: Locator): AugmentedLocator => { return Object.assign(locator, { xpath: (query: string) => { if (!query.startsWith('xpath=')) query = 'xpath=' + query; return locator.locator(query); }, }); }; ```

iterator.ts

```typescript import type { ElementHandle, JSHandle, Locator } from '@playwright/test'; import type { 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 */ export function iterator(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.com/a/69183016/4343719) StackOverflow answer (even though it's for Puppeteer the same principle still applies to Playwright). */ return async ( selector: string, evaluateFn: ((element: E, argument: A) => Promise) | ((element: E, argument: A) => boolean), argument: A = undefined as A ): Promise => { const query = makeXPathQuery(selector); let targetLocator: Locator = locator; let count: number = 0; do { targetLocator = targetLocator.locator(query).first(); const result = await targetLocator.evaluate(evaluateFn, argument); if (result) return targetLocator; } while (count !== tries); throw new Error('Internal error!',); }; } const makeXPathQuery = (selector: string): string => { return selector.startsWith('xpath=') ? selector : 'xpath=' + selector; }; type Element = SVGElement | HTMLElement; type Argument = Serializable | JSHandle | ElementHandle | undefined; ```

xpath.ts

```typescript const queries = { 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()', }, } as const; export const xpath = { query: queries, }; ```