PracticeQAAutomation logoPracticeQAAutomation

Locator Playground

Every Playwright locator and its Cypress equivalent on one page — role, text, label, placeholder, alt text, title, test id, CSS, XPath, filters, iframes and shadow DOM. Every snippet here is run against this page in our test suite.

Show code for

Cheat sheet

The closest Cypress equivalent for every Playwright locator. Each row is practiced in a section below.

Find byPlaywrightCypress
Role + namegetByRole('button', { name })cy.contains('button', text) · cy.findByRole (plugin)
Visible textgetByText(text | /regex/)cy.contains(text | /regex/)
Form labelgetByLabel(text)cy.contains('label', text) → input
PlaceholdergetByPlaceholder(text)cy.get('[placeholder="…"]')
Image altgetByAltText(text)cy.get('img[alt="…"]')
Title attributegetByTitle(text)cy.get('[title="…"]')
Test idgetByTestId(id)cy.get('[data-cy="…"]')
CSSlocator('css')cy.get('css')
XPathlocator('//xpath')Not built in — use CSS / cy.contains
Narrow by contentfilter({ hasText, has, hasNot, visible }).filter() · .not() · :has() · :visible
Positionfirst() · last() · nth(i).first() · .last() · .eq(i)
Scopelocator.getBy…() chaining.within() · .find()
Walk the DOMlocator('xpath=ancestor::…') · locator('..').closest() · .parent() · .siblings()
iframeframeLocator() · contentFrame()its('0.contentDocument.body') → cy.wrap
Shadow DOMPierced automatically (not XPath).shadow() · { includeShadowDom: true }

By role & accessible name

The locator users and screen readers “see”: headings, buttons, toggles, links and checkboxes — plus states like pressed, expanded, checked and disabled.

Account settings

Help centre

Playwright
const box = page.getByTestId('loc-role');
await expect(box.getByRole('heading', { level: 3, name: 'Account settings' })).toBeVisible();
// name matches a substring, ignoring case — exact: true stops 'Save' also matching 'Save draft'
await box.getByRole('button', { name: 'Save', exact: true }).click();
await expect(box.getByRole('status')).toHaveText('Saved');
// icon-only button: the name comes from aria-label
await box.getByRole('button', { name: 'Close panel' }).click();
await expect(box.getByRole('status')).toHaveText('Panel closed');
await expect(box.getByRole('button', { name: 'Delete account' })).toBeDisabled();
await box.getByRole('button', { name: 'Bold', pressed: false }).click();
await expect(box.getByRole('button', { name: 'Bold', pressed: true })).toBeVisible();
await box.getByRole('button', { name: 'More options', expanded: false }).click();
await expect(box.getByRole('list', { name: 'More options' })).toBeVisible();
await box.getByRole('checkbox', { name: 'Email me updates' }).check();
await expect(box.getByRole('checkbox', { checked: true })).toHaveCount(1);
await expect(box.getByRole('link', { name: 'Help centre' })).toHaveAttribute('href', '#help-centre');

By visible text

Text split across tags, messy whitespace, case, exact matches, regular expressions — and text that is in the DOM but hidden.

Order #1042 has shipped

Welcome back, Asha

Total: ₹1,299.00

Secret promo code

Free delivery · Free delivery on orders over ₹499

Playwright
const box = page.getByTestId('loc-text');
// text split across <strong> still matches, and whitespace is normalised
await expect(box.getByText('Order #1042 has shipped')).toBeVisible();
await expect(box.getByText('Welcome back, Asha')).toBeVisible();
// default: substring, case-insensitive · exact: true: whole string, case-sensitive
await expect(box.getByText('free delivery')).toHaveCount(2);
await expect(box.getByText('Free delivery', { exact: true })).toHaveCount(1);
await expect(box.getByText(/₹\d[\d,]*\.\d{2}/)).toHaveText('Total: ₹1,299.00');
// hidden text is still found — assert on visibility, or keep only visible matches
await expect(box.getByText('Secret promo code')).toBeHidden();
await expect(box.getByText('Secret promo code').filter({ visible: true })).toHaveCount(0);

By label

The four ways an input gets its label: <label for>, a wrapping <label>, aria-label and aria-labelledby.

One-time code
Playwright
const form = page.getByTestId('loc-labels');
await form.getByLabel('Email address').fill('asha@example.com');   // <label for="…">
await form.getByLabel('Full name').fill('Asha Rao');                // <label> wraps the input
await form.getByLabel('Search products').fill('mouse');             // aria-label
await form.getByLabel('One-time code').fill('123456');              // aria-labelledby
await expect(form.getByLabel('Email address')).toHaveValue('asha@example.com');
await expect(form.getByRole('textbox', { name: 'One-time code' })).toHaveValue('123456');

By placeholder, alt text & title

Inputs without a label, images and tooltips — located by the attributes users actually read.

PracticeQAAutomation logo7QA
Playwright
const box = page.getByTestId('loc-attrs');
await box.getByPlaceholder('name@company.com').fill('asha@example.com');
await expect(box.getByAltText('PracticeQAAutomation logo')).toBeVisible();
await expect(box.getByTitle('Open issues')).toHaveText('7');
await expect(box.getByTitle('Quality Assurance')).toHaveText('QA');

