From 2e581f16252f418634b1777e33eaccabf826639e Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 25 Nov 2019 15:00:04 -0800 Subject: [PATCH] feat(emulate): implement emulateMedia color scheme in FF (#71) --- browser_patches/firefox/BUILD_NUMBER | 2 +- .../patches/0001-chore-bootstrap.patch | 37 +++++++++++-------- docs/api.md | 27 +------------- src/chromium/Page.ts | 21 ++++++----- src/firefox/Page.ts | 17 +++++---- src/input.ts | 3 ++ src/webkit/Page.ts | 16 +++++--- test/emulation.spec.js | 33 ++++++----------- 8 files changed, 70 insertions(+), 86 deletions(-) diff --git a/browser_patches/firefox/BUILD_NUMBER b/browser_patches/firefox/BUILD_NUMBER index dd11724042..7d802a3e71 100644 --- a/browser_patches/firefox/BUILD_NUMBER +++ b/browser_patches/firefox/BUILD_NUMBER @@ -1 +1 @@ -1001 +1002 diff --git a/browser_patches/firefox/patches/0001-chore-bootstrap.patch b/browser_patches/firefox/patches/0001-chore-bootstrap.patch index 3e780abdd4..dd97b9ea2d 100644 --- a/browser_patches/firefox/patches/0001-chore-bootstrap.patch +++ b/browser_patches/firefox/patches/0001-chore-bootstrap.patch @@ -1,6 +1,6 @@ -From acb1ee0450ffe7739ed96e556095fb0f1c5d60d0 Mon Sep 17 00:00:00 2001 +From 533a4ce6515be3665e44ddeae30ecf5052a17191 Mon Sep 17 00:00:00 2001 From: Pavel -Date: Mon, 25 Nov 2019 13:47:08 -0800 +Date: Mon, 25 Nov 2019 14:56:33 -0800 Subject: [PATCH] chore: bootstrap --- @@ -23,7 +23,7 @@ Subject: [PATCH] chore: bootstrap testing/juggler/content/ContentSession.js | 63 ++ testing/juggler/content/FrameTree.js | 232 ++++++ testing/juggler/content/NetworkMonitor.js | 62 ++ - testing/juggler/content/PageAgent.js | 638 +++++++++++++++++ + testing/juggler/content/PageAgent.js | 644 +++++++++++++++++ testing/juggler/content/RuntimeAgent.js | 468 ++++++++++++ testing/juggler/content/ScrollbarManager.js | 85 +++ .../juggler/content/floating-scrollbars.css | 47 ++ @@ -37,7 +37,7 @@ Subject: [PATCH] chore: bootstrap testing/juggler/protocol/NetworkHandler.js | 154 ++++ testing/juggler/protocol/PageHandler.js | 277 ++++++++ testing/juggler/protocol/PrimitiveTypes.js | 143 ++++ - testing/juggler/protocol/Protocol.js | 669 ++++++++++++++++++ + testing/juggler/protocol/Protocol.js | 670 ++++++++++++++++++ testing/juggler/protocol/RuntimeHandler.js | 41 ++ testing/juggler/protocol/TargetHandler.js | 75 ++ .../statusfilter/nsBrowserStatusFilter.cpp | 12 +- @@ -46,7 +46,7 @@ Subject: [PATCH] chore: bootstrap uriloader/base/nsDocLoader.h | 5 + uriloader/base/nsIWebProgress.idl | 7 +- uriloader/base/nsIWebProgressListener2.idl | 23 + - 42 files changed, 4579 insertions(+), 7 deletions(-) + 42 files changed, 4586 insertions(+), 7 deletions(-) create mode 100644 testing/juggler/BrowserContextManager.js create mode 100644 testing/juggler/Helper.js create mode 100644 testing/juggler/NetworkObserver.js @@ -1795,10 +1795,10 @@ index 000000000000..2508cce41565 + diff --git a/testing/juggler/content/PageAgent.js b/testing/juggler/content/PageAgent.js new file mode 100644 -index 000000000000..0e47a99eda47 +index 000000000000..34b799b53113 --- /dev/null +++ b/testing/juggler/content/PageAgent.js -@@ -0,0 +1,638 @@ +@@ -0,0 +1,644 @@ +"use strict"; +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const Ci = Components.interfaces; @@ -1857,12 +1857,18 @@ index 000000000000..0e47a99eda47 + this._scrollbarManager.setFloatingScrollbars(isMobile); + } + -+ async setEmulatedMedia({media}) { ++ async setEmulatedMedia({type, colorScheme}) { + const docShell = this._frameTree.mainFrame().docShell(); -+ if (media) -+ docShell.contentViewer.emulateMedium(media); -+ else -+ docShell.contentViewer.stopEmulatingMedium(); ++ const cv = docShell.contentViewer; ++ if (type === '') ++ cv.stopEmulatingMedium(); ++ else if (type) ++ cv.emulateMedium(type); ++ switch (colorScheme) { ++ case 'light': cv.emulatePrefersColorScheme(cv.PREFERS_COLOR_SCHEME_LIGHT); break; ++ case 'dark': cv.emulatePrefersColorScheme(cv.PREFERS_COLOR_SCHEME_DARK); break; ++ case 'no-preference': cv.emulatePrefersColorScheme(cv.PREFERS_COLOR_SCHEME_NO_PREFERENCE); break; ++ } + } + + async setUserAgent({userAgent}) { @@ -4123,10 +4129,10 @@ index 000000000000..78b6601b91d0 +this.EXPORTED_SYMBOLS = ['t', 'checkScheme']; diff --git a/testing/juggler/protocol/Protocol.js b/testing/juggler/protocol/Protocol.js new file mode 100644 -index 000000000000..0b2044e057a4 +index 000000000000..1ed27df14a1a --- /dev/null +++ b/testing/juggler/protocol/Protocol.js -@@ -0,0 +1,669 @@ +@@ -0,0 +1,670 @@ +const {t, checkScheme} = ChromeUtils.import('chrome://juggler/content/protocol/PrimitiveTypes.js'); + +// Protocol-specific types. @@ -4617,7 +4623,8 @@ index 000000000000..0b2044e057a4 + }, + 'setEmulatedMedia': { + params: { -+ media: t.Enum(['screen', 'print', '']), ++ type: t.Optional(t.Enum(['screen', 'print', ''])), ++ colorScheme: t.Optional(t.Enum(['dark', 'light', 'no-preference'])), + }, + }, + 'setCacheDisabled': { diff --git a/docs/api.md b/docs/api.md index cd9ffa783b..d102ff1ab6 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1240,9 +1240,7 @@ List of all available devices is available in the source code: [DeviceDescriptor #### page.emulateMedia(options) - `options` <[Object]> - `type` Optional. Changes the CSS media type of the page. The only allowed values are `'screen'`, `'print'` and `null`. Passing `null` disables CSS media emulation. - - `features` > Optional. Given an array of media feature objects, emulates CSS media features on the page. Each media feature object must have the following properties: - - `name` <[string]> The CSS media feature name. Supported names are `'prefers-colors-scheme'` and `'prefers-reduced-motion'`. - - `value` <[string]> The value for the given CSS media feature. + - `colorScheme` Optional. Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. - returns: <[Promise]> ```js @@ -1265,34 +1263,13 @@ await page.evaluate(() => matchMedia('print').matches)); ``` ```js -await page.emulateMedia({ features: [{ name: 'prefers-color-scheme', value: 'dark' }] }); +await page.emulateMedia({ colorScheme: 'dark' }] }); await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches)); // → true await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches)); // → false await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches)); // → false - -await page.emulateMedia({ features: [{ name: 'prefers-reduced-motion', value: 'reduce' }] }); -await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches)); -// → true -await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches)); -// → false - -await page.emulateMedia({ features: [ - { name: 'prefers-color-scheme', value: 'dark' }, - { name: 'prefers-reduced-motion', value: 'reduce' }, -] }); -await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches)); -// → true -await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches)); -// → false -await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches)); -// → false -await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches)); -// → true -await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches)); -// → false ``` #### page.emulateTimezone(timezoneId) diff --git a/src/chromium/Page.ts b/src/chromium/Page.ts index cb847324b7..357043266f 100644 --- a/src/chromium/Page.ts +++ b/src/chromium/Page.ts @@ -20,7 +20,7 @@ import * as fs from 'fs'; import * as mime from 'mime'; import * as path from 'path'; import { assert, debugError, helper } from '../helper'; -import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from '../input'; +import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption, mediaTypes, mediaColorSchemes } from '../input'; import { TimeoutSettings } from '../TimeoutSettings'; import { Browser } from './Browser'; import { BrowserContext } from './BrowserContext'; @@ -78,6 +78,7 @@ export class Page extends EventEmitter { private _fileChooserInterceptionIsDisabled = false; private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>(); private _disconnectPromise: Promise | undefined; + private _emulatedMediaType: string | undefined; static async create(client: CDPSession, target: Target, ignoreHTTPSErrors: boolean, defaultViewport: Viewport | null, screenshotTaskQueue: TaskQueue): Promise { const page = new Page(client, target, ignoreHTTPSErrors, screenshotTaskQueue); @@ -534,15 +535,15 @@ export class Page extends EventEmitter { await this._client.send('Page.setBypassCSP', { enabled }); } - async emulateMedia(options: { type?: string, features?: MediaFeature[] }) { - assert(options.type === 'screen' || options.type === 'print' || options.type === undefined, 'Unsupported media type: ' + options.type); - if (options.features) { - options.features.forEach(mediaFeature => { - const name = mediaFeature.name; - assert(/^prefers-(?:color-scheme|reduced-motion)$/.test(name), 'Unsupported media feature: ' + name); - }); - } - await this._client.send('Emulation.setEmulatedMedia', { media: options.type, features: options.features }); + async emulateMedia(options: { + type?: string, + colorScheme?: 'dark' | 'light' | 'no-preference' }) { + assert(!options.type || mediaTypes.has(options.type), 'Unsupported media type: ' + options.type); + assert(!options.colorScheme || mediaColorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme); + const media = typeof options.type === 'undefined' ? this._emulatedMediaType : options.type; + const features = typeof options.colorScheme === 'undefined' ? [] : [{ name: 'prefers-color-scheme', value: options.colorScheme }]; + await this._client.send('Emulation.setEmulatedMedia', { media: media || '', features }); + this._emulatedMediaType = options.type; } async emulateTimezone(timezoneId: string | null) { diff --git a/src/firefox/Page.ts b/src/firefox/Page.ts index 69f89ad229..0cb0495b89 100644 --- a/src/firefox/Page.ts +++ b/src/firefox/Page.ts @@ -15,7 +15,6 @@ import { Mouse, RawKeyboardImpl } from './Input'; import { createHandle, ElementHandle, JSHandle } from './JSHandle'; import { NavigationWatchdog } from './NavigationWatchdog'; import { NetworkManager, NetworkManagerEvents, Request, Response } from './NetworkManager'; -import { ClickOptions, MultiClickOptions } from '../input'; import * as input from '../input'; const writeFileAsync = helper.promisify(fs.writeFile); @@ -151,10 +150,12 @@ export class Page extends EventEmitter { await this._networkManager.setExtraHTTPHeaders(headers); } - async emulateMedia(options: { type?: string, features?: MediaFeature[] }) { - assert(!options.features, 'Media feature emulation is not supported'); - assert(options.type === 'screen' || options.type === 'print' || options.type === undefined, 'Unsupported media type: ' + options.type); - await this._session.send('Page.setEmulatedMedia', { media: options.type || '' }); + async emulateMedia(options: { + type?: string, + colorScheme?: 'dark' | 'light' | 'no-preference' }) { + assert(!options.type || input.mediaTypes.has(options.type), 'Unsupported media type: ' + options.type); + assert(!options.colorScheme || input.mediaColorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme); + await this._session.send('Page.setEmulatedMedia', options); } async exposeFunction(name: string, playwrightFunction: Function) { @@ -478,15 +479,15 @@ export class Page extends EventEmitter { return this.mainFrame().addStyleTag(options); } - click(selector: string, options?: ClickOptions) { + click(selector: string, options?: input.ClickOptions) { return this.mainFrame().click(selector, options); } - dblclick(selector: string, options?: MultiClickOptions) { + dblclick(selector: string, options?: input.MultiClickOptions) { return this.mainFrame().dblclick(selector, options); } - tripleclick(selector: string, options?: MultiClickOptions) { + tripleclick(selector: string, options?: input.MultiClickOptions) { return this.mainFrame().tripleclick(selector, options); } diff --git a/src/input.ts b/src/input.ts index 86b700aeed..09fd7b4971 100644 --- a/src/input.ts +++ b/src/input.ts @@ -310,3 +310,6 @@ export const fillFunction = (element: HTMLElement) => { } return false; }; + +export const mediaTypes = new Set(['screen', 'print']); +export const mediaColorSchemes = new Set(['dark', 'light', 'no-preference']); diff --git a/src/webkit/Page.ts b/src/webkit/Page.ts index 7ff405e5e3..e5b8709b03 100644 --- a/src/webkit/Page.ts +++ b/src/webkit/Page.ts @@ -19,7 +19,7 @@ import { EventEmitter } from 'events'; import * as fs from 'fs'; import * as mime from 'mime'; import { assert, debugError, helper, RegisteredListener } from '../helper'; -import { ClickOptions, MultiClickOptions } from '../input'; +import { ClickOptions, mediaColorSchemes, mediaTypes, MultiClickOptions } from '../input'; import { TimeoutSettings } from '../TimeoutSettings'; import { Browser, BrowserContext } from './Browser'; import { TargetSession, TargetSessionEvents } from './Connection'; @@ -56,6 +56,7 @@ export class Page extends EventEmitter { private _workers = new Map(); private _disconnectPromise: Promise | undefined; private _sessionListeners: RegisteredListener[] = []; + private _emulatedMediaType: string | undefined; static async create(session: TargetSession, target: Target, defaultViewport: Viewport | null, screenshotTaskQueue: TaskQueue): Promise { const page = new Page(session, target, screenshotTaskQueue); @@ -326,10 +327,15 @@ export class Page extends EventEmitter { ]); } - async emulateMedia(options: { type?: string, features?: MediaFeature[] }) { - assert(!options.features, 'Media feature emulation is not supported'); - assert(options.type === 'screen' || options.type === 'print' || options.type === undefined, 'Unsupported media type: ' + options.type); - await this._session.send('Page.setEmulatedMedia', { media: options.type }); + async emulateMedia(options: { + type?: string | null, + colorScheme?: 'dark' | 'light' | 'no-preference' | null }) { + assert(!options.type || mediaTypes.has(options.type), 'Unsupported media type: ' + options.type); + assert(!options.colorScheme || mediaColorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme); + assert(!options.colorScheme, 'Media feature emulation is not supported'); + const media = typeof options.type === 'undefined' ? this._emulatedMediaType : options.type; + await this._session.send('Page.setEmulatedMedia', { media: media || '' }); + this._emulatedMediaType = options.type; } async setViewport(viewport: Viewport) { diff --git a/test/emulation.spec.js b/test/emulation.spec.js index 5069125591..dddb3c8c3e 100644 --- a/test/emulation.spec.js +++ b/test/emulation.spec.js @@ -104,6 +104,9 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(false); expect(await page.evaluate(() => matchMedia('print').matches)).toBe(true); await page.emulateMedia({}); + expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(false); + expect(await page.evaluate(() => matchMedia('print').matches)).toBe(true); + await page.emulateMedia({ type: '' }); expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(true); expect(await page.evaluate(() => matchMedia('print').matches)).toBe(false); }); @@ -114,39 +117,25 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME }); }); - describe.skip(FFOX || WEBKIT)('Page.emulateMedia features', function() { + describe.skip(WEBKIT)('Page.emulateMedia colorScheme', function() { it('should work', async({page, server}) => { - await page.emulateMedia({ features: [ - { name: 'prefers-reduced-motion', value: '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({ features: [ - { name: 'prefers-color-scheme', value: 'light' }, - ] }); + 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); expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches)).toBe(false); - await page.emulateMedia({ features: [ - { name: 'prefers-color-scheme', value: 'dark' }, - ] }); + await page.emulateMedia({ colorScheme: 'dark' }); expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches)).toBe(true); expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches)).toBe(false); expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches)).toBe(false); - await page.emulateMedia({ features: [ - { name: 'prefers-reduced-motion', value: 'reduce' }, - { name: 'prefers-color-scheme', value: 'light' }, - ] }); - 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); - expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches)).toBe(true); + await page.emulateMedia({ colorScheme: 'no-preference' }); expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches)).toBe(false); - expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches)).toBe(false); + expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches)).toBe(false); + expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches)).toBe(true); }); it('should throw in case of bad argument', async({page, server}) => { let error = null; - await page.emulateMedia({ features: [{ name: 'bad', value: '' }] }).catch(e => error = e); - expect(error.message).toBe('Unsupported media feature: bad'); + await page.emulateMedia({ colorScheme: 'bad' }).catch(e => error = e); + expect(error.message).toBe('Unsupported color scheme: bad'); }); });