Skip to content

Commit 6b2ae72

Browse files
committed
chore: introduce expect.toContainClass (instead of partial: true)
1 parent b7799f3 commit 6b2ae72

File tree

6 files changed

+240
-51
lines changed

6 files changed

+240
-51
lines changed

docs/src/api/class-locatorassertions.md

Lines changed: 132 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,21 @@ The opposite of [`method: LocatorAssertions.toBeVisible`].
197197
### option: LocatorAssertions.NotToBeVisible.timeout = %%-csharp-java-python-assertions-timeout-%%
198198
* since: v1.18
199199

200+
## async method: LocatorAssertions.NotToContainClass
201+
* since: v1.52
202+
* langs: python
203+
204+
The opposite of [`method: LocatorAssertions.toContainClass`].
205+
206+
### param: LocatorAssertions.NotToContainClass.expected
207+
* since: v1.52
208+
- `expected` <[string]|[RegExp]|[Array]<[string]>|[Array]<[RegExp]>|[Array]<[string]|[RegExp]>>
209+
210+
Expected class or RegExp or a list of those.
211+
212+
### option: LocatorAssertions.NotToContainClass.timeout = %%-csharp-java-python-assertions-timeout-%%
213+
* since: v1.52
214+
200215
## async method: LocatorAssertions.NotToContainText
201216
* since: v1.20
202217
* langs: python
@@ -1018,6 +1033,122 @@ await Expect(
10181033
### option: LocatorAssertions.toBeVisible.timeout = %%-csharp-java-python-assertions-timeout-%%
10191034
* since: v1.18
10201035

1036+
## async method: LocatorAssertions.toContainClass
1037+
* since: v1.52
1038+
* langs:
1039+
- alias-java: containsClass
1040+
1041+
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.
1042+
1043+
**Usage**
1044+
1045+
```html
1046+
<div class='middle selected row' id='component'></div>
1047+
```
1048+
1049+
```js
1050+
const locator = page.locator('#component');
1051+
await expect(locator).toContainClass('middle selected row');
1052+
await expect(locator).toContainClass('selected');
1053+
await expect(locator).toContainClass('row middle');
1054+
```
1055+
1056+
```java
1057+
assertThat(page.locator("#component")).containsClass("middle selected row");
1058+
assertThat(page.locator("#component")).containsClass("selected");
1059+
assertThat(page.locator("#component")).containsClass("row middle");
1060+
```
1061+
1062+
```python async
1063+
from playwright.async_api import expect
1064+
1065+
locator = page.locator("#component")
1066+
await expect(locator).to_contain_class("middle selected row")
1067+
await expect(locator).to_contain_class("selected")
1068+
await expect(locator).to_contain_class("row middle")
1069+
```
1070+
1071+
```python sync
1072+
from playwright.sync_api import expect
1073+
1074+
locator = page.locator("#component")
1075+
expect(locator).to_contain_class("middle selected row")
1076+
expect(locator).to_contain_class("selected")
1077+
expect(locator).to_contain_class("row middle")
1078+
```
1079+
1080+
```csharp
1081+
var locator = Page.Locator("#component");
1082+
await Expect(locator).ToContainClassAsync("middle selected row");
1083+
await Expect(locator).ToContainClassAsync("selected");
1084+
await Expect(locator).ToContainClassAsync("row middle");
1085+
```
1086+
1087+
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:
1088+
1089+
```html
1090+
<div class='list'></div>
1091+
<div class='component'></div>
1092+
<div class='component selected active-element'></div>
1093+
<div class='component'></div>
1094+
</div>
1095+
```
1096+
1097+
```js
1098+
const locator = page.locator('list > .component');
1099+
await expect(locator).toContainClass(['component', 'component selected', 'component']);
1100+
```
1101+
1102+
```java
1103+
assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"});
1104+
```
1105+
1106+
```python async
1107+
from playwright.async_api import expect
1108+
1109+
locator = page.locator("list > .component")
1110+
await expect(locator).to_have_class(["component", "component selected", "component"])
1111+
```
1112+
1113+
```python sync
1114+
from playwright.sync_api import expect
1115+
1116+
locator = page.locator("list > .component")
1117+
expect(locator).to_have_class(["component", "component selected", "component"])
1118+
```
1119+
1120+
```csharp
1121+
var locator = Page.Locator("list > .component");
1122+
await Expect(locator).ToContainClassAsync(new string[]{"component", "component selected", "component"});
1123+
```
1124+
1125+
### param: LocatorAssertions.toContainClass.expected
1126+
* since: v1.52
1127+
* langs: js
1128+
- `expected` <[string]|[RegExp]|[Array]<[string]|[RegExp]>>
1129+
1130+
Expected class or RegExp or a list of those.
1131+
1132+
### param: LocatorAssertions.toContainClass.expected
1133+
* since: v1.52
1134+
* langs: python
1135+
- `expected` <[string]|[RegExp]|[Array]<[string]>|[Array]<[RegExp]>|[Array]<[string]|[RegExp]>>
1136+
1137+
Expected class or RegExp or a list of those.
1138+
1139+
### param: LocatorAssertions.toContainClass.expected
1140+
* since: v1.52
1141+
* langs: java, csharp
1142+
- `expected` <[string]|[RegExp]|[Array]<[string]>|[Array]<[RegExp]>>
1143+
1144+
Expected class or RegExp or a list of those.
1145+
1146+
### option: LocatorAssertions.toContainClass.timeout = %%-js-assertions-timeout-%%
1147+
* since: v1.52
1148+
1149+
### option: LocatorAssertions.toContainClass.timeout = %%-csharp-java-python-assertions-timeout-%%
1150+
* since: v1.52
1151+
10211152
## async method: LocatorAssertions.toContainText
10221153
* since: v1.20
10231154
* langs:
@@ -1431,7 +1562,7 @@ Attribute name.
14311562
* langs:
14321563
- alias-java: hasClass
14331564

1434-
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`].
1565+
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 [`method: LocatorAssertions.toContainClass`].
14351566

14361567
**Usage**
14371568

@@ -1527,12 +1658,6 @@ Expected class or RegExp or a list of those.
15271658

15281659
Expected class or RegExp or a list of those.
15291660

1530-
### option: LocatorAssertions.toHaveClass.partial
1531-
* since: v1.52
1532-
- `partial` <[boolean]>
1533-
1534-
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.
1535-
15361661
### option: LocatorAssertions.toHaveClass.timeout = %%-js-assertions-timeout-%%
15371662
* since: v1.18
15381663

packages/injected/src/injectedScript.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,12 +1452,12 @@ export class InjectedScript {
14521452
if (value === null)
14531453
return { received: null, matches: false };
14541454
received = value;
1455-
} else if (expression === 'to.have.class') {
1455+
} else if (['to.have.class', 'to.contain.class'].includes(expression)) {
14561456
if (!options.expectedText)
14571457
throw this.createStacklessError('Expected text is not provided for ' + expression);
14581458
return {
14591459
received: element.classList.toString(),
1460-
matches: new ExpectedTextMatcher(this.builtins, options.expectedText[0]).matchesClassList(this, element.classList, options.expressionArg.partial),
1460+
matches: new ExpectedTextMatcher(this.builtins, options.expectedText[0]).matchesClassList(this, element.classList, /* partial */ expression === 'to.contain.class'),
14611461
};
14621462
} else if (expression === 'to.have.css') {
14631463
received = this.window.getComputedStyle(element).getPropertyValue(options.expressionArg);
@@ -1506,13 +1506,13 @@ export class InjectedScript {
15061506
if (!options.expectedText)
15071507
throw this.createStacklessError('Expected text is not provided for ' + expression);
15081508

1509-
if (expression === 'to.have.class.array') {
1509+
if (['to.have.class.array', 'to.contain.class.array'].includes(expression)) {
15101510
const receivedClassLists = elements.map(e => e.classList);
15111511
const received = receivedClassLists.map(String);
15121512
if (receivedClassLists.length !== options.expectedText.length)
15131513
return { received, matches: false };
15141514
const matches = this._matchSequentially(options.expectedText, receivedClassLists, (matcher, r) =>
1515-
matcher.matchesClassList(this, r, options.expressionArg.partial)
1515+
matcher.matchesClassList(this, r, /* partial */ expression === 'to.contain.class.array')
15161516
);
15171517
return {
15181518
received: received,
@@ -1644,7 +1644,7 @@ class ExpectedTextMatcher {
16441644
matchesClassList(injectedScript: InjectedScript, classList: DOMTokenList, partial: boolean): boolean {
16451645
if (partial) {
16461646
if (this._regex)
1647-
throw injectedScript.createStacklessError('Partial matching does not support regular expressions. Please provide a string value.');
1647+
return !!this._regex.test(classList.toString());
16481648
return this._string!.split(/\s+/g).filter(Boolean).every(className => classList.contains(className));
16491649
}
16501650
return this.matches(classList.toString());

packages/playwright/src/matchers/expect.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
toBeInViewport,
3535
toBeOK,
3636
toBeVisible,
37+
toContainClass,
3738
toContainText,
3839
toHaveAccessibleDescription,
3940
toHaveAccessibleErrorMessage,
@@ -257,6 +258,7 @@ const customAsyncMatchers = {
257258
toBeOK,
258259
toBeVisible,
259260
toContainText,
261+
toContainClass,
260262
toHaveAccessibleDescription,
261263
toHaveAccessibleName,
262264
toHaveAccessibleErrorMessage,

packages/playwright/src/matchers/matchers.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -252,18 +252,36 @@ export function toHaveClass(
252252
this: ExpectMatcherState,
253253
locator: LocatorEx,
254254
expected: string | RegExp | (string | RegExp)[],
255-
options?: { timeout?: number, partial: boolean },
255+
options?: { timeout?: number },
256256
) {
257-
const partial = options?.partial;
258257
if (Array.isArray(expected)) {
259258
return toEqual.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => {
260259
const expectedText = serializeExpectedTextValues(expected);
261-
return await locator._expect('to.have.class.array', { expectedText, expressionArg: { partial }, isNot, timeout });
260+
return await locator._expect('to.have.class.array', { expectedText, isNot, timeout });
262261
}, expected, options);
263262
} else {
264263
return toMatchText.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => {
265264
const expectedText = serializeExpectedTextValues([expected]);
266-
return await locator._expect('to.have.class', { expectedText, expressionArg: { partial }, isNot, timeout });
265+
return await locator._expect('to.have.class', { expectedText, isNot, timeout });
266+
}, expected, options);
267+
}
268+
}
269+
270+
export function toContainClass(
271+
this: ExpectMatcherState,
272+
locator: LocatorEx,
273+
expected: string | RegExp | (string | RegExp)[],
274+
options?: { timeout?: number },
275+
) {
276+
if (Array.isArray(expected)) {
277+
return toEqual.call(this, 'toContainClass', locator, 'Locator', async (isNot, timeout) => {
278+
const expectedText = serializeExpectedTextValues(expected);
279+
return await locator._expect('to.contain.class.array', { expectedText, isNot, timeout });
280+
}, expected, options);
281+
} else {
282+
return toMatchText.call(this, 'toContainClass', locator, 'Locator', async (isNot, timeout) => {
283+
const expectedText = serializeExpectedTextValues([expected]);
284+
return await locator._expect('to.contain.class', { expectedText, isNot, timeout });
267285
}, expected, options);
268286
}
269287
}

packages/playwright/types/test.d.ts

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8200,6 +8200,51 @@ interface LocatorAssertions {
82008200
visible?: boolean;
82018201
}): Promise<void>;
82028202

8203+
/**
8204+
* Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with given CSS classes.
8205+
* All classes from the asserted value, separated by spaces, must be present in the
8206+
* [Element.classList](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) in any order.
8207+
*
8208+
* **Usage**
8209+
*
8210+
* ```html
8211+
* <div class='middle selected row' id='component'></div>
8212+
* ```
8213+
*
8214+
* ```js
8215+
* const locator = page.locator('#component');
8216+
* await expect(locator).toContainClass('middle selected row');
8217+
* await expect(locator).toContainClass('selected');
8218+
* await expect(locator).toContainClass('row middle');
8219+
* ```
8220+
*
8221+
* When an array is passed, the method asserts that the list of elements located matches the corresponding list of
8222+
* expected class values. Each element's class attribute is matched against the corresponding string or regular
8223+
* expression in the array:
8224+
*
8225+
* ```html
8226+
* <div class='list'></div>
8227+
* <div class='component'></div>
8228+
* <div class='component selected active-element'></div>
8229+
* <div class='component'></div>
8230+
* </div>
8231+
* ```
8232+
*
8233+
* ```js
8234+
* const locator = page.locator('list > .component');
8235+
* await expect(locator).toContainClass(['component', 'component selected', 'component']);
8236+
* ```
8237+
*
8238+
* @param expected Expected class or RegExp or a list of those.
8239+
* @param options
8240+
*/
8241+
toContainClass(expected: string|RegExp|ReadonlyArray<string|RegExp>, options?: {
8242+
/**
8243+
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
8244+
*/
8245+
timeout?: number;
8246+
}): Promise<void>;
8247+
82038248
/**
82048249
* Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element that contains the given
82058250
* text. All nested elements will be considered when computing the text content of the element. You can use regular
@@ -8409,7 +8454,7 @@ interface LocatorAssertions {
84098454
* Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with given CSS classes.
84108455
* When a string is provided, it must fully match the element's `class` attribute. To match individual classes or
84118456
* perform partial matches use
8412-
* [`partial`](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-have-class-option-partial).
8457+
* [expect(locator).toContainClass(expected[, options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-contain-class).
84138458
*
84148459
* **Usage**
84158460
*
@@ -8437,15 +8482,6 @@ interface LocatorAssertions {
84378482
* @param options
84388483
*/
84398484
toHaveClass(expected: string|RegExp|ReadonlyArray<string|RegExp>, options?: {
8440-
/**
8441-
* Whether to perform a partial match, defaults to `false`. In an exact match, which is the default, the `className`
8442-
* attribute must be exactly the same as the asserted value. In a partial match, all classes from the asserted value,
8443-
* separated by spaces, must be present in the
8444-
* [Element.classList](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) in any order. Partial match
8445-
* does not support a regular expression.
8446-
*/
8447-
partial?: boolean;
8448-
84498485
/**
84508486
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
84518487
*/

0 commit comments

Comments
 (0)