feat: added reduced motion media query emulation (#6646)

This commit is contained in:
Max Schmitt 2021-05-22 01:56:09 +02:00 committed by GitHub
parent af2fec6bcf
commit ba29e99ace
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 143 additions and 27 deletions

View File

@ -8,12 +8,12 @@
},
{
"name": "firefox",
"revision": "1265",
"revision": "1266",
"installByDefault": true
},
{
"name": "firefox-stable",
"revision": "1255",
"revision": "1256",
"installByDefault": false
},
{

View File

@ -970,6 +970,11 @@ Passing `null` disables CSS media emulation.
Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. Passing
`null` disables color scheme emulation.
### option: Page.emulateMedia.reducedMotion
- `reducedMotion` <null|[ReducedMotion]<"reduce"|"no-preference">>
Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. Passing `null` disables reduced motion emulation.
## async method: Page.evalOnSelector
* langs:
- alias-python: eval_on_selector

View File

@ -366,6 +366,12 @@ Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/W
Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See
[`method: Page.emulateMedia`] for more details. Defaults to `'light'`.
## context-option-reducedMotion
- `reducedMotion` <[ReducedMotion]<"reduce"|"no-preference">>
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-logger
* langs: js
- `logger` <[Logger]>
@ -578,6 +584,7 @@ using the [`method: AndroidDevice.setDefaultTimeout`] method.
- %%-context-option-offline-%%
- %%-context-option-httpcredentials-%%
- %%-context-option-colorscheme-%%
- %%-context-option-reducedMotion-%%
- %%-context-option-logger-%%
- %%-context-option-videospath-%%
- %%-context-option-videosize-%%

View File

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

View File

@ -121,6 +121,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
await this._page.emulateMedia({
media: params.media === 'null' ? null : params.media,
colorScheme: params.colorScheme === 'null' ? null : params.colorScheme,
reducedMotion: params.reducedMotion === 'null' ? null : params.reducedMotion,
});
}

View File

