diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index cce865be20..f4c2699fb9 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -999,6 +999,15 @@ Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. Passing `null` disables reduced motion emulation. +### option: Page.emulateMedia.forcedColors +- `forcedColors` > + +Emulates `'forced-colors'` media feature, supported values are `'active'` and `'none'`. Passing `null` disables forced colors emulation. + +:::note +It's not supported in WebKit, see [here](https://bugs.webkit.org/show_bug.cgi?id=225281) in their issue tracker. +::: + ## async method: Page.evalOnSelector * langs: - alias-python: eval_on_selector diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 47137228b6..3a1021f9cd 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -421,6 +421,16 @@ Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'` Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. See [`method: Page.emulateMedia`] for more details. Defaults to `'no-preference'`. +## context-option-forcedColors +- `forcedColors` <[ForcedColors]<"active"|"none">> + +Emulates `'forced-colors'` media feature, supported values are `'active'`, `'none'`. See [`method: Page.emulateMedia`] for more details. Defaults +to `'none'`. + +:::note +It's not supported in WebKit, see [here](https://bugs.webkit.org/show_bug.cgi?id=225281) in their issue tracker. +::: + ## context-option-logger * langs: js - `logger` <[Logger]> @@ -642,6 +652,7 @@ using the [`method: AndroidDevice.setDefaultTimeout`] method. - %%-context-option-httpcredentials-%% - %%-context-option-colorscheme-%% - %%-context-option-reducedMotion-%% +- %%-context-option-forcedColors-%% - %%-context-option-logger-%% - %%-context-option-videospath-%% - %%-context-option-videosize-%% diff --git a/src/client/page.ts b/src/client/page.ts index 2c05e90bd9..441670200d 100644 --- a/src/client/page.ts +++ b/src/client/page.ts @@ -410,12 +410,13 @@ export class Page extends ChannelOwner { await channel.emulateMedia({ media: options.media === null ? 'null' : options.media, colorScheme: options.colorScheme === null ? 'null' : options.colorScheme, reducedMotion: options.reducedMotion === null ? 'null' : options.reducedMotion, + forcedColors: options.forcedColors === null ? 'null' : options.forcedColors, }); }); } diff --git a/src/dispatchers/pageDispatcher.ts b/src/dispatchers/pageDispatcher.ts index 365c33e1f5..e78727c402 100644 --- a/src/dispatchers/pageDispatcher.ts +++ b/src/dispatchers/pageDispatcher.ts @@ -122,6 +122,7 @@ export class PageDispatcher extends Dispatcher Validator): Scheme { hasTouch: tOptional(tBoolean), colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference'])), reducedMotion: tOptional(tEnum(['reduce', 'no-preference'])), + forcedColors: tOptional(tEnum(['active', 'none'])), acceptDownloads: tOptional(tBoolean), baseURL: tOptional(tString), _debugName: tOptional(tString), @@ -325,6 +326,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { hasTouch: tOptional(tBoolean), colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference'])), reducedMotion: tOptional(tEnum(['reduce', 'no-preference'])), + forcedColors: tOptional(tEnum(['active', 'none'])), acceptDownloads: tOptional(tBoolean), baseURL: tOptional(tString), _debugName: tOptional(tString), @@ -474,6 +476,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { media: tOptional(tEnum(['screen', 'print', 'null'])), colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference', 'null'])), reducedMotion: tOptional(tEnum(['reduce', 'no-preference', 'null'])), + forcedColors: tOptional(tEnum(['active', 'none', 'null'])), }); scheme.PageExposeBindingParams = tObject({ name: tString, @@ -1264,6 +1267,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { hasTouch: tOptional(tBoolean), colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference'])), reducedMotion: tOptional(tEnum(['reduce', 'no-preference'])), + forcedColors: tOptional(tEnum(['active', 'none'])), acceptDownloads: tOptional(tBoolean), _debugName: tOptional(tString), recordVideo: tOptional(tObject({ diff --git a/src/server/chromium/crPage.ts b/src/server/chromium/crPage.ts index 454cbff2c0..1483679cca 100644 --- a/src/server/chromium/crPage.ts +++ b/src/server/chromium/crPage.ts @@ -982,9 +982,11 @@ class FrameSession { return; const colorScheme = this._page._state.colorScheme === null ? '' : this._page._state.colorScheme; const reducedMotion = this._page._state.reducedMotion === null ? '' : this._page._state.reducedMotion; + const forcedColors = this._page._state.forcedColors === null ? '' : this._page._state.forcedColors; const features = [ { name: 'prefers-color-scheme', value: colorScheme }, { name: 'prefers-reduced-motion', value: reducedMotion }, + { name: 'forced-colors', value: forcedColors }, ]; // Empty string disables the override. await this._client.send('Emulation.setEmulatedMedia', { media: this._page._state.mediaType || '', features }); diff --git a/src/server/firefox/ffBrowser.ts b/src/server/firefox/ffBrowser.ts index a8931b38ff..8fe16baa73 100644 --- a/src/server/firefox/ffBrowser.ts +++ b/src/server/firefox/ffBrowser.ts @@ -209,6 +209,10 @@ export class FFBrowserContext extends BrowserContext { browserContextId, reducedMotion: this._options.reducedMotion !== undefined ? this._options.reducedMotion : 'no-preference', })); + promises.push(this._browser._connection.send('Browser.setForcedColors', { + browserContextId, + forcedColors: this._options.forcedColors !== undefined ? this._options.forcedColors : 'none', + })); if (this._options.recordVideo) { promises.push(this._ensureVideosPath().then(() => { return this._browser._connection.send('Browser.setVideoRecordingOptions', { diff --git a/src/server/firefox/ffPage.ts b/src/server/firefox/ffPage.ts index 4f69a6adf1..5a10a84c3e 100644 --- a/src/server/firefox/ffPage.ts +++ b/src/server/firefox/ffPage.ts @@ -354,11 +354,13 @@ export class FFPage implements PageDelegate { async updateEmulateMedia(): Promise { const colorScheme = this._page._state.colorScheme === null ? undefined : this._page._state.colorScheme; const reducedMotion = this._page._state.reducedMotion === null ? undefined : this._page._state.reducedMotion; + const forcedColors = this._page._state.forcedColors === null ? undefined : this._page._state.forcedColors; await this._session.send('Page.setEmulatedMedia', { // Empty string means reset. type: this._page._state.mediaType === null ? '' : this._page._state.mediaType, colorScheme, reducedMotion, + forcedColors, }); } diff --git a/src/server/page.ts b/src/server/page.ts index 77b61ac9c4..77dd2ba92e 100644 --- a/src/server/page.ts +++ b/src/server/page.ts @@ -90,6 +90,7 @@ type PageState = { mediaType: types.MediaType | null; colorScheme: types.ColorScheme | null; reducedMotion: types.ReducedMotion | null; + forcedColors: types.ForcedColors | null; extraHTTPHeaders: types.HeadersArray | null; }; @@ -154,6 +155,7 @@ export class Page extends SdkObject { mediaType: null, colorScheme: browserContext._options.colorScheme !== undefined ? browserContext._options.colorScheme : 'light', reducedMotion: browserContext._options.reducedMotion !== undefined ? browserContext._options.reducedMotion : 'no-preference', + forcedColors: browserContext._options.forcedColors !== undefined ? browserContext._options.forcedColors : 'none', extraHTTPHeaders: null, }; this.accessibility = new accessibility.Accessibility(delegate.getAccessibilityTree.bind(delegate)); @@ -353,13 +355,15 @@ export class Page extends SdkObject { }), this._timeoutSettings.navigationTimeout(options)); } - async emulateMedia(options: { media?: types.MediaType | null, colorScheme?: types.ColorScheme | null, reducedMotion?: types.ReducedMotion | null }) { + async emulateMedia(options: { media?: types.MediaType | null, colorScheme?: types.ColorScheme | null, reducedMotion?: types.ReducedMotion | null, forcedColors?: types.ForcedColors | null }) { if (options.media !== undefined) this._state.mediaType = options.media; if (options.colorScheme !== undefined) this._state.colorScheme = options.colorScheme; if (options.reducedMotion !== undefined) this._state.reducedMotion = options.reducedMotion; + if (options.forcedColors !== undefined) + this._state.forcedColors = options.forcedColors; await this._delegate.updateEmulateMedia(); await this._doSlowMo(); } diff --git a/src/server/types.ts b/src/server/types.ts index da442574de..ac6938d200 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -96,6 +96,9 @@ export const colorSchemes: Set = new Set(['dark', 'light', 'no-pref export type ReducedMotion = 'no-preference' | 'reduce'; export const reducedMotions: Set = new Set(['no-preference', 'reduce']); +export type ForcedColors = 'active' | 'none'; +export const forcedColors: Set = new Set(['active', 'none']); + export type DeviceDescriptor = { userAgent: string, viewport: Size, @@ -256,6 +259,7 @@ export type BrowserContextOptions = { hasTouch?: boolean, colorScheme?: ColorScheme, reducedMotion?: ReducedMotion, + forcedColors?: ForcedColors, acceptDownloads?: boolean, recordVideo?: { dir: string, diff --git a/tests/defaultbrowsercontext-2.spec.ts b/tests/defaultbrowsercontext-2.spec.ts index 119aca3f92..af39c089f2 100644 --- a/tests/defaultbrowsercontext-2.spec.ts +++ b/tests/defaultbrowsercontext-2.spec.ts @@ -44,6 +44,13 @@ it('should support reducedMotion option', async ({launchPersistent}) => { expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: no-preference)').matches)).toBe(false); }); +it('should support forcedColors option', async ({launchPersistent, browserName}) => { + it.skip(browserName === 'webkit', 'https://bugs.webkit.org/show_bug.cgi?id=225281'); + const {page} = await launchPersistent({forcedColors: 'active'}); + expect(await page.evaluate(() => matchMedia('(forced-colors: active)').matches)).toBe(true); + expect(await page.evaluate(() => matchMedia('(forced-colors: none)').matches)).toBe(false); +}); + it('should support timezoneId option', async ({launchPersistent, browserName}) => { const {page} = await launchPersistent({locale: 'en-US', timezoneId: 'America/Jamaica'}); expect(await page.evaluate(() => new Date(1479579154987).toString())).toBe('Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)'); diff --git a/tests/page/page-emulate-media.spec.ts b/tests/page/page-emulate-media.spec.ts index 32c29df85c..a82039de2b 100644 --- a/tests/page/page-emulate-media.spec.ts +++ b/tests/page/page-emulate-media.spec.ts @@ -40,7 +40,7 @@ it('should throw in case of bad media argument', async ({page}) => { expect(error.message).toContain('media: expected one of (screen|print|null)'); }); -it('should emulate scheme work', async ({page}) => { +it('should emulate colorScheme should work', async ({page}) => { await page.emulateMedia({ colorScheme: 'light' }); expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches)).toBe(true); expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches)).toBe(false); @@ -125,3 +125,17 @@ it('should emulate reduced motion', async ({page}) => { expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: no-preference)').matches)).toBe(true); await page.emulateMedia({ reducedMotion: null }); }); + +it('should emulate forcedColors ', async ({page, browserName, isAndroid}) => { + it.skip(browserName === 'webkit', 'https://bugs.webkit.org/show_bug.cgi?id=225281'); + it.fixme(isAndroid); + expect(await page.evaluate(() => matchMedia('(forced-colors: none)').matches)).toBe(true); + await page.emulateMedia({ forcedColors: 'none' }); + expect(await page.evaluate(() => matchMedia('(forced-colors: none)').matches)).toBe(true); + expect(await page.evaluate(() => matchMedia('(forced-colors: active)').matches)).toBe(false); + await page.emulateMedia({ forcedColors: 'active' }); + expect(await page.evaluate(() => matchMedia('(forced-colors: none)').matches)).toBe(false); + expect(await page.evaluate(() => matchMedia('(forced-colors: active)').matches)).toBe(true); + await page.emulateMedia({ forcedColors: null }); + expect(await page.evaluate(() => matchMedia('(forced-colors: none)').matches)).toBe(true); +}); diff --git a/types/types.d.ts b/types/types.d.ts index f3808ea058..920c0de783 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -1932,6 +1932,14 @@ export interface Page { */ colorScheme?: null|"light"|"dark"|"no-preference"; + /** + * Emulates `'forced-colors'` media feature, supported values are `'active'` and `'none'`. Passing `null` disables forced + * colors emulation. + * + * > NOTE: It's not supported in WebKit, see [here](https://bugs.webkit.org/show_bug.cgi?id=225281) in their issue tracker. + */ + forcedColors?: null|"active"|"none"; + /** * Changes the CSS media type of the page. The only allowed values are `'screen'`, `'print'` and `null`. Passing `null` * disables CSS media emulation. @@ -9719,6 +9727,15 @@ export interface BrowserType { */ extraHTTPHeaders?: { [key: string]: string; }; + /** + * Emulates `'forced-colors'` media feature, supported values are `'active'`, `'none'`. See + * [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details. Defaults + * to `'none'`. + * + * > NOTE: It's not supported in WebKit, see [here](https://bugs.webkit.org/show_bug.cgi?id=225281) in their issue tracker. + */ + forcedColors?: "active"|"none"; + geolocation?: { /** * Latitude between -90 and 90. @@ -10904,6 +10921,15 @@ export interface AndroidDevice { */ extraHTTPHeaders?: { [key: string]: string; }; + /** + * Emulates `'forced-colors'` media feature, supported values are `'active'`, `'none'`. See + * [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details. Defaults + * to `'none'`. + * + * > NOTE: It's not supported in WebKit, see [here](https://bugs.webkit.org/show_bug.cgi?id=225281) in their issue tracker. + */ + forcedColors?: "active"|"none"; + geolocation?: { /** * Latitude between -90 and 90. @@ -11671,6 +11697,15 @@ export interface Browser extends EventEmitter { */ extraHTTPHeaders?: { [key: string]: string; }; + /** + * Emulates `'forced-colors'` media feature, supported values are `'active'`, `'none'`. See + * [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details. Defaults + * to `'none'`. + * + * > NOTE: It's not supported in WebKit, see [here](https://bugs.webkit.org/show_bug.cgi?id=225281) in their issue tracker. + */ + forcedColors?: "active"|"none"; + geolocation?: { /** * Latitude between -90 and 90. @@ -13932,6 +13967,15 @@ export interface BrowserContextOptions { */ extraHTTPHeaders?: { [key: string]: string; }; + /** + * Emulates `'forced-colors'` media feature, supported values are `'active'`, `'none'`. See + * [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details. Defaults + * to `'none'`. + * + * > NOTE: It's not supported in WebKit, see [here](https://bugs.webkit.org/show_bug.cgi?id=225281) in their issue tracker. + */ + forcedColors?: "active"|"none"; + geolocation?: Geolocation; /**