feat: add forced-colors media query emulation (#6903)

This commit is contained in:
Max Schmitt 2021-09-03 21:48:06 +02:00 committed by GitHub
parent f7a490f80e
commit e7d4d61442
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 135 additions and 3 deletions

View file

@ -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` <null|[ForcedColors]<"active"|"none">>
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

View file

@ -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-%%

View file

@ -410,12 +410,13 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
});
}
async emulateMedia(options: { media?: 'screen' | 'print' | null, colorScheme?: 'dark' | 'light' | 'no-preference' | null, reducedMotion?: 'reduce' | 'no-preference' | null } = {}) {
async emulateMedia(options: { media?: 'screen' | 'print' | null, colorScheme?: 'dark' | 'light' | 'no-preference' | null, reducedMotion?: 'reduce' | 'no-preference' | null, forcedColors?: 'active' | 'none' | null } = {}) {
return this._wrapApiCall(async (channel: channels.PageChannel) => {
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,
});
});
}

View file

@ -122,6 +122,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer, c
media: params.media === 'null' ? null : params.media,
colorScheme: params.colorScheme === 'null' ? null : params.colorScheme,
reducedMotion: params.reducedMotion === 'null' ? null : params.reducedMotion,
forcedColors: params.forcedColors === 'null' ? null : params.forcedColors,
});
}

View file

@ -422,6 +422,7 @@ export type BrowserTypeLaunchPersistentContextParams = {
hasTouch?: boolean,
colorScheme?: 'dark' | 'light' | 'no-preference',
reducedMotion?: 'reduce' | 'no-preference',
forcedColors?: 'active' | 'none',
acceptDownloads?: boolean,
baseURL?: string,
_debugName?: string,
@ -494,6 +495,7 @@ export type BrowserTypeLaunchPersistentContextOptions = {
hasTouch?: boolean,
colorScheme?: 'dark' | 'light' | 'no-preference',
reducedMotion?: 'reduce' | 'no-preference',
forcedColors?: 'active' | 'none',
acceptDownloads?: boolean,
baseURL?: string,
_debugName?: string,
@ -587,6 +589,7 @@ export type BrowserNewContextParams = {
hasTouch?: boolean,
colorScheme?: 'dark' | 'light' | 'no-preference',
reducedMotion?: 'reduce' | 'no-preference',
forcedColors?: 'active' | 'none',
acceptDownloads?: boolean,
baseURL?: string,
_debugName?: string,
@ -646,6 +649,7 @@ export type BrowserNewContextOptions = {
hasTouch?: boolean,
colorScheme?: 'dark' | 'light' | 'no-preference',
reducedMotion?: 'reduce' | 'no-preference',
forcedColors?: 'active' | 'none',
acceptDownloads?: boolean,
baseURL?: string,
_debugName?: string,
@ -1168,11 +1172,13 @@ export type PageEmulateMediaParams = {
media?: 'screen' | 'print' | 'null',
colorScheme?: 'dark' | 'light' | 'no-preference' | 'null',
reducedMotion?: 'reduce' | 'no-preference' | 'null',
forcedColors?: 'active' | 'none' | 'null',
};
export type PageEmulateMediaOptions = {
media?: 'screen' | 'print' | 'null',
colorScheme?: 'dark' | 'light' | 'no-preference' | 'null',
reducedMotion?: 'reduce' | 'no-preference' | 'null',
forcedColors?: 'active' | 'none' | 'null',
};
export type PageEmulateMediaResult = void;
export type PageExposeBindingParams = {
@ -3365,6 +3371,7 @@ export type AndroidDeviceLaunchBrowserParams = {
hasTouch?: boolean,
colorScheme?: 'dark' | 'light' | 'no-preference',
reducedMotion?: 'reduce' | 'no-preference',
forcedColors?: 'active' | 'none',
acceptDownloads?: boolean,
_debugName?: string,
recordVideo?: {
@ -3411,6 +3418,7 @@ export type AndroidDeviceLaunchBrowserOptions = {
hasTouch?: boolean,
colorScheme?: 'dark' | 'light' | 'no-preference',
reducedMotion?: 'reduce' | 'no-preference',
forcedColors?: 'active' | 'none',
acceptDownloads?: boolean,
_debugName?: string,
recordVideo?: {

View file

@ -312,6 +312,11 @@ ContextOptions:
literals:
- reduce
- no-preference
forcedColors:
type: enum?
literals:
- active
- none
acceptDownloads: boolean?
baseURL: string?
_debugName: string?
@ -834,6 +839,13 @@ Page:
- no-preference
# Reset emulated value to the system default.
- null
forcedColors:
type: enum?
literals:
- active
- none
# Reset emulated value to the system default.
- null
exposeBinding:
parameters:
@ -2719,6 +2731,11 @@ AndroidDevice:
literals:
- reduce
- no-preference
forcedColors:
type: enum?
literals:
- active
- none
acceptDownloads: boolean?
_debugName: string?
recordVideo:

View file

@ -266,6 +266,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),
@ -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({

View file

@ -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 });

View file

@ -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', {

View file

@ -354,11 +354,13 @@ export class FFPage implements PageDelegate {
async updateEmulateMedia(): Promise<void> {
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,
});
}

View file

@ -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();
}

View file

@ -96,6 +96,9 @@ export const colorSchemes: Set<ColorScheme> = new Set(['dark', 'light', 'no-pref
export type ReducedMotion = 'no-preference' | 'reduce';
export const reducedMotions: Set<ReducedMotion> = new Set(['no-preference', 'reduce']);
export type ForcedColors = 'active' | 'none';
export const forcedColors: Set<ForcedColors> = 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,

View file

@ -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)');

View file

@ -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);
});

44
types/types.d.ts vendored
View file

@ -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<Unused = {}> {
*/
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;
/**