@ -313,6 +313,7 @@ export type BrowserTypeLaunchPersistentContextParams = {
isMobile?: boolean,
hasTouch?: boolean,
colorScheme?: 'dark' | 'light' | 'no-preference',
reducedMotion?: 'reduce' | 'no-preference',
acceptDownloads?: boolean,
_debugName?: string,
recordVideo?: {
@ -382,6 +383,7 @@ export type BrowserTypeLaunchPersistentContextOptions = {
isMobile?: boolean,
hasTouch?: boolean,
colorScheme?: 'dark' | 'light' | 'no-preference',
reducedMotion?: 'reduce' | 'no-preference',
acceptDownloads?: boolean,
_debugName?: string,
recordVideo?: {
@ -471,6 +473,7 @@ export type BrowserNewContextParams = {
isMobile?: boolean,
hasTouch?: boolean,
colorScheme?: 'dark' | 'light' | 'no-preference',
reducedMotion?: 'reduce' | 'no-preference',
acceptDownloads?: boolean,
_debugName?: string,
recordVideo?: {
@ -527,6 +530,7 @@ export type BrowserNewContextOptions = {
isMobile?: boolean,
hasTouch?: boolean,
colorScheme?: 'dark' | 'light' | 'no-preference',
reducedMotion?: 'reduce' | 'no-preference',
acceptDownloads?: boolean,
_debugName?: string,
recordVideo?: {
@ -973,10 +977,12 @@ export type PageCloseResult = void;
export type PageEmulateMediaParams = {
media?: 'screen' | 'print' | 'null',
colorScheme?: 'dark' | 'light' | 'no-preference' | 'null',
reducedMotion?: 'reduce' | 'no-preference' | 'null',
};
export type PageEmulateMediaOptions = {
media?: 'screen' | 'print' | 'null',
colorScheme?: 'dark' | 'light' | 'no-preference' | 'null',
reducedMotion?: 'reduce' | 'no-preference' | 'null',
};
export type PageEmulateMediaResult = void;
export type PageExposeBindingParams = {
@ -2934,6 +2940,7 @@ export type AndroidDeviceLaunchBrowserParams = {
isMobile?: boolean,
hasTouch?: boolean,
colorScheme?: 'dark' | 'light' | 'no-preference',
reducedMotion?: 'reduce' | 'no-preference',
acceptDownloads?: boolean,
_debugName?: string,
recordVideo?: {
@ -2978,6 +2985,7 @@ export type AndroidDeviceLaunchBrowserOptions = {
isMobile?: boolean,
hasTouch?: boolean,
colorScheme?: 'dark' | 'light' | 'no-preference',
reducedMotion?: 'reduce' | 'no-preference',
acceptDownloads?: boolean,
_debugName?: string,
recordVideo?: {

View File

@ -312,6 +312,11 @@ ContextOptions:
- dark
- light
- no-preference
reducedMotion:
type: enum?
literals:
- reduce
- no-preference
acceptDownloads: boolean?
_debugName: string?
recordVideo:
@ -717,6 +722,13 @@ Page:
- no-preference
# Reset emulated value to the system default.
- null
reducedMotion:
type: enum?
literals:
- reduce
- no-preference
# Reset emulated value to the system default.
- null
exposeBinding:
parameters:
@ -2374,6 +2386,11 @@ AndroidDevice:
- dark
- light
- no-preference
reducedMotion:
type: enum?
literals:
- reduce
- no-preference
acceptDownloads: boolean?
_debugName: string?
recordVideo:

View File

@ -231,6 +231,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
isMobile: tOptional(tBoolean),
hasTouch: tOptional(tBoolean),
colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference'])),
reducedMotion: tOptional(tEnum(['reduce', 'no-preference'])),
acceptDownloads: tOptional(tBoolean),
_debugName: tOptional(tString),
recordVideo: tOptional(tObject({
@ -289,6 +290,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
isMobile: tOptional(tBoolean),
hasTouch: tOptional(tBoolean),
colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference'])),
reducedMotion: tOptional(tEnum(['reduce', 'no-preference'])),
acceptDownloads: tOptional(tBoolean),
_debugName: tOptional(tString),
recordVideo: tOptional(tObject({
@ -410,6 +412,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
scheme.PageEmulateMediaParams = tObject({
media: tOptional(tEnum(['screen', 'print', 'null'])),
colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference', 'null'])),
reducedMotion: tOptional(tEnum(['reduce', 'no-preference', 'null'])),
});
scheme.PageExposeBindingParams = tObject({
name: tString,
@ -1128,6 +1131,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
isMobile: tOptional(tBoolean),
hasTouch: tOptional(tBoolean),
colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference'])),
reducedMotion: tOptional(tEnum(['reduce', 'no-preference'])),
acceptDownloads: tOptional(tBoolean),
_debugName: tOptional(tString),
recordVideo: tOptional(tObject({

View File

@ -1006,8 +1006,13 @@ class FrameSession {
async _updateEmulateMedia(initial: boolean): Promise<void> {
if (this._crPage._browserContext._browser.isClank())
return;
const colorScheme = this._page._state.colorScheme || this._crPage._browserContext._options.colorScheme || 'light';
const features = colorScheme ? [{ name: 'prefers-color-scheme', value: colorScheme }] : [];
const colorScheme = this._page._state.colorScheme === null ? '' : this._page._state.colorScheme;
const reducedMotion = this._page._state.reducedMotion === null ? '' : this._page._state.reducedMotion;
const features = [
{ name: 'prefers-color-scheme', value: colorScheme },
{ name: 'prefers-reduced-motion', value: reducedMotion },
];
// Empty string disables the override.
await this._client.send('Emulation.setEmulatedMedia', { media: this._page._state.mediaType || '', features });
}

View File

@ -198,8 +198,14 @@ export class FFBrowserContext extends BrowserContext {
promises.push(this.setGeolocation(this._options.geolocation));
if (this._options.offline)
promises.push(this.setOffline(this._options.offline));
if (this._options.colorScheme)
promises.push(this._browser._connection.send('Browser.setColorScheme', { browserContextId, colorScheme: this._options.colorScheme }));
promises.push(this._browser._connection.send('Browser.setColorScheme', {
browserContextId,
colorScheme: this._options.colorScheme !== undefined ? this._options.colorScheme : 'light',
}));
promises.push(this._browser._connection.send('Browser.setReducedMotion', {
browserContextId,
reducedMotion: this._options.reducedMotion !== undefined ? this._options.reducedMotion : 'no-preference',
}));
if (this._options.recordVideo) {
promises.push(this._ensureVideosPath().then(() => {
return this._browser._connection.send('Browser.setVideoRecordingOptions', {

View File

@ -352,11 +352,13 @@ export class FFPage implements PageDelegate {
}
async updateEmulateMedia(): Promise<void> {
const colorScheme = this._page._state.colorScheme || this._browserContext._options.colorScheme || 'light';
const colorScheme = this._page._state.colorScheme === null ? undefined : this._page._state.colorScheme;
const reducedMotion = this._page._state.reducedMotion === null ? undefined : this._page._state.reducedMotion;
await this._session.send('Page.setEmulatedMedia', {
// Empty string means reset.
type: this._page._state.mediaType === null ? '' : this._page._state.mediaType,
colorScheme
colorScheme,
reducedMotion,
});
}

View File

@ -88,6 +88,7 @@ type PageState = {
emulatedSize: { screen: types.Size, viewport: types.Size } | null;
mediaType: types.MediaType | null;
colorScheme: types.ColorScheme | null;
reducedMotion: types.ReducedMotion | null;
extraHTTPHeaders: types.HeadersArray | null;
};
@ -159,7 +160,8 @@ export class Page extends SdkObject {
this._state = {
emulatedSize: browserContext._options.viewport ? { viewport: browserContext._options.viewport, screen: browserContext._options.screen || browserContext._options.viewport } : null,
mediaType: null,
colorScheme: null,
colorScheme: browserContext._options.colorScheme !== undefined ? browserContext._options.colorScheme : 'light',
reducedMotion: browserContext._options.reducedMotion !== undefined ? browserContext._options.reducedMotion : 'no-preference',
extraHTTPHeaders: null,
};
this.accessibility = new accessibility.Accessibility(delegate.getAccessibilityTree.bind(delegate));
@ -359,15 +361,13 @@ export class Page extends SdkObject {
}), this._timeoutSettings.navigationTimeout(options));
}
async emulateMedia(options: { media?: types.MediaType | null, colorScheme?: types.ColorScheme | null }) {
if (options.media !== undefined)
assert(options.media === null || types.mediaTypes.has(options.media), 'media: expected one of (screen|print|null)');
if (options.colorScheme !== undefined)
assert(options.colorScheme === null || types.colorSchemes.has(options.colorScheme), 'colorScheme: expected one of (dark|light|no-preference|null)');
async emulateMedia(options: { media?: types.MediaType | null, colorScheme?: types.ColorScheme | null, reducedMotion?: types.ReducedMotion | 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;
await this._delegate.updateEmulateMedia();
await this._doSlowMo();
}

View File

@ -84,6 +84,9 @@ export const mediaTypes: Set<MediaType> = new Set(['screen', 'print']);
export type ColorScheme = 'dark' | 'light' | 'no-preference';
export const colorSchemes: Set<ColorScheme> = new Set(['dark', 'light', 'no-preference']);
export type ReducedMotion = 'no-preference' | 'reduce';
export const reducedMotions: Set<ReducedMotion> = new Set(['no-preference', 'reduce']);
export type DeviceDescriptor = {
userAgent: string,
viewport: Size,
@ -237,6 +240,7 @@ export type BrowserContextOptions = {
isMobile?: boolean,
hasTouch?: boolean,
colorScheme?: ColorScheme,
reducedMotion?: ReducedMotion,
acceptDownloads?: boolean,
recordVideo?: {
dir: string,

View File

@ -180,8 +180,8 @@ export class WKPage implements PageDelegate {
const contextOptions = this._browserContext._options;
if (contextOptions.userAgent)
promises.push(session.send('Page.overrideUserAgent', { value: contextOptions.userAgent }));
if (this._page._state.mediaType || this._page._state.colorScheme)
promises.push(WKPage._setEmulateMedia(session, this._page._state.mediaType, this._page._state.colorScheme));
if (this._page._state.mediaType || this._page._state.colorScheme || this._page._state.reducedMotion)
promises.push(WKPage._setEmulateMedia(session, this._page._state.mediaType, this._page._state.colorScheme, this._page._state.reducedMotion));
for (const world of ['main', 'utility'] as const) {
const bootstrapScript = this._calculateBootstrapScript(world);
if (bootstrapScript.length)
@ -580,17 +580,21 @@ export class WKPage implements PageDelegate {
await this._page._onFileChooserOpened(handle);
}
private static async _setEmulateMedia(session: WKSession, mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise<void> {
private static async _setEmulateMedia(session: WKSession, mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null, reducedMotion: types.ReducedMotion | null): Promise<void> {
const promises = [];
promises.push(session.send('Page.setEmulatedMedia', { media: mediaType || '' }));
if (colorScheme !== null) {
let appearance: any = '';
switch (colorScheme) {
case 'light': appearance = 'Light'; break;
case 'dark': appearance = 'Dark'; break;
}
promises.push(session.send('Page.setForcedAppearance', { appearance }));
let appearance: any = undefined;
switch (colorScheme) {
case 'light': appearance = 'Light'; break;
case 'dark': appearance = 'Dark'; break;
}
promises.push(session.send('Page.setForcedAppearance', { appearance }));
let reducedMotionWk: any = undefined;
switch (reducedMotion) {
case 'reduce': reducedMotionWk = 'Reduce'; break;
case 'no-preference': reducedMotionWk = 'NoPreference'; break;
}
promises.push(session.send('Page.setForcedReducedMotion', { reducedMotion: reducedMotionWk }));
await Promise.all(promises);
}
@ -609,8 +613,9 @@ export class WKPage implements PageDelegate {
}
async updateEmulateMedia(): Promise<void> {
const colorScheme = this._page._state.colorScheme || this._browserContext._options.colorScheme || 'light';
await this._forAllSessions(session => WKPage._setEmulateMedia(session, this._page._state.mediaType, colorScheme));
const colorScheme = this._page._state.colorScheme;
const reducedMotion = this._page._state.reducedMotion;
await this._forAllSessions(session => WKPage._setEmulateMedia(session, this._page._state.mediaType, colorScheme, reducedMotion));
}
async setEmulatedSize(emulatedSize: types.EmulatedSize): Promise<void> {

View File

@ -38,6 +38,12 @@ it('should support colorScheme option', async ({launchPersistent}) => {
expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches)).toBe(true);
});
it('should support reducedMotion option', async ({launchPersistent}) => {
const {page} = await launchPersistent({reducedMotion: 'reduce'});
expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches)).toBe(true);
expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: no-preference)').matches)).toBe(false);
});
it('should support timezoneId option', async ({launchPersistent}) => {
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

@ -114,3 +114,14 @@ it('should change the actual colors in css', async ({page}) => {
await page.emulateMedia({ colorScheme: 'light' });
expect(await getBackgroundColor()).toBe('rgb(255, 255, 255)');
});
it('should emulate reduced motion', async ({page}) => {
expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: no-preference)').matches)).toBe(true);
await page.emulateMedia({ reducedMotion: 'reduce' });
expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches)).toBe(true);
expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: no-preference)').matches)).toBe(false);
await page.emulateMedia({ reducedMotion: 'no-preference' });
expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches)).toBe(false);
expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: no-preference)').matches)).toBe(true);
await page.emulateMedia({ reducedMotion: null });
});

34
types/types.d.ts vendored
View File

@ -1679,6 +1679,12 @@ export interface Page {
* disables CSS media emulation.
*/
media?: null|"screen"|"print";
/**
* Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. Passing `null`
* disables reduced motion emulation.
*/
reducedMotion?: null|"reduce"|"no-preference";
}): Promise<void>;
/**
@ -7174,6 +7180,13 @@ export interface BrowserType<Unused = {}> {
};
};
/**
* Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. See
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#pageemulatemediaoptions) for more details.
* Defaults to `'no-preference'`.
*/
reducedMotion?: "reduce"|"no-preference";
/**
* Emulates consistent window screen size available inside web page via `window.screen`. Is only used when the `viewport`
* is set.
@ -8197,6 +8210,13 @@ export interface AndroidDevice {
};
};
/**
* Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. See
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#pageemulatemediaoptions) for more details.
* Defaults to `'no-preference'`.
*/
reducedMotion?: "reduce"|"no-preference";
/**
* Emulates consistent window screen size available inside web page via `window.screen`. Is only used when the `viewport`
* is set.
@ -8974,6 +8994,13 @@ export interface Browser extends EventEmitter {
};
};
/**
* Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. See
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#pageemulatemediaoptions) for more details.
* Defaults to `'no-preference'`.
*/
reducedMotion?: "reduce"|"no-preference";
/**
* Emulates consistent window screen size available inside web page via `window.screen`. Is only used when the `viewport`
* is set.
@ -11019,6 +11046,13 @@ export interface BrowserContextOptions {
};
};
/**
* Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. See
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#pageemulatemediaoptions) for more details.
* Defaults to `'no-preference'`.
*/
reducedMotion?: "reduce"|"no-preference";
/**
* Emulates consistent window screen size available inside web page via `window.screen`. Is only used when the `viewport`
* is set.