From 6b2ae7264e5b98f146dcddcb491c1a6dc74107b3 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 9 Apr 2025 10:40:56 +0200 Subject: [PATCH 1/5] chore: introduce expect.toContainClass (instead of partial: true) --- docs/src/api/class-locatorassertions.md | 139 ++++++++++++++++++- packages/injected/src/injectedScript.ts | 10 +- packages/playwright/src/matchers/expect.ts | 2 + packages/playwright/src/matchers/matchers.ts | 26 +++- packages/playwright/types/test.d.ts | 56 ++++++-- tests/page/expect-misc.spec.ts | 58 ++++---- 6 files changed, 240 insertions(+), 51 deletions(-) diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index aef5639515fb2..145b807670dbc 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]|[RegExp]|[Array]<[string]>|[Array]<[RegExp]>|[Array]<[string]|[RegExp]>> + +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,122 @@ 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 values. Each element's class attribute is matched against the corresponding string or regular expression in the array: + +```html +
+
+
+
+ +``` + +```js +const locator = page.locator('list > .component'); +await expect(locator).toContainClass(['component', 'component selected', 'component']); +``` + +```java +assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"}); +``` + +```python async +from playwright.async_api import expect + +locator = page.locator("list > .component") +await expect(locator).to_have_class(["component", "component selected", "component"]) +``` + +```python sync +from playwright.sync_api import expect + +locator = page.locator("list > .component") +expect(locator).to_have_class(["component", "component selected", "component"]) +``` + +```csharp +var locator = Page.Locator("list > .component"); +await Expect(locator).ToContainClassAsync(new string[]{"component", "component selected", "component"}); +``` + +### param: LocatorAssertions.toContainClass.expected +* since: v1.52 +* langs: js +- `expected` <[string]|[RegExp]|[Array]<[string]|[RegExp]>> + +Expected class or RegExp or a list of those. + +### param: LocatorAssertions.toContainClass.expected +* since: v1.52 +* langs: python +- `expected` <[string]|[RegExp]|[Array]<[string]>|[Array]<[RegExp]>|[Array]<[string]|[RegExp]>> + +Expected class or RegExp or a list of those. + +### param: LocatorAssertions.toContainClass.expected +* since: v1.52 +* langs: java, csharp +- `expected` <[string]|[RegExp]|[Array]<[string]>|[Array]<[RegExp]>> + +Expected class or RegExp or a list of those. + +### 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 +1562,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 or perform partial matches use [`method: LocatorAssertions.toContainClass`]. **Usage** @@ -1527,12 +1658,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..0315e54b562a0 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, @@ -1644,7 +1644,7 @@ class ExpectedTextMatcher { matchesClassList(injectedScript: InjectedScript, classList: DOMTokenList, partial: boolean): boolean { if (partial) { if (this._regex) - throw injectedScript.createStacklessError('Partial matching does not support regular expressions. Please provide a string value.'); + return !!this._regex.test(classList.toString()); return this._string!.split(/\s+/g).filter(Boolean).every(className => classList.contains(className)); } return this.matches(classList.toString()); 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..9efad90030a6a 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -252,18 +252,36 @@ 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 | RegExp | (string | RegExp)[], + options?: { timeout?: number }, +) { + if (Array.isArray(expected)) { + 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 { + 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..725a0fe6148b0 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -8200,6 +8200,51 @@ interface LocatorAssertions { visible?: boolean; }): Promise; + /** + * 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 + *
+ * ``` + * + * ```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 values. Each element's class attribute is matched against the corresponding string or regular + * expression in the array: + * + * ```html + *
+ *
+ *
+ *
+ * + * ``` + * + * ```js + * const locator = page.locator('list > .component'); + * await expect(locator).toContainClass(['component', 'component selected', 'component']); + * ``` + * + * @param expected Expected class or RegExp or a list of those. + * @param options + */ + toContainClass(expected: string|RegExp|ReadonlyArray, options?: { + /** + * Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + /** * 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 @@ -8409,7 +8454,7 @@ 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). + * [expect(locator).toContainClass(expected[, options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-contain-class). * * **Usage** * @@ -8437,15 +8482,6 @@ interface LocatorAssertions { * @param options */ toHaveClass(expected: string|RegExp|ReadonlyArray, 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`. */ diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts index 2fb6d7e51633c..033a4690ae1cd 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -220,37 +220,45 @@ 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('
'); +test.describe('toContainClass', () => { + test('pass', async ({ page }) => { + await page.setContent('
'); 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 + 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 + }); - await page.setContent('
'); - await expect(locator).toHaveClass('foo bar', { partial: true }); - await expect(locator).toHaveClass('', { partial: true }); + test('pass with SVGs', async ({ page }) => { + await page.setContent(``); + await expect(page.locator('svg')).toContainClass('c1'); + }); + + test('fail', async ({ page }) => { + await page.setContent('
'); + const locator = page.locator('div'); + const error = await expect(locator).toContainClass('does-not-exist', { timeout: 1000 }).catch(e => e); + expect(error.message).toContain('expect.toContainClass with timeout 1000ms'); }); - test('allow matching partial class names with array', async ({ page }) => { - await page.setContent('
'); + test('pass with array', async ({ page }) => { + await page.setContent('
'); + const locator = page.locator('div'); + await expect(locator).toContainClass(['foo', 'hello', /[a-z]az/]); + await expect(locator).toContainClass([/foo/, /hello/, /[a-z]az/]); + 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('
'); 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(['foo', 'bar', /not-matching/], { timeout: 1000 }).catch(e => e); + expect(error.message).toContain('expect.toContainClass with timeout 1000ms'); }); }); From ce461f27dc61edb339634bbf8bcf7115ae39afbc Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 9 Apr 2025 11:18:17 +0200 Subject: [PATCH 2/5] Apply suggestions from code review Co-authored-by: Dmitry Gozman Signed-off-by: Max Schmitt --- docs/src/api/class-locatorassertions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 145b807670dbc..094065336371a 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -1084,7 +1084,7 @@ 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 values. Each element's class attribute is matched against the corresponding string or regular expression in the array: +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
@@ -1562,7 +1562,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 [`method: LocatorAssertions.toContainClass`]. +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** From 0eb649ba80968b8790d4e6dc18bb47df916a981f Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 9 Apr 2025 11:28:03 +0200 Subject: [PATCH 3/5] review feedback --- docs/src/api/class-locatorassertions.md | 34 +++++++++----------- packages/injected/src/injectedScript.ts | 2 +- packages/playwright/src/matchers/matchers.ts | 6 +++- packages/playwright/types/test.d.ts | 19 +++++------ tests/page/expect-misc.spec.ts | 7 ++-- 5 files changed, 33 insertions(+), 35 deletions(-) diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 094065336371a..75a18f3c654fe 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -205,7 +205,7 @@ The opposite of [`method: LocatorAssertions.toContainClass`]. ### param: LocatorAssertions.NotToContainClass.expected * since: v1.52 -- `expected` <[string]|[RegExp]|[Array]<[string]>|[Array]<[RegExp]>|[Array]<[string]|[RegExp]>> +- `expected` <[string]|[Array]<[string]>> Expected class or RegExp or a list of those. @@ -1088,44 +1088,44 @@ When an array is passed, the method asserts that the list of elements located ma ```html
-
-
-
+
+
+
``` ```js const locator = page.locator('list > .component'); -await expect(locator).toContainClass(['component', 'component selected', 'component']); +await expect(locator).toContainClass(['inactive', 'active', 'inactive']); ``` ```java -assertThat(page.locator("list > .component")).hasClass(new String[] {"component", "component selected", "component"}); +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_have_class(["component", "component selected", "component"]) +await expect(locator).to_contain_class(["inactive", "active", "inactive"]) ``` ```python sync from playwright.sync_api import expect locator = page.locator("list > .component") -expect(locator).to_have_class(["component", "component selected", "component"]) +await expect(locator).to_contain_class(["inactive", "active", "inactive"]) ``` ```csharp var locator = Page.Locator("list > .component"); -await Expect(locator).ToContainClassAsync(new string[]{"component", "component selected", "component"}); +await Expect(locator).ToContainClassAsync(new string[]{"inactive", "active", "inactive"}); ``` ### param: LocatorAssertions.toContainClass.expected * since: v1.52 * langs: js -- `expected` <[string]|[RegExp]|[Array]<[string]|[RegExp]>> +- `expected` <[string]|[Array]<[string]>> Expected class or RegExp or a list of those. @@ -1573,14 +1573,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 @@ -1588,7 +1586,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 @@ -1596,15 +1594,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: diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index 0315e54b562a0..0e5d07bcf2da8 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -1644,7 +1644,7 @@ class ExpectedTextMatcher { matchesClassList(injectedScript: InjectedScript, classList: DOMTokenList, partial: boolean): boolean { if (partial) { if (this._regex) - return !!this._regex.test(classList.toString()); + throw injectedScript.createStacklessError('Partial matching does not support regular expressions. Please provide a string value.'); return this._string!.split(/\s+/g).filter(Boolean).every(className => classList.contains(className)); } return this.matches(classList.toString()); diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index 9efad90030a6a..342b559fa1498 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -270,15 +270,19 @@ export function toHaveClass( export function toContainClass( this: ExpectMatcherState, locator: LocatorEx, - expected: string | RegExp | (string | RegExp)[], + 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 }); diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 725a0fe6148b0..7e5b1a2f04a9c 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -8219,26 +8219,25 @@ interface LocatorAssertions { * ``` * * 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: + * 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(['component', 'component selected', 'component']); + * await expect(locator).toContainClass(['inactive', 'active', 'inactive']); * ``` * * @param expected Expected class or RegExp or a list of those. * @param options */ - toContainClass(expected: string|RegExp|ReadonlyArray, options?: { + toContainClass(expected: string|ReadonlyArray, options?: { /** * Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`. */ @@ -8452,8 +8451,7 @@ 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 + * 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** @@ -8465,8 +8463,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 diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts index 033a4690ae1cd..290d9f202c5c1 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -231,6 +231,7 @@ test.describe('toContainClass', () => { 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('pass with SVGs', async ({ page }) => { @@ -248,8 +249,8 @@ test.describe('toContainClass', () => { test('pass with array', async ({ page }) => { await page.setContent('
'); const locator = page.locator('div'); - await expect(locator).toContainClass(['foo', 'hello', /[a-z]az/]); - await expect(locator).toContainClass([/foo/, /hello/, /[a-z]az/]); + 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 }); @@ -257,7 +258,7 @@ test.describe('toContainClass', () => { test('fail with array', async ({ page }) => { await page.setContent('
'); const locator = page.locator('div'); - const error = await expect(locator).toContainClass(['foo', 'bar', /not-matching/], { timeout: 1000 }).catch(e => e); + const error = await expect(locator).toContainClass(['foo', 'bar', 'baz'], { timeout: 1000 }).catch(e => e); expect(error.message).toContain('expect.toContainClass with timeout 1000ms'); }); }); From 892203a698ebcb295e79fc5eaec711c4a71a168c Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 9 Apr 2025 13:00:33 +0200 Subject: [PATCH 4/5] Update docs/src/api/class-locatorassertions.md Co-authored-by: Dmitry Gozman Signed-off-by: Max Schmitt --- docs/src/api/class-locatorassertions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 75a18f3c654fe..5877102629163 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -1127,7 +1127,7 @@ await Expect(locator).ToContainClassAsync(new string[]{"inactive", "active", "in * langs: js - `expected` <[string]|[Array]<[string]>> -Expected class or RegExp or a list of those. +A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements. ### param: LocatorAssertions.toContainClass.expected * since: v1.52 From 3997250b9f7294ffa0332a63cc3d5a89208d704b Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 9 Apr 2025 13:01:59 +0200 Subject: [PATCH 5/5] nits --- docs/src/api/class-locatorassertions.md | 15 --------------- packages/playwright/types/test.d.ts | 3 ++- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 5877102629163..78ed036c0fab3 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -1124,25 +1124,10 @@ await Expect(locator).ToContainClassAsync(new string[]{"inactive", "active", "in ### param: LocatorAssertions.toContainClass.expected * since: v1.52 -* langs: js - `expected` <[string]|[Array]<[string]>> A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements. -### param: LocatorAssertions.toContainClass.expected -* since: v1.52 -* langs: python -- `expected` <[string]|[RegExp]|[Array]<[string]>|[Array]<[RegExp]>|[Array]<[string]|[RegExp]>> - -Expected class or RegExp or a list of those. - -### param: LocatorAssertions.toContainClass.expected -* since: v1.52 -* langs: java, csharp -- `expected` <[string]|[RegExp]|[Array]<[string]>|[Array]<[RegExp]>> - -Expected class or RegExp or a list of those. - ### option: LocatorAssertions.toContainClass.timeout = %%-js-assertions-timeout-%% * since: v1.52 diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 7e5b1a2f04a9c..37fd01e81b539 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -8234,7 +8234,8 @@ interface LocatorAssertions { * await expect(locator).toContainClass(['inactive', 'active', 'inactive']); * ``` * - * @param expected Expected class or RegExp or a list of those. + * @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, options?: {