From 56822466c220ced68af2d8a1f6fc0990b0de0810 Mon Sep 17 00:00:00 2001 From: JustasMonkev Date: Tue, 7 Apr 2026 20:15:28 +0300 Subject: [PATCH 1/3] feat(expect): support pseudo-element in toHaveCSS --- docs/src/api/class-locatorassertions.md | 6 ++++++ packages/injected/src/injectedScript.ts | 2 +- packages/playwright-core/src/protocol/validator.ts | 1 + packages/playwright/src/matchers/matchers.ts | 6 +++--- packages/playwright/types/test.d.ts | 5 +++++ packages/protocol/src/channels.d.ts | 2 ++ packages/protocol/src/protocol.yml | 2 +- tests/page/expect-misc.spec.ts | 14 ++++++++++++++ 8 files changed, 33 insertions(+), 5 deletions(-) diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 2ab9ba50ee16a..0d3c07e97a2eb 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -1747,6 +1747,12 @@ CSS property name. CSS property value. +### option: LocatorAssertions.toHaveCSS.pseudoElement +* since: v1.60 +- `pseudoElement` <[string]> + +Pseudo-element to read computed styles from, for example `'::before'` or `'::after'`. + ### option: LocatorAssertions.toHaveCSS.timeout = %%-js-assertions-timeout-%% * since: v1.18 diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index 732155168dfa7..aa3d45f8fed7c 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -1612,7 +1612,7 @@ export class InjectedScript { matches: new ExpectedTextMatcher(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); + received = this.window.getComputedStyle(element, options.pseudoElement).getPropertyValue(options.expressionArg); } else if (expression === 'to.have.id') { received = element.id; } else if (expression === 'to.have.text') { diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index a0174127d8215..92385561e5825 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -2016,6 +2016,7 @@ scheme.FrameExpectParams = tObject({ selector: tOptional(tString), expression: tString, expressionArg: tOptional(tAny), + pseudoElement: tOptional(tString), expectedText: tOptional(tArray(tType('ExpectedTextValue'))), expectedNumber: tOptional(tFloat), expectedValue: tOptional(tType('SerializedArgument')), diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index 49ee53986556f..85f7be71663e9 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -315,17 +315,17 @@ export function toHaveCount( }, expected, options); } -export function toHaveCSS(this: ExpectMatcherStateInternal, locator: LocatorEx, name: string, expected: string | RegExp, options?: { timeout?: number }): Promise>; +export function toHaveCSS(this: ExpectMatcherStateInternal, locator: LocatorEx, name: string, expected: string | RegExp, options?: { timeout?: number, pseudoElement?: string }): Promise>; export function toHaveCSS( this: ExpectMatcherStateInternal, locator: LocatorEx, name: string, expected: string | RegExp, - options?: { timeout?: number }, + options?: { timeout?: number, pseudoElement?: string }, ) { return toMatchText.call(this, 'toHaveCSS', locator, 'Locator', async (isNot, timeout) => { const expectedText = serializeExpectedTextValues([expected]); - return await locator._expect('to.have.css', { expressionArg: name, expectedText, isNot, timeout }); + return await locator._expect('to.have.css', { expressionArg: name, expectedText, isNot, pseudoElement: options?.pseudoElement, timeout }); }, expected, options); } diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 1f1650941645c..9a37b64a308dd 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -9200,6 +9200,11 @@ interface LocatorAssertions { * @param options */ toHaveCSS(name: string, value: string|RegExp, options?: { + /** + * Pseudo-element to read computed styles from, for example `'::before'` or `'::after'`. + */ + pseudoElement?: string; + /** * Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`. */ diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 323143bb1af9d..cdfb2833fde3c 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -3471,6 +3471,7 @@ export type FrameExpectParams = { selector?: string, expression: string, expressionArg?: any, + pseudoElement?: string, expectedText?: ExpectedTextValue[], expectedNumber?: number, expectedValue?: SerializedArgument, @@ -3481,6 +3482,7 @@ export type FrameExpectParams = { export type FrameExpectOptions = { selector?: string, expressionArg?: any, + pseudoElement?: string, expectedText?: ExpectedTextValue[], expectedNumber?: number, expectedValue?: SerializedArgument, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index c11dad3b8f4e1..8c82aadaf41b4 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -2951,6 +2951,7 @@ Frame: selector: string? expression: string expressionArg: json? + pseudoElement: string? expectedText: type: array? items: ExpectedTextValue @@ -4587,4 +4588,3 @@ JsonPipe: parameters: reason: string? - diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts index ccedf8c06bf9a..fcacb3101bca7 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -522,6 +522,20 @@ test.describe('toHaveCSS', () => { await expect(locator).toHaveCSS('color', 'rgb(255, 0, 0)'); }); + test('should support pseudo element', async ({ page }) => { + await page.setContent(` + +
+ `); + const locator = page.locator('#node'); + await expect(locator).toHaveCSS('color', 'rgb(255, 0, 0)', { pseudoElement: '::before' }); + }); + test('custom css properties', async ({ page }) => { await page.setContent('
Text content
'); const locator = page.locator('#node'); From b99db1e0e1c39bac745b82ac6d7e41dea9df3aff Mon Sep 17 00:00:00 2001 From: JustasMonkev Date: Tue, 7 Apr 2026 22:24:19 +0300 Subject: [PATCH 2/3] feat(expect): support pseudo-element in toHaveCSS --- docs/src/api/class-locatorassertions.md | 6 +++--- packages/injected/src/injectedScript.ts | 2 +- packages/playwright-core/src/protocol/validator.ts | 2 +- packages/playwright/src/matchers/matchers.ts | 6 +++--- packages/playwright/types/test.d.ts | 5 ++--- packages/protocol/src/channels.d.ts | 5 ++--- packages/protocol/src/protocol.yml | 7 +++++-- tests/page/expect-misc.spec.ts | 2 +- 8 files changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 0d3c07e97a2eb..64a4bd758d962 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -1747,11 +1747,11 @@ CSS property name. CSS property value. -### option: LocatorAssertions.toHaveCSS.pseudoElement +### option: LocatorAssertions.toHaveCSS.pseudo * since: v1.60 -- `pseudoElement` <[string]> +- `pseudo` <"before"|"after"> -Pseudo-element to read computed styles from, for example `'::before'` or `'::after'`. +Pseudo-element to read computed styles from. ### option: LocatorAssertions.toHaveCSS.timeout = %%-js-assertions-timeout-%% * since: v1.18 diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index aa3d45f8fed7c..9f28be81af056 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -1612,7 +1612,7 @@ export class InjectedScript { matches: new ExpectedTextMatcher(options.expectedText[0]).matchesClassList(this, element.classList, /* partial */ expression === 'to.contain.class'), }; } else if (expression === 'to.have.css') { - received = this.window.getComputedStyle(element, options.pseudoElement).getPropertyValue(options.expressionArg); + received = this.window.getComputedStyle(element, options.pseudo ? `::${options.pseudo}` : undefined).getPropertyValue(options.expressionArg); } else if (expression === 'to.have.id') { received = element.id; } else if (expression === 'to.have.text') { diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 92385561e5825..c1e0b58f2d62f 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -2016,7 +2016,7 @@ scheme.FrameExpectParams = tObject({ selector: tOptional(tString), expression: tString, expressionArg: tOptional(tAny), - pseudoElement: tOptional(tString), + pseudo: tOptional(tEnum(['before', 'after'])), expectedText: tOptional(tArray(tType('ExpectedTextValue'))), expectedNumber: tOptional(tFloat), expectedValue: tOptional(tType('SerializedArgument')), diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index 85f7be71663e9..b9f0b2beb27e5 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -315,17 +315,17 @@ export function toHaveCount( }, expected, options); } -export function toHaveCSS(this: ExpectMatcherStateInternal, locator: LocatorEx, name: string, expected: string | RegExp, options?: { timeout?: number, pseudoElement?: string }): Promise>; +export function toHaveCSS(this: ExpectMatcherStateInternal, locator: LocatorEx, name: string, expected: string | RegExp, options?: { timeout?: number, pseudo?: 'before' | 'after' }): Promise>; export function toHaveCSS( this: ExpectMatcherStateInternal, locator: LocatorEx, name: string, expected: string | RegExp, - options?: { timeout?: number, pseudoElement?: string }, + options?: { timeout?: number, pseudo?: 'before' | 'after' }, ) { return toMatchText.call(this, 'toHaveCSS', locator, 'Locator', async (isNot, timeout) => { const expectedText = serializeExpectedTextValues([expected]); - return await locator._expect('to.have.css', { expressionArg: name, expectedText, isNot, pseudoElement: options?.pseudoElement, timeout }); + return await locator._expect('to.have.css', { expressionArg: name, expectedText, isNot, pseudo: options?.pseudo, timeout }); }, expected, options); } diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 9a37b64a308dd..972f728a14b7a 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -9201,9 +9201,9 @@ interface LocatorAssertions { */ toHaveCSS(name: string, value: string|RegExp, options?: { /** - * Pseudo-element to read computed styles from, for example `'::before'` or `'::after'`. + * Pseudo-element to read computed styles from. */ - pseudoElement?: string; + pseudo?: "before"|"after"; /** * Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`. @@ -10375,4 +10375,3 @@ interface TestConfigWebServer { */ url?: string; } - diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index cdfb2833fde3c..5dbade79576a6 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -3471,7 +3471,7 @@ export type FrameExpectParams = { selector?: string, expression: string, expressionArg?: any, - pseudoElement?: string, + pseudo?: 'before' | 'after', expectedText?: ExpectedTextValue[], expectedNumber?: number, expectedValue?: SerializedArgument, @@ -3482,7 +3482,7 @@ export type FrameExpectParams = { export type FrameExpectOptions = { selector?: string, expressionArg?: any, - pseudoElement?: string, + pseudo?: 'before' | 'after', expectedText?: ExpectedTextValue[], expectedNumber?: number, expectedValue?: SerializedArgument, @@ -5375,4 +5375,3 @@ export interface JsonPipeEvents { 'message': JsonPipeMessageEvent; 'closed': JsonPipeClosedEvent; } - diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 8c82aadaf41b4..e81d966c8d24a 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -2951,7 +2951,11 @@ Frame: selector: string? expression: string expressionArg: json? - pseudoElement: string? + pseudo: + type: enum? + literals: + - before + - after expectedText: type: array? items: ExpectedTextValue @@ -4587,4 +4591,3 @@ JsonPipe: closed: parameters: reason: string? - diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts index fcacb3101bca7..9ae86bbee5032 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -533,7 +533,7 @@ test.describe('toHaveCSS', () => {
`); const locator = page.locator('#node'); - await expect(locator).toHaveCSS('color', 'rgb(255, 0, 0)', { pseudoElement: '::before' }); + await expect(locator).toHaveCSS('color', 'rgb(255, 0, 0)', { pseudo: 'before' }); }); test('custom css properties', async ({ page }) => { From 4e6e844c181d350c909824fe8a7da0840ac423d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 16:34:49 +0000 Subject: [PATCH 3/3] fix(assertions): use named type for pseudo option in toHaveCSS docs The string literal union <"before"|"after"> requires a named type wrapper for the doc parser. Changed to <[PseudoElement]<"before"|"after">>. --- docs/src/api/class-locatorassertions.md | 2 +- packages/playwright/types/test.d.ts | 1 + packages/protocol/src/channels.d.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 64a4bd758d962..dcfafda26a7fd 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -1749,7 +1749,7 @@ CSS property value. ### option: LocatorAssertions.toHaveCSS.pseudo * since: v1.60 -- `pseudo` <"before"|"after"> +- `pseudo` <[PseudoElement]<"before"|"after">> Pseudo-element to read computed styles from. diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 972f728a14b7a..601900e9388e6 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -10375,3 +10375,4 @@ interface TestConfigWebServer { */ url?: string; } + diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 5dbade79576a6..24670f2dcbd0a 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -5375,3 +5375,4 @@ export interface JsonPipeEvents { 'message': JsonPipeMessageEvent; 'closed': JsonPipeClosedEvent; } +