By test id

Four common conventions on one page: data-testid, data-cy, data-test and data-qa.

Playwright
const box = page.getByTestId('loc-testids');
await box.getByTestId('pay-now').click();                 // data-testid is the default
await expect(box.getByRole('status')).toHaveText('Pay now clicked');
await box.locator('[data-cy="apply-coupon"]').click();   // any other attribute: plain CSS
await expect(box.getByRole('status')).toHaveText('Apply coupon clicked');
await box.locator('[data-test="save-address"]').click();
await expect(box.getByRole('status')).toHaveText('Save address clicked');
// Or make getByTestId read another attribute in playwright.config.ts:
//   use: { testIdAttribute: 'data-qa' }  →  await page.getByTestId('place-order').click();
await box.locator('[data-qa="place-order"]').click();
await expect(box.getByRole('status')).toHaveText('Place order clicked');

CSS & XPath

Attribute operators, :has(), :nth-child(), Playwright's :has-text() and XPath — and what Cypress uses instead of XPath.

  • Wireless Mouse Sale
  • Mechanical Keyboard
  • USB-C Hub Sale
  • Laptop Stand
Playwright
const list = page.getByTestId('loc-css');
await expect(list.locator('li.item--sale')).toHaveCount(2);
await expect(list.locator('[id^="sku-1"]')).toHaveCount(2);       // ^= starts with, $= ends, *= contains
await expect(list.locator('li:has(.badge)')).toHaveCount(2);      // CSS :has()
await expect(list.locator('li:nth-child(2)')).toContainText('Mechanical Keyboard');
await expect(list.locator('li:has-text("USB-C")')).toHaveAttribute('data-price', '1299'); // Playwright pseudo-class
// XPath: start with // (or use the xpath= prefix)
await expect(page.locator('//ul[@data-testid="loc-css"]/li[@data-price > 1000]')).toHaveCount(2);
await expect(list.locator('xpath=./li[not(span)]')).toHaveCount(2);

Filter, chain & pick by position

Find the card by its text, by a child, or by what it doesn't contain — then first/last/nth, and()/or(), and walking up and across the DOM.

Wireless Mouse

₹799In stock

Mechanical Keyboard

₹3,499Out of stock

USB-C Hub

₹1,299In stock

Webcam HD

₹2,199Out of stock

Laptop Stand

₹999In stock

Playwright
const section = page.getByTestId('loc-cards');
const cards = section.getByRole('article');                // role locators skip the hidden card
await expect(cards).toHaveCount(5);
await cards.filter({ hasText: 'USB-C Hub' }).getByRole('button', { name: 'Add to cart' }).click();
await expect(section.getByRole('status')).toHaveText('Added USB-C Hub');
await expect(cards.filter({ has: page.getByRole('button', { disabled: true }) })).toHaveCount(2);
await expect(cards.filter({ hasNotText: 'Out of stock' })).toHaveCount(3);
await expect(cards.filter({ hasNot: page.getByText('Out of stock') })).toHaveCount(3);
await expect(cards.first()).toContainText('Wireless Mouse');
await expect(cards.last()).toContainText('Laptop Stand');
await expect(cards.nth(1)).toContainText('Mechanical Keyboard');
// CSS locators include hidden elements — filter({ visible: true }) drops them
await expect(section.locator('article')).toHaveCount(6);
await expect(section.locator('article').filter({ visible: true })).toHaveCount(5);
// and(): both must match · or(): either one
await expect(cards.getByRole('button').and(page.locator(':enabled'))).toHaveCount(3);
await expect(section.getByText('Sold out').or(section.getByText('Out of stock')).first()).toBeVisible();
// walk up to the card from its heading
await expect(section.getByRole('heading', { name: 'Webcam HD' }).locator('xpath=ancestor::article').getByRole('button')).toBeDisabled();

Inside an iframe

A same-origin iframe with its own form. Playwright switches with frameLocator() or contentFrame(); Cypress wraps the iframe's body.

Playwright
const frame = page.frameLocator('[data-testid="loc-frame"]');
await frame.getByLabel('Your email').fill('asha@example.com');
await frame.getByRole('button', { name: 'Subscribe' }).click();
await expect(frame.getByRole('status')).toHaveText('Subscribed asha@example.com');
// or start from the <iframe> element itself
await expect(page.getByTitle('Newsletter form').contentFrame().getByRole('status')).toHaveText(/Subscribed/);

Inside a shadow root

An open shadow root. Playwright's CSS and getBy* locators pierce it automatically (XPath does not); Cypress needs .shadow() or includeShadowDom.

Playwright
const host = page.getByTestId('loc-shadow-host');
await host.getByLabel('Coupon').fill('SAVE10');
await host.getByRole('button', { name: 'Apply' }).click();
await expect(host.locator('.msg')).toHaveText('Coupon SAVE10 applied');
// XPath does not enter shadow roots
await expect(page.locator('//div[@data-testid="loc-shadow-host"]//button')).toHaveCount(0);