mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(matchers): add toContainClass (#15491)
This commit is contained in:
parent
008a85b143
commit
e4debd0bf6
@ -721,6 +721,99 @@ await Expect(locator).ToBeVisibleAsync();
|
||||
### option: LocatorAssertions.toBeVisible.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
* since: v1.18
|
||||
|
||||
## async method: LocatorAssertions.toContainClass
|
||||
* since: v1.24
|
||||
* langs:
|
||||
- alias-java: containsClass
|
||||
|
||||
Ensures the [Locator] points to an element that contains the given CSS class (or multiple).
|
||||
In contrast to [`method: LocatorAssertions.toHaveClass`] which requires that the [Locator] has exactly the provided classes, `toContainClass` verifies that the [Locator] has a subset (or all) of the given CSS classes.
|
||||
|
||||
```html
|
||||
<div class='foo bar baz' id='component'>
|
||||
<div class='item alice'></div>
|
||||
<div class='item bob'></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
const locator = page.locator('#component');
|
||||
await expect(locator).toContainClass('bar baz'); // pass, both classes are on element
|
||||
await expect(locator).toContainClass('ba'); // fail, element has no 'ba' class
|
||||
|
||||
const itemLocator = page.locator('#component .item');
|
||||
await expect(itemLocator).toContainClass(['alice', 'bob']); // pass, first element has alice, second bob
|
||||
await expect(itemLocator).toContainClass(['alice', 'bob carl']); // no carl class found on second item element
|
||||
await expect(itemLocator).toContainClass(['alice', 'bob', 'foobar']); // we expect 3 elements with the item class, but there are only 2
|
||||
```
|
||||
|
||||
```java
|
||||
Locator locator = page.locator("#component");
|
||||
assertThat(locator).containsClass("bar baz"); // pass, both classes are on element
|
||||
assertThat(locator).containsClass("ba"); // fail, element has no 'ba' class
|
||||
|
||||
Locator itemLocator = page.locator("#component .item");
|
||||
assertThat(itemLocator).toContainClass(new String[] {"alice", "bob"}); // pass, first element has alice, second bob
|
||||
assertThat(itemLocator).toContainClass(new String[] {"alice", "bob carl"}); // no carl class found on second item element
|
||||
assertThat(itemLocator).toContainClass(new String[] {"alice", "bob", "foobar"}); // we expect 3 elements with the item class, but there are only 2
|
||||
```
|
||||
|
||||
```python async
|
||||
from playwright.async_api import expect
|
||||
|
||||
locator = page.locator('#component')
|
||||
expect(locator).to_contain_class('bar baz') # pass, both classes are on element
|
||||
expect(locator).to_contain_class('ba') # fail, element has no 'ba' class
|
||||
|
||||
item_locator = page.locator('#component .item')
|
||||
expect(item_locator).to_contain_class(['alice', 'bob']) # pass, first element has alice, second bob
|
||||
expect(item_locator).to_contain_class(['alice', 'bob carl']) # no carl class found on second item element
|
||||
expect(item_locator).to_contain_class(['alice', 'bob', 'foobar']) # we expect 3 elements with the item class, but there are only 2
|
||||
```
|
||||
|
||||
```python sync
|
||||
from playwright.sync_api import expect
|
||||
|
||||
locator = page.locator('#component')
|
||||
await expect(locator).to_contain_class('bar baz') # pass, both classes are on element
|
||||
await expect(locator).to_contain_class('ba') # fail, element has no 'ba' class
|
||||
|
||||
item_locator = page.locator('#component .item')
|
||||
await expect(item_locator).to_contain_class(['alice', 'bob']) # pass, first element has alice, second bob
|
||||
await expect(item_locator).to_contain_class(['alice', 'bob carl']) # no carl class found on second item element
|
||||
await expect(item_locator).to_contain_class(['alice', 'bob', 'foobar']) # we expect 3 elements with the item class, but there are only 2
|
||||
```
|
||||
|
||||
```csharp
|
||||
var locator = Page.Locator("#component");
|
||||
await Expect(locator).ToContainClassAsync("bar baz"); // pass, both classes are on element
|
||||
await Expect(locator).ToContainClassAsync("ba"); // fail, element has no "ba" class
|
||||
|
||||
var itemLocator = page.locator("#component .item");
|
||||
await Expect(itemLocator).ToContainClassAsync(new string[]{"alice", "bob"}); // pass, first element has alice, second bob
|
||||
await Expect(itemLocator).ToContainClassAsync(new string[]{"alice", "bob carl"}); // no carl class found on second item element
|
||||
await Expect(itemLocator).ToContainClassAsync(new string[]{"alice", "bob", "foobar"}); // we expect 3 elements with the item class, but there are only 2
|
||||
```
|
||||
|
||||
Note that locator must point to a single element when passing a string or to multiple elements when passing an array.
|
||||
|
||||
### param: LocatorAssertions.toContainClass.expected
|
||||
* since: v1.24
|
||||
- `expected` <[string]|[Array]<[string]>>
|
||||
|
||||
Expected classnames, whitespace separated. When passing an array, the given classes must be present on the locator elements.
|
||||
|
||||
### option: LocatorAssertions.toContainClass.ignoreCase
|
||||
* since: v1.24
|
||||
- `ignoreCase` <[boolean]>
|
||||
|
||||
Whether to perform case-insensitive match.
|
||||
|
||||
### option: LocatorAssertions.toContainClass.timeout = %%-js-assertions-timeout-%%
|
||||
* since: v1.24
|
||||
### option: LocatorAssertions.toContainClass.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
* since: v1.24
|
||||
|
||||
## async method: LocatorAssertions.toContainText
|
||||
* since: v1.20
|
||||
* langs:
|
||||
@ -883,15 +976,22 @@ Expected attribute value.
|
||||
* langs:
|
||||
- alias-java: hasClass
|
||||
|
||||
Ensures the [Locator] points to an element with given CSS class.
|
||||
Ensures the [Locator] points to an element with given CSS classes. This needs to be a full match
|
||||
or using a relaxed regular expression. For matching partial class names, use [`method: LocatorAssertions.toContainClass`].
|
||||
|
||||
```html
|
||||
<div class='selected row' id='component'></div>
|
||||
```
|
||||
|
||||
```js
|
||||
const locator = page.locator('#component');
|
||||
await expect(locator).toHaveClass(/selected/);
|
||||
await expect(locator).toHaveClass('selected row');
|
||||
```
|
||||
|
||||
```java
|
||||
assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
|
||||
assertThat(page.locator("#component")).hasClass("selected row");
|
||||
```
|
||||
|
||||
```python async
|
||||
@ -899,6 +999,7 @@ from playwright.async_api import expect
|
||||
|
||||
locator = page.locator("#component")
|
||||
await expect(locator).to_have_class(re.compile(r"selected"))
|
||||
await expect(locator).to_have_class("selected row")
|
||||
```
|
||||
|
||||
```python sync
|
||||
@ -906,11 +1007,13 @@ from playwright.sync_api import expect
|
||||
|
||||
locator = page.locator("#component")
|
||||
expect(locator).to_have_class(re.compile(r"selected"))
|
||||
expect(locator).to_have_class("selected row")
|
||||
```
|
||||
|
||||
```csharp
|
||||
var locator = Page.Locator("#component");
|
||||
await Expect(locator).ToHaveClassAsync(new Regex("selected"));
|
||||
await Expect(locator).ToHaveClassAsync("selected row");
|
||||
```
|
||||
|
||||
Note that if array is passed as an expected value, entire lists of elements can be asserted:
|
||||
|
||||
@ -79,7 +79,7 @@ export class InjectedScript {
|
||||
private _highlight: Highlight | undefined;
|
||||
readonly isUnderTest: boolean;
|
||||
|
||||
constructor(isUnderTest: boolean, stableRafCount: number, browserName: string, customEngines: { name: string, engine: SelectorEngine}[]) {
|
||||
constructor(isUnderTest: boolean, stableRafCount: number, browserName: string, customEngines: { name: string, engine: SelectorEngine }[]) {
|
||||
this.isUnderTest = isUnderTest;
|
||||
this._evaluator = new SelectorEvaluatorImpl(new Map());
|
||||
|
||||
@ -445,7 +445,7 @@ export class InjectedScript {
|
||||
element = element.closest('button, [role=button], [role=checkbox], [role=radio]') || element;
|
||||
if (behavior === 'follow-label') {
|
||||
if (!element.matches('input, textarea, button, select, [role=button], [role=checkbox], [role=radio]') &&
|
||||
!(element as any).isContentEditable) {
|
||||
!(element as any).isContentEditable) {
|
||||
// Go up to the label that might be connected to the input/textarea.
|
||||
element = element.closest('label') || element;
|
||||
}
|
||||
@ -1070,7 +1070,7 @@ export class InjectedScript {
|
||||
let received: string | undefined;
|
||||
if (expression === 'to.have.attribute') {
|
||||
received = element.getAttribute(options.expressionArg) || '';
|
||||
} else if (expression === 'to.have.class') {
|
||||
} else if (expression === 'to.have.class' || expression === 'to.contain.class') {
|
||||
received = element.classList.toString();
|
||||
} else if (expression === 'to.have.css') {
|
||||
received = window.getComputedStyle(element).getPropertyValue(options.expressionArg);
|
||||
@ -1091,7 +1091,9 @@ export class InjectedScript {
|
||||
|
||||
if (received !== undefined && options.expectedText) {
|
||||
const matcher = new ExpectedTextMatcher(options.expectedText[0]);
|
||||
return { received, matches: matcher.matches(received) };
|
||||
return { received, matches: matcher.matches(received, {
|
||||
toContainClass: expression === 'to.contain.class',
|
||||
}) };
|
||||
}
|
||||
}
|
||||
|
||||
@ -1111,7 +1113,7 @@ export class InjectedScript {
|
||||
let received: string[] | undefined;
|
||||
if (expression === 'to.have.text.array' || expression === 'to.contain.text.array')
|
||||
received = elements.map(e => options.useInnerText ? (e as HTMLElement).innerText : e.textContent || '');
|
||||
else if (expression === 'to.have.class.array')
|
||||
else if (expression === 'to.have.class.array' || expression === 'to.contain.class.array')
|
||||
received = elements.map(e => e.classList.toString());
|
||||
|
||||
if (received && options.expectedText) {
|
||||
@ -1125,7 +1127,9 @@ export class InjectedScript {
|
||||
const matchers = options.expectedText.map(e => new ExpectedTextMatcher(e));
|
||||
let mIndex = 0, rIndex = 0;
|
||||
while (mIndex < matchers.length && rIndex < received.length) {
|
||||
if (matchers[mIndex].matches(received[rIndex]))
|
||||
if (matchers[mIndex].matches(received[rIndex], {
|
||||
toContainClass: expression === 'to.contain.class.array',
|
||||
}))
|
||||
++mIndex;
|
||||
++rIndex;
|
||||
}
|
||||
@ -1151,11 +1155,11 @@ function oneLine(s: string): string {
|
||||
return s.replace(/\n/g, '↵').replace(/\t/g, '⇆');
|
||||
}
|
||||
|
||||
const eventType = new Map<string, 'mouse'|'keyboard'|'touch'|'pointer'|'focus'|'drag'>([
|
||||
const eventType = new Map<string, 'mouse' | 'keyboard' | 'touch' | 'pointer' | 'focus' | 'drag'>([
|
||||
['auxclick', 'mouse'],
|
||||
['click', 'mouse'],
|
||||
['dblclick', 'mouse'],
|
||||
['mousedown','mouse'],
|
||||
['mousedown', 'mouse'],
|
||||
['mouseeenter', 'mouse'],
|
||||
['mouseleave', 'mouse'],
|
||||
['mousemove', 'mouse'],
|
||||
@ -1258,7 +1262,9 @@ class ExpectedTextMatcher {
|
||||
}
|
||||
}
|
||||
|
||||
matches(text: string): boolean {
|
||||
matches(text: string, { toContainClass }: { toContainClass?: boolean } = {}): boolean {
|
||||
if (toContainClass)
|
||||
return this.matchesClassList(text);
|
||||
if (!this._regex)
|
||||
text = this.normalize(text)!;
|
||||
if (this._string !== undefined)
|
||||
@ -1270,6 +1276,18 @@ class ExpectedTextMatcher {
|
||||
return false;
|
||||
}
|
||||
|
||||
private matchesClassList(received: string): boolean {
|
||||
const expected = this.normalizeClassList(this._string || '');
|
||||
if (expected.length === 0)
|
||||
return false;
|
||||
const normalizedReceived = this.normalizeClassList(received);
|
||||
return expected.every(classListEntry => normalizedReceived.includes(classListEntry));
|
||||
}
|
||||
|
||||
private normalizeClassList(classList: string): string[] {
|
||||
return classList.trim().split(/\s+/g).map(c => this.normalize(c)).filter(c => c) as string[];
|
||||
}
|
||||
|
||||
private normalize(s: string | undefined): string | undefined {
|
||||
if (!s)
|
||||
return s;
|
||||
|
||||
@ -29,6 +29,7 @@ import {
|
||||
toContainText,
|
||||
toHaveAttribute,
|
||||
toHaveClass,
|
||||
toContainClass,
|
||||
toHaveCount,
|
||||
toHaveCSS,
|
||||
toHaveId,
|
||||
@ -134,6 +135,7 @@ const customMatchers = {
|
||||
toContainText,
|
||||
toHaveAttribute,
|
||||
toHaveClass,
|
||||
toContainClass,
|
||||
toHaveCount,
|
||||
toHaveCSS,
|
||||
toHaveId,
|
||||
|
||||
@ -164,6 +164,25 @@ export function toHaveClass(
|
||||
}
|
||||
}
|
||||
|
||||
export function toContainClass(
|
||||
this: ReturnType<Expect['getState']>,
|
||||
locator: LocatorEx,
|
||||
expected: string | string[],
|
||||
options?: { timeout?: number, ignoreCase?: boolean },
|
||||
) {
|
||||
if (Array.isArray(expected)) {
|
||||
return toEqual.call(this, 'toContainClass', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
const expectedText = toExpectedTextValues(expected, { ignoreCase: options?.ignoreCase });
|
||||
return await locator._expect(customStackTrace, 'to.contain.class.array', { expectedText, isNot, timeout });
|
||||
}, expected, options);
|
||||
} else {
|
||||
return toMatchText.call(this, 'toContainClass', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
const expectedText = toExpectedTextValues([expected], { ignoreCase: options?.ignoreCase });
|
||||
return await locator._expect(customStackTrace, 'to.contain.class', { expectedText, isNot, timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
}
|
||||
|
||||
export function toHaveCount(
|
||||
this: ReturnType<Expect['getState']>,
|
||||
locator: LocatorEx,
|
||||
|
||||
49
packages/playwright-test/types/test.d.ts
vendored
49
packages/playwright-test/types/test.d.ts
vendored
@ -3327,6 +3327,46 @@ interface LocatorAssertions {
|
||||
timeout?: number;
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* Ensures the [Locator] points to an element that contains the given CSS class (or multiple). In contrast to
|
||||
* [locatorAssertions.toHaveClass(expected[, options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-have-class)
|
||||
* which requires that the [Locator] has exactly the provided classes, `toContainClass` verifies that the [Locator] has a
|
||||
* subset (or all) of the given CSS classes.
|
||||
*
|
||||
* ```html
|
||||
* <div class='foo bar baz' id='component'>
|
||||
* <div class='item alice'></div>
|
||||
* <div class='item bob'></div>
|
||||
* </div>
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* const locator = page.locator('#component');
|
||||
* await expect(locator).toContainClass('bar baz'); // pass, both classes are on element
|
||||
* await expect(locator).toContainClass('ba'); // fail, element has no 'ba' class
|
||||
*
|
||||
* const itemLocator = page.locator('#component .item');
|
||||
* await expect(itemLocator).toContainClass(['alice', 'bob']); // pass, first element has alice, second bob
|
||||
* await expect(itemLocator).toContainClass(['alice', 'bob carl']); // no carl class found on second item element
|
||||
* await expect(itemLocator).toContainClass(['alice', 'bob', 'foobar']); // we expect 3 elements with the item class, but there are only 2
|
||||
* ```
|
||||
*
|
||||
* Note that locator must point to a single element when passing a string or to multiple elements when passing an array.
|
||||
* @param expected Expected classnames, whitespace separated. When passing an array, the given classes must be present on the locator elements.
|
||||
* @param options
|
||||
*/
|
||||
toContainClass(expected: string|Array<string>, options?: {
|
||||
/**
|
||||
* Whether to perform case-insensitive match.
|
||||
*/
|
||||
ignoreCase?: boolean;
|
||||
|
||||
/**
|
||||
* Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`.
|
||||
*/
|
||||
timeout?: number;
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* Ensures the [Locator] points to an element that contains the given text. You can use regular expressions for the value
|
||||
* as well.
|
||||
@ -3385,11 +3425,18 @@ interface LocatorAssertions {
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* Ensures the [Locator] points to an element with given CSS class.
|
||||
* Ensures the [Locator] points to an element with given CSS classes. This needs to be a full match or using a relaxed
|
||||
* regular expression. For matching partial class names, use
|
||||
* [locatorAssertions.toContainClass(expected[, options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-contain-class).
|
||||
*
|
||||
* ```html
|
||||
* <div class='selected row' id='component'></div>
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* const locator = page.locator('#component');
|
||||
* await expect(locator).toHaveClass(/selected/);
|
||||
* await expect(locator).toHaveClass('selected row');
|
||||
* ```
|
||||
*
|
||||
* Note that if array is passed as an expected value, entire lists of elements can be asserted:
|
||||
|
||||
@ -262,6 +262,60 @@ test('should support toHaveClass w/ array', async ({ runInlineTest }) => {
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test('should support toContainClass', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
|
||||
test('pass', async ({ page }) => {
|
||||
await page.setContent(\`
|
||||
<div class="foo bar baz">
|
||||
<span class="alice item baz"></span>
|
||||
<span class="bob item baz"></span>
|
||||
</div>
|
||||
\`);
|
||||
const locator = page.locator('div');
|
||||
await expect(locator).toContainClass('foo');
|
||||
// Leading/trailing whitespace
|
||||
await expect(locator).toContainClass(' foo ');
|
||||
// empty should not pass
|
||||
await expect(locator).not.toContainClass('');
|
||||
await expect(locator).toContainClass('bar');
|
||||
await expect(locator).toContainClass('baz');
|
||||
await expect(locator).toContainClass('foo baz');
|
||||
await expect(locator).toContainClass('baz foo');
|
||||
await expect(locator).not.toContainClass('ba');
|
||||
|
||||
await expect(locator).toContainClass('BAZ FoO', { ignoreCase: true });
|
||||
await expect(locator).not.toContainClass('BAZ');
|
||||
|
||||
const locatorSpan = page.locator('div span');
|
||||
await expect(locatorSpan).toContainClass(['alice baz', 'bob']);
|
||||
await expect(locatorSpan).not.toContainClass(['alice', 'alice']);
|
||||
});
|
||||
|
||||
test('fail', async ({ page }) => {
|
||||
await page.setContent('<div class="bar baz"></div>');
|
||||
const locator = page.locator('div');
|
||||
await expect(locator).toContainClass('foo', { timeout: 1000 });
|
||||
});
|
||||
|
||||
test('fail length mismatch', async ({ page }) => {
|
||||
await page.setContent('<div><span class="alice"></span><span class="bob"></span></div>');
|
||||
const locator = page.locator('div span');
|
||||
await expect(locator).toContainClass('alice', { timeout: 1000 });
|
||||
});
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
const output = stripAnsi(result.output);
|
||||
expect(output).toContain('expect(locator).toContainClass');
|
||||
expect(output).toContain('Expected string: \"foo\"');
|
||||
expect(output).toContain('Received string: \"bar baz\"');
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.failed).toBe(2);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test('should support toHaveTitle', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user