diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md
index 7120e44887..4a48d4dbf1 100644
--- a/docs/src/api/class-frame.md
+++ b/docs/src/api/class-frame.md
@@ -918,6 +918,16 @@ Attribute name to get the value for.
### option: Frame.getByLabelText.exact = %%-locator-get-by-text-exact-%%
+## method: Frame.getByPlaceholderText
+* since: v1.27
+- returns: <[Locator]>
+
+%%-template-locator-get-by-placeholder-text-%%
+
+### param: Frame.getByPlaceholderText.text = %%-locator-get-by-text-text-%%
+### option: Frame.getByPlaceholderText.exact = %%-locator-get-by-text-exact-%%
+
+
## method: Frame.getByRole
* since: v1.27
- returns: <[Locator]>
diff --git a/docs/src/api/class-framelocator.md b/docs/src/api/class-framelocator.md
index 920c0c8411..78b02dc374 100644
--- a/docs/src/api/class-framelocator.md
+++ b/docs/src/api/class-framelocator.md
@@ -124,6 +124,16 @@ in that iframe.
### option: FrameLocator.getByLabelText.exact = %%-locator-get-by-text-exact-%%
+## method: FrameLocator.getByPlaceholderText
+* since: v1.27
+- returns: <[Locator]>
+
+%%-template-locator-get-by-placeholder-text-%%
+
+### param: FrameLocator.getByPlaceholderText.text = %%-locator-get-by-text-text-%%
+### option: FrameLocator.getByPlaceholderText.exact = %%-locator-get-by-text-exact-%%
+
+
## method: FrameLocator.getByRole
* since: v1.27
- returns: <[Locator]>
diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md
index 233772b357..7f3465bfeb 100644
--- a/docs/src/api/class-locator.md
+++ b/docs/src/api/class-locator.md
@@ -644,6 +644,16 @@ Attribute name to get the value for.
### option: Locator.getByLabelText.exact = %%-locator-get-by-text-exact-%%
+## method: Locator.getByPlaceholderText
+* since: v1.27
+- returns: <[Locator]>
+
+%%-template-locator-get-by-placeholder-text-%%
+
+### param: Locator.getByPlaceholderText.text = %%-locator-get-by-text-text-%%
+### option: Locator.getByPlaceholderText.exact = %%-locator-get-by-text-exact-%%
+
+
## method: Locator.getByRole
* since: v1.27
- returns: <[Locator]>
diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md
index cee5229031..63f1083951 100644
--- a/docs/src/api/class-page.md
+++ b/docs/src/api/class-page.md
@@ -2193,6 +2193,16 @@ Attribute name to get the value for.
### option: Page.getByLabelText.exact = %%-locator-get-by-text-exact-%%
+## method: Page.getByPlaceholderText
+* since: v1.27
+- returns: <[Locator]>
+
+%%-template-locator-get-by-placeholder-text-%%
+
+### param: Page.getByPlaceholderText.text = %%-locator-get-by-text-text-%%
+### option: Page.getByPlaceholderText.exact = %%-locator-get-by-text-exact-%%
+
+
## method: Page.getByRole
* since: v1.27
- returns: <[Locator]>
diff --git a/docs/src/api/params.md b/docs/src/api/params.md
index 29d2087e71..669aa40d0d 100644
--- a/docs/src/api/params.md
+++ b/docs/src/api/params.md
@@ -1188,6 +1188,13 @@ Allows locating input elements by the text of the associated label. For example,
```
+## template-locator-get-by-placeholder-text
+
+Allows locating input elements by the placeholder text. For example, this method will find the input by placeholder "Country":
+
+```html
+
+```
## template-locator-get-by-role
diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts
index 20ab160358..a3420ee834 100644
--- a/packages/playwright-core/src/client/frame.ts
+++ b/packages/playwright-core/src/client/frame.ts
@@ -307,6 +307,10 @@ export class Frame extends ChannelOwner implements api.Fr
return this.locator(Locator.getByLabelTextSelector(text, options));
}
+ getByPlaceholderText(text: string | RegExp, options?: { exact?: boolean }): Locator {
+ return this.locator(Locator.getByPlaceholderTextSelector(text, options));
+ }
+
getByText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.locator(Locator.getByTextSelector(text, options));
}
diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts
index c169934012..90cd069684 100644
--- a/packages/playwright-core/src/client/locator.ts
+++ b/packages/playwright-core/src/client/locator.ts
@@ -52,7 +52,7 @@ export class Locator implements api.Locator {
}
static getByTestIdSelector(testId: string): string {
- return `css=[${Locator._testIdAttributeName}=${JSON.stringify(testId)}]`;
+ return `attr=[${Locator._testIdAttributeName}=${JSON.stringify(testId)}]`;
}
static getByLabelTextSelector(text: string | RegExp, options?: { exact?: boolean }): string {
@@ -63,6 +63,12 @@ export class Locator implements api.Locator {
return selector + ' >> control=resolve-label';
}
+ static getByPlaceholderTextSelector(text: string | RegExp, options?: { exact?: boolean }): string {
+ if (!isString(text))
+ return `attr=[placeholder=${text}]`;
+ return `attr=[placeholder=${JSON.stringify(text)}${options?.exact ? 's' : 'i'}]`;
+ }
+
static getByTextSelector(text: string | RegExp, options?: { exact?: boolean }): string {
if (!isString(text))
return `text=${text}`;
@@ -197,6 +203,10 @@ export class Locator implements api.Locator {
return this.locator(Locator.getByLabelTextSelector(text, options));
}
+ getByPlaceholderText(text: string | RegExp, options?: { exact?: boolean }): Locator {
+ return this.locator(Locator.getByPlaceholderTextSelector(text, options));
+ }
+
getByText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.locator(Locator.getByTextSelector(text, options));
}
@@ -386,6 +396,11 @@ export class FrameLocator implements api.FrameLocator {
getByLabelText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.locator(Locator.getByLabelTextSelector(text, options));
}
+
+ getByPlaceholderText(text: string | RegExp, options?: { exact?: boolean }): Locator {
+ return this.locator(Locator.getByPlaceholderTextSelector(text, options));
+ }
+
getByText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.locator(Locator.getByTextSelector(text, options));
}
diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts
index 65ed01b573..b28d87d216 100644
--- a/packages/playwright-core/src/client/page.ts
+++ b/packages/playwright-core/src/client/page.ts
@@ -572,6 +572,10 @@ export class Page extends ChannelOwner implements api.Page
return this.mainFrame().getByLabelText(text, options);
}
+ getByPlaceholderText(text: string | RegExp, options?: { exact?: boolean }): Locator {
+ return this.mainFrame().getByPlaceholderText(text, options);
+ }
+
getByText(text: string | RegExp, options?: { exact?: boolean }): Locator {
return this.mainFrame().getByText(text, options);
}
diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts
index 1aae3c635c..804896d1a9 100644
--- a/packages/playwright-core/src/server/injected/injectedScript.ts
+++ b/packages/playwright-core/src/server/injected/injectedScript.ts
@@ -19,6 +19,7 @@ import { XPathEngine } from './xpathSelectorEngine';
import { ReactEngine } from './reactSelectorEngine';
import { VueEngine } from './vueSelectorEngine';
import { RoleEngine } from './roleSelectorEngine';
+import { parseAttributeSelector } from '../isomorphic/selectorParser';
import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from '../isomorphic/selectorParser';
import { allEngineNames, parseSelector, stringifySelector } from '../isomorphic/selectorParser';
import { type TextMatcher, elementMatchesText, createRegexTextMatcher, createStrictTextMatcher, createLaxTextMatcher, elementText } from './selectorUtils';
@@ -104,6 +105,7 @@ export class InjectedScript {
this._engines.set('visible', this._createVisibleEngine());
this._engines.set('control', this._createControlEngine());
this._engines.set('has', this._createHasEngine());
+ this._engines.set('attr', this._createNamedAttributeEngine());
for (const { name, engine } of customEngines)
this._engines.set(name, engine);
@@ -271,6 +273,31 @@ export class InjectedScript {
};
}
+ private _createNamedAttributeEngine(): SelectorEngine {
+ const queryList = (root: SelectorRoot, selector: string): Element[] => {
+ const parsed = parseAttributeSelector(selector, true);
+ if (parsed.name || parsed.attributes.length !== 1)
+ throw new Error('Malformed attribute selector: ' + selector);
+ const { name, value, caseSensitive } = parsed.attributes[0];
+ const lowerCaseValue = caseSensitive ? null : value.toLowerCase();
+ let matcher: (s: string) => boolean;
+ if (value instanceof RegExp)
+ matcher = s => !!s.match(value);
+ else if (caseSensitive)
+ matcher = s => s === value;
+ else
+ matcher = s => s.toLowerCase().includes(lowerCaseValue!);
+ const elements = this._evaluator._queryCSS({ scope: root as Document | Element, pierceShadow: true }, `[${name}]`);
+ return elements.filter(e => matcher(e.getAttribute(name)!));
+ };
+
+ return {
+ queryAll: (root: SelectorRoot, selector: string): Element[] => {
+ return queryList(root, selector);
+ }
+ };
+ }
+
private _createControlEngine(): SelectorEngineV2 {
return {
queryAll(root: SelectorRoot, body: any) {
diff --git a/packages/playwright-core/src/server/selectors.ts b/packages/playwright-core/src/server/selectors.ts
index 8a63d90e10..ea449efc4e 100644
--- a/packages/playwright-core/src/server/selectors.ts
+++ b/packages/playwright-core/src/server/selectors.ts
@@ -46,7 +46,7 @@ export class Selectors {
'data-test-id', 'data-test-id:light',
'data-test', 'data-test:light',
'nth', 'visible', 'control', 'has',
- 'role',
+ 'role', 'attr'
]);
this._builtinEnginesInMainWorld = new Set([
'_react', '_vue',
diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts
index 33bc0c443f..e5d4a52324 100644
--- a/packages/playwright-core/types/types.d.ts
+++ b/packages/playwright-core/types/types.d.ts
@@ -2475,6 +2475,24 @@ export interface Page {
exact?: boolean;
}): Locator;
+ /**
+ * Allows locating input elements by the placeholder text. For example, this method will find the input by placeholder
+ * "Country":
+ *
+ * ```html
+ *
+ * ```
+ *
+ * @param text Text to locate the element for.
+ * @param options
+ */
+ getByPlaceholderText(text: string|RegExp, options?: {
+ /**
+ * Whether to find an exact match: case-sensitive and whole-string. Default to false.
+ */
+ exact?: boolean;
+ }): Locator;
+
/**
* Allows locating elements by their [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles),
* [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and
@@ -5509,6 +5527,24 @@ export interface Frame {
exact?: boolean;
}): Locator;
+ /**
+ * Allows locating input elements by the placeholder text. For example, this method will find the input by placeholder
+ * "Country":
+ *
+ * ```html
+ *
+ * ```
+ *
+ * @param text Text to locate the element for.
+ * @param options
+ */
+ getByPlaceholderText(text: string|RegExp, options?: {
+ /**
+ * Whether to find an exact match: case-sensitive and whole-string. Default to false.
+ */
+ exact?: boolean;
+ }): Locator;
+
/**
* Allows locating elements by their [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles),
* [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and
@@ -9891,6 +9927,24 @@ export interface Locator {
exact?: boolean;
}): Locator;
+ /**
+ * Allows locating input elements by the placeholder text. For example, this method will find the input by placeholder
+ * "Country":
+ *
+ * ```html
+ *
+ * ```
+ *
+ * @param text Text to locate the element for.
+ * @param options
+ */
+ getByPlaceholderText(text: string|RegExp, options?: {
+ /**
+ * Whether to find an exact match: case-sensitive and whole-string. Default to false.
+ */
+ exact?: boolean;
+ }): Locator;
+
/**
* Allows locating elements by their [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles),
* [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and
@@ -15094,6 +15148,24 @@ export interface FrameLocator {
exact?: boolean;
}): Locator;
+ /**
+ * Allows locating input elements by the placeholder text. For example, this method will find the input by placeholder
+ * "Country":
+ *
+ * ```html
+ *
+ * ```
+ *
+ * @param text Text to locate the element for.
+ * @param options
+ */
+ getByPlaceholderText(text: string|RegExp, options?: {
+ /**
+ * Whether to find an exact match: case-sensitive and whole-string. Default to false.
+ */
+ exact?: boolean;
+ }): Locator;
+
/**
* Allows locating elements by their [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles),
* [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and
diff --git a/tests/page/locator-frame.spec.ts b/tests/page/locator-frame.spec.ts
index 42e5e3b582..6e43190ffb 100644
--- a/tests/page/locator-frame.spec.ts
+++ b/tests/page/locator-frame.spec.ts
@@ -35,7 +35,7 @@ async function routeIframe(page: Page) {
1
2
-
+