diff --git a/.eslintignore b/.eslintignore index 70e57e2111..687648ad61 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,6 +8,7 @@ node6-test/* node6-testrunner/* lib/ *.js -src/chromium/protocol.d.ts src/generated/* +src/chromium/protocol.d.ts +src/firefox/protocol.d.ts src/webkit/protocol.d.ts diff --git a/.gitignore b/.gitignore index c2ae7f3ebc..311c1c0bb1 100644 --- a/.gitignore +++ b/.gitignore @@ -14,8 +14,9 @@ package-lock.json yarn.lock /node6 -/src/chromium/protocol.d.ts /src/generated/* +/src/chromium/protocol.d.ts +/src/firefox/protocol.d.ts /src/webkit/protocol.d.ts /utils/browser/playwright-web.js /index.d.ts diff --git a/install.js b/install.js index 12ec4b26d9..441b3e3443 100644 --- a/install.js +++ b/install.js @@ -26,7 +26,7 @@ try { } (async function() { - const {generateWebKitProtocol, generateChromeProtocol} = require('./utils/protocol-types-generator/') ; + const {generateWebKitProtocol, generateFirefoxProtocol, generateChromeProtocol} = require('./utils/protocol-types-generator/') ; try { const chromeRevision = await downloadBrowser('chromium', require('./chromium').createBrowserFetcher()); await generateChromeProtocol(chromeRevision); @@ -35,7 +35,8 @@ try { } try { - await downloadBrowser('firefox', require('./firefox').createBrowserFetcher()); + const firefoxRevision = await downloadBrowser('firefox', require('./firefox').createBrowserFetcher()); + await generateFirefoxProtocol(firefoxRevision); } catch (e) { console.warn(e.message); } diff --git a/package.json b/package.json index 1f32de3eb0..e617283475 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "jpeg-js": "^0.3.4", "minimist": "^1.2.0", "ncp": "^2.0.0", + "node-stream-zip": "^1.8.2", "pixelmatch": "^4.0.2", "pngjs": "^3.3.3", "text-diff": "^1.0.1", diff --git a/src/firefox/Connection.ts b/src/firefox/Connection.ts index 5f5122221e..6474713b90 100644 --- a/src/firefox/Connection.ts +++ b/src/firefox/Connection.ts @@ -19,6 +19,7 @@ import {assert} from '../helper'; import {EventEmitter} from 'events'; import * as debug from 'debug'; import { ConnectionTransport } from '../ConnectionTransport'; +import { Protocol } from './protocol'; const debugProtocol = debug('playwright:protocol'); export const ConnectionEvents = { @@ -144,6 +145,12 @@ export class JugglerSession extends EventEmitter { private _callbacks: Map; private _targetType: string; private _sessionId: string; + on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; + addListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; + off: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; + removeListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; + once: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; + constructor(connection: Connection, targetType: string, sessionId: string) { super(); this._callbacks = new Map(); @@ -152,7 +159,10 @@ export class JugglerSession extends EventEmitter { this._sessionId = sessionId; } - send(method: string, params: any = {}): Promise { + send( + method: T, + params?: Protocol.CommandParameters[T] + ): Promise { if (!this._connection) return Promise.reject(new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`)); const id = this._connection._rawSend({sessionId: this._sessionId, method, params}); diff --git a/src/firefox/ExecutionContext.ts b/src/firefox/ExecutionContext.ts index 502964a11c..19ed5058db 100644 --- a/src/firefox/ExecutionContext.ts +++ b/src/firefox/ExecutionContext.ts @@ -18,6 +18,7 @@ import {helper, debugError} from '../helper'; import * as js from '../javascript'; import { JugglerSession } from './Connection'; +import { Protocol } from './protocol'; export class ExecutionContextDelegate implements js.ExecutionContextDelegate { _session: JugglerSession; @@ -104,7 +105,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate { checkException(payload.exceptionDetails); return context._createHandle(payload.result); - function rewriteError(error) { + function rewriteError(error) : never { if (error.message.includes('Failed to find execution context with id')) throw new Error('Execution context was destroyed, most likely because of a navigation.'); throw error; @@ -167,7 +168,7 @@ function checkException(exceptionDetails?: any) { } } -export function deserializeValue({unserializableValue, value}) { +export function deserializeValue({unserializableValue, value}: Protocol.RemoteObject) { if (unserializableValue === 'Infinity') return Infinity; if (unserializableValue === '-Infinity') diff --git a/src/firefox/NetworkManager.ts b/src/firefox/NetworkManager.ts index 07c00480e6..2512918e39 100644 --- a/src/firefox/NetworkManager.ts +++ b/src/firefox/NetworkManager.ts @@ -189,7 +189,7 @@ class InterceptableRequest { (this.request as any)[interceptableRequestSymbol] = this; } - async continue(overrides: any = {}) { + async continue(overrides: {url?: string, method?: string, postData?: string, headers?: {[key: string]: string}} = {}) { assert(!overrides.url, 'Playwright-Firefox does not support overriding URL'); assert(!overrides.method, 'Playwright-Firefox does not support overriding method'); assert(!overrides.postData, 'Playwright-Firefox does not support overriding postData'); diff --git a/src/firefox/Page.ts b/src/firefox/Page.ts index fa3a07870a..95d6ee1346 100644 --- a/src/firefox/Page.ts +++ b/src/firefox/Page.ts @@ -118,7 +118,7 @@ export class Page extends EventEmitter { } async emulateMedia(options: { - type?: string, + type?: ""|"screen"|"print", 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); diff --git a/test/jshandle.spec.js b/test/jshandle.spec.js index 7d120336c6..f0c90055dc 100644 --- a/test/jshandle.spec.js +++ b/test/jshandle.spec.js @@ -97,6 +97,11 @@ module.exports.addTests = function({testRunner, expect, CHROME, FFOX, WEBKIT}) { else if (FFOX) expect(error.message).toContain('Object is not serializable'); }); + it('should work with tricky values', async({page, server}) => { + const aHandle = await page.evaluateHandle(() => ({a: 1})); + const json = await aHandle.jsonValue(); + expect(json).toEqual({a: 1}); + }); }); describe('JSHandle.getProperties', function() { diff --git a/utils/protocol-types-generator/index.js b/utils/protocol-types-generator/index.js index 5ab9885bc4..59c1d4fc9e 100644 --- a/utils/protocol-types-generator/index.js +++ b/utils/protocol-types-generator/index.js @@ -1,6 +1,8 @@ // @ts-check const path = require('path'); const fs = require('fs'); +const StreamZip = require('node-stream-zip'); +const vm = require('vm'); async function generateChromeProtocol(revision) { const outputPath = path.join(__dirname, '..', '..', 'src', 'chromium', 'protocol.d.ts'); @@ -114,4 +116,99 @@ function typeOfProperty(property, domain) { return property.type; } -module.exports = {generateChromeProtocol, generateWebKitProtocol}; \ No newline at end of file +async function generateFirefoxProtocol(revision) { + const outputPath = path.join(__dirname, '..', '..', 'src', 'firefox', 'protocol.d.ts'); + if (revision.local && fs.existsSync(outputPath)) + return; + const zip = new StreamZip({file: path.join(revision.executablePath, '..', 'omni.ja'), storeEntries: true}); + // @ts-ignore + await new Promise(x => zip.on('ready', x)); + const data = zip.entryDataSync(zip.entry('chrome/juggler/content/protocol/Protocol.js')) + + const ctx = vm.createContext(); + const protocolJSCode = data.toString('utf8'); + function inject() { + this.ChromeUtils = { + import: () => ({t}) + } + const t = {}; + t.String = {"$type": "string"}; + t.Number = {"$type": "number"}; + t.Boolean = {"$type": "boolean"}; + t.Undefined = {"$type": "undefined"}; + t.Any = {"$type": "any"}; + + t.Enum = function(values) { + return {"$type": "enum", "$values": values}; + } + + t.Nullable = function(scheme) { + return {...scheme, "$nullable": true}; + } + + t.Optional = function(scheme) { + return {...scheme, "$optional": true}; + } + + t.Array = function(scheme) { + return {"$type": "array", "$items": scheme}; + } + + t.Recursive = function(types, schemeName) { + return {"$type": "ref", "$ref": schemeName }; + } + } + const json = vm.runInContext(`(${inject})();${protocolJSCode}; this.protocol.types = types; this.protocol;`, ctx); + fs.writeFileSync(outputPath, firefoxJSONToTS(json)); + console.log(`Wrote protocol.d.ts for Firefox to ${path.relative(process.cwd(), outputPath)}`); +} + +function firefoxJSONToTS(json) { + const domains = Object.entries(json.domains); + return `// This is generated from /utils/protocol-types-generator/index.js +export module Protocol {${Object.entries(json.types).map(([typeName, type]) => ` + export type ${typeName} = ${firefoxTypeToString(type, ' ')};`).join('')} +${domains.map(([domainName, domain]) => ` + export module ${domainName} {${(Object.entries(domain.events)).map(([eventName, event]) => ` + export type ${eventName}Payload = ${firefoxTypeToString(event)}`).join('')}${(Object.entries(domain.methods)).map(([commandName, command]) => ` + export type ${commandName}Parameters = ${firefoxTypeToString(command.params)}; + export type ${commandName}ReturnValue = ${firefoxTypeToString(command.returns)};`).join('')} + }`).join('')} + export interface Events {${domains.map(([domainName, domain]) => Object.keys(domain.events).map(eventName => ` + "${domainName}.${eventName}": ${domainName}.${eventName}Payload;`).join('')).join('')} + } + export interface CommandParameters {${domains.map(([domainName, domain]) => Object.keys(domain.methods).map(commandName => ` + "${domainName}.${commandName}": ${domainName}.${commandName}Parameters;`).join('')).join('')} + } + export interface CommandReturnValues {${domains.map(([domainName, domain]) => Object.keys(domain.methods).map(commandName => ` + "${domainName}.${commandName}": ${domainName}.${commandName}ReturnValue;`).join('')).join('')} + } +}` + +} + +function firefoxTypeToString(type, indent=' ') { + if (!type) + return 'void'; + if (!type['$type']) { + const properties = Object.entries(type).filter(([name]) => !name.startsWith('$')); + const lines = []; + lines.push('{'); + for (const [propertyName, property] of properties) { + const nameSuffix = property['$optional'] ? '?' : ''; + const valueSuffix = property['$nullable'] ? '|null' : '' + lines.push(`${indent} ${propertyName}${nameSuffix}: ${firefoxTypeToString(property, indent + ' ')}${valueSuffix};`); + } + lines.push(`${indent}}`); + return lines.join('\n'); + } + if (type['$type'] === 'ref') + return type['$ref']; + if (type['$type'] === 'array') + return firefoxTypeToString(type['$items'], indent) + '[]'; + if (type['$type'] === 'enum') + return type['$values'].map(v => JSON.stringify(v)).join('|'); + return type['$type']; +} + +module.exports = {generateChromeProtocol, generateFirefoxProtocol, generateWebKitProtocol}; \ No newline at end of file