diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index b4ef5d34a3..b42aae325d 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -86,6 +86,20 @@ assertThat(locator).not().containsText("error"); await Expect(locator).Not.ToContainTextAsync("error"); ``` +## async method: LocatorAssertions.NotToBeAttached +* since: v1.33 +* langs: python + +The opposite of [`method: LocatorAssertions.toBeAttached`]. + +### option: LocatorAssertions.NotToBeAttached.attached +* since: v1.33 +- `attached` <[boolean]> + +### option: LocatorAssertions.NotToBeAttached.timeout = %%-csharp-java-python-assertions-timeout-%% +* since: v1.33 + + ## async method: LocatorAssertions.NotToBeChecked * since: v1.20 * langs: python @@ -377,6 +391,47 @@ Expected options currently selected. ### option: LocatorAssertions.NotToHaveValues.timeout = %%-csharp-java-python-assertions-timeout-%% * since: v1.23 + +## async method: LocatorAssertions.toBeAttached +* since: v1.33 +* langs: + - alias-java: isAttached + +Ensures that [Locator] points to an [attached](../actionability.md#attached) DOM node. + +**Usage** + +```js +await expect(page.getByText('Hidden text')).toBeAttached(); +``` + +```java +assertThat(page.getByText("Hidden text")).isAttached(); +``` + +```python async +await expect(page.get_by_text("Hidden text")).to_be_attached() +``` + +```python sync +expect(page.get_by_text("Hidden text")).to_be_attached() +``` + +```csharp +await Expect(Page.GetByText("Hidden text")).ToBeAttachedAsync(); +``` + +### option: LocatorAssertions.toBeAttached.attached +* since: v1.33 +- `attached` <[boolean]> + +### option: LocatorAssertions.toBeAttached.timeout = %%-js-assertions-timeout-%% +* since: v1.33 + +### option: LocatorAssertions.toBeAttached.timeout = %%-csharp-java-python-assertions-timeout-%% +* since: v1.33 + + ## async method: LocatorAssertions.toBeChecked * since: v1.20 * langs: @@ -781,31 +836,23 @@ Ensures that [Locator] points to an [attached](../actionability.md#attached) and **Usage** ```js -const locator = page.locator('.my-element'); -await expect(locator).toBeVisible(); +await expect(page.getByText('Welcome')).toBeVisible(); ``` ```java -assertThat(page.locator(".my-element")).isVisible(); +assertThat(page.getByText("Welcome")).isVisible(); ``` ```python async -from playwright.async_api import expect - -locator = page.locator('.my-element') -await expect(locator).to_be_visible() +await expect(page.get_by_text("Welcome")).to_be_visible() ``` ```python sync -from playwright.sync_api import expect - -locator = page.locator('.my-element') -expect(locator).to_be_visible() +expect(page.get_by_text("Welcome")).to_be_visible() ``` ```csharp -var locator = Page.Locator(".my-element"); -await Expect(locator).ToBeVisibleAsync(); +await Expect(Page.GetByText("Welcome")).ToBeVisibleAsync(); ``` ### option: LocatorAssertions.toBeVisible.visible diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index e4dbdcd875..fd2c91f6e6 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -1153,6 +1153,12 @@ export class InjectedScript { // expect(locator).not.toBeVisible() passes when there is no element. if (options.isNot && options.expression === 'to.be.visible') return { matches: false }; + // expect(locator).toBeAttached({ attached: false }) passes when there is no element. + if (!options.isNot && options.expression === 'to.be.detached') + return { matches: true }; + // expect(locator).not.toBeAttached() passes when there is no element. + if (options.isNot && options.expression === 'to.be.attached') + return { matches: false }; // expect(locator).not.toBeInViewport() passes when there is no element. if (options.isNot && options.expression === 'to.be.in.viewport') return { matches: false }; @@ -1191,6 +1197,10 @@ export class InjectedScript { elementState = this.elementState(element, 'hidden'); } else if (expression === 'to.be.visible') { elementState = this.elementState(element, 'visible'); + } else if (expression === 'to.be.attached') { + elementState = true; + } else if (expression === 'to.be.detached') { + elementState = false; } if (elementState !== undefined) { diff --git a/packages/playwright-test/src/matchers/expect.ts b/packages/playwright-test/src/matchers/expect.ts index 1007d43ab7..c41f4e8761 100644 --- a/packages/playwright-test/src/matchers/expect.ts +++ b/packages/playwright-test/src/matchers/expect.ts @@ -21,6 +21,7 @@ import { pollAgainstTimeout } from 'playwright-core/lib/utils'; import type { ExpectZone } from 'playwright-core/lib/utils'; import { + toBeAttached, toBeChecked, toBeDisabled, toBeEditable, @@ -130,6 +131,7 @@ expect.poll = (actual: unknown, messageOrOptions: ExpectMessageOrOptions) => { expectLibrary.setState({ expand: false }); const customMatchers = { + toBeAttached, toBeChecked, toBeDisabled, toBeEditable, diff --git a/packages/playwright-test/src/matchers/matchers.ts b/packages/playwright-test/src/matchers/matchers.ts index bd62c79b11..25f6635204 100644 --- a/packages/playwright-test/src/matchers/matchers.ts +++ b/packages/playwright-test/src/matchers/matchers.ts @@ -34,6 +34,17 @@ interface APIResponseEx extends APIResponse { _fetchLog(): Promise; } +export function toBeAttached( + this: ReturnType, + locator: LocatorEx, + options?: { attached?: boolean, timeout?: number }, +) { + return toBeTruthy.call(this, 'toBeAttached', locator, 'Locator', async (isNot, timeout) => { + const attached = !options || options.attached === undefined || options.attached === true; + return await locator._expect(attached ? 'to.be.attached' : 'to.be.detached', { isNot, timeout }); + }, options); +} + export function toBeChecked( this: ReturnType, locator: LocatorEx, diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index 3689130dd6..b7d851cc08 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -4321,6 +4321,26 @@ interface APIResponseAssertions { * */ interface LocatorAssertions { + /** + * Ensures that [Locator] points to an [attached](https://playwright.dev/docs/actionability#attached) DOM node. + * + * **Usage** + * + * ```js + * await expect(page.getByText('Hidden text')).toBeAttached(); + * ``` + * + * @param options + */ + toBeAttached(options?: { + attached?: boolean; + + /** + * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + /** * Ensures the [Locator] points to a checked input. * @@ -4503,8 +4523,7 @@ interface LocatorAssertions { * **Usage** * * ```js - * const locator = page.locator('.my-element'); - * await expect(locator).toBeVisible(); + * await expect(page.getByText('Welcome')).toBeVisible(); * ``` * * @param options diff --git a/tests/page/expect-boolean.spec.ts b/tests/page/expect-boolean.spec.ts index f66a035693..719fa33877 100644 --- a/tests/page/expect-boolean.spec.ts +++ b/tests/page/expect-boolean.spec.ts @@ -523,3 +523,106 @@ test.describe(() => { }); }); }); + +test.describe('toBeAttached', () => { + test('default', async ({ page }) => { + await page.setContent(''); + const locator = page.locator('input'); + await expect(locator).toBeAttached(); + }); + + test('with hidden element', async ({ page }) => { + await page.setContent(''); + const locator = page.locator('button'); + await expect(locator).toBeAttached(); + }); + + test('with not', async ({ page }) => { + await page.setContent(''); + const locator = page.locator('input'); + await expect(locator).not.toBeAttached(); + }); + + test('with attached:true', async ({ page }) => { + await page.setContent(''); + const locator = page.locator('button'); + await expect(locator).toBeAttached({ attached: true }); + }); + + test('with attached:false', async ({ page }) => { + await page.setContent(''); + const locator = page.locator('input'); + await expect(locator).toBeAttached({ attached: false }); + }); + + test('with not and attached:false', async ({ page }) => { + await page.setContent(''); + const locator = page.locator('button'); + await expect(locator).not.toBeAttached({ attached: false }); + }); + + test('eventually', async ({ page }) => { + await page.setContent('
'); + const locator = page.locator('span'); + setTimeout(() => { + page.$eval('div', div => div.innerHTML = 'Hello').catch(() => {}); + }, 0); + await expect(locator).toBeAttached(); + }); + + test('eventually with not', async ({ page }) => { + await page.setContent('
Hello
'); + const locator = page.locator('span'); + setTimeout(() => { + page.$eval('div', div => div.textContent = '').catch(() => {}); + }, 0); + await expect(locator).not.toBeAttached(); + }); + + test('fail', async ({ page }) => { + await page.setContent(''); + const locator = page.locator('input'); + const error = await expect(locator).toBeAttached({ timeout: 1000 }).catch(e => e); + expect(error.message).not.toContain(`locator resolved to`); + }); + + test('fail with not', async ({ page }) => { + await page.setContent(''); + const locator = page.locator('input'); + const error = await expect(locator).not.toBeAttached({ timeout: 1000 }).catch(e => e); + expect(error.message).toContain(`locator resolved to `); + }); + + test('with impossible timeout', async ({ page }) => { + await page.setContent('
Text content
'); + await expect(page.locator('#node')).toBeAttached({ timeout: 1 }); + }); + + test('with impossible timeout .not', async ({ page }) => { + await page.setContent('
Text content
'); + await expect(page.locator('no-such-thing')).not.toBeAttached({ timeout: 1 }); + }); + + test('with frameLocator', async ({ page }) => { + await page.setContent('
'); + const locator = page.frameLocator('iframe').locator('input'); + let done = false; + const promise = expect(locator).toBeAttached().then(() => done = true); + await page.waitForTimeout(1000); + expect(done).toBe(false); + await page.setContent(''); + await promise; + expect(done).toBe(true); + }); + + test('over navigation', async ({ page, server }) => { + await page.goto(server.EMPTY_PAGE); + let done = false; + const promise = expect(page.locator('input')).toBeAttached().then(() => done = true); + await page.waitForTimeout(1000); + expect(done).toBe(false); + await page.goto(server.PREFIX + '/input/checkbox.html'); + await promise; + expect(done).toBe(true); + }); +});