Skip to content

Commit

Permalink
feat(expect): expect(locator).toBeAttached() (#22067)
Browse files Browse the repository at this point in the history
Fixes #13467.
  • Loading branch information
dgozman committed Mar 29, 2023
1 parent 0b170dd commit 6929214
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 15 deletions.
73 changes: 60 additions & 13 deletions docs/src/api/class-locatorassertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions packages/playwright-core/src/server/injected/injectedScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright-test/src/matchers/expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
pollAgainstTimeout } from 'playwright-core/lib/utils';
import type { ExpectZone } from 'playwright-core/lib/utils';
import {
toBeAttached,
toBeChecked,
toBeDisabled,
toBeEditable,
Expand Down Expand Up @@ -130,6 +131,7 @@ expect.poll = (actual: unknown, messageOrOptions: ExpectMessageOrOptions) => {

expectLibrary.setState({ expand: false });
const customMatchers = {
toBeAttached,
toBeChecked,
toBeDisabled,
toBeEditable,
Expand Down
11 changes: 11 additions & 0 deletions packages/playwright-test/src/matchers/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ interface APIResponseEx extends APIResponse {
_fetchLog(): Promise<string[]>;
}

export function toBeAttached(
this: ReturnType<Expect['getState']>,
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<Expect['getState']>,
locator: LocatorEx,
Expand Down
23 changes: 21 additions & 2 deletions packages/playwright-test/types/test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;

/**
* Ensures the [Locator] points to a checked input.
*
Expand Down Expand Up @@ -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
Expand Down
103 changes: 103 additions & 0 deletions tests/page/expect-boolean.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,3 +523,106 @@ test.describe(() => {
});
});
});

test.describe('toBeAttached', () => {
test('default', async ({ page }) => {
await page.setContent('<input></input>');
const locator = page.locator('input');
await expect(locator).toBeAttached();
});

test('with hidden element', async ({ page }) => {
await page.setContent('<button style="display:none">hello</button>');
const locator = page.locator('button');
await expect(locator).toBeAttached();
});

test('with not', async ({ page }) => {
await page.setContent('<button>hello</button>');
const locator = page.locator('input');
await expect(locator).not.toBeAttached();
});

test('with attached:true', async ({ page }) => {
await page.setContent('<button>hello</button>');
const locator = page.locator('button');
await expect(locator).toBeAttached({ attached: true });
});

test('with attached:false', async ({ page }) => {
await page.setContent('<button>hello</button>');
const locator = page.locator('input');
await expect(locator).toBeAttached({ attached: false });
});

test('with not and attached:false', async ({ page }) => {
await page.setContent('<button>hello</button>');
const locator = page.locator('button');
await expect(locator).not.toBeAttached({ attached: false });
});

test('eventually', async ({ page }) => {
await page.setContent('<div></div>');
const locator = page.locator('span');
setTimeout(() => {
page.$eval('div', div => div.innerHTML = '<span>Hello</span>').catch(() => {});
}, 0);
await expect(locator).toBeAttached();
});

test('eventually with not', async ({ page }) => {
await page.setContent('<div><span>Hello</span></div>');
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('<button>Hello</button>');
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('<input></input>');
const locator = page.locator('input');
const error = await expect(locator).not.toBeAttached({ timeout: 1000 }).catch(e => e);
expect(error.message).toContain(`locator resolved to <input/>`);
});

test('with impossible timeout', async ({ page }) => {
await page.setContent('<div id=node>Text content</div>');
await expect(page.locator('#node')).toBeAttached({ timeout: 1 });
});

test('with impossible timeout .not', async ({ page }) => {
await page.setContent('<div id=node>Text content</div>');
await expect(page.locator('no-such-thing')).not.toBeAttached({ timeout: 1 });
});

test('with frameLocator', async ({ page }) => {
await page.setContent('<div></div>');
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('<iframe srcdoc="<input>"></iframe>');
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);
});
});

0 comments on commit 6929214

Please sign in to comment.