Skip to content

Commit adc3ded

Browse files
committed
chore: add toHaveClass partial option
1 parent 36c55d8 commit adc3ded

File tree

5 files changed

+43
-8
lines changed

5 files changed

+43
-8
lines changed

docs/src/api/class-locatorassertions.md

+6
Original file line numberDiff line numberDiff line change
@@ -1523,6 +1523,12 @@ Expected class or RegExp or a list of those.
15231523

15241524
Expected class or RegExp or a list of those.
15251525

1526+
### option: LocatorAssertions.toHaveClass.partial
1527+
* since: v1.52
1528+
- `partial` <[boolean]>
1529+
1530+
Weather to allow matching a partial class name. Defaults to `false`. By default `toHaveClass` will match the entire `className` attribute.
1531+
15261532
### option: LocatorAssertions.toHaveClass.timeout = %%-js-assertions-timeout-%%
15271533
* since: v1.18
15281534

packages/playwright-core/src/server/injected/injectedScript.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -1391,14 +1391,14 @@ export class InjectedScript {
13911391

13921392
{
13931393
// Single text value.
1394-
let received: string | undefined;
1394+
let received: string | string[] | undefined;
13951395
if (expression === 'to.have.attribute.value') {
13961396
const value = element.getAttribute(options.expressionArg);
13971397
if (value === null)
13981398
return { received: null, matches: false };
13991399
received = value;
14001400
} else if (expression === 'to.have.class') {
1401-
received = element.classList.toString();
1401+
received = options.expressionArg.partial ? [...element.classList] : element.classList.toString();
14021402
} else if (expression === 'to.have.css') {
14031403
received = this.window.getComputedStyle(element).getPropertyValue(options.expressionArg);
14041404
} else if (expression === 'to.have.id') {
@@ -1443,11 +1443,11 @@ export class InjectedScript {
14431443
}
14441444

14451445
// List of values.
1446-
let received: string[] | undefined;
1446+
let received: (string | string[])[] | undefined;
14471447
if (expression === 'to.have.text.array' || expression === 'to.contain.text.array')
14481448
received = elements.map(e => options.useInnerText ? (e as HTMLElement).innerText : elementText(new Map(), e).full);
14491449
else if (expression === 'to.have.class.array')
1450-
received = elements.map(e => e.classList.toString());
1450+
received = elements.map(e => options.expressionArg.partial ? [...e.classList] : e.classList.toString());
14511451

14521452
if (received && options.expectedText) {
14531453
// "To match an array" is "to contain an array" + "equal length"
@@ -1611,7 +1611,9 @@ class ExpectedTextMatcher {
16111611
}
16121612
}
16131613

1614-
matches(text: string): boolean {
1614+
matches(text: string | string[]): boolean {
1615+
if (Array.isArray(text))
1616+
return text.some(t => this.matches(t));
16151617
if (!this._regex)
16161618
text = this.normalize(text)!;
16171619
if (this._string !== undefined)

packages/playwright/src/matchers/matchers.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -252,17 +252,18 @@ export function toHaveClass(
252252
this: ExpectMatcherState,
253253
locator: LocatorEx,
254254
expected: string | RegExp | (string | RegExp)[],
255-
options?: { timeout?: number },
255+
options?: { timeout?: number, partial: boolean },
256256
) {
257+
const partial = options?.partial;
257258
if (Array.isArray(expected)) {
258259
return toEqual.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => {
259260
const expectedText = serializeExpectedTextValues(expected);
260-
return await locator._expect('to.have.class.array', { expectedText, isNot, timeout });
261+
return await locator._expect('to.have.class.array', { expectedText, expressionArg: { partial }, isNot, timeout });
261262
}, expected, options);
262263
} else {
263264
return toMatchText.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => {
264265
const expectedText = serializeExpectedTextValues([expected]);
265-
return await locator._expect('to.have.class', { expectedText, isNot, timeout });
266+
return await locator._expect('to.have.class', { expectedText, expressionArg: { partial }, isNot, timeout });
266267
}, expected, options);
267268
}
268269
}

packages/playwright/types/test.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -8424,6 +8424,12 @@ interface LocatorAssertions {
84248424
* @param options
84258425
*/
84268426
toHaveClass(expected: string|RegExp|ReadonlyArray<string|RegExp>, options?: {
8427+
/**
8428+
* Weather to allow matching a partial class name. Defaults to `false`. By default `toHaveClass` will match the entire
8429+
* `className` attribute.
8430+
*/
8431+
partial?: boolean;
8432+
84278433
/**
84288434
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
84298435
*/

tests/page/expect-misc.spec.ts

+20
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,26 @@ test.describe('toHaveClass', () => {
220220
const error = await expect(locator).toHaveClass(['foo', 'bar', /[a-z]az/], { timeout: 1000 }).catch(e => e);
221221
expect(error.message).toContain('expect.toHaveClass with timeout 1000ms');
222222
});
223+
224+
test('allow matching partial class names', async ({ page }) => {
225+
await page.setContent('<div class="foo bar baz"></div>');
226+
const locator = page.locator('div');
227+
await expect(locator).toHaveClass('foo', { partial: true });
228+
await expect(locator).toHaveClass(/f.o/, { partial: true });
229+
await expect(locator).not.toHaveClass('foo');
230+
await expect(locator).not.toHaveClass('foo', { partial: false });
231+
});
232+
233+
test('allow matching partial class names with array', async ({ page }) => {
234+
await page.setContent('<div class="foo f2oo"></div><div class="bar b2ar"></div><div class="bar b2ar"></div>');
235+
const locator = page.locator('div');
236+
await expect(locator).toHaveClass(['foo', 'b2ar', 'b2ar'], { partial: true });
237+
await expect(locator).not.toHaveClass(['foo', 'b2ar', 'b2ar']);
238+
await expect(locator).not.toHaveClass(['foo', 'b2ar', /b2?ar/]); // partial allows matching multiple classes
239+
await expect(locator).not.toHaveClass(['foo', 'b2ar', 'b2ar'], { partial: false });
240+
await expect(locator).not.toHaveClass(['foo', 'b2ar'], { partial: true });
241+
await expect(locator).not.toHaveClass(['foo', 'b2ar'], { partial: false });
242+
});
223243
});
224244

225245
test.describe('toHaveTitle', () => {

0 commit comments

Comments
 (0)