diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index aef5639515fb2..78ed036c0fab3 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -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 +
+``` + +```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 + + + + + +``` + +```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 diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index ebb57cc23b908..0e5d07bcf2da8 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -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, diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index c050b6e8ddce0..565e2dff6a07b 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -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, diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index 0d8ef8213c2c3..342b559fa1498 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -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); } } diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index c349529c0ec61..37fd01e81b539 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -8200,6 +8200,51 @@ interface LocatorAssertions { visible?: boolean; }): Promise