chore: introduce expect.toContainClass (instead of partial: true) (#35551)
Signed-off-by: Max Schmitt <max@schmitt.mx> Co-authored-by: Dmitry Gozman <dgozman@gmail.com>
This commit is contained in:
parent
a8314361b0
commit
22e134e280
|
@ -197,6 +197,21 @@ The opposite of [`method: LocatorAssertions.toBeVisible`].
|
|||
### option: LocatorAssertions.NotToBeVisible.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
* since: v1.18
|
||||
|
||||
## async method: LocatorAssertions.NotToContainClass
|
||||
* since: v1.52
|
||||
* langs: python
|
||||
|
||||
The opposite of [`method: LocatorAssertions.toContainClass`].
|
||||
|
||||
### param: LocatorAssertions.NotToContainClass.expected
|
||||
* since: v1.52
|
||||
- `expected` <[string]|[Array]<[string]>>
|
||||
|
||||
Expected class or RegExp or a list of those.
|
||||
|
||||
### option: LocatorAssertions.NotToContainClass.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
* since: v1.52
|
||||
|
||||
## async method: LocatorAssertions.NotToContainText
|
||||
* since: v1.20
|
||||
* langs: python
|
||||
|
@ -1018,6 +1033,107 @@ await Expect(
|
|||
### option: LocatorAssertions.toBeVisible.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
* since: v1.18
|
||||
|
||||
## async method: LocatorAssertions.toContainClass
|
||||
* since: v1.52
|
||||
* langs:
|
||||
- alias-java: containsClass
|
||||
|
||||
Ensures the [Locator] points to an element with given CSS classes. All classes from the asserted value, separated by spaces, must be present in the [Element.classList](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) in any order.
|
||||
|
||||
**Usage**
|
||||
|
||||
```html
|
||||
<div class='middle selected row' id='component'></div>
|
||||
```
|
||||
|
||||
```js
|
||||
const locator = page.locator('#component');
|
||||
await expect(locator).toContainClass('middle selected row');
|
||||
await expect(locator).toContainClass('selected');
|
||||
await expect(locator).toContainClass('row middle');
|
||||
```
|
||||
|
||||
```java
|
||||
assertThat(page.locator("#component")).containsClass("middle selected row");
|
||||
assertThat(page.locator("#component")).containsClass("selected");
|
||||
assertThat(page.locator("#component")).containsClass("row middle");
|
||||
```
|
||||
|
||||
```python async
|
||||
from playwright.async_api import expect
|
||||
|
||||
locator = page.locator("#component")
|
||||
await expect(locator).to_contain_class("middle selected row")
|
||||
await expect(locator).to_contain_class("selected")
|
||||
await expect(locator).to_contain_class("row middle")
|
||||
```
|
||||
|
||||
```python sync
|
||||
from playwright.sync_api import expect
|
||||
|
||||
locator = page.locator("#component")
|
||||
expect(locator).to_contain_class("middle selected row")
|
||||
expect(locator).to_contain_class("selected")
|
||||
expect(locator).to_contain_class("row middle")
|
||||
```
|
||||
|
||||
```csharp
|
||||
var locator = Page.Locator("#component");
|
||||
await Expect(locator).ToContainClassAsync("middle selected row");
|
||||
await Expect(locator).ToContainClassAsync("selected");
|
||||
await Expect(locator).ToContainClassAsync("row middle");
|
||||
```
|
||||
|
||||
When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected class lists. Each element's class attribute is matched against the corresponding class in the array:
|
||||
|
||||
```html
|
||||
<div class='list'></div>
|
||||
<div class='component inactive'></div>
|
||||
<div class='component active'></div>
|
||||
<div class='component inactive'></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
const locator = page.locator('list > .component');
|
||||
await expect(locator).toContainClass(['inactive', 'active', 'inactive']);
|
||||
```
|
||||
|
||||
```java
|
||||
assertThat(page.locator("list > .component")).containsClass(new String[] {"inactive", "active", "inactive"});
|
||||
```
|
||||
|
||||
```python async
|
||||
from playwright.async_api import expect
|
||||
|
||||
locator = page.locator("list > .component")
|
||||
await expect(locator).to_contain_class(["inactive", "active", "inactive"])
|
||||
```
|
||||
|
||||
```python sync
|
||||
from playwright.sync_api import expect
|
||||
|
||||
locator = page.locator("list > .component")
|
||||
await expect(locator).to_contain_class(["inactive", "active", "inactive"])
|
||||
```
|
||||
|
||||
```csharp
|
||||
var locator = Page.Locator("list > .component");
|
||||
await Expect(locator).ToContainClassAsync(new string[]{"inactive", "active", "inactive"});
|
||||
```
|
||||
|
||||
### param: LocatorAssertions.toContainClass.expected
|
||||
* since: v1.52
|
||||
- `expected` <[string]|[Array]<[string]>>
|
||||
|
||||
A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements.
|
||||
|
||||
### option: LocatorAssertions.toContainClass.timeout = %%-js-assertions-timeout-%%
|
||||
* since: v1.52
|
||||
|
||||
### option: LocatorAssertions.toContainClass.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
* since: v1.52
|
||||
|
||||
## async method: LocatorAssertions.toContainText
|
||||
* since: v1.20
|
||||
* langs:
|
||||
|
@ -1431,7 +1547,7 @@ Attribute name.
|
|||
* langs:
|
||||
- alias-java: hasClass
|
||||
|
||||
Ensures the [Locator] points to an element with given CSS classes. When a string is provided, it must fully match the element's `class` attribute. To match individual classes or perform partial matches use [`option: LocatorAssertions.toHaveClass.partial`].
|
||||
Ensures the [Locator] points to an element with given CSS classes. When a string is provided, it must fully match the element's `class` attribute. To match individual classes use [`method: LocatorAssertions.toContainClass`].
|
||||
|
||||
**Usage**
|
||||
|
||||
|
@ -1442,14 +1558,12 @@ Ensures the [Locator] points to an element with given CSS classes. When a string
|
|||
```js
|
||||
const locator = page.locator('#component');
|
||||
await expect(locator).toHaveClass('middle selected row');
|
||||
await expect(locator).toHaveClass('selected', { partial: true });
|
||||
await expect(locator).toHaveClass('middle row', { partial: true });
|
||||
await expect(locator).toHaveClass(/(^|\s)selected(\s|$)/);
|
||||
```
|
||||
|
||||
```java
|
||||
assertThat(page.locator("#component")).hasClass("middle selected row");
|
||||
assertThat(page.locator("#component")).hasClass("selected", new LocatorAssertions.HasClassOptions().setPartial(true));
|
||||
assertThat(page.locator("#component")).hasClass("middle row", new LocatorAssertions.HasClassOptions().setPartial(true));
|
||||
assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
|
||||
```
|
||||
|
||||
```python async
|
||||
|
@ -1457,7 +1571,7 @@ from playwright.async_api import expect
|
|||
|
||||
locator = page.locator("#component")
|
||||
await expect(locator).to_have_class("middle selected row")
|
||||
await expect(locator).to_have_class("middle row", partial=True)
|
||||
await expect(locator).to_have_class(re.compile(r"(^|\\s)selected(\\s|$)"))
|
||||
```
|
||||
|
||||
```python sync
|
||||
|
@ -1465,15 +1579,13 @@ from playwright.sync_api import expect
|
|||
|
||||
locator = page.locator("#component")
|
||||
expect(locator).to_have_class("middle selected row")
|
||||
expect(locator).to_have_class("selected", partial=True)
|
||||
expect(locator).to_have_class("middle row", partial=True)
|
||||
expect(locator).to_have_class(re.compile(r"(^|\\s)selected(\\s|$)"))
|
||||
```
|
||||
|
||||
```csharp
|
||||
var locator = Page.Locator("#component");
|
||||
await Expect(locator).ToHaveClassAsync("middle selected row");
|
||||
await Expect(locator).ToHaveClassAsync("selected", new() { Partial = true });
|
||||
await Expect(locator).ToHaveClassAsync("middle row", new() { Partial = true });
|
||||
await Expect(locator).ToHaveClassAsync(new Regex("(^|\\s)selected(\\s|$)"));
|
||||
```
|
||||
|
||||
When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected class values. Each element's class attribute is matched against the corresponding string or regular expression in the array:
|
||||
|
@ -1527,12 +1639,6 @@ Expected class or RegExp or a list of those.
|
|||
|
||||
Expected class or RegExp or a list of those.
|
||||
|
||||
### option: LocatorAssertions.toHaveClass.partial
|
||||
* since: v1.52
|
||||
- `partial` <[boolean]>
|
||||
|
||||
Whether to perform a partial match, defaults to `false`. In an exact match, which is the default, the `className` attribute must be exactly the same as the asserted value. In a partial match, all classes from the asserted value, separated by spaces, must be present in the [Element.classList](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) in any order. Partial match does not support a regular expression.
|
||||
|
||||
### option: LocatorAssertions.toHaveClass.timeout = %%-js-assertions-timeout-%%
|
||||
* since: v1.18
|
||||
|
||||
|
|
|
@ -1452,12 +1452,12 @@ export class InjectedScript {
|
|||
if (value === null)
|
||||
return { received: null, matches: false };
|
||||
received = value;
|
||||
} else if (expression === 'to.have.class') {
|
||||
} else if (['to.have.class', 'to.contain.class'].includes(expression)) {
|
||||
if (!options.expectedText)
|
||||
throw this.createStacklessError('Expected text is not provided for ' + expression);
|
||||
return {
|
||||
received: element.classList.toString(),
|
||||
matches: new ExpectedTextMatcher(this.builtins, options.expectedText[0]).matchesClassList(this, element.classList, options.expressionArg.partial),
|
||||
matches: new ExpectedTextMatcher(this.builtins, options.expectedText[0]).matchesClassList(this, element.classList, /* partial */ expression === 'to.contain.class'),
|
||||
};
|
||||
} else if (expression === 'to.have.css') {
|
||||
received = this.window.getComputedStyle(element).getPropertyValue(options.expressionArg);
|
||||
|
@ -1506,13 +1506,13 @@ export class InjectedScript {
|
|||
if (!options.expectedText)
|
||||
throw this.createStacklessError('Expected text is not provided for ' + expression);
|
||||
|
||||
if (expression === 'to.have.class.array') {
|
||||
if (['to.have.class.array', 'to.contain.class.array'].includes(expression)) {
|
||||
const receivedClassLists = elements.map(e => e.classList);
|
||||
const received = receivedClassLists.map(String);
|
||||
if (receivedClassLists.length !== options.expectedText.length)
|
||||
return { received, matches: false };
|
||||
const matches = this._matchSequentially(options.expectedText, receivedClassLists, (matcher, r) =>
|
||||
matcher.matchesClassList(this, r, options.expressionArg.partial)
|
||||
matcher.matchesClassList(this, r, /* partial */ expression === 'to.contain.class.array')
|
||||
);
|
||||
return {
|
||||
received: received,
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
toBeInViewport,
|
||||
toBeOK,
|
||||
toBeVisible,
|
||||
toContainClass,
|
||||
toContainText,
|
||||
toHaveAccessibleDescription,
|
||||
toHaveAccessibleErrorMessage,
|
||||
|
@ -257,6 +258,7 @@ const customAsyncMatchers = {
|
|||
toBeOK,
|
||||
toBeVisible,
|
||||
toContainText,
|
||||
toContainClass,
|
||||
toHaveAccessibleDescription,
|
||||
toHaveAccessibleName,
|
||||
toHaveAccessibleErrorMessage,
|
||||
|
|
|
@ -252,18 +252,40 @@ export function toHaveClass(
|
|||
this: ExpectMatcherState,
|
||||
locator: LocatorEx,
|
||||
expected: string | RegExp | (string | RegExp)[],
|
||||
options?: { timeout?: number, partial: boolean },
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
const partial = options?.partial;
|
||||
if (Array.isArray(expected)) {
|
||||
return toEqual.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => {
|
||||
const expectedText = serializeExpectedTextValues(expected);
|
||||
return await locator._expect('to.have.class.array', { expectedText, expressionArg: { partial }, isNot, timeout });
|
||||
return await locator._expect('to.have.class.array', { expectedText, isNot, timeout });
|
||||
}, expected, options);
|
||||
} else {
|
||||
return toMatchText.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => {
|
||||
const expectedText = serializeExpectedTextValues([expected]);
|
||||
return await locator._expect('to.have.class', { expectedText, expressionArg: { partial }, isNot, timeout });
|
||||
return await locator._expect('to.have.class', { expectedText, isNot, timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
}
|
||||
|
||||
export function toContainClass(
|
||||
this: ExpectMatcherState,
|
||||
locator: LocatorEx,
|
||||
expected: string | string[],
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
if (Array.isArray(expected)) {
|
||||
if (expected.some(e => isRegExp(e)))
|
||||
throw new Error(`"expected" argument in toContainClass cannot contain RegExp values`);
|
||||
return toEqual.call(this, 'toContainClass', locator, 'Locator', async (isNot, timeout) => {
|
||||
const expectedText = serializeExpectedTextValues(expected);
|
||||
return await locator._expect('to.contain.class.array', { expectedText, isNot, timeout });
|
||||
}, expected, options);
|
||||
} else {
|
||||
if (isRegExp(expected))
|
||||
throw new Error(`"expected" argument in toContainClass cannot be a RegExp value`);
|
||||
return toMatchText.call(this, 'toContainClass', locator, 'Locator', async (isNot, timeout) => {
|
||||
const expectedText = serializeExpectedTextValues([expected]);
|
||||
return await locator._expect('to.contain.class', { expectedText, isNot, timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8200,6 +8200,51 @@ interface LocatorAssertions {
|
|||
visible?: boolean;
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with given CSS classes.
|
||||
* All classes from the asserted value, separated by spaces, must be present in the
|
||||
* [Element.classList](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) in any order.
|
||||
*
|
||||
* **Usage**
|
||||
*
|
||||
* ```html
|
||||
* <div class='middle selected row' id='component'></div>
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* const locator = page.locator('#component');
|
||||
* await expect(locator).toContainClass('middle selected row');
|
||||
* await expect(locator).toContainClass('selected');
|
||||
* await expect(locator).toContainClass('row middle');
|
||||
* ```
|
||||
*
|
||||
* When an array is passed, the method asserts that the list of elements located matches the corresponding list of
|
||||
* expected class lists. Each element's class attribute is matched against the corresponding class in the array:
|
||||
*
|
||||
* ```html
|
||||
* <div class='list'></div>
|
||||
* <div class='component inactive'></div>
|
||||
* <div class='component active'></div>
|
||||
* <div class='component inactive'></div>
|
||||
* </div>
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* const locator = page.locator('list > .component');
|
||||
* await expect(locator).toContainClass(['inactive', 'active', 'inactive']);
|
||||
* ```
|
||||
*
|
||||
* @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple
|
||||
* elements.
|
||||
* @param options
|
||||
*/
|
||||
toContainClass(expected: string|ReadonlyArray<string>, options?: {
|
||||
/**
|
||||
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
|
||||
*/
|
||||
timeout?: number;
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element that contains the given
|
||||
* text. All nested elements will be considered when computing the text content of the element. You can use regular
|
||||
|
@ -8407,9 +8452,8 @@ interface LocatorAssertions {
|
|||
|
||||
/**
|
||||
* Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with given CSS classes.
|
||||
* When a string is provided, it must fully match the element's `class` attribute. To match individual classes or
|
||||
* perform partial matches use
|
||||
* [`partial`](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-have-class-option-partial).
|
||||
* When a string is provided, it must fully match the element's `class` attribute. To match individual classes use
|
||||
* [expect(locator).toContainClass(expected[, options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-contain-class).
|
||||
*
|
||||
* **Usage**
|
||||
*
|
||||
|
@ -8420,8 +8464,7 @@ interface LocatorAssertions {
|
|||
* ```js
|
||||
* const locator = page.locator('#component');
|
||||
* await expect(locator).toHaveClass('middle selected row');
|
||||
* await expect(locator).toHaveClass('selected', { partial: true });
|
||||
* await expect(locator).toHaveClass('middle row', { partial: true });
|
||||
* await expect(locator).toHaveClass(/(^|\s)selected(\s|$)/);
|
||||
* ```
|
||||
*
|
||||
* When an array is passed, the method asserts that the list of elements located matches the corresponding list of
|
||||
|
@ -8437,15 +8480,6 @@ interface LocatorAssertions {
|
|||
* @param options
|
||||
*/
|
||||
toHaveClass(expected: string|RegExp|ReadonlyArray<string|RegExp>, options?: {
|
||||
/**
|
||||
* Whether to perform a partial match, defaults to `false`. In an exact match, which is the default, the `className`
|
||||
* attribute must be exactly the same as the asserted value. In a partial match, all classes from the asserted value,
|
||||
* separated by spaces, must be present in the
|
||||
* [Element.classList](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) in any order. Partial match
|
||||
* does not support a regular expression.
|
||||
*/
|
||||
partial?: boolean;
|
||||
|
||||
/**
|
||||
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
|
||||
*/
|
||||
|
|
|
@ -220,37 +220,46 @@ test.describe('toHaveClass', () => {
|
|||
const error = await expect(locator).toHaveClass(['foo', 'bar', /[a-z]az/], { timeout: 1000 }).catch(e => e);
|
||||
expect(error.message).toContain('expect.toHaveClass with timeout 1000ms');
|
||||
});
|
||||
});
|
||||
|
||||
test('allow matching partial class names', async ({ page }) => {
|
||||
await page.setContent('<div class="foo bar"></div>');
|
||||
const locator = page.locator('div');
|
||||
await expect(locator).toHaveClass('foo', { partial: true });
|
||||
await expect(locator).toHaveClass('bar', { partial: true });
|
||||
await expect(
|
||||
expect(locator).toHaveClass(/f.o/, { partial: true })
|
||||
).rejects.toThrow('Partial matching does not support regular expressions. Please provide a string value.');
|
||||
await expect(locator).not.toHaveClass('foo');
|
||||
await expect(locator).not.toHaveClass('foo', { partial: false });
|
||||
await expect(locator).toHaveClass(' bar foo ', { partial: true });
|
||||
await expect(locator).not.toHaveClass('does-not-exist', { partial: true });
|
||||
await expect(locator).not.toHaveClass(' baz foo ', { partial: true }); // Strip whitespace and match individual classes
|
||||
|
||||
test.describe('toContainClass', () => {
|
||||
test('pass', async ({ page }) => {
|
||||
await page.setContent('<div class="foo bar baz"></div>');
|
||||
await expect(locator).toHaveClass('foo bar', { partial: true });
|
||||
await expect(locator).toHaveClass('', { partial: true });
|
||||
const locator = page.locator('div');
|
||||
await expect(locator).toContainClass('');
|
||||
await expect(locator).toContainClass('bar');
|
||||
await expect(locator).toContainClass('baz bar');
|
||||
await expect(locator).toContainClass(' bar foo ');
|
||||
await expect(locator).not.toContainClass(' baz not-matching '); // Strip whitespace and match individual classes
|
||||
expect(() => expect(locator).toContainClass(/foo|bar/ as any)).toThrow(/"expected\" argument in toContainClass cannot be a RegExp value/);
|
||||
});
|
||||
|
||||
test('allow matching partial class names with array', async ({ page }) => {
|
||||
await page.setContent('<div class="aaa"></div><div class="bbb b2b"></div><div class="ccc"></div>');
|
||||
test('pass with SVGs', async ({ page }) => {
|
||||
await page.setContent(`<svg class="c1 c2" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"></svg>`);
|
||||
await expect(page.locator('svg')).toContainClass('c1');
|
||||
});
|
||||
|
||||
test('fail', async ({ page }) => {
|
||||
await page.setContent('<div class="bar baz"></div>');
|
||||
const locator = page.locator('div');
|
||||
await expect(locator).toHaveClass(['aaa', 'b2b', 'ccc'], { partial: true });
|
||||
await expect(locator).not.toHaveClass(['aaa', 'b2b', 'ccc']);
|
||||
await expect(
|
||||
expect(locator).toHaveClass([/b2?ar/, /b2?ar/, /b2?ar/], { partial: true })
|
||||
).rejects.toThrow('Partial matching does not support regular expressions. Please provide a string value.');
|
||||
await expect(locator).not.toHaveClass(['aaa', 'b2b', 'ccc'], { partial: false });
|
||||
await expect(locator).not.toHaveClass(['not-there', 'b2b', 'ccc'], { partial: true }); // Class not there
|
||||
await expect(locator).not.toHaveClass(['aaa', 'b2b'], { partial: false }); // Length mismatch
|
||||
const error = await expect(locator).toContainClass('does-not-exist', { timeout: 1000 }).catch(e => e);
|
||||
expect(error.message).toContain('expect.toContainClass with timeout 1000ms');
|
||||
});
|
||||
|
||||
test('pass with array', async ({ page }) => {
|
||||
await page.setContent('<div class="foo"></div><div class="hello bar"></div><div class="baz"></div>');
|
||||
const locator = page.locator('div');
|
||||
await expect(locator).toContainClass(['foo', 'hello', 'baz']);
|
||||
expect(() => expect(locator).toContainClass(['foo', 'hello', /baz/] as any)).toThrow(/"expected" argument in toContainClass cannot contain RegExp values/);
|
||||
await expect(locator).not.toHaveClass(['not-there', 'hello', 'baz']); // Class not there
|
||||
await expect(locator).not.toHaveClass(['foo', 'hello']); // Length mismatch
|
||||
});
|
||||
|
||||
test('fail with array', async ({ page }) => {
|
||||
await page.setContent('<div class="foo"></div><div class="bar"></div><div class="bar"></div>');
|
||||
const locator = page.locator('div');
|
||||
const error = await expect(locator).toContainClass(['foo', 'bar', 'baz'], { timeout: 1000 }).catch(e => e);
|
||||
expect(error.message).toContain('expect.toContainClass with timeout 1000ms');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue