diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /remote/test | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/test')
456 files changed, 61988 insertions, 0 deletions
diff --git a/remote/test/browser/README.md b/remote/test/browser/README.md new file mode 100644 index 0000000000..65ed2b862d --- /dev/null +++ b/remote/test/browser/README.md @@ -0,0 +1,11 @@ +Update chrome-remote-interface.js +================================= + +Upstream instructions on +https://github.com/cyrus-and/chrome-remote-interface#using-vanilla-javascript: + + % git clone https://github.com/cyrus-and/chrome-remote-interface.git + % cd chrome-remote-interface + % npm install + % TARGET=var DEBUG=true npm run webpack + % cp chrome-remote-interface.js ../ diff --git a/remote/test/browser/browser.ini b/remote/test/browser/browser.ini new file mode 100644 index 0000000000..cdadb50f5c --- /dev/null +++ b/remote/test/browser/browser.ini @@ -0,0 +1,17 @@ +[DEFAULT] +tags = remote +subsuite = remote +prefs = + remote.enabled=true +support-files = + chrome-remote-interface.js + head.js + +[browser_agent.js] +[browser_cdp.js] +skip-if = socketprocess_networking +[browser_httpd.js] +[browser_main_target.js] +skip-if = socketprocess_networking +[browser_session.js] +[browser_tabs.js] diff --git a/remote/test/browser/browser_agent.js b/remote/test/browser/browser_agent.js new file mode 100644 index 0000000000..1578aaa687 --- /dev/null +++ b/remote/test/browser/browser_agent.js @@ -0,0 +1,131 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Preferences } = ChromeUtils.import( + "resource://gre/modules/Preferences.jsm" +); + +// To fully test the Remote Agent's capabilities an instance of the interface +// needs to be used. This refers to the Rust specific implementation. +const remoteAgentInstance = Cc["@mozilla.org/remote/agent;1"].createInstance( + Ci.nsIRemoteAgent +); + +const URL = "http://localhost:0"; + +// set up test conditions and clean up +function add_agent_task(originalTask) { + const task = async function() { + try { + await RemoteAgent.close(); + await originalTask(); + } finally { + Preferences.reset("remote.enabled"); + Preferences.reset("remote.force-local"); + } + }; + Object.defineProperty(task, "name", { + value: originalTask.name, + writable: false, + }); + add_plain_task(task); +} + +add_agent_task(async function debuggerAddress() { + const port = getNonAtomicFreePort(); + + await RemoteAgent.listen(`http://localhost:${port}`); + is( + remoteAgentInstance.debuggerAddress, + `localhost:${port}`, + "debuggerAddress set" + ); +}); + +add_agent_task(async function listening() { + is(remoteAgentInstance.listening, false, "Agent is not listening"); + await RemoteAgent.listen(URL); + is(remoteAgentInstance.listening, true, "Agent is listening"); +}); + +add_agent_task(async function listen() { + const port = getNonAtomicFreePort(); + + let boundURL; + function observer(subject, topic, data) { + Services.obs.removeObserver(observer, topic); + boundURL = Services.io.newURI(data); + } + Services.obs.addObserver(observer, "remote-listening"); + + await RemoteAgent.listen("http://localhost:" + port); + isnot(boundURL, undefined, "remote-listening observer notified"); + is( + boundURL.port, + port, + `expected default port ${port}, got ${boundURL.port}` + ); +}); + +add_agent_task(async function listenWhenDisabled() { + Preferences.set("remote.enabled", false); + try { + await RemoteAgent.listen(URL); + fail("listen() did not return exception"); + } catch (e) { + is(e.result, Cr.NS_ERROR_NOT_AVAILABLE); + is(e.message, "Disabled by preference"); + } +}); + +// TODO(ato): https://bugzil.la/1590829 +add_agent_task(async function listenTakesString() { + await RemoteAgent.listen("http://localhost:0"); + await RemoteAgent.close(); +}); + +// TODO(ato): https://bugzil.la/1590829 +add_agent_task(async function listenNonURL() { + try { + await RemoteAgent.listen("foobar"); + fail("listen() did not reject non-URL"); + } catch (e) { + is(e.result, Cr.NS_ERROR_MALFORMED_URI); + } +}); + +add_agent_task(async function listenRestrictedToLoopbackDevice() { + try { + await RemoteAgent.listen("http://0.0.0.0:0"); + fail("listen() did not reject non-loopback device"); + } catch (e) { + is(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + is(e.message, "Restricted to loopback devices"); + } +}); + +add_agent_task(async function listenNonLoopbackDevice() { + Preferences.set("remote.force-local", false); + await RemoteAgent.listen("http://0.0.0.0:0"); +}); + +add_agent_task(async function test_close() { + await RemoteAgent.listen(URL); + await RemoteAgent.close(); + // no-op when not listening + await RemoteAgent.close(); +}); + +function getNonAtomicFreePort() { + const so = Cc["@mozilla.org/network/server-socket;1"].createInstance( + Ci.nsIServerSocket + ); + try { + so.init(-1, true /* aLoopbackOnly */, -1 /* aBackLog */); + return so.port; + } finally { + so.close(); + } +} diff --git a/remote/test/browser/browser_cdp.js b/remote/test/browser/browser_cdp.js new file mode 100644 index 0000000000..971bff6b5d --- /dev/null +++ b/remote/test/browser/browser_cdp.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test very basic CDP features. +add_task(async function testCDP({ client }) { + const { Browser, Log, Page } = client; + + ok("Browser" in client, "Browser domain is available"); + ok("Log" in client, "Log domain is available"); + ok("Page" in client, "Page domain is available"); + + const version = await Browser.getVersion(); + const { isHeadless } = Cc["@mozilla.org/gfx/info;1"].getService( + Ci.nsIGfxInfo + ); + is( + version.product, + isHeadless ? "Headless Firefox" : "Firefox", + "Browser.getVersion works and depends on headless mode" + ); + is( + version.userAgent, + window.navigator.userAgent, + "Browser.getVersion().userAgent is correct" + ); + + // receive console.log messages and print them + let result = await Log.enable(); + info("Log domain has been enabled"); + Assert.deepEqual(result, {}, "Got expected result value"); + + Log.entryAdded(({ entry }) => { + const { timestamp, level, text, args } = entry; + const msg = text || args.join(" "); + console.log(`${new Date(timestamp)}\t${level.toUpperCase()}\t${msg}`); + }); + + // turn on navigation related events, such as DOMContentLoaded et al. + result = await Page.enable(); + info("Page domain has been enabled"); + Assert.deepEqual(result, {}, "Got expected result value"); + + const frameStoppedLoading = Page.frameStoppedLoading(); + const navigatedWithinDocument = Page.navigatedWithinDocument(); + const loadEventFired = Page.loadEventFired(); + await Page.navigate({ + url: toDataURL(`<script>console.log("foo")</script>`), + }); + info("A new page has been requested"); + + await loadEventFired; + info("`Page.loadEventFired` fired"); + + await frameStoppedLoading; + info("`Page.frameStoppedLoading` fired"); + + await navigatedWithinDocument; + info("`Page.navigatedWithinDocument` fired"); +}); diff --git a/remote/test/browser/browser_httpd.js b/remote/test/browser/browser_httpd.js new file mode 100644 index 0000000000..1176ba9417 --- /dev/null +++ b/remote/test/browser/browser_httpd.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function json_version() { + await RemoteAgent.listen(`http://localhost:0`); + + const { userAgent } = Cc[ + "@mozilla.org/network/protocol;1?name=http" + ].getService(Ci.nsIHttpProtocolHandler); + + const json = await requestJSON("/version"); + is( + json.Browser, + `${Services.appinfo.name}/${Services.appinfo.version}`, + "Browser name and version found" + ); + is(json["Protocol-Version"], "1.0", "Protocol version found"); + is(json["User-Agent"], userAgent, "User agent found"); + is(json["V8-Version"], "1.0", "V8 version found"); + is(json["WebKit-Version"], "1.0", "Webkit version found"); + is( + json.webSocketDebuggerUrl, + RemoteAgent.targets.getMainProcessTarget().wsDebuggerURL, + "Websocket URL for main process target found" + ); +}); + +function requestJSON(path) { + const url = `http://${RemoteAgent.debuggerAddress}`; + + return new Promise(resolve => { + var xhr = new XMLHttpRequest(); + xhr.open("GET", `${url}/json${path}`); + xhr.responseType = "json"; + xhr.onloadend = () => { + resolve(xhr.response); + }; + xhr.send(); + }); +} diff --git a/remote/test/browser/browser_main_target.js b/remote/test/browser/browser_main_target.js new file mode 100644 index 0000000000..117c55f2b0 --- /dev/null +++ b/remote/test/browser/browser_main_target.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test very basic CDP features. + +add_task(async function({ CDP }) { + const { mainProcessTarget } = RemoteAgent.targets; + ok( + mainProcessTarget, + "The main process target is instantiated after the call to `listen`" + ); + + const targetURL = mainProcessTarget.wsDebuggerURL; + + const client = await CDP({ target: targetURL }); + info("CDP client has been instantiated"); + + try { + const { Browser, Target } = client; + ok(Browser, "The main process target exposes Browser domain"); + ok(Target, "The main process target exposes Target domain"); + + const version = await Browser.getVersion(); + + const { isHeadless } = Cc["@mozilla.org/gfx/info;1"].getService( + Ci.nsIGfxInfo + ); + const expectedProduct = isHeadless ? "Headless Firefox" : "Firefox"; + is(version.product, expectedProduct, "Browser.getVersion works"); + + const { webSocketDebuggerUrl } = await CDP.Version(); + is( + webSocketDebuggerUrl, + targetURL, + "Version endpoint refers to the same Main process target" + ); + } finally { + await client.close(); + } +}); diff --git a/remote/test/browser/browser_session.js b/remote/test/browser/browser_session.js new file mode 100644 index 0000000000..659021182b --- /dev/null +++ b/remote/test/browser/browser_session.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function({ CDP }) { + const { webSocketDebuggerUrl } = await CDP.Version(); + const client = await CDP({ target: webSocketDebuggerUrl }); + + try { + await client.send("Hoobaflooba"); + } catch (e) { + ok(e.message.match(/Invalid method format/)); + } + try { + await client.send("Hooba.flooba"); + } catch (e) { + ok(e.message.match(/UnknownMethodError/)); + } + + await client.close(); +}); diff --git a/remote/test/browser/browser_tabs.js b/remote/test/browser/browser_tabs.js new file mode 100644 index 0000000000..353d94267e --- /dev/null +++ b/remote/test/browser/browser_tabs.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test very basic CDP features. + +const TEST_URL = toDataURL("default-test-page"); + +add_task(async function({ CDP }) { + // Use gBrowser.addTab instead of BrowserTestUtils as it creates the tab differently. + // It demonstrates a race around tab.linkedBrowser.browsingContext being undefined + // when accessing this property early. + const tab = gBrowser.addTab(TEST_URL, { + skipAnimation: true, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + let targets = await getTargets(CDP); + ok( + targets.some(target => target.url == TEST_URL), + "Found the tab in target list" + ); + + BrowserTestUtils.removeTab(tab); + + targets = await getTargets(CDP); + ok( + !targets.some(target => target.url == TEST_URL), + "Tab has been removed from the target list" + ); +}); diff --git a/remote/test/browser/chrome-remote-interface.js b/remote/test/browser/chrome-remote-interface.js new file mode 100644 index 0000000000..2df9c701fe --- /dev/null +++ b/remote/test/browser/chrome-remote-interface.js @@ -0,0 +1,8 @@ +var CDP=function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(r,i,function(t){return e[t]}.bind(null,i));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=152)}([function(e,t,n){var r=n(2),i=n(19),o=n(12),a=n(13),s=n(20),c=function(e,t,n){var p,d,u,l,m=e&c.F,f=e&c.G,h=e&c.S,g=e&c.P,y=e&c.B,b=f?r:h?r[t]||(r[t]={}):(r[t]||{}).prototype,v=f?i:i[t]||(i[t]={}),w=v.prototype||(v.prototype={});for(p in f&&(n=t),n)u=((d=!m&&b&&void 0!==b[p])?b:n)[p],l=y&&d?s(u,r):g&&"function"==typeof u?s(Function.call,u):u,b&&a(b,p,u,e&c.U),v[p]!=u&&o(v,p,l),g&&w[p]!=u&&(w[p]=u)};r.core=i,c.F=1,c.G=2,c.S=4,c.P=8,c.B=16,c.W=32,c.U=64,c.R=128,e.exports=c},function(e,t,n){var r=n(4);e.exports=function(e){if(!r(e))throw TypeError(e+" is not an object!");return e}},function(e,t){var n=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(e,t){e.exports=function(e){try{return!!e()}catch(e){return!0}}},function(e,t){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},function(e,t,n){var r=n(51)("wks"),i=n(36),o=n(2).Symbol,a="function"==typeof o;(e.exports=function(e){return r[e]||(r[e]=a&&o[e]||(a?o:i)("Symbol."+e))}).store=r},function(e,t,n){var r=n(22),i=Math.min;e.exports=function(e){return e>0?i(r(e),9007199254740991):0}},function(e,t,n){e.exports=!n(3)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(e,t,n){var r=n(1),i=n(103),o=n(24),a=Object.defineProperty;t.f=n(7)?Object.defineProperty:function(e,t,n){if(r(e),t=o(t,!0),r(n),i)try{return a(e,t,n)}catch(e){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(e[t]=n.value),e}},function(e,t,n){var r=n(25);e.exports=function(e){return Object(r(e))}},function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t,n){var r=n(8),i=n(35);e.exports=n(7)?function(e,t,n){return r.f(e,t,i(1,n))}:function(e,t,n){return e[t]=n,e}},function(e,t,n){var r=n(2),i=n(12),o=n(15),a=n(36)("src"),s=n(156),c=(""+s).split("toString");n(19).inspectSource=function(e){return s.call(e)},(e.exports=function(e,t,n,s){var p="function"==typeof n;p&&(o(n,"name")||i(n,"name",t)),e[t]!==n&&(p&&(o(n,a)||i(n,a,e[t]?""+e[t]:c.join(String(t)))),e===r?e[t]=n:s?e[t]?e[t]=n:i(e,t,n):(delete e[t],i(e,t,n)))})(Function.prototype,"toString",function(){return"function"==typeof this&&this[a]||s.call(this)})},function(e,t,n){var r=n(0),i=n(3),o=n(25),a=/"/g,s=function(e,t,n,r){var i=String(o(e)),s="<"+t;return""!==n&&(s+=" "+n+'="'+String(r).replace(a,""")+'"'),s+">"+i+"</"+t+">"};e.exports=function(e,t){var n={};n[e]=t(s),r(r.P+r.F*i(function(){var t=""[e]('"');return t!==t.toLowerCase()||t.split('"').length>3}),"String",n)}},function(e,t){var n={}.hasOwnProperty;e.exports=function(e,t){return n.call(e,t)}},function(e,t,n){var r=n(52),i=n(25);e.exports=function(e){return r(i(e))}},function(e,t,n){var r=n(53),i=n(35),o=n(16),a=n(24),s=n(15),c=n(103),p=Object.getOwnPropertyDescriptor;t.f=n(7)?p:function(e,t){if(e=o(e),t=a(t,!0),c)try{return p(e,t)}catch(e){}if(s(e,t))return i(!r.f.call(e,t),e[t])}},function(e,t,n){var r=n(15),i=n(9),o=n(78)("IE_PROTO"),a=Object.prototype;e.exports=Object.getPrototypeOf||function(e){return e=i(e),r(e,o)?e[o]:"function"==typeof e.constructor&&e instanceof e.constructor?e.constructor.prototype:e instanceof Object?a:null}},function(e,t){var n=e.exports={version:"2.6.9"};"number"==typeof __e&&(__e=n)},function(e,t,n){var r=n(10);e.exports=function(e,t,n){if(r(e),void 0===t)return e;switch(n){case 1:return function(n){return e.call(t,n)};case 2:return function(n,r){return e.call(t,n,r)};case 3:return function(n,r,i){return e.call(t,n,r,i)}}return function(){return e.apply(t,arguments)}}},function(e,t){var n={}.toString;e.exports=function(e){return n.call(e).slice(8,-1)}},function(e,t){var n=Math.ceil,r=Math.floor;e.exports=function(e){return isNaN(e=+e)?0:(e>0?r:n)(e)}},function(e,t,n){"use strict";var r=n(3);e.exports=function(e,t){return!!e&&r(function(){t?e.call(null,function(){},1):e.call(null)})}},function(e,t,n){var r=n(4);e.exports=function(e,t){if(!r(e))return e;var n,i;if(t&&"function"==typeof(n=e.toString)&&!r(i=n.call(e)))return i;if("function"==typeof(n=e.valueOf)&&!r(i=n.call(e)))return i;if(!t&&"function"==typeof(n=e.toString)&&!r(i=n.call(e)))return i;throw TypeError("Can't convert object to primitive value")}},function(e,t){e.exports=function(e){if(null==e)throw TypeError("Can't call method on "+e);return e}},function(e,t,n){var r=n(0),i=n(19),o=n(3);e.exports=function(e,t){var n=(i.Object||{})[e]||Object[e],a={};a[e]=t(n),r(r.S+r.F*o(function(){n(1)}),"Object",a)}},function(e,t,n){var r=n(20),i=n(52),o=n(9),a=n(6),s=n(94);e.exports=function(e,t){var n=1==e,c=2==e,p=3==e,d=4==e,u=6==e,l=5==e||u,m=t||s;return function(t,s,f){for(var h,g,y=o(t),b=i(y),v=r(s,f,3),w=a(b.length),S=0,x=n?m(t,w):c?m(t,0):void 0;w>S;S++)if((l||S in b)&&(g=v(h=b[S],S,y),e))if(n)x[S]=g;else if(g)switch(e){case 3:return!0;case 5:return h;case 6:return S;case 2:x.push(h)}else if(d)return!1;return u?-1:p||d?d:x}}},function(e,t,n){"use strict";if(n(7)){var r=n(31),i=n(2),o=n(3),a=n(0),s=n(69),c=n(102),p=n(20),d=n(42),u=n(35),l=n(12),m=n(44),f=n(22),h=n(6),g=n(131),y=n(38),b=n(24),v=n(15),w=n(47),S=n(4),x=n(9),I=n(91),T=n(39),R=n(18),k=n(40).f,C=n(93),O=n(36),$=n(5),E=n(27),D=n(59),j=n(55),P=n(96),A=n(49),M=n(64),N=n(41),_=n(95),L=n(120),q=n(8),F=n(17),U=q.f,B=F.f,W=i.RangeError,H=i.TypeError,z=i.Uint8Array,V=Array.prototype,G=c.ArrayBuffer,J=c.DataView,X=E(0),Y=E(2),K=E(3),Q=E(4),Z=E(5),ee=E(6),te=D(!0),ne=D(!1),re=P.values,ie=P.keys,oe=P.entries,ae=V.lastIndexOf,se=V.reduce,ce=V.reduceRight,pe=V.join,de=V.sort,ue=V.slice,le=V.toString,me=V.toLocaleString,fe=$("iterator"),he=$("toStringTag"),ge=O("typed_constructor"),ye=O("def_constructor"),be=s.CONSTR,ve=s.TYPED,we=s.VIEW,Se=E(1,function(e,t){return ke(j(e,e[ye]),t)}),xe=o(function(){return 1===new z(new Uint16Array([1]).buffer)[0]}),Ie=!!z&&!!z.prototype.set&&o(function(){new z(1).set({})}),Te=function(e,t){var n=f(e);if(n<0||n%t)throw W("Wrong offset!");return n},Re=function(e){if(S(e)&&ve in e)return e;throw H(e+" is not a typed array!")},ke=function(e,t){if(!(S(e)&&ge in e))throw H("It is not a typed array constructor!");return new e(t)},Ce=function(e,t){return Oe(j(e,e[ye]),t)},Oe=function(e,t){for(var n=0,r=t.length,i=ke(e,r);r>n;)i[n]=t[n++];return i},$e=function(e,t,n){U(e,t,{get:function(){return this._d[n]}})},Ee=function(e){var t,n,r,i,o,a,s=x(e),c=arguments.length,d=c>1?arguments[1]:void 0,u=void 0!==d,l=C(s);if(null!=l&&!I(l)){for(a=l.call(s),r=[],t=0;!(o=a.next()).done;t++)r.push(o.value);s=r}for(u&&c>2&&(d=p(d,arguments[2],2)),t=0,n=h(s.length),i=ke(this,n);n>t;t++)i[t]=u?d(s[t],t):s[t];return i},De=function(){for(var e=0,t=arguments.length,n=ke(this,t);t>e;)n[e]=arguments[e++];return n},je=!!z&&o(function(){me.call(new z(1))}),Pe=function(){return me.apply(je?ue.call(Re(this)):Re(this),arguments)},Ae={copyWithin:function(e,t){return L.call(Re(this),e,t,arguments.length>2?arguments[2]:void 0)},every:function(e){return Q(Re(this),e,arguments.length>1?arguments[1]:void 0)},fill:function(e){return _.apply(Re(this),arguments)},filter:function(e){return Ce(this,Y(Re(this),e,arguments.length>1?arguments[1]:void 0))},find:function(e){return Z(Re(this),e,arguments.length>1?arguments[1]:void 0)},findIndex:function(e){return ee(Re(this),e,arguments.length>1?arguments[1]:void 0)},forEach:function(e){X(Re(this),e,arguments.length>1?arguments[1]:void 0)},indexOf:function(e){return ne(Re(this),e,arguments.length>1?arguments[1]:void 0)},includes:function(e){return te(Re(this),e,arguments.length>1?arguments[1]:void 0)},join:function(e){return pe.apply(Re(this),arguments)},lastIndexOf:function(e){return ae.apply(Re(this),arguments)},map:function(e){return Se(Re(this),e,arguments.length>1?arguments[1]:void 0)},reduce:function(e){return se.apply(Re(this),arguments)},reduceRight:function(e){return ce.apply(Re(this),arguments)},reverse:function(){for(var e,t=Re(this).length,n=Math.floor(t/2),r=0;r<n;)e=this[r],this[r++]=this[--t],this[t]=e;return this},some:function(e){return K(Re(this),e,arguments.length>1?arguments[1]:void 0)},sort:function(e){return de.call(Re(this),e)},subarray:function(e,t){var n=Re(this),r=n.length,i=y(e,r);return new(j(n,n[ye]))(n.buffer,n.byteOffset+i*n.BYTES_PER_ELEMENT,h((void 0===t?r:y(t,r))-i))}},Me=function(e,t){return Ce(this,ue.call(Re(this),e,t))},Ne=function(e){Re(this);var t=Te(arguments[1],1),n=this.length,r=x(e),i=h(r.length),o=0;if(i+t>n)throw W("Wrong length!");for(;o<i;)this[t+o]=r[o++]},_e={entries:function(){return oe.call(Re(this))},keys:function(){return ie.call(Re(this))},values:function(){return re.call(Re(this))}},Le=function(e,t){return S(e)&&e[ve]&&"symbol"!=typeof t&&t in e&&String(+t)==String(t)},qe=function(e,t){return Le(e,t=b(t,!0))?u(2,e[t]):B(e,t)},Fe=function(e,t,n){return!(Le(e,t=b(t,!0))&&S(n)&&v(n,"value"))||v(n,"get")||v(n,"set")||n.configurable||v(n,"writable")&&!n.writable||v(n,"enumerable")&&!n.enumerable?U(e,t,n):(e[t]=n.value,e)};be||(F.f=qe,q.f=Fe),a(a.S+a.F*!be,"Object",{getOwnPropertyDescriptor:qe,defineProperty:Fe}),o(function(){le.call({})})&&(le=me=function(){return pe.call(this)});var Ue=m({},Ae);m(Ue,_e),l(Ue,fe,_e.values),m(Ue,{slice:Me,set:Ne,constructor:function(){},toString:le,toLocaleString:Pe}),$e(Ue,"buffer","b"),$e(Ue,"byteOffset","o"),$e(Ue,"byteLength","l"),$e(Ue,"length","e"),U(Ue,he,{get:function(){return this[ve]}}),e.exports=function(e,t,n,c){var p=e+((c=!!c)?"Clamped":"")+"Array",u="get"+e,m="set"+e,f=i[p],y=f||{},b=f&&R(f),v=!f||!s.ABV,x={},I=f&&f.prototype,C=function(e,n){U(e,n,{get:function(){return function(e,n){var r=e._d;return r.v[u](n*t+r.o,xe)}(this,n)},set:function(e){return function(e,n,r){var i=e._d;c&&(r=(r=Math.round(r))<0?0:r>255?255:255&r),i.v[m](n*t+i.o,r,xe)}(this,n,e)},enumerable:!0})};v?(f=n(function(e,n,r,i){d(e,f,p,"_d");var o,a,s,c,u=0,m=0;if(S(n)){if(!(n instanceof G||"ArrayBuffer"==(c=w(n))||"SharedArrayBuffer"==c))return ve in n?Oe(f,n):Ee.call(f,n);o=n,m=Te(r,t);var y=n.byteLength;if(void 0===i){if(y%t)throw W("Wrong length!");if((a=y-m)<0)throw W("Wrong length!")}else if((a=h(i)*t)+m>y)throw W("Wrong length!");s=a/t}else s=g(n),o=new G(a=s*t);for(l(e,"_d",{b:o,o:m,l:a,e:s,v:new J(o)});u<s;)C(e,u++)}),I=f.prototype=T(Ue),l(I,"constructor",f)):o(function(){f(1)})&&o(function(){new f(-1)})&&M(function(e){new f,new f(null),new f(1.5),new f(e)},!0)||(f=n(function(e,n,r,i){var o;return d(e,f,p),S(n)?n instanceof G||"ArrayBuffer"==(o=w(n))||"SharedArrayBuffer"==o?void 0!==i?new y(n,Te(r,t),i):void 0!==r?new y(n,Te(r,t)):new y(n):ve in n?Oe(f,n):Ee.call(f,n):new y(g(n))}),X(b!==Function.prototype?k(y).concat(k(b)):k(y),function(e){e in f||l(f,e,y[e])}),f.prototype=I,r||(I.constructor=f));var O=I[fe],$=!!O&&("values"==O.name||null==O.name),E=_e.values;l(f,ge,!0),l(I,ve,p),l(I,we,!0),l(I,ye,f),(c?new f(1)[he]==p:he in I)||U(I,he,{get:function(){return p}}),x[p]=f,a(a.G+a.W+a.F*(f!=y),x),a(a.S,p,{BYTES_PER_ELEMENT:t}),a(a.S+a.F*o(function(){y.of.call(f,1)}),p,{from:Ee,of:De}),"BYTES_PER_ELEMENT"in I||l(I,"BYTES_PER_ELEMENT",t),a(a.P,p,Ae),N(p),a(a.P+a.F*Ie,p,{set:Ne}),a(a.P+a.F*!$,p,_e),r||I.toString==le||(I.toString=le),a(a.P+a.F*o(function(){new f(1).slice()}),p,{slice:Me}),a(a.P+a.F*(o(function(){return[1,2].toLocaleString()!=new f([1,2]).toLocaleString()})||!o(function(){I.toLocaleString.call([1,2])})),p,{toLocaleString:Pe}),A[p]=$?O:E,r||$||l(I,fe,E)}}else e.exports=function(){}},function(e,t,n){var r=n(126),i=n(0),o=n(51)("metadata"),a=o.store||(o.store=new(n(129))),s=function(e,t,n){var i=a.get(e);if(!i){if(!n)return;a.set(e,i=new r)}var o=i.get(t);if(!o){if(!n)return;i.set(t,o=new r)}return o};e.exports={store:a,map:s,has:function(e,t,n){var r=s(t,n,!1);return void 0!==r&&r.has(e)},get:function(e,t,n){var r=s(t,n,!1);return void 0===r?void 0:r.get(e)},set:function(e,t,n,r){s(n,r,!0).set(e,t)},keys:function(e,t){var n=s(e,t,!1),r=[];return n&&n.forEach(function(e,t){r.push(t)}),r},key:function(e){return void 0===e||"symbol"==typeof e?e:String(e)},exp:function(e){i(i.S,"Reflect",e)}}},function(e,t){var n,r,i=e.exports={};function o(){throw new Error("setTimeout has not been defined")}function a(){throw new Error("clearTimeout has not been defined")}function s(e){if(n===setTimeout)return setTimeout(e,0);if((n===o||!n)&&setTimeout)return n=setTimeout,setTimeout(e,0);try{return n(e,0)}catch(t){try{return n.call(null,e,0)}catch(t){return n.call(this,e,0)}}}!function(){try{n="function"==typeof setTimeout?setTimeout:o}catch(e){n=o}try{r="function"==typeof clearTimeout?clearTimeout:a}catch(e){r=a}}();var c,p=[],d=!1,u=-1;function l(){d&&c&&(d=!1,c.length?p=c.concat(p):u=-1,p.length&&m())}function m(){if(!d){var e=s(l);d=!0;for(var t=p.length;t;){for(c=p,p=[];++u<t;)c&&c[u].run();u=-1,t=p.length}c=null,d=!1,function(e){if(r===clearTimeout)return clearTimeout(e);if((r===a||!r)&&clearTimeout)return r=clearTimeout,clearTimeout(e);try{r(e)}catch(t){try{return r.call(null,e)}catch(t){return r.call(this,e)}}}(e)}}function f(e,t){this.fun=e,this.array=t}function h(){}i.nextTick=function(e){var t=new Array(arguments.length-1);if(arguments.length>1)for(var n=1;n<arguments.length;n++)t[n-1]=arguments[n];p.push(new f(e,t)),1!==p.length||d||s(m)},f.prototype.run=function(){this.fun.apply(null,this.array)},i.title="browser",i.browser=!0,i.env={},i.argv=[],i.version="",i.versions={},i.on=h,i.addListener=h,i.once=h,i.off=h,i.removeListener=h,i.removeAllListeners=h,i.emit=h,i.prependListener=h,i.prependOnceListener=h,i.listeners=function(e){return[]},i.binding=function(e){throw new Error("process.binding is not supported")},i.cwd=function(){return"/"},i.chdir=function(e){throw new Error("process.chdir is not supported")},i.umask=function(){return 0}},function(e,t){e.exports=!1},function(e,t,n){var r=n(36)("meta"),i=n(4),o=n(15),a=n(8).f,s=0,c=Object.isExtensible||function(){return!0},p=!n(3)(function(){return c(Object.preventExtensions({}))}),d=function(e){a(e,r,{value:{i:"O"+ ++s,w:{}}})},u=e.exports={KEY:r,NEED:!1,fastKey:function(e,t){if(!i(e))return"symbol"==typeof e?e:("string"==typeof e?"S":"P")+e;if(!o(e,r)){if(!c(e))return"F";if(!t)return"E";d(e)}return e[r].i},getWeak:function(e,t){if(!o(e,r)){if(!c(e))return!0;if(!t)return!1;d(e)}return e[r].w},onFreeze:function(e){return p&&u.NEED&&c(e)&&!o(e,r)&&d(e),e}}},function(e,t,n){var r=n(5)("unscopables"),i=Array.prototype;null==i[r]&&n(12)(i,r,{}),e.exports=function(e){i[r][e]=!0}},function(e,t){"function"==typeof Object.create?e.exports=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(e,t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},function(e,t){e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},function(e,t){var n=0,r=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++n+r).toString(36))}},function(e,t,n){var r=n(105),i=n(79);e.exports=Object.keys||function(e){return r(e,i)}},function(e,t,n){var r=n(22),i=Math.max,o=Math.min;e.exports=function(e,t){return(e=r(e))<0?i(e+t,0):o(e,t)}},function(e,t,n){var r=n(1),i=n(106),o=n(79),a=n(78)("IE_PROTO"),s=function(){},c=function(){var e,t=n(76)("iframe"),r=o.length;for(t.style.display="none",n(80).appendChild(t),t.src="javascript:",(e=t.contentWindow.document).open(),e.write("<script>document.F=Object<\/script>"),e.close(),c=e.F;r--;)delete c.prototype[o[r]];return c()};e.exports=Object.create||function(e,t){var n;return null!==e?(s.prototype=r(e),n=new s,s.prototype=null,n[a]=e):n=c(),void 0===t?n:i(n,t)}},function(e,t,n){var r=n(105),i=n(79).concat("length","prototype");t.f=Object.getOwnPropertyNames||function(e){return r(e,i)}},function(e,t,n){"use strict";var r=n(2),i=n(8),o=n(7),a=n(5)("species");e.exports=function(e){var t=r[e];o&&t&&!t[a]&&i.f(t,a,{configurable:!0,get:function(){return this}})}},function(e,t){e.exports=function(e,t,n,r){if(!(e instanceof t)||void 0!==r&&r in e)throw TypeError(n+": incorrect invocation!");return e}},function(e,t,n){var r=n(20),i=n(118),o=n(91),a=n(1),s=n(6),c=n(93),p={},d={};(t=e.exports=function(e,t,n,u,l){var m,f,h,g,y=l?function(){return e}:c(e),b=r(n,u,t?2:1),v=0;if("function"!=typeof y)throw TypeError(e+" is not iterable!");if(o(y)){for(m=s(e.length);m>v;v++)if((g=t?b(a(f=e[v])[0],f[1]):b(e[v]))===p||g===d)return g}else for(h=y.call(e);!(f=h.next()).done;)if((g=i(h,b,f.value,t))===p||g===d)return g}).BREAK=p,t.RETURN=d},function(e,t,n){var r=n(13);e.exports=function(e,t,n){for(var i in t)r(e,i,t[i],n);return e}},function(e,t,n){var r=n(4);e.exports=function(e,t){if(!r(e)||e._t!==t)throw TypeError("Incompatible receiver, "+t+" required!");return e}},function(e,t,n){var r=n(8).f,i=n(15),o=n(5)("toStringTag");e.exports=function(e,t,n){e&&!i(e=n?e:e.prototype,o)&&r(e,o,{configurable:!0,value:t})}},function(e,t,n){var r=n(21),i=n(5)("toStringTag"),o="Arguments"==r(function(){return arguments}());e.exports=function(e){var t,n,a;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(n=function(e,t){try{return e[t]}catch(e){}}(t=Object(e),i))?n:o?r(t):"Object"==(a=r(t))&&"function"==typeof t.callee?"Arguments":a}},function(e,t,n){var r=n(0),i=n(25),o=n(3),a=n(82),s="["+a+"]",c=RegExp("^"+s+s+"*"),p=RegExp(s+s+"*$"),d=function(e,t,n){var i={},s=o(function(){return!!a[e]()||"
"!="
"[e]()}),c=i[e]=s?t(u):a[e];n&&(i[n]=c),r(r.P+r.F*s,"String",i)},u=d.trim=function(e,t){return e=String(i(e)),1&t&&(e=e.replace(c,"")),2&t&&(e=e.replace(p,"")),e};e.exports=d},function(e,t){e.exports={}},function(e,t,n){"use strict";var r=n(73),i=Object.keys||function(e){var t=[];for(var n in e)t.push(n);return t};e.exports=u;var o=n(58);o.inherits=n(34);var a=n(145),s=n(148);o.inherits(u,a);for(var c=i(s.prototype),p=0;p<c.length;p++){var d=c[p];u.prototype[d]||(u.prototype[d]=s.prototype[d])}function u(e){if(!(this instanceof u))return new u(e);a.call(this,e),s.call(this,e),e&&!1===e.readable&&(this.readable=!1),e&&!1===e.writable&&(this.writable=!1),this.allowHalfOpen=!0,e&&!1===e.allowHalfOpen&&(this.allowHalfOpen=!1),this.once("end",l)}function l(){this.allowHalfOpen||this._writableState.ended||r.nextTick(m,this)}function m(e){e.end()}Object.defineProperty(u.prototype,"writableHighWaterMark",{enumerable:!1,get:function(){return this._writableState.highWaterMark}}),Object.defineProperty(u.prototype,"destroyed",{get:function(){return void 0!==this._readableState&&void 0!==this._writableState&&(this._readableState.destroyed&&this._writableState.destroyed)},set:function(e){void 0!==this._readableState&&void 0!==this._writableState&&(this._readableState.destroyed=e,this._writableState.destroyed=e)}}),u.prototype._destroy=function(e,t){this.push(null),this.end(),r.nextTick(t,e)}},function(e,t,n){var r=n(19),i=n(2),o=i["__core-js_shared__"]||(i["__core-js_shared__"]={});(e.exports=function(e,t){return o[e]||(o[e]=void 0!==t?t:{})})("versions",[]).push({version:r.version,mode:n(31)?"pure":"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})},function(e,t,n){var r=n(21);e.exports=Object("z").propertyIsEnumerable(0)?Object:function(e){return"String"==r(e)?e.split(""):Object(e)}},function(e,t){t.f={}.propertyIsEnumerable},function(e,t,n){"use strict";var r=n(1);e.exports=function(){var e=r(this),t="";return e.global&&(t+="g"),e.ignoreCase&&(t+="i"),e.multiline&&(t+="m"),e.unicode&&(t+="u"),e.sticky&&(t+="y"),t}},function(e,t,n){var r=n(1),i=n(10),o=n(5)("species");e.exports=function(e,t){var n,a=r(e).constructor;return void 0===a||null==(n=r(a)[o])?t:i(n)}},function(e,t,n){"use strict";var r,i="object"==typeof Reflect?Reflect:null,o=i&&"function"==typeof i.apply?i.apply:function(e,t,n){return Function.prototype.apply.call(e,t,n)};r=i&&"function"==typeof i.ownKeys?i.ownKeys:Object.getOwnPropertySymbols?function(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:function(e){return Object.getOwnPropertyNames(e)};var a=Number.isNaN||function(e){return e!=e};function s(){s.init.call(this)}e.exports=s,s.EventEmitter=s,s.prototype._events=void 0,s.prototype._eventsCount=0,s.prototype._maxListeners=void 0;var c=10;function p(e){return void 0===e._maxListeners?s.defaultMaxListeners:e._maxListeners}function d(e,t,n,r){var i,o,a,s;if("function"!=typeof n)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof n);if(void 0===(o=e._events)?(o=e._events=Object.create(null),e._eventsCount=0):(void 0!==o.newListener&&(e.emit("newListener",t,n.listener?n.listener:n),o=e._events),a=o[t]),void 0===a)a=o[t]=n,++e._eventsCount;else if("function"==typeof a?a=o[t]=r?[n,a]:[a,n]:r?a.unshift(n):a.push(n),(i=p(e))>0&&a.length>i&&!a.warned){a.warned=!0;var c=new Error("Possible EventEmitter memory leak detected. "+a.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");c.name="MaxListenersExceededWarning",c.emitter=e,c.type=t,c.count=a.length,s=c,console&&console.warn&&console.warn(s)}return e}function u(e,t,n){var r={fired:!1,wrapFn:void 0,target:e,type:t,listener:n},i=function(){for(var e=[],t=0;t<arguments.length;t++)e.push(arguments[t]);this.fired||(this.target.removeListener(this.type,this.wrapFn),this.fired=!0,o(this.listener,this.target,e))}.bind(r);return i.listener=n,r.wrapFn=i,i}function l(e,t,n){var r=e._events;if(void 0===r)return[];var i=r[t];return void 0===i?[]:"function"==typeof i?n?[i.listener||i]:[i]:n?function(e){for(var t=new Array(e.length),n=0;n<t.length;++n)t[n]=e[n].listener||e[n];return t}(i):f(i,i.length)}function m(e){var t=this._events;if(void 0!==t){var n=t[e];if("function"==typeof n)return 1;if(void 0!==n)return n.length}return 0}function f(e,t){for(var n=new Array(t),r=0;r<t;++r)n[r]=e[r];return n}Object.defineProperty(s,"defaultMaxListeners",{enumerable:!0,get:function(){return c},set:function(e){if("number"!=typeof e||e<0||a(e))throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received '+e+".");c=e}}),s.init=function(){void 0!==this._events&&this._events!==Object.getPrototypeOf(this)._events||(this._events=Object.create(null),this._eventsCount=0),this._maxListeners=this._maxListeners||void 0},s.prototype.setMaxListeners=function(e){if("number"!=typeof e||e<0||a(e))throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received '+e+".");return this._maxListeners=e,this},s.prototype.getMaxListeners=function(){return p(this)},s.prototype.emit=function(e){for(var t=[],n=1;n<arguments.length;n++)t.push(arguments[n]);var r="error"===e,i=this._events;if(void 0!==i)r=r&&void 0===i.error;else if(!r)return!1;if(r){var a;if(t.length>0&&(a=t[0]),a instanceof Error)throw a;var s=new Error("Unhandled error."+(a?" ("+a.message+")":""));throw s.context=a,s}var c=i[e];if(void 0===c)return!1;if("function"==typeof c)o(c,this,t);else{var p=c.length,d=f(c,p);for(n=0;n<p;++n)o(d[n],this,t)}return!0},s.prototype.addListener=function(e,t){return d(this,e,t,!1)},s.prototype.on=s.prototype.addListener,s.prototype.prependListener=function(e,t){return d(this,e,t,!0)},s.prototype.once=function(e,t){if("function"!=typeof t)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof t);return this.on(e,u(this,e,t)),this},s.prototype.prependOnceListener=function(e,t){if("function"!=typeof t)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof t);return this.prependListener(e,u(this,e,t)),this},s.prototype.removeListener=function(e,t){var n,r,i,o,a;if("function"!=typeof t)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof t);if(void 0===(r=this._events))return this;if(void 0===(n=r[e]))return this;if(n===t||n.listener===t)0==--this._eventsCount?this._events=Object.create(null):(delete r[e],r.removeListener&&this.emit("removeListener",e,n.listener||t));else if("function"!=typeof n){for(i=-1,o=n.length-1;o>=0;o--)if(n[o]===t||n[o].listener===t){a=n[o].listener,i=o;break}if(i<0)return this;0===i?n.shift():function(e,t){for(;t+1<e.length;t++)e[t]=e[t+1];e.pop()}(n,i),1===n.length&&(r[e]=n[0]),void 0!==r.removeListener&&this.emit("removeListener",e,a||t)}return this},s.prototype.off=s.prototype.removeListener,s.prototype.removeAllListeners=function(e){var t,n,r;if(void 0===(n=this._events))return this;if(void 0===n.removeListener)return 0===arguments.length?(this._events=Object.create(null),this._eventsCount=0):void 0!==n[e]&&(0==--this._eventsCount?this._events=Object.create(null):delete n[e]),this;if(0===arguments.length){var i,o=Object.keys(n);for(r=0;r<o.length;++r)"removeListener"!==(i=o[r])&&this.removeAllListeners(i);return this.removeAllListeners("removeListener"),this._events=Object.create(null),this._eventsCount=0,this}if("function"==typeof(t=n[e]))this.removeListener(e,t);else if(void 0!==t)for(r=t.length-1;r>=0;r--)this.removeListener(e,t[r]);return this},s.prototype.listeners=function(e){return l(this,e,!0)},s.prototype.rawListeners=function(e){return l(this,e,!1)},s.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):m.call(e,t)},s.prototype.listenerCount=m,s.prototype.eventNames=function(){return this._eventsCount>0?r(this._events):[]}},function(e,t,n){"use strict";(function(e){ +/*! + * The buffer module from node.js, for the browser. + * + * @author Feross Aboukhadijeh <feross@feross.org> <http://feross.org> + * @license MIT + */ +var r=n(357),i=n(358),o=n(141);function a(){return c.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function s(e,t){if(a()<t)throw new RangeError("Invalid typed array length");return c.TYPED_ARRAY_SUPPORT?(e=new Uint8Array(t)).__proto__=c.prototype:(null===e&&(e=new c(t)),e.length=t),e}function c(e,t,n){if(!(c.TYPED_ARRAY_SUPPORT||this instanceof c))return new c(e,t,n);if("number"==typeof e){if("string"==typeof t)throw new Error("If encoding is specified then the first argument must be a string");return u(this,e)}return p(this,e,t,n)}function p(e,t,n,r){if("number"==typeof t)throw new TypeError('"value" argument must not be a number');return"undefined"!=typeof ArrayBuffer&&t instanceof ArrayBuffer?function(e,t,n,r){if(t.byteLength,n<0||t.byteLength<n)throw new RangeError("'offset' is out of bounds");if(t.byteLength<n+(r||0))throw new RangeError("'length' is out of bounds");t=void 0===n&&void 0===r?new Uint8Array(t):void 0===r?new Uint8Array(t,n):new Uint8Array(t,n,r);c.TYPED_ARRAY_SUPPORT?(e=t).__proto__=c.prototype:e=l(e,t);return e}(e,t,n,r):"string"==typeof t?function(e,t,n){"string"==typeof n&&""!==n||(n="utf8");if(!c.isEncoding(n))throw new TypeError('"encoding" must be a valid string encoding');var r=0|f(t,n),i=(e=s(e,r)).write(t,n);i!==r&&(e=e.slice(0,i));return e}(e,t,n):function(e,t){if(c.isBuffer(t)){var n=0|m(t.length);return 0===(e=s(e,n)).length?e:(t.copy(e,0,0,n),e)}if(t){if("undefined"!=typeof ArrayBuffer&&t.buffer instanceof ArrayBuffer||"length"in t)return"number"!=typeof t.length||(r=t.length)!=r?s(e,0):l(e,t);if("Buffer"===t.type&&o(t.data))return l(e,t.data)}var r;throw new TypeError("First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.")}(e,t)}function d(e){if("number"!=typeof e)throw new TypeError('"size" argument must be a number');if(e<0)throw new RangeError('"size" argument must not be negative')}function u(e,t){if(d(t),e=s(e,t<0?0:0|m(t)),!c.TYPED_ARRAY_SUPPORT)for(var n=0;n<t;++n)e[n]=0;return e}function l(e,t){var n=t.length<0?0:0|m(t.length);e=s(e,n);for(var r=0;r<n;r+=1)e[r]=255&t[r];return e}function m(e){if(e>=a())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+a().toString(16)+" bytes");return 0|e}function f(e,t){if(c.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var r=!1;;)switch(t){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":case void 0:return U(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return B(e).length;default:if(r)return U(e).length;t=(""+t).toLowerCase(),r=!0}}function h(e,t,n){var r=!1;if((void 0===t||t<0)&&(t=0),t>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(t>>>=0))return"";for(e||(e="utf8");;)switch(e){case"hex":return E(this,t,n);case"utf8":case"utf-8":return k(this,t,n);case"ascii":return O(this,t,n);case"latin1":case"binary":return $(this,t,n);case"base64":return R(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return D(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}function g(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function y(e,t,n,r,i){if(0===e.length)return-1;if("string"==typeof n?(r=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),n=+n,isNaN(n)&&(n=i?0:e.length-1),n<0&&(n=e.length+n),n>=e.length){if(i)return-1;n=e.length-1}else if(n<0){if(!i)return-1;n=0}if("string"==typeof t&&(t=c.from(t,r)),c.isBuffer(t))return 0===t.length?-1:b(e,t,n,r,i);if("number"==typeof t)return t&=255,c.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?i?Uint8Array.prototype.indexOf.call(e,t,n):Uint8Array.prototype.lastIndexOf.call(e,t,n):b(e,[t],n,r,i);throw new TypeError("val must be string, number or Buffer")}function b(e,t,n,r,i){var o,a=1,s=e.length,c=t.length;if(void 0!==r&&("ucs2"===(r=String(r).toLowerCase())||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;a=2,s/=2,c/=2,n/=2}function p(e,t){return 1===a?e[t]:e.readUInt16BE(t*a)}if(i){var d=-1;for(o=n;o<s;o++)if(p(e,o)===p(t,-1===d?0:o-d)){if(-1===d&&(d=o),o-d+1===c)return d*a}else-1!==d&&(o-=o-d),d=-1}else for(n+c>s&&(n=s-c),o=n;o>=0;o--){for(var u=!0,l=0;l<c;l++)if(p(e,o+l)!==p(t,l)){u=!1;break}if(u)return o}return-1}function v(e,t,n,r){n=Number(n)||0;var i=e.length-n;r?(r=Number(r))>i&&(r=i):r=i;var o=t.length;if(o%2!=0)throw new TypeError("Invalid hex string");r>o/2&&(r=o/2);for(var a=0;a<r;++a){var s=parseInt(t.substr(2*a,2),16);if(isNaN(s))return a;e[n+a]=s}return a}function w(e,t,n,r){return W(U(t,e.length-n),e,n,r)}function S(e,t,n,r){return W(function(e){for(var t=[],n=0;n<e.length;++n)t.push(255&e.charCodeAt(n));return t}(t),e,n,r)}function x(e,t,n,r){return S(e,t,n,r)}function I(e,t,n,r){return W(B(t),e,n,r)}function T(e,t,n,r){return W(function(e,t){for(var n,r,i,o=[],a=0;a<e.length&&!((t-=2)<0);++a)n=e.charCodeAt(a),r=n>>8,i=n%256,o.push(i),o.push(r);return o}(t,e.length-n),e,n,r)}function R(e,t,n){return 0===t&&n===e.length?r.fromByteArray(e):r.fromByteArray(e.slice(t,n))}function k(e,t,n){n=Math.min(e.length,n);for(var r=[],i=t;i<n;){var o,a,s,c,p=e[i],d=null,u=p>239?4:p>223?3:p>191?2:1;if(i+u<=n)switch(u){case 1:p<128&&(d=p);break;case 2:128==(192&(o=e[i+1]))&&(c=(31&p)<<6|63&o)>127&&(d=c);break;case 3:o=e[i+1],a=e[i+2],128==(192&o)&&128==(192&a)&&(c=(15&p)<<12|(63&o)<<6|63&a)>2047&&(c<55296||c>57343)&&(d=c);break;case 4:o=e[i+1],a=e[i+2],s=e[i+3],128==(192&o)&&128==(192&a)&&128==(192&s)&&(c=(15&p)<<18|(63&o)<<12|(63&a)<<6|63&s)>65535&&c<1114112&&(d=c)}null===d?(d=65533,u=1):d>65535&&(d-=65536,r.push(d>>>10&1023|55296),d=56320|1023&d),r.push(d),i+=u}return function(e){var t=e.length;if(t<=C)return String.fromCharCode.apply(String,e);var n="",r=0;for(;r<t;)n+=String.fromCharCode.apply(String,e.slice(r,r+=C));return n}(r)}t.Buffer=c,t.SlowBuffer=function(e){+e!=e&&(e=0);return c.alloc(+e)},t.INSPECT_MAX_BYTES=50,c.TYPED_ARRAY_SUPPORT=void 0!==e.TYPED_ARRAY_SUPPORT?e.TYPED_ARRAY_SUPPORT:function(){try{var e=new Uint8Array(1);return e.__proto__={__proto__:Uint8Array.prototype,foo:function(){return 42}},42===e.foo()&&"function"==typeof e.subarray&&0===e.subarray(1,1).byteLength}catch(e){return!1}}(),t.kMaxLength=a(),c.poolSize=8192,c._augment=function(e){return e.__proto__=c.prototype,e},c.from=function(e,t,n){return p(null,e,t,n)},c.TYPED_ARRAY_SUPPORT&&(c.prototype.__proto__=Uint8Array.prototype,c.__proto__=Uint8Array,"undefined"!=typeof Symbol&&Symbol.species&&c[Symbol.species]===c&&Object.defineProperty(c,Symbol.species,{value:null,configurable:!0})),c.alloc=function(e,t,n){return function(e,t,n,r){return d(t),t<=0?s(e,t):void 0!==n?"string"==typeof r?s(e,t).fill(n,r):s(e,t).fill(n):s(e,t)}(null,e,t,n)},c.allocUnsafe=function(e){return u(null,e)},c.allocUnsafeSlow=function(e){return u(null,e)},c.isBuffer=function(e){return!(null==e||!e._isBuffer)},c.compare=function(e,t){if(!c.isBuffer(e)||!c.isBuffer(t))throw new TypeError("Arguments must be Buffers");if(e===t)return 0;for(var n=e.length,r=t.length,i=0,o=Math.min(n,r);i<o;++i)if(e[i]!==t[i]){n=e[i],r=t[i];break}return n<r?-1:r<n?1:0},c.isEncoding=function(e){switch(String(e).toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"latin1":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return!0;default:return!1}},c.concat=function(e,t){if(!o(e))throw new TypeError('"list" argument must be an Array of Buffers');if(0===e.length)return c.alloc(0);var n;if(void 0===t)for(t=0,n=0;n<e.length;++n)t+=e[n].length;var r=c.allocUnsafe(t),i=0;for(n=0;n<e.length;++n){var a=e[n];if(!c.isBuffer(a))throw new TypeError('"list" argument must be an Array of Buffers');a.copy(r,i),i+=a.length}return r},c.byteLength=f,c.prototype._isBuffer=!0,c.prototype.swap16=function(){var e=this.length;if(e%2!=0)throw new RangeError("Buffer size must be a multiple of 16-bits");for(var t=0;t<e;t+=2)g(this,t,t+1);return this},c.prototype.swap32=function(){var e=this.length;if(e%4!=0)throw new RangeError("Buffer size must be a multiple of 32-bits");for(var t=0;t<e;t+=4)g(this,t,t+3),g(this,t+1,t+2);return this},c.prototype.swap64=function(){var e=this.length;if(e%8!=0)throw new RangeError("Buffer size must be a multiple of 64-bits");for(var t=0;t<e;t+=8)g(this,t,t+7),g(this,t+1,t+6),g(this,t+2,t+5),g(this,t+3,t+4);return this},c.prototype.toString=function(){var e=0|this.length;return 0===e?"":0===arguments.length?k(this,0,e):h.apply(this,arguments)},c.prototype.equals=function(e){if(!c.isBuffer(e))throw new TypeError("Argument must be a Buffer");return this===e||0===c.compare(this,e)},c.prototype.inspect=function(){var e="",n=t.INSPECT_MAX_BYTES;return this.length>0&&(e=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(e+=" ... ")),"<Buffer "+e+">"},c.prototype.compare=function(e,t,n,r,i){if(!c.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===i&&(i=this.length),t<0||n>e.length||r<0||i>this.length)throw new RangeError("out of range index");if(r>=i&&t>=n)return 0;if(r>=i)return-1;if(t>=n)return 1;if(this===e)return 0;for(var o=(i>>>=0)-(r>>>=0),a=(n>>>=0)-(t>>>=0),s=Math.min(o,a),p=this.slice(r,i),d=e.slice(t,n),u=0;u<s;++u)if(p[u]!==d[u]){o=p[u],a=d[u];break}return o<a?-1:a<o?1:0},c.prototype.includes=function(e,t,n){return-1!==this.indexOf(e,t,n)},c.prototype.indexOf=function(e,t,n){return y(this,e,t,n,!0)},c.prototype.lastIndexOf=function(e,t,n){return y(this,e,t,n,!1)},c.prototype.write=function(e,t,n,r){if(void 0===t)r="utf8",n=this.length,t=0;else if(void 0===n&&"string"==typeof t)r=t,n=this.length,t=0;else{if(!isFinite(t))throw new Error("Buffer.write(string, encoding, offset[, length]) is no longer supported");t|=0,isFinite(n)?(n|=0,void 0===r&&(r="utf8")):(r=n,n=void 0)}var i=this.length-t;if((void 0===n||n>i)&&(n=i),e.length>0&&(n<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var o=!1;;)switch(r){case"hex":return v(this,e,t,n);case"utf8":case"utf-8":return w(this,e,t,n);case"ascii":return S(this,e,t,n);case"latin1":case"binary":return x(this,e,t,n);case"base64":return I(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return T(this,e,t,n);default:if(o)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),o=!0}},c.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var C=4096;function O(e,t,n){var r="";n=Math.min(e.length,n);for(var i=t;i<n;++i)r+=String.fromCharCode(127&e[i]);return r}function $(e,t,n){var r="";n=Math.min(e.length,n);for(var i=t;i<n;++i)r+=String.fromCharCode(e[i]);return r}function E(e,t,n){var r=e.length;(!t||t<0)&&(t=0),(!n||n<0||n>r)&&(n=r);for(var i="",o=t;o<n;++o)i+=F(e[o]);return i}function D(e,t,n){for(var r=e.slice(t,n),i="",o=0;o<r.length;o+=2)i+=String.fromCharCode(r[o]+256*r[o+1]);return i}function j(e,t,n){if(e%1!=0||e<0)throw new RangeError("offset is not uint");if(e+t>n)throw new RangeError("Trying to access beyond buffer length")}function P(e,t,n,r,i,o){if(!c.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>i||t<o)throw new RangeError('"value" argument is out of bounds');if(n+r>e.length)throw new RangeError("Index out of range")}function A(e,t,n,r){t<0&&(t=65535+t+1);for(var i=0,o=Math.min(e.length-n,2);i<o;++i)e[n+i]=(t&255<<8*(r?i:1-i))>>>8*(r?i:1-i)}function M(e,t,n,r){t<0&&(t=4294967295+t+1);for(var i=0,o=Math.min(e.length-n,4);i<o;++i)e[n+i]=t>>>8*(r?i:3-i)&255}function N(e,t,n,r,i,o){if(n+r>e.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function _(e,t,n,r,o){return o||N(e,0,n,4),i.write(e,t,n,r,23,4),n+4}function L(e,t,n,r,o){return o||N(e,0,n,8),i.write(e,t,n,r,52,8),n+8}c.prototype.slice=function(e,t){var n,r=this.length;if((e=~~e)<0?(e+=r)<0&&(e=0):e>r&&(e=r),(t=void 0===t?r:~~t)<0?(t+=r)<0&&(t=0):t>r&&(t=r),t<e&&(t=e),c.TYPED_ARRAY_SUPPORT)(n=this.subarray(e,t)).__proto__=c.prototype;else{var i=t-e;n=new c(i,void 0);for(var o=0;o<i;++o)n[o]=this[o+e]}return n},c.prototype.readUIntLE=function(e,t,n){e|=0,t|=0,n||j(e,t,this.length);for(var r=this[e],i=1,o=0;++o<t&&(i*=256);)r+=this[e+o]*i;return r},c.prototype.readUIntBE=function(e,t,n){e|=0,t|=0,n||j(e,t,this.length);for(var r=this[e+--t],i=1;t>0&&(i*=256);)r+=this[e+--t]*i;return r},c.prototype.readUInt8=function(e,t){return t||j(e,1,this.length),this[e]},c.prototype.readUInt16LE=function(e,t){return t||j(e,2,this.length),this[e]|this[e+1]<<8},c.prototype.readUInt16BE=function(e,t){return t||j(e,2,this.length),this[e]<<8|this[e+1]},c.prototype.readUInt32LE=function(e,t){return t||j(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},c.prototype.readUInt32BE=function(e,t){return t||j(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},c.prototype.readIntLE=function(e,t,n){e|=0,t|=0,n||j(e,t,this.length);for(var r=this[e],i=1,o=0;++o<t&&(i*=256);)r+=this[e+o]*i;return r>=(i*=128)&&(r-=Math.pow(2,8*t)),r},c.prototype.readIntBE=function(e,t,n){e|=0,t|=0,n||j(e,t,this.length);for(var r=t,i=1,o=this[e+--r];r>0&&(i*=256);)o+=this[e+--r]*i;return o>=(i*=128)&&(o-=Math.pow(2,8*t)),o},c.prototype.readInt8=function(e,t){return t||j(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},c.prototype.readInt16LE=function(e,t){t||j(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},c.prototype.readInt16BE=function(e,t){t||j(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},c.prototype.readInt32LE=function(e,t){return t||j(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},c.prototype.readInt32BE=function(e,t){return t||j(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},c.prototype.readFloatLE=function(e,t){return t||j(e,4,this.length),i.read(this,e,!0,23,4)},c.prototype.readFloatBE=function(e,t){return t||j(e,4,this.length),i.read(this,e,!1,23,4)},c.prototype.readDoubleLE=function(e,t){return t||j(e,8,this.length),i.read(this,e,!0,52,8)},c.prototype.readDoubleBE=function(e,t){return t||j(e,8,this.length),i.read(this,e,!1,52,8)},c.prototype.writeUIntLE=function(e,t,n,r){(e=+e,t|=0,n|=0,r)||P(this,e,t,n,Math.pow(2,8*n)-1,0);var i=1,o=0;for(this[t]=255&e;++o<n&&(i*=256);)this[t+o]=e/i&255;return t+n},c.prototype.writeUIntBE=function(e,t,n,r){(e=+e,t|=0,n|=0,r)||P(this,e,t,n,Math.pow(2,8*n)-1,0);var i=n-1,o=1;for(this[t+i]=255&e;--i>=0&&(o*=256);)this[t+i]=e/o&255;return t+n},c.prototype.writeUInt8=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,1,255,0),c.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},c.prototype.writeUInt16LE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,2,65535,0),c.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):A(this,e,t,!0),t+2},c.prototype.writeUInt16BE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,2,65535,0),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):A(this,e,t,!1),t+2},c.prototype.writeUInt32LE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,4,4294967295,0),c.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):M(this,e,t,!0),t+4},c.prototype.writeUInt32BE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,4,4294967295,0),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):M(this,e,t,!1),t+4},c.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t|=0,!r){var i=Math.pow(2,8*n-1);P(this,e,t,n,i-1,-i)}var o=0,a=1,s=0;for(this[t]=255&e;++o<n&&(a*=256);)e<0&&0===s&&0!==this[t+o-1]&&(s=1),this[t+o]=(e/a>>0)-s&255;return t+n},c.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t|=0,!r){var i=Math.pow(2,8*n-1);P(this,e,t,n,i-1,-i)}var o=n-1,a=1,s=0;for(this[t+o]=255&e;--o>=0&&(a*=256);)e<0&&0===s&&0!==this[t+o+1]&&(s=1),this[t+o]=(e/a>>0)-s&255;return t+n},c.prototype.writeInt8=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,1,127,-128),c.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[t]=255&e,t+1},c.prototype.writeInt16LE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,2,32767,-32768),c.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):A(this,e,t,!0),t+2},c.prototype.writeInt16BE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,2,32767,-32768),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):A(this,e,t,!1),t+2},c.prototype.writeInt32LE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,4,2147483647,-2147483648),c.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):M(this,e,t,!0),t+4},c.prototype.writeInt32BE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):M(this,e,t,!1),t+4},c.prototype.writeFloatLE=function(e,t,n){return _(this,e,t,!0,n)},c.prototype.writeFloatBE=function(e,t,n){return _(this,e,t,!1,n)},c.prototype.writeDoubleLE=function(e,t,n){return L(this,e,t,!0,n)},c.prototype.writeDoubleBE=function(e,t,n){return L(this,e,t,!1,n)},c.prototype.copy=function(e,t,n,r){if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&r<n&&(r=n),r===n)return 0;if(0===e.length||0===this.length)return 0;if(t<0)throw new RangeError("targetStart out of bounds");if(n<0||n>=this.length)throw new RangeError("sourceStart out of bounds");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-t<r-n&&(r=e.length-t+n);var i,o=r-n;if(this===e&&n<t&&t<r)for(i=o-1;i>=0;--i)e[i+t]=this[i+n];else if(o<1e3||!c.TYPED_ARRAY_SUPPORT)for(i=0;i<o;++i)e[i+t]=this[i+n];else Uint8Array.prototype.set.call(e,this.subarray(n,n+o),t);return o},c.prototype.fill=function(e,t,n,r){if("string"==typeof e){if("string"==typeof t?(r=t,t=0,n=this.length):"string"==typeof n&&(r=n,n=this.length),1===e.length){var i=e.charCodeAt(0);i<256&&(e=i)}if(void 0!==r&&"string"!=typeof r)throw new TypeError("encoding must be a string");if("string"==typeof r&&!c.isEncoding(r))throw new TypeError("Unknown encoding: "+r)}else"number"==typeof e&&(e&=255);if(t<0||this.length<t||this.length<n)throw new RangeError("Out of range index");if(n<=t)return this;var o;if(t>>>=0,n=void 0===n?this.length:n>>>0,e||(e=0),"number"==typeof e)for(o=t;o<n;++o)this[o]=e;else{var a=c.isBuffer(e)?e:U(new c(e,r).toString()),s=a.length;for(o=0;o<n-t;++o)this[o+t]=a[o%s]}return this};var q=/[^+\/0-9A-Za-z-_]/g;function F(e){return e<16?"0"+e.toString(16):e.toString(16)}function U(e,t){var n;t=t||1/0;for(var r=e.length,i=null,o=[],a=0;a<r;++a){if((n=e.charCodeAt(a))>55295&&n<57344){if(!i){if(n>56319){(t-=3)>-1&&o.push(239,191,189);continue}if(a+1===r){(t-=3)>-1&&o.push(239,191,189);continue}i=n;continue}if(n<56320){(t-=3)>-1&&o.push(239,191,189),i=n;continue}n=65536+(i-55296<<10|n-56320)}else i&&(t-=3)>-1&&o.push(239,191,189);if(i=null,n<128){if((t-=1)<0)break;o.push(n)}else if(n<2048){if((t-=2)<0)break;o.push(n>>6|192,63&n|128)}else if(n<65536){if((t-=3)<0)break;o.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;o.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return o}function B(e){return r.toByteArray(function(e){if((e=function(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}(e).replace(q,"")).length<2)return"";for(;e.length%4!=0;)e+="=";return e}(e))}function W(e,t,n,r){for(var i=0;i<r&&!(i+n>=t.length||i>=e.length);++i)t[i+n]=e[i];return i}}).call(this,n(11))},function(e,t,n){(function(e){function n(e){return Object.prototype.toString.call(e)}t.isArray=function(e){return Array.isArray?Array.isArray(e):"[object Array]"===n(e)},t.isBoolean=function(e){return"boolean"==typeof e},t.isNull=function(e){return null===e},t.isNullOrUndefined=function(e){return null==e},t.isNumber=function(e){return"number"==typeof e},t.isString=function(e){return"string"==typeof e},t.isSymbol=function(e){return"symbol"==typeof e},t.isUndefined=function(e){return void 0===e},t.isRegExp=function(e){return"[object RegExp]"===n(e)},t.isObject=function(e){return"object"==typeof e&&null!==e},t.isDate=function(e){return"[object Date]"===n(e)},t.isError=function(e){return"[object Error]"===n(e)||e instanceof Error},t.isFunction=function(e){return"function"==typeof e},t.isPrimitive=function(e){return null===e||"boolean"==typeof e||"number"==typeof e||"string"==typeof e||"symbol"==typeof e||void 0===e},t.isBuffer=e.isBuffer}).call(this,n(57).Buffer)},function(e,t,n){var r=n(16),i=n(6),o=n(38);e.exports=function(e){return function(t,n,a){var s,c=r(t),p=i(c.length),d=o(a,p);if(e&&n!=n){for(;p>d;)if((s=c[d++])!=s)return!0}else for(;p>d;d++)if((e||d in c)&&c[d]===n)return e||d||0;return!e&&-1}}},function(e,t){t.f=Object.getOwnPropertySymbols},function(e,t,n){var r=n(21);e.exports=Array.isArray||function(e){return"Array"==r(e)}},function(e,t,n){var r=n(22),i=n(25);e.exports=function(e){return function(t,n){var o,a,s=String(i(t)),c=r(n),p=s.length;return c<0||c>=p?e?"":void 0:(o=s.charCodeAt(c))<55296||o>56319||c+1===p||(a=s.charCodeAt(c+1))<56320||a>57343?e?s.charAt(c):o:e?s.slice(c,c+2):a-56320+(o-55296<<10)+65536}}},function(e,t,n){var r=n(4),i=n(21),o=n(5)("match");e.exports=function(e){var t;return r(e)&&(void 0!==(t=e[o])?!!t:"RegExp"==i(e))}},function(e,t,n){var r=n(5)("iterator"),i=!1;try{var o=[7][r]();o.return=function(){i=!0},Array.from(o,function(){throw 2})}catch(e){}e.exports=function(e,t){if(!t&&!i)return!1;var n=!1;try{var o=[7],a=o[r]();a.next=function(){return{done:n=!0}},o[r]=function(){return a},e(o)}catch(e){}return n}},function(e,t,n){"use strict";var r=n(47),i=RegExp.prototype.exec;e.exports=function(e,t){var n=e.exec;if("function"==typeof n){var o=n.call(e,t);if("object"!=typeof o)throw new TypeError("RegExp exec method returned something other than an Object or null");return o}if("RegExp"!==r(e))throw new TypeError("RegExp#exec called on incompatible receiver");return i.call(e,t)}},function(e,t,n){"use strict";n(122);var r=n(13),i=n(12),o=n(3),a=n(25),s=n(5),c=n(97),p=s("species"),d=!o(function(){var e=/./;return e.exec=function(){var e=[];return e.groups={a:"7"},e},"7"!=="".replace(e,"$<a>")}),u=function(){var e=/(?:)/,t=e.exec;e.exec=function(){return t.apply(this,arguments)};var n="ab".split(e);return 2===n.length&&"a"===n[0]&&"b"===n[1]}();e.exports=function(e,t,n){var l=s(e),m=!o(function(){var t={};return t[l]=function(){return 7},7!=""[e](t)}),f=m?!o(function(){var t=!1,n=/a/;return n.exec=function(){return t=!0,null},"split"===e&&(n.constructor={},n.constructor[p]=function(){return n}),n[l](""),!t}):void 0;if(!m||!f||"replace"===e&&!d||"split"===e&&!u){var h=/./[l],g=n(a,l,""[e],function(e,t,n,r,i){return t.exec===c?m&&!i?{done:!0,value:h.call(t,n,r)}:{done:!0,value:e.call(n,t,r)}:{done:!1}}),y=g[0],b=g[1];r(String.prototype,e,y),i(RegExp.prototype,l,2==t?function(e,t){return b.call(e,this,t)}:function(e){return b.call(e,this)})}}},function(e,t,n){var r=n(2).navigator;e.exports=r&&r.userAgent||""},function(e,t,n){"use strict";var r=n(2),i=n(0),o=n(13),a=n(44),s=n(32),c=n(43),p=n(42),d=n(4),u=n(3),l=n(64),m=n(46),f=n(83);e.exports=function(e,t,n,h,g,y){var b=r[e],v=b,w=g?"set":"add",S=v&&v.prototype,x={},I=function(e){var t=S[e];o(S,e,"delete"==e?function(e){return!(y&&!d(e))&&t.call(this,0===e?0:e)}:"has"==e?function(e){return!(y&&!d(e))&&t.call(this,0===e?0:e)}:"get"==e?function(e){return y&&!d(e)?void 0:t.call(this,0===e?0:e)}:"add"==e?function(e){return t.call(this,0===e?0:e),this}:function(e,n){return t.call(this,0===e?0:e,n),this})};if("function"==typeof v&&(y||S.forEach&&!u(function(){(new v).entries().next()}))){var T=new v,R=T[w](y?{}:-0,1)!=T,k=u(function(){T.has(1)}),C=l(function(e){new v(e)}),O=!y&&u(function(){for(var e=new v,t=5;t--;)e[w](t,t);return!e.has(-0)});C||((v=t(function(t,n){p(t,v,e);var r=f(new b,t,v);return null!=n&&c(n,g,r[w],r),r})).prototype=S,S.constructor=v),(k||O)&&(I("delete"),I("has"),g&&I("get")),(O||R)&&I(w),y&&S.clear&&delete S.clear}else v=h.getConstructor(t,e,g,w),a(v.prototype,n),s.NEED=!0;return m(v,e),x[e]=v,i(i.G+i.W+i.F*(v!=b),x),y||h.setStrong(v,e,g),v}},function(e,t,n){for(var r,i=n(2),o=n(12),a=n(36),s=a("typed_array"),c=a("view"),p=!(!i.ArrayBuffer||!i.DataView),d=p,u=0,l="Int8Array,Uint8Array,Uint8ClampedArray,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array".split(",");u<9;)(r=i[l[u++]])?(o(r.prototype,s,!0),o(r.prototype,c,!0)):d=!1;e.exports={ABV:p,CONSTR:d,TYPED:s,VIEW:c}},function(e,t,n){"use strict";e.exports=n(31)||!n(3)(function(){var e=Math.random();__defineSetter__.call(null,e,function(){}),delete n(2)[e]})},function(e,t,n){"use strict";var r=n(0);e.exports=function(e){r(r.S,e,{of:function(){for(var e=arguments.length,t=new Array(e);e--;)t[e]=arguments[e];return new this(t)}})}},function(e,t,n){"use strict";var r=n(0),i=n(10),o=n(20),a=n(43);e.exports=function(e){r(r.S,e,{from:function(e){var t,n,r,s,c=arguments[1];return i(this),(t=void 0!==c)&&i(c),null==e?new this:(n=[],t?(r=0,s=o(c,arguments[2],2),a(e,!1,function(e){n.push(s(e,r++))})):a(e,!1,n.push,n),new this(n))}})}},function(e,t,n){"use strict";(function(t){void 0===t||!t.version||0===t.version.indexOf("v0.")||0===t.version.indexOf("v1.")&&0!==t.version.indexOf("v1.8.")?e.exports={nextTick:function(e,n,r,i){if("function"!=typeof e)throw new TypeError('"callback" argument must be a function');var o,a,s=arguments.length;switch(s){case 0:case 1:return t.nextTick(e);case 2:return t.nextTick(function(){e.call(null,n)});case 3:return t.nextTick(function(){e.call(null,n,r)});case 4:return t.nextTick(function(){e.call(null,n,r,i)});default:for(o=new Array(s-1),a=0;a<o.length;)o[a++]=arguments[a];return t.nextTick(function(){e.apply(null,o)})}}}:e.exports=t}).call(this,n(30))},function(e,t,n){var r=n(57),i=r.Buffer;function o(e,t){for(var n in e)t[n]=e[n]}function a(e,t,n){return i(e,t,n)}i.from&&i.alloc&&i.allocUnsafe&&i.allocUnsafeSlow?e.exports=r:(o(r,t),t.Buffer=a),o(i,a),a.from=function(e,t,n){if("number"==typeof e)throw new TypeError("Argument must not be a number");return i(e,t,n)},a.alloc=function(e,t,n){if("number"!=typeof e)throw new TypeError("Argument must be a number");var r=i(e);return void 0!==t?"string"==typeof n?r.fill(t,n):r.fill(t):r.fill(0),r},a.allocUnsafe=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return i(e)},a.allocUnsafeSlow=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return r.SlowBuffer(e)}},function(e,t,n){"use strict";var r=n(369),i=n(371);function o(){this.protocol=null,this.slashes=null,this.auth=null,this.host=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.query=null,this.pathname=null,this.path=null,this.href=null}t.parse=v,t.resolve=function(e,t){return v(e,!1,!0).resolve(t)},t.resolveObject=function(e,t){return e?v(e,!1,!0).resolveObject(t):t},t.format=function(e){i.isString(e)&&(e=v(e));return e instanceof o?e.format():o.prototype.format.call(e)},t.Url=o;var a=/^([a-z0-9.+-]+:)/i,s=/:[0-9]*$/,c=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,p=["{","}","|","\\","^","`"].concat(["<",">",'"',"`"," ","\r","\n","\t"]),d=["'"].concat(p),u=["%","/","?",";","#"].concat(d),l=["/","?","#"],m=/^[+a-z0-9A-Z_-]{0,63}$/,f=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,h={javascript:!0,"javascript:":!0},g={javascript:!0,"javascript:":!0},y={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},b=n(372);function v(e,t,n){if(e&&i.isObject(e)&&e instanceof o)return e;var r=new o;return r.parse(e,t,n),r}o.prototype.parse=function(e,t,n){if(!i.isString(e))throw new TypeError("Parameter 'url' must be a string, not "+typeof e);var o=e.indexOf("?"),s=-1!==o&&o<e.indexOf("#")?"?":"#",p=e.split(s);p[0]=p[0].replace(/\\/g,"/");var v=e=p.join(s);if(v=v.trim(),!n&&1===e.split("#").length){var w=c.exec(v);if(w)return this.path=v,this.href=v,this.pathname=w[1],w[2]?(this.search=w[2],this.query=t?b.parse(this.search.substr(1)):this.search.substr(1)):t&&(this.search="",this.query={}),this}var S=a.exec(v);if(S){var x=(S=S[0]).toLowerCase();this.protocol=x,v=v.substr(S.length)}if(n||S||v.match(/^\/\/[^@\/]+@[^@\/]+/)){var I="//"===v.substr(0,2);!I||S&&g[S]||(v=v.substr(2),this.slashes=!0)}if(!g[S]&&(I||S&&!y[S])){for(var T,R,k=-1,C=0;C<l.length;C++){-1!==(O=v.indexOf(l[C]))&&(-1===k||O<k)&&(k=O)}-1!==(R=-1===k?v.lastIndexOf("@"):v.lastIndexOf("@",k))&&(T=v.slice(0,R),v=v.slice(R+1),this.auth=decodeURIComponent(T)),k=-1;for(C=0;C<u.length;C++){var O;-1!==(O=v.indexOf(u[C]))&&(-1===k||O<k)&&(k=O)}-1===k&&(k=v.length),this.host=v.slice(0,k),v=v.slice(k),this.parseHost(),this.hostname=this.hostname||"";var $="["===this.hostname[0]&&"]"===this.hostname[this.hostname.length-1];if(!$)for(var E=this.hostname.split(/\./),D=(C=0,E.length);C<D;C++){var j=E[C];if(j&&!j.match(m)){for(var P="",A=0,M=j.length;A<M;A++)j.charCodeAt(A)>127?P+="x":P+=j[A];if(!P.match(m)){var N=E.slice(0,C),_=E.slice(C+1),L=j.match(f);L&&(N.push(L[1]),_.unshift(L[2])),_.length&&(v="/"+_.join(".")+v),this.hostname=N.join(".");break}}}this.hostname.length>255?this.hostname="":this.hostname=this.hostname.toLowerCase(),$||(this.hostname=r.toASCII(this.hostname));var q=this.port?":"+this.port:"",F=this.hostname||"";this.host=F+q,this.href+=this.host,$&&(this.hostname=this.hostname.substr(1,this.hostname.length-2),"/"!==v[0]&&(v="/"+v))}if(!h[x])for(C=0,D=d.length;C<D;C++){var U=d[C];if(-1!==v.indexOf(U)){var B=encodeURIComponent(U);B===U&&(B=escape(U)),v=v.split(U).join(B)}}var W=v.indexOf("#");-1!==W&&(this.hash=v.substr(W),v=v.slice(0,W));var H=v.indexOf("?");if(-1!==H?(this.search=v.substr(H),this.query=v.substr(H+1),t&&(this.query=b.parse(this.query)),v=v.slice(0,H)):t&&(this.search="",this.query={}),v&&(this.pathname=v),y[x]&&this.hostname&&!this.pathname&&(this.pathname="/"),this.pathname||this.search){q=this.pathname||"";var z=this.search||"";this.path=q+z}return this.href=this.format(),this},o.prototype.format=function(){var e=this.auth||"";e&&(e=(e=encodeURIComponent(e)).replace(/%3A/i,":"),e+="@");var t=this.protocol||"",n=this.pathname||"",r=this.hash||"",o=!1,a="";this.host?o=e+this.host:this.hostname&&(o=e+(-1===this.hostname.indexOf(":")?this.hostname:"["+this.hostname+"]"),this.port&&(o+=":"+this.port)),this.query&&i.isObject(this.query)&&Object.keys(this.query).length&&(a=b.stringify(this.query));var s=this.search||a&&"?"+a||"";return t&&":"!==t.substr(-1)&&(t+=":"),this.slashes||(!t||y[t])&&!1!==o?(o="//"+(o||""),n&&"/"!==n.charAt(0)&&(n="/"+n)):o||(o=""),r&&"#"!==r.charAt(0)&&(r="#"+r),s&&"?"!==s.charAt(0)&&(s="?"+s),t+o+(n=n.replace(/[?#]/g,function(e){return encodeURIComponent(e)}))+(s=s.replace("#","%23"))+r},o.prototype.resolve=function(e){return this.resolveObject(v(e,!1,!0)).format()},o.prototype.resolveObject=function(e){if(i.isString(e)){var t=new o;t.parse(e,!1,!0),e=t}for(var n=new o,r=Object.keys(this),a=0;a<r.length;a++){var s=r[a];n[s]=this[s]}if(n.hash=e.hash,""===e.href)return n.href=n.format(),n;if(e.slashes&&!e.protocol){for(var c=Object.keys(e),p=0;p<c.length;p++){var d=c[p];"protocol"!==d&&(n[d]=e[d])}return y[n.protocol]&&n.hostname&&!n.pathname&&(n.path=n.pathname="/"),n.href=n.format(),n}if(e.protocol&&e.protocol!==n.protocol){if(!y[e.protocol]){for(var u=Object.keys(e),l=0;l<u.length;l++){var m=u[l];n[m]=e[m]}return n.href=n.format(),n}if(n.protocol=e.protocol,e.host||g[e.protocol])n.pathname=e.pathname;else{for(var f=(e.pathname||"").split("/");f.length&&!(e.host=f.shift()););e.host||(e.host=""),e.hostname||(e.hostname=""),""!==f[0]&&f.unshift(""),f.length<2&&f.unshift(""),n.pathname=f.join("/")}if(n.search=e.search,n.query=e.query,n.host=e.host||"",n.auth=e.auth,n.hostname=e.hostname||e.host,n.port=e.port,n.pathname||n.search){var h=n.pathname||"",b=n.search||"";n.path=h+b}return n.slashes=n.slashes||e.slashes,n.href=n.format(),n}var v=n.pathname&&"/"===n.pathname.charAt(0),w=e.host||e.pathname&&"/"===e.pathname.charAt(0),S=w||v||n.host&&e.pathname,x=S,I=n.pathname&&n.pathname.split("/")||[],T=(f=e.pathname&&e.pathname.split("/")||[],n.protocol&&!y[n.protocol]);if(T&&(n.hostname="",n.port=null,n.host&&(""===I[0]?I[0]=n.host:I.unshift(n.host)),n.host="",e.protocol&&(e.hostname=null,e.port=null,e.host&&(""===f[0]?f[0]=e.host:f.unshift(e.host)),e.host=null),S=S&&(""===f[0]||""===I[0])),w)n.host=e.host||""===e.host?e.host:n.host,n.hostname=e.hostname||""===e.hostname?e.hostname:n.hostname,n.search=e.search,n.query=e.query,I=f;else if(f.length)I||(I=[]),I.pop(),I=I.concat(f),n.search=e.search,n.query=e.query;else if(!i.isNullOrUndefined(e.search)){if(T)n.hostname=n.host=I.shift(),($=!!(n.host&&n.host.indexOf("@")>0)&&n.host.split("@"))&&(n.auth=$.shift(),n.host=n.hostname=$.shift());return n.search=e.search,n.query=e.query,i.isNull(n.pathname)&&i.isNull(n.search)||(n.path=(n.pathname?n.pathname:"")+(n.search?n.search:"")),n.href=n.format(),n}if(!I.length)return n.pathname=null,n.search?n.path="/"+n.search:n.path=null,n.href=n.format(),n;for(var R=I.slice(-1)[0],k=(n.host||e.host||I.length>1)&&("."===R||".."===R)||""===R,C=0,O=I.length;O>=0;O--)"."===(R=I[O])?I.splice(O,1):".."===R?(I.splice(O,1),C++):C&&(I.splice(O,1),C--);if(!S&&!x)for(;C--;C)I.unshift("..");!S||""===I[0]||I[0]&&"/"===I[0].charAt(0)||I.unshift(""),k&&"/"!==I.join("/").substr(-1)&&I.push("");var $,E=""===I[0]||I[0]&&"/"===I[0].charAt(0);T&&(n.hostname=n.host=E?"":I.length?I.shift():"",($=!!(n.host&&n.host.indexOf("@")>0)&&n.host.split("@"))&&(n.auth=$.shift(),n.host=n.hostname=$.shift()));return(S=S||n.host&&I.length)&&!E&&I.unshift(""),I.length?n.pathname=I.join("/"):(n.pathname=null,n.path=null),i.isNull(n.pathname)&&i.isNull(n.search)||(n.path=(n.pathname?n.pathname:"")+(n.search?n.search:"")),n.auth=e.auth||n.auth,n.slashes=n.slashes||e.slashes,n.href=n.format(),n},o.prototype.parseHost=function(){var e=this.host,t=s.exec(e);t&&(":"!==(t=t[0])&&(this.port=t.substr(1)),e=e.substr(0,e.length-t.length)),e&&(this.hostname=e)}},function(e,t,n){var r=n(4),i=n(2).document,o=r(i)&&r(i.createElement);e.exports=function(e){return o?i.createElement(e):{}}},function(e,t,n){var r=n(2),i=n(19),o=n(31),a=n(104),s=n(8).f;e.exports=function(e){var t=i.Symbol||(i.Symbol=o?{}:r.Symbol||{});"_"==e.charAt(0)||e in t||s(t,e,{value:a.f(e)})}},function(e,t,n){var r=n(51)("keys"),i=n(36);e.exports=function(e){return r[e]||(r[e]=i(e))}},function(e,t){e.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(e,t,n){var r=n(2).document;e.exports=r&&r.documentElement},function(e,t,n){var r=n(4),i=n(1),o=function(e,t){if(i(e),!r(t)&&null!==t)throw TypeError(t+": can't set as prototype!")};e.exports={set:Object.setPrototypeOf||("__proto__"in{}?function(e,t,r){try{(r=n(20)(Function.call,n(17).f(Object.prototype,"__proto__").set,2))(e,[]),t=!(e instanceof Array)}catch(e){t=!0}return function(e,n){return o(e,n),t?e.__proto__=n:r(e,n),e}}({},!1):void 0),check:o}},function(e,t){e.exports="\t\n\v\f\r \u2028\u2029\ufeff"},function(e,t,n){var r=n(4),i=n(81).set;e.exports=function(e,t,n){var o,a=t.constructor;return a!==n&&"function"==typeof a&&(o=a.prototype)!==n.prototype&&r(o)&&i&&i(e,o),e}},function(e,t,n){"use strict";var r=n(22),i=n(25);e.exports=function(e){var t=String(i(this)),n="",o=r(e);if(o<0||o==1/0)throw RangeError("Count can't be negative");for(;o>0;(o>>>=1)&&(t+=t))1&o&&(n+=t);return n}},function(e,t){e.exports=Math.sign||function(e){return 0==(e=+e)||e!=e?e:e<0?-1:1}},function(e,t){var n=Math.expm1;e.exports=!n||n(10)>22025.465794806718||n(10)<22025.465794806718||-2e-17!=n(-2e-17)?function(e){return 0==(e=+e)?e:e>-1e-6&&e<1e-6?e+e*e/2:Math.exp(e)-1}:n},function(e,t,n){"use strict";var r=n(31),i=n(0),o=n(13),a=n(12),s=n(49),c=n(88),p=n(46),d=n(18),u=n(5)("iterator"),l=!([].keys&&"next"in[].keys()),m=function(){return this};e.exports=function(e,t,n,f,h,g,y){c(n,t,f);var b,v,w,S=function(e){if(!l&&e in R)return R[e];switch(e){case"keys":case"values":return function(){return new n(this,e)}}return function(){return new n(this,e)}},x=t+" Iterator",I="values"==h,T=!1,R=e.prototype,k=R[u]||R["@@iterator"]||h&&R[h],C=k||S(h),O=h?I?S("entries"):C:void 0,$="Array"==t&&R.entries||k;if($&&(w=d($.call(new e)))!==Object.prototype&&w.next&&(p(w,x,!0),r||"function"==typeof w[u]||a(w,u,m)),I&&k&&"values"!==k.name&&(T=!0,C=function(){return k.call(this)}),r&&!y||!l&&!T&&R[u]||a(R,u,C),s[t]=C,s[x]=m,h)if(b={values:I?C:S("values"),keys:g?C:S("keys"),entries:O},y)for(v in b)v in R||o(R,v,b[v]);else i(i.P+i.F*(l||T),t,b);return b}},function(e,t,n){"use strict";var r=n(39),i=n(35),o=n(46),a={};n(12)(a,n(5)("iterator"),function(){return this}),e.exports=function(e,t,n){e.prototype=r(a,{next:i(1,n)}),o(e,t+" Iterator")}},function(e,t,n){var r=n(63),i=n(25);e.exports=function(e,t,n){if(r(t))throw TypeError("String#"+n+" doesn't accept regex!");return String(i(e))}},function(e,t,n){var r=n(5)("match");e.exports=function(e){var t=/./;try{"/./"[e](t)}catch(n){try{return t[r]=!1,!"/./"[e](t)}catch(e){}}return!0}},function(e,t,n){var r=n(49),i=n(5)("iterator"),o=Array.prototype;e.exports=function(e){return void 0!==e&&(r.Array===e||o[i]===e)}},function(e,t,n){"use strict";var r=n(8),i=n(35);e.exports=function(e,t,n){t in e?r.f(e,t,i(0,n)):e[t]=n}},function(e,t,n){var r=n(47),i=n(5)("iterator"),o=n(49);e.exports=n(19).getIteratorMethod=function(e){if(null!=e)return e[i]||e["@@iterator"]||o[r(e)]}},function(e,t,n){var r=n(245);e.exports=function(e,t){return new(r(e))(t)}},function(e,t,n){"use strict";var r=n(9),i=n(38),o=n(6);e.exports=function(e){for(var t=r(this),n=o(t.length),a=arguments.length,s=i(a>1?arguments[1]:void 0,n),c=a>2?arguments[2]:void 0,p=void 0===c?n:i(c,n);p>s;)t[s++]=e;return t}},function(e,t,n){"use strict";var r=n(33),i=n(121),o=n(49),a=n(16);e.exports=n(87)(Array,"Array",function(e,t){this._t=a(e),this._i=0,this._k=t},function(){var e=this._t,t=this._k,n=this._i++;return!e||n>=e.length?(this._t=void 0,i(1)):i(0,"keys"==t?n:"values"==t?e[n]:[n,e[n]])},"values"),o.Arguments=o.Array,r("keys"),r("values"),r("entries")},function(e,t,n){"use strict";var r,i,o=n(54),a=RegExp.prototype.exec,s=String.prototype.replace,c=a,p=(r=/a/,i=/b*/g,a.call(r,"a"),a.call(i,"a"),0!==r.lastIndex||0!==i.lastIndex),d=void 0!==/()??/.exec("")[1];(p||d)&&(c=function(e){var t,n,r,i,c=this;return d&&(n=new RegExp("^"+c.source+"$(?!\\s)",o.call(c))),p&&(t=c.lastIndex),r=a.call(c,e),p&&r&&(c.lastIndex=c.global?r.index+r[0].length:t),d&&r&&r.length>1&&s.call(r[0],n,function(){for(i=1;i<arguments.length-2;i++)void 0===arguments[i]&&(r[i]=void 0)}),r}),e.exports=c},function(e,t,n){"use strict";var r=n(62)(!0);e.exports=function(e,t,n){return t+(n?r(e,t).length:1)}},function(e,t,n){var r,i,o,a=n(20),s=n(111),c=n(80),p=n(76),d=n(2),u=d.process,l=d.setImmediate,m=d.clearImmediate,f=d.MessageChannel,h=d.Dispatch,g=0,y={},b=function(){var e=+this;if(y.hasOwnProperty(e)){var t=y[e];delete y[e],t()}},v=function(e){b.call(e.data)};l&&m||(l=function(e){for(var t=[],n=1;arguments.length>n;)t.push(arguments[n++]);return y[++g]=function(){s("function"==typeof e?e:Function(e),t)},r(g),g},m=function(e){delete y[e]},"process"==n(21)(u)?r=function(e){u.nextTick(a(b,e,1))}:h&&h.now?r=function(e){h.now(a(b,e,1))}:f?(o=(i=new f).port2,i.port1.onmessage=v,r=a(o.postMessage,o,1)):d.addEventListener&&"function"==typeof postMessage&&!d.importScripts?(r=function(e){d.postMessage(e+"","*")},d.addEventListener("message",v,!1)):r="onreadystatechange"in p("script")?function(e){c.appendChild(p("script")).onreadystatechange=function(){c.removeChild(this),b.call(e)}}:function(e){setTimeout(a(b,e,1),0)}),e.exports={set:l,clear:m}},function(e,t,n){var r=n(2),i=n(99).set,o=r.MutationObserver||r.WebKitMutationObserver,a=r.process,s=r.Promise,c="process"==n(21)(a);e.exports=function(){var e,t,n,p=function(){var r,i;for(c&&(r=a.domain)&&r.exit();e;){i=e.fn,e=e.next;try{i()}catch(r){throw e?n():t=void 0,r}}t=void 0,r&&r.enter()};if(c)n=function(){a.nextTick(p)};else if(!o||r.navigator&&r.navigator.standalone)if(s&&s.resolve){var d=s.resolve(void 0);n=function(){d.then(p)}}else n=function(){i.call(r,p)};else{var u=!0,l=document.createTextNode("");new o(p).observe(l,{characterData:!0}),n=function(){l.data=u=!u}}return function(r){var i={fn:r,next:void 0};t&&(t.next=i),e||(e=i,n()),t=i}}},function(e,t,n){"use strict";var r=n(10);function i(e){var t,n;this.promise=new e(function(e,r){if(void 0!==t||void 0!==n)throw TypeError("Bad Promise constructor");t=e,n=r}),this.resolve=r(t),this.reject=r(n)}e.exports.f=function(e){return new i(e)}},function(e,t,n){"use strict";var r=n(2),i=n(7),o=n(31),a=n(69),s=n(12),c=n(44),p=n(3),d=n(42),u=n(22),l=n(6),m=n(131),f=n(40).f,h=n(8).f,g=n(95),y=n(46),b="prototype",v="Wrong index!",w=r.ArrayBuffer,S=r.DataView,x=r.Math,I=r.RangeError,T=r.Infinity,R=w,k=x.abs,C=x.pow,O=x.floor,$=x.log,E=x.LN2,D=i?"_b":"buffer",j=i?"_l":"byteLength",P=i?"_o":"byteOffset";function A(e,t,n){var r,i,o,a=new Array(n),s=8*n-t-1,c=(1<<s)-1,p=c>>1,d=23===t?C(2,-24)-C(2,-77):0,u=0,l=e<0||0===e&&1/e<0?1:0;for((e=k(e))!=e||e===T?(i=e!=e?1:0,r=c):(r=O($(e)/E),e*(o=C(2,-r))<1&&(r--,o*=2),(e+=r+p>=1?d/o:d*C(2,1-p))*o>=2&&(r++,o/=2),r+p>=c?(i=0,r=c):r+p>=1?(i=(e*o-1)*C(2,t),r+=p):(i=e*C(2,p-1)*C(2,t),r=0));t>=8;a[u++]=255&i,i/=256,t-=8);for(r=r<<t|i,s+=t;s>0;a[u++]=255&r,r/=256,s-=8);return a[--u]|=128*l,a}function M(e,t,n){var r,i=8*n-t-1,o=(1<<i)-1,a=o>>1,s=i-7,c=n-1,p=e[c--],d=127&p;for(p>>=7;s>0;d=256*d+e[c],c--,s-=8);for(r=d&(1<<-s)-1,d>>=-s,s+=t;s>0;r=256*r+e[c],c--,s-=8);if(0===d)d=1-a;else{if(d===o)return r?NaN:p?-T:T;r+=C(2,t),d-=a}return(p?-1:1)*r*C(2,d-t)}function N(e){return e[3]<<24|e[2]<<16|e[1]<<8|e[0]}function _(e){return[255&e]}function L(e){return[255&e,e>>8&255]}function q(e){return[255&e,e>>8&255,e>>16&255,e>>24&255]}function F(e){return A(e,52,8)}function U(e){return A(e,23,4)}function B(e,t,n){h(e[b],t,{get:function(){return this[n]}})}function W(e,t,n,r){var i=m(+n);if(i+t>e[j])throw I(v);var o=e[D]._b,a=i+e[P],s=o.slice(a,a+t);return r?s:s.reverse()}function H(e,t,n,r,i,o){var a=m(+n);if(a+t>e[j])throw I(v);for(var s=e[D]._b,c=a+e[P],p=r(+i),d=0;d<t;d++)s[c+d]=p[o?d:t-d-1]}if(a.ABV){if(!p(function(){w(1)})||!p(function(){new w(-1)})||p(function(){return new w,new w(1.5),new w(NaN),"ArrayBuffer"!=w.name})){for(var z,V=(w=function(e){return d(this,w),new R(m(e))})[b]=R[b],G=f(R),J=0;G.length>J;)(z=G[J++])in w||s(w,z,R[z]);o||(V.constructor=w)}var X=new S(new w(2)),Y=S[b].setInt8;X.setInt8(0,2147483648),X.setInt8(1,2147483649),!X.getInt8(0)&&X.getInt8(1)||c(S[b],{setInt8:function(e,t){Y.call(this,e,t<<24>>24)},setUint8:function(e,t){Y.call(this,e,t<<24>>24)}},!0)}else w=function(e){d(this,w,"ArrayBuffer");var t=m(e);this._b=g.call(new Array(t),0),this[j]=t},S=function(e,t,n){d(this,S,"DataView"),d(e,w,"DataView");var r=e[j],i=u(t);if(i<0||i>r)throw I("Wrong offset!");if(i+(n=void 0===n?r-i:l(n))>r)throw I("Wrong length!");this[D]=e,this[P]=i,this[j]=n},i&&(B(w,"byteLength","_l"),B(S,"buffer","_b"),B(S,"byteLength","_l"),B(S,"byteOffset","_o")),c(S[b],{getInt8:function(e){return W(this,1,e)[0]<<24>>24},getUint8:function(e){return W(this,1,e)[0]},getInt16:function(e){var t=W(this,2,e,arguments[1]);return(t[1]<<8|t[0])<<16>>16},getUint16:function(e){var t=W(this,2,e,arguments[1]);return t[1]<<8|t[0]},getInt32:function(e){return N(W(this,4,e,arguments[1]))},getUint32:function(e){return N(W(this,4,e,arguments[1]))>>>0},getFloat32:function(e){return M(W(this,4,e,arguments[1]),23,4)},getFloat64:function(e){return M(W(this,8,e,arguments[1]),52,8)},setInt8:function(e,t){H(this,1,e,_,t)},setUint8:function(e,t){H(this,1,e,_,t)},setInt16:function(e,t){H(this,2,e,L,t,arguments[2])},setUint16:function(e,t){H(this,2,e,L,t,arguments[2])},setInt32:function(e,t){H(this,4,e,q,t,arguments[2])},setUint32:function(e,t){H(this,4,e,q,t,arguments[2])},setFloat32:function(e,t){H(this,4,e,U,t,arguments[2])},setFloat64:function(e,t){H(this,8,e,F,t,arguments[2])}});y(w,"ArrayBuffer"),y(S,"DataView"),s(S[b],a.VIEW,!0),t.ArrayBuffer=w,t.DataView=S},function(e,t,n){e.exports=!n(7)&&!n(3)(function(){return 7!=Object.defineProperty(n(76)("div"),"a",{get:function(){return 7}}).a})},function(e,t,n){t.f=n(5)},function(e,t,n){var r=n(15),i=n(16),o=n(59)(!1),a=n(78)("IE_PROTO");e.exports=function(e,t){var n,s=i(e),c=0,p=[];for(n in s)n!=a&&r(s,n)&&p.push(n);for(;t.length>c;)r(s,n=t[c++])&&(~o(p,n)||p.push(n));return p}},function(e,t,n){var r=n(8),i=n(1),o=n(37);e.exports=n(7)?Object.defineProperties:function(e,t){i(e);for(var n,a=o(t),s=a.length,c=0;s>c;)r.f(e,n=a[c++],t[n]);return e}},function(e,t,n){var r=n(16),i=n(40).f,o={}.toString,a="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[];e.exports.f=function(e){return a&&"[object Window]"==o.call(e)?function(e){try{return i(e)}catch(e){return a.slice()}}(e):i(r(e))}},function(e,t,n){"use strict";var r=n(7),i=n(37),o=n(60),a=n(53),s=n(9),c=n(52),p=Object.assign;e.exports=!p||n(3)(function(){var e={},t={},n=Symbol(),r="abcdefghijklmnopqrst";return e[n]=7,r.split("").forEach(function(e){t[e]=e}),7!=p({},e)[n]||Object.keys(p({},t)).join("")!=r})?function(e,t){for(var n=s(e),p=arguments.length,d=1,u=o.f,l=a.f;p>d;)for(var m,f=c(arguments[d++]),h=u?i(f).concat(u(f)):i(f),g=h.length,y=0;g>y;)m=h[y++],r&&!l.call(f,m)||(n[m]=f[m]);return n}:p},function(e,t){e.exports=Object.is||function(e,t){return e===t?0!==e||1/e==1/t:e!=e&&t!=t}},function(e,t,n){"use strict";var r=n(10),i=n(4),o=n(111),a=[].slice,s={},c=function(e,t,n){if(!(t in s)){for(var r=[],i=0;i<t;i++)r[i]="a["+i+"]";s[t]=Function("F,a","return new F("+r.join(",")+")")}return s[t](e,n)};e.exports=Function.bind||function(e){var t=r(this),n=a.call(arguments,1),s=function(){var r=n.concat(a.call(arguments));return this instanceof s?c(t,r.length,r):o(t,r,e)};return i(t.prototype)&&(s.prototype=t.prototype),s}},function(e,t){e.exports=function(e,t,n){var r=void 0===n;switch(t.length){case 0:return r?e():e.call(n);case 1:return r?e(t[0]):e.call(n,t[0]);case 2:return r?e(t[0],t[1]):e.call(n,t[0],t[1]);case 3:return r?e(t[0],t[1],t[2]):e.call(n,t[0],t[1],t[2]);case 4:return r?e(t[0],t[1],t[2],t[3]):e.call(n,t[0],t[1],t[2],t[3])}return e.apply(n,t)}},function(e,t,n){var r=n(2).parseInt,i=n(48).trim,o=n(82),a=/^[-+]?0[xX]/;e.exports=8!==r(o+"08")||22!==r(o+"0x16")?function(e,t){var n=i(String(e),3);return r(n,t>>>0||(a.test(n)?16:10))}:r},function(e,t,n){var r=n(2).parseFloat,i=n(48).trim;e.exports=1/r(n(82)+"-0")!=-1/0?function(e){var t=i(String(e),3),n=r(t);return 0===n&&"-"==t.charAt(0)?-0:n}:r},function(e,t,n){var r=n(21);e.exports=function(e,t){if("number"!=typeof e&&"Number"!=r(e))throw TypeError(t);return+e}},function(e,t,n){var r=n(4),i=Math.floor;e.exports=function(e){return!r(e)&&isFinite(e)&&i(e)===e}},function(e,t){e.exports=Math.log1p||function(e){return(e=+e)>-1e-8&&e<1e-8?e-e*e/2:Math.log(1+e)}},function(e,t,n){var r=n(85),i=Math.pow,o=i(2,-52),a=i(2,-23),s=i(2,127)*(2-a),c=i(2,-126);e.exports=Math.fround||function(e){var t,n,i=Math.abs(e),p=r(e);return i<c?p*(i/c/a+1/o-1/o)*c*a:(n=(t=(1+a/o)*i)-(t-i))>s||n!=n?p*(1/0):p*n}},function(e,t,n){var r=n(1);e.exports=function(e,t,n,i){try{return i?t(r(n)[0],n[1]):t(n)}catch(t){var o=e.return;throw void 0!==o&&r(o.call(e)),t}}},function(e,t,n){var r=n(10),i=n(9),o=n(52),a=n(6);e.exports=function(e,t,n,s,c){r(t);var p=i(e),d=o(p),u=a(p.length),l=c?u-1:0,m=c?-1:1;if(n<2)for(;;){if(l in d){s=d[l],l+=m;break}if(l+=m,c?l<0:u<=l)throw TypeError("Reduce of empty array with no initial value")}for(;c?l>=0:u>l;l+=m)l in d&&(s=t(s,d[l],l,p));return s}},function(e,t,n){"use strict";var r=n(9),i=n(38),o=n(6);e.exports=[].copyWithin||function(e,t){var n=r(this),a=o(n.length),s=i(e,a),c=i(t,a),p=arguments.length>2?arguments[2]:void 0,d=Math.min((void 0===p?a:i(p,a))-c,a-s),u=1;for(c<s&&s<c+d&&(u=-1,c+=d-1,s+=d-1);d-- >0;)c in n?n[s]=n[c]:delete n[s],s+=u,c+=u;return n}},function(e,t){e.exports=function(e,t){return{value:t,done:!!e}}},function(e,t,n){"use strict";var r=n(97);n(0)({target:"RegExp",proto:!0,forced:r!==/./.exec},{exec:r})},function(e,t,n){n(7)&&"g"!=/./g.flags&&n(8).f(RegExp.prototype,"flags",{configurable:!0,get:n(54)})},function(e,t){e.exports=function(e){try{return{e:!1,v:e()}}catch(e){return{e:!0,v:e}}}},function(e,t,n){var r=n(1),i=n(4),o=n(101);e.exports=function(e,t){if(r(e),i(t)&&t.constructor===e)return t;var n=o.f(e);return(0,n.resolve)(t),n.promise}},function(e,t,n){"use strict";var r=n(127),i=n(45);e.exports=n(68)("Map",function(e){return function(){return e(this,arguments.length>0?arguments[0]:void 0)}},{get:function(e){var t=r.getEntry(i(this,"Map"),e);return t&&t.v},set:function(e,t){return r.def(i(this,"Map"),0===e?0:e,t)}},r,!0)},function(e,t,n){"use strict";var r=n(8).f,i=n(39),o=n(44),a=n(20),s=n(42),c=n(43),p=n(87),d=n(121),u=n(41),l=n(7),m=n(32).fastKey,f=n(45),h=l?"_s":"size",g=function(e,t){var n,r=m(t);if("F"!==r)return e._i[r];for(n=e._f;n;n=n.n)if(n.k==t)return n};e.exports={getConstructor:function(e,t,n,p){var d=e(function(e,r){s(e,d,t,"_i"),e._t=t,e._i=i(null),e._f=void 0,e._l=void 0,e[h]=0,null!=r&&c(r,n,e[p],e)});return o(d.prototype,{clear:function(){for(var e=f(this,t),n=e._i,r=e._f;r;r=r.n)r.r=!0,r.p&&(r.p=r.p.n=void 0),delete n[r.i];e._f=e._l=void 0,e[h]=0},delete:function(e){var n=f(this,t),r=g(n,e);if(r){var i=r.n,o=r.p;delete n._i[r.i],r.r=!0,o&&(o.n=i),i&&(i.p=o),n._f==r&&(n._f=i),n._l==r&&(n._l=o),n[h]--}return!!r},forEach:function(e){f(this,t);for(var n,r=a(e,arguments.length>1?arguments[1]:void 0,3);n=n?n.n:this._f;)for(r(n.v,n.k,this);n&&n.r;)n=n.p},has:function(e){return!!g(f(this,t),e)}}),l&&r(d.prototype,"size",{get:function(){return f(this,t)[h]}}),d},def:function(e,t,n){var r,i,o=g(e,t);return o?o.v=n:(e._l=o={i:i=m(t,!0),k:t,v:n,p:r=e._l,n:void 0,r:!1},e._f||(e._f=o),r&&(r.n=o),e[h]++,"F"!==i&&(e._i[i]=o)),e},getEntry:g,setStrong:function(e,t,n){p(e,t,function(e,n){this._t=f(e,t),this._k=n,this._l=void 0},function(){for(var e=this._k,t=this._l;t&&t.r;)t=t.p;return this._t&&(this._l=t=t?t.n:this._t._f)?d(0,"keys"==e?t.k:"values"==e?t.v:[t.k,t.v]):(this._t=void 0,d(1))},n?"entries":"values",!n,!0),u(t)}}},function(e,t,n){"use strict";var r=n(127),i=n(45);e.exports=n(68)("Set",function(e){return function(){return e(this,arguments.length>0?arguments[0]:void 0)}},{add:function(e){return r.def(i(this,"Set"),e=0===e?0:e,e)}},r)},function(e,t,n){"use strict";var r,i=n(2),o=n(27)(0),a=n(13),s=n(32),c=n(108),p=n(130),d=n(4),u=n(45),l=n(45),m=!i.ActiveXObject&&"ActiveXObject"in i,f=s.getWeak,h=Object.isExtensible,g=p.ufstore,y=function(e){return function(){return e(this,arguments.length>0?arguments[0]:void 0)}},b={get:function(e){if(d(e)){var t=f(e);return!0===t?g(u(this,"WeakMap")).get(e):t?t[this._i]:void 0}},set:function(e,t){return p.def(u(this,"WeakMap"),e,t)}},v=e.exports=n(68)("WeakMap",y,b,p,!0,!0);l&&m&&(c((r=p.getConstructor(y,"WeakMap")).prototype,b),s.NEED=!0,o(["delete","has","get","set"],function(e){var t=v.prototype,n=t[e];a(t,e,function(t,i){if(d(t)&&!h(t)){this._f||(this._f=new r);var o=this._f[e](t,i);return"set"==e?this:o}return n.call(this,t,i)})}))},function(e,t,n){"use strict";var r=n(44),i=n(32).getWeak,o=n(1),a=n(4),s=n(42),c=n(43),p=n(27),d=n(15),u=n(45),l=p(5),m=p(6),f=0,h=function(e){return e._l||(e._l=new g)},g=function(){this.a=[]},y=function(e,t){return l(e.a,function(e){return e[0]===t})};g.prototype={get:function(e){var t=y(this,e);if(t)return t[1]},has:function(e){return!!y(this,e)},set:function(e,t){var n=y(this,e);n?n[1]=t:this.a.push([e,t])},delete:function(e){var t=m(this.a,function(t){return t[0]===e});return~t&&this.a.splice(t,1),!!~t}},e.exports={getConstructor:function(e,t,n,o){var p=e(function(e,r){s(e,p,t,"_i"),e._t=t,e._i=f++,e._l=void 0,null!=r&&c(r,n,e[o],e)});return r(p.prototype,{delete:function(e){if(!a(e))return!1;var n=i(e);return!0===n?h(u(this,t)).delete(e):n&&d(n,this._i)&&delete n[this._i]},has:function(e){if(!a(e))return!1;var n=i(e);return!0===n?h(u(this,t)).has(e):n&&d(n,this._i)}}),p},def:function(e,t,n){var r=i(o(t),!0);return!0===r?h(e).set(t,n):r[e._i]=n,e},ufstore:h}},function(e,t,n){var r=n(22),i=n(6);e.exports=function(e){if(void 0===e)return 0;var t=r(e),n=i(t);if(t!==n)throw RangeError("Wrong length!");return n}},function(e,t,n){var r=n(40),i=n(60),o=n(1),a=n(2).Reflect;e.exports=a&&a.ownKeys||function(e){var t=r.f(o(e)),n=i.f;return n?t.concat(n(e)):t}},function(e,t,n){"use strict";var r=n(61),i=n(4),o=n(6),a=n(20),s=n(5)("isConcatSpreadable");e.exports=function e(t,n,c,p,d,u,l,m){for(var f,h,g=d,y=0,b=!!l&&a(l,m,3);y<p;){if(y in c){if(f=b?b(c[y],y,n):c[y],h=!1,i(f)&&(h=void 0!==(h=f[s])?!!h:r(f)),h&&u>0)g=e(t,n,f,o(f.length),g,u-1)-1;else{if(g>=9007199254740991)throw TypeError();t[g]=f}g++}y++}return g}},function(e,t,n){var r=n(6),i=n(84),o=n(25);e.exports=function(e,t,n,a){var s=String(o(e)),c=s.length,p=void 0===n?" ":String(n),d=r(t);if(d<=c||""==p)return s;var u=d-c,l=i.call(p,Math.ceil(u/p.length));return l.length>u&&(l=l.slice(0,u)),a?l+s:s+l}},function(e,t,n){var r=n(7),i=n(37),o=n(16),a=n(53).f;e.exports=function(e){return function(t){for(var n,s=o(t),c=i(s),p=c.length,d=0,u=[];p>d;)n=c[d++],r&&!a.call(s,n)||u.push(e?[n,s[n]]:s[n]);return u}}},function(e,t,n){var r=n(47),i=n(137);e.exports=function(e){return function(){if(r(this)!=e)throw TypeError(e+"#toJSON isn't generic");return i(this)}}},function(e,t,n){var r=n(43);e.exports=function(e,t){var n=[];return r(e,!1,n.push,n,t),n}},function(e,t){e.exports=Math.scale||function(e,t,n,r,i){return 0===arguments.length||e!=e||t!=t||n!=n||r!=r||i!=i?NaN:e===1/0||e===-1/0?e:(e-t)*(i-r)/(n-t)+r}},function(e,t,n){"use strict";const r=n(140),i=n(375),o=n(151),a=n(376);function s(e,t){e.host=e.host||o.HOST,e.port=e.port||o.PORT,e.secure=!!e.secure,e.useHostName=!!e.useHostName,e.alterPath=e.alterPath||(e=>e);const n={...e};n.path=e.alterPath(e.path),a(e.secure?i:r,n,t)}function c(e){return(t,n)=>("function"==typeof t&&(n=t,t=void 0),t=t||{},"function"==typeof n?void e(t,n):new Promise((n,r)=>{e(t,(e,t)=>{e?r(e):n(t)})}))}e.exports.Protocol=c(function(e,t){if(e.local){const e=n(377);t(null,e)}else e.path="/json/protocol",s(e,(e,n)=>{e?t(e):t(null,JSON.parse(n))})}),e.exports.List=c(function(e,t){e.path="/json/list",s(e,(e,n)=>{e?t(e):t(null,JSON.parse(n))})}),e.exports.New=c(function(e,t){e.path="/json/new",Object.prototype.hasOwnProperty.call(e,"url")&&(e.path+=`?${e.url}`),s(e,(e,n)=>{e?t(e):t(null,JSON.parse(n))})}),e.exports.Activate=c(function(e,t){e.path="/json/activate/"+e.id,s(e,e=>{t(e||null)})}),e.exports.Close=c(function(e,t){e.path="/json/close/"+e.id,s(e,e=>{t(e||null)})}),e.exports.Version=c(function(e,t){e.path="/json/version",s(e,(e,n)=>{e?t(e):t(null,JSON.parse(n))})})},function(e,t,n){(function(e){var r=n(356),i=n(143),o=n(367),a=n(368),s=n(75),c=t;c.request=function(t,n){t="string"==typeof t?s.parse(t):o(t);var i=-1===e.location.protocol.search(/^https?:$/)?"http:":"",a=t.protocol||i,c=t.hostname||t.host,p=t.port,d=t.path||"/";c&&-1!==c.indexOf(":")&&(c="["+c+"]"),t.url=(c?a+"//"+c:"")+(p?":"+p:"")+d,t.method=(t.method||"GET").toUpperCase(),t.headers=t.headers||{};var u=new r(t);return n&&u.on("response",n),u},c.get=function(e,t){var n=c.request(e,t);return n.end(),n},c.ClientRequest=r,c.IncomingMessage=i.IncomingMessage,c.Agent=function(){},c.Agent.defaultMaxSockets=4,c.globalAgent=new c.Agent,c.STATUS_CODES=a,c.METHODS=["CHECKOUT","CONNECT","COPY","DELETE","GET","HEAD","LOCK","M-SEARCH","MERGE","MKACTIVITY","MKCOL","MOVE","NOTIFY","OPTIONS","PATCH","POST","PROPFIND","PROPPATCH","PURGE","PUT","REPORT","SEARCH","SUBSCRIBE","TRACE","UNLOCK","UNSUBSCRIBE"]}).call(this,n(11))},function(e,t){var n={}.toString;e.exports=Array.isArray||function(e){return"[object Array]"==n.call(e)}},function(e,t,n){(function(e){t.fetch=s(e.fetch)&&s(e.ReadableStream),t.writableStream=s(e.WritableStream),t.abortController=s(e.AbortController),t.blobConstructor=!1;try{new Blob([new ArrayBuffer(1)]),t.blobConstructor=!0}catch(e){}var n;function r(){if(void 0!==n)return n;if(e.XMLHttpRequest){n=new e.XMLHttpRequest;try{n.open("GET",e.XDomainRequest?"/":"https://example.com")}catch(e){n=null}}else n=null;return n}function i(e){var t=r();if(!t)return!1;try{return t.responseType=e,t.responseType===e}catch(e){}return!1}var o=void 0!==e.ArrayBuffer,a=o&&s(e.ArrayBuffer.prototype.slice);function s(e){return"function"==typeof e}t.arraybuffer=t.fetch||o&&i("arraybuffer"),t.msstream=!t.fetch&&a&&i("ms-stream"),t.mozchunkedarraybuffer=!t.fetch&&o&&i("moz-chunked-arraybuffer"),t.overrideMimeType=t.fetch||!!r()&&s(r().overrideMimeType),t.vbArray=s(e.VBArray),n=null}).call(this,n(11))},function(e,t,n){(function(e,r,i){var o=n(142),a=n(34),s=n(144),c=t.readyStates={UNSENT:0,OPENED:1,HEADERS_RECEIVED:2,LOADING:3,DONE:4},p=t.IncomingMessage=function(t,n,a,c){var p=this;if(s.Readable.call(p),p._mode=a,p.headers={},p.rawHeaders=[],p.trailers={},p.rawTrailers=[],p.on("end",function(){e.nextTick(function(){p.emit("close")})}),"fetch"===a){if(p._fetchResponse=n,p.url=n.url,p.statusCode=n.status,p.statusMessage=n.statusText,n.headers.forEach(function(e,t){p.headers[t.toLowerCase()]=e,p.rawHeaders.push(t,e)}),o.writableStream){var d=new WritableStream({write:function(e){return new Promise(function(t,n){p._destroyed?n():p.push(new r(e))?t():p._resumeFetch=t})},close:function(){i.clearTimeout(c),p._destroyed||p.push(null)},abort:function(e){p._destroyed||p.emit("error",e)}});try{return void n.body.pipeTo(d).catch(function(e){i.clearTimeout(c),p._destroyed||p.emit("error",e)})}catch(e){}}var u=n.body.getReader();!function e(){u.read().then(function(t){if(!p._destroyed){if(t.done)return i.clearTimeout(c),void p.push(null);p.push(new r(t.value)),e()}}).catch(function(e){i.clearTimeout(c),p._destroyed||p.emit("error",e)})}()}else{if(p._xhr=t,p._pos=0,p.url=t.responseURL,p.statusCode=t.status,p.statusMessage=t.statusText,t.getAllResponseHeaders().split(/\r?\n/).forEach(function(e){var t=e.match(/^([^:]+):\s*(.*)/);if(t){var n=t[1].toLowerCase();"set-cookie"===n?(void 0===p.headers[n]&&(p.headers[n]=[]),p.headers[n].push(t[2])):void 0!==p.headers[n]?p.headers[n]+=", "+t[2]:p.headers[n]=t[2],p.rawHeaders.push(t[1],t[2])}}),p._charset="x-user-defined",!o.overrideMimeType){var l=p.rawHeaders["mime-type"];if(l){var m=l.match(/;\s*charset=([^;])(;|$)/);m&&(p._charset=m[1].toLowerCase())}p._charset||(p._charset="utf-8")}}};a(p,s.Readable),p.prototype._read=function(){var e=this._resumeFetch;e&&(this._resumeFetch=null,e())},p.prototype._onXHRProgress=function(){var e=this,t=e._xhr,n=null;switch(e._mode){case"text:vbarray":if(t.readyState!==c.DONE)break;try{n=new i.VBArray(t.responseBody).toArray()}catch(e){}if(null!==n){e.push(new r(n));break}case"text":try{n=t.responseText}catch(t){e._mode="text:vbarray";break}if(n.length>e._pos){var o=n.substr(e._pos);if("x-user-defined"===e._charset){for(var a=new r(o.length),s=0;s<o.length;s++)a[s]=255&o.charCodeAt(s);e.push(a)}else e.push(o,e._charset);e._pos=n.length}break;case"arraybuffer":if(t.readyState!==c.DONE||!t.response)break;n=t.response,e.push(new r(new Uint8Array(n)));break;case"moz-chunked-arraybuffer":if(n=t.response,t.readyState!==c.LOADING||!n)break;e.push(new r(new Uint8Array(n)));break;case"ms-stream":if(n=t.response,t.readyState!==c.LOADING)break;var p=new i.MSStreamReader;p.onprogress=function(){p.result.byteLength>e._pos&&(e.push(new r(new Uint8Array(p.result.slice(e._pos)))),e._pos=p.result.byteLength)},p.onload=function(){e.push(null)},p.readAsArrayBuffer(n)}e._xhr.readyState===c.DONE&&"ms-stream"!==e._mode&&e.push(null)}}).call(this,n(30),n(57).Buffer,n(11))},function(e,t,n){(t=e.exports=n(145)).Stream=t,t.Readable=t,t.Writable=n(148),t.Duplex=n(50),t.Transform=n(150),t.PassThrough=n(365)},function(e,t,n){"use strict";(function(t,r){var i=n(73);e.exports=v;var o,a=n(141);v.ReadableState=b;n(56).EventEmitter;var s=function(e,t){return e.listeners(t).length},c=n(146),p=n(74).Buffer,d=t.Uint8Array||function(){};var u=n(58);u.inherits=n(34);var l=n(359),m=void 0;m=l&&l.debuglog?l.debuglog("stream"):function(){};var f,h=n(360),g=n(147);u.inherits(v,c);var y=["error","close","destroy","pause","resume"];function b(e,t){e=e||{};var r=t instanceof(o=o||n(50));this.objectMode=!!e.objectMode,r&&(this.objectMode=this.objectMode||!!e.readableObjectMode);var i=e.highWaterMark,a=e.readableHighWaterMark,s=this.objectMode?16:16384;this.highWaterMark=i||0===i?i:r&&(a||0===a)?a:s,this.highWaterMark=Math.floor(this.highWaterMark),this.buffer=new h,this.length=0,this.pipes=null,this.pipesCount=0,this.flowing=null,this.ended=!1,this.endEmitted=!1,this.reading=!1,this.sync=!0,this.needReadable=!1,this.emittedReadable=!1,this.readableListening=!1,this.resumeScheduled=!1,this.destroyed=!1,this.defaultEncoding=e.defaultEncoding||"utf8",this.awaitDrain=0,this.readingMore=!1,this.decoder=null,this.encoding=null,e.encoding&&(f||(f=n(149).StringDecoder),this.decoder=new f(e.encoding),this.encoding=e.encoding)}function v(e){if(o=o||n(50),!(this instanceof v))return new v(e);this._readableState=new b(e,this),this.readable=!0,e&&("function"==typeof e.read&&(this._read=e.read),"function"==typeof e.destroy&&(this._destroy=e.destroy)),c.call(this)}function w(e,t,n,r,i){var o,a=e._readableState;null===t?(a.reading=!1,function(e,t){if(t.ended)return;if(t.decoder){var n=t.decoder.end();n&&n.length&&(t.buffer.push(n),t.length+=t.objectMode?1:n.length)}t.ended=!0,T(e)}(e,a)):(i||(o=function(e,t){var n;r=t,p.isBuffer(r)||r instanceof d||"string"==typeof t||void 0===t||e.objectMode||(n=new TypeError("Invalid non-string/buffer chunk"));var r;return n}(a,t)),o?e.emit("error",o):a.objectMode||t&&t.length>0?("string"==typeof t||a.objectMode||Object.getPrototypeOf(t)===p.prototype||(t=function(e){return p.from(e)}(t)),r?a.endEmitted?e.emit("error",new Error("stream.unshift() after end event")):S(e,a,t,!0):a.ended?e.emit("error",new Error("stream.push() after EOF")):(a.reading=!1,a.decoder&&!n?(t=a.decoder.write(t),a.objectMode||0!==t.length?S(e,a,t,!1):k(e,a)):S(e,a,t,!1))):r||(a.reading=!1));return function(e){return!e.ended&&(e.needReadable||e.length<e.highWaterMark||0===e.length)}(a)}function S(e,t,n,r){t.flowing&&0===t.length&&!t.sync?(e.emit("data",n),e.read(0)):(t.length+=t.objectMode?1:n.length,r?t.buffer.unshift(n):t.buffer.push(n),t.needReadable&&T(e)),k(e,t)}Object.defineProperty(v.prototype,"destroyed",{get:function(){return void 0!==this._readableState&&this._readableState.destroyed},set:function(e){this._readableState&&(this._readableState.destroyed=e)}}),v.prototype.destroy=g.destroy,v.prototype._undestroy=g.undestroy,v.prototype._destroy=function(e,t){this.push(null),t(e)},v.prototype.push=function(e,t){var n,r=this._readableState;return r.objectMode?n=!0:"string"==typeof e&&((t=t||r.defaultEncoding)!==r.encoding&&(e=p.from(e,t),t=""),n=!0),w(this,e,t,!1,n)},v.prototype.unshift=function(e){return w(this,e,null,!0,!1)},v.prototype.isPaused=function(){return!1===this._readableState.flowing},v.prototype.setEncoding=function(e){return f||(f=n(149).StringDecoder),this._readableState.decoder=new f(e),this._readableState.encoding=e,this};var x=8388608;function I(e,t){return e<=0||0===t.length&&t.ended?0:t.objectMode?1:e!=e?t.flowing&&t.length?t.buffer.head.data.length:t.length:(e>t.highWaterMark&&(t.highWaterMark=function(e){return e>=x?e=x:(e--,e|=e>>>1,e|=e>>>2,e|=e>>>4,e|=e>>>8,e|=e>>>16,e++),e}(e)),e<=t.length?e:t.ended?t.length:(t.needReadable=!0,0))}function T(e){var t=e._readableState;t.needReadable=!1,t.emittedReadable||(m("emitReadable",t.flowing),t.emittedReadable=!0,t.sync?i.nextTick(R,e):R(e))}function R(e){m("emit readable"),e.emit("readable"),E(e)}function k(e,t){t.readingMore||(t.readingMore=!0,i.nextTick(C,e,t))}function C(e,t){for(var n=t.length;!t.reading&&!t.flowing&&!t.ended&&t.length<t.highWaterMark&&(m("maybeReadMore read 0"),e.read(0),n!==t.length);)n=t.length;t.readingMore=!1}function O(e){m("readable nexttick read 0"),e.read(0)}function $(e,t){t.reading||(m("resume read 0"),e.read(0)),t.resumeScheduled=!1,t.awaitDrain=0,e.emit("resume"),E(e),t.flowing&&!t.reading&&e.read(0)}function E(e){var t=e._readableState;for(m("flow",t.flowing);t.flowing&&null!==e.read(););}function D(e,t){return 0===t.length?null:(t.objectMode?n=t.buffer.shift():!e||e>=t.length?(n=t.decoder?t.buffer.join(""):1===t.buffer.length?t.buffer.head.data:t.buffer.concat(t.length),t.buffer.clear()):n=function(e,t,n){var r;e<t.head.data.length?(r=t.head.data.slice(0,e),t.head.data=t.head.data.slice(e)):r=e===t.head.data.length?t.shift():n?function(e,t){var n=t.head,r=1,i=n.data;e-=i.length;for(;n=n.next;){var o=n.data,a=e>o.length?o.length:e;if(a===o.length?i+=o:i+=o.slice(0,e),0===(e-=a)){a===o.length?(++r,n.next?t.head=n.next:t.head=t.tail=null):(t.head=n,n.data=o.slice(a));break}++r}return t.length-=r,i}(e,t):function(e,t){var n=p.allocUnsafe(e),r=t.head,i=1;r.data.copy(n),e-=r.data.length;for(;r=r.next;){var o=r.data,a=e>o.length?o.length:e;if(o.copy(n,n.length-e,0,a),0===(e-=a)){a===o.length?(++i,r.next?t.head=r.next:t.head=t.tail=null):(t.head=r,r.data=o.slice(a));break}++i}return t.length-=i,n}(e,t);return r}(e,t.buffer,t.decoder),n);var n}function j(e){var t=e._readableState;if(t.length>0)throw new Error('"endReadable()" called on non-empty stream');t.endEmitted||(t.ended=!0,i.nextTick(P,t,e))}function P(e,t){e.endEmitted||0!==e.length||(e.endEmitted=!0,t.readable=!1,t.emit("end"))}function A(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1}v.prototype.read=function(e){m("read",e),e=parseInt(e,10);var t=this._readableState,n=e;if(0!==e&&(t.emittedReadable=!1),0===e&&t.needReadable&&(t.length>=t.highWaterMark||t.ended))return m("read: emitReadable",t.length,t.ended),0===t.length&&t.ended?j(this):T(this),null;if(0===(e=I(e,t))&&t.ended)return 0===t.length&&j(this),null;var r,i=t.needReadable;return m("need readable",i),(0===t.length||t.length-e<t.highWaterMark)&&m("length less than watermark",i=!0),t.ended||t.reading?m("reading or ended",i=!1):i&&(m("do read"),t.reading=!0,t.sync=!0,0===t.length&&(t.needReadable=!0),this._read(t.highWaterMark),t.sync=!1,t.reading||(e=I(n,t))),null===(r=e>0?D(e,t):null)?(t.needReadable=!0,e=0):t.length-=e,0===t.length&&(t.ended||(t.needReadable=!0),n!==e&&t.ended&&j(this)),null!==r&&this.emit("data",r),r},v.prototype._read=function(e){this.emit("error",new Error("_read() is not implemented"))},v.prototype.pipe=function(e,t){var n=this,o=this._readableState;switch(o.pipesCount){case 0:o.pipes=e;break;case 1:o.pipes=[o.pipes,e];break;default:o.pipes.push(e)}o.pipesCount+=1,m("pipe count=%d opts=%j",o.pipesCount,t);var c=(!t||!1!==t.end)&&e!==r.stdout&&e!==r.stderr?d:v;function p(t,r){m("onunpipe"),t===n&&r&&!1===r.hasUnpiped&&(r.hasUnpiped=!0,m("cleanup"),e.removeListener("close",y),e.removeListener("finish",b),e.removeListener("drain",u),e.removeListener("error",g),e.removeListener("unpipe",p),n.removeListener("end",d),n.removeListener("end",v),n.removeListener("data",h),l=!0,!o.awaitDrain||e._writableState&&!e._writableState.needDrain||u())}function d(){m("onend"),e.end()}o.endEmitted?i.nextTick(c):n.once("end",c),e.on("unpipe",p);var u=function(e){return function(){var t=e._readableState;m("pipeOnDrain",t.awaitDrain),t.awaitDrain&&t.awaitDrain--,0===t.awaitDrain&&s(e,"data")&&(t.flowing=!0,E(e))}}(n);e.on("drain",u);var l=!1;var f=!1;function h(t){m("ondata"),f=!1,!1!==e.write(t)||f||((1===o.pipesCount&&o.pipes===e||o.pipesCount>1&&-1!==A(o.pipes,e))&&!l&&(m("false write response, pause",n._readableState.awaitDrain),n._readableState.awaitDrain++,f=!0),n.pause())}function g(t){m("onerror",t),v(),e.removeListener("error",g),0===s(e,"error")&&e.emit("error",t)}function y(){e.removeListener("finish",b),v()}function b(){m("onfinish"),e.removeListener("close",y),v()}function v(){m("unpipe"),n.unpipe(e)}return n.on("data",h),function(e,t,n){if("function"==typeof e.prependListener)return e.prependListener(t,n);e._events&&e._events[t]?a(e._events[t])?e._events[t].unshift(n):e._events[t]=[n,e._events[t]]:e.on(t,n)}(e,"error",g),e.once("close",y),e.once("finish",b),e.emit("pipe",n),o.flowing||(m("pipe resume"),n.resume()),e},v.prototype.unpipe=function(e){var t=this._readableState,n={hasUnpiped:!1};if(0===t.pipesCount)return this;if(1===t.pipesCount)return e&&e!==t.pipes?this:(e||(e=t.pipes),t.pipes=null,t.pipesCount=0,t.flowing=!1,e&&e.emit("unpipe",this,n),this);if(!e){var r=t.pipes,i=t.pipesCount;t.pipes=null,t.pipesCount=0,t.flowing=!1;for(var o=0;o<i;o++)r[o].emit("unpipe",this,n);return this}var a=A(t.pipes,e);return-1===a?this:(t.pipes.splice(a,1),t.pipesCount-=1,1===t.pipesCount&&(t.pipes=t.pipes[0]),e.emit("unpipe",this,n),this)},v.prototype.on=function(e,t){var n=c.prototype.on.call(this,e,t);if("data"===e)!1!==this._readableState.flowing&&this.resume();else if("readable"===e){var r=this._readableState;r.endEmitted||r.readableListening||(r.readableListening=r.needReadable=!0,r.emittedReadable=!1,r.reading?r.length&&T(this):i.nextTick(O,this))}return n},v.prototype.addListener=v.prototype.on,v.prototype.resume=function(){var e=this._readableState;return e.flowing||(m("resume"),e.flowing=!0,function(e,t){t.resumeScheduled||(t.resumeScheduled=!0,i.nextTick($,e,t))}(this,e)),this},v.prototype.pause=function(){return m("call pause flowing=%j",this._readableState.flowing),!1!==this._readableState.flowing&&(m("pause"),this._readableState.flowing=!1,this.emit("pause")),this},v.prototype.wrap=function(e){var t=this,n=this._readableState,r=!1;for(var i in e.on("end",function(){if(m("wrapped end"),n.decoder&&!n.ended){var e=n.decoder.end();e&&e.length&&t.push(e)}t.push(null)}),e.on("data",function(i){(m("wrapped data"),n.decoder&&(i=n.decoder.write(i)),n.objectMode&&null==i)||(n.objectMode||i&&i.length)&&(t.push(i)||(r=!0,e.pause()))}),e)void 0===this[i]&&"function"==typeof e[i]&&(this[i]=function(t){return function(){return e[t].apply(e,arguments)}}(i));for(var o=0;o<y.length;o++)e.on(y[o],this.emit.bind(this,y[o]));return this._read=function(t){m("wrapped _read",t),r&&(r=!1,e.resume())},this},Object.defineProperty(v.prototype,"readableHighWaterMark",{enumerable:!1,get:function(){return this._readableState.highWaterMark}}),v._fromList=D}).call(this,n(11),n(30))},function(e,t,n){e.exports=n(56).EventEmitter},function(e,t,n){"use strict";var r=n(73);function i(e,t){e.emit("error",t)}e.exports={destroy:function(e,t){var n=this,o=this._readableState&&this._readableState.destroyed,a=this._writableState&&this._writableState.destroyed;return o||a?(t?t(e):!e||this._writableState&&this._writableState.errorEmitted||r.nextTick(i,this,e),this):(this._readableState&&(this._readableState.destroyed=!0),this._writableState&&(this._writableState.destroyed=!0),this._destroy(e||null,function(e){!t&&e?(r.nextTick(i,n,e),n._writableState&&(n._writableState.errorEmitted=!0)):t&&t(e)}),this)},undestroy:function(){this._readableState&&(this._readableState.destroyed=!1,this._readableState.reading=!1,this._readableState.ended=!1,this._readableState.endEmitted=!1),this._writableState&&(this._writableState.destroyed=!1,this._writableState.ended=!1,this._writableState.ending=!1,this._writableState.finished=!1,this._writableState.errorEmitted=!1)}}},function(e,t,n){"use strict";(function(t,r,i){var o=n(73);function a(e){var t=this;this.next=null,this.entry=null,this.finish=function(){!function(e,t,n){var r=e.entry;e.entry=null;for(;r;){var i=r.callback;t.pendingcb--,i(n),r=r.next}t.corkedRequestsFree?t.corkedRequestsFree.next=e:t.corkedRequestsFree=e}(t,e)}}e.exports=b;var s,c=!t.browser&&["v0.10","v0.9."].indexOf(t.version.slice(0,5))>-1?r:o.nextTick;b.WritableState=y;var p=n(58);p.inherits=n(34);var d={deprecate:n(364)},u=n(146),l=n(74).Buffer,m=i.Uint8Array||function(){};var f,h=n(147);function g(){}function y(e,t){s=s||n(50),e=e||{};var r=t instanceof s;this.objectMode=!!e.objectMode,r&&(this.objectMode=this.objectMode||!!e.writableObjectMode);var i=e.highWaterMark,p=e.writableHighWaterMark,d=this.objectMode?16:16384;this.highWaterMark=i||0===i?i:r&&(p||0===p)?p:d,this.highWaterMark=Math.floor(this.highWaterMark),this.finalCalled=!1,this.needDrain=!1,this.ending=!1,this.ended=!1,this.finished=!1,this.destroyed=!1;var u=!1===e.decodeStrings;this.decodeStrings=!u,this.defaultEncoding=e.defaultEncoding||"utf8",this.length=0,this.writing=!1,this.corked=0,this.sync=!0,this.bufferProcessing=!1,this.onwrite=function(e){!function(e,t){var n=e._writableState,r=n.sync,i=n.writecb;if(function(e){e.writing=!1,e.writecb=null,e.length-=e.writelen,e.writelen=0}(n),t)!function(e,t,n,r,i){--t.pendingcb,n?(o.nextTick(i,r),o.nextTick(T,e,t),e._writableState.errorEmitted=!0,e.emit("error",r)):(i(r),e._writableState.errorEmitted=!0,e.emit("error",r),T(e,t))}(e,n,r,t,i);else{var a=x(n);a||n.corked||n.bufferProcessing||!n.bufferedRequest||S(e,n),r?c(w,e,n,a,i):w(e,n,a,i)}}(t,e)},this.writecb=null,this.writelen=0,this.bufferedRequest=null,this.lastBufferedRequest=null,this.pendingcb=0,this.prefinished=!1,this.errorEmitted=!1,this.bufferedRequestCount=0,this.corkedRequestsFree=new a(this)}function b(e){if(s=s||n(50),!(f.call(b,this)||this instanceof s))return new b(e);this._writableState=new y(e,this),this.writable=!0,e&&("function"==typeof e.write&&(this._write=e.write),"function"==typeof e.writev&&(this._writev=e.writev),"function"==typeof e.destroy&&(this._destroy=e.destroy),"function"==typeof e.final&&(this._final=e.final)),u.call(this)}function v(e,t,n,r,i,o,a){t.writelen=r,t.writecb=a,t.writing=!0,t.sync=!0,n?e._writev(i,t.onwrite):e._write(i,o,t.onwrite),t.sync=!1}function w(e,t,n,r){n||function(e,t){0===t.length&&t.needDrain&&(t.needDrain=!1,e.emit("drain"))}(e,t),t.pendingcb--,r(),T(e,t)}function S(e,t){t.bufferProcessing=!0;var n=t.bufferedRequest;if(e._writev&&n&&n.next){var r=t.bufferedRequestCount,i=new Array(r),o=t.corkedRequestsFree;o.entry=n;for(var s=0,c=!0;n;)i[s]=n,n.isBuf||(c=!1),n=n.next,s+=1;i.allBuffers=c,v(e,t,!0,t.length,i,"",o.finish),t.pendingcb++,t.lastBufferedRequest=null,o.next?(t.corkedRequestsFree=o.next,o.next=null):t.corkedRequestsFree=new a(t),t.bufferedRequestCount=0}else{for(;n;){var p=n.chunk,d=n.encoding,u=n.callback;if(v(e,t,!1,t.objectMode?1:p.length,p,d,u),n=n.next,t.bufferedRequestCount--,t.writing)break}null===n&&(t.lastBufferedRequest=null)}t.bufferedRequest=n,t.bufferProcessing=!1}function x(e){return e.ending&&0===e.length&&null===e.bufferedRequest&&!e.finished&&!e.writing}function I(e,t){e._final(function(n){t.pendingcb--,n&&e.emit("error",n),t.prefinished=!0,e.emit("prefinish"),T(e,t)})}function T(e,t){var n=x(t);return n&&(!function(e,t){t.prefinished||t.finalCalled||("function"==typeof e._final?(t.pendingcb++,t.finalCalled=!0,o.nextTick(I,e,t)):(t.prefinished=!0,e.emit("prefinish")))}(e,t),0===t.pendingcb&&(t.finished=!0,e.emit("finish"))),n}p.inherits(b,u),y.prototype.getBuffer=function(){for(var e=this.bufferedRequest,t=[];e;)t.push(e),e=e.next;return t},function(){try{Object.defineProperty(y.prototype,"buffer",{get:d.deprecate(function(){return this.getBuffer()},"_writableState.buffer is deprecated. Use _writableState.getBuffer instead.","DEP0003")})}catch(e){}}(),"function"==typeof Symbol&&Symbol.hasInstance&&"function"==typeof Function.prototype[Symbol.hasInstance]?(f=Function.prototype[Symbol.hasInstance],Object.defineProperty(b,Symbol.hasInstance,{value:function(e){return!!f.call(this,e)||this===b&&(e&&e._writableState instanceof y)}})):f=function(e){return e instanceof this},b.prototype.pipe=function(){this.emit("error",new Error("Cannot pipe, not readable"))},b.prototype.write=function(e,t,n){var r,i=this._writableState,a=!1,s=!i.objectMode&&(r=e,l.isBuffer(r)||r instanceof m);return s&&!l.isBuffer(e)&&(e=function(e){return l.from(e)}(e)),"function"==typeof t&&(n=t,t=null),s?t="buffer":t||(t=i.defaultEncoding),"function"!=typeof n&&(n=g),i.ended?function(e,t){var n=new Error("write after end");e.emit("error",n),o.nextTick(t,n)}(this,n):(s||function(e,t,n,r){var i=!0,a=!1;return null===n?a=new TypeError("May not write null values to stream"):"string"==typeof n||void 0===n||t.objectMode||(a=new TypeError("Invalid non-string/buffer chunk")),a&&(e.emit("error",a),o.nextTick(r,a),i=!1),i}(this,i,e,n))&&(i.pendingcb++,a=function(e,t,n,r,i,o){if(!n){var a=function(e,t,n){e.objectMode||!1===e.decodeStrings||"string"!=typeof t||(t=l.from(t,n));return t}(t,r,i);r!==a&&(n=!0,i="buffer",r=a)}var s=t.objectMode?1:r.length;t.length+=s;var c=t.length<t.highWaterMark;c||(t.needDrain=!0);if(t.writing||t.corked){var p=t.lastBufferedRequest;t.lastBufferedRequest={chunk:r,encoding:i,isBuf:n,callback:o,next:null},p?p.next=t.lastBufferedRequest:t.bufferedRequest=t.lastBufferedRequest,t.bufferedRequestCount+=1}else v(e,t,!1,s,r,i,o);return c}(this,i,s,e,t,n)),a},b.prototype.cork=function(){this._writableState.corked++},b.prototype.uncork=function(){var e=this._writableState;e.corked&&(e.corked--,e.writing||e.corked||e.finished||e.bufferProcessing||!e.bufferedRequest||S(this,e))},b.prototype.setDefaultEncoding=function(e){if("string"==typeof e&&(e=e.toLowerCase()),!(["hex","utf8","utf-8","ascii","binary","base64","ucs2","ucs-2","utf16le","utf-16le","raw"].indexOf((e+"").toLowerCase())>-1))throw new TypeError("Unknown encoding: "+e);return this._writableState.defaultEncoding=e,this},Object.defineProperty(b.prototype,"writableHighWaterMark",{enumerable:!1,get:function(){return this._writableState.highWaterMark}}),b.prototype._write=function(e,t,n){n(new Error("_write() is not implemented"))},b.prototype._writev=null,b.prototype.end=function(e,t,n){var r=this._writableState;"function"==typeof e?(n=e,e=null,t=null):"function"==typeof t&&(n=t,t=null),null!=e&&this.write(e,t),r.corked&&(r.corked=1,this.uncork()),r.ending||r.finished||function(e,t,n){t.ending=!0,T(e,t),n&&(t.finished?o.nextTick(n):e.once("finish",n));t.ended=!0,e.writable=!1}(this,r,n)},Object.defineProperty(b.prototype,"destroyed",{get:function(){return void 0!==this._writableState&&this._writableState.destroyed},set:function(e){this._writableState&&(this._writableState.destroyed=e)}}),b.prototype.destroy=h.destroy,b.prototype._undestroy=h.undestroy,b.prototype._destroy=function(e,t){this.end(),t(e)}}).call(this,n(30),n(362).setImmediate,n(11))},function(e,t,n){"use strict";var r=n(74).Buffer,i=r.isEncoding||function(e){switch((e=""+e)&&e.toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":case"raw":return!0;default:return!1}};function o(e){var t;switch(this.encoding=function(e){var t=function(e){if(!e)return"utf8";for(var t;;)switch(e){case"utf8":case"utf-8":return"utf8";case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return"utf16le";case"latin1":case"binary":return"latin1";case"base64":case"ascii":case"hex":return e;default:if(t)return;e=(""+e).toLowerCase(),t=!0}}(e);if("string"!=typeof t&&(r.isEncoding===i||!i(e)))throw new Error("Unknown encoding: "+e);return t||e}(e),this.encoding){case"utf16le":this.text=c,this.end=p,t=4;break;case"utf8":this.fillLast=s,t=4;break;case"base64":this.text=d,this.end=u,t=3;break;default:return this.write=l,void(this.end=m)}this.lastNeed=0,this.lastTotal=0,this.lastChar=r.allocUnsafe(t)}function a(e){return e<=127?0:e>>5==6?2:e>>4==14?3:e>>3==30?4:e>>6==2?-1:-2}function s(e){var t=this.lastTotal-this.lastNeed,n=function(e,t,n){if(128!=(192&t[0]))return e.lastNeed=0,"�";if(e.lastNeed>1&&t.length>1){if(128!=(192&t[1]))return e.lastNeed=1,"�";if(e.lastNeed>2&&t.length>2&&128!=(192&t[2]))return e.lastNeed=2,"�"}}(this,e);return void 0!==n?n:this.lastNeed<=e.length?(e.copy(this.lastChar,t,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal)):(e.copy(this.lastChar,t,0,e.length),void(this.lastNeed-=e.length))}function c(e,t){if((e.length-t)%2==0){var n=e.toString("utf16le",t);if(n){var r=n.charCodeAt(n.length-1);if(r>=55296&&r<=56319)return this.lastNeed=2,this.lastTotal=4,this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1],n.slice(0,-1)}return n}return this.lastNeed=1,this.lastTotal=2,this.lastChar[0]=e[e.length-1],e.toString("utf16le",t,e.length-1)}function p(e){var t=e&&e.length?this.write(e):"";if(this.lastNeed){var n=this.lastTotal-this.lastNeed;return t+this.lastChar.toString("utf16le",0,n)}return t}function d(e,t){var n=(e.length-t)%3;return 0===n?e.toString("base64",t):(this.lastNeed=3-n,this.lastTotal=3,1===n?this.lastChar[0]=e[e.length-1]:(this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1]),e.toString("base64",t,e.length-n))}function u(e){var t=e&&e.length?this.write(e):"";return this.lastNeed?t+this.lastChar.toString("base64",0,3-this.lastNeed):t}function l(e){return e.toString(this.encoding)}function m(e){return e&&e.length?this.write(e):""}t.StringDecoder=o,o.prototype.write=function(e){if(0===e.length)return"";var t,n;if(this.lastNeed){if(void 0===(t=this.fillLast(e)))return"";n=this.lastNeed,this.lastNeed=0}else n=0;return n<e.length?t?t+this.text(e,n):this.text(e,n):t||""},o.prototype.end=function(e){var t=e&&e.length?this.write(e):"";return this.lastNeed?t+"�":t},o.prototype.text=function(e,t){var n=function(e,t,n){var r=t.length-1;if(r<n)return 0;var i=a(t[r]);if(i>=0)return i>0&&(e.lastNeed=i-1),i;if(--r<n||-2===i)return 0;if((i=a(t[r]))>=0)return i>0&&(e.lastNeed=i-2),i;if(--r<n||-2===i)return 0;if((i=a(t[r]))>=0)return i>0&&(2===i?i=0:e.lastNeed=i-3),i;return 0}(this,e,t);if(!this.lastNeed)return e.toString("utf8",t);this.lastTotal=n;var r=e.length-(n-this.lastNeed);return e.copy(this.lastChar,0,r),e.toString("utf8",t,r)},o.prototype.fillLast=function(e){if(this.lastNeed<=e.length)return e.copy(this.lastChar,this.lastTotal-this.lastNeed,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal);e.copy(this.lastChar,this.lastTotal-this.lastNeed,0,e.length),this.lastNeed-=e.length}},function(e,t,n){"use strict";e.exports=a;var r=n(50),i=n(58);function o(e,t){var n=this._transformState;n.transforming=!1;var r=n.writecb;if(!r)return this.emit("error",new Error("write callback called multiple times"));n.writechunk=null,n.writecb=null,null!=t&&this.push(t),r(e);var i=this._readableState;i.reading=!1,(i.needReadable||i.length<i.highWaterMark)&&this._read(i.highWaterMark)}function a(e){if(!(this instanceof a))return new a(e);r.call(this,e),this._transformState={afterTransform:o.bind(this),needTransform:!1,transforming:!1,writecb:null,writechunk:null,writeencoding:null},this._readableState.needReadable=!0,this._readableState.sync=!1,e&&("function"==typeof e.transform&&(this._transform=e.transform),"function"==typeof e.flush&&(this._flush=e.flush)),this.on("prefinish",s)}function s(){var e=this;"function"==typeof this._flush?this._flush(function(t,n){c(e,t,n)}):c(this,null,null)}function c(e,t,n){if(t)return e.emit("error",t);if(null!=n&&e.push(n),e._writableState.length)throw new Error("Calling transform done when ws.length != 0");if(e._transformState.transforming)throw new Error("Calling transform done when still transforming");return e.push(null)}i.inherits=n(34),i.inherits(a,r),a.prototype.push=function(e,t){return this._transformState.needTransform=!1,r.prototype.push.call(this,e,t)},a.prototype._transform=function(e,t,n){throw new Error("_transform() is not implemented")},a.prototype._write=function(e,t,n){var r=this._transformState;if(r.writecb=n,r.writechunk=e,r.writeencoding=t,!r.transforming){var i=this._readableState;(r.needTransform||i.needReadable||i.length<i.highWaterMark)&&this._read(i.highWaterMark)}},a.prototype._read=function(e){var t=this._transformState;null!==t.writechunk&&t.writecb&&!t.transforming?(t.transforming=!0,this._transform(t.writechunk,t.writeencoding,t.afterTransform)):t.needTransform=!0},a.prototype._destroy=function(e,t){var n=this;r.prototype._destroy.call(this,e,function(e){t(e),n.emit("close")})}},function(e,t,n){"use strict";e.exports.HOST="localhost",e.exports.PORT=9222},function(e,t,n){n(153),e.exports=n(355)},function(e,t,n){"use strict";(function(e){if(n(154),n(351),n(352),e._babelPolyfill)throw new Error("only one instance of babel-polyfill is allowed");e._babelPolyfill=!0;var t="defineProperty";function r(e,n,r){e[n]||Object[t](e,n,{writable:!0,configurable:!0,value:r})}r(String.prototype,"padLeft","".padStart),r(String.prototype,"padRight","".padEnd),"pop,reverse,shift,keys,values,entries,indexOf,every,some,forEach,map,filter,find,findIndex,includes,join,slice,concat,push,splice,unshift,sort,lastIndexOf,reduce,reduceRight,copyWithin,fill".split(",").forEach(function(e){[][e]&&r(Array,e,Function.call.bind([][e]))})}).call(this,n(11))},function(e,t,n){n(155),n(158),n(159),n(160),n(161),n(162),n(163),n(164),n(165),n(166),n(167),n(168),n(169),n(170),n(171),n(172),n(173),n(174),n(175),n(176),n(177),n(178),n(179),n(180),n(181),n(182),n(183),n(184),n(185),n(186),n(187),n(188),n(189),n(190),n(191),n(192),n(193),n(194),n(195),n(196),n(197),n(198),n(199),n(200),n(201),n(202),n(203),n(204),n(205),n(206),n(207),n(208),n(209),n(210),n(211),n(212),n(213),n(214),n(215),n(216),n(217),n(218),n(219),n(220),n(221),n(222),n(223),n(224),n(225),n(226),n(227),n(228),n(229),n(230),n(231),n(232),n(233),n(235),n(236),n(238),n(239),n(240),n(241),n(242),n(243),n(244),n(246),n(247),n(248),n(249),n(250),n(251),n(252),n(253),n(254),n(255),n(256),n(257),n(258),n(96),n(259),n(122),n(260),n(123),n(261),n(262),n(263),n(264),n(265),n(126),n(128),n(129),n(266),n(267),n(268),n(269),n(270),n(271),n(272),n(273),n(274),n(275),n(276),n(277),n(278),n(279),n(280),n(281),n(282),n(283),n(284),n(285),n(286),n(287),n(288),n(289),n(290),n(291),n(292),n(293),n(294),n(295),n(296),n(297),n(298),n(299),n(300),n(301),n(302),n(303),n(304),n(305),n(306),n(307),n(308),n(309),n(310),n(311),n(312),n(313),n(314),n(315),n(316),n(317),n(318),n(319),n(320),n(321),n(322),n(323),n(324),n(325),n(326),n(327),n(328),n(329),n(330),n(331),n(332),n(333),n(334),n(335),n(336),n(337),n(338),n(339),n(340),n(341),n(342),n(343),n(344),n(345),n(346),n(347),n(348),n(349),n(350),e.exports=n(19)},function(e,t,n){"use strict";var r=n(2),i=n(15),o=n(7),a=n(0),s=n(13),c=n(32).KEY,p=n(3),d=n(51),u=n(46),l=n(36),m=n(5),f=n(104),h=n(77),g=n(157),y=n(61),b=n(1),v=n(4),w=n(9),S=n(16),x=n(24),I=n(35),T=n(39),R=n(107),k=n(17),C=n(60),O=n(8),$=n(37),E=k.f,D=O.f,j=R.f,P=r.Symbol,A=r.JSON,M=A&&A.stringify,N=m("_hidden"),_=m("toPrimitive"),L={}.propertyIsEnumerable,q=d("symbol-registry"),F=d("symbols"),U=d("op-symbols"),B=Object.prototype,W="function"==typeof P&&!!C.f,H=r.QObject,z=!H||!H.prototype||!H.prototype.findChild,V=o&&p(function(){return 7!=T(D({},"a",{get:function(){return D(this,"a",{value:7}).a}})).a})?function(e,t,n){var r=E(B,t);r&&delete B[t],D(e,t,n),r&&e!==B&&D(B,t,r)}:D,G=function(e){var t=F[e]=T(P.prototype);return t._k=e,t},J=W&&"symbol"==typeof P.iterator?function(e){return"symbol"==typeof e}:function(e){return e instanceof P},X=function(e,t,n){return e===B&&X(U,t,n),b(e),t=x(t,!0),b(n),i(F,t)?(n.enumerable?(i(e,N)&&e[N][t]&&(e[N][t]=!1),n=T(n,{enumerable:I(0,!1)})):(i(e,N)||D(e,N,I(1,{})),e[N][t]=!0),V(e,t,n)):D(e,t,n)},Y=function(e,t){b(e);for(var n,r=g(t=S(t)),i=0,o=r.length;o>i;)X(e,n=r[i++],t[n]);return e},K=function(e){var t=L.call(this,e=x(e,!0));return!(this===B&&i(F,e)&&!i(U,e))&&(!(t||!i(this,e)||!i(F,e)||i(this,N)&&this[N][e])||t)},Q=function(e,t){if(e=S(e),t=x(t,!0),e!==B||!i(F,t)||i(U,t)){var n=E(e,t);return!n||!i(F,t)||i(e,N)&&e[N][t]||(n.enumerable=!0),n}},Z=function(e){for(var t,n=j(S(e)),r=[],o=0;n.length>o;)i(F,t=n[o++])||t==N||t==c||r.push(t);return r},ee=function(e){for(var t,n=e===B,r=j(n?U:S(e)),o=[],a=0;r.length>a;)!i(F,t=r[a++])||n&&!i(B,t)||o.push(F[t]);return o};W||(s((P=function(){if(this instanceof P)throw TypeError("Symbol is not a constructor!");var e=l(arguments.length>0?arguments[0]:void 0),t=function(n){this===B&&t.call(U,n),i(this,N)&&i(this[N],e)&&(this[N][e]=!1),V(this,e,I(1,n))};return o&&z&&V(B,e,{configurable:!0,set:t}),G(e)}).prototype,"toString",function(){return this._k}),k.f=Q,O.f=X,n(40).f=R.f=Z,n(53).f=K,C.f=ee,o&&!n(31)&&s(B,"propertyIsEnumerable",K,!0),f.f=function(e){return G(m(e))}),a(a.G+a.W+a.F*!W,{Symbol:P});for(var te="hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","),ne=0;te.length>ne;)m(te[ne++]);for(var re=$(m.store),ie=0;re.length>ie;)h(re[ie++]);a(a.S+a.F*!W,"Symbol",{for:function(e){return i(q,e+="")?q[e]:q[e]=P(e)},keyFor:function(e){if(!J(e))throw TypeError(e+" is not a symbol!");for(var t in q)if(q[t]===e)return t},useSetter:function(){z=!0},useSimple:function(){z=!1}}),a(a.S+a.F*!W,"Object",{create:function(e,t){return void 0===t?T(e):Y(T(e),t)},defineProperty:X,defineProperties:Y,getOwnPropertyDescriptor:Q,getOwnPropertyNames:Z,getOwnPropertySymbols:ee});var oe=p(function(){C.f(1)});a(a.S+a.F*oe,"Object",{getOwnPropertySymbols:function(e){return C.f(w(e))}}),A&&a(a.S+a.F*(!W||p(function(){var e=P();return"[null]"!=M([e])||"{}"!=M({a:e})||"{}"!=M(Object(e))})),"JSON",{stringify:function(e){for(var t,n,r=[e],i=1;arguments.length>i;)r.push(arguments[i++]);if(n=t=r[1],(v(t)||void 0!==e)&&!J(e))return y(t)||(t=function(e,t){if("function"==typeof n&&(t=n.call(this,e,t)),!J(t))return t}),r[1]=t,M.apply(A,r)}}),P.prototype[_]||n(12)(P.prototype,_,P.prototype.valueOf),u(P,"Symbol"),u(Math,"Math",!0),u(r.JSON,"JSON",!0)},function(e,t,n){e.exports=n(51)("native-function-to-string",Function.toString)},function(e,t,n){var r=n(37),i=n(60),o=n(53);e.exports=function(e){var t=r(e),n=i.f;if(n)for(var a,s=n(e),c=o.f,p=0;s.length>p;)c.call(e,a=s[p++])&&t.push(a);return t}},function(e,t,n){var r=n(0);r(r.S,"Object",{create:n(39)})},function(e,t,n){var r=n(0);r(r.S+r.F*!n(7),"Object",{defineProperty:n(8).f})},function(e,t,n){var r=n(0);r(r.S+r.F*!n(7),"Object",{defineProperties:n(106)})},function(e,t,n){var r=n(16),i=n(17).f;n(26)("getOwnPropertyDescriptor",function(){return function(e,t){return i(r(e),t)}})},function(e,t,n){var r=n(9),i=n(18);n(26)("getPrototypeOf",function(){return function(e){return i(r(e))}})},function(e,t,n){var r=n(9),i=n(37);n(26)("keys",function(){return function(e){return i(r(e))}})},function(e,t,n){n(26)("getOwnPropertyNames",function(){return n(107).f})},function(e,t,n){var r=n(4),i=n(32).onFreeze;n(26)("freeze",function(e){return function(t){return e&&r(t)?e(i(t)):t}})},function(e,t,n){var r=n(4),i=n(32).onFreeze;n(26)("seal",function(e){return function(t){return e&&r(t)?e(i(t)):t}})},function(e,t,n){var r=n(4),i=n(32).onFreeze;n(26)("preventExtensions",function(e){return function(t){return e&&r(t)?e(i(t)):t}})},function(e,t,n){var r=n(4);n(26)("isFrozen",function(e){return function(t){return!r(t)||!!e&&e(t)}})},function(e,t,n){var r=n(4);n(26)("isSealed",function(e){return function(t){return!r(t)||!!e&&e(t)}})},function(e,t,n){var r=n(4);n(26)("isExtensible",function(e){return function(t){return!!r(t)&&(!e||e(t))}})},function(e,t,n){var r=n(0);r(r.S+r.F,"Object",{assign:n(108)})},function(e,t,n){var r=n(0);r(r.S,"Object",{is:n(109)})},function(e,t,n){var r=n(0);r(r.S,"Object",{setPrototypeOf:n(81).set})},function(e,t,n){"use strict";var r=n(47),i={};i[n(5)("toStringTag")]="z",i+""!="[object z]"&&n(13)(Object.prototype,"toString",function(){return"[object "+r(this)+"]"},!0)},function(e,t,n){var r=n(0);r(r.P,"Function",{bind:n(110)})},function(e,t,n){var r=n(8).f,i=Function.prototype,o=/^\s*function ([^ (]*)/;"name"in i||n(7)&&r(i,"name",{configurable:!0,get:function(){try{return(""+this).match(o)[1]}catch(e){return""}}})},function(e,t,n){"use strict";var r=n(4),i=n(18),o=n(5)("hasInstance"),a=Function.prototype;o in a||n(8).f(a,o,{value:function(e){if("function"!=typeof this||!r(e))return!1;if(!r(this.prototype))return e instanceof this;for(;e=i(e);)if(this.prototype===e)return!0;return!1}})},function(e,t,n){var r=n(0),i=n(112);r(r.G+r.F*(parseInt!=i),{parseInt:i})},function(e,t,n){var r=n(0),i=n(113);r(r.G+r.F*(parseFloat!=i),{parseFloat:i})},function(e,t,n){"use strict";var r=n(2),i=n(15),o=n(21),a=n(83),s=n(24),c=n(3),p=n(40).f,d=n(17).f,u=n(8).f,l=n(48).trim,m=r.Number,f=m,h=m.prototype,g="Number"==o(n(39)(h)),y="trim"in String.prototype,b=function(e){var t=s(e,!1);if("string"==typeof t&&t.length>2){var n,r,i,o=(t=y?t.trim():l(t,3)).charCodeAt(0);if(43===o||45===o){if(88===(n=t.charCodeAt(2))||120===n)return NaN}else if(48===o){switch(t.charCodeAt(1)){case 66:case 98:r=2,i=49;break;case 79:case 111:r=8,i=55;break;default:return+t}for(var a,c=t.slice(2),p=0,d=c.length;p<d;p++)if((a=c.charCodeAt(p))<48||a>i)return NaN;return parseInt(c,r)}}return+t};if(!m(" 0o1")||!m("0b1")||m("+0x1")){m=function(e){var t=arguments.length<1?0:e,n=this;return n instanceof m&&(g?c(function(){h.valueOf.call(n)}):"Number"!=o(n))?a(new f(b(t)),n,m):b(t)};for(var v,w=n(7)?p(f):"MAX_VALUE,MIN_VALUE,NaN,NEGATIVE_INFINITY,POSITIVE_INFINITY,EPSILON,isFinite,isInteger,isNaN,isSafeInteger,MAX_SAFE_INTEGER,MIN_SAFE_INTEGER,parseFloat,parseInt,isInteger".split(","),S=0;w.length>S;S++)i(f,v=w[S])&&!i(m,v)&&u(m,v,d(f,v));m.prototype=h,h.constructor=m,n(13)(r,"Number",m)}},function(e,t,n){"use strict";var r=n(0),i=n(22),o=n(114),a=n(84),s=1..toFixed,c=Math.floor,p=[0,0,0,0,0,0],d="Number.toFixed: incorrect invocation!",u=function(e,t){for(var n=-1,r=t;++n<6;)r+=e*p[n],p[n]=r%1e7,r=c(r/1e7)},l=function(e){for(var t=6,n=0;--t>=0;)n+=p[t],p[t]=c(n/e),n=n%e*1e7},m=function(){for(var e=6,t="";--e>=0;)if(""!==t||0===e||0!==p[e]){var n=String(p[e]);t=""===t?n:t+a.call("0",7-n.length)+n}return t},f=function(e,t,n){return 0===t?n:t%2==1?f(e,t-1,n*e):f(e*e,t/2,n)};r(r.P+r.F*(!!s&&("0.000"!==8e-5.toFixed(3)||"1"!==.9.toFixed(0)||"1.25"!==1.255.toFixed(2)||"1000000000000000128"!==(0xde0b6b3a7640080).toFixed(0))||!n(3)(function(){s.call({})})),"Number",{toFixed:function(e){var t,n,r,s,c=o(this,d),p=i(e),h="",g="0";if(p<0||p>20)throw RangeError(d);if(c!=c)return"NaN";if(c<=-1e21||c>=1e21)return String(c);if(c<0&&(h="-",c=-c),c>1e-21)if(n=(t=function(e){for(var t=0,n=e;n>=4096;)t+=12,n/=4096;for(;n>=2;)t+=1,n/=2;return t}(c*f(2,69,1))-69)<0?c*f(2,-t,1):c/f(2,t,1),n*=4503599627370496,(t=52-t)>0){for(u(0,n),r=p;r>=7;)u(1e7,0),r-=7;for(u(f(10,r,1),0),r=t-1;r>=23;)l(1<<23),r-=23;l(1<<r),u(1,1),l(2),g=m()}else u(0,n),u(1<<-t,0),g=m()+a.call("0",p);return g=p>0?h+((s=g.length)<=p?"0."+a.call("0",p-s)+g:g.slice(0,s-p)+"."+g.slice(s-p)):h+g}})},function(e,t,n){"use strict";var r=n(0),i=n(3),o=n(114),a=1..toPrecision;r(r.P+r.F*(i(function(){return"1"!==a.call(1,void 0)})||!i(function(){a.call({})})),"Number",{toPrecision:function(e){var t=o(this,"Number#toPrecision: incorrect invocation!");return void 0===e?a.call(t):a.call(t,e)}})},function(e,t,n){var r=n(0);r(r.S,"Number",{EPSILON:Math.pow(2,-52)})},function(e,t,n){var r=n(0),i=n(2).isFinite;r(r.S,"Number",{isFinite:function(e){return"number"==typeof e&&i(e)}})},function(e,t,n){var r=n(0);r(r.S,"Number",{isInteger:n(115)})},function(e,t,n){var r=n(0);r(r.S,"Number",{isNaN:function(e){return e!=e}})},function(e,t,n){var r=n(0),i=n(115),o=Math.abs;r(r.S,"Number",{isSafeInteger:function(e){return i(e)&&o(e)<=9007199254740991}})},function(e,t,n){var r=n(0);r(r.S,"Number",{MAX_SAFE_INTEGER:9007199254740991})},function(e,t,n){var r=n(0);r(r.S,"Number",{MIN_SAFE_INTEGER:-9007199254740991})},function(e,t,n){var r=n(0),i=n(113);r(r.S+r.F*(Number.parseFloat!=i),"Number",{parseFloat:i})},function(e,t,n){var r=n(0),i=n(112);r(r.S+r.F*(Number.parseInt!=i),"Number",{parseInt:i})},function(e,t,n){var r=n(0),i=n(116),o=Math.sqrt,a=Math.acosh;r(r.S+r.F*!(a&&710==Math.floor(a(Number.MAX_VALUE))&&a(1/0)==1/0),"Math",{acosh:function(e){return(e=+e)<1?NaN:e>94906265.62425156?Math.log(e)+Math.LN2:i(e-1+o(e-1)*o(e+1))}})},function(e,t,n){var r=n(0),i=Math.asinh;r(r.S+r.F*!(i&&1/i(0)>0),"Math",{asinh:function e(t){return isFinite(t=+t)&&0!=t?t<0?-e(-t):Math.log(t+Math.sqrt(t*t+1)):t}})},function(e,t,n){var r=n(0),i=Math.atanh;r(r.S+r.F*!(i&&1/i(-0)<0),"Math",{atanh:function(e){return 0==(e=+e)?e:Math.log((1+e)/(1-e))/2}})},function(e,t,n){var r=n(0),i=n(85);r(r.S,"Math",{cbrt:function(e){return i(e=+e)*Math.pow(Math.abs(e),1/3)}})},function(e,t,n){var r=n(0);r(r.S,"Math",{clz32:function(e){return(e>>>=0)?31-Math.floor(Math.log(e+.5)*Math.LOG2E):32}})},function(e,t,n){var r=n(0),i=Math.exp;r(r.S,"Math",{cosh:function(e){return(i(e=+e)+i(-e))/2}})},function(e,t,n){var r=n(0),i=n(86);r(r.S+r.F*(i!=Math.expm1),"Math",{expm1:i})},function(e,t,n){var r=n(0);r(r.S,"Math",{fround:n(117)})},function(e,t,n){var r=n(0),i=Math.abs;r(r.S,"Math",{hypot:function(e,t){for(var n,r,o=0,a=0,s=arguments.length,c=0;a<s;)c<(n=i(arguments[a++]))?(o=o*(r=c/n)*r+1,c=n):o+=n>0?(r=n/c)*r:n;return c===1/0?1/0:c*Math.sqrt(o)}})},function(e,t,n){var r=n(0),i=Math.imul;r(r.S+r.F*n(3)(function(){return-5!=i(4294967295,5)||2!=i.length}),"Math",{imul:function(e,t){var n=+e,r=+t,i=65535&n,o=65535&r;return 0|i*o+((65535&n>>>16)*o+i*(65535&r>>>16)<<16>>>0)}})},function(e,t,n){var r=n(0);r(r.S,"Math",{log10:function(e){return Math.log(e)*Math.LOG10E}})},function(e,t,n){var r=n(0);r(r.S,"Math",{log1p:n(116)})},function(e,t,n){var r=n(0);r(r.S,"Math",{log2:function(e){return Math.log(e)/Math.LN2}})},function(e,t,n){var r=n(0);r(r.S,"Math",{sign:n(85)})},function(e,t,n){var r=n(0),i=n(86),o=Math.exp;r(r.S+r.F*n(3)(function(){return-2e-17!=!Math.sinh(-2e-17)}),"Math",{sinh:function(e){return Math.abs(e=+e)<1?(i(e)-i(-e))/2:(o(e-1)-o(-e-1))*(Math.E/2)}})},function(e,t,n){var r=n(0),i=n(86),o=Math.exp;r(r.S,"Math",{tanh:function(e){var t=i(e=+e),n=i(-e);return t==1/0?1:n==1/0?-1:(t-n)/(o(e)+o(-e))}})},function(e,t,n){var r=n(0);r(r.S,"Math",{trunc:function(e){return(e>0?Math.floor:Math.ceil)(e)}})},function(e,t,n){var r=n(0),i=n(38),o=String.fromCharCode,a=String.fromCodePoint;r(r.S+r.F*(!!a&&1!=a.length),"String",{fromCodePoint:function(e){for(var t,n=[],r=arguments.length,a=0;r>a;){if(t=+arguments[a++],i(t,1114111)!==t)throw RangeError(t+" is not a valid code point");n.push(t<65536?o(t):o(55296+((t-=65536)>>10),t%1024+56320))}return n.join("")}})},function(e,t,n){var r=n(0),i=n(16),o=n(6);r(r.S,"String",{raw:function(e){for(var t=i(e.raw),n=o(t.length),r=arguments.length,a=[],s=0;n>s;)a.push(String(t[s++])),s<r&&a.push(String(arguments[s]));return a.join("")}})},function(e,t,n){"use strict";n(48)("trim",function(e){return function(){return e(this,3)}})},function(e,t,n){"use strict";var r=n(62)(!0);n(87)(String,"String",function(e){this._t=String(e),this._i=0},function(){var e,t=this._t,n=this._i;return n>=t.length?{value:void 0,done:!0}:(e=r(t,n),this._i+=e.length,{value:e,done:!1})})},function(e,t,n){"use strict";var r=n(0),i=n(62)(!1);r(r.P,"String",{codePointAt:function(e){return i(this,e)}})},function(e,t,n){"use strict";var r=n(0),i=n(6),o=n(89),a="".endsWith;r(r.P+r.F*n(90)("endsWith"),"String",{endsWith:function(e){var t=o(this,e,"endsWith"),n=arguments.length>1?arguments[1]:void 0,r=i(t.length),s=void 0===n?r:Math.min(i(n),r),c=String(e);return a?a.call(t,c,s):t.slice(s-c.length,s)===c}})},function(e,t,n){"use strict";var r=n(0),i=n(89);r(r.P+r.F*n(90)("includes"),"String",{includes:function(e){return!!~i(this,e,"includes").indexOf(e,arguments.length>1?arguments[1]:void 0)}})},function(e,t,n){var r=n(0);r(r.P,"String",{repeat:n(84)})},function(e,t,n){"use strict";var r=n(0),i=n(6),o=n(89),a="".startsWith;r(r.P+r.F*n(90)("startsWith"),"String",{startsWith:function(e){var t=o(this,e,"startsWith"),n=i(Math.min(arguments.length>1?arguments[1]:void 0,t.length)),r=String(e);return a?a.call(t,r,n):t.slice(n,n+r.length)===r}})},function(e,t,n){"use strict";n(14)("anchor",function(e){return function(t){return e(this,"a","name",t)}})},function(e,t,n){"use strict";n(14)("big",function(e){return function(){return e(this,"big","","")}})},function(e,t,n){"use strict";n(14)("blink",function(e){return function(){return e(this,"blink","","")}})},function(e,t,n){"use strict";n(14)("bold",function(e){return function(){return e(this,"b","","")}})},function(e,t,n){"use strict";n(14)("fixed",function(e){return function(){return e(this,"tt","","")}})},function(e,t,n){"use strict";n(14)("fontcolor",function(e){return function(t){return e(this,"font","color",t)}})},function(e,t,n){"use strict";n(14)("fontsize",function(e){return function(t){return e(this,"font","size",t)}})},function(e,t,n){"use strict";n(14)("italics",function(e){return function(){return e(this,"i","","")}})},function(e,t,n){"use strict";n(14)("link",function(e){return function(t){return e(this,"a","href",t)}})},function(e,t,n){"use strict";n(14)("small",function(e){return function(){return e(this,"small","","")}})},function(e,t,n){"use strict";n(14)("strike",function(e){return function(){return e(this,"strike","","")}})},function(e,t,n){"use strict";n(14)("sub",function(e){return function(){return e(this,"sub","","")}})},function(e,t,n){"use strict";n(14)("sup",function(e){return function(){return e(this,"sup","","")}})},function(e,t,n){var r=n(0);r(r.S,"Date",{now:function(){return(new Date).getTime()}})},function(e,t,n){"use strict";var r=n(0),i=n(9),o=n(24);r(r.P+r.F*n(3)(function(){return null!==new Date(NaN).toJSON()||1!==Date.prototype.toJSON.call({toISOString:function(){return 1}})}),"Date",{toJSON:function(e){var t=i(this),n=o(t);return"number"!=typeof n||isFinite(n)?t.toISOString():null}})},function(e,t,n){var r=n(0),i=n(234);r(r.P+r.F*(Date.prototype.toISOString!==i),"Date",{toISOString:i})},function(e,t,n){"use strict";var r=n(3),i=Date.prototype.getTime,o=Date.prototype.toISOString,a=function(e){return e>9?e:"0"+e};e.exports=r(function(){return"0385-07-25T07:06:39.999Z"!=o.call(new Date(-5e13-1))})||!r(function(){o.call(new Date(NaN))})?function(){if(!isFinite(i.call(this)))throw RangeError("Invalid time value");var e=this,t=e.getUTCFullYear(),n=e.getUTCMilliseconds(),r=t<0?"-":t>9999?"+":"";return r+("00000"+Math.abs(t)).slice(r?-6:-4)+"-"+a(e.getUTCMonth()+1)+"-"+a(e.getUTCDate())+"T"+a(e.getUTCHours())+":"+a(e.getUTCMinutes())+":"+a(e.getUTCSeconds())+"."+(n>99?n:"0"+a(n))+"Z"}:o},function(e,t,n){var r=Date.prototype,i=r.toString,o=r.getTime;new Date(NaN)+""!="Invalid Date"&&n(13)(r,"toString",function(){var e=o.call(this);return e==e?i.call(this):"Invalid Date"})},function(e,t,n){var r=n(5)("toPrimitive"),i=Date.prototype;r in i||n(12)(i,r,n(237))},function(e,t,n){"use strict";var r=n(1),i=n(24);e.exports=function(e){if("string"!==e&&"number"!==e&&"default"!==e)throw TypeError("Incorrect hint");return i(r(this),"number"!=e)}},function(e,t,n){var r=n(0);r(r.S,"Array",{isArray:n(61)})},function(e,t,n){"use strict";var r=n(20),i=n(0),o=n(9),a=n(118),s=n(91),c=n(6),p=n(92),d=n(93);i(i.S+i.F*!n(64)(function(e){Array.from(e)}),"Array",{from:function(e){var t,n,i,u,l=o(e),m="function"==typeof this?this:Array,f=arguments.length,h=f>1?arguments[1]:void 0,g=void 0!==h,y=0,b=d(l);if(g&&(h=r(h,f>2?arguments[2]:void 0,2)),null==b||m==Array&&s(b))for(n=new m(t=c(l.length));t>y;y++)p(n,y,g?h(l[y],y):l[y]);else for(u=b.call(l),n=new m;!(i=u.next()).done;y++)p(n,y,g?a(u,h,[i.value,y],!0):i.value);return n.length=y,n}})},function(e,t,n){"use strict";var r=n(0),i=n(92);r(r.S+r.F*n(3)(function(){function e(){}return!(Array.of.call(e)instanceof e)}),"Array",{of:function(){for(var e=0,t=arguments.length,n=new("function"==typeof this?this:Array)(t);t>e;)i(n,e,arguments[e++]);return n.length=t,n}})},function(e,t,n){"use strict";var r=n(0),i=n(16),o=[].join;r(r.P+r.F*(n(52)!=Object||!n(23)(o)),"Array",{join:function(e){return o.call(i(this),void 0===e?",":e)}})},function(e,t,n){"use strict";var r=n(0),i=n(80),o=n(21),a=n(38),s=n(6),c=[].slice;r(r.P+r.F*n(3)(function(){i&&c.call(i)}),"Array",{slice:function(e,t){var n=s(this.length),r=o(this);if(t=void 0===t?n:t,"Array"==r)return c.call(this,e,t);for(var i=a(e,n),p=a(t,n),d=s(p-i),u=new Array(d),l=0;l<d;l++)u[l]="String"==r?this.charAt(i+l):this[i+l];return u}})},function(e,t,n){"use strict";var r=n(0),i=n(10),o=n(9),a=n(3),s=[].sort,c=[1,2,3];r(r.P+r.F*(a(function(){c.sort(void 0)})||!a(function(){c.sort(null)})||!n(23)(s)),"Array",{sort:function(e){return void 0===e?s.call(o(this)):s.call(o(this),i(e))}})},function(e,t,n){"use strict";var r=n(0),i=n(27)(0),o=n(23)([].forEach,!0);r(r.P+r.F*!o,"Array",{forEach:function(e){return i(this,e,arguments[1])}})},function(e,t,n){var r=n(4),i=n(61),o=n(5)("species");e.exports=function(e){var t;return i(e)&&("function"!=typeof(t=e.constructor)||t!==Array&&!i(t.prototype)||(t=void 0),r(t)&&null===(t=t[o])&&(t=void 0)),void 0===t?Array:t}},function(e,t,n){"use strict";var r=n(0),i=n(27)(1);r(r.P+r.F*!n(23)([].map,!0),"Array",{map:function(e){return i(this,e,arguments[1])}})},function(e,t,n){"use strict";var r=n(0),i=n(27)(2);r(r.P+r.F*!n(23)([].filter,!0),"Array",{filter:function(e){return i(this,e,arguments[1])}})},function(e,t,n){"use strict";var r=n(0),i=n(27)(3);r(r.P+r.F*!n(23)([].some,!0),"Array",{some:function(e){return i(this,e,arguments[1])}})},function(e,t,n){"use strict";var r=n(0),i=n(27)(4);r(r.P+r.F*!n(23)([].every,!0),"Array",{every:function(e){return i(this,e,arguments[1])}})},function(e,t,n){"use strict";var r=n(0),i=n(119);r(r.P+r.F*!n(23)([].reduce,!0),"Array",{reduce:function(e){return i(this,e,arguments.length,arguments[1],!1)}})},function(e,t,n){"use strict";var r=n(0),i=n(119);r(r.P+r.F*!n(23)([].reduceRight,!0),"Array",{reduceRight:function(e){return i(this,e,arguments.length,arguments[1],!0)}})},function(e,t,n){"use strict";var r=n(0),i=n(59)(!1),o=[].indexOf,a=!!o&&1/[1].indexOf(1,-0)<0;r(r.P+r.F*(a||!n(23)(o)),"Array",{indexOf:function(e){return a?o.apply(this,arguments)||0:i(this,e,arguments[1])}})},function(e,t,n){"use strict";var r=n(0),i=n(16),o=n(22),a=n(6),s=[].lastIndexOf,c=!!s&&1/[1].lastIndexOf(1,-0)<0;r(r.P+r.F*(c||!n(23)(s)),"Array",{lastIndexOf:function(e){if(c)return s.apply(this,arguments)||0;var t=i(this),n=a(t.length),r=n-1;for(arguments.length>1&&(r=Math.min(r,o(arguments[1]))),r<0&&(r=n+r);r>=0;r--)if(r in t&&t[r]===e)return r||0;return-1}})},function(e,t,n){var r=n(0);r(r.P,"Array",{copyWithin:n(120)}),n(33)("copyWithin")},function(e,t,n){var r=n(0);r(r.P,"Array",{fill:n(95)}),n(33)("fill")},function(e,t,n){"use strict";var r=n(0),i=n(27)(5),o=!0;"find"in[]&&Array(1).find(function(){o=!1}),r(r.P+r.F*o,"Array",{find:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),n(33)("find")},function(e,t,n){"use strict";var r=n(0),i=n(27)(6),o="findIndex",a=!0;o in[]&&Array(1)[o](function(){a=!1}),r(r.P+r.F*a,"Array",{findIndex:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),n(33)(o)},function(e,t,n){n(41)("Array")},function(e,t,n){var r=n(2),i=n(83),o=n(8).f,a=n(40).f,s=n(63),c=n(54),p=r.RegExp,d=p,u=p.prototype,l=/a/g,m=/a/g,f=new p(l)!==l;if(n(7)&&(!f||n(3)(function(){return m[n(5)("match")]=!1,p(l)!=l||p(m)==m||"/a/i"!=p(l,"i")}))){p=function(e,t){var n=this instanceof p,r=s(e),o=void 0===t;return!n&&r&&e.constructor===p&&o?e:i(f?new d(r&&!o?e.source:e,t):d((r=e instanceof p)?e.source:e,r&&o?c.call(e):t),n?this:u,p)};for(var h=function(e){e in p||o(p,e,{configurable:!0,get:function(){return d[e]},set:function(t){d[e]=t}})},g=a(d),y=0;g.length>y;)h(g[y++]);u.constructor=p,p.prototype=u,n(13)(r,"RegExp",p)}n(41)("RegExp")},function(e,t,n){"use strict";n(123);var r=n(1),i=n(54),o=n(7),a=/./.toString,s=function(e){n(13)(RegExp.prototype,"toString",e,!0)};n(3)(function(){return"/a/b"!=a.call({source:"a",flags:"b"})})?s(function(){var e=r(this);return"/".concat(e.source,"/","flags"in e?e.flags:!o&&e instanceof RegExp?i.call(e):void 0)}):"toString"!=a.name&&s(function(){return a.call(this)})},function(e,t,n){"use strict";var r=n(1),i=n(6),o=n(98),a=n(65);n(66)("match",1,function(e,t,n,s){return[function(n){var r=e(this),i=null==n?void 0:n[t];return void 0!==i?i.call(n,r):new RegExp(n)[t](String(r))},function(e){var t=s(n,e,this);if(t.done)return t.value;var c=r(e),p=String(this);if(!c.global)return a(c,p);var d=c.unicode;c.lastIndex=0;for(var u,l=[],m=0;null!==(u=a(c,p));){var f=String(u[0]);l[m]=f,""===f&&(c.lastIndex=o(p,i(c.lastIndex),d)),m++}return 0===m?null:l}]})},function(e,t,n){"use strict";var r=n(1),i=n(9),o=n(6),a=n(22),s=n(98),c=n(65),p=Math.max,d=Math.min,u=Math.floor,l=/\$([$&`']|\d\d?|<[^>]*>)/g,m=/\$([$&`']|\d\d?)/g;n(66)("replace",2,function(e,t,n,f){return[function(r,i){var o=e(this),a=null==r?void 0:r[t];return void 0!==a?a.call(r,o,i):n.call(String(o),r,i)},function(e,t){var i=f(n,e,this,t);if(i.done)return i.value;var u=r(e),l=String(this),m="function"==typeof t;m||(t=String(t));var g=u.global;if(g){var y=u.unicode;u.lastIndex=0}for(var b=[];;){var v=c(u,l);if(null===v)break;if(b.push(v),!g)break;""===String(v[0])&&(u.lastIndex=s(l,o(u.lastIndex),y))}for(var w,S="",x=0,I=0;I<b.length;I++){v=b[I];for(var T=String(v[0]),R=p(d(a(v.index),l.length),0),k=[],C=1;C<v.length;C++)k.push(void 0===(w=v[C])?w:String(w));var O=v.groups;if(m){var $=[T].concat(k,R,l);void 0!==O&&$.push(O);var E=String(t.apply(void 0,$))}else E=h(T,l,R,k,O,t);R>=x&&(S+=l.slice(x,R)+E,x=R+T.length)}return S+l.slice(x)}];function h(e,t,r,o,a,s){var c=r+e.length,p=o.length,d=m;return void 0!==a&&(a=i(a),d=l),n.call(s,d,function(n,i){var s;switch(i.charAt(0)){case"$":return"$";case"&":return e;case"`":return t.slice(0,r);case"'":return t.slice(c);case"<":s=a[i.slice(1,-1)];break;default:var d=+i;if(0===d)return n;if(d>p){var l=u(d/10);return 0===l?n:l<=p?void 0===o[l-1]?i.charAt(1):o[l-1]+i.charAt(1):n}s=o[d-1]}return void 0===s?"":s})}})},function(e,t,n){"use strict";var r=n(1),i=n(109),o=n(65);n(66)("search",1,function(e,t,n,a){return[function(n){var r=e(this),i=null==n?void 0:n[t];return void 0!==i?i.call(n,r):new RegExp(n)[t](String(r))},function(e){var t=a(n,e,this);if(t.done)return t.value;var s=r(e),c=String(this),p=s.lastIndex;i(p,0)||(s.lastIndex=0);var d=o(s,c);return i(s.lastIndex,p)||(s.lastIndex=p),null===d?-1:d.index}]})},function(e,t,n){"use strict";var r=n(63),i=n(1),o=n(55),a=n(98),s=n(6),c=n(65),p=n(97),d=n(3),u=Math.min,l=[].push,m=!d(function(){RegExp(4294967295,"y")});n(66)("split",2,function(e,t,n,d){var f;return f="c"=="abbc".split(/(b)*/)[1]||4!="test".split(/(?:)/,-1).length||2!="ab".split(/(?:ab)*/).length||4!=".".split(/(.?)(.?)/).length||".".split(/()()/).length>1||"".split(/.?/).length?function(e,t){var i=String(this);if(void 0===e&&0===t)return[];if(!r(e))return n.call(i,e,t);for(var o,a,s,c=[],d=(e.ignoreCase?"i":"")+(e.multiline?"m":"")+(e.unicode?"u":"")+(e.sticky?"y":""),u=0,m=void 0===t?4294967295:t>>>0,f=new RegExp(e.source,d+"g");(o=p.call(f,i))&&!((a=f.lastIndex)>u&&(c.push(i.slice(u,o.index)),o.length>1&&o.index<i.length&&l.apply(c,o.slice(1)),s=o[0].length,u=a,c.length>=m));)f.lastIndex===o.index&&f.lastIndex++;return u===i.length?!s&&f.test("")||c.push(""):c.push(i.slice(u)),c.length>m?c.slice(0,m):c}:"0".split(void 0,0).length?function(e,t){return void 0===e&&0===t?[]:n.call(this,e,t)}:n,[function(n,r){var i=e(this),o=null==n?void 0:n[t];return void 0!==o?o.call(n,i,r):f.call(String(i),n,r)},function(e,t){var r=d(f,e,this,t,f!==n);if(r.done)return r.value;var p=i(e),l=String(this),h=o(p,RegExp),g=p.unicode,y=(p.ignoreCase?"i":"")+(p.multiline?"m":"")+(p.unicode?"u":"")+(m?"y":"g"),b=new h(m?p:"^(?:"+p.source+")",y),v=void 0===t?4294967295:t>>>0;if(0===v)return[];if(0===l.length)return null===c(b,l)?[l]:[];for(var w=0,S=0,x=[];S<l.length;){b.lastIndex=m?S:0;var I,T=c(b,m?l:l.slice(S));if(null===T||(I=u(s(b.lastIndex+(m?0:S)),l.length))===w)S=a(l,S,g);else{if(x.push(l.slice(w,S)),x.length===v)return x;for(var R=1;R<=T.length-1;R++)if(x.push(T[R]),x.length===v)return x;S=w=I}}return x.push(l.slice(w)),x}]})},function(e,t,n){"use strict";var r,i,o,a,s=n(31),c=n(2),p=n(20),d=n(47),u=n(0),l=n(4),m=n(10),f=n(42),h=n(43),g=n(55),y=n(99).set,b=n(100)(),v=n(101),w=n(124),S=n(67),x=n(125),I=c.TypeError,T=c.process,R=T&&T.versions,k=R&&R.v8||"",C=c.Promise,O="process"==d(T),$=function(){},E=i=v.f,D=!!function(){try{var e=C.resolve(1),t=(e.constructor={})[n(5)("species")]=function(e){e($,$)};return(O||"function"==typeof PromiseRejectionEvent)&&e.then($)instanceof t&&0!==k.indexOf("6.6")&&-1===S.indexOf("Chrome/66")}catch(e){}}(),j=function(e){var t;return!(!l(e)||"function"!=typeof(t=e.then))&&t},P=function(e,t){if(!e._n){e._n=!0;var n=e._c;b(function(){for(var r=e._v,i=1==e._s,o=0,a=function(t){var n,o,a,s=i?t.ok:t.fail,c=t.resolve,p=t.reject,d=t.domain;try{s?(i||(2==e._h&&N(e),e._h=1),!0===s?n=r:(d&&d.enter(),n=s(r),d&&(d.exit(),a=!0)),n===t.promise?p(I("Promise-chain cycle")):(o=j(n))?o.call(n,c,p):c(n)):p(r)}catch(e){d&&!a&&d.exit(),p(e)}};n.length>o;)a(n[o++]);e._c=[],e._n=!1,t&&!e._h&&A(e)})}},A=function(e){y.call(c,function(){var t,n,r,i=e._v,o=M(e);if(o&&(t=w(function(){O?T.emit("unhandledRejection",i,e):(n=c.onunhandledrejection)?n({promise:e,reason:i}):(r=c.console)&&r.error&&r.error("Unhandled promise rejection",i)}),e._h=O||M(e)?2:1),e._a=void 0,o&&t.e)throw t.v})},M=function(e){return 1!==e._h&&0===(e._a||e._c).length},N=function(e){y.call(c,function(){var t;O?T.emit("rejectionHandled",e):(t=c.onrejectionhandled)&&t({promise:e,reason:e._v})})},_=function(e){var t=this;t._d||(t._d=!0,(t=t._w||t)._v=e,t._s=2,t._a||(t._a=t._c.slice()),P(t,!0))},L=function(e){var t,n=this;if(!n._d){n._d=!0,n=n._w||n;try{if(n===e)throw I("Promise can't be resolved itself");(t=j(e))?b(function(){var r={_w:n,_d:!1};try{t.call(e,p(L,r,1),p(_,r,1))}catch(e){_.call(r,e)}}):(n._v=e,n._s=1,P(n,!1))}catch(e){_.call({_w:n,_d:!1},e)}}};D||(C=function(e){f(this,C,"Promise","_h"),m(e),r.call(this);try{e(p(L,this,1),p(_,this,1))}catch(e){_.call(this,e)}},(r=function(e){this._c=[],this._a=void 0,this._s=0,this._d=!1,this._v=void 0,this._h=0,this._n=!1}).prototype=n(44)(C.prototype,{then:function(e,t){var n=E(g(this,C));return n.ok="function"!=typeof e||e,n.fail="function"==typeof t&&t,n.domain=O?T.domain:void 0,this._c.push(n),this._a&&this._a.push(n),this._s&&P(this,!1),n.promise},catch:function(e){return this.then(void 0,e)}}),o=function(){var e=new r;this.promise=e,this.resolve=p(L,e,1),this.reject=p(_,e,1)},v.f=E=function(e){return e===C||e===a?new o(e):i(e)}),u(u.G+u.W+u.F*!D,{Promise:C}),n(46)(C,"Promise"),n(41)("Promise"),a=n(19).Promise,u(u.S+u.F*!D,"Promise",{reject:function(e){var t=E(this);return(0,t.reject)(e),t.promise}}),u(u.S+u.F*(s||!D),"Promise",{resolve:function(e){return x(s&&this===a?C:this,e)}}),u(u.S+u.F*!(D&&n(64)(function(e){C.all(e).catch($)})),"Promise",{all:function(e){var t=this,n=E(t),r=n.resolve,i=n.reject,o=w(function(){var n=[],o=0,a=1;h(e,!1,function(e){var s=o++,c=!1;n.push(void 0),a++,t.resolve(e).then(function(e){c||(c=!0,n[s]=e,--a||r(n))},i)}),--a||r(n)});return o.e&&i(o.v),n.promise},race:function(e){var t=this,n=E(t),r=n.reject,i=w(function(){h(e,!1,function(e){t.resolve(e).then(n.resolve,r)})});return i.e&&r(i.v),n.promise}})},function(e,t,n){"use strict";var r=n(130),i=n(45);n(68)("WeakSet",function(e){return function(){return e(this,arguments.length>0?arguments[0]:void 0)}},{add:function(e){return r.def(i(this,"WeakSet"),e,!0)}},r,!1,!0)},function(e,t,n){"use strict";var r=n(0),i=n(69),o=n(102),a=n(1),s=n(38),c=n(6),p=n(4),d=n(2).ArrayBuffer,u=n(55),l=o.ArrayBuffer,m=o.DataView,f=i.ABV&&d.isView,h=l.prototype.slice,g=i.VIEW;r(r.G+r.W+r.F*(d!==l),{ArrayBuffer:l}),r(r.S+r.F*!i.CONSTR,"ArrayBuffer",{isView:function(e){return f&&f(e)||p(e)&&g in e}}),r(r.P+r.U+r.F*n(3)(function(){return!new l(2).slice(1,void 0).byteLength}),"ArrayBuffer",{slice:function(e,t){if(void 0!==h&&void 0===t)return h.call(a(this),e);for(var n=a(this).byteLength,r=s(e,n),i=s(void 0===t?n:t,n),o=new(u(this,l))(c(i-r)),p=new m(this),d=new m(o),f=0;r<i;)d.setUint8(f++,p.getUint8(r++));return o}}),n(41)("ArrayBuffer")},function(e,t,n){var r=n(0);r(r.G+r.W+r.F*!n(69).ABV,{DataView:n(102).DataView})},function(e,t,n){n(28)("Int8",1,function(e){return function(t,n,r){return e(this,t,n,r)}})},function(e,t,n){n(28)("Uint8",1,function(e){return function(t,n,r){return e(this,t,n,r)}})},function(e,t,n){n(28)("Uint8",1,function(e){return function(t,n,r){return e(this,t,n,r)}},!0)},function(e,t,n){n(28)("Int16",2,function(e){return function(t,n,r){return e(this,t,n,r)}})},function(e,t,n){n(28)("Uint16",2,function(e){return function(t,n,r){return e(this,t,n,r)}})},function(e,t,n){n(28)("Int32",4,function(e){return function(t,n,r){return e(this,t,n,r)}})},function(e,t,n){n(28)("Uint32",4,function(e){return function(t,n,r){return e(this,t,n,r)}})},function(e,t,n){n(28)("Float32",4,function(e){return function(t,n,r){return e(this,t,n,r)}})},function(e,t,n){n(28)("Float64",8,function(e){return function(t,n,r){return e(this,t,n,r)}})},function(e,t,n){var r=n(0),i=n(10),o=n(1),a=(n(2).Reflect||{}).apply,s=Function.apply;r(r.S+r.F*!n(3)(function(){a(function(){})}),"Reflect",{apply:function(e,t,n){var r=i(e),c=o(n);return a?a(r,t,c):s.call(r,t,c)}})},function(e,t,n){var r=n(0),i=n(39),o=n(10),a=n(1),s=n(4),c=n(3),p=n(110),d=(n(2).Reflect||{}).construct,u=c(function(){function e(){}return!(d(function(){},[],e)instanceof e)}),l=!c(function(){d(function(){})});r(r.S+r.F*(u||l),"Reflect",{construct:function(e,t){o(e),a(t);var n=arguments.length<3?e:o(arguments[2]);if(l&&!u)return d(e,t,n);if(e==n){switch(t.length){case 0:return new e;case 1:return new e(t[0]);case 2:return new e(t[0],t[1]);case 3:return new e(t[0],t[1],t[2]);case 4:return new e(t[0],t[1],t[2],t[3])}var r=[null];return r.push.apply(r,t),new(p.apply(e,r))}var c=n.prototype,m=i(s(c)?c:Object.prototype),f=Function.apply.call(e,m,t);return s(f)?f:m}})},function(e,t,n){var r=n(8),i=n(0),o=n(1),a=n(24);i(i.S+i.F*n(3)(function(){Reflect.defineProperty(r.f({},1,{value:1}),1,{value:2})}),"Reflect",{defineProperty:function(e,t,n){o(e),t=a(t,!0),o(n);try{return r.f(e,t,n),!0}catch(e){return!1}}})},function(e,t,n){var r=n(0),i=n(17).f,o=n(1);r(r.S,"Reflect",{deleteProperty:function(e,t){var n=i(o(e),t);return!(n&&!n.configurable)&&delete e[t]}})},function(e,t,n){"use strict";var r=n(0),i=n(1),o=function(e){this._t=i(e),this._i=0;var t,n=this._k=[];for(t in e)n.push(t)};n(88)(o,"Object",function(){var e,t=this._k;do{if(this._i>=t.length)return{value:void 0,done:!0}}while(!((e=t[this._i++])in this._t));return{value:e,done:!1}}),r(r.S,"Reflect",{enumerate:function(e){return new o(e)}})},function(e,t,n){var r=n(17),i=n(18),o=n(15),a=n(0),s=n(4),c=n(1);a(a.S,"Reflect",{get:function e(t,n){var a,p,d=arguments.length<3?t:arguments[2];return c(t)===d?t[n]:(a=r.f(t,n))?o(a,"value")?a.value:void 0!==a.get?a.get.call(d):void 0:s(p=i(t))?e(p,n,d):void 0}})},function(e,t,n){var r=n(17),i=n(0),o=n(1);i(i.S,"Reflect",{getOwnPropertyDescriptor:function(e,t){return r.f(o(e),t)}})},function(e,t,n){var r=n(0),i=n(18),o=n(1);r(r.S,"Reflect",{getPrototypeOf:function(e){return i(o(e))}})},function(e,t,n){var r=n(0);r(r.S,"Reflect",{has:function(e,t){return t in e}})},function(e,t,n){var r=n(0),i=n(1),o=Object.isExtensible;r(r.S,"Reflect",{isExtensible:function(e){return i(e),!o||o(e)}})},function(e,t,n){var r=n(0);r(r.S,"Reflect",{ownKeys:n(132)})},function(e,t,n){var r=n(0),i=n(1),o=Object.preventExtensions;r(r.S,"Reflect",{preventExtensions:function(e){i(e);try{return o&&o(e),!0}catch(e){return!1}}})},function(e,t,n){var r=n(8),i=n(17),o=n(18),a=n(15),s=n(0),c=n(35),p=n(1),d=n(4);s(s.S,"Reflect",{set:function e(t,n,s){var u,l,m=arguments.length<4?t:arguments[3],f=i.f(p(t),n);if(!f){if(d(l=o(t)))return e(l,n,s,m);f=c(0)}if(a(f,"value")){if(!1===f.writable||!d(m))return!1;if(u=i.f(m,n)){if(u.get||u.set||!1===u.writable)return!1;u.value=s,r.f(m,n,u)}else r.f(m,n,c(0,s));return!0}return void 0!==f.set&&(f.set.call(m,s),!0)}})},function(e,t,n){var r=n(0),i=n(81);i&&r(r.S,"Reflect",{setPrototypeOf:function(e,t){i.check(e,t);try{return i.set(e,t),!0}catch(e){return!1}}})},function(e,t,n){"use strict";var r=n(0),i=n(59)(!0);r(r.P,"Array",{includes:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),n(33)("includes")},function(e,t,n){"use strict";var r=n(0),i=n(133),o=n(9),a=n(6),s=n(10),c=n(94);r(r.P,"Array",{flatMap:function(e){var t,n,r=o(this);return s(e),t=a(r.length),n=c(r,0),i(n,r,r,t,0,1,e,arguments[1]),n}}),n(33)("flatMap")},function(e,t,n){"use strict";var r=n(0),i=n(133),o=n(9),a=n(6),s=n(22),c=n(94);r(r.P,"Array",{flatten:function(){var e=arguments[0],t=o(this),n=a(t.length),r=c(t,0);return i(r,t,t,n,0,void 0===e?1:s(e)),r}}),n(33)("flatten")},function(e,t,n){"use strict";var r=n(0),i=n(62)(!0);r(r.P,"String",{at:function(e){return i(this,e)}})},function(e,t,n){"use strict";var r=n(0),i=n(134),o=n(67),a=/Version\/10\.\d+(\.\d+)?( Mobile\/\w+)? Safari\//.test(o);r(r.P+r.F*a,"String",{padStart:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0,!0)}})},function(e,t,n){"use strict";var r=n(0),i=n(134),o=n(67),a=/Version\/10\.\d+(\.\d+)?( Mobile\/\w+)? Safari\//.test(o);r(r.P+r.F*a,"String",{padEnd:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0,!1)}})},function(e,t,n){"use strict";n(48)("trimLeft",function(e){return function(){return e(this,1)}},"trimStart")},function(e,t,n){"use strict";n(48)("trimRight",function(e){return function(){return e(this,2)}},"trimEnd")},function(e,t,n){"use strict";var r=n(0),i=n(25),o=n(6),a=n(63),s=n(54),c=RegExp.prototype,p=function(e,t){this._r=e,this._s=t};n(88)(p,"RegExp String",function(){var e=this._r.exec(this._s);return{value:e,done:null===e}}),r(r.P,"String",{matchAll:function(e){if(i(this),!a(e))throw TypeError(e+" is not a regexp!");var t=String(this),n="flags"in c?String(e.flags):s.call(e),r=new RegExp(e.source,~n.indexOf("g")?n:"g"+n);return r.lastIndex=o(e.lastIndex),new p(r,t)}})},function(e,t,n){n(77)("asyncIterator")},function(e,t,n){n(77)("observable")},function(e,t,n){var r=n(0),i=n(132),o=n(16),a=n(17),s=n(92);r(r.S,"Object",{getOwnPropertyDescriptors:function(e){for(var t,n,r=o(e),c=a.f,p=i(r),d={},u=0;p.length>u;)void 0!==(n=c(r,t=p[u++]))&&s(d,t,n);return d}})},function(e,t,n){var r=n(0),i=n(135)(!1);r(r.S,"Object",{values:function(e){return i(e)}})},function(e,t,n){var r=n(0),i=n(135)(!0);r(r.S,"Object",{entries:function(e){return i(e)}})},function(e,t,n){"use strict";var r=n(0),i=n(9),o=n(10),a=n(8);n(7)&&r(r.P+n(70),"Object",{__defineGetter__:function(e,t){a.f(i(this),e,{get:o(t),enumerable:!0,configurable:!0})}})},function(e,t,n){"use strict";var r=n(0),i=n(9),o=n(10),a=n(8);n(7)&&r(r.P+n(70),"Object",{__defineSetter__:function(e,t){a.f(i(this),e,{set:o(t),enumerable:!0,configurable:!0})}})},function(e,t,n){"use strict";var r=n(0),i=n(9),o=n(24),a=n(18),s=n(17).f;n(7)&&r(r.P+n(70),"Object",{__lookupGetter__:function(e){var t,n=i(this),r=o(e,!0);do{if(t=s(n,r))return t.get}while(n=a(n))}})},function(e,t,n){"use strict";var r=n(0),i=n(9),o=n(24),a=n(18),s=n(17).f;n(7)&&r(r.P+n(70),"Object",{__lookupSetter__:function(e){var t,n=i(this),r=o(e,!0);do{if(t=s(n,r))return t.set}while(n=a(n))}})},function(e,t,n){var r=n(0);r(r.P+r.R,"Map",{toJSON:n(136)("Map")})},function(e,t,n){var r=n(0);r(r.P+r.R,"Set",{toJSON:n(136)("Set")})},function(e,t,n){n(71)("Map")},function(e,t,n){n(71)("Set")},function(e,t,n){n(71)("WeakMap")},function(e,t,n){n(71)("WeakSet")},function(e,t,n){n(72)("Map")},function(e,t,n){n(72)("Set")},function(e,t,n){n(72)("WeakMap")},function(e,t,n){n(72)("WeakSet")},function(e,t,n){var r=n(0);r(r.G,{global:n(2)})},function(e,t,n){var r=n(0);r(r.S,"System",{global:n(2)})},function(e,t,n){var r=n(0),i=n(21);r(r.S,"Error",{isError:function(e){return"Error"===i(e)}})},function(e,t,n){var r=n(0);r(r.S,"Math",{clamp:function(e,t,n){return Math.min(n,Math.max(t,e))}})},function(e,t,n){var r=n(0);r(r.S,"Math",{DEG_PER_RAD:Math.PI/180})},function(e,t,n){var r=n(0),i=180/Math.PI;r(r.S,"Math",{degrees:function(e){return e*i}})},function(e,t,n){var r=n(0),i=n(138),o=n(117);r(r.S,"Math",{fscale:function(e,t,n,r,a){return o(i(e,t,n,r,a))}})},function(e,t,n){var r=n(0);r(r.S,"Math",{iaddh:function(e,t,n,r){var i=e>>>0,o=n>>>0;return(t>>>0)+(r>>>0)+((i&o|(i|o)&~(i+o>>>0))>>>31)|0}})},function(e,t,n){var r=n(0);r(r.S,"Math",{isubh:function(e,t,n,r){var i=e>>>0,o=n>>>0;return(t>>>0)-(r>>>0)-((~i&o|~(i^o)&i-o>>>0)>>>31)|0}})},function(e,t,n){var r=n(0);r(r.S,"Math",{imulh:function(e,t){var n=+e,r=+t,i=65535&n,o=65535&r,a=n>>16,s=r>>16,c=(a*o>>>0)+(i*o>>>16);return a*s+(c>>16)+((i*s>>>0)+(65535&c)>>16)}})},function(e,t,n){var r=n(0);r(r.S,"Math",{RAD_PER_DEG:180/Math.PI})},function(e,t,n){var r=n(0),i=Math.PI/180;r(r.S,"Math",{radians:function(e){return e*i}})},function(e,t,n){var r=n(0);r(r.S,"Math",{scale:n(138)})},function(e,t,n){var r=n(0);r(r.S,"Math",{umulh:function(e,t){var n=+e,r=+t,i=65535&n,o=65535&r,a=n>>>16,s=r>>>16,c=(a*o>>>0)+(i*o>>>16);return a*s+(c>>>16)+((i*s>>>0)+(65535&c)>>>16)}})},function(e,t,n){var r=n(0);r(r.S,"Math",{signbit:function(e){return(e=+e)!=e?e:0==e?1/e==1/0:e>0}})},function(e,t,n){"use strict";var r=n(0),i=n(19),o=n(2),a=n(55),s=n(125);r(r.P+r.R,"Promise",{finally:function(e){var t=a(this,i.Promise||o.Promise),n="function"==typeof e;return this.then(n?function(n){return s(t,e()).then(function(){return n})}:e,n?function(n){return s(t,e()).then(function(){throw n})}:e)}})},function(e,t,n){"use strict";var r=n(0),i=n(101),o=n(124);r(r.S,"Promise",{try:function(e){var t=i.f(this),n=o(e);return(n.e?t.reject:t.resolve)(n.v),t.promise}})},function(e,t,n){var r=n(29),i=n(1),o=r.key,a=r.set;r.exp({defineMetadata:function(e,t,n,r){a(e,t,i(n),o(r))}})},function(e,t,n){var r=n(29),i=n(1),o=r.key,a=r.map,s=r.store;r.exp({deleteMetadata:function(e,t){var n=arguments.length<3?void 0:o(arguments[2]),r=a(i(t),n,!1);if(void 0===r||!r.delete(e))return!1;if(r.size)return!0;var c=s.get(t);return c.delete(n),!!c.size||s.delete(t)}})},function(e,t,n){var r=n(29),i=n(1),o=n(18),a=r.has,s=r.get,c=r.key,p=function(e,t,n){if(a(e,t,n))return s(e,t,n);var r=o(t);return null!==r?p(e,r,n):void 0};r.exp({getMetadata:function(e,t){return p(e,i(t),arguments.length<3?void 0:c(arguments[2]))}})},function(e,t,n){var r=n(128),i=n(137),o=n(29),a=n(1),s=n(18),c=o.keys,p=o.key,d=function(e,t){var n=c(e,t),o=s(e);if(null===o)return n;var a=d(o,t);return a.length?n.length?i(new r(n.concat(a))):a:n};o.exp({getMetadataKeys:function(e){return d(a(e),arguments.length<2?void 0:p(arguments[1]))}})},function(e,t,n){var r=n(29),i=n(1),o=r.get,a=r.key;r.exp({getOwnMetadata:function(e,t){return o(e,i(t),arguments.length<3?void 0:a(arguments[2]))}})},function(e,t,n){var r=n(29),i=n(1),o=r.keys,a=r.key;r.exp({getOwnMetadataKeys:function(e){return o(i(e),arguments.length<2?void 0:a(arguments[1]))}})},function(e,t,n){var r=n(29),i=n(1),o=n(18),a=r.has,s=r.key,c=function(e,t,n){if(a(e,t,n))return!0;var r=o(t);return null!==r&&c(e,r,n)};r.exp({hasMetadata:function(e,t){return c(e,i(t),arguments.length<3?void 0:s(arguments[2]))}})},function(e,t,n){var r=n(29),i=n(1),o=r.has,a=r.key;r.exp({hasOwnMetadata:function(e,t){return o(e,i(t),arguments.length<3?void 0:a(arguments[2]))}})},function(e,t,n){var r=n(29),i=n(1),o=n(10),a=r.key,s=r.set;r.exp({metadata:function(e,t){return function(n,r){s(e,t,(void 0!==r?i:o)(n),a(r))}}})},function(e,t,n){var r=n(0),i=n(100)(),o=n(2).process,a="process"==n(21)(o);r(r.G,{asap:function(e){var t=a&&o.domain;i(t?t.bind(e):e)}})},function(e,t,n){"use strict";var r=n(0),i=n(2),o=n(19),a=n(100)(),s=n(5)("observable"),c=n(10),p=n(1),d=n(42),u=n(44),l=n(12),m=n(43),f=m.RETURN,h=function(e){return null==e?void 0:c(e)},g=function(e){var t=e._c;t&&(e._c=void 0,t())},y=function(e){return void 0===e._o},b=function(e){y(e)||(e._o=void 0,g(e))},v=function(e,t){p(e),this._c=void 0,this._o=e,e=new w(this);try{var n=t(e),r=n;null!=n&&("function"==typeof n.unsubscribe?n=function(){r.unsubscribe()}:c(n),this._c=n)}catch(t){return void e.error(t)}y(this)&&g(this)};v.prototype=u({},{unsubscribe:function(){b(this)}});var w=function(e){this._s=e};w.prototype=u({},{next:function(e){var t=this._s;if(!y(t)){var n=t._o;try{var r=h(n.next);if(r)return r.call(n,e)}catch(e){try{b(t)}finally{throw e}}}},error:function(e){var t=this._s;if(y(t))throw e;var n=t._o;t._o=void 0;try{var r=h(n.error);if(!r)throw e;e=r.call(n,e)}catch(e){try{g(t)}finally{throw e}}return g(t),e},complete:function(e){var t=this._s;if(!y(t)){var n=t._o;t._o=void 0;try{var r=h(n.complete);e=r?r.call(n,e):void 0}catch(e){try{g(t)}finally{throw e}}return g(t),e}}});var S=function(e){d(this,S,"Observable","_f")._f=c(e)};u(S.prototype,{subscribe:function(e){return new v(e,this._f)},forEach:function(e){var t=this;return new(o.Promise||i.Promise)(function(n,r){c(e);var i=t.subscribe({next:function(t){try{return e(t)}catch(e){r(e),i.unsubscribe()}},error:r,complete:n})})}}),u(S,{from:function(e){var t="function"==typeof this?this:S,n=h(p(e)[s]);if(n){var r=p(n.call(e));return r.constructor===t?r:new t(function(e){return r.subscribe(e)})}return new t(function(t){var n=!1;return a(function(){if(!n){try{if(m(e,!1,function(e){if(t.next(e),n)return f})===f)return}catch(e){if(n)throw e;return void t.error(e)}t.complete()}}),function(){n=!0}})},of:function(){for(var e=0,t=arguments.length,n=new Array(t);e<t;)n[e]=arguments[e++];return new("function"==typeof this?this:S)(function(e){var t=!1;return a(function(){if(!t){for(var r=0;r<n.length;++r)if(e.next(n[r]),t)return;e.complete()}}),function(){t=!0}})}}),l(S.prototype,s,function(){return this}),r(r.G,{Observable:S}),n(41)("Observable")},function(e,t,n){var r=n(2),i=n(0),o=n(67),a=[].slice,s=/MSIE .\./.test(o),c=function(e){return function(t,n){var r=arguments.length>2,i=!!r&&a.call(arguments,2);return e(r?function(){("function"==typeof t?t:Function(t)).apply(this,i)}:t,n)}};i(i.G+i.B+i.F*s,{setTimeout:c(r.setTimeout),setInterval:c(r.setInterval)})},function(e,t,n){var r=n(0),i=n(99);r(r.G+r.B,{setImmediate:i.set,clearImmediate:i.clear})},function(e,t,n){for(var r=n(96),i=n(37),o=n(13),a=n(2),s=n(12),c=n(49),p=n(5),d=p("iterator"),u=p("toStringTag"),l=c.Array,m={CSSRuleList:!0,CSSStyleDeclaration:!1,CSSValueList:!1,ClientRectList:!1,DOMRectList:!1,DOMStringList:!1,DOMTokenList:!0,DataTransferItemList:!1,FileList:!1,HTMLAllCollection:!1,HTMLCollection:!1,HTMLFormElement:!1,HTMLSelectElement:!1,MediaList:!0,MimeTypeArray:!1,NamedNodeMap:!1,NodeList:!0,PaintRequestList:!1,Plugin:!1,PluginArray:!1,SVGLengthList:!1,SVGNumberList:!1,SVGPathSegList:!1,SVGPointList:!1,SVGStringList:!1,SVGTransformList:!1,SourceBufferList:!1,StyleSheetList:!0,TextTrackCueList:!1,TextTrackList:!1,TouchList:!1},f=i(m),h=0;h<f.length;h++){var g,y=f[h],b=m[y],v=a[y],w=v&&v.prototype;if(w&&(w[d]||s(w,d,l),w[u]||s(w,u,y),c[y]=l,b))for(g in r)w[g]||o(w,g,r[g],!0)}},function(e,t,n){(function(t){!function(t){"use strict";var n,r=Object.prototype,i=r.hasOwnProperty,o="function"==typeof Symbol?Symbol:{},a=o.iterator||"@@iterator",s=o.asyncIterator||"@@asyncIterator",c=o.toStringTag||"@@toStringTag",p="object"==typeof e,d=t.regeneratorRuntime;if(d)p&&(e.exports=d);else{(d=t.regeneratorRuntime=p?e.exports:{}).wrap=w;var u="suspendedStart",l="suspendedYield",m="executing",f="completed",h={},g={};g[a]=function(){return this};var y=Object.getPrototypeOf,b=y&&y(y(D([])));b&&b!==r&&i.call(b,a)&&(g=b);var v=T.prototype=x.prototype=Object.create(g);I.prototype=v.constructor=T,T.constructor=I,T[c]=I.displayName="GeneratorFunction",d.isGeneratorFunction=function(e){var t="function"==typeof e&&e.constructor;return!!t&&(t===I||"GeneratorFunction"===(t.displayName||t.name))},d.mark=function(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,T):(e.__proto__=T,c in e||(e[c]="GeneratorFunction")),e.prototype=Object.create(v),e},d.awrap=function(e){return{__await:e}},R(k.prototype),k.prototype[s]=function(){return this},d.AsyncIterator=k,d.async=function(e,t,n,r){var i=new k(w(e,t,n,r));return d.isGeneratorFunction(t)?i:i.next().then(function(e){return e.done?e.value:i.next()})},R(v),v[c]="Generator",v[a]=function(){return this},v.toString=function(){return"[object Generator]"},d.keys=function(e){var t=[];for(var n in e)t.push(n);return t.reverse(),function n(){for(;t.length;){var r=t.pop();if(r in e)return n.value=r,n.done=!1,n}return n.done=!0,n}},d.values=D,E.prototype={constructor:E,reset:function(e){if(this.prev=0,this.next=0,this.sent=this._sent=n,this.done=!1,this.delegate=null,this.method="next",this.arg=n,this.tryEntries.forEach($),!e)for(var t in this)"t"===t.charAt(0)&&i.call(this,t)&&!isNaN(+t.slice(1))&&(this[t]=n)},stop:function(){this.done=!0;var e=this.tryEntries[0].completion;if("throw"===e.type)throw e.arg;return this.rval},dispatchException:function(e){if(this.done)throw e;var t=this;function r(r,i){return s.type="throw",s.arg=e,t.next=r,i&&(t.method="next",t.arg=n),!!i}for(var o=this.tryEntries.length-1;o>=0;--o){var a=this.tryEntries[o],s=a.completion;if("root"===a.tryLoc)return r("end");if(a.tryLoc<=this.prev){var c=i.call(a,"catchLoc"),p=i.call(a,"finallyLoc");if(c&&p){if(this.prev<a.catchLoc)return r(a.catchLoc,!0);if(this.prev<a.finallyLoc)return r(a.finallyLoc)}else if(c){if(this.prev<a.catchLoc)return r(a.catchLoc,!0)}else{if(!p)throw new Error("try statement without catch or finally");if(this.prev<a.finallyLoc)return r(a.finallyLoc)}}}},abrupt:function(e,t){for(var n=this.tryEntries.length-1;n>=0;--n){var r=this.tryEntries[n];if(r.tryLoc<=this.prev&&i.call(r,"finallyLoc")&&this.prev<r.finallyLoc){var o=r;break}}o&&("break"===e||"continue"===e)&&o.tryLoc<=t&&t<=o.finallyLoc&&(o=null);var a=o?o.completion:{};return a.type=e,a.arg=t,o?(this.method="next",this.next=o.finallyLoc,h):this.complete(a)},complete:function(e,t){if("throw"===e.type)throw e.arg;return"break"===e.type||"continue"===e.type?this.next=e.arg:"return"===e.type?(this.rval=this.arg=e.arg,this.method="return",this.next="end"):"normal"===e.type&&t&&(this.next=t),h},finish:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.finallyLoc===e)return this.complete(n.completion,n.afterLoc),$(n),h}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.tryLoc===e){var r=n.completion;if("throw"===r.type){var i=r.arg;$(n)}return i}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,r){return this.delegate={iterator:D(e),resultName:t,nextLoc:r},"next"===this.method&&(this.arg=n),h}}}function w(e,t,n,r){var i=t&&t.prototype instanceof x?t:x,o=Object.create(i.prototype),a=new E(r||[]);return o._invoke=function(e,t,n){var r=u;return function(i,o){if(r===m)throw new Error("Generator is already running");if(r===f){if("throw"===i)throw o;return j()}for(n.method=i,n.arg=o;;){var a=n.delegate;if(a){var s=C(a,n);if(s){if(s===h)continue;return s}}if("next"===n.method)n.sent=n._sent=n.arg;else if("throw"===n.method){if(r===u)throw r=f,n.arg;n.dispatchException(n.arg)}else"return"===n.method&&n.abrupt("return",n.arg);r=m;var c=S(e,t,n);if("normal"===c.type){if(r=n.done?f:l,c.arg===h)continue;return{value:c.arg,done:n.done}}"throw"===c.type&&(r=f,n.method="throw",n.arg=c.arg)}}}(e,n,a),o}function S(e,t,n){try{return{type:"normal",arg:e.call(t,n)}}catch(e){return{type:"throw",arg:e}}}function x(){}function I(){}function T(){}function R(e){["next","throw","return"].forEach(function(t){e[t]=function(e){return this._invoke(t,e)}})}function k(e){function n(t,r,o,a){var s=S(e[t],e,r);if("throw"!==s.type){var c=s.arg,p=c.value;return p&&"object"==typeof p&&i.call(p,"__await")?Promise.resolve(p.__await).then(function(e){n("next",e,o,a)},function(e){n("throw",e,o,a)}):Promise.resolve(p).then(function(e){c.value=e,o(c)},a)}a(s.arg)}var r;"object"==typeof t.process&&t.process.domain&&(n=t.process.domain.bind(n)),this._invoke=function(e,t){function i(){return new Promise(function(r,i){n(e,t,r,i)})}return r=r?r.then(i,i):i()}}function C(e,t){var r=e.iterator[t.method];if(r===n){if(t.delegate=null,"throw"===t.method){if(e.iterator.return&&(t.method="return",t.arg=n,C(e,t),"throw"===t.method))return h;t.method="throw",t.arg=new TypeError("The iterator does not provide a 'throw' method")}return h}var i=S(r,e.iterator,t.arg);if("throw"===i.type)return t.method="throw",t.arg=i.arg,t.delegate=null,h;var o=i.arg;return o?o.done?(t[e.resultName]=o.value,t.next=e.nextLoc,"return"!==t.method&&(t.method="next",t.arg=n),t.delegate=null,h):o:(t.method="throw",t.arg=new TypeError("iterator result is not an object"),t.delegate=null,h)}function O(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function $(e){var t=e.completion||{};t.type="normal",delete t.arg,e.completion=t}function E(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(O,this),this.reset(!0)}function D(e){if(e){var t=e[a];if(t)return t.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var r=-1,o=function t(){for(;++r<e.length;)if(i.call(e,r))return t.value=e[r],t.done=!1,t;return t.value=n,t.done=!0,t};return o.next=o}}return{next:j}}function j(){return{value:n,done:!0}}}("object"==typeof t?t:"object"==typeof window?window:"object"==typeof self?self:this)}).call(this,n(11))},function(e,t,n){n(353),e.exports=n(19).RegExp.escape},function(e,t,n){var r=n(0),i=n(354)(/[\\^$*+?.()|[\]{}]/g,"\\$&");r(r.S,"RegExp",{escape:function(e){return i(e)}})},function(e,t){e.exports=function(e,t){var n=t===Object(t)?function(e){return t[e]}:t;return function(t){return String(t).replace(e,n)}}},function(e,t,n){"use strict";(function(t){const r=n(56),i=n(139),o=n(378);e.exports=function(e,n){"function"==typeof e&&(n=e,e=void 0);const i=new r;return"function"==typeof n?(t.nextTick(()=>{new o(e,i)}),i.once("connect",n)):new Promise((t,n)=>{i.once("connect",t),i.once("error",n),new o(e,i)})},e.exports.Protocol=i.Protocol,e.exports.List=i.List,e.exports.New=i.New,e.exports.Activate=i.Activate,e.exports.Close=i.Close,e.exports.Version=i.Version}).call(this,n(30))},function(e,t,n){(function(t,r,i){var o=n(142),a=n(34),s=n(143),c=n(144),p=n(366),d=s.IncomingMessage,u=s.readyStates;var l=e.exports=function(e){var n,r=this;c.Writable.call(r),r._opts=e,r._body=[],r._headers={},e.auth&&r.setHeader("Authorization","Basic "+new t(e.auth).toString("base64")),Object.keys(e.headers).forEach(function(t){r.setHeader(t,e.headers[t])});var i=!0;if("disable-fetch"===e.mode||"requestTimeout"in e&&!o.abortController)i=!1,n=!0;else if("prefer-streaming"===e.mode)n=!1;else if("allow-wrong-content-type"===e.mode)n=!o.overrideMimeType;else{if(e.mode&&"default"!==e.mode&&"prefer-fast"!==e.mode)throw new Error("Invalid value for opts.mode");n=!0}r._mode=function(e,t){return o.fetch&&t?"fetch":o.mozchunkedarraybuffer?"moz-chunked-arraybuffer":o.msstream?"ms-stream":o.arraybuffer&&e?"arraybuffer":o.vbArray&&e?"text:vbarray":"text"}(n,i),r._fetchTimer=null,r.on("finish",function(){r._onFinish()})};a(l,c.Writable),l.prototype.setHeader=function(e,t){var n=e.toLowerCase();-1===m.indexOf(n)&&(this._headers[n]={name:e,value:t})},l.prototype.getHeader=function(e){var t=this._headers[e.toLowerCase()];return t?t.value:null},l.prototype.removeHeader=function(e){delete this._headers[e.toLowerCase()]},l.prototype._onFinish=function(){var e=this;if(!e._destroyed){var n=e._opts,a=e._headers,s=null;"GET"!==n.method&&"HEAD"!==n.method&&(s=o.arraybuffer?p(t.concat(e._body)):o.blobConstructor?new r.Blob(e._body.map(function(e){return p(e)}),{type:(a["content-type"]||{}).value||""}):t.concat(e._body).toString());var c=[];if(Object.keys(a).forEach(function(e){var t=a[e].name,n=a[e].value;Array.isArray(n)?n.forEach(function(e){c.push([t,e])}):c.push([t,n])}),"fetch"===e._mode){var d=null;if(o.abortController){var l=new AbortController;d=l.signal,e._fetchAbortController=l,"requestTimeout"in n&&0!==n.requestTimeout&&(e._fetchTimer=r.setTimeout(function(){e.emit("requestTimeout"),e._fetchAbortController&&e._fetchAbortController.abort()},n.requestTimeout))}r.fetch(e._opts.url,{method:e._opts.method,headers:c,body:s||void 0,mode:"cors",credentials:n.withCredentials?"include":"same-origin",signal:d}).then(function(t){e._fetchResponse=t,e._connect()},function(t){r.clearTimeout(e._fetchTimer),e._destroyed||e.emit("error",t)})}else{var m=e._xhr=new r.XMLHttpRequest;try{m.open(e._opts.method,e._opts.url,!0)}catch(t){return void i.nextTick(function(){e.emit("error",t)})}"responseType"in m&&(m.responseType=e._mode.split(":")[0]),"withCredentials"in m&&(m.withCredentials=!!n.withCredentials),"text"===e._mode&&"overrideMimeType"in m&&m.overrideMimeType("text/plain; charset=x-user-defined"),"requestTimeout"in n&&(m.timeout=n.requestTimeout,m.ontimeout=function(){e.emit("requestTimeout")}),c.forEach(function(e){m.setRequestHeader(e[0],e[1])}),e._response=null,m.onreadystatechange=function(){switch(m.readyState){case u.LOADING:case u.DONE:e._onXHRProgress()}},"moz-chunked-arraybuffer"===e._mode&&(m.onprogress=function(){e._onXHRProgress()}),m.onerror=function(){e._destroyed||e.emit("error",new Error("XHR error"))};try{m.send(s)}catch(t){return void i.nextTick(function(){e.emit("error",t)})}}}},l.prototype._onXHRProgress=function(){(function(e){try{var t=e.status;return null!==t&&0!==t}catch(e){return!1}})(this._xhr)&&!this._destroyed&&(this._response||this._connect(),this._response._onXHRProgress())},l.prototype._connect=function(){var e=this;e._destroyed||(e._response=new d(e._xhr,e._fetchResponse,e._mode,e._fetchTimer),e._response.on("error",function(t){e.emit("error",t)}),e.emit("response",e._response))},l.prototype._write=function(e,t,n){this._body.push(e),n()},l.prototype.abort=l.prototype.destroy=function(){this._destroyed=!0,r.clearTimeout(this._fetchTimer),this._response&&(this._response._destroyed=!0),this._xhr?this._xhr.abort():this._fetchAbortController&&this._fetchAbortController.abort()},l.prototype.end=function(e,t,n){"function"==typeof e&&(n=e,e=void 0),c.Writable.prototype.end.call(this,e,t,n)},l.prototype.flushHeaders=function(){},l.prototype.setTimeout=function(){},l.prototype.setNoDelay=function(){},l.prototype.setSocketKeepAlive=function(){};var m=["accept-charset","accept-encoding","access-control-request-headers","access-control-request-method","connection","content-length","cookie","cookie2","date","dnt","expect","host","keep-alive","origin","referer","te","trailer","transfer-encoding","upgrade","via"]}).call(this,n(57).Buffer,n(11),n(30))},function(e,t,n){"use strict";t.byteLength=function(e){var t=p(e),n=t[0],r=t[1];return 3*(n+r)/4-r},t.toByteArray=function(e){for(var t,n=p(e),r=n[0],a=n[1],s=new o(function(e,t,n){return 3*(t+n)/4-n}(0,r,a)),c=0,d=a>0?r-4:r,u=0;u<d;u+=4)t=i[e.charCodeAt(u)]<<18|i[e.charCodeAt(u+1)]<<12|i[e.charCodeAt(u+2)]<<6|i[e.charCodeAt(u+3)],s[c++]=t>>16&255,s[c++]=t>>8&255,s[c++]=255&t;2===a&&(t=i[e.charCodeAt(u)]<<2|i[e.charCodeAt(u+1)]>>4,s[c++]=255&t);1===a&&(t=i[e.charCodeAt(u)]<<10|i[e.charCodeAt(u+1)]<<4|i[e.charCodeAt(u+2)]>>2,s[c++]=t>>8&255,s[c++]=255&t);return s},t.fromByteArray=function(e){for(var t,n=e.length,i=n%3,o=[],a=0,s=n-i;a<s;a+=16383)o.push(d(e,a,a+16383>s?s:a+16383));1===i?(t=e[n-1],o.push(r[t>>2]+r[t<<4&63]+"==")):2===i&&(t=(e[n-2]<<8)+e[n-1],o.push(r[t>>10]+r[t>>4&63]+r[t<<2&63]+"="));return o.join("")};for(var r=[],i=[],o="undefined"!=typeof Uint8Array?Uint8Array:Array,a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",s=0,c=a.length;s<c;++s)r[s]=a[s],i[a.charCodeAt(s)]=s;function p(e){var t=e.length;if(t%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var n=e.indexOf("=");return-1===n&&(n=t),[n,n===t?0:4-n%4]}function d(e,t,n){for(var i,o,a=[],s=t;s<n;s+=3)i=(e[s]<<16&16711680)+(e[s+1]<<8&65280)+(255&e[s+2]),a.push(r[(o=i)>>18&63]+r[o>>12&63]+r[o>>6&63]+r[63&o]);return a.join("")}i["-".charCodeAt(0)]=62,i["_".charCodeAt(0)]=63},function(e,t){t.read=function(e,t,n,r,i){var o,a,s=8*i-r-1,c=(1<<s)-1,p=c>>1,d=-7,u=n?i-1:0,l=n?-1:1,m=e[t+u];for(u+=l,o=m&(1<<-d)-1,m>>=-d,d+=s;d>0;o=256*o+e[t+u],u+=l,d-=8);for(a=o&(1<<-d)-1,o>>=-d,d+=r;d>0;a=256*a+e[t+u],u+=l,d-=8);if(0===o)o=1-p;else{if(o===c)return a?NaN:1/0*(m?-1:1);a+=Math.pow(2,r),o-=p}return(m?-1:1)*a*Math.pow(2,o-r)},t.write=function(e,t,n,r,i,o){var a,s,c,p=8*o-i-1,d=(1<<p)-1,u=d>>1,l=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,m=r?0:o-1,f=r?1:-1,h=t<0||0===t&&1/t<0?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(s=isNaN(t)?1:0,a=d):(a=Math.floor(Math.log(t)/Math.LN2),t*(c=Math.pow(2,-a))<1&&(a--,c*=2),(t+=a+u>=1?l/c:l*Math.pow(2,1-u))*c>=2&&(a++,c/=2),a+u>=d?(s=0,a=d):a+u>=1?(s=(t*c-1)*Math.pow(2,i),a+=u):(s=t*Math.pow(2,u-1)*Math.pow(2,i),a=0));i>=8;e[n+m]=255&s,m+=f,s/=256,i-=8);for(a=a<<i|s,p+=i;p>0;e[n+m]=255&a,m+=f,a/=256,p-=8);e[n+m-f]|=128*h}},function(e,t){},function(e,t,n){"use strict";var r=n(74).Buffer,i=n(361);e.exports=function(){function e(){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.head=null,this.tail=null,this.length=0}return e.prototype.push=function(e){var t={data:e,next:null};this.length>0?this.tail.next=t:this.head=t,this.tail=t,++this.length},e.prototype.unshift=function(e){var t={data:e,next:this.head};0===this.length&&(this.tail=t),this.head=t,++this.length},e.prototype.shift=function(){if(0!==this.length){var e=this.head.data;return 1===this.length?this.head=this.tail=null:this.head=this.head.next,--this.length,e}},e.prototype.clear=function(){this.head=this.tail=null,this.length=0},e.prototype.join=function(e){if(0===this.length)return"";for(var t=this.head,n=""+t.data;t=t.next;)n+=e+t.data;return n},e.prototype.concat=function(e){if(0===this.length)return r.alloc(0);if(1===this.length)return this.head.data;for(var t,n,i,o=r.allocUnsafe(e>>>0),a=this.head,s=0;a;)t=a.data,n=o,i=s,t.copy(n,i),s+=a.data.length,a=a.next;return o},e}(),i&&i.inspect&&i.inspect.custom&&(e.exports.prototype[i.inspect.custom]=function(){var e=i.inspect({length:this.length});return this.constructor.name+" "+e})},function(e,t){},function(e,t,n){(function(e){var r=void 0!==e&&e||"undefined"!=typeof self&&self||window,i=Function.prototype.apply;function o(e,t){this._id=e,this._clearFn=t}t.setTimeout=function(){return new o(i.call(setTimeout,r,arguments),clearTimeout)},t.setInterval=function(){return new o(i.call(setInterval,r,arguments),clearInterval)},t.clearTimeout=t.clearInterval=function(e){e&&e.close()},o.prototype.unref=o.prototype.ref=function(){},o.prototype.close=function(){this._clearFn.call(r,this._id)},t.enroll=function(e,t){clearTimeout(e._idleTimeoutId),e._idleTimeout=t},t.unenroll=function(e){clearTimeout(e._idleTimeoutId),e._idleTimeout=-1},t._unrefActive=t.active=function(e){clearTimeout(e._idleTimeoutId);var t=e._idleTimeout;t>=0&&(e._idleTimeoutId=setTimeout(function(){e._onTimeout&&e._onTimeout()},t))},n(363),t.setImmediate="undefined"!=typeof self&&self.setImmediate||void 0!==e&&e.setImmediate||this&&this.setImmediate,t.clearImmediate="undefined"!=typeof self&&self.clearImmediate||void 0!==e&&e.clearImmediate||this&&this.clearImmediate}).call(this,n(11))},function(e,t,n){(function(e,t){!function(e,n){"use strict";if(!e.setImmediate){var r,i,o,a,s,c=1,p={},d=!1,u=e.document,l=Object.getPrototypeOf&&Object.getPrototypeOf(e);l=l&&l.setTimeout?l:e,"[object process]"==={}.toString.call(e.process)?r=function(e){t.nextTick(function(){f(e)})}:!function(){if(e.postMessage&&!e.importScripts){var t=!0,n=e.onmessage;return e.onmessage=function(){t=!1},e.postMessage("","*"),e.onmessage=n,t}}()?e.MessageChannel?((o=new MessageChannel).port1.onmessage=function(e){f(e.data)},r=function(e){o.port2.postMessage(e)}):u&&"onreadystatechange"in u.createElement("script")?(i=u.documentElement,r=function(e){var t=u.createElement("script");t.onreadystatechange=function(){f(e),t.onreadystatechange=null,i.removeChild(t),t=null},i.appendChild(t)}):r=function(e){setTimeout(f,0,e)}:(a="setImmediate$"+Math.random()+"$",s=function(t){t.source===e&&"string"==typeof t.data&&0===t.data.indexOf(a)&&f(+t.data.slice(a.length))},e.addEventListener?e.addEventListener("message",s,!1):e.attachEvent("onmessage",s),r=function(t){e.postMessage(a+t,"*")}),l.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),n=0;n<t.length;n++)t[n]=arguments[n+1];var i={callback:e,args:t};return p[c]=i,r(c),c++},l.clearImmediate=m}function m(e){delete p[e]}function f(e){if(d)setTimeout(f,0,e);else{var t=p[e];if(t){d=!0;try{!function(e){var t=e.callback,r=e.args;switch(r.length){case 0:t();break;case 1:t(r[0]);break;case 2:t(r[0],r[1]);break;case 3:t(r[0],r[1],r[2]);break;default:t.apply(n,r)}}(t)}finally{m(e),d=!1}}}}}("undefined"==typeof self?void 0===e?this:e:self)}).call(this,n(11),n(30))},function(e,t,n){(function(t){function n(e){try{if(!t.localStorage)return!1}catch(e){return!1}var n=t.localStorage[e];return null!=n&&"true"===String(n).toLowerCase()}e.exports=function(e,t){if(n("noDeprecation"))return e;var r=!1;return function(){if(!r){if(n("throwDeprecation"))throw new Error(t);n("traceDeprecation")?console.trace(t):console.warn(t),r=!0}return e.apply(this,arguments)}}}).call(this,n(11))},function(e,t,n){"use strict";e.exports=o;var r=n(150),i=n(58);function o(e){if(!(this instanceof o))return new o(e);r.call(this,e)}i.inherits=n(34),i.inherits(o,r),o.prototype._transform=function(e,t,n){n(null,e)}},function(e,t,n){var r=n(57).Buffer;e.exports=function(e){if(e instanceof Uint8Array){if(0===e.byteOffset&&e.byteLength===e.buffer.byteLength)return e.buffer;if("function"==typeof e.buffer.slice)return e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength)}if(r.isBuffer(e)){for(var t=new Uint8Array(e.length),n=e.length,i=0;i<n;i++)t[i]=e[i];return t.buffer}throw new Error("Argument must be a Buffer")}},function(e,t){e.exports=function(){for(var e={},t=0;t<arguments.length;t++){var r=arguments[t];for(var i in r)n.call(r,i)&&(e[i]=r[i])}return e};var n=Object.prototype.hasOwnProperty},function(e,t){e.exports={100:"Continue",101:"Switching Protocols",102:"Processing",200:"OK",201:"Created",202:"Accepted",203:"Non-Authoritative Information",204:"No Content",205:"Reset Content",206:"Partial Content",207:"Multi-Status",208:"Already Reported",226:"IM Used",300:"Multiple Choices",301:"Moved Permanently",302:"Found",303:"See Other",304:"Not Modified",305:"Use Proxy",307:"Temporary Redirect",308:"Permanent Redirect",400:"Bad Request",401:"Unauthorized",402:"Payment Required",403:"Forbidden",404:"Not Found",405:"Method Not Allowed",406:"Not Acceptable",407:"Proxy Authentication Required",408:"Request Timeout",409:"Conflict",410:"Gone",411:"Length Required",412:"Precondition Failed",413:"Payload Too Large",414:"URI Too Long",415:"Unsupported Media Type",416:"Range Not Satisfiable",417:"Expectation Failed",418:"I'm a teapot",421:"Misdirected Request",422:"Unprocessable Entity",423:"Locked",424:"Failed Dependency",425:"Unordered Collection",426:"Upgrade Required",428:"Precondition Required",429:"Too Many Requests",431:"Request Header Fields Too Large",451:"Unavailable For Legal Reasons",500:"Internal Server Error",501:"Not Implemented",502:"Bad Gateway",503:"Service Unavailable",504:"Gateway Timeout",505:"HTTP Version Not Supported",506:"Variant Also Negotiates",507:"Insufficient Storage",508:"Loop Detected",509:"Bandwidth Limit Exceeded",510:"Not Extended",511:"Network Authentication Required"}},function(e,t,n){(function(e,r){var i;/*! https://mths.be/punycode v1.4.1 by @mathias */!function(o){t&&t.nodeType,e&&e.nodeType;var a="object"==typeof r&&r;a.global!==a&&a.window!==a&&a.self;var s,c=2147483647,p=36,d=1,u=26,l=38,m=700,f=72,h=128,g="-",y=/^xn--/,b=/[^\x20-\x7E]/,v=/[\x2E\u3002\uFF0E\uFF61]/g,w={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)","invalid-input":"Invalid input"},S=p-d,x=Math.floor,I=String.fromCharCode;function T(e){throw new RangeError(w[e])}function R(e,t){for(var n=e.length,r=[];n--;)r[n]=t(e[n]);return r}function k(e,t){var n=e.split("@"),r="";return n.length>1&&(r=n[0]+"@",e=n[1]),r+R((e=e.replace(v,".")).split("."),t).join(".")}function C(e){for(var t,n,r=[],i=0,o=e.length;i<o;)(t=e.charCodeAt(i++))>=55296&&t<=56319&&i<o?56320==(64512&(n=e.charCodeAt(i++)))?r.push(((1023&t)<<10)+(1023&n)+65536):(r.push(t),i--):r.push(t);return r}function O(e){return R(e,function(e){var t="";return e>65535&&(t+=I((e-=65536)>>>10&1023|55296),e=56320|1023&e),t+=I(e)}).join("")}function $(e,t){return e+22+75*(e<26)-((0!=t)<<5)}function E(e,t,n){var r=0;for(e=n?x(e/m):e>>1,e+=x(e/t);e>S*u>>1;r+=p)e=x(e/S);return x(r+(S+1)*e/(e+l))}function D(e){var t,n,r,i,o,a,s,l,m,y,b,v=[],w=e.length,S=0,I=h,R=f;for((n=e.lastIndexOf(g))<0&&(n=0),r=0;r<n;++r)e.charCodeAt(r)>=128&&T("not-basic"),v.push(e.charCodeAt(r));for(i=n>0?n+1:0;i<w;){for(o=S,a=1,s=p;i>=w&&T("invalid-input"),((l=(b=e.charCodeAt(i++))-48<10?b-22:b-65<26?b-65:b-97<26?b-97:p)>=p||l>x((c-S)/a))&&T("overflow"),S+=l*a,!(l<(m=s<=R?d:s>=R+u?u:s-R));s+=p)a>x(c/(y=p-m))&&T("overflow"),a*=y;R=E(S-o,t=v.length+1,0==o),x(S/t)>c-I&&T("overflow"),I+=x(S/t),S%=t,v.splice(S++,0,I)}return O(v)}function j(e){var t,n,r,i,o,a,s,l,m,y,b,v,w,S,R,k=[];for(v=(e=C(e)).length,t=h,n=0,o=f,a=0;a<v;++a)(b=e[a])<128&&k.push(I(b));for(r=i=k.length,i&&k.push(g);r<v;){for(s=c,a=0;a<v;++a)(b=e[a])>=t&&b<s&&(s=b);for(s-t>x((c-n)/(w=r+1))&&T("overflow"),n+=(s-t)*w,t=s,a=0;a<v;++a)if((b=e[a])<t&&++n>c&&T("overflow"),b==t){for(l=n,m=p;!(l<(y=m<=o?d:m>=o+u?u:m-o));m+=p)R=l-y,S=p-y,k.push(I($(y+R%S,0))),l=x(R/S);k.push(I($(l,0))),o=E(n,w,r==i),n=0,++r}++n,++t}return k.join("")}s={version:"1.4.1",ucs2:{decode:C,encode:O},decode:D,encode:j,toASCII:function(e){return k(e,function(e){return b.test(e)?"xn--"+j(e):e})},toUnicode:function(e){return k(e,function(e){return y.test(e)?D(e.slice(4).toLowerCase()):e})}},void 0===(i=function(){return s}.call(t,n,t,e))||(e.exports=i)}()}).call(this,n(370)(e),n(11))},function(e,t){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children||(e.children=[]),Object.defineProperty(e,"loaded",{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,"id",{enumerable:!0,get:function(){return e.i}}),e.webpackPolyfill=1),e}},function(e,t,n){"use strict";e.exports={isString:function(e){return"string"==typeof e},isObject:function(e){return"object"==typeof e&&null!==e},isNull:function(e){return null===e},isNullOrUndefined:function(e){return null==e}}},function(e,t,n){"use strict";t.decode=t.parse=n(373),t.encode=t.stringify=n(374)},function(e,t,n){"use strict";function r(e,t){return Object.prototype.hasOwnProperty.call(e,t)}e.exports=function(e,t,n,o){t=t||"&",n=n||"=";var a={};if("string"!=typeof e||0===e.length)return a;var s=/\+/g;e=e.split(t);var c=1e3;o&&"number"==typeof o.maxKeys&&(c=o.maxKeys);var p=e.length;c>0&&p>c&&(p=c);for(var d=0;d<p;++d){var u,l,m,f,h=e[d].replace(s,"%20"),g=h.indexOf(n);g>=0?(u=h.substr(0,g),l=h.substr(g+1)):(u=h,l=""),m=decodeURIComponent(u),f=decodeURIComponent(l),r(a,m)?i(a[m])?a[m].push(f):a[m]=[a[m],f]:a[m]=f}return a};var i=Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)}},function(e,t,n){"use strict";var r=function(e){switch(typeof e){case"string":return e;case"boolean":return e?"true":"false";case"number":return isFinite(e)?e:"";default:return""}};e.exports=function(e,t,n,s){return t=t||"&",n=n||"=",null===e&&(e=void 0),"object"==typeof e?o(a(e),function(a){var s=encodeURIComponent(r(a))+n;return i(e[a])?o(e[a],function(e){return s+encodeURIComponent(r(e))}).join(t):s+encodeURIComponent(r(e[a]))}).join(t):s?encodeURIComponent(r(s))+n+encodeURIComponent(r(e)):""};var i=Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)};function o(e,t){if(e.map)return e.map(t);for(var n=[],r=0;r<e.length;r++)n.push(t(e[r],r));return n}var a=Object.keys||function(e){var t=[];for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&t.push(n);return t}},function(e,t,n){var r=n(140),i=n(75),o=e.exports;for(var a in r)r.hasOwnProperty(a)&&(o[a]=r[a]);function s(e){if("string"==typeof e&&(e=i.parse(e)),e.protocol||(e.protocol="https:"),"https:"!==e.protocol)throw new Error('Protocol "'+e.protocol+'" not supported. Expected "https:"');return e}o.request=function(e,t){return e=s(e),r.request.call(this,e,t)},o.get=function(e,t){return e=s(e),r.get.call(this,e,t)}},function(e,t){e.exports=function(e,t,n){window.criRequest(t,n)}},function(e){e.exports=JSON.parse('{"version":{"major":"1","minor":"3"},"domains":[{"domain":"Accessibility","experimental":true,"dependencies":["DOM"],"types":[{"id":"AXNodeId","description":"Unique accessibility node identifier.","type":"string"},{"id":"AXValueType","description":"Enum of possible property types.","type":"string","enum":["boolean","tristate","booleanOrUndefined","idref","idrefList","integer","node","nodeList","number","string","computedString","token","tokenList","domRelation","role","internalRole","valueUndefined"]},{"id":"AXValueSourceType","description":"Enum of possible property sources.","type":"string","enum":["attribute","implicit","style","contents","placeholder","relatedElement"]},{"id":"AXValueNativeSourceType","description":"Enum of possible native property sources (as a subtype of a particular AXValueSourceType).","type":"string","enum":["figcaption","label","labelfor","labelwrapped","legend","tablecaption","title","other"]},{"id":"AXValueSource","description":"A single source for a computed AX property.","type":"object","properties":[{"name":"type","description":"What type of source this is.","$ref":"AXValueSourceType"},{"name":"value","description":"The value of this property source.","optional":true,"$ref":"AXValue"},{"name":"attribute","description":"The name of the relevant attribute, if any.","optional":true,"type":"string"},{"name":"attributeValue","description":"The value of the relevant attribute, if any.","optional":true,"$ref":"AXValue"},{"name":"superseded","description":"Whether this source is superseded by a higher priority source.","optional":true,"type":"boolean"},{"name":"nativeSource","description":"The native markup source for this value, e.g. a <label> element.","optional":true,"$ref":"AXValueNativeSourceType"},{"name":"nativeSourceValue","description":"The value, such as a node or node list, of the native source.","optional":true,"$ref":"AXValue"},{"name":"invalid","description":"Whether the value for this property is invalid.","optional":true,"type":"boolean"},{"name":"invalidReason","description":"Reason for the value being invalid, if it is.","optional":true,"type":"string"}]},{"id":"AXRelatedNode","type":"object","properties":[{"name":"backendDOMNodeId","description":"The BackendNodeId of the related DOM node.","$ref":"DOM.BackendNodeId"},{"name":"idref","description":"The IDRef value provided, if any.","optional":true,"type":"string"},{"name":"text","description":"The text alternative of this node in the current context.","optional":true,"type":"string"}]},{"id":"AXProperty","type":"object","properties":[{"name":"name","description":"The name of this property.","$ref":"AXPropertyName"},{"name":"value","description":"The value of this property.","$ref":"AXValue"}]},{"id":"AXValue","description":"A single computed AX property.","type":"object","properties":[{"name":"type","description":"The type of this value.","$ref":"AXValueType"},{"name":"value","description":"The computed value of this property.","optional":true,"type":"any"},{"name":"relatedNodes","description":"One or more related nodes, if applicable.","optional":true,"type":"array","items":{"$ref":"AXRelatedNode"}},{"name":"sources","description":"The sources which contributed to the computation of this property.","optional":true,"type":"array","items":{"$ref":"AXValueSource"}}]},{"id":"AXPropertyName","description":"Values of AXProperty name:\\n- from \'busy\' to \'roledescription\': states which apply to every AX node\\n- from \'live\' to \'root\': attributes which apply to nodes in live regions\\n- from \'autocomplete\' to \'valuetext\': attributes which apply to widgets\\n- from \'checked\' to \'selected\': states which apply to widgets\\n- from \'activedescendant\' to \'owns\' - relationships between elements other than parent/child/sibling.","type":"string","enum":["busy","disabled","editable","focusable","focused","hidden","hiddenRoot","invalid","keyshortcuts","settable","roledescription","live","atomic","relevant","root","autocomplete","hasPopup","level","multiselectable","orientation","multiline","readonly","required","valuemin","valuemax","valuetext","checked","expanded","modal","pressed","selected","activedescendant","controls","describedby","details","errormessage","flowto","labelledby","owns"]},{"id":"AXNode","description":"A node in the accessibility tree.","type":"object","properties":[{"name":"nodeId","description":"Unique identifier for this node.","$ref":"AXNodeId"},{"name":"ignored","description":"Whether this node is ignored for accessibility","type":"boolean"},{"name":"ignoredReasons","description":"Collection of reasons why this node is hidden.","optional":true,"type":"array","items":{"$ref":"AXProperty"}},{"name":"role","description":"This `Node`\'s role, whether explicit or implicit.","optional":true,"$ref":"AXValue"},{"name":"name","description":"The accessible name for this `Node`.","optional":true,"$ref":"AXValue"},{"name":"description","description":"The accessible description for this `Node`.","optional":true,"$ref":"AXValue"},{"name":"value","description":"The value for this `Node`.","optional":true,"$ref":"AXValue"},{"name":"properties","description":"All other properties","optional":true,"type":"array","items":{"$ref":"AXProperty"}},{"name":"childIds","description":"IDs for each of this node\'s child nodes.","optional":true,"type":"array","items":{"$ref":"AXNodeId"}},{"name":"backendDOMNodeId","description":"The backend ID for the associated DOM node, if any.","optional":true,"$ref":"DOM.BackendNodeId"}]}],"commands":[{"name":"disable","description":"Disables the accessibility domain."},{"name":"enable","description":"Enables the accessibility domain which causes `AXNodeId`s to remain consistent between method calls.\\nThis turns on accessibility for the page, which can impact performance until accessibility is disabled."},{"name":"getPartialAXTree","description":"Fetches the accessibility node and partial accessibility tree for this DOM node, if it exists.","experimental":true,"parameters":[{"name":"nodeId","description":"Identifier of the node to get the partial accessibility tree for.","optional":true,"$ref":"DOM.NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node to get the partial accessibility tree for.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper to get the partial accessibility tree for.","optional":true,"$ref":"Runtime.RemoteObjectId"},{"name":"fetchRelatives","description":"Whether to fetch this nodes ancestors, siblings and children. Defaults to true.","optional":true,"type":"boolean"}],"returns":[{"name":"nodes","description":"The `Accessibility.AXNode` for this DOM node, if it exists, plus its ancestors, siblings and\\nchildren, if requested.","type":"array","items":{"$ref":"AXNode"}}]},{"name":"getFullAXTree","description":"Fetches the entire accessibility tree","experimental":true,"returns":[{"name":"nodes","type":"array","items":{"$ref":"AXNode"}}]}]},{"domain":"Animation","experimental":true,"dependencies":["Runtime","DOM"],"types":[{"id":"Animation","description":"Animation instance.","type":"object","properties":[{"name":"id","description":"`Animation`\'s id.","type":"string"},{"name":"name","description":"`Animation`\'s name.","type":"string"},{"name":"pausedState","description":"`Animation`\'s internal paused state.","type":"boolean"},{"name":"playState","description":"`Animation`\'s play state.","type":"string"},{"name":"playbackRate","description":"`Animation`\'s playback rate.","type":"number"},{"name":"startTime","description":"`Animation`\'s start time.","type":"number"},{"name":"currentTime","description":"`Animation`\'s current time.","type":"number"},{"name":"type","description":"Animation type of `Animation`.","type":"string","enum":["CSSTransition","CSSAnimation","WebAnimation"]},{"name":"source","description":"`Animation`\'s source animation node.","optional":true,"$ref":"AnimationEffect"},{"name":"cssId","description":"A unique ID for `Animation` representing the sources that triggered this CSS\\nanimation/transition.","optional":true,"type":"string"}]},{"id":"AnimationEffect","description":"AnimationEffect instance","type":"object","properties":[{"name":"delay","description":"`AnimationEffect`\'s delay.","type":"number"},{"name":"endDelay","description":"`AnimationEffect`\'s end delay.","type":"number"},{"name":"iterationStart","description":"`AnimationEffect`\'s iteration start.","type":"number"},{"name":"iterations","description":"`AnimationEffect`\'s iterations.","type":"number"},{"name":"duration","description":"`AnimationEffect`\'s iteration duration.","type":"number"},{"name":"direction","description":"`AnimationEffect`\'s playback direction.","type":"string"},{"name":"fill","description":"`AnimationEffect`\'s fill mode.","type":"string"},{"name":"backendNodeId","description":"`AnimationEffect`\'s target node.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"keyframesRule","description":"`AnimationEffect`\'s keyframes.","optional":true,"$ref":"KeyframesRule"},{"name":"easing","description":"`AnimationEffect`\'s timing function.","type":"string"}]},{"id":"KeyframesRule","description":"Keyframes Rule","type":"object","properties":[{"name":"name","description":"CSS keyframed animation\'s name.","optional":true,"type":"string"},{"name":"keyframes","description":"List of animation keyframes.","type":"array","items":{"$ref":"KeyframeStyle"}}]},{"id":"KeyframeStyle","description":"Keyframe Style","type":"object","properties":[{"name":"offset","description":"Keyframe\'s time offset.","type":"string"},{"name":"easing","description":"`AnimationEffect`\'s timing function.","type":"string"}]}],"commands":[{"name":"disable","description":"Disables animation domain notifications."},{"name":"enable","description":"Enables animation domain notifications."},{"name":"getCurrentTime","description":"Returns the current time of the an animation.","parameters":[{"name":"id","description":"Id of animation.","type":"string"}],"returns":[{"name":"currentTime","description":"Current time of the page.","type":"number"}]},{"name":"getPlaybackRate","description":"Gets the playback rate of the document timeline.","returns":[{"name":"playbackRate","description":"Playback rate for animations on page.","type":"number"}]},{"name":"releaseAnimations","description":"Releases a set of animations to no longer be manipulated.","parameters":[{"name":"animations","description":"List of animation ids to seek.","type":"array","items":{"type":"string"}}]},{"name":"resolveAnimation","description":"Gets the remote object of the Animation.","parameters":[{"name":"animationId","description":"Animation id.","type":"string"}],"returns":[{"name":"remoteObject","description":"Corresponding remote object.","$ref":"Runtime.RemoteObject"}]},{"name":"seekAnimations","description":"Seek a set of animations to a particular time within each animation.","parameters":[{"name":"animations","description":"List of animation ids to seek.","type":"array","items":{"type":"string"}},{"name":"currentTime","description":"Set the current time of each animation.","type":"number"}]},{"name":"setPaused","description":"Sets the paused state of a set of animations.","parameters":[{"name":"animations","description":"Animations to set the pause state of.","type":"array","items":{"type":"string"}},{"name":"paused","description":"Paused state to set to.","type":"boolean"}]},{"name":"setPlaybackRate","description":"Sets the playback rate of the document timeline.","parameters":[{"name":"playbackRate","description":"Playback rate for animations on page","type":"number"}]},{"name":"setTiming","description":"Sets the timing of an animation node.","parameters":[{"name":"animationId","description":"Animation id.","type":"string"},{"name":"duration","description":"Duration of the animation.","type":"number"},{"name":"delay","description":"Delay of the animation.","type":"number"}]}],"events":[{"name":"animationCanceled","description":"Event for when an animation has been cancelled.","parameters":[{"name":"id","description":"Id of the animation that was cancelled.","type":"string"}]},{"name":"animationCreated","description":"Event for each animation that has been created.","parameters":[{"name":"id","description":"Id of the animation that was created.","type":"string"}]},{"name":"animationStarted","description":"Event for animation that has been started.","parameters":[{"name":"animation","description":"Animation that was started.","$ref":"Animation"}]}]},{"domain":"ApplicationCache","experimental":true,"types":[{"id":"ApplicationCacheResource","description":"Detailed application cache resource information.","type":"object","properties":[{"name":"url","description":"Resource url.","type":"string"},{"name":"size","description":"Resource size.","type":"integer"},{"name":"type","description":"Resource type.","type":"string"}]},{"id":"ApplicationCache","description":"Detailed application cache information.","type":"object","properties":[{"name":"manifestURL","description":"Manifest URL.","type":"string"},{"name":"size","description":"Application cache size.","type":"number"},{"name":"creationTime","description":"Application cache creation time.","type":"number"},{"name":"updateTime","description":"Application cache update time.","type":"number"},{"name":"resources","description":"Application cache resources.","type":"array","items":{"$ref":"ApplicationCacheResource"}}]},{"id":"FrameWithManifest","description":"Frame identifier - manifest URL pair.","type":"object","properties":[{"name":"frameId","description":"Frame identifier.","$ref":"Page.FrameId"},{"name":"manifestURL","description":"Manifest URL.","type":"string"},{"name":"status","description":"Application cache status.","type":"integer"}]}],"commands":[{"name":"enable","description":"Enables application cache domain notifications."},{"name":"getApplicationCacheForFrame","description":"Returns relevant application cache data for the document in given frame.","parameters":[{"name":"frameId","description":"Identifier of the frame containing document whose application cache is retrieved.","$ref":"Page.FrameId"}],"returns":[{"name":"applicationCache","description":"Relevant application cache data for the document in given frame.","$ref":"ApplicationCache"}]},{"name":"getFramesWithManifests","description":"Returns array of frame identifiers with manifest urls for each frame containing a document\\nassociated with some application cache.","returns":[{"name":"frameIds","description":"Array of frame identifiers with manifest urls for each frame containing a document\\nassociated with some application cache.","type":"array","items":{"$ref":"FrameWithManifest"}}]},{"name":"getManifestForFrame","description":"Returns manifest URL for document in the given frame.","parameters":[{"name":"frameId","description":"Identifier of the frame containing document whose manifest is retrieved.","$ref":"Page.FrameId"}],"returns":[{"name":"manifestURL","description":"Manifest URL for document in the given frame.","type":"string"}]}],"events":[{"name":"applicationCacheStatusUpdated","parameters":[{"name":"frameId","description":"Identifier of the frame containing document whose application cache updated status.","$ref":"Page.FrameId"},{"name":"manifestURL","description":"Manifest URL.","type":"string"},{"name":"status","description":"Updated application cache status.","type":"integer"}]},{"name":"networkStateUpdated","parameters":[{"name":"isNowOnline","type":"boolean"}]}]},{"domain":"Audits","description":"Audits domain allows investigation of page violations and possible improvements.","experimental":true,"dependencies":["Network"],"commands":[{"name":"getEncodedResponse","description":"Returns the response body and size if it were re-encoded with the specified settings. Only\\napplies to images.","parameters":[{"name":"requestId","description":"Identifier of the network request to get content for.","$ref":"Network.RequestId"},{"name":"encoding","description":"The encoding to use.","type":"string","enum":["webp","jpeg","png"]},{"name":"quality","description":"The quality of the encoding (0-1). (defaults to 1)","optional":true,"type":"number"},{"name":"sizeOnly","description":"Whether to only return the size information (defaults to false).","optional":true,"type":"boolean"}],"returns":[{"name":"body","description":"The encoded body as a base64 string. Omitted if sizeOnly is true.","optional":true,"type":"string"},{"name":"originalSize","description":"Size before re-encoding.","type":"integer"},{"name":"encodedSize","description":"Size after re-encoding.","type":"integer"}]}]},{"domain":"BackgroundService","description":"Defines events for background web platform features.","experimental":true,"types":[{"id":"ServiceName","description":"The Background Service that will be associated with the commands/events.\\nEvery Background Service operates independently, but they share the same\\nAPI.","type":"string","enum":["backgroundFetch","backgroundSync","pushMessaging","notifications","paymentHandler"]},{"id":"EventMetadata","description":"A key-value pair for additional event information to pass along.","type":"object","properties":[{"name":"key","type":"string"},{"name":"value","type":"string"}]},{"id":"BackgroundServiceEvent","type":"object","properties":[{"name":"timestamp","description":"Timestamp of the event (in seconds).","$ref":"Network.TimeSinceEpoch"},{"name":"origin","description":"The origin this event belongs to.","type":"string"},{"name":"serviceWorkerRegistrationId","description":"The Service Worker ID that initiated the event.","$ref":"ServiceWorker.RegistrationID"},{"name":"service","description":"The Background Service this event belongs to.","$ref":"ServiceName"},{"name":"eventName","description":"A description of the event.","type":"string"},{"name":"instanceId","description":"An identifier that groups related events together.","type":"string"},{"name":"eventMetadata","description":"A list of event-specific information.","type":"array","items":{"$ref":"EventMetadata"}}]}],"commands":[{"name":"startObserving","description":"Enables event updates for the service.","parameters":[{"name":"service","$ref":"ServiceName"}]},{"name":"stopObserving","description":"Disables event updates for the service.","parameters":[{"name":"service","$ref":"ServiceName"}]},{"name":"setRecording","description":"Set the recording state for the service.","parameters":[{"name":"shouldRecord","type":"boolean"},{"name":"service","$ref":"ServiceName"}]},{"name":"clearEvents","description":"Clears all stored data for the service.","parameters":[{"name":"service","$ref":"ServiceName"}]}],"events":[{"name":"recordingStateChanged","description":"Called when the recording state for the service has been updated.","parameters":[{"name":"isRecording","type":"boolean"},{"name":"service","$ref":"ServiceName"}]},{"name":"backgroundServiceEventReceived","description":"Called with all existing backgroundServiceEvents when enabled, and all new\\nevents afterwards if enabled and recording.","parameters":[{"name":"backgroundServiceEvent","$ref":"BackgroundServiceEvent"}]}]},{"domain":"Browser","description":"The Browser domain defines methods and events for browser managing.","types":[{"id":"WindowID","experimental":true,"type":"integer"},{"id":"WindowState","description":"The state of the browser window.","experimental":true,"type":"string","enum":["normal","minimized","maximized","fullscreen"]},{"id":"Bounds","description":"Browser window bounds information","experimental":true,"type":"object","properties":[{"name":"left","description":"The offset from the left edge of the screen to the window in pixels.","optional":true,"type":"integer"},{"name":"top","description":"The offset from the top edge of the screen to the window in pixels.","optional":true,"type":"integer"},{"name":"width","description":"The window width in pixels.","optional":true,"type":"integer"},{"name":"height","description":"The window height in pixels.","optional":true,"type":"integer"},{"name":"windowState","description":"The window state. Default to normal.","optional":true,"$ref":"WindowState"}]},{"id":"PermissionType","experimental":true,"type":"string","enum":["accessibilityEvents","audioCapture","backgroundSync","backgroundFetch","clipboardRead","clipboardWrite","durableStorage","flash","geolocation","midi","midiSysex","notifications","paymentHandler","periodicBackgroundSync","protectedMediaIdentifier","sensors","videoCapture","idleDetection","wakeLockScreen","wakeLockSystem"]},{"id":"Bucket","description":"Chrome histogram bucket.","experimental":true,"type":"object","properties":[{"name":"low","description":"Minimum value (inclusive).","type":"integer"},{"name":"high","description":"Maximum value (exclusive).","type":"integer"},{"name":"count","description":"Number of samples.","type":"integer"}]},{"id":"Histogram","description":"Chrome histogram.","experimental":true,"type":"object","properties":[{"name":"name","description":"Name.","type":"string"},{"name":"sum","description":"Sum of sample values.","type":"integer"},{"name":"count","description":"Total number of samples.","type":"integer"},{"name":"buckets","description":"Buckets.","type":"array","items":{"$ref":"Bucket"}}]}],"commands":[{"name":"grantPermissions","description":"Grant specific permissions to the given origin and reject all others.","experimental":true,"parameters":[{"name":"origin","type":"string"},{"name":"permissions","type":"array","items":{"$ref":"PermissionType"}},{"name":"browserContextId","description":"BrowserContext to override permissions. When omitted, default browser context is used.","optional":true,"$ref":"Target.BrowserContextID"}]},{"name":"resetPermissions","description":"Reset all permission management for all origins.","experimental":true,"parameters":[{"name":"browserContextId","description":"BrowserContext to reset permissions. When omitted, default browser context is used.","optional":true,"$ref":"Target.BrowserContextID"}]},{"name":"close","description":"Close browser gracefully."},{"name":"crash","description":"Crashes browser on the main thread.","experimental":true},{"name":"crashGpuProcess","description":"Crashes GPU process.","experimental":true},{"name":"getVersion","description":"Returns version information.","returns":[{"name":"protocolVersion","description":"Protocol version.","type":"string"},{"name":"product","description":"Product name.","type":"string"},{"name":"revision","description":"Product revision.","type":"string"},{"name":"userAgent","description":"User-Agent.","type":"string"},{"name":"jsVersion","description":"V8 version.","type":"string"}]},{"name":"getBrowserCommandLine","description":"Returns the command line switches for the browser process if, and only if\\n--enable-automation is on the commandline.","experimental":true,"returns":[{"name":"arguments","description":"Commandline parameters","type":"array","items":{"type":"string"}}]},{"name":"getHistograms","description":"Get Chrome histograms.","experimental":true,"parameters":[{"name":"query","description":"Requested substring in name. Only histograms which have query as a\\nsubstring in their name are extracted. An empty or absent query returns\\nall histograms.","optional":true,"type":"string"},{"name":"delta","description":"If true, retrieve delta since last call.","optional":true,"type":"boolean"}],"returns":[{"name":"histograms","description":"Histograms.","type":"array","items":{"$ref":"Histogram"}}]},{"name":"getHistogram","description":"Get a Chrome histogram by name.","experimental":true,"parameters":[{"name":"name","description":"Requested histogram name.","type":"string"},{"name":"delta","description":"If true, retrieve delta since last call.","optional":true,"type":"boolean"}],"returns":[{"name":"histogram","description":"Histogram.","$ref":"Histogram"}]},{"name":"getWindowBounds","description":"Get position and size of the browser window.","experimental":true,"parameters":[{"name":"windowId","description":"Browser window id.","$ref":"WindowID"}],"returns":[{"name":"bounds","description":"Bounds information of the window. When window state is \'minimized\', the restored window\\nposition and size are returned.","$ref":"Bounds"}]},{"name":"getWindowForTarget","description":"Get the browser window that contains the devtools target.","experimental":true,"parameters":[{"name":"targetId","description":"Devtools agent host id. If called as a part of the session, associated targetId is used.","optional":true,"$ref":"Target.TargetID"}],"returns":[{"name":"windowId","description":"Browser window id.","$ref":"WindowID"},{"name":"bounds","description":"Bounds information of the window. When window state is \'minimized\', the restored window\\nposition and size are returned.","$ref":"Bounds"}]},{"name":"setWindowBounds","description":"Set position and/or size of the browser window.","experimental":true,"parameters":[{"name":"windowId","description":"Browser window id.","$ref":"WindowID"},{"name":"bounds","description":"New window bounds. The \'minimized\', \'maximized\' and \'fullscreen\' states cannot be combined\\nwith \'left\', \'top\', \'width\' or \'height\'. Leaves unspecified fields unchanged.","$ref":"Bounds"}]},{"name":"setDockTile","description":"Set dock tile details, platform-specific.","experimental":true,"parameters":[{"name":"badgeLabel","optional":true,"type":"string"},{"name":"image","description":"Png encoded image.","optional":true,"type":"string"}]}]},{"domain":"CSS","description":"This domain exposes CSS read/write operations. All CSS objects (stylesheets, rules, and styles)\\nhave an associated `id` used in subsequent operations on the related object. Each object type has\\na specific `id` structure, and those are not interchangeable between objects of different kinds.\\nCSS objects can be loaded using the `get*ForNode()` calls (which accept a DOM node id). A client\\ncan also keep track of stylesheets via the `styleSheetAdded`/`styleSheetRemoved` events and\\nsubsequently load the required stylesheet contents using the `getStyleSheet[Text]()` methods.","experimental":true,"dependencies":["DOM"],"types":[{"id":"StyleSheetId","type":"string"},{"id":"StyleSheetOrigin","description":"Stylesheet type: \\"injected\\" for stylesheets injected via extension, \\"user-agent\\" for user-agent\\nstylesheets, \\"inspector\\" for stylesheets created by the inspector (i.e. those holding the \\"via\\ninspector\\" rules), \\"regular\\" for regular stylesheets.","type":"string","enum":["injected","user-agent","inspector","regular"]},{"id":"PseudoElementMatches","description":"CSS rule collection for a single pseudo style.","type":"object","properties":[{"name":"pseudoType","description":"Pseudo element type.","$ref":"DOM.PseudoType"},{"name":"matches","description":"Matches of CSS rules applicable to the pseudo style.","type":"array","items":{"$ref":"RuleMatch"}}]},{"id":"InheritedStyleEntry","description":"Inherited CSS rule collection from ancestor node.","type":"object","properties":[{"name":"inlineStyle","description":"The ancestor node\'s inline style, if any, in the style inheritance chain.","optional":true,"$ref":"CSSStyle"},{"name":"matchedCSSRules","description":"Matches of CSS rules matching the ancestor node in the style inheritance chain.","type":"array","items":{"$ref":"RuleMatch"}}]},{"id":"RuleMatch","description":"Match data for a CSS rule.","type":"object","properties":[{"name":"rule","description":"CSS rule in the match.","$ref":"CSSRule"},{"name":"matchingSelectors","description":"Matching selector indices in the rule\'s selectorList selectors (0-based).","type":"array","items":{"type":"integer"}}]},{"id":"Value","description":"Data for a simple selector (these are delimited by commas in a selector list).","type":"object","properties":[{"name":"text","description":"Value text.","type":"string"},{"name":"range","description":"Value range in the underlying resource (if available).","optional":true,"$ref":"SourceRange"}]},{"id":"SelectorList","description":"Selector list data.","type":"object","properties":[{"name":"selectors","description":"Selectors in the list.","type":"array","items":{"$ref":"Value"}},{"name":"text","description":"Rule selector text.","type":"string"}]},{"id":"CSSStyleSheetHeader","description":"CSS stylesheet metainformation.","type":"object","properties":[{"name":"styleSheetId","description":"The stylesheet identifier.","$ref":"StyleSheetId"},{"name":"frameId","description":"Owner frame identifier.","$ref":"Page.FrameId"},{"name":"sourceURL","description":"Stylesheet resource URL.","type":"string"},{"name":"sourceMapURL","description":"URL of source map associated with the stylesheet (if any).","optional":true,"type":"string"},{"name":"origin","description":"Stylesheet origin.","$ref":"StyleSheetOrigin"},{"name":"title","description":"Stylesheet title.","type":"string"},{"name":"ownerNode","description":"The backend id for the owner node of the stylesheet.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"disabled","description":"Denotes whether the stylesheet is disabled.","type":"boolean"},{"name":"hasSourceURL","description":"Whether the sourceURL field value comes from the sourceURL comment.","optional":true,"type":"boolean"},{"name":"isInline","description":"Whether this stylesheet is created for STYLE tag by parser. This flag is not set for\\ndocument.written STYLE tags.","type":"boolean"},{"name":"startLine","description":"Line offset of the stylesheet within the resource (zero based).","type":"number"},{"name":"startColumn","description":"Column offset of the stylesheet within the resource (zero based).","type":"number"},{"name":"length","description":"Size of the content (in characters).","type":"number"}]},{"id":"CSSRule","description":"CSS rule representation.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.","optional":true,"$ref":"StyleSheetId"},{"name":"selectorList","description":"Rule selector data.","$ref":"SelectorList"},{"name":"origin","description":"Parent stylesheet\'s origin.","$ref":"StyleSheetOrigin"},{"name":"style","description":"Associated style declaration.","$ref":"CSSStyle"},{"name":"media","description":"Media list array (for rules involving media queries). The array enumerates media queries\\nstarting with the innermost one, going outwards.","optional":true,"type":"array","items":{"$ref":"CSSMedia"}}]},{"id":"RuleUsage","description":"CSS coverage information.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.","$ref":"StyleSheetId"},{"name":"startOffset","description":"Offset of the start of the rule (including selector) from the beginning of the stylesheet.","type":"number"},{"name":"endOffset","description":"Offset of the end of the rule body from the beginning of the stylesheet.","type":"number"},{"name":"used","description":"Indicates whether the rule was actually used by some element in the page.","type":"boolean"}]},{"id":"SourceRange","description":"Text range within a resource. All numbers are zero-based.","type":"object","properties":[{"name":"startLine","description":"Start line of range.","type":"integer"},{"name":"startColumn","description":"Start column of range (inclusive).","type":"integer"},{"name":"endLine","description":"End line of range","type":"integer"},{"name":"endColumn","description":"End column of range (exclusive).","type":"integer"}]},{"id":"ShorthandEntry","type":"object","properties":[{"name":"name","description":"Shorthand name.","type":"string"},{"name":"value","description":"Shorthand value.","type":"string"},{"name":"important","description":"Whether the property has \\"!important\\" annotation (implies `false` if absent).","optional":true,"type":"boolean"}]},{"id":"CSSComputedStyleProperty","type":"object","properties":[{"name":"name","description":"Computed style property name.","type":"string"},{"name":"value","description":"Computed style property value.","type":"string"}]},{"id":"CSSStyle","description":"CSS style representation.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.","optional":true,"$ref":"StyleSheetId"},{"name":"cssProperties","description":"CSS properties in the style.","type":"array","items":{"$ref":"CSSProperty"}},{"name":"shorthandEntries","description":"Computed values for all shorthands found in the style.","type":"array","items":{"$ref":"ShorthandEntry"}},{"name":"cssText","description":"Style declaration text (if available).","optional":true,"type":"string"},{"name":"range","description":"Style declaration range in the enclosing stylesheet (if available).","optional":true,"$ref":"SourceRange"}]},{"id":"CSSProperty","description":"CSS property declaration data.","type":"object","properties":[{"name":"name","description":"The property name.","type":"string"},{"name":"value","description":"The property value.","type":"string"},{"name":"important","description":"Whether the property has \\"!important\\" annotation (implies `false` if absent).","optional":true,"type":"boolean"},{"name":"implicit","description":"Whether the property is implicit (implies `false` if absent).","optional":true,"type":"boolean"},{"name":"text","description":"The full property text as specified in the style.","optional":true,"type":"string"},{"name":"parsedOk","description":"Whether the property is understood by the browser (implies `true` if absent).","optional":true,"type":"boolean"},{"name":"disabled","description":"Whether the property is disabled by the user (present for source-based properties only).","optional":true,"type":"boolean"},{"name":"range","description":"The entire property range in the enclosing style declaration (if available).","optional":true,"$ref":"SourceRange"}]},{"id":"CSSMedia","description":"CSS media rule descriptor.","type":"object","properties":[{"name":"text","description":"Media query text.","type":"string"},{"name":"source","description":"Source of the media query: \\"mediaRule\\" if specified by a @media rule, \\"importRule\\" if\\nspecified by an @import rule, \\"linkedSheet\\" if specified by a \\"media\\" attribute in a linked\\nstylesheet\'s LINK tag, \\"inlineSheet\\" if specified by a \\"media\\" attribute in an inline\\nstylesheet\'s STYLE tag.","type":"string","enum":["mediaRule","importRule","linkedSheet","inlineSheet"]},{"name":"sourceURL","description":"URL of the document containing the media query description.","optional":true,"type":"string"},{"name":"range","description":"The associated rule (@media or @import) header range in the enclosing stylesheet (if\\navailable).","optional":true,"$ref":"SourceRange"},{"name":"styleSheetId","description":"Identifier of the stylesheet containing this object (if exists).","optional":true,"$ref":"StyleSheetId"},{"name":"mediaList","description":"Array of media queries.","optional":true,"type":"array","items":{"$ref":"MediaQuery"}}]},{"id":"MediaQuery","description":"Media query descriptor.","type":"object","properties":[{"name":"expressions","description":"Array of media query expressions.","type":"array","items":{"$ref":"MediaQueryExpression"}},{"name":"active","description":"Whether the media query condition is satisfied.","type":"boolean"}]},{"id":"MediaQueryExpression","description":"Media query expression descriptor.","type":"object","properties":[{"name":"value","description":"Media query expression value.","type":"number"},{"name":"unit","description":"Media query expression units.","type":"string"},{"name":"feature","description":"Media query expression feature.","type":"string"},{"name":"valueRange","description":"The associated range of the value text in the enclosing stylesheet (if available).","optional":true,"$ref":"SourceRange"},{"name":"computedLength","description":"Computed length of media query expression (if applicable).","optional":true,"type":"number"}]},{"id":"PlatformFontUsage","description":"Information about amount of glyphs that were rendered with given font.","type":"object","properties":[{"name":"familyName","description":"Font\'s family name reported by platform.","type":"string"},{"name":"isCustomFont","description":"Indicates if the font was downloaded or resolved locally.","type":"boolean"},{"name":"glyphCount","description":"Amount of glyphs that were rendered with this font.","type":"number"}]},{"id":"FontFace","description":"Properties of a web font: https://www.w3.org/TR/2008/REC-CSS2-20080411/fonts.html#font-descriptions","type":"object","properties":[{"name":"fontFamily","description":"The font-family.","type":"string"},{"name":"fontStyle","description":"The font-style.","type":"string"},{"name":"fontVariant","description":"The font-variant.","type":"string"},{"name":"fontWeight","description":"The font-weight.","type":"string"},{"name":"fontStretch","description":"The font-stretch.","type":"string"},{"name":"unicodeRange","description":"The unicode-range.","type":"string"},{"name":"src","description":"The src.","type":"string"},{"name":"platformFontFamily","description":"The resolved platform font family","type":"string"}]},{"id":"CSSKeyframesRule","description":"CSS keyframes rule representation.","type":"object","properties":[{"name":"animationName","description":"Animation name.","$ref":"Value"},{"name":"keyframes","description":"List of keyframes.","type":"array","items":{"$ref":"CSSKeyframeRule"}}]},{"id":"CSSKeyframeRule","description":"CSS keyframe rule representation.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.","optional":true,"$ref":"StyleSheetId"},{"name":"origin","description":"Parent stylesheet\'s origin.","$ref":"StyleSheetOrigin"},{"name":"keyText","description":"Associated key text.","$ref":"Value"},{"name":"style","description":"Associated style declaration.","$ref":"CSSStyle"}]},{"id":"StyleDeclarationEdit","description":"A descriptor of operation to mutate style declaration text.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier.","$ref":"StyleSheetId"},{"name":"range","description":"The range of the style text in the enclosing stylesheet.","$ref":"SourceRange"},{"name":"text","description":"New style text.","type":"string"}]}],"commands":[{"name":"addRule","description":"Inserts a new rule with the given `ruleText` in a stylesheet with given `styleSheetId`, at the\\nposition specified by `location`.","parameters":[{"name":"styleSheetId","description":"The css style sheet identifier where a new rule should be inserted.","$ref":"StyleSheetId"},{"name":"ruleText","description":"The text of a new rule.","type":"string"},{"name":"location","description":"Text position of a new rule in the target style sheet.","$ref":"SourceRange"}],"returns":[{"name":"rule","description":"The newly created rule.","$ref":"CSSRule"}]},{"name":"collectClassNames","description":"Returns all class names from specified stylesheet.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"}],"returns":[{"name":"classNames","description":"Class name list.","type":"array","items":{"type":"string"}}]},{"name":"createStyleSheet","description":"Creates a new special \\"via-inspector\\" stylesheet in the frame with given `frameId`.","parameters":[{"name":"frameId","description":"Identifier of the frame where \\"via-inspector\\" stylesheet should be created.","$ref":"Page.FrameId"}],"returns":[{"name":"styleSheetId","description":"Identifier of the created \\"via-inspector\\" stylesheet.","$ref":"StyleSheetId"}]},{"name":"disable","description":"Disables the CSS agent for the given page."},{"name":"enable","description":"Enables the CSS agent for the given page. Clients should not assume that the CSS agent has been\\nenabled until the result of this command is received."},{"name":"forcePseudoState","description":"Ensures that the given node will have specified pseudo-classes whenever its style is computed by\\nthe browser.","parameters":[{"name":"nodeId","description":"The element id for which to force the pseudo state.","$ref":"DOM.NodeId"},{"name":"forcedPseudoClasses","description":"Element pseudo classes to force when computing the element\'s style.","type":"array","items":{"type":"string"}}]},{"name":"getBackgroundColors","parameters":[{"name":"nodeId","description":"Id of the node to get background colors for.","$ref":"DOM.NodeId"}],"returns":[{"name":"backgroundColors","description":"The range of background colors behind this element, if it contains any visible text. If no\\nvisible text is present, this will be undefined. In the case of a flat background color,\\nthis will consist of simply that color. In the case of a gradient, this will consist of each\\nof the color stops. For anything more complicated, this will be an empty array. Images will\\nbe ignored (as if the image had failed to load).","optional":true,"type":"array","items":{"type":"string"}},{"name":"computedFontSize","description":"The computed font size for this node, as a CSS computed value string (e.g. \'12px\').","optional":true,"type":"string"},{"name":"computedFontWeight","description":"The computed font weight for this node, as a CSS computed value string (e.g. \'normal\' or\\n\'100\').","optional":true,"type":"string"}]},{"name":"getComputedStyleForNode","description":"Returns the computed style for a DOM node identified by `nodeId`.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}],"returns":[{"name":"computedStyle","description":"Computed style for the specified DOM node.","type":"array","items":{"$ref":"CSSComputedStyleProperty"}}]},{"name":"getInlineStylesForNode","description":"Returns the styles defined inline (explicitly in the \\"style\\" attribute and implicitly, using DOM\\nattributes) for a DOM node identified by `nodeId`.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}],"returns":[{"name":"inlineStyle","description":"Inline style for the specified DOM node.","optional":true,"$ref":"CSSStyle"},{"name":"attributesStyle","description":"Attribute-defined element style (e.g. resulting from \\"width=20 height=100%\\").","optional":true,"$ref":"CSSStyle"}]},{"name":"getMatchedStylesForNode","description":"Returns requested styles for a DOM node identified by `nodeId`.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}],"returns":[{"name":"inlineStyle","description":"Inline style for the specified DOM node.","optional":true,"$ref":"CSSStyle"},{"name":"attributesStyle","description":"Attribute-defined element style (e.g. resulting from \\"width=20 height=100%\\").","optional":true,"$ref":"CSSStyle"},{"name":"matchedCSSRules","description":"CSS rules matching this node, from all applicable stylesheets.","optional":true,"type":"array","items":{"$ref":"RuleMatch"}},{"name":"pseudoElements","description":"Pseudo style matches for this node.","optional":true,"type":"array","items":{"$ref":"PseudoElementMatches"}},{"name":"inherited","description":"A chain of inherited styles (from the immediate node parent up to the DOM tree root).","optional":true,"type":"array","items":{"$ref":"InheritedStyleEntry"}},{"name":"cssKeyframesRules","description":"A list of CSS keyframed animations matching this node.","optional":true,"type":"array","items":{"$ref":"CSSKeyframesRule"}}]},{"name":"getMediaQueries","description":"Returns all media queries parsed by the rendering engine.","returns":[{"name":"medias","type":"array","items":{"$ref":"CSSMedia"}}]},{"name":"getPlatformFontsForNode","description":"Requests information about platform fonts which we used to render child TextNodes in the given\\nnode.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}],"returns":[{"name":"fonts","description":"Usage statistics for every employed platform font.","type":"array","items":{"$ref":"PlatformFontUsage"}}]},{"name":"getStyleSheetText","description":"Returns the current textual content for a stylesheet.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"}],"returns":[{"name":"text","description":"The stylesheet text.","type":"string"}]},{"name":"setEffectivePropertyValueForNode","description":"Find a rule with the given active property for the given node and set the new value for this\\nproperty","parameters":[{"name":"nodeId","description":"The element id for which to set property.","$ref":"DOM.NodeId"},{"name":"propertyName","type":"string"},{"name":"value","type":"string"}]},{"name":"setKeyframeKey","description":"Modifies the keyframe rule key text.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"range","$ref":"SourceRange"},{"name":"keyText","type":"string"}],"returns":[{"name":"keyText","description":"The resulting key text after modification.","$ref":"Value"}]},{"name":"setMediaText","description":"Modifies the rule selector.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"range","$ref":"SourceRange"},{"name":"text","type":"string"}],"returns":[{"name":"media","description":"The resulting CSS media rule after modification.","$ref":"CSSMedia"}]},{"name":"setRuleSelector","description":"Modifies the rule selector.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"range","$ref":"SourceRange"},{"name":"selector","type":"string"}],"returns":[{"name":"selectorList","description":"The resulting selector list after modification.","$ref":"SelectorList"}]},{"name":"setStyleSheetText","description":"Sets the new stylesheet text.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"text","type":"string"}],"returns":[{"name":"sourceMapURL","description":"URL of source map associated with script (if any).","optional":true,"type":"string"}]},{"name":"setStyleTexts","description":"Applies specified style edits one after another in the given order.","parameters":[{"name":"edits","type":"array","items":{"$ref":"StyleDeclarationEdit"}}],"returns":[{"name":"styles","description":"The resulting styles after modification.","type":"array","items":{"$ref":"CSSStyle"}}]},{"name":"startRuleUsageTracking","description":"Enables the selector recording."},{"name":"stopRuleUsageTracking","description":"Stop tracking rule usage and return the list of rules that were used since last call to\\n`takeCoverageDelta` (or since start of coverage instrumentation)","returns":[{"name":"ruleUsage","type":"array","items":{"$ref":"RuleUsage"}}]},{"name":"takeCoverageDelta","description":"Obtain list of rules that became used since last call to this method (or since start of coverage\\ninstrumentation)","returns":[{"name":"coverage","type":"array","items":{"$ref":"RuleUsage"}}]}],"events":[{"name":"fontsUpdated","description":"Fires whenever a web font is updated. A non-empty font parameter indicates a successfully loaded\\nweb font","parameters":[{"name":"font","description":"The web font that has loaded.","optional":true,"$ref":"FontFace"}]},{"name":"mediaQueryResultChanged","description":"Fires whenever a MediaQuery result changes (for example, after a browser window has been\\nresized.) The current implementation considers only viewport-dependent media features."},{"name":"styleSheetAdded","description":"Fired whenever an active document stylesheet is added.","parameters":[{"name":"header","description":"Added stylesheet metainfo.","$ref":"CSSStyleSheetHeader"}]},{"name":"styleSheetChanged","description":"Fired whenever a stylesheet is changed as a result of the client operation.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"}]},{"name":"styleSheetRemoved","description":"Fired whenever an active document stylesheet is removed.","parameters":[{"name":"styleSheetId","description":"Identifier of the removed stylesheet.","$ref":"StyleSheetId"}]}]},{"domain":"CacheStorage","experimental":true,"types":[{"id":"CacheId","description":"Unique identifier of the Cache object.","type":"string"},{"id":"CachedResponseType","description":"type of HTTP response cached","type":"string","enum":["basic","cors","default","error","opaqueResponse","opaqueRedirect"]},{"id":"DataEntry","description":"Data entry.","type":"object","properties":[{"name":"requestURL","description":"Request URL.","type":"string"},{"name":"requestMethod","description":"Request method.","type":"string"},{"name":"requestHeaders","description":"Request headers","type":"array","items":{"$ref":"Header"}},{"name":"responseTime","description":"Number of seconds since epoch.","type":"number"},{"name":"responseStatus","description":"HTTP response status code.","type":"integer"},{"name":"responseStatusText","description":"HTTP response status text.","type":"string"},{"name":"responseType","description":"HTTP response type","$ref":"CachedResponseType"},{"name":"responseHeaders","description":"Response headers","type":"array","items":{"$ref":"Header"}}]},{"id":"Cache","description":"Cache identifier.","type":"object","properties":[{"name":"cacheId","description":"An opaque unique id of the cache.","$ref":"CacheId"},{"name":"securityOrigin","description":"Security origin of the cache.","type":"string"},{"name":"cacheName","description":"The name of the cache.","type":"string"}]},{"id":"Header","type":"object","properties":[{"name":"name","type":"string"},{"name":"value","type":"string"}]},{"id":"CachedResponse","description":"Cached response","type":"object","properties":[{"name":"body","description":"Entry content, base64-encoded.","type":"string"}]}],"commands":[{"name":"deleteCache","description":"Deletes a cache.","parameters":[{"name":"cacheId","description":"Id of cache for deletion.","$ref":"CacheId"}]},{"name":"deleteEntry","description":"Deletes a cache entry.","parameters":[{"name":"cacheId","description":"Id of cache where the entry will be deleted.","$ref":"CacheId"},{"name":"request","description":"URL spec of the request.","type":"string"}]},{"name":"requestCacheNames","description":"Requests cache names.","parameters":[{"name":"securityOrigin","description":"Security origin.","type":"string"}],"returns":[{"name":"caches","description":"Caches for the security origin.","type":"array","items":{"$ref":"Cache"}}]},{"name":"requestCachedResponse","description":"Fetches cache entry.","parameters":[{"name":"cacheId","description":"Id of cache that contains the entry.","$ref":"CacheId"},{"name":"requestURL","description":"URL spec of the request.","type":"string"},{"name":"requestHeaders","description":"headers of the request.","type":"array","items":{"$ref":"Header"}}],"returns":[{"name":"response","description":"Response read from the cache.","$ref":"CachedResponse"}]},{"name":"requestEntries","description":"Requests data from cache.","parameters":[{"name":"cacheId","description":"ID of cache to get entries from.","$ref":"CacheId"},{"name":"skipCount","description":"Number of records to skip.","type":"integer"},{"name":"pageSize","description":"Number of records to fetch.","type":"integer"},{"name":"pathFilter","description":"If present, only return the entries containing this substring in the path","optional":true,"type":"string"}],"returns":[{"name":"cacheDataEntries","description":"Array of object store data entries.","type":"array","items":{"$ref":"DataEntry"}},{"name":"returnCount","description":"Count of returned entries from this storage. If pathFilter is empty, it\\nis the count of all entries from this storage.","type":"number"}]}]},{"domain":"Cast","description":"A domain for interacting with Cast, Presentation API, and Remote Playback API\\nfunctionalities.","experimental":true,"types":[{"id":"Sink","type":"object","properties":[{"name":"name","type":"string"},{"name":"id","type":"string"},{"name":"session","description":"Text describing the current session. Present only if there is an active\\nsession on the sink.","optional":true,"type":"string"}]}],"commands":[{"name":"enable","description":"Starts observing for sinks that can be used for tab mirroring, and if set,\\nsinks compatible with |presentationUrl| as well. When sinks are found, a\\n|sinksUpdated| event is fired.\\nAlso starts observing for issue messages. When an issue is added or removed,\\nan |issueUpdated| event is fired.","parameters":[{"name":"presentationUrl","optional":true,"type":"string"}]},{"name":"disable","description":"Stops observing for sinks and issues."},{"name":"setSinkToUse","description":"Sets a sink to be used when the web page requests the browser to choose a\\nsink via Presentation API, Remote Playback API, or Cast SDK.","parameters":[{"name":"sinkName","type":"string"}]},{"name":"startTabMirroring","description":"Starts mirroring the tab to the sink.","parameters":[{"name":"sinkName","type":"string"}]},{"name":"stopCasting","description":"Stops the active Cast session on the sink.","parameters":[{"name":"sinkName","type":"string"}]}],"events":[{"name":"sinksUpdated","description":"This is fired whenever the list of available sinks changes. A sink is a\\ndevice or a software surface that you can cast to.","parameters":[{"name":"sinks","type":"array","items":{"$ref":"Sink"}}]},{"name":"issueUpdated","description":"This is fired whenever the outstanding issue/error message changes.\\n|issueMessage| is empty if there is no issue.","parameters":[{"name":"issueMessage","type":"string"}]}]},{"domain":"DOM","description":"This domain exposes DOM read/write operations. Each DOM Node is represented with its mirror object\\nthat has an `id`. This `id` can be used to get additional information on the Node, resolve it into\\nthe JavaScript object wrapper, etc. It is important that client receives DOM events only for the\\nnodes that are known to the client. Backend keeps track of the nodes that were sent to the client\\nand never sends the same node twice. It is client\'s responsibility to collect information about\\nthe nodes that were sent to the client.<p>Note that `iframe` owner elements will return\\ncorresponding document elements as their child nodes.</p>","dependencies":["Runtime"],"types":[{"id":"NodeId","description":"Unique DOM node identifier.","type":"integer"},{"id":"BackendNodeId","description":"Unique DOM node identifier used to reference a node that may not have been pushed to the\\nfront-end.","type":"integer"},{"id":"BackendNode","description":"Backend node with a friendly name.","type":"object","properties":[{"name":"nodeType","description":"`Node`\'s nodeType.","type":"integer"},{"name":"nodeName","description":"`Node`\'s nodeName.","type":"string"},{"name":"backendNodeId","$ref":"BackendNodeId"}]},{"id":"PseudoType","description":"Pseudo element type.","type":"string","enum":["first-line","first-letter","before","after","backdrop","selection","first-line-inherited","scrollbar","scrollbar-thumb","scrollbar-button","scrollbar-track","scrollbar-track-piece","scrollbar-corner","resizer","input-list-button"]},{"id":"ShadowRootType","description":"Shadow root type.","type":"string","enum":["user-agent","open","closed"]},{"id":"Node","description":"DOM interaction is implemented in terms of mirror objects that represent the actual DOM nodes.\\nDOMNode is a base node mirror type.","type":"object","properties":[{"name":"nodeId","description":"Node identifier that is passed into the rest of the DOM messages as the `nodeId`. Backend\\nwill only push node with given `id` once. It is aware of all requested nodes and will only\\nfire DOM events for nodes known to the client.","$ref":"NodeId"},{"name":"parentId","description":"The id of the parent node if any.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"The BackendNodeId for this node.","$ref":"BackendNodeId"},{"name":"nodeType","description":"`Node`\'s nodeType.","type":"integer"},{"name":"nodeName","description":"`Node`\'s nodeName.","type":"string"},{"name":"localName","description":"`Node`\'s localName.","type":"string"},{"name":"nodeValue","description":"`Node`\'s nodeValue.","type":"string"},{"name":"childNodeCount","description":"Child count for `Container` nodes.","optional":true,"type":"integer"},{"name":"children","description":"Child nodes of this node when requested with children.","optional":true,"type":"array","items":{"$ref":"Node"}},{"name":"attributes","description":"Attributes of the `Element` node in the form of flat array `[name1, value1, name2, value2]`.","optional":true,"type":"array","items":{"type":"string"}},{"name":"documentURL","description":"Document URL that `Document` or `FrameOwner` node points to.","optional":true,"type":"string"},{"name":"baseURL","description":"Base URL that `Document` or `FrameOwner` node uses for URL completion.","optional":true,"type":"string"},{"name":"publicId","description":"`DocumentType`\'s publicId.","optional":true,"type":"string"},{"name":"systemId","description":"`DocumentType`\'s systemId.","optional":true,"type":"string"},{"name":"internalSubset","description":"`DocumentType`\'s internalSubset.","optional":true,"type":"string"},{"name":"xmlVersion","description":"`Document`\'s XML version in case of XML documents.","optional":true,"type":"string"},{"name":"name","description":"`Attr`\'s name.","optional":true,"type":"string"},{"name":"value","description":"`Attr`\'s value.","optional":true,"type":"string"},{"name":"pseudoType","description":"Pseudo element type for this node.","optional":true,"$ref":"PseudoType"},{"name":"shadowRootType","description":"Shadow root type.","optional":true,"$ref":"ShadowRootType"},{"name":"frameId","description":"Frame ID for frame owner elements.","optional":true,"$ref":"Page.FrameId"},{"name":"contentDocument","description":"Content document for frame owner elements.","optional":true,"$ref":"Node"},{"name":"shadowRoots","description":"Shadow root list for given element host.","optional":true,"type":"array","items":{"$ref":"Node"}},{"name":"templateContent","description":"Content document fragment for template elements.","optional":true,"$ref":"Node"},{"name":"pseudoElements","description":"Pseudo elements associated with this node.","optional":true,"type":"array","items":{"$ref":"Node"}},{"name":"importedDocument","description":"Import document for the HTMLImport links.","optional":true,"$ref":"Node"},{"name":"distributedNodes","description":"Distributed nodes for given insertion point.","optional":true,"type":"array","items":{"$ref":"BackendNode"}},{"name":"isSVG","description":"Whether the node is SVG.","optional":true,"type":"boolean"}]},{"id":"RGBA","description":"A structure holding an RGBA color.","type":"object","properties":[{"name":"r","description":"The red component, in the [0-255] range.","type":"integer"},{"name":"g","description":"The green component, in the [0-255] range.","type":"integer"},{"name":"b","description":"The blue component, in the [0-255] range.","type":"integer"},{"name":"a","description":"The alpha component, in the [0-1] range (default: 1).","optional":true,"type":"number"}]},{"id":"Quad","description":"An array of quad vertices, x immediately followed by y for each point, points clock-wise.","type":"array","items":{"type":"number"}},{"id":"BoxModel","description":"Box model.","type":"object","properties":[{"name":"content","description":"Content box","$ref":"Quad"},{"name":"padding","description":"Padding box","$ref":"Quad"},{"name":"border","description":"Border box","$ref":"Quad"},{"name":"margin","description":"Margin box","$ref":"Quad"},{"name":"width","description":"Node width","type":"integer"},{"name":"height","description":"Node height","type":"integer"},{"name":"shapeOutside","description":"Shape outside coordinates","optional":true,"$ref":"ShapeOutsideInfo"}]},{"id":"ShapeOutsideInfo","description":"CSS Shape Outside details.","type":"object","properties":[{"name":"bounds","description":"Shape bounds","$ref":"Quad"},{"name":"shape","description":"Shape coordinate details","type":"array","items":{"type":"any"}},{"name":"marginShape","description":"Margin shape bounds","type":"array","items":{"type":"any"}}]},{"id":"Rect","description":"Rectangle.","type":"object","properties":[{"name":"x","description":"X coordinate","type":"number"},{"name":"y","description":"Y coordinate","type":"number"},{"name":"width","description":"Rectangle width","type":"number"},{"name":"height","description":"Rectangle height","type":"number"}]}],"commands":[{"name":"collectClassNamesFromSubtree","description":"Collects class names for the node with given id and all of it\'s child nodes.","experimental":true,"parameters":[{"name":"nodeId","description":"Id of the node to collect class names.","$ref":"NodeId"}],"returns":[{"name":"classNames","description":"Class name list.","type":"array","items":{"type":"string"}}]},{"name":"copyTo","description":"Creates a deep copy of the specified node and places it into the target container before the\\ngiven anchor.","experimental":true,"parameters":[{"name":"nodeId","description":"Id of the node to copy.","$ref":"NodeId"},{"name":"targetNodeId","description":"Id of the element to drop the copy into.","$ref":"NodeId"},{"name":"insertBeforeNodeId","description":"Drop the copy before this node (if absent, the copy becomes the last child of\\n`targetNodeId`).","optional":true,"$ref":"NodeId"}],"returns":[{"name":"nodeId","description":"Id of the node clone.","$ref":"NodeId"}]},{"name":"describeNode","description":"Describes node given its id, does not require domain to be enabled. Does not start tracking any\\nobjects, can be used for automation.","parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"},{"name":"depth","description":"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false).","optional":true,"type":"boolean"}],"returns":[{"name":"node","description":"Node description.","$ref":"Node"}]},{"name":"disable","description":"Disables DOM agent for the given page."},{"name":"discardSearchResults","description":"Discards search results from the session with the given id. `getSearchResults` should no longer\\nbe called for that search.","experimental":true,"parameters":[{"name":"searchId","description":"Unique search session identifier.","type":"string"}]},{"name":"enable","description":"Enables DOM agent for the given page."},{"name":"focus","description":"Focuses the given element.","parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}]},{"name":"getAttributes","description":"Returns attributes for the specified node.","parameters":[{"name":"nodeId","description":"Id of the node to retrieve attibutes for.","$ref":"NodeId"}],"returns":[{"name":"attributes","description":"An interleaved array of node attribute names and values.","type":"array","items":{"type":"string"}}]},{"name":"getBoxModel","description":"Returns boxes for the given node.","parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"model","description":"Box model for the node.","$ref":"BoxModel"}]},{"name":"getContentQuads","description":"Returns quads that describe node position on the page. This method\\nmight return multiple quads for inline nodes.","experimental":true,"parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"quads","description":"Quads that describe node layout relative to viewport.","type":"array","items":{"$ref":"Quad"}}]},{"name":"getDocument","description":"Returns the root DOM node (and optionally the subtree) to the caller.","parameters":[{"name":"depth","description":"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false).","optional":true,"type":"boolean"}],"returns":[{"name":"root","description":"Resulting node.","$ref":"Node"}]},{"name":"getFlattenedDocument","description":"Returns the root DOM node (and optionally the subtree) to the caller.","parameters":[{"name":"depth","description":"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false).","optional":true,"type":"boolean"}],"returns":[{"name":"nodes","description":"Resulting node.","type":"array","items":{"$ref":"Node"}}]},{"name":"getNodeForLocation","description":"Returns node id at given location. Depending on whether DOM domain is enabled, nodeId is\\neither returned or not.","experimental":true,"parameters":[{"name":"x","description":"X coordinate.","type":"integer"},{"name":"y","description":"Y coordinate.","type":"integer"},{"name":"includeUserAgentShadowDOM","description":"False to skip to the nearest non-UA shadow root ancestor (default: false).","optional":true,"type":"boolean"}],"returns":[{"name":"backendNodeId","description":"Resulting node.","$ref":"BackendNodeId"},{"name":"nodeId","description":"Id of the node at given coordinates, only when enabled and requested document.","optional":true,"$ref":"NodeId"}]},{"name":"getOuterHTML","description":"Returns node\'s HTML markup.","parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"outerHTML","description":"Outer HTML markup.","type":"string"}]},{"name":"getRelayoutBoundary","description":"Returns the id of the nearest ancestor that is a relayout boundary.","experimental":true,"parameters":[{"name":"nodeId","description":"Id of the node.","$ref":"NodeId"}],"returns":[{"name":"nodeId","description":"Relayout boundary node id for the given node.","$ref":"NodeId"}]},{"name":"getSearchResults","description":"Returns search results from given `fromIndex` to given `toIndex` from the search with the given\\nidentifier.","experimental":true,"parameters":[{"name":"searchId","description":"Unique search session identifier.","type":"string"},{"name":"fromIndex","description":"Start index of the search result to be returned.","type":"integer"},{"name":"toIndex","description":"End index of the search result to be returned.","type":"integer"}],"returns":[{"name":"nodeIds","description":"Ids of the search result nodes.","type":"array","items":{"$ref":"NodeId"}}]},{"name":"hideHighlight","description":"Hides any highlight.","redirect":"Overlay"},{"name":"highlightNode","description":"Highlights DOM node.","redirect":"Overlay"},{"name":"highlightRect","description":"Highlights given rectangle.","redirect":"Overlay"},{"name":"markUndoableState","description":"Marks last undoable state.","experimental":true},{"name":"moveTo","description":"Moves node into the new container, places it before the given anchor.","parameters":[{"name":"nodeId","description":"Id of the node to move.","$ref":"NodeId"},{"name":"targetNodeId","description":"Id of the element to drop the moved node into.","$ref":"NodeId"},{"name":"insertBeforeNodeId","description":"Drop node before this one (if absent, the moved node becomes the last child of\\n`targetNodeId`).","optional":true,"$ref":"NodeId"}],"returns":[{"name":"nodeId","description":"New id of the moved node.","$ref":"NodeId"}]},{"name":"performSearch","description":"Searches for a given string in the DOM tree. Use `getSearchResults` to access search results or\\n`cancelSearch` to end this search session.","experimental":true,"parameters":[{"name":"query","description":"Plain text or query selector or XPath search query.","type":"string"},{"name":"includeUserAgentShadowDOM","description":"True to search in user agent shadow DOM.","optional":true,"type":"boolean"}],"returns":[{"name":"searchId","description":"Unique search session identifier.","type":"string"},{"name":"resultCount","description":"Number of search results.","type":"integer"}]},{"name":"pushNodeByPathToFrontend","description":"Requests that the node is sent to the caller given its path. // FIXME, use XPath","experimental":true,"parameters":[{"name":"path","description":"Path to node in the proprietary format.","type":"string"}],"returns":[{"name":"nodeId","description":"Id of the node for given path.","$ref":"NodeId"}]},{"name":"pushNodesByBackendIdsToFrontend","description":"Requests that a batch of nodes is sent to the caller given their backend node ids.","experimental":true,"parameters":[{"name":"backendNodeIds","description":"The array of backend node ids.","type":"array","items":{"$ref":"BackendNodeId"}}],"returns":[{"name":"nodeIds","description":"The array of ids of pushed nodes that correspond to the backend ids specified in\\nbackendNodeIds.","type":"array","items":{"$ref":"NodeId"}}]},{"name":"querySelector","description":"Executes `querySelector` on a given node.","parameters":[{"name":"nodeId","description":"Id of the node to query upon.","$ref":"NodeId"},{"name":"selector","description":"Selector string.","type":"string"}],"returns":[{"name":"nodeId","description":"Query selector result.","$ref":"NodeId"}]},{"name":"querySelectorAll","description":"Executes `querySelectorAll` on a given node.","parameters":[{"name":"nodeId","description":"Id of the node to query upon.","$ref":"NodeId"},{"name":"selector","description":"Selector string.","type":"string"}],"returns":[{"name":"nodeIds","description":"Query selector result.","type":"array","items":{"$ref":"NodeId"}}]},{"name":"redo","description":"Re-does the last undone action.","experimental":true},{"name":"removeAttribute","description":"Removes attribute with given name from an element with given id.","parameters":[{"name":"nodeId","description":"Id of the element to remove attribute from.","$ref":"NodeId"},{"name":"name","description":"Name of the attribute to remove.","type":"string"}]},{"name":"removeNode","description":"Removes node with given id.","parameters":[{"name":"nodeId","description":"Id of the node to remove.","$ref":"NodeId"}]},{"name":"requestChildNodes","description":"Requests that children of the node with given id are returned to the caller in form of\\n`setChildNodes` events where not only immediate children are retrieved, but all children down to\\nthe specified depth.","parameters":[{"name":"nodeId","description":"Id of the node to get children for.","$ref":"NodeId"},{"name":"depth","description":"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the sub-tree\\n(default is false).","optional":true,"type":"boolean"}]},{"name":"requestNode","description":"Requests that the node is sent to the caller given the JavaScript node object reference. All\\nnodes that form the path from the node to the root are also sent to the client as a series of\\n`setChildNodes` notifications.","parameters":[{"name":"objectId","description":"JavaScript object id to convert into node.","$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"nodeId","description":"Node id for given object.","$ref":"NodeId"}]},{"name":"resolveNode","description":"Resolves the JavaScript node object for a given NodeId or BackendNodeId.","parameters":[{"name":"nodeId","description":"Id of the node to resolve.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Backend identifier of the node to resolve.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects.","optional":true,"type":"string"},{"name":"executionContextId","description":"Execution context in which to resolve the node.","optional":true,"$ref":"Runtime.ExecutionContextId"}],"returns":[{"name":"object","description":"JavaScript object wrapper for given node.","$ref":"Runtime.RemoteObject"}]},{"name":"setAttributeValue","description":"Sets attribute for an element with given id.","parameters":[{"name":"nodeId","description":"Id of the element to set attribute for.","$ref":"NodeId"},{"name":"name","description":"Attribute name.","type":"string"},{"name":"value","description":"Attribute value.","type":"string"}]},{"name":"setAttributesAsText","description":"Sets attributes on element with given id. This method is useful when user edits some existing\\nattribute value and types in several attribute name/value pairs.","parameters":[{"name":"nodeId","description":"Id of the element to set attributes for.","$ref":"NodeId"},{"name":"text","description":"Text with a number of attributes. Will parse this text using HTML parser.","type":"string"},{"name":"name","description":"Attribute name to replace with new attributes derived from text in case text parsed\\nsuccessfully.","optional":true,"type":"string"}]},{"name":"setFileInputFiles","description":"Sets files for the given file input element.","parameters":[{"name":"files","description":"Array of file paths to set.","type":"array","items":{"type":"string"}},{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}]},{"name":"getFileInfo","description":"Returns file information for the given\\nFile wrapper.","experimental":true,"parameters":[{"name":"objectId","description":"JavaScript object id of the node wrapper.","$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"path","type":"string"}]},{"name":"setInspectedNode","description":"Enables console to refer to the node with given id via $x (see Command Line API for more details\\n$x functions).","experimental":true,"parameters":[{"name":"nodeId","description":"DOM node id to be accessible by means of $x command line API.","$ref":"NodeId"}]},{"name":"setNodeName","description":"Sets node name for a node with given id.","parameters":[{"name":"nodeId","description":"Id of the node to set name for.","$ref":"NodeId"},{"name":"name","description":"New node\'s name.","type":"string"}],"returns":[{"name":"nodeId","description":"New node\'s id.","$ref":"NodeId"}]},{"name":"setNodeValue","description":"Sets node value for a node with given id.","parameters":[{"name":"nodeId","description":"Id of the node to set value for.","$ref":"NodeId"},{"name":"value","description":"New node\'s value.","type":"string"}]},{"name":"setOuterHTML","description":"Sets node HTML markup, returns new node id.","parameters":[{"name":"nodeId","description":"Id of the node to set markup for.","$ref":"NodeId"},{"name":"outerHTML","description":"Outer HTML markup to set.","type":"string"}]},{"name":"undo","description":"Undoes the last performed action.","experimental":true},{"name":"getFrameOwner","description":"Returns iframe node that owns iframe with the given domain.","experimental":true,"parameters":[{"name":"frameId","$ref":"Page.FrameId"}],"returns":[{"name":"backendNodeId","description":"Resulting node.","$ref":"BackendNodeId"},{"name":"nodeId","description":"Id of the node at given coordinates, only when enabled and requested document.","optional":true,"$ref":"NodeId"}]}],"events":[{"name":"attributeModified","description":"Fired when `Element`\'s attribute is modified.","parameters":[{"name":"nodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"name","description":"Attribute name.","type":"string"},{"name":"value","description":"Attribute value.","type":"string"}]},{"name":"attributeRemoved","description":"Fired when `Element`\'s attribute is removed.","parameters":[{"name":"nodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"name","description":"A ttribute name.","type":"string"}]},{"name":"characterDataModified","description":"Mirrors `DOMCharacterDataModified` event.","parameters":[{"name":"nodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"characterData","description":"New text value.","type":"string"}]},{"name":"childNodeCountUpdated","description":"Fired when `Container`\'s child node count has changed.","parameters":[{"name":"nodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"childNodeCount","description":"New node count.","type":"integer"}]},{"name":"childNodeInserted","description":"Mirrors `DOMNodeInserted` event.","parameters":[{"name":"parentNodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"previousNodeId","description":"If of the previous siblint.","$ref":"NodeId"},{"name":"node","description":"Inserted node data.","$ref":"Node"}]},{"name":"childNodeRemoved","description":"Mirrors `DOMNodeRemoved` event.","parameters":[{"name":"parentNodeId","description":"Parent id.","$ref":"NodeId"},{"name":"nodeId","description":"Id of the node that has been removed.","$ref":"NodeId"}]},{"name":"distributedNodesUpdated","description":"Called when distrubution is changed.","experimental":true,"parameters":[{"name":"insertionPointId","description":"Insertion point where distrubuted nodes were updated.","$ref":"NodeId"},{"name":"distributedNodes","description":"Distributed nodes for given insertion point.","type":"array","items":{"$ref":"BackendNode"}}]},{"name":"documentUpdated","description":"Fired when `Document` has been totally updated. Node ids are no longer valid."},{"name":"inlineStyleInvalidated","description":"Fired when `Element`\'s inline style is modified via a CSS property modification.","experimental":true,"parameters":[{"name":"nodeIds","description":"Ids of the nodes for which the inline styles have been invalidated.","type":"array","items":{"$ref":"NodeId"}}]},{"name":"pseudoElementAdded","description":"Called when a pseudo element is added to an element.","experimental":true,"parameters":[{"name":"parentId","description":"Pseudo element\'s parent element id.","$ref":"NodeId"},{"name":"pseudoElement","description":"The added pseudo element.","$ref":"Node"}]},{"name":"pseudoElementRemoved","description":"Called when a pseudo element is removed from an element.","experimental":true,"parameters":[{"name":"parentId","description":"Pseudo element\'s parent element id.","$ref":"NodeId"},{"name":"pseudoElementId","description":"The removed pseudo element id.","$ref":"NodeId"}]},{"name":"setChildNodes","description":"Fired when backend wants to provide client with the missing DOM structure. This happens upon\\nmost of the calls requesting node ids.","parameters":[{"name":"parentId","description":"Parent node id to populate with children.","$ref":"NodeId"},{"name":"nodes","description":"Child nodes array.","type":"array","items":{"$ref":"Node"}}]},{"name":"shadowRootPopped","description":"Called when shadow root is popped from the element.","experimental":true,"parameters":[{"name":"hostId","description":"Host element id.","$ref":"NodeId"},{"name":"rootId","description":"Shadow root id.","$ref":"NodeId"}]},{"name":"shadowRootPushed","description":"Called when shadow root is pushed into the element.","experimental":true,"parameters":[{"name":"hostId","description":"Host element id.","$ref":"NodeId"},{"name":"root","description":"Shadow root.","$ref":"Node"}]}]},{"domain":"DOMDebugger","description":"DOM debugging allows setting breakpoints on particular DOM operations and events. JavaScript\\nexecution will stop on these operations as if there was a regular breakpoint set.","dependencies":["DOM","Debugger","Runtime"],"types":[{"id":"DOMBreakpointType","description":"DOM breakpoint type.","type":"string","enum":["subtree-modified","attribute-modified","node-removed"]},{"id":"EventListener","description":"Object event listener.","type":"object","properties":[{"name":"type","description":"`EventListener`\'s type.","type":"string"},{"name":"useCapture","description":"`EventListener`\'s useCapture.","type":"boolean"},{"name":"passive","description":"`EventListener`\'s passive flag.","type":"boolean"},{"name":"once","description":"`EventListener`\'s once flag.","type":"boolean"},{"name":"scriptId","description":"Script id of the handler code.","$ref":"Runtime.ScriptId"},{"name":"lineNumber","description":"Line number in the script (0-based).","type":"integer"},{"name":"columnNumber","description":"Column number in the script (0-based).","type":"integer"},{"name":"handler","description":"Event handler function value.","optional":true,"$ref":"Runtime.RemoteObject"},{"name":"originalHandler","description":"Event original handler function value.","optional":true,"$ref":"Runtime.RemoteObject"},{"name":"backendNodeId","description":"Node the listener is added to (if any).","optional":true,"$ref":"DOM.BackendNodeId"}]}],"commands":[{"name":"getEventListeners","description":"Returns event listeners of the given object.","parameters":[{"name":"objectId","description":"Identifier of the object to return listeners for.","$ref":"Runtime.RemoteObjectId"},{"name":"depth","description":"The maximum depth at which Node children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false). Reports listeners for all contexts if pierce is enabled.","optional":true,"type":"boolean"}],"returns":[{"name":"listeners","description":"Array of relevant listeners.","type":"array","items":{"$ref":"EventListener"}}]},{"name":"removeDOMBreakpoint","description":"Removes DOM breakpoint that was set using `setDOMBreakpoint`.","parameters":[{"name":"nodeId","description":"Identifier of the node to remove breakpoint from.","$ref":"DOM.NodeId"},{"name":"type","description":"Type of the breakpoint to remove.","$ref":"DOMBreakpointType"}]},{"name":"removeEventListenerBreakpoint","description":"Removes breakpoint on particular DOM event.","parameters":[{"name":"eventName","description":"Event name.","type":"string"},{"name":"targetName","description":"EventTarget interface name.","experimental":true,"optional":true,"type":"string"}]},{"name":"removeInstrumentationBreakpoint","description":"Removes breakpoint on particular native event.","experimental":true,"parameters":[{"name":"eventName","description":"Instrumentation name to stop on.","type":"string"}]},{"name":"removeXHRBreakpoint","description":"Removes breakpoint from XMLHttpRequest.","parameters":[{"name":"url","description":"Resource URL substring.","type":"string"}]},{"name":"setDOMBreakpoint","description":"Sets breakpoint on particular operation with DOM.","parameters":[{"name":"nodeId","description":"Identifier of the node to set breakpoint on.","$ref":"DOM.NodeId"},{"name":"type","description":"Type of the operation to stop upon.","$ref":"DOMBreakpointType"}]},{"name":"setEventListenerBreakpoint","description":"Sets breakpoint on particular DOM event.","parameters":[{"name":"eventName","description":"DOM Event name to stop on (any DOM event will do).","type":"string"},{"name":"targetName","description":"EventTarget interface name to stop on. If equal to `\\"*\\"` or not provided, will stop on any\\nEventTarget.","experimental":true,"optional":true,"type":"string"}]},{"name":"setInstrumentationBreakpoint","description":"Sets breakpoint on particular native event.","experimental":true,"parameters":[{"name":"eventName","description":"Instrumentation name to stop on.","type":"string"}]},{"name":"setXHRBreakpoint","description":"Sets breakpoint on XMLHttpRequest.","parameters":[{"name":"url","description":"Resource URL substring. All XHRs having this substring in the URL will get stopped upon.","type":"string"}]}]},{"domain":"DOMSnapshot","description":"This domain facilitates obtaining document snapshots with DOM, layout, and style information.","experimental":true,"dependencies":["CSS","DOM","DOMDebugger","Page"],"types":[{"id":"DOMNode","description":"A Node in the DOM tree.","type":"object","properties":[{"name":"nodeType","description":"`Node`\'s nodeType.","type":"integer"},{"name":"nodeName","description":"`Node`\'s nodeName.","type":"string"},{"name":"nodeValue","description":"`Node`\'s nodeValue.","type":"string"},{"name":"textValue","description":"Only set for textarea elements, contains the text value.","optional":true,"type":"string"},{"name":"inputValue","description":"Only set for input elements, contains the input\'s associated text value.","optional":true,"type":"string"},{"name":"inputChecked","description":"Only set for radio and checkbox input elements, indicates if the element has been checked","optional":true,"type":"boolean"},{"name":"optionSelected","description":"Only set for option elements, indicates if the element has been selected","optional":true,"type":"boolean"},{"name":"backendNodeId","description":"`Node`\'s id, corresponds to DOM.Node.backendNodeId.","$ref":"DOM.BackendNodeId"},{"name":"childNodeIndexes","description":"The indexes of the node\'s child nodes in the `domNodes` array returned by `getSnapshot`, if\\nany.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"attributes","description":"Attributes of an `Element` node.","optional":true,"type":"array","items":{"$ref":"NameValue"}},{"name":"pseudoElementIndexes","description":"Indexes of pseudo elements associated with this node in the `domNodes` array returned by\\n`getSnapshot`, if any.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"layoutNodeIndex","description":"The index of the node\'s related layout tree node in the `layoutTreeNodes` array returned by\\n`getSnapshot`, if any.","optional":true,"type":"integer"},{"name":"documentURL","description":"Document URL that `Document` or `FrameOwner` node points to.","optional":true,"type":"string"},{"name":"baseURL","description":"Base URL that `Document` or `FrameOwner` node uses for URL completion.","optional":true,"type":"string"},{"name":"contentLanguage","description":"Only set for documents, contains the document\'s content language.","optional":true,"type":"string"},{"name":"documentEncoding","description":"Only set for documents, contains the document\'s character set encoding.","optional":true,"type":"string"},{"name":"publicId","description":"`DocumentType` node\'s publicId.","optional":true,"type":"string"},{"name":"systemId","description":"`DocumentType` node\'s systemId.","optional":true,"type":"string"},{"name":"frameId","description":"Frame ID for frame owner elements and also for the document node.","optional":true,"$ref":"Page.FrameId"},{"name":"contentDocumentIndex","description":"The index of a frame owner element\'s content document in the `domNodes` array returned by\\n`getSnapshot`, if any.","optional":true,"type":"integer"},{"name":"pseudoType","description":"Type of a pseudo element node.","optional":true,"$ref":"DOM.PseudoType"},{"name":"shadowRootType","description":"Shadow root type.","optional":true,"$ref":"DOM.ShadowRootType"},{"name":"isClickable","description":"Whether this DOM node responds to mouse clicks. This includes nodes that have had click\\nevent listeners attached via JavaScript as well as anchor tags that naturally navigate when\\nclicked.","optional":true,"type":"boolean"},{"name":"eventListeners","description":"Details of the node\'s event listeners, if any.","optional":true,"type":"array","items":{"$ref":"DOMDebugger.EventListener"}},{"name":"currentSourceURL","description":"The selected url for nodes with a srcset attribute.","optional":true,"type":"string"},{"name":"originURL","description":"The url of the script (if any) that generates this node.","optional":true,"type":"string"},{"name":"scrollOffsetX","description":"Scroll offsets, set when this node is a Document.","optional":true,"type":"number"},{"name":"scrollOffsetY","optional":true,"type":"number"}]},{"id":"InlineTextBox","description":"Details of post layout rendered text positions. The exact layout should not be regarded as\\nstable and may change between versions.","type":"object","properties":[{"name":"boundingBox","description":"The bounding box in document coordinates. Note that scroll offset of the document is ignored.","$ref":"DOM.Rect"},{"name":"startCharacterIndex","description":"The starting index in characters, for this post layout textbox substring. Characters that\\nwould be represented as a surrogate pair in UTF-16 have length 2.","type":"integer"},{"name":"numCharacters","description":"The number of characters in this post layout textbox substring. Characters that would be\\nrepresented as a surrogate pair in UTF-16 have length 2.","type":"integer"}]},{"id":"LayoutTreeNode","description":"Details of an element in the DOM tree with a LayoutObject.","type":"object","properties":[{"name":"domNodeIndex","description":"The index of the related DOM node in the `domNodes` array returned by `getSnapshot`.","type":"integer"},{"name":"boundingBox","description":"The bounding box in document coordinates. Note that scroll offset of the document is ignored.","$ref":"DOM.Rect"},{"name":"layoutText","description":"Contents of the LayoutText, if any.","optional":true,"type":"string"},{"name":"inlineTextNodes","description":"The post-layout inline text nodes, if any.","optional":true,"type":"array","items":{"$ref":"InlineTextBox"}},{"name":"styleIndex","description":"Index into the `computedStyles` array returned by `getSnapshot`.","optional":true,"type":"integer"},{"name":"paintOrder","description":"Global paint order index, which is determined by the stacking order of the nodes. Nodes\\nthat are painted together will have the same index. Only provided if includePaintOrder in\\ngetSnapshot was true.","optional":true,"type":"integer"},{"name":"isStackingContext","description":"Set to true to indicate the element begins a new stacking context.","optional":true,"type":"boolean"}]},{"id":"ComputedStyle","description":"A subset of the full ComputedStyle as defined by the request whitelist.","type":"object","properties":[{"name":"properties","description":"Name/value pairs of computed style properties.","type":"array","items":{"$ref":"NameValue"}}]},{"id":"NameValue","description":"A name/value pair.","type":"object","properties":[{"name":"name","description":"Attribute/property name.","type":"string"},{"name":"value","description":"Attribute/property value.","type":"string"}]},{"id":"StringIndex","description":"Index of the string in the strings table.","type":"integer"},{"id":"ArrayOfStrings","description":"Index of the string in the strings table.","type":"array","items":{"$ref":"StringIndex"}},{"id":"RareStringData","description":"Data that is only present on rare nodes.","type":"object","properties":[{"name":"index","type":"array","items":{"type":"integer"}},{"name":"value","type":"array","items":{"$ref":"StringIndex"}}]},{"id":"RareBooleanData","type":"object","properties":[{"name":"index","type":"array","items":{"type":"integer"}}]},{"id":"RareIntegerData","type":"object","properties":[{"name":"index","type":"array","items":{"type":"integer"}},{"name":"value","type":"array","items":{"type":"integer"}}]},{"id":"Rectangle","type":"array","items":{"type":"number"}},{"id":"DocumentSnapshot","description":"Document snapshot.","type":"object","properties":[{"name":"documentURL","description":"Document URL that `Document` or `FrameOwner` node points to.","$ref":"StringIndex"},{"name":"baseURL","description":"Base URL that `Document` or `FrameOwner` node uses for URL completion.","$ref":"StringIndex"},{"name":"contentLanguage","description":"Contains the document\'s content language.","$ref":"StringIndex"},{"name":"encodingName","description":"Contains the document\'s character set encoding.","$ref":"StringIndex"},{"name":"publicId","description":"`DocumentType` node\'s publicId.","$ref":"StringIndex"},{"name":"systemId","description":"`DocumentType` node\'s systemId.","$ref":"StringIndex"},{"name":"frameId","description":"Frame ID for frame owner elements and also for the document node.","$ref":"StringIndex"},{"name":"nodes","description":"A table with dom nodes.","$ref":"NodeTreeSnapshot"},{"name":"layout","description":"The nodes in the layout tree.","$ref":"LayoutTreeSnapshot"},{"name":"textBoxes","description":"The post-layout inline text nodes.","$ref":"TextBoxSnapshot"},{"name":"scrollOffsetX","description":"Horizontal scroll offset.","optional":true,"type":"number"},{"name":"scrollOffsetY","description":"Vertical scroll offset.","optional":true,"type":"number"}]},{"id":"NodeTreeSnapshot","description":"Table containing nodes.","type":"object","properties":[{"name":"parentIndex","description":"Parent node index.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"nodeType","description":"`Node`\'s nodeType.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"nodeName","description":"`Node`\'s nodeName.","optional":true,"type":"array","items":{"$ref":"StringIndex"}},{"name":"nodeValue","description":"`Node`\'s nodeValue.","optional":true,"type":"array","items":{"$ref":"StringIndex"}},{"name":"backendNodeId","description":"`Node`\'s id, corresponds to DOM.Node.backendNodeId.","optional":true,"type":"array","items":{"$ref":"DOM.BackendNodeId"}},{"name":"attributes","description":"Attributes of an `Element` node. Flatten name, value pairs.","optional":true,"type":"array","items":{"$ref":"ArrayOfStrings"}},{"name":"textValue","description":"Only set for textarea elements, contains the text value.","optional":true,"$ref":"RareStringData"},{"name":"inputValue","description":"Only set for input elements, contains the input\'s associated text value.","optional":true,"$ref":"RareStringData"},{"name":"inputChecked","description":"Only set for radio and checkbox input elements, indicates if the element has been checked","optional":true,"$ref":"RareBooleanData"},{"name":"optionSelected","description":"Only set for option elements, indicates if the element has been selected","optional":true,"$ref":"RareBooleanData"},{"name":"contentDocumentIndex","description":"The index of the document in the list of the snapshot documents.","optional":true,"$ref":"RareIntegerData"},{"name":"pseudoType","description":"Type of a pseudo element node.","optional":true,"$ref":"RareStringData"},{"name":"isClickable","description":"Whether this DOM node responds to mouse clicks. This includes nodes that have had click\\nevent listeners attached via JavaScript as well as anchor tags that naturally navigate when\\nclicked.","optional":true,"$ref":"RareBooleanData"},{"name":"currentSourceURL","description":"The selected url for nodes with a srcset attribute.","optional":true,"$ref":"RareStringData"},{"name":"originURL","description":"The url of the script (if any) that generates this node.","optional":true,"$ref":"RareStringData"}]},{"id":"LayoutTreeSnapshot","description":"Table of details of an element in the DOM tree with a LayoutObject.","type":"object","properties":[{"name":"nodeIndex","description":"Index of the corresponding node in the `NodeTreeSnapshot` array returned by `captureSnapshot`.","type":"array","items":{"type":"integer"}},{"name":"styles","description":"Array of indexes specifying computed style strings, filtered according to the `computedStyles` parameter passed to `captureSnapshot`.","type":"array","items":{"$ref":"ArrayOfStrings"}},{"name":"bounds","description":"The absolute position bounding box.","type":"array","items":{"$ref":"Rectangle"}},{"name":"text","description":"Contents of the LayoutText, if any.","type":"array","items":{"$ref":"StringIndex"}},{"name":"stackingContexts","description":"Stacking context information.","$ref":"RareBooleanData"},{"name":"offsetRects","description":"The offset rect of nodes. Only available when includeDOMRects is set to true","optional":true,"type":"array","items":{"$ref":"Rectangle"}},{"name":"scrollRects","description":"The scroll rect of nodes. Only available when includeDOMRects is set to true","optional":true,"type":"array","items":{"$ref":"Rectangle"}},{"name":"clientRects","description":"The client rect of nodes. Only available when includeDOMRects is set to true","optional":true,"type":"array","items":{"$ref":"Rectangle"}}]},{"id":"TextBoxSnapshot","description":"Table of details of the post layout rendered text positions. The exact layout should not be regarded as\\nstable and may change between versions.","type":"object","properties":[{"name":"layoutIndex","description":"Index of the layout tree node that owns this box collection.","type":"array","items":{"type":"integer"}},{"name":"bounds","description":"The absolute position bounding box.","type":"array","items":{"$ref":"Rectangle"}},{"name":"start","description":"The starting index in characters, for this post layout textbox substring. Characters that\\nwould be represented as a surrogate pair in UTF-16 have length 2.","type":"array","items":{"type":"integer"}},{"name":"length","description":"The number of characters in this post layout textbox substring. Characters that would be\\nrepresented as a surrogate pair in UTF-16 have length 2.","type":"array","items":{"type":"integer"}}]}],"commands":[{"name":"disable","description":"Disables DOM snapshot agent for the given page."},{"name":"enable","description":"Enables DOM snapshot agent for the given page."},{"name":"getSnapshot","description":"Returns a document snapshot, including the full DOM tree of the root node (including iframes,\\ntemplate contents, and imported documents) in a flattened array, as well as layout and\\nwhite-listed computed style information for the nodes. Shadow DOM in the returned DOM tree is\\nflattened.","deprecated":true,"parameters":[{"name":"computedStyleWhitelist","description":"Whitelist of computed styles to return.","type":"array","items":{"type":"string"}},{"name":"includeEventListeners","description":"Whether or not to retrieve details of DOM listeners (default false).","optional":true,"type":"boolean"},{"name":"includePaintOrder","description":"Whether to determine and include the paint order index of LayoutTreeNodes (default false).","optional":true,"type":"boolean"},{"name":"includeUserAgentShadowTree","description":"Whether to include UA shadow tree in the snapshot (default false).","optional":true,"type":"boolean"}],"returns":[{"name":"domNodes","description":"The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document.","type":"array","items":{"$ref":"DOMNode"}},{"name":"layoutTreeNodes","description":"The nodes in the layout tree.","type":"array","items":{"$ref":"LayoutTreeNode"}},{"name":"computedStyles","description":"Whitelisted ComputedStyle properties for each node in the layout tree.","type":"array","items":{"$ref":"ComputedStyle"}}]},{"name":"captureSnapshot","description":"Returns a document snapshot, including the full DOM tree of the root node (including iframes,\\ntemplate contents, and imported documents) in a flattened array, as well as layout and\\nwhite-listed computed style information for the nodes. Shadow DOM in the returned DOM tree is\\nflattened.","parameters":[{"name":"computedStyles","description":"Whitelist of computed styles to return.","type":"array","items":{"type":"string"}},{"name":"includeDOMRects","description":"Whether to include DOM rectangles (offsetRects, clientRects, scrollRects) into the snapshot","optional":true,"type":"boolean"}],"returns":[{"name":"documents","description":"The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document.","type":"array","items":{"$ref":"DocumentSnapshot"}},{"name":"strings","description":"Shared string table that all string properties refer to with indexes.","type":"array","items":{"type":"string"}}]}]},{"domain":"DOMStorage","description":"Query and modify DOM storage.","experimental":true,"types":[{"id":"StorageId","description":"DOM Storage identifier.","type":"object","properties":[{"name":"securityOrigin","description":"Security origin for the storage.","type":"string"},{"name":"isLocalStorage","description":"Whether the storage is local storage (not session storage).","type":"boolean"}]},{"id":"Item","description":"DOM Storage item.","type":"array","items":{"type":"string"}}],"commands":[{"name":"clear","parameters":[{"name":"storageId","$ref":"StorageId"}]},{"name":"disable","description":"Disables storage tracking, prevents storage events from being sent to the client."},{"name":"enable","description":"Enables storage tracking, storage events will now be delivered to the client."},{"name":"getDOMStorageItems","parameters":[{"name":"storageId","$ref":"StorageId"}],"returns":[{"name":"entries","type":"array","items":{"$ref":"Item"}}]},{"name":"removeDOMStorageItem","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"}]},{"name":"setDOMStorageItem","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"},{"name":"value","type":"string"}]}],"events":[{"name":"domStorageItemAdded","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"},{"name":"newValue","type":"string"}]},{"name":"domStorageItemRemoved","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"}]},{"name":"domStorageItemUpdated","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"},{"name":"oldValue","type":"string"},{"name":"newValue","type":"string"}]},{"name":"domStorageItemsCleared","parameters":[{"name":"storageId","$ref":"StorageId"}]}]},{"domain":"Database","experimental":true,"types":[{"id":"DatabaseId","description":"Unique identifier of Database object.","type":"string"},{"id":"Database","description":"Database object.","type":"object","properties":[{"name":"id","description":"Database ID.","$ref":"DatabaseId"},{"name":"domain","description":"Database domain.","type":"string"},{"name":"name","description":"Database name.","type":"string"},{"name":"version","description":"Database version.","type":"string"}]},{"id":"Error","description":"Database error.","type":"object","properties":[{"name":"message","description":"Error message.","type":"string"},{"name":"code","description":"Error code.","type":"integer"}]}],"commands":[{"name":"disable","description":"Disables database tracking, prevents database events from being sent to the client."},{"name":"enable","description":"Enables database tracking, database events will now be delivered to the client."},{"name":"executeSQL","parameters":[{"name":"databaseId","$ref":"DatabaseId"},{"name":"query","type":"string"}],"returns":[{"name":"columnNames","optional":true,"type":"array","items":{"type":"string"}},{"name":"values","optional":true,"type":"array","items":{"type":"any"}},{"name":"sqlError","optional":true,"$ref":"Error"}]},{"name":"getDatabaseTableNames","parameters":[{"name":"databaseId","$ref":"DatabaseId"}],"returns":[{"name":"tableNames","type":"array","items":{"type":"string"}}]}],"events":[{"name":"addDatabase","parameters":[{"name":"database","$ref":"Database"}]}]},{"domain":"DeviceOrientation","experimental":true,"commands":[{"name":"clearDeviceOrientationOverride","description":"Clears the overridden Device Orientation."},{"name":"setDeviceOrientationOverride","description":"Overrides the Device Orientation.","parameters":[{"name":"alpha","description":"Mock alpha","type":"number"},{"name":"beta","description":"Mock beta","type":"number"},{"name":"gamma","description":"Mock gamma","type":"number"}]}]},{"domain":"Emulation","description":"This domain emulates different environments for the page.","dependencies":["DOM","Page","Runtime"],"types":[{"id":"ScreenOrientation","description":"Screen orientation.","type":"object","properties":[{"name":"type","description":"Orientation type.","type":"string","enum":["portraitPrimary","portraitSecondary","landscapePrimary","landscapeSecondary"]},{"name":"angle","description":"Orientation angle.","type":"integer"}]},{"id":"VirtualTimePolicy","description":"advance: If the scheduler runs out of immediate work, the virtual time base may fast forward to\\nallow the next delayed task (if any) to run; pause: The virtual time base may not advance;\\npauseIfNetworkFetchesPending: The virtual time base may not advance if there are any pending\\nresource fetches.","experimental":true,"type":"string","enum":["advance","pause","pauseIfNetworkFetchesPending"]}],"commands":[{"name":"canEmulate","description":"Tells whether emulation is supported.","returns":[{"name":"result","description":"True if emulation is supported.","type":"boolean"}]},{"name":"clearDeviceMetricsOverride","description":"Clears the overriden device metrics."},{"name":"clearGeolocationOverride","description":"Clears the overriden Geolocation Position and Error."},{"name":"resetPageScaleFactor","description":"Requests that page scale factor is reset to initial values.","experimental":true},{"name":"setFocusEmulationEnabled","description":"Enables or disables simulating a focused and active page.","experimental":true,"parameters":[{"name":"enabled","description":"Whether to enable to disable focus emulation.","type":"boolean"}]},{"name":"setCPUThrottlingRate","description":"Enables CPU throttling to emulate slow CPUs.","experimental":true,"parameters":[{"name":"rate","description":"Throttling rate as a slowdown factor (1 is no throttle, 2 is 2x slowdown, etc).","type":"number"}]},{"name":"setDefaultBackgroundColorOverride","description":"Sets or clears an override of the default background color of the frame. This override is used\\nif the content does not specify one.","parameters":[{"name":"color","description":"RGBA of the default background color. If not specified, any existing override will be\\ncleared.","optional":true,"$ref":"DOM.RGBA"}]},{"name":"setDeviceMetricsOverride","description":"Overrides the values of device screen dimensions (window.screen.width, window.screen.height,\\nwindow.innerWidth, window.innerHeight, and \\"device-width\\"/\\"device-height\\"-related CSS media\\nquery results).","parameters":[{"name":"width","description":"Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override.","type":"integer"},{"name":"height","description":"Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override.","type":"integer"},{"name":"deviceScaleFactor","description":"Overriding device scale factor value. 0 disables the override.","type":"number"},{"name":"mobile","description":"Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text\\nautosizing and more.","type":"boolean"},{"name":"scale","description":"Scale to apply to resulting view image.","experimental":true,"optional":true,"type":"number"},{"name":"screenWidth","description":"Overriding screen width value in pixels (minimum 0, maximum 10000000).","experimental":true,"optional":true,"type":"integer"},{"name":"screenHeight","description":"Overriding screen height value in pixels (minimum 0, maximum 10000000).","experimental":true,"optional":true,"type":"integer"},{"name":"positionX","description":"Overriding view X position on screen in pixels (minimum 0, maximum 10000000).","experimental":true,"optional":true,"type":"integer"},{"name":"positionY","description":"Overriding view Y position on screen in pixels (minimum 0, maximum 10000000).","experimental":true,"optional":true,"type":"integer"},{"name":"dontSetVisibleSize","description":"Do not set visible view size, rely upon explicit setVisibleSize call.","experimental":true,"optional":true,"type":"boolean"},{"name":"screenOrientation","description":"Screen orientation override.","optional":true,"$ref":"ScreenOrientation"},{"name":"viewport","description":"If set, the visible area of the page will be overridden to this viewport. This viewport\\nchange is not observed by the page, e.g. viewport-relative elements do not change positions.","experimental":true,"optional":true,"$ref":"Page.Viewport"}]},{"name":"setScrollbarsHidden","experimental":true,"parameters":[{"name":"hidden","description":"Whether scrollbars should be always hidden.","type":"boolean"}]},{"name":"setDocumentCookieDisabled","experimental":true,"parameters":[{"name":"disabled","description":"Whether document.coookie API should be disabled.","type":"boolean"}]},{"name":"setEmitTouchEventsForMouse","experimental":true,"parameters":[{"name":"enabled","description":"Whether touch emulation based on mouse input should be enabled.","type":"boolean"},{"name":"configuration","description":"Touch/gesture events configuration. Default: current platform.","optional":true,"type":"string","enum":["mobile","desktop"]}]},{"name":"setEmulatedMedia","description":"Emulates the given media for CSS media queries.","parameters":[{"name":"media","description":"Media type to emulate. Empty string disables the override.","type":"string"}]},{"name":"setGeolocationOverride","description":"Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position\\nunavailable.","parameters":[{"name":"latitude","description":"Mock latitude","optional":true,"type":"number"},{"name":"longitude","description":"Mock longitude","optional":true,"type":"number"},{"name":"accuracy","description":"Mock accuracy","optional":true,"type":"number"}]},{"name":"setNavigatorOverrides","description":"Overrides value returned by the javascript navigator object.","experimental":true,"deprecated":true,"parameters":[{"name":"platform","description":"The platform navigator.platform should return.","type":"string"}]},{"name":"setPageScaleFactor","description":"Sets a specified page scale factor.","experimental":true,"parameters":[{"name":"pageScaleFactor","description":"Page scale factor.","type":"number"}]},{"name":"setScriptExecutionDisabled","description":"Switches script execution in the page.","parameters":[{"name":"value","description":"Whether script execution should be disabled in the page.","type":"boolean"}]},{"name":"setTouchEmulationEnabled","description":"Enables touch on platforms which do not support them.","parameters":[{"name":"enabled","description":"Whether the touch event emulation should be enabled.","type":"boolean"},{"name":"maxTouchPoints","description":"Maximum touch points supported. Defaults to one.","optional":true,"type":"integer"}]},{"name":"setVirtualTimePolicy","description":"Turns on virtual time for all frames (replacing real-time with a synthetic time source) and sets\\nthe current virtual time policy. Note this supersedes any previous time budget.","experimental":true,"parameters":[{"name":"policy","$ref":"VirtualTimePolicy"},{"name":"budget","description":"If set, after this many virtual milliseconds have elapsed virtual time will be paused and a\\nvirtualTimeBudgetExpired event is sent.","optional":true,"type":"number"},{"name":"maxVirtualTimeTaskStarvationCount","description":"If set this specifies the maximum number of tasks that can be run before virtual is forced\\nforwards to prevent deadlock.","optional":true,"type":"integer"},{"name":"waitForNavigation","description":"If set the virtual time policy change should be deferred until any frame starts navigating.\\nNote any previous deferred policy change is superseded.","optional":true,"type":"boolean"},{"name":"initialVirtualTime","description":"If set, base::Time::Now will be overriden to initially return this value.","optional":true,"$ref":"Network.TimeSinceEpoch"}],"returns":[{"name":"virtualTimeTicksBase","description":"Absolute timestamp at which virtual time was first enabled (up time in milliseconds).","type":"number"}]},{"name":"setTimezoneOverride","description":"Overrides default host system timezone with the specified one.","experimental":true,"parameters":[{"name":"timezoneId","description":"The timezone identifier. If empty, disables the override and\\nrestores default host system timezone.","type":"string"}]},{"name":"setVisibleSize","description":"Resizes the frame/viewport of the page. Note that this does not affect the frame\'s container\\n(e.g. browser window). Can be used to produce screenshots of the specified size. Not supported\\non Android.","experimental":true,"deprecated":true,"parameters":[{"name":"width","description":"Frame width (DIP).","type":"integer"},{"name":"height","description":"Frame height (DIP).","type":"integer"}]},{"name":"setUserAgentOverride","description":"Allows overriding user agent with the given string.","parameters":[{"name":"userAgent","description":"User agent to use.","type":"string"},{"name":"acceptLanguage","description":"Browser langugage to emulate.","optional":true,"type":"string"},{"name":"platform","description":"The platform navigator.platform should return.","optional":true,"type":"string"}]}],"events":[{"name":"virtualTimeBudgetExpired","description":"Notification sent after the virtual time budget for the current VirtualTimePolicy has run out.","experimental":true}]},{"domain":"HeadlessExperimental","description":"This domain provides experimental commands only supported in headless mode.","experimental":true,"dependencies":["Page","Runtime"],"types":[{"id":"ScreenshotParams","description":"Encoding options for a screenshot.","type":"object","properties":[{"name":"format","description":"Image compression format (defaults to png).","optional":true,"type":"string","enum":["jpeg","png"]},{"name":"quality","description":"Compression quality from range [0..100] (jpeg only).","optional":true,"type":"integer"}]}],"commands":[{"name":"beginFrame","description":"Sends a BeginFrame to the target and returns when the frame was completed. Optionally captures a\\nscreenshot from the resulting frame. Requires that the target was created with enabled\\nBeginFrameControl. Designed for use with --run-all-compositor-stages-before-draw, see also\\nhttps://goo.gl/3zHXhB for more background.","parameters":[{"name":"frameTimeTicks","description":"Timestamp of this BeginFrame in Renderer TimeTicks (milliseconds of uptime). If not set,\\nthe current time will be used.","optional":true,"type":"number"},{"name":"interval","description":"The interval between BeginFrames that is reported to the compositor, in milliseconds.\\nDefaults to a 60 frames/second interval, i.e. about 16.666 milliseconds.","optional":true,"type":"number"},{"name":"noDisplayUpdates","description":"Whether updates should not be committed and drawn onto the display. False by default. If\\ntrue, only side effects of the BeginFrame will be run, such as layout and animations, but\\nany visual updates may not be visible on the display or in screenshots.","optional":true,"type":"boolean"},{"name":"screenshot","description":"If set, a screenshot of the frame will be captured and returned in the response. Otherwise,\\nno screenshot will be captured. Note that capturing a screenshot can fail, for example,\\nduring renderer initialization. In such a case, no screenshot data will be returned.","optional":true,"$ref":"ScreenshotParams"}],"returns":[{"name":"hasDamage","description":"Whether the BeginFrame resulted in damage and, thus, a new frame was committed to the\\ndisplay. Reported for diagnostic uses, may be removed in the future.","type":"boolean"},{"name":"screenshotData","description":"Base64-encoded image data of the screenshot, if one was requested and successfully taken.","optional":true,"type":"string"}]},{"name":"disable","description":"Disables headless events for the target."},{"name":"enable","description":"Enables headless events for the target."}],"events":[{"name":"needsBeginFramesChanged","description":"Issued when the target starts or stops needing BeginFrames.","parameters":[{"name":"needsBeginFrames","description":"True if BeginFrames are needed, false otherwise.","type":"boolean"}]}]},{"domain":"IO","description":"Input/Output operations for streams produced by DevTools.","types":[{"id":"StreamHandle","description":"This is either obtained from another method or specifed as `blob:<uuid>` where\\n`<uuid>` is an UUID of a Blob.","type":"string"}],"commands":[{"name":"close","description":"Close the stream, discard any temporary backing storage.","parameters":[{"name":"handle","description":"Handle of the stream to close.","$ref":"StreamHandle"}]},{"name":"read","description":"Read a chunk of the stream","parameters":[{"name":"handle","description":"Handle of the stream to read.","$ref":"StreamHandle"},{"name":"offset","description":"Seek to the specified offset before reading (if not specificed, proceed with offset\\nfollowing the last read). Some types of streams may only support sequential reads.","optional":true,"type":"integer"},{"name":"size","description":"Maximum number of bytes to read (left upon the agent discretion if not specified).","optional":true,"type":"integer"}],"returns":[{"name":"base64Encoded","description":"Set if the data is base64-encoded","optional":true,"type":"boolean"},{"name":"data","description":"Data that were read.","type":"string"},{"name":"eof","description":"Set if the end-of-file condition occured while reading.","type":"boolean"}]},{"name":"resolveBlob","description":"Return UUID of Blob object specified by a remote object id.","parameters":[{"name":"objectId","description":"Object id of a Blob object wrapper.","$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"uuid","description":"UUID of the specified Blob.","type":"string"}]}]},{"domain":"IndexedDB","experimental":true,"dependencies":["Runtime"],"types":[{"id":"DatabaseWithObjectStores","description":"Database with an array of object stores.","type":"object","properties":[{"name":"name","description":"Database name.","type":"string"},{"name":"version","description":"Database version (type is not \'integer\', as the standard\\nrequires the version number to be \'unsigned long long\')","type":"number"},{"name":"objectStores","description":"Object stores in this database.","type":"array","items":{"$ref":"ObjectStore"}}]},{"id":"ObjectStore","description":"Object store.","type":"object","properties":[{"name":"name","description":"Object store name.","type":"string"},{"name":"keyPath","description":"Object store key path.","$ref":"KeyPath"},{"name":"autoIncrement","description":"If true, object store has auto increment flag set.","type":"boolean"},{"name":"indexes","description":"Indexes in this object store.","type":"array","items":{"$ref":"ObjectStoreIndex"}}]},{"id":"ObjectStoreIndex","description":"Object store index.","type":"object","properties":[{"name":"name","description":"Index name.","type":"string"},{"name":"keyPath","description":"Index key path.","$ref":"KeyPath"},{"name":"unique","description":"If true, index is unique.","type":"boolean"},{"name":"multiEntry","description":"If true, index allows multiple entries for a key.","type":"boolean"}]},{"id":"Key","description":"Key.","type":"object","properties":[{"name":"type","description":"Key type.","type":"string","enum":["number","string","date","array"]},{"name":"number","description":"Number value.","optional":true,"type":"number"},{"name":"string","description":"String value.","optional":true,"type":"string"},{"name":"date","description":"Date value.","optional":true,"type":"number"},{"name":"array","description":"Array value.","optional":true,"type":"array","items":{"$ref":"Key"}}]},{"id":"KeyRange","description":"Key range.","type":"object","properties":[{"name":"lower","description":"Lower bound.","optional":true,"$ref":"Key"},{"name":"upper","description":"Upper bound.","optional":true,"$ref":"Key"},{"name":"lowerOpen","description":"If true lower bound is open.","type":"boolean"},{"name":"upperOpen","description":"If true upper bound is open.","type":"boolean"}]},{"id":"DataEntry","description":"Data entry.","type":"object","properties":[{"name":"key","description":"Key object.","$ref":"Runtime.RemoteObject"},{"name":"primaryKey","description":"Primary key object.","$ref":"Runtime.RemoteObject"},{"name":"value","description":"Value object.","$ref":"Runtime.RemoteObject"}]},{"id":"KeyPath","description":"Key path.","type":"object","properties":[{"name":"type","description":"Key path type.","type":"string","enum":["null","string","array"]},{"name":"string","description":"String value.","optional":true,"type":"string"},{"name":"array","description":"Array value.","optional":true,"type":"array","items":{"type":"string"}}]}],"commands":[{"name":"clearObjectStore","description":"Clears all entries from an object store.","parameters":[{"name":"securityOrigin","description":"Security origin.","type":"string"},{"name":"databaseName","description":"Database name.","type":"string"},{"name":"objectStoreName","description":"Object store name.","type":"string"}]},{"name":"deleteDatabase","description":"Deletes a database.","parameters":[{"name":"securityOrigin","description":"Security origin.","type":"string"},{"name":"databaseName","description":"Database name.","type":"string"}]},{"name":"deleteObjectStoreEntries","description":"Delete a range of entries from an object store","parameters":[{"name":"securityOrigin","type":"string"},{"name":"databaseName","type":"string"},{"name":"objectStoreName","type":"string"},{"name":"keyRange","description":"Range of entry keys to delete","$ref":"KeyRange"}]},{"name":"disable","description":"Disables events from backend."},{"name":"enable","description":"Enables events from backend."},{"name":"requestData","description":"Requests data from object store or index.","parameters":[{"name":"securityOrigin","description":"Security origin.","type":"string"},{"name":"databaseName","description":"Database name.","type":"string"},{"name":"objectStoreName","description":"Object store name.","type":"string"},{"name":"indexName","description":"Index name, empty string for object store data requests.","type":"string"},{"name":"skipCount","description":"Number of records to skip.","type":"integer"},{"name":"pageSize","description":"Number of records to fetch.","type":"integer"},{"name":"keyRange","description":"Key range.","optional":true,"$ref":"KeyRange"}],"returns":[{"name":"objectStoreDataEntries","description":"Array of object store data entries.","type":"array","items":{"$ref":"DataEntry"}},{"name":"hasMore","description":"If true, there are more entries to fetch in the given range.","type":"boolean"}]},{"name":"getMetadata","description":"Gets metadata of an object store","parameters":[{"name":"securityOrigin","description":"Security origin.","type":"string"},{"name":"databaseName","description":"Database name.","type":"string"},{"name":"objectStoreName","description":"Object store name.","type":"string"}],"returns":[{"name":"entriesCount","description":"the entries count","type":"number"},{"name":"keyGeneratorValue","description":"the current value of key generator, to become the next inserted\\nkey into the object store. Valid if objectStore.autoIncrement\\nis true.","type":"number"}]},{"name":"requestDatabase","description":"Requests database with given name in given frame.","parameters":[{"name":"securityOrigin","description":"Security origin.","type":"string"},{"name":"databaseName","description":"Database name.","type":"string"}],"returns":[{"name":"databaseWithObjectStores","description":"Database with an array of object stores.","$ref":"DatabaseWithObjectStores"}]},{"name":"requestDatabaseNames","description":"Requests database names for given security origin.","parameters":[{"name":"securityOrigin","description":"Security origin.","type":"string"}],"returns":[{"name":"databaseNames","description":"Database names for origin.","type":"array","items":{"type":"string"}}]}]},{"domain":"Input","types":[{"id":"TouchPoint","type":"object","properties":[{"name":"x","description":"X coordinate of the event relative to the main frame\'s viewport in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the event relative to the main frame\'s viewport in CSS pixels. 0 refers to\\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.","type":"number"},{"name":"radiusX","description":"X radius of the touch area (default: 1.0).","optional":true,"type":"number"},{"name":"radiusY","description":"Y radius of the touch area (default: 1.0).","optional":true,"type":"number"},{"name":"rotationAngle","description":"Rotation angle (default: 0.0).","optional":true,"type":"number"},{"name":"force","description":"Force (default: 1.0).","optional":true,"type":"number"},{"name":"id","description":"Identifier used to track touch sources between events, must be unique within an event.","optional":true,"type":"number"}]},{"id":"GestureSourceType","experimental":true,"type":"string","enum":["default","touch","mouse"]},{"id":"TimeSinceEpoch","description":"UTC time in seconds, counted from January 1, 1970.","type":"number"}],"commands":[{"name":"dispatchKeyEvent","description":"Dispatches a key event to the page.","parameters":[{"name":"type","description":"Type of the key event.","type":"string","enum":["keyDown","keyUp","rawKeyDown","char"]},{"name":"modifiers","description":"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).","optional":true,"type":"integer"},{"name":"timestamp","description":"Time at which the event occurred.","optional":true,"$ref":"TimeSinceEpoch"},{"name":"text","description":"Text as generated by processing a virtual key code with a keyboard layout. Not needed for\\nfor `keyUp` and `rawKeyDown` events (default: \\"\\")","optional":true,"type":"string"},{"name":"unmodifiedText","description":"Text that would have been generated by the keyboard if no modifiers were pressed (except for\\nshift). Useful for shortcut (accelerator) key handling (default: \\"\\").","optional":true,"type":"string"},{"name":"keyIdentifier","description":"Unique key identifier (e.g., \'U+0041\') (default: \\"\\").","optional":true,"type":"string"},{"name":"code","description":"Unique DOM defined string value for each physical key (e.g., \'KeyA\') (default: \\"\\").","optional":true,"type":"string"},{"name":"key","description":"Unique DOM defined string value describing the meaning of the key in the context of active\\nmodifiers, keyboard layout, etc (e.g., \'AltGr\') (default: \\"\\").","optional":true,"type":"string"},{"name":"windowsVirtualKeyCode","description":"Windows virtual key code (default: 0).","optional":true,"type":"integer"},{"name":"nativeVirtualKeyCode","description":"Native virtual key code (default: 0).","optional":true,"type":"integer"},{"name":"autoRepeat","description":"Whether the event was generated from auto repeat (default: false).","optional":true,"type":"boolean"},{"name":"isKeypad","description":"Whether the event was generated from the keypad (default: false).","optional":true,"type":"boolean"},{"name":"isSystemKey","description":"Whether the event was a system key event (default: false).","optional":true,"type":"boolean"},{"name":"location","description":"Whether the event was from the left or right side of the keyboard. 1=Left, 2=Right (default:\\n0).","optional":true,"type":"integer"}]},{"name":"insertText","description":"This method emulates inserting text that doesn\'t come from a key press,\\nfor example an emoji keyboard or an IME.","experimental":true,"parameters":[{"name":"text","description":"The text to insert.","type":"string"}]},{"name":"dispatchMouseEvent","description":"Dispatches a mouse event to the page.","parameters":[{"name":"type","description":"Type of the mouse event.","type":"string","enum":["mousePressed","mouseReleased","mouseMoved","mouseWheel"]},{"name":"x","description":"X coordinate of the event relative to the main frame\'s viewport in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the event relative to the main frame\'s viewport in CSS pixels. 0 refers to\\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.","type":"number"},{"name":"modifiers","description":"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).","optional":true,"type":"integer"},{"name":"timestamp","description":"Time at which the event occurred.","optional":true,"$ref":"TimeSinceEpoch"},{"name":"button","description":"Mouse button (default: \\"none\\").","optional":true,"type":"string","enum":["none","left","middle","right","back","forward"]},{"name":"buttons","description":"A number indicating which buttons are pressed on the mouse when a mouse event is triggered.\\nLeft=1, Right=2, Middle=4, Back=8, Forward=16, None=0.","optional":true,"type":"integer"},{"name":"clickCount","description":"Number of times the mouse button was clicked (default: 0).","optional":true,"type":"integer"},{"name":"deltaX","description":"X delta in CSS pixels for mouse wheel event (default: 0).","optional":true,"type":"number"},{"name":"deltaY","description":"Y delta in CSS pixels for mouse wheel event (default: 0).","optional":true,"type":"number"},{"name":"pointerType","description":"Pointer type (default: \\"mouse\\").","optional":true,"type":"string","enum":["mouse","pen"]}]},{"name":"dispatchTouchEvent","description":"Dispatches a touch event to the page.","parameters":[{"name":"type","description":"Type of the touch event. TouchEnd and TouchCancel must not contain any touch points, while\\nTouchStart and TouchMove must contains at least one.","type":"string","enum":["touchStart","touchEnd","touchMove","touchCancel"]},{"name":"touchPoints","description":"Active touch points on the touch device. One event per any changed point (compared to\\nprevious touch event in a sequence) is generated, emulating pressing/moving/releasing points\\none by one.","type":"array","items":{"$ref":"TouchPoint"}},{"name":"modifiers","description":"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).","optional":true,"type":"integer"},{"name":"timestamp","description":"Time at which the event occurred.","optional":true,"$ref":"TimeSinceEpoch"}]},{"name":"emulateTouchFromMouseEvent","description":"Emulates touch event from the mouse event parameters.","experimental":true,"parameters":[{"name":"type","description":"Type of the mouse event.","type":"string","enum":["mousePressed","mouseReleased","mouseMoved","mouseWheel"]},{"name":"x","description":"X coordinate of the mouse pointer in DIP.","type":"integer"},{"name":"y","description":"Y coordinate of the mouse pointer in DIP.","type":"integer"},{"name":"button","description":"Mouse button.","type":"string","enum":["none","left","middle","right"]},{"name":"timestamp","description":"Time at which the event occurred (default: current time).","optional":true,"$ref":"TimeSinceEpoch"},{"name":"deltaX","description":"X delta in DIP for mouse wheel event (default: 0).","optional":true,"type":"number"},{"name":"deltaY","description":"Y delta in DIP for mouse wheel event (default: 0).","optional":true,"type":"number"},{"name":"modifiers","description":"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).","optional":true,"type":"integer"},{"name":"clickCount","description":"Number of times the mouse button was clicked (default: 0).","optional":true,"type":"integer"}]},{"name":"setIgnoreInputEvents","description":"Ignores input events (useful while auditing page).","parameters":[{"name":"ignore","description":"Ignores input events processing when set to true.","type":"boolean"}]},{"name":"synthesizePinchGesture","description":"Synthesizes a pinch gesture over a time period by issuing appropriate touch events.","experimental":true,"parameters":[{"name":"x","description":"X coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"scaleFactor","description":"Relative scale factor after zooming (>1.0 zooms in, <1.0 zooms out).","type":"number"},{"name":"relativeSpeed","description":"Relative pointer speed in pixels per second (default: 800).","optional":true,"type":"integer"},{"name":"gestureSourceType","description":"Which type of input events to be generated (default: \'default\', which queries the platform\\nfor the preferred input type).","optional":true,"$ref":"GestureSourceType"}]},{"name":"synthesizeScrollGesture","description":"Synthesizes a scroll gesture over a time period by issuing appropriate touch events.","experimental":true,"parameters":[{"name":"x","description":"X coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"xDistance","description":"The distance to scroll along the X axis (positive to scroll left).","optional":true,"type":"number"},{"name":"yDistance","description":"The distance to scroll along the Y axis (positive to scroll up).","optional":true,"type":"number"},{"name":"xOverscroll","description":"The number of additional pixels to scroll back along the X axis, in addition to the given\\ndistance.","optional":true,"type":"number"},{"name":"yOverscroll","description":"The number of additional pixels to scroll back along the Y axis, in addition to the given\\ndistance.","optional":true,"type":"number"},{"name":"preventFling","description":"Prevent fling (default: true).","optional":true,"type":"boolean"},{"name":"speed","description":"Swipe speed in pixels per second (default: 800).","optional":true,"type":"integer"},{"name":"gestureSourceType","description":"Which type of input events to be generated (default: \'default\', which queries the platform\\nfor the preferred input type).","optional":true,"$ref":"GestureSourceType"},{"name":"repeatCount","description":"The number of times to repeat the gesture (default: 0).","optional":true,"type":"integer"},{"name":"repeatDelayMs","description":"The number of milliseconds delay between each repeat. (default: 250).","optional":true,"type":"integer"},{"name":"interactionMarkerName","description":"The name of the interaction markers to generate, if not empty (default: \\"\\").","optional":true,"type":"string"}]},{"name":"synthesizeTapGesture","description":"Synthesizes a tap gesture over a time period by issuing appropriate touch events.","experimental":true,"parameters":[{"name":"x","description":"X coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"duration","description":"Duration between touchdown and touchup events in ms (default: 50).","optional":true,"type":"integer"},{"name":"tapCount","description":"Number of times to perform the tap (e.g. 2 for double tap, default: 1).","optional":true,"type":"integer"},{"name":"gestureSourceType","description":"Which type of input events to be generated (default: \'default\', which queries the platform\\nfor the preferred input type).","optional":true,"$ref":"GestureSourceType"}]}]},{"domain":"Inspector","experimental":true,"commands":[{"name":"disable","description":"Disables inspector domain notifications."},{"name":"enable","description":"Enables inspector domain notifications."}],"events":[{"name":"detached","description":"Fired when remote debugging connection is about to be terminated. Contains detach reason.","parameters":[{"name":"reason","description":"The reason why connection has been terminated.","type":"string"}]},{"name":"targetCrashed","description":"Fired when debugging target has crashed"},{"name":"targetReloadedAfterCrash","description":"Fired when debugging target has reloaded after crash"}]},{"domain":"LayerTree","experimental":true,"dependencies":["DOM"],"types":[{"id":"LayerId","description":"Unique Layer identifier.","type":"string"},{"id":"SnapshotId","description":"Unique snapshot identifier.","type":"string"},{"id":"ScrollRect","description":"Rectangle where scrolling happens on the main thread.","type":"object","properties":[{"name":"rect","description":"Rectangle itself.","$ref":"DOM.Rect"},{"name":"type","description":"Reason for rectangle to force scrolling on the main thread","type":"string","enum":["RepaintsOnScroll","TouchEventHandler","WheelEventHandler"]}]},{"id":"StickyPositionConstraint","description":"Sticky position constraints.","type":"object","properties":[{"name":"stickyBoxRect","description":"Layout rectangle of the sticky element before being shifted","$ref":"DOM.Rect"},{"name":"containingBlockRect","description":"Layout rectangle of the containing block of the sticky element","$ref":"DOM.Rect"},{"name":"nearestLayerShiftingStickyBox","description":"The nearest sticky layer that shifts the sticky box","optional":true,"$ref":"LayerId"},{"name":"nearestLayerShiftingContainingBlock","description":"The nearest sticky layer that shifts the containing block","optional":true,"$ref":"LayerId"}]},{"id":"PictureTile","description":"Serialized fragment of layer picture along with its offset within the layer.","type":"object","properties":[{"name":"x","description":"Offset from owning layer left boundary","type":"number"},{"name":"y","description":"Offset from owning layer top boundary","type":"number"},{"name":"picture","description":"Base64-encoded snapshot data.","type":"string"}]},{"id":"Layer","description":"Information about a compositing layer.","type":"object","properties":[{"name":"layerId","description":"The unique id for this layer.","$ref":"LayerId"},{"name":"parentLayerId","description":"The id of parent (not present for root).","optional":true,"$ref":"LayerId"},{"name":"backendNodeId","description":"The backend id for the node associated with this layer.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"offsetX","description":"Offset from parent layer, X coordinate.","type":"number"},{"name":"offsetY","description":"Offset from parent layer, Y coordinate.","type":"number"},{"name":"width","description":"Layer width.","type":"number"},{"name":"height","description":"Layer height.","type":"number"},{"name":"transform","description":"Transformation matrix for layer, default is identity matrix","optional":true,"type":"array","items":{"type":"number"}},{"name":"anchorX","description":"Transform anchor point X, absent if no transform specified","optional":true,"type":"number"},{"name":"anchorY","description":"Transform anchor point Y, absent if no transform specified","optional":true,"type":"number"},{"name":"anchorZ","description":"Transform anchor point Z, absent if no transform specified","optional":true,"type":"number"},{"name":"paintCount","description":"Indicates how many time this layer has painted.","type":"integer"},{"name":"drawsContent","description":"Indicates whether this layer hosts any content, rather than being used for\\ntransform/scrolling purposes only.","type":"boolean"},{"name":"invisible","description":"Set if layer is not visible.","optional":true,"type":"boolean"},{"name":"scrollRects","description":"Rectangles scrolling on main thread only.","optional":true,"type":"array","items":{"$ref":"ScrollRect"}},{"name":"stickyPositionConstraint","description":"Sticky position constraint information","optional":true,"$ref":"StickyPositionConstraint"}]},{"id":"PaintProfile","description":"Array of timings, one per paint step.","type":"array","items":{"type":"number"}}],"commands":[{"name":"compositingReasons","description":"Provides the reasons why the given layer was composited.","parameters":[{"name":"layerId","description":"The id of the layer for which we want to get the reasons it was composited.","$ref":"LayerId"}],"returns":[{"name":"compositingReasons","description":"A list of strings specifying reasons for the given layer to become composited.","type":"array","items":{"type":"string"}}]},{"name":"disable","description":"Disables compositing tree inspection."},{"name":"enable","description":"Enables compositing tree inspection."},{"name":"loadSnapshot","description":"Returns the snapshot identifier.","parameters":[{"name":"tiles","description":"An array of tiles composing the snapshot.","type":"array","items":{"$ref":"PictureTile"}}],"returns":[{"name":"snapshotId","description":"The id of the snapshot.","$ref":"SnapshotId"}]},{"name":"makeSnapshot","description":"Returns the layer snapshot identifier.","parameters":[{"name":"layerId","description":"The id of the layer.","$ref":"LayerId"}],"returns":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"}]},{"name":"profileSnapshot","parameters":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"},{"name":"minRepeatCount","description":"The maximum number of times to replay the snapshot (1, if not specified).","optional":true,"type":"integer"},{"name":"minDuration","description":"The minimum duration (in seconds) to replay the snapshot.","optional":true,"type":"number"},{"name":"clipRect","description":"The clip rectangle to apply when replaying the snapshot.","optional":true,"$ref":"DOM.Rect"}],"returns":[{"name":"timings","description":"The array of paint profiles, one per run.","type":"array","items":{"$ref":"PaintProfile"}}]},{"name":"releaseSnapshot","description":"Releases layer snapshot captured by the back-end.","parameters":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"}]},{"name":"replaySnapshot","description":"Replays the layer snapshot and returns the resulting bitmap.","parameters":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"},{"name":"fromStep","description":"The first step to replay from (replay from the very start if not specified).","optional":true,"type":"integer"},{"name":"toStep","description":"The last step to replay to (replay till the end if not specified).","optional":true,"type":"integer"},{"name":"scale","description":"The scale to apply while replaying (defaults to 1).","optional":true,"type":"number"}],"returns":[{"name":"dataURL","description":"A data: URL for resulting image.","type":"string"}]},{"name":"snapshotCommandLog","description":"Replays the layer snapshot and returns canvas log.","parameters":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"}],"returns":[{"name":"commandLog","description":"The array of canvas function calls.","type":"array","items":{"type":"object"}}]}],"events":[{"name":"layerPainted","parameters":[{"name":"layerId","description":"The id of the painted layer.","$ref":"LayerId"},{"name":"clip","description":"Clip rectangle.","$ref":"DOM.Rect"}]},{"name":"layerTreeDidChange","parameters":[{"name":"layers","description":"Layer tree, absent if not in the comspositing mode.","optional":true,"type":"array","items":{"$ref":"Layer"}}]}]},{"domain":"Log","description":"Provides access to log entries.","dependencies":["Runtime","Network"],"types":[{"id":"LogEntry","description":"Log entry.","type":"object","properties":[{"name":"source","description":"Log entry source.","type":"string","enum":["xml","javascript","network","storage","appcache","rendering","security","deprecation","worker","violation","intervention","recommendation","other"]},{"name":"level","description":"Log entry severity.","type":"string","enum":["verbose","info","warning","error"]},{"name":"text","description":"Logged text.","type":"string"},{"name":"timestamp","description":"Timestamp when this entry was added.","$ref":"Runtime.Timestamp"},{"name":"url","description":"URL of the resource if known.","optional":true,"type":"string"},{"name":"lineNumber","description":"Line number in the resource.","optional":true,"type":"integer"},{"name":"stackTrace","description":"JavaScript stack trace.","optional":true,"$ref":"Runtime.StackTrace"},{"name":"networkRequestId","description":"Identifier of the network request associated with this entry.","optional":true,"$ref":"Network.RequestId"},{"name":"workerId","description":"Identifier of the worker associated with this entry.","optional":true,"type":"string"},{"name":"args","description":"Call arguments.","optional":true,"type":"array","items":{"$ref":"Runtime.RemoteObject"}}]},{"id":"ViolationSetting","description":"Violation configuration setting.","type":"object","properties":[{"name":"name","description":"Violation type.","type":"string","enum":["longTask","longLayout","blockedEvent","blockedParser","discouragedAPIUse","handler","recurringHandler"]},{"name":"threshold","description":"Time threshold to trigger upon.","type":"number"}]}],"commands":[{"name":"clear","description":"Clears the log."},{"name":"disable","description":"Disables log domain, prevents further log entries from being reported to the client."},{"name":"enable","description":"Enables log domain, sends the entries collected so far to the client by means of the\\n`entryAdded` notification."},{"name":"startViolationsReport","description":"start violation reporting.","parameters":[{"name":"config","description":"Configuration for violations.","type":"array","items":{"$ref":"ViolationSetting"}}]},{"name":"stopViolationsReport","description":"Stop violation reporting."}],"events":[{"name":"entryAdded","description":"Issued when new message was logged.","parameters":[{"name":"entry","description":"The entry.","$ref":"LogEntry"}]}]},{"domain":"Memory","experimental":true,"types":[{"id":"PressureLevel","description":"Memory pressure level.","type":"string","enum":["moderate","critical"]},{"id":"SamplingProfileNode","description":"Heap profile sample.","type":"object","properties":[{"name":"size","description":"Size of the sampled allocation.","type":"number"},{"name":"total","description":"Total bytes attributed to this sample.","type":"number"},{"name":"stack","description":"Execution stack at the point of allocation.","type":"array","items":{"type":"string"}}]},{"id":"SamplingProfile","description":"Array of heap profile samples.","type":"object","properties":[{"name":"samples","type":"array","items":{"$ref":"SamplingProfileNode"}},{"name":"modules","type":"array","items":{"$ref":"Module"}}]},{"id":"Module","description":"Executable module information","type":"object","properties":[{"name":"name","description":"Name of the module.","type":"string"},{"name":"uuid","description":"UUID of the module.","type":"string"},{"name":"baseAddress","description":"Base address where the module is loaded into memory. Encoded as a decimal\\nor hexadecimal (0x prefixed) string.","type":"string"},{"name":"size","description":"Size of the module in bytes.","type":"number"}]}],"commands":[{"name":"getDOMCounters","returns":[{"name":"documents","type":"integer"},{"name":"nodes","type":"integer"},{"name":"jsEventListeners","type":"integer"}]},{"name":"prepareForLeakDetection"},{"name":"forciblyPurgeJavaScriptMemory","description":"Simulate OomIntervention by purging V8 memory."},{"name":"setPressureNotificationsSuppressed","description":"Enable/disable suppressing memory pressure notifications in all processes.","parameters":[{"name":"suppressed","description":"If true, memory pressure notifications will be suppressed.","type":"boolean"}]},{"name":"simulatePressureNotification","description":"Simulate a memory pressure notification in all processes.","parameters":[{"name":"level","description":"Memory pressure level of the notification.","$ref":"PressureLevel"}]},{"name":"startSampling","description":"Start collecting native memory profile.","parameters":[{"name":"samplingInterval","description":"Average number of bytes between samples.","optional":true,"type":"integer"},{"name":"suppressRandomness","description":"Do not randomize intervals between samples.","optional":true,"type":"boolean"}]},{"name":"stopSampling","description":"Stop collecting native memory profile."},{"name":"getAllTimeSamplingProfile","description":"Retrieve native memory allocations profile\\ncollected since renderer process startup.","returns":[{"name":"profile","$ref":"SamplingProfile"}]},{"name":"getBrowserSamplingProfile","description":"Retrieve native memory allocations profile\\ncollected since browser process startup.","returns":[{"name":"profile","$ref":"SamplingProfile"}]},{"name":"getSamplingProfile","description":"Retrieve native memory allocations profile collected since last\\n`startSampling` call.","returns":[{"name":"profile","$ref":"SamplingProfile"}]}]},{"domain":"Network","description":"Network domain allows tracking network activities of the page. It exposes information about http,\\nfile, data and other requests and responses, their headers, bodies, timing, etc.","dependencies":["Debugger","Runtime","Security"],"types":[{"id":"ResourceType","description":"Resource type as it was perceived by the rendering engine.","type":"string","enum":["Document","Stylesheet","Image","Media","Font","Script","TextTrack","XHR","Fetch","EventSource","WebSocket","Manifest","SignedExchange","Ping","CSPViolationReport","Other"]},{"id":"LoaderId","description":"Unique loader identifier.","type":"string"},{"id":"RequestId","description":"Unique request identifier.","type":"string"},{"id":"InterceptionId","description":"Unique intercepted request identifier.","type":"string"},{"id":"ErrorReason","description":"Network level fetch failure reason.","type":"string","enum":["Failed","Aborted","TimedOut","AccessDenied","ConnectionClosed","ConnectionReset","ConnectionRefused","ConnectionAborted","ConnectionFailed","NameNotResolved","InternetDisconnected","AddressUnreachable","BlockedByClient","BlockedByResponse"]},{"id":"TimeSinceEpoch","description":"UTC time in seconds, counted from January 1, 1970.","type":"number"},{"id":"MonotonicTime","description":"Monotonically increasing time in seconds since an arbitrary point in the past.","type":"number"},{"id":"Headers","description":"Request / response headers as keys / values of JSON object.","type":"object"},{"id":"ConnectionType","description":"The underlying connection technology that the browser is supposedly using.","type":"string","enum":["none","cellular2g","cellular3g","cellular4g","bluetooth","ethernet","wifi","wimax","other"]},{"id":"CookieSameSite","description":"Represents the cookie\'s \'SameSite\' status:\\nhttps://tools.ietf.org/html/draft-west-first-party-cookies","type":"string","enum":["Strict","Lax","Extended","None"]},{"id":"ResourceTiming","description":"Timing information for the request.","type":"object","properties":[{"name":"requestTime","description":"Timing\'s requestTime is a baseline in seconds, while the other numbers are ticks in\\nmilliseconds relatively to this requestTime.","type":"number"},{"name":"proxyStart","description":"Started resolving proxy.","type":"number"},{"name":"proxyEnd","description":"Finished resolving proxy.","type":"number"},{"name":"dnsStart","description":"Started DNS address resolve.","type":"number"},{"name":"dnsEnd","description":"Finished DNS address resolve.","type":"number"},{"name":"connectStart","description":"Started connecting to the remote host.","type":"number"},{"name":"connectEnd","description":"Connected to the remote host.","type":"number"},{"name":"sslStart","description":"Started SSL handshake.","type":"number"},{"name":"sslEnd","description":"Finished SSL handshake.","type":"number"},{"name":"workerStart","description":"Started running ServiceWorker.","experimental":true,"type":"number"},{"name":"workerReady","description":"Finished Starting ServiceWorker.","experimental":true,"type":"number"},{"name":"sendStart","description":"Started sending request.","type":"number"},{"name":"sendEnd","description":"Finished sending request.","type":"number"},{"name":"pushStart","description":"Time the server started pushing request.","experimental":true,"type":"number"},{"name":"pushEnd","description":"Time the server finished pushing request.","experimental":true,"type":"number"},{"name":"receiveHeadersEnd","description":"Finished receiving response headers.","type":"number"}]},{"id":"ResourcePriority","description":"Loading priority of a resource request.","type":"string","enum":["VeryLow","Low","Medium","High","VeryHigh"]},{"id":"Request","description":"HTTP request data.","type":"object","properties":[{"name":"url","description":"Request URL (without fragment).","type":"string"},{"name":"urlFragment","description":"Fragment of the requested URL starting with hash, if present.","optional":true,"type":"string"},{"name":"method","description":"HTTP request method.","type":"string"},{"name":"headers","description":"HTTP request headers.","$ref":"Headers"},{"name":"postData","description":"HTTP POST request data.","optional":true,"type":"string"},{"name":"hasPostData","description":"True when the request has POST data. Note that postData might still be omitted when this flag is true when the data is too long.","optional":true,"type":"boolean"},{"name":"mixedContentType","description":"The mixed content type of the request.","optional":true,"$ref":"Security.MixedContentType"},{"name":"initialPriority","description":"Priority of the resource request at the time request is sent.","$ref":"ResourcePriority"},{"name":"referrerPolicy","description":"The referrer policy of the request, as defined in https://www.w3.org/TR/referrer-policy/","type":"string","enum":["unsafe-url","no-referrer-when-downgrade","no-referrer","origin","origin-when-cross-origin","same-origin","strict-origin","strict-origin-when-cross-origin"]},{"name":"isLinkPreload","description":"Whether is loaded via link preload.","optional":true,"type":"boolean"}]},{"id":"SignedCertificateTimestamp","description":"Details of a signed certificate timestamp (SCT).","type":"object","properties":[{"name":"status","description":"Validation status.","type":"string"},{"name":"origin","description":"Origin.","type":"string"},{"name":"logDescription","description":"Log name / description.","type":"string"},{"name":"logId","description":"Log ID.","type":"string"},{"name":"timestamp","description":"Issuance date.","$ref":"TimeSinceEpoch"},{"name":"hashAlgorithm","description":"Hash algorithm.","type":"string"},{"name":"signatureAlgorithm","description":"Signature algorithm.","type":"string"},{"name":"signatureData","description":"Signature data.","type":"string"}]},{"id":"SecurityDetails","description":"Security details about a request.","type":"object","properties":[{"name":"protocol","description":"Protocol name (e.g. \\"TLS 1.2\\" or \\"QUIC\\").","type":"string"},{"name":"keyExchange","description":"Key Exchange used by the connection, or the empty string if not applicable.","type":"string"},{"name":"keyExchangeGroup","description":"(EC)DH group used by the connection, if applicable.","optional":true,"type":"string"},{"name":"cipher","description":"Cipher name.","type":"string"},{"name":"mac","description":"TLS MAC. Note that AEAD ciphers do not have separate MACs.","optional":true,"type":"string"},{"name":"certificateId","description":"Certificate ID value.","$ref":"Security.CertificateId"},{"name":"subjectName","description":"Certificate subject name.","type":"string"},{"name":"sanList","description":"Subject Alternative Name (SAN) DNS names and IP addresses.","type":"array","items":{"type":"string"}},{"name":"issuer","description":"Name of the issuing CA.","type":"string"},{"name":"validFrom","description":"Certificate valid from date.","$ref":"TimeSinceEpoch"},{"name":"validTo","description":"Certificate valid to (expiration) date","$ref":"TimeSinceEpoch"},{"name":"signedCertificateTimestampList","description":"List of signed certificate timestamps (SCTs).","type":"array","items":{"$ref":"SignedCertificateTimestamp"}},{"name":"certificateTransparencyCompliance","description":"Whether the request complied with Certificate Transparency policy","$ref":"CertificateTransparencyCompliance"}]},{"id":"CertificateTransparencyCompliance","description":"Whether the request complied with Certificate Transparency policy.","type":"string","enum":["unknown","not-compliant","compliant"]},{"id":"BlockedReason","description":"The reason why request was blocked.","type":"string","enum":["other","csp","mixed-content","origin","inspector","subresource-filter","content-type","collapsed-by-client"]},{"id":"Response","description":"HTTP response data.","type":"object","properties":[{"name":"url","description":"Response URL. This URL can be different from CachedResource.url in case of redirect.","type":"string"},{"name":"status","description":"HTTP response status code.","type":"integer"},{"name":"statusText","description":"HTTP response status text.","type":"string"},{"name":"headers","description":"HTTP response headers.","$ref":"Headers"},{"name":"headersText","description":"HTTP response headers text.","optional":true,"type":"string"},{"name":"mimeType","description":"Resource mimeType as determined by the browser.","type":"string"},{"name":"requestHeaders","description":"Refined HTTP request headers that were actually transmitted over the network.","optional":true,"$ref":"Headers"},{"name":"requestHeadersText","description":"HTTP request headers text.","optional":true,"type":"string"},{"name":"connectionReused","description":"Specifies whether physical connection was actually reused for this request.","type":"boolean"},{"name":"connectionId","description":"Physical connection id that was actually used for this request.","type":"number"},{"name":"remoteIPAddress","description":"Remote IP address.","optional":true,"type":"string"},{"name":"remotePort","description":"Remote port.","optional":true,"type":"integer"},{"name":"fromDiskCache","description":"Specifies that the request was served from the disk cache.","optional":true,"type":"boolean"},{"name":"fromServiceWorker","description":"Specifies that the request was served from the ServiceWorker.","optional":true,"type":"boolean"},{"name":"fromPrefetchCache","description":"Specifies that the request was served from the prefetch cache.","optional":true,"type":"boolean"},{"name":"encodedDataLength","description":"Total number of bytes received for this request so far.","type":"number"},{"name":"timing","description":"Timing information for the given request.","optional":true,"$ref":"ResourceTiming"},{"name":"protocol","description":"Protocol used to fetch this request.","optional":true,"type":"string"},{"name":"securityState","description":"Security state of the request resource.","$ref":"Security.SecurityState"},{"name":"securityDetails","description":"Security details for the request.","optional":true,"$ref":"SecurityDetails"}]},{"id":"WebSocketRequest","description":"WebSocket request data.","type":"object","properties":[{"name":"headers","description":"HTTP request headers.","$ref":"Headers"}]},{"id":"WebSocketResponse","description":"WebSocket response data.","type":"object","properties":[{"name":"status","description":"HTTP response status code.","type":"integer"},{"name":"statusText","description":"HTTP response status text.","type":"string"},{"name":"headers","description":"HTTP response headers.","$ref":"Headers"},{"name":"headersText","description":"HTTP response headers text.","optional":true,"type":"string"},{"name":"requestHeaders","description":"HTTP request headers.","optional":true,"$ref":"Headers"},{"name":"requestHeadersText","description":"HTTP request headers text.","optional":true,"type":"string"}]},{"id":"WebSocketFrame","description":"WebSocket message data. This represents an entire WebSocket message, not just a fragmented frame as the name suggests.","type":"object","properties":[{"name":"opcode","description":"WebSocket message opcode.","type":"number"},{"name":"mask","description":"WebSocket message mask.","type":"boolean"},{"name":"payloadData","description":"WebSocket message payload data.\\nIf the opcode is 1, this is a text message and payloadData is a UTF-8 string.\\nIf the opcode isn\'t 1, then payloadData is a base64 encoded string representing binary data.","type":"string"}]},{"id":"CachedResource","description":"Information about the cached resource.","type":"object","properties":[{"name":"url","description":"Resource URL. This is the url of the original network request.","type":"string"},{"name":"type","description":"Type of this resource.","$ref":"ResourceType"},{"name":"response","description":"Cached response data.","optional":true,"$ref":"Response"},{"name":"bodySize","description":"Cached response body size.","type":"number"}]},{"id":"Initiator","description":"Information about the request initiator.","type":"object","properties":[{"name":"type","description":"Type of this initiator.","type":"string","enum":["parser","script","preload","SignedExchange","other"]},{"name":"stack","description":"Initiator JavaScript stack trace, set for Script only.","optional":true,"$ref":"Runtime.StackTrace"},{"name":"url","description":"Initiator URL, set for Parser type or for Script type (when script is importing module) or for SignedExchange type.","optional":true,"type":"string"},{"name":"lineNumber","description":"Initiator line number, set for Parser type or for Script type (when script is importing\\nmodule) (0-based).","optional":true,"type":"number"}]},{"id":"Cookie","description":"Cookie object","type":"object","properties":[{"name":"name","description":"Cookie name.","type":"string"},{"name":"value","description":"Cookie value.","type":"string"},{"name":"domain","description":"Cookie domain.","type":"string"},{"name":"path","description":"Cookie path.","type":"string"},{"name":"expires","description":"Cookie expiration date as the number of seconds since the UNIX epoch.","type":"number"},{"name":"size","description":"Cookie size.","type":"integer"},{"name":"httpOnly","description":"True if cookie is http-only.","type":"boolean"},{"name":"secure","description":"True if cookie is secure.","type":"boolean"},{"name":"session","description":"True in case of session cookie.","type":"boolean"},{"name":"sameSite","description":"Cookie SameSite type.","optional":true,"$ref":"CookieSameSite"}]},{"id":"CookieParam","description":"Cookie parameter object","type":"object","properties":[{"name":"name","description":"Cookie name.","type":"string"},{"name":"value","description":"Cookie value.","type":"string"},{"name":"url","description":"The request-URI to associate with the setting of the cookie. This value can affect the\\ndefault domain and path values of the created cookie.","optional":true,"type":"string"},{"name":"domain","description":"Cookie domain.","optional":true,"type":"string"},{"name":"path","description":"Cookie path.","optional":true,"type":"string"},{"name":"secure","description":"True if cookie is secure.","optional":true,"type":"boolean"},{"name":"httpOnly","description":"True if cookie is http-only.","optional":true,"type":"boolean"},{"name":"sameSite","description":"Cookie SameSite type.","optional":true,"$ref":"CookieSameSite"},{"name":"expires","description":"Cookie expiration date, session cookie if not set","optional":true,"$ref":"TimeSinceEpoch"}]},{"id":"AuthChallenge","description":"Authorization challenge for HTTP status code 401 or 407.","experimental":true,"type":"object","properties":[{"name":"source","description":"Source of the authentication challenge.","optional":true,"type":"string","enum":["Server","Proxy"]},{"name":"origin","description":"Origin of the challenger.","type":"string"},{"name":"scheme","description":"The authentication scheme used, such as basic or digest","type":"string"},{"name":"realm","description":"The realm of the challenge. May be empty.","type":"string"}]},{"id":"AuthChallengeResponse","description":"Response to an AuthChallenge.","experimental":true,"type":"object","properties":[{"name":"response","description":"The decision on what to do in response to the authorization challenge. Default means\\ndeferring to the default behavior of the net stack, which will likely either the Cancel\\nauthentication or display a popup dialog box.","type":"string","enum":["Default","CancelAuth","ProvideCredentials"]},{"name":"username","description":"The username to provide, possibly empty. Should only be set if response is\\nProvideCredentials.","optional":true,"type":"string"},{"name":"password","description":"The password to provide, possibly empty. Should only be set if response is\\nProvideCredentials.","optional":true,"type":"string"}]},{"id":"InterceptionStage","description":"Stages of the interception to begin intercepting. Request will intercept before the request is\\nsent. Response will intercept after the response is received.","experimental":true,"type":"string","enum":["Request","HeadersReceived"]},{"id":"RequestPattern","description":"Request pattern for interception.","experimental":true,"type":"object","properties":[{"name":"urlPattern","description":"Wildcards (\'*\' -> zero or more, \'?\' -> exactly one) are allowed. Escape character is\\nbackslash. Omitting is equivalent to \\"*\\".","optional":true,"type":"string"},{"name":"resourceType","description":"If set, only requests for matching resource types will be intercepted.","optional":true,"$ref":"ResourceType"},{"name":"interceptionStage","description":"Stage at wich to begin intercepting requests. Default is Request.","optional":true,"$ref":"InterceptionStage"}]},{"id":"SignedExchangeSignature","description":"Information about a signed exchange signature.\\nhttps://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#rfc.section.3.1","experimental":true,"type":"object","properties":[{"name":"label","description":"Signed exchange signature label.","type":"string"},{"name":"signature","description":"The hex string of signed exchange signature.","type":"string"},{"name":"integrity","description":"Signed exchange signature integrity.","type":"string"},{"name":"certUrl","description":"Signed exchange signature cert Url.","optional":true,"type":"string"},{"name":"certSha256","description":"The hex string of signed exchange signature cert sha256.","optional":true,"type":"string"},{"name":"validityUrl","description":"Signed exchange signature validity Url.","type":"string"},{"name":"date","description":"Signed exchange signature date.","type":"integer"},{"name":"expires","description":"Signed exchange signature expires.","type":"integer"},{"name":"certificates","description":"The encoded certificates.","optional":true,"type":"array","items":{"type":"string"}}]},{"id":"SignedExchangeHeader","description":"Information about a signed exchange header.\\nhttps://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#cbor-representation","experimental":true,"type":"object","properties":[{"name":"requestUrl","description":"Signed exchange request URL.","type":"string"},{"name":"responseCode","description":"Signed exchange response code.","type":"integer"},{"name":"responseHeaders","description":"Signed exchange response headers.","$ref":"Headers"},{"name":"signatures","description":"Signed exchange response signature.","type":"array","items":{"$ref":"SignedExchangeSignature"}},{"name":"headerIntegrity","description":"Signed exchange header integrity hash in the form of \\"sha256-<base64-hash-value>\\".","type":"string"}]},{"id":"SignedExchangeErrorField","description":"Field type for a signed exchange related error.","experimental":true,"type":"string","enum":["signatureSig","signatureIntegrity","signatureCertUrl","signatureCertSha256","signatureValidityUrl","signatureTimestamps"]},{"id":"SignedExchangeError","description":"Information about a signed exchange response.","experimental":true,"type":"object","properties":[{"name":"message","description":"Error message.","type":"string"},{"name":"signatureIndex","description":"The index of the signature which caused the error.","optional":true,"type":"integer"},{"name":"errorField","description":"The field which caused the error.","optional":true,"$ref":"SignedExchangeErrorField"}]},{"id":"SignedExchangeInfo","description":"Information about a signed exchange response.","experimental":true,"type":"object","properties":[{"name":"outerResponse","description":"The outer response of signed HTTP exchange which was received from network.","$ref":"Response"},{"name":"header","description":"Information about the signed exchange header.","optional":true,"$ref":"SignedExchangeHeader"},{"name":"securityDetails","description":"Security details for the signed exchange header.","optional":true,"$ref":"SecurityDetails"},{"name":"errors","description":"Errors occurred while handling the signed exchagne.","optional":true,"type":"array","items":{"$ref":"SignedExchangeError"}}]}],"commands":[{"name":"canClearBrowserCache","description":"Tells whether clearing browser cache is supported.","deprecated":true,"returns":[{"name":"result","description":"True if browser cache can be cleared.","type":"boolean"}]},{"name":"canClearBrowserCookies","description":"Tells whether clearing browser cookies is supported.","deprecated":true,"returns":[{"name":"result","description":"True if browser cookies can be cleared.","type":"boolean"}]},{"name":"canEmulateNetworkConditions","description":"Tells whether emulation of network conditions is supported.","deprecated":true,"returns":[{"name":"result","description":"True if emulation of network conditions is supported.","type":"boolean"}]},{"name":"clearBrowserCache","description":"Clears browser cache."},{"name":"clearBrowserCookies","description":"Clears browser cookies."},{"name":"continueInterceptedRequest","description":"Response to Network.requestIntercepted which either modifies the request to continue with any\\nmodifications, or blocks it, or completes it with the provided response bytes. If a network\\nfetch occurs as a result which encounters a redirect an additional Network.requestIntercepted\\nevent will be sent with the same InterceptionId.\\nDeprecated, use Fetch.continueRequest, Fetch.fulfillRequest and Fetch.failRequest instead.","experimental":true,"deprecated":true,"parameters":[{"name":"interceptionId","$ref":"InterceptionId"},{"name":"errorReason","description":"If set this causes the request to fail with the given reason. Passing `Aborted` for requests\\nmarked with `isNavigationRequest` also cancels the navigation. Must not be set in response\\nto an authChallenge.","optional":true,"$ref":"ErrorReason"},{"name":"rawResponse","description":"If set the requests completes using with the provided base64 encoded raw response, including\\nHTTP status line and headers etc... Must not be set in response to an authChallenge.","optional":true,"type":"string"},{"name":"url","description":"If set the request url will be modified in a way that\'s not observable by page. Must not be\\nset in response to an authChallenge.","optional":true,"type":"string"},{"name":"method","description":"If set this allows the request method to be overridden. Must not be set in response to an\\nauthChallenge.","optional":true,"type":"string"},{"name":"postData","description":"If set this allows postData to be set. Must not be set in response to an authChallenge.","optional":true,"type":"string"},{"name":"headers","description":"If set this allows the request headers to be changed. Must not be set in response to an\\nauthChallenge.","optional":true,"$ref":"Headers"},{"name":"authChallengeResponse","description":"Response to a requestIntercepted with an authChallenge. Must not be set otherwise.","optional":true,"$ref":"AuthChallengeResponse"}]},{"name":"deleteCookies","description":"Deletes browser cookies with matching name and url or domain/path pair.","parameters":[{"name":"name","description":"Name of the cookies to remove.","type":"string"},{"name":"url","description":"If specified, deletes all the cookies with the given name where domain and path match\\nprovided URL.","optional":true,"type":"string"},{"name":"domain","description":"If specified, deletes only cookies with the exact domain.","optional":true,"type":"string"},{"name":"path","description":"If specified, deletes only cookies with the exact path.","optional":true,"type":"string"}]},{"name":"disable","description":"Disables network tracking, prevents network events from being sent to the client."},{"name":"emulateNetworkConditions","description":"Activates emulation of network conditions.","parameters":[{"name":"offline","description":"True to emulate internet disconnection.","type":"boolean"},{"name":"latency","description":"Minimum latency from request sent to response headers received (ms).","type":"number"},{"name":"downloadThroughput","description":"Maximal aggregated download throughput (bytes/sec). -1 disables download throttling.","type":"number"},{"name":"uploadThroughput","description":"Maximal aggregated upload throughput (bytes/sec). -1 disables upload throttling.","type":"number"},{"name":"connectionType","description":"Connection type if known.","optional":true,"$ref":"ConnectionType"}]},{"name":"enable","description":"Enables network tracking, network events will now be delivered to the client.","parameters":[{"name":"maxTotalBufferSize","description":"Buffer size in bytes to use when preserving network payloads (XHRs, etc).","experimental":true,"optional":true,"type":"integer"},{"name":"maxResourceBufferSize","description":"Per-resource buffer size in bytes to use when preserving network payloads (XHRs, etc).","experimental":true,"optional":true,"type":"integer"},{"name":"maxPostDataSize","description":"Longest post body size (in bytes) that would be included in requestWillBeSent notification","optional":true,"type":"integer"}]},{"name":"getAllCookies","description":"Returns all browser cookies. Depending on the backend support, will return detailed cookie\\ninformation in the `cookies` field.","returns":[{"name":"cookies","description":"Array of cookie objects.","type":"array","items":{"$ref":"Cookie"}}]},{"name":"getCertificate","description":"Returns the DER-encoded certificate.","experimental":true,"parameters":[{"name":"origin","description":"Origin to get certificate for.","type":"string"}],"returns":[{"name":"tableNames","type":"array","items":{"type":"string"}}]},{"name":"getCookies","description":"Returns all browser cookies for the current URL. Depending on the backend support, will return\\ndetailed cookie information in the `cookies` field.","parameters":[{"name":"urls","description":"The list of URLs for which applicable cookies will be fetched","optional":true,"type":"array","items":{"type":"string"}}],"returns":[{"name":"cookies","description":"Array of cookie objects.","type":"array","items":{"$ref":"Cookie"}}]},{"name":"getResponseBody","description":"Returns content served for the given request.","parameters":[{"name":"requestId","description":"Identifier of the network request to get content for.","$ref":"RequestId"}],"returns":[{"name":"body","description":"Response body.","type":"string"},{"name":"base64Encoded","description":"True, if content was sent as base64.","type":"boolean"}]},{"name":"getRequestPostData","description":"Returns post data sent with the request. Returns an error when no data was sent with the request.","parameters":[{"name":"requestId","description":"Identifier of the network request to get content for.","$ref":"RequestId"}],"returns":[{"name":"postData","description":"Request body string, omitting files from multipart requests","type":"string"}]},{"name":"getResponseBodyForInterception","description":"Returns content served for the given currently intercepted request.","experimental":true,"parameters":[{"name":"interceptionId","description":"Identifier for the intercepted request to get body for.","$ref":"InterceptionId"}],"returns":[{"name":"body","description":"Response body.","type":"string"},{"name":"base64Encoded","description":"True, if content was sent as base64.","type":"boolean"}]},{"name":"takeResponseBodyForInterceptionAsStream","description":"Returns a handle to the stream representing the response body. Note that after this command,\\nthe intercepted request can\'t be continued as is -- you either need to cancel it or to provide\\nthe response body. The stream only supports sequential read, IO.read will fail if the position\\nis specified.","experimental":true,"parameters":[{"name":"interceptionId","$ref":"InterceptionId"}],"returns":[{"name":"stream","$ref":"IO.StreamHandle"}]},{"name":"replayXHR","description":"This method sends a new XMLHttpRequest which is identical to the original one. The following\\nparameters should be identical: method, url, async, request body, extra headers, withCredentials\\nattribute, user, password.","experimental":true,"parameters":[{"name":"requestId","description":"Identifier of XHR to replay.","$ref":"RequestId"}]},{"name":"searchInResponseBody","description":"Searches for given string in response content.","experimental":true,"parameters":[{"name":"requestId","description":"Identifier of the network response to search.","$ref":"RequestId"},{"name":"query","description":"String to search for.","type":"string"},{"name":"caseSensitive","description":"If true, search is case sensitive.","optional":true,"type":"boolean"},{"name":"isRegex","description":"If true, treats string parameter as regex.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"List of search matches.","type":"array","items":{"$ref":"Debugger.SearchMatch"}}]},{"name":"setBlockedURLs","description":"Blocks URLs from loading.","experimental":true,"parameters":[{"name":"urls","description":"URL patterns to block. Wildcards (\'*\') are allowed.","type":"array","items":{"type":"string"}}]},{"name":"setBypassServiceWorker","description":"Toggles ignoring of service worker for each request.","experimental":true,"parameters":[{"name":"bypass","description":"Bypass service worker and load from network.","type":"boolean"}]},{"name":"setCacheDisabled","description":"Toggles ignoring cache for each request. If `true`, cache will not be used.","parameters":[{"name":"cacheDisabled","description":"Cache disabled state.","type":"boolean"}]},{"name":"setCookie","description":"Sets a cookie with the given cookie data; may overwrite equivalent cookies if they exist.","parameters":[{"name":"name","description":"Cookie name.","type":"string"},{"name":"value","description":"Cookie value.","type":"string"},{"name":"url","description":"The request-URI to associate with the setting of the cookie. This value can affect the\\ndefault domain and path values of the created cookie.","optional":true,"type":"string"},{"name":"domain","description":"Cookie domain.","optional":true,"type":"string"},{"name":"path","description":"Cookie path.","optional":true,"type":"string"},{"name":"secure","description":"True if cookie is secure.","optional":true,"type":"boolean"},{"name":"httpOnly","description":"True if cookie is http-only.","optional":true,"type":"boolean"},{"name":"sameSite","description":"Cookie SameSite type.","optional":true,"$ref":"CookieSameSite"},{"name":"expires","description":"Cookie expiration date, session cookie if not set","optional":true,"$ref":"TimeSinceEpoch"}],"returns":[{"name":"success","description":"True if successfully set cookie.","type":"boolean"}]},{"name":"setCookies","description":"Sets given cookies.","parameters":[{"name":"cookies","description":"Cookies to be set.","type":"array","items":{"$ref":"CookieParam"}}]},{"name":"setDataSizeLimitsForTest","description":"For testing.","experimental":true,"parameters":[{"name":"maxTotalSize","description":"Maximum total buffer size.","type":"integer"},{"name":"maxResourceSize","description":"Maximum per-resource size.","type":"integer"}]},{"name":"setExtraHTTPHeaders","description":"Specifies whether to always send extra HTTP headers with the requests from this page.","parameters":[{"name":"headers","description":"Map with extra HTTP headers.","$ref":"Headers"}]},{"name":"setRequestInterception","description":"Sets the requests to intercept that match the provided patterns and optionally resource types.\\nDeprecated, please use Fetch.enable instead.","experimental":true,"deprecated":true,"parameters":[{"name":"patterns","description":"Requests matching any of these patterns will be forwarded and wait for the corresponding\\ncontinueInterceptedRequest call.","type":"array","items":{"$ref":"RequestPattern"}}]},{"name":"setUserAgentOverride","description":"Allows overriding user agent with the given string.","redirect":"Emulation","parameters":[{"name":"userAgent","description":"User agent to use.","type":"string"},{"name":"acceptLanguage","description":"Browser langugage to emulate.","optional":true,"type":"string"},{"name":"platform","description":"The platform navigator.platform should return.","optional":true,"type":"string"}]}],"events":[{"name":"dataReceived","description":"Fired when data chunk was received over the network.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"dataLength","description":"Data chunk length.","type":"integer"},{"name":"encodedDataLength","description":"Actual bytes received (might be less than dataLength for compressed encodings).","type":"integer"}]},{"name":"eventSourceMessageReceived","description":"Fired when EventSource message is received.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"eventName","description":"Message type.","type":"string"},{"name":"eventId","description":"Message identifier.","type":"string"},{"name":"data","description":"Message content.","type":"string"}]},{"name":"loadingFailed","description":"Fired when HTTP request has failed to load.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"type","description":"Resource type.","$ref":"ResourceType"},{"name":"errorText","description":"User friendly error message.","type":"string"},{"name":"canceled","description":"True if loading was canceled.","optional":true,"type":"boolean"},{"name":"blockedReason","description":"The reason why loading was blocked, if any.","optional":true,"$ref":"BlockedReason"}]},{"name":"loadingFinished","description":"Fired when HTTP request has finished loading.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"encodedDataLength","description":"Total number of bytes received for this request.","type":"number"},{"name":"shouldReportCorbBlocking","description":"Set when 1) response was blocked by Cross-Origin Read Blocking and also\\n2) this needs to be reported to the DevTools console.","optional":true,"type":"boolean"}]},{"name":"requestIntercepted","description":"Details of an intercepted HTTP request, which must be either allowed, blocked, modified or\\nmocked.\\nDeprecated, use Fetch.requestPaused instead.","experimental":true,"deprecated":true,"parameters":[{"name":"interceptionId","description":"Each request the page makes will have a unique id, however if any redirects are encountered\\nwhile processing that fetch, they will be reported with the same id as the original fetch.\\nLikewise if HTTP authentication is needed then the same fetch id will be used.","$ref":"InterceptionId"},{"name":"request","$ref":"Request"},{"name":"frameId","description":"The id of the frame that initiated the request.","$ref":"Page.FrameId"},{"name":"resourceType","description":"How the requested resource will be used.","$ref":"ResourceType"},{"name":"isNavigationRequest","description":"Whether this is a navigation request, which can abort the navigation completely.","type":"boolean"},{"name":"isDownload","description":"Set if the request is a navigation that will result in a download.\\nOnly present after response is received from the server (i.e. HeadersReceived stage).","optional":true,"type":"boolean"},{"name":"redirectUrl","description":"Redirect location, only sent if a redirect was intercepted.","optional":true,"type":"string"},{"name":"authChallenge","description":"Details of the Authorization Challenge encountered. If this is set then\\ncontinueInterceptedRequest must contain an authChallengeResponse.","optional":true,"$ref":"AuthChallenge"},{"name":"responseErrorReason","description":"Response error if intercepted at response stage or if redirect occurred while intercepting\\nrequest.","optional":true,"$ref":"ErrorReason"},{"name":"responseStatusCode","description":"Response code if intercepted at response stage or if redirect occurred while intercepting\\nrequest or auth retry occurred.","optional":true,"type":"integer"},{"name":"responseHeaders","description":"Response headers if intercepted at the response stage or if redirect occurred while\\nintercepting request or auth retry occurred.","optional":true,"$ref":"Headers"},{"name":"requestId","description":"If the intercepted request had a corresponding requestWillBeSent event fired for it, then\\nthis requestId will be the same as the requestId present in the requestWillBeSent event.","optional":true,"$ref":"RequestId"}]},{"name":"requestServedFromCache","description":"Fired if request ended up loading from cache.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"}]},{"name":"requestWillBeSent","description":"Fired when page is about to send HTTP request.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"loaderId","description":"Loader identifier. Empty string if the request is fetched from worker.","$ref":"LoaderId"},{"name":"documentURL","description":"URL of the document this request is loaded for.","type":"string"},{"name":"request","description":"Request data.","$ref":"Request"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"wallTime","description":"Timestamp.","$ref":"TimeSinceEpoch"},{"name":"initiator","description":"Request initiator.","$ref":"Initiator"},{"name":"redirectResponse","description":"Redirect response data.","optional":true,"$ref":"Response"},{"name":"type","description":"Type of this resource.","optional":true,"$ref":"ResourceType"},{"name":"frameId","description":"Frame identifier.","optional":true,"$ref":"Page.FrameId"},{"name":"hasUserGesture","description":"Whether the request is initiated by a user gesture. Defaults to false.","optional":true,"type":"boolean"}]},{"name":"resourceChangedPriority","description":"Fired when resource loading priority is changed","experimental":true,"parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"newPriority","description":"New priority","$ref":"ResourcePriority"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"}]},{"name":"signedExchangeReceived","description":"Fired when a signed exchange was received over the network","experimental":true,"parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"info","description":"Information about the signed exchange response.","$ref":"SignedExchangeInfo"}]},{"name":"responseReceived","description":"Fired when HTTP response is available.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"loaderId","description":"Loader identifier. Empty string if the request is fetched from worker.","$ref":"LoaderId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"type","description":"Resource type.","$ref":"ResourceType"},{"name":"response","description":"Response data.","$ref":"Response"},{"name":"frameId","description":"Frame identifier.","optional":true,"$ref":"Page.FrameId"}]},{"name":"webSocketClosed","description":"Fired when WebSocket is closed.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"}]},{"name":"webSocketCreated","description":"Fired upon WebSocket creation.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"url","description":"WebSocket request URL.","type":"string"},{"name":"initiator","description":"Request initiator.","optional":true,"$ref":"Initiator"}]},{"name":"webSocketFrameError","description":"Fired when WebSocket message error occurs.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"errorMessage","description":"WebSocket error message.","type":"string"}]},{"name":"webSocketFrameReceived","description":"Fired when WebSocket message is received.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"response","description":"WebSocket response data.","$ref":"WebSocketFrame"}]},{"name":"webSocketFrameSent","description":"Fired when WebSocket message is sent.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"response","description":"WebSocket response data.","$ref":"WebSocketFrame"}]},{"name":"webSocketHandshakeResponseReceived","description":"Fired when WebSocket handshake response becomes available.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"response","description":"WebSocket response data.","$ref":"WebSocketResponse"}]},{"name":"webSocketWillSendHandshakeRequest","description":"Fired when WebSocket is about to initiate handshake.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"wallTime","description":"UTC Timestamp.","$ref":"TimeSinceEpoch"},{"name":"request","description":"WebSocket request data.","$ref":"WebSocketRequest"}]}]},{"domain":"Overlay","description":"This domain provides various functionality related to drawing atop the inspected page.","experimental":true,"dependencies":["DOM","Page","Runtime"],"types":[{"id":"HighlightConfig","description":"Configuration data for the highlighting of page elements.","type":"object","properties":[{"name":"showInfo","description":"Whether the node info tooltip should be shown (default: false).","optional":true,"type":"boolean"},{"name":"showStyles","description":"Whether the node styles in the tooltip (default: false).","optional":true,"type":"boolean"},{"name":"showRulers","description":"Whether the rulers should be shown (default: false).","optional":true,"type":"boolean"},{"name":"showExtensionLines","description":"Whether the extension lines from node to the rulers should be shown (default: false).","optional":true,"type":"boolean"},{"name":"contentColor","description":"The content box highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"paddingColor","description":"The padding highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"borderColor","description":"The border highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"marginColor","description":"The margin highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"eventTargetColor","description":"The event target element highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"shapeColor","description":"The shape outside fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"shapeMarginColor","description":"The shape margin fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"cssGridColor","description":"The grid layout color (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"id":"InspectMode","type":"string","enum":["searchForNode","searchForUAShadowDOM","captureAreaScreenshot","showDistances","none"]}],"commands":[{"name":"disable","description":"Disables domain notifications."},{"name":"enable","description":"Enables domain notifications."},{"name":"getHighlightObjectForTest","description":"For testing.","parameters":[{"name":"nodeId","description":"Id of the node to get highlight object for.","$ref":"DOM.NodeId"},{"name":"includeDistance","description":"Whether to include distance info.","optional":true,"type":"boolean"},{"name":"includeStyle","description":"Whether to include style info.","optional":true,"type":"boolean"}],"returns":[{"name":"highlight","description":"Highlight data for the node.","type":"object"}]},{"name":"hideHighlight","description":"Hides any highlight."},{"name":"highlightFrame","description":"Highlights owner element of the frame with given id.","parameters":[{"name":"frameId","description":"Identifier of the frame to highlight.","$ref":"Page.FrameId"},{"name":"contentColor","description":"The content box highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"contentOutlineColor","description":"The content box highlight outline color (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"name":"highlightNode","description":"Highlights DOM node with given id or with the given JavaScript object wrapper. Either nodeId or\\nobjectId must be specified.","parameters":[{"name":"highlightConfig","description":"A descriptor for the highlight appearance.","$ref":"HighlightConfig"},{"name":"nodeId","description":"Identifier of the node to highlight.","optional":true,"$ref":"DOM.NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node to highlight.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node to be highlighted.","optional":true,"$ref":"Runtime.RemoteObjectId"},{"name":"selector","description":"Selectors to highlight relevant nodes.","optional":true,"type":"string"}]},{"name":"highlightQuad","description":"Highlights given quad. Coordinates are absolute with respect to the main frame viewport.","parameters":[{"name":"quad","description":"Quad to highlight","$ref":"DOM.Quad"},{"name":"color","description":"The highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"outlineColor","description":"The highlight outline color (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"name":"highlightRect","description":"Highlights given rectangle. Coordinates are absolute with respect to the main frame viewport.","parameters":[{"name":"x","description":"X coordinate","type":"integer"},{"name":"y","description":"Y coordinate","type":"integer"},{"name":"width","description":"Rectangle width","type":"integer"},{"name":"height","description":"Rectangle height","type":"integer"},{"name":"color","description":"The highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"outlineColor","description":"The highlight outline color (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"name":"setInspectMode","description":"Enters the \'inspect\' mode. In this mode, elements that user is hovering over are highlighted.\\nBackend then generates \'inspectNodeRequested\' event upon element selection.","parameters":[{"name":"mode","description":"Set an inspection mode.","$ref":"InspectMode"},{"name":"highlightConfig","description":"A descriptor for the highlight appearance of hovered-over nodes. May be omitted if `enabled\\n== false`.","optional":true,"$ref":"HighlightConfig"}]},{"name":"setShowAdHighlights","description":"Highlights owner element of all frames detected to be ads.","parameters":[{"name":"show","description":"True for showing ad highlights","type":"boolean"}]},{"name":"setPausedInDebuggerMessage","parameters":[{"name":"message","description":"The message to display, also triggers resume and step over controls.","optional":true,"type":"string"}]},{"name":"setShowDebugBorders","description":"Requests that backend shows debug borders on layers","parameters":[{"name":"show","description":"True for showing debug borders","type":"boolean"}]},{"name":"setShowFPSCounter","description":"Requests that backend shows the FPS counter","parameters":[{"name":"show","description":"True for showing the FPS counter","type":"boolean"}]},{"name":"setShowPaintRects","description":"Requests that backend shows paint rectangles","parameters":[{"name":"result","description":"True for showing paint rectangles","type":"boolean"}]},{"name":"setShowLayoutShiftRegions","description":"Requests that backend shows layout shift regions","parameters":[{"name":"result","description":"True for showing layout shift regions","type":"boolean"}]},{"name":"setShowScrollBottleneckRects","description":"Requests that backend shows scroll bottleneck rects","parameters":[{"name":"show","description":"True for showing scroll bottleneck rects","type":"boolean"}]},{"name":"setShowHitTestBorders","description":"Requests that backend shows hit-test borders on layers","parameters":[{"name":"show","description":"True for showing hit-test borders","type":"boolean"}]},{"name":"setShowViewportSizeOnResize","description":"Paints viewport size upon main frame resize.","parameters":[{"name":"show","description":"Whether to paint size or not.","type":"boolean"}]}],"events":[{"name":"inspectNodeRequested","description":"Fired when the node should be inspected. This happens after call to `setInspectMode` or when\\nuser manually inspects an element.","parameters":[{"name":"backendNodeId","description":"Id of the node to inspect.","$ref":"DOM.BackendNodeId"}]},{"name":"nodeHighlightRequested","description":"Fired when the node should be highlighted. This happens after call to `setInspectMode`.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}]},{"name":"screenshotRequested","description":"Fired when user asks to capture screenshot of some area on the page.","parameters":[{"name":"viewport","description":"Viewport to capture, in device independent pixels (dip).","$ref":"Page.Viewport"}]},{"name":"inspectModeCanceled","description":"Fired when user cancels the inspect mode."}]},{"domain":"Page","description":"Actions and events related to the inspected page belong to the page domain.","dependencies":["Debugger","DOM","IO","Network","Runtime"],"types":[{"id":"FrameId","description":"Unique frame identifier.","type":"string"},{"id":"Frame","description":"Information about the Frame on the page.","type":"object","properties":[{"name":"id","description":"Frame unique identifier.","type":"string"},{"name":"parentId","description":"Parent frame identifier.","optional":true,"type":"string"},{"name":"loaderId","description":"Identifier of the loader associated with this frame.","$ref":"Network.LoaderId"},{"name":"name","description":"Frame\'s name as specified in the tag.","optional":true,"type":"string"},{"name":"url","description":"Frame document\'s URL without fragment.","type":"string"},{"name":"urlFragment","description":"Frame document\'s URL fragment including the \'#\'.","experimental":true,"optional":true,"type":"string"},{"name":"securityOrigin","description":"Frame document\'s security origin.","type":"string"},{"name":"mimeType","description":"Frame document\'s mimeType as determined by the browser.","type":"string"},{"name":"unreachableUrl","description":"If the frame failed to load, this contains the URL that could not be loaded. Note that unlike url above, this URL may contain a fragment.","experimental":true,"optional":true,"type":"string"}]},{"id":"FrameResource","description":"Information about the Resource on the page.","experimental":true,"type":"object","properties":[{"name":"url","description":"Resource URL.","type":"string"},{"name":"type","description":"Type of this resource.","$ref":"Network.ResourceType"},{"name":"mimeType","description":"Resource mimeType as determined by the browser.","type":"string"},{"name":"lastModified","description":"last-modified timestamp as reported by server.","optional":true,"$ref":"Network.TimeSinceEpoch"},{"name":"contentSize","description":"Resource content size.","optional":true,"type":"number"},{"name":"failed","description":"True if the resource failed to load.","optional":true,"type":"boolean"},{"name":"canceled","description":"True if the resource was canceled during loading.","optional":true,"type":"boolean"}]},{"id":"FrameResourceTree","description":"Information about the Frame hierarchy along with their cached resources.","experimental":true,"type":"object","properties":[{"name":"frame","description":"Frame information for this tree item.","$ref":"Frame"},{"name":"childFrames","description":"Child frames.","optional":true,"type":"array","items":{"$ref":"FrameResourceTree"}},{"name":"resources","description":"Information about frame resources.","type":"array","items":{"$ref":"FrameResource"}}]},{"id":"FrameTree","description":"Information about the Frame hierarchy.","type":"object","properties":[{"name":"frame","description":"Frame information for this tree item.","$ref":"Frame"},{"name":"childFrames","description":"Child frames.","optional":true,"type":"array","items":{"$ref":"FrameTree"}}]},{"id":"ScriptIdentifier","description":"Unique script identifier.","type":"string"},{"id":"TransitionType","description":"Transition type.","type":"string","enum":["link","typed","address_bar","auto_bookmark","auto_subframe","manual_subframe","generated","auto_toplevel","form_submit","reload","keyword","keyword_generated","other"]},{"id":"NavigationEntry","description":"Navigation history entry.","type":"object","properties":[{"name":"id","description":"Unique id of the navigation history entry.","type":"integer"},{"name":"url","description":"URL of the navigation history entry.","type":"string"},{"name":"userTypedURL","description":"URL that the user typed in the url bar.","type":"string"},{"name":"title","description":"Title of the navigation history entry.","type":"string"},{"name":"transitionType","description":"Transition type.","$ref":"TransitionType"}]},{"id":"ScreencastFrameMetadata","description":"Screencast frame metadata.","experimental":true,"type":"object","properties":[{"name":"offsetTop","description":"Top offset in DIP.","type":"number"},{"name":"pageScaleFactor","description":"Page scale factor.","type":"number"},{"name":"deviceWidth","description":"Device screen width in DIP.","type":"number"},{"name":"deviceHeight","description":"Device screen height in DIP.","type":"number"},{"name":"scrollOffsetX","description":"Position of horizontal scroll in CSS pixels.","type":"number"},{"name":"scrollOffsetY","description":"Position of vertical scroll in CSS pixels.","type":"number"},{"name":"timestamp","description":"Frame swap timestamp.","optional":true,"$ref":"Network.TimeSinceEpoch"}]},{"id":"DialogType","description":"Javascript dialog type.","type":"string","enum":["alert","confirm","prompt","beforeunload"]},{"id":"AppManifestError","description":"Error while paring app manifest.","type":"object","properties":[{"name":"message","description":"Error message.","type":"string"},{"name":"critical","description":"If criticial, this is a non-recoverable parse error.","type":"integer"},{"name":"line","description":"Error line.","type":"integer"},{"name":"column","description":"Error column.","type":"integer"}]},{"id":"LayoutViewport","description":"Layout viewport position and dimensions.","type":"object","properties":[{"name":"pageX","description":"Horizontal offset relative to the document (CSS pixels).","type":"integer"},{"name":"pageY","description":"Vertical offset relative to the document (CSS pixels).","type":"integer"},{"name":"clientWidth","description":"Width (CSS pixels), excludes scrollbar if present.","type":"integer"},{"name":"clientHeight","description":"Height (CSS pixels), excludes scrollbar if present.","type":"integer"}]},{"id":"VisualViewport","description":"Visual viewport position, dimensions, and scale.","type":"object","properties":[{"name":"offsetX","description":"Horizontal offset relative to the layout viewport (CSS pixels).","type":"number"},{"name":"offsetY","description":"Vertical offset relative to the layout viewport (CSS pixels).","type":"number"},{"name":"pageX","description":"Horizontal offset relative to the document (CSS pixels).","type":"number"},{"name":"pageY","description":"Vertical offset relative to the document (CSS pixels).","type":"number"},{"name":"clientWidth","description":"Width (CSS pixels), excludes scrollbar if present.","type":"number"},{"name":"clientHeight","description":"Height (CSS pixels), excludes scrollbar if present.","type":"number"},{"name":"scale","description":"Scale relative to the ideal viewport (size at width=device-width).","type":"number"},{"name":"zoom","description":"Page zoom factor (CSS to device independent pixels ratio).","optional":true,"type":"number"}]},{"id":"Viewport","description":"Viewport for capturing screenshot.","type":"object","properties":[{"name":"x","description":"X offset in device independent pixels (dip).","type":"number"},{"name":"y","description":"Y offset in device independent pixels (dip).","type":"number"},{"name":"width","description":"Rectangle width in device independent pixels (dip).","type":"number"},{"name":"height","description":"Rectangle height in device independent pixels (dip).","type":"number"},{"name":"scale","description":"Page scale factor.","type":"number"}]},{"id":"FontFamilies","description":"Generic font families collection.","experimental":true,"type":"object","properties":[{"name":"standard","description":"The standard font-family.","optional":true,"type":"string"},{"name":"fixed","description":"The fixed font-family.","optional":true,"type":"string"},{"name":"serif","description":"The serif font-family.","optional":true,"type":"string"},{"name":"sansSerif","description":"The sansSerif font-family.","optional":true,"type":"string"},{"name":"cursive","description":"The cursive font-family.","optional":true,"type":"string"},{"name":"fantasy","description":"The fantasy font-family.","optional":true,"type":"string"},{"name":"pictograph","description":"The pictograph font-family.","optional":true,"type":"string"}]},{"id":"FontSizes","description":"Default font sizes.","experimental":true,"type":"object","properties":[{"name":"standard","description":"Default standard font size.","optional":true,"type":"integer"},{"name":"fixed","description":"Default fixed font size.","optional":true,"type":"integer"}]},{"id":"ClientNavigationReason","experimental":true,"type":"string","enum":["formSubmissionGet","formSubmissionPost","httpHeaderRefresh","scriptInitiated","metaTagRefresh","pageBlockInterstitial","reload"]}],"commands":[{"name":"addScriptToEvaluateOnLoad","description":"Deprecated, please use addScriptToEvaluateOnNewDocument instead.","experimental":true,"deprecated":true,"parameters":[{"name":"scriptSource","type":"string"}],"returns":[{"name":"identifier","description":"Identifier of the added script.","$ref":"ScriptIdentifier"}]},{"name":"addScriptToEvaluateOnNewDocument","description":"Evaluates given script in every frame upon creation (before loading frame\'s scripts).","parameters":[{"name":"source","type":"string"},{"name":"worldName","description":"If specified, creates an isolated world with the given name and evaluates given script in it.\\nThis world name will be used as the ExecutionContextDescription::name when the corresponding\\nevent is emitted.","experimental":true,"optional":true,"type":"string"}],"returns":[{"name":"identifier","description":"Identifier of the added script.","$ref":"ScriptIdentifier"}]},{"name":"bringToFront","description":"Brings page to front (activates tab)."},{"name":"captureScreenshot","description":"Capture page screenshot.","parameters":[{"name":"format","description":"Image compression format (defaults to png).","optional":true,"type":"string","enum":["jpeg","png"]},{"name":"quality","description":"Compression quality from range [0..100] (jpeg only).","optional":true,"type":"integer"},{"name":"clip","description":"Capture the screenshot of a given region only.","optional":true,"$ref":"Viewport"},{"name":"fromSurface","description":"Capture the screenshot from the surface, rather than the view. Defaults to true.","experimental":true,"optional":true,"type":"boolean"}],"returns":[{"name":"data","description":"Base64-encoded image data.","type":"string"}]},{"name":"captureSnapshot","description":"Returns a snapshot of the page as a string. For MHTML format, the serialization includes\\niframes, shadow DOM, external resources, and element-inline styles.","experimental":true,"parameters":[{"name":"format","description":"Format (defaults to mhtml).","optional":true,"type":"string","enum":["mhtml"]}],"returns":[{"name":"data","description":"Serialized page data.","type":"string"}]},{"name":"clearDeviceMetricsOverride","description":"Clears the overriden device metrics.","experimental":true,"deprecated":true,"redirect":"Emulation"},{"name":"clearDeviceOrientationOverride","description":"Clears the overridden Device Orientation.","experimental":true,"deprecated":true,"redirect":"DeviceOrientation"},{"name":"clearGeolocationOverride","description":"Clears the overriden Geolocation Position and Error.","deprecated":true,"redirect":"Emulation"},{"name":"createIsolatedWorld","description":"Creates an isolated world for the given frame.","parameters":[{"name":"frameId","description":"Id of the frame in which the isolated world should be created.","$ref":"FrameId"},{"name":"worldName","description":"An optional name which is reported in the Execution Context.","optional":true,"type":"string"},{"name":"grantUniveralAccess","description":"Whether or not universal access should be granted to the isolated world. This is a powerful\\noption, use with caution.","optional":true,"type":"boolean"}],"returns":[{"name":"executionContextId","description":"Execution context of the isolated world.","$ref":"Runtime.ExecutionContextId"}]},{"name":"deleteCookie","description":"Deletes browser cookie with given name, domain and path.","experimental":true,"deprecated":true,"redirect":"Network","parameters":[{"name":"cookieName","description":"Name of the cookie to remove.","type":"string"},{"name":"url","description":"URL to match cooke domain and path.","type":"string"}]},{"name":"disable","description":"Disables page domain notifications."},{"name":"enable","description":"Enables page domain notifications."},{"name":"getAppManifest","returns":[{"name":"url","description":"Manifest location.","type":"string"},{"name":"errors","type":"array","items":{"$ref":"AppManifestError"}},{"name":"data","description":"Manifest content.","optional":true,"type":"string"}]},{"name":"getInstallabilityErrors","experimental":true,"returns":[{"name":"errors","type":"array","items":{"type":"string"}}]},{"name":"getCookies","description":"Returns all browser cookies. Depending on the backend support, will return detailed cookie\\ninformation in the `cookies` field.","experimental":true,"deprecated":true,"redirect":"Network","returns":[{"name":"cookies","description":"Array of cookie objects.","type":"array","items":{"$ref":"Network.Cookie"}}]},{"name":"getFrameTree","description":"Returns present frame tree structure.","returns":[{"name":"frameTree","description":"Present frame tree structure.","$ref":"FrameTree"}]},{"name":"getLayoutMetrics","description":"Returns metrics relating to the layouting of the page, such as viewport bounds/scale.","returns":[{"name":"layoutViewport","description":"Metrics relating to the layout viewport.","$ref":"LayoutViewport"},{"name":"visualViewport","description":"Metrics relating to the visual viewport.","$ref":"VisualViewport"},{"name":"contentSize","description":"Size of scrollable area.","$ref":"DOM.Rect"}]},{"name":"getNavigationHistory","description":"Returns navigation history for the current page.","returns":[{"name":"currentIndex","description":"Index of the current navigation history entry.","type":"integer"},{"name":"entries","description":"Array of navigation history entries.","type":"array","items":{"$ref":"NavigationEntry"}}]},{"name":"resetNavigationHistory","description":"Resets navigation history for the current page."},{"name":"getResourceContent","description":"Returns content of the given resource.","experimental":true,"parameters":[{"name":"frameId","description":"Frame id to get resource for.","$ref":"FrameId"},{"name":"url","description":"URL of the resource to get content for.","type":"string"}],"returns":[{"name":"content","description":"Resource content.","type":"string"},{"name":"base64Encoded","description":"True, if content was served as base64.","type":"boolean"}]},{"name":"getResourceTree","description":"Returns present frame / resource tree structure.","experimental":true,"returns":[{"name":"frameTree","description":"Present frame / resource tree structure.","$ref":"FrameResourceTree"}]},{"name":"handleJavaScriptDialog","description":"Accepts or dismisses a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload).","parameters":[{"name":"accept","description":"Whether to accept or dismiss the dialog.","type":"boolean"},{"name":"promptText","description":"The text to enter into the dialog prompt before accepting. Used only if this is a prompt\\ndialog.","optional":true,"type":"string"}]},{"name":"navigate","description":"Navigates current page to the given URL.","parameters":[{"name":"url","description":"URL to navigate the page to.","type":"string"},{"name":"referrer","description":"Referrer URL.","optional":true,"type":"string"},{"name":"transitionType","description":"Intended transition type.","optional":true,"$ref":"TransitionType"},{"name":"frameId","description":"Frame id to navigate, if not specified navigates the top frame.","optional":true,"$ref":"FrameId"}],"returns":[{"name":"frameId","description":"Frame id that has navigated (or failed to navigate)","$ref":"FrameId"},{"name":"loaderId","description":"Loader identifier.","optional":true,"$ref":"Network.LoaderId"},{"name":"errorText","description":"User friendly error message, present if and only if navigation has failed.","optional":true,"type":"string"}]},{"name":"navigateToHistoryEntry","description":"Navigates current page to the given history entry.","parameters":[{"name":"entryId","description":"Unique id of the entry to navigate to.","type":"integer"}]},{"name":"printToPDF","description":"Print page as PDF.","parameters":[{"name":"landscape","description":"Paper orientation. Defaults to false.","optional":true,"type":"boolean"},{"name":"displayHeaderFooter","description":"Display header and footer. Defaults to false.","optional":true,"type":"boolean"},{"name":"printBackground","description":"Print background graphics. Defaults to false.","optional":true,"type":"boolean"},{"name":"scale","description":"Scale of the webpage rendering. Defaults to 1.","optional":true,"type":"number"},{"name":"paperWidth","description":"Paper width in inches. Defaults to 8.5 inches.","optional":true,"type":"number"},{"name":"paperHeight","description":"Paper height in inches. Defaults to 11 inches.","optional":true,"type":"number"},{"name":"marginTop","description":"Top margin in inches. Defaults to 1cm (~0.4 inches).","optional":true,"type":"number"},{"name":"marginBottom","description":"Bottom margin in inches. Defaults to 1cm (~0.4 inches).","optional":true,"type":"number"},{"name":"marginLeft","description":"Left margin in inches. Defaults to 1cm (~0.4 inches).","optional":true,"type":"number"},{"name":"marginRight","description":"Right margin in inches. Defaults to 1cm (~0.4 inches).","optional":true,"type":"number"},{"name":"pageRanges","description":"Paper ranges to print, e.g., \'1-5, 8, 11-13\'. Defaults to the empty string, which means\\nprint all pages.","optional":true,"type":"string"},{"name":"ignoreInvalidPageRanges","description":"Whether to silently ignore invalid but successfully parsed page ranges, such as \'3-2\'.\\nDefaults to false.","optional":true,"type":"boolean"},{"name":"headerTemplate","description":"HTML template for the print header. Should be valid HTML markup with following\\nclasses used to inject printing values into them:\\n- `date`: formatted print date\\n- `title`: document title\\n- `url`: document location\\n- `pageNumber`: current page number\\n- `totalPages`: total pages in the document\\n\\nFor example, `<span class=title></span>` would generate span containing the title.","optional":true,"type":"string"},{"name":"footerTemplate","description":"HTML template for the print footer. Should use the same format as the `headerTemplate`.","optional":true,"type":"string"},{"name":"preferCSSPageSize","description":"Whether or not to prefer page size as defined by css. Defaults to false,\\nin which case the content will be scaled to fit the paper size.","optional":true,"type":"boolean"},{"name":"transferMode","description":"return as stream","experimental":true,"optional":true,"type":"string","enum":["ReturnAsBase64","ReturnAsStream"]}],"returns":[{"name":"data","description":"Base64-encoded pdf data. Empty if |returnAsStream| is specified.","type":"string"},{"name":"stream","description":"A handle of the stream that holds resulting PDF data.","experimental":true,"optional":true,"$ref":"IO.StreamHandle"}]},{"name":"reload","description":"Reloads given page optionally ignoring the cache.","parameters":[{"name":"ignoreCache","description":"If true, browser cache is ignored (as if the user pressed Shift+refresh).","optional":true,"type":"boolean"},{"name":"scriptToEvaluateOnLoad","description":"If set, the script will be injected into all frames of the inspected page after reload.\\nArgument will be ignored if reloading dataURL origin.","optional":true,"type":"string"}]},{"name":"removeScriptToEvaluateOnLoad","description":"Deprecated, please use removeScriptToEvaluateOnNewDocument instead.","experimental":true,"deprecated":true,"parameters":[{"name":"identifier","$ref":"ScriptIdentifier"}]},{"name":"removeScriptToEvaluateOnNewDocument","description":"Removes given script from the list.","parameters":[{"name":"identifier","$ref":"ScriptIdentifier"}]},{"name":"screencastFrameAck","description":"Acknowledges that a screencast frame has been received by the frontend.","experimental":true,"parameters":[{"name":"sessionId","description":"Frame number.","type":"integer"}]},{"name":"searchInResource","description":"Searches for given string in resource content.","experimental":true,"parameters":[{"name":"frameId","description":"Frame id for resource to search in.","$ref":"FrameId"},{"name":"url","description":"URL of the resource to search in.","type":"string"},{"name":"query","description":"String to search for.","type":"string"},{"name":"caseSensitive","description":"If true, search is case sensitive.","optional":true,"type":"boolean"},{"name":"isRegex","description":"If true, treats string parameter as regex.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"List of search matches.","type":"array","items":{"$ref":"Debugger.SearchMatch"}}]},{"name":"setAdBlockingEnabled","description":"Enable Chrome\'s experimental ad filter on all sites.","experimental":true,"parameters":[{"name":"enabled","description":"Whether to block ads.","type":"boolean"}]},{"name":"setBypassCSP","description":"Enable page Content Security Policy by-passing.","experimental":true,"parameters":[{"name":"enabled","description":"Whether to bypass page CSP.","type":"boolean"}]},{"name":"setDeviceMetricsOverride","description":"Overrides the values of device screen dimensions (window.screen.width, window.screen.height,\\nwindow.innerWidth, window.innerHeight, and \\"device-width\\"/\\"device-height\\"-related CSS media\\nquery results).","experimental":true,"deprecated":true,"redirect":"Emulation","parameters":[{"name":"width","description":"Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override.","type":"integer"},{"name":"height","description":"Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override.","type":"integer"},{"name":"deviceScaleFactor","description":"Overriding device scale factor value. 0 disables the override.","type":"number"},{"name":"mobile","description":"Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text\\nautosizing and more.","type":"boolean"},{"name":"scale","description":"Scale to apply to resulting view image.","optional":true,"type":"number"},{"name":"screenWidth","description":"Overriding screen width value in pixels (minimum 0, maximum 10000000).","optional":true,"type":"integer"},{"name":"screenHeight","description":"Overriding screen height value in pixels (minimum 0, maximum 10000000).","optional":true,"type":"integer"},{"name":"positionX","description":"Overriding view X position on screen in pixels (minimum 0, maximum 10000000).","optional":true,"type":"integer"},{"name":"positionY","description":"Overriding view Y position on screen in pixels (minimum 0, maximum 10000000).","optional":true,"type":"integer"},{"name":"dontSetVisibleSize","description":"Do not set visible view size, rely upon explicit setVisibleSize call.","optional":true,"type":"boolean"},{"name":"screenOrientation","description":"Screen orientation override.","optional":true,"$ref":"Emulation.ScreenOrientation"},{"name":"viewport","description":"The viewport dimensions and scale. If not set, the override is cleared.","optional":true,"$ref":"Viewport"}]},{"name":"setDeviceOrientationOverride","description":"Overrides the Device Orientation.","experimental":true,"deprecated":true,"redirect":"DeviceOrientation","parameters":[{"name":"alpha","description":"Mock alpha","type":"number"},{"name":"beta","description":"Mock beta","type":"number"},{"name":"gamma","description":"Mock gamma","type":"number"}]},{"name":"setFontFamilies","description":"Set generic font families.","experimental":true,"parameters":[{"name":"fontFamilies","description":"Specifies font families to set. If a font family is not specified, it won\'t be changed.","$ref":"FontFamilies"}]},{"name":"setFontSizes","description":"Set default font sizes.","experimental":true,"parameters":[{"name":"fontSizes","description":"Specifies font sizes to set. If a font size is not specified, it won\'t be changed.","$ref":"FontSizes"}]},{"name":"setDocumentContent","description":"Sets given markup as the document\'s HTML.","parameters":[{"name":"frameId","description":"Frame id to set HTML for.","$ref":"FrameId"},{"name":"html","description":"HTML content to set.","type":"string"}]},{"name":"setDownloadBehavior","description":"Set the behavior when downloading a file.","experimental":true,"parameters":[{"name":"behavior","description":"Whether to allow all or deny all download requests, or use default Chrome behavior if\\navailable (otherwise deny).","type":"string","enum":["deny","allow","default"]},{"name":"downloadPath","description":"The default path to save downloaded files to. This is requred if behavior is set to \'allow\'","optional":true,"type":"string"}]},{"name":"setGeolocationOverride","description":"Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position\\nunavailable.","deprecated":true,"redirect":"Emulation","parameters":[{"name":"latitude","description":"Mock latitude","optional":true,"type":"number"},{"name":"longitude","description":"Mock longitude","optional":true,"type":"number"},{"name":"accuracy","description":"Mock accuracy","optional":true,"type":"number"}]},{"name":"setLifecycleEventsEnabled","description":"Controls whether page will emit lifecycle events.","experimental":true,"parameters":[{"name":"enabled","description":"If true, starts emitting lifecycle events.","type":"boolean"}]},{"name":"setTouchEmulationEnabled","description":"Toggles mouse event-based touch event emulation.","experimental":true,"deprecated":true,"redirect":"Emulation","parameters":[{"name":"enabled","description":"Whether the touch event emulation should be enabled.","type":"boolean"},{"name":"configuration","description":"Touch/gesture events configuration. Default: current platform.","optional":true,"type":"string","enum":["mobile","desktop"]}]},{"name":"startScreencast","description":"Starts sending each frame using the `screencastFrame` event.","experimental":true,"parameters":[{"name":"format","description":"Image compression format.","optional":true,"type":"string","enum":["jpeg","png"]},{"name":"quality","description":"Compression quality from range [0..100].","optional":true,"type":"integer"},{"name":"maxWidth","description":"Maximum screenshot width.","optional":true,"type":"integer"},{"name":"maxHeight","description":"Maximum screenshot height.","optional":true,"type":"integer"},{"name":"everyNthFrame","description":"Send every n-th frame.","optional":true,"type":"integer"}]},{"name":"stopLoading","description":"Force the page stop all navigations and pending resource fetches."},{"name":"crash","description":"Crashes renderer on the IO thread, generates minidumps.","experimental":true},{"name":"close","description":"Tries to close page, running its beforeunload hooks, if any.","experimental":true},{"name":"setWebLifecycleState","description":"Tries to update the web lifecycle state of the page.\\nIt will transition the page to the given state according to:\\nhttps://github.com/WICG/web-lifecycle/","experimental":true,"parameters":[{"name":"state","description":"Target lifecycle state","type":"string","enum":["frozen","active"]}]},{"name":"stopScreencast","description":"Stops sending each frame in the `screencastFrame`.","experimental":true},{"name":"setProduceCompilationCache","description":"Forces compilation cache to be generated for every subresource script.","experimental":true,"parameters":[{"name":"enabled","type":"boolean"}]},{"name":"addCompilationCache","description":"Seeds compilation cache for given url. Compilation cache does not survive\\ncross-process navigation.","experimental":true,"parameters":[{"name":"url","type":"string"},{"name":"data","description":"Base64-encoded data","type":"string"}]},{"name":"clearCompilationCache","description":"Clears seeded compilation cache.","experimental":true},{"name":"generateTestReport","description":"Generates a report for testing.","experimental":true,"parameters":[{"name":"message","description":"Message to be displayed in the report.","type":"string"},{"name":"group","description":"Specifies the endpoint group to deliver the report to.","optional":true,"type":"string"}]},{"name":"waitForDebugger","description":"Pauses page execution. Can be resumed using generic Runtime.runIfWaitingForDebugger.","experimental":true},{"name":"setInterceptFileChooserDialog","description":"Intercept file chooser requests and transfer control to protocol clients.\\nWhen file chooser interception is enabled, native file chooser dialog is not shown.\\nInstead, a protocol event `Page.fileChooserOpened` is emitted.\\nFile chooser can be handled with `page.handleFileChooser` command.","experimental":true,"parameters":[{"name":"enabled","type":"boolean"}]},{"name":"handleFileChooser","description":"Accepts or cancels an intercepted file chooser dialog.","experimental":true,"parameters":[{"name":"action","type":"string","enum":["accept","cancel","fallback"]},{"name":"files","description":"Array of absolute file paths to set, only respected with `accept` action.","optional":true,"type":"array","items":{"type":"string"}}]}],"events":[{"name":"domContentEventFired","parameters":[{"name":"timestamp","$ref":"Network.MonotonicTime"}]},{"name":"fileChooserOpened","description":"Emitted only when `page.interceptFileChooser` is enabled.","parameters":[{"name":"mode","type":"string","enum":["selectSingle","selectMultiple"]}]},{"name":"frameAttached","description":"Fired when frame has been attached to its parent.","parameters":[{"name":"frameId","description":"Id of the frame that has been attached.","$ref":"FrameId"},{"name":"parentFrameId","description":"Parent frame identifier.","$ref":"FrameId"},{"name":"stack","description":"JavaScript stack trace of when frame was attached, only set if frame initiated from script.","optional":true,"$ref":"Runtime.StackTrace"}]},{"name":"frameClearedScheduledNavigation","description":"Fired when frame no longer has a scheduled navigation.","deprecated":true,"parameters":[{"name":"frameId","description":"Id of the frame that has cleared its scheduled navigation.","$ref":"FrameId"}]},{"name":"frameDetached","description":"Fired when frame has been detached from its parent.","parameters":[{"name":"frameId","description":"Id of the frame that has been detached.","$ref":"FrameId"}]},{"name":"frameNavigated","description":"Fired once navigation of the frame has completed. Frame is now associated with the new loader.","parameters":[{"name":"frame","description":"Frame object.","$ref":"Frame"}]},{"name":"frameResized","experimental":true},{"name":"frameRequestedNavigation","description":"Fired when a renderer-initiated navigation is requested.\\nNavigation may still be cancelled after the event is issued.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame that is being navigated.","$ref":"FrameId"},{"name":"reason","description":"The reason for the navigation.","$ref":"ClientNavigationReason"},{"name":"url","description":"The destination URL for the requested navigation.","type":"string"}]},{"name":"frameScheduledNavigation","description":"Fired when frame schedules a potential navigation.","deprecated":true,"parameters":[{"name":"frameId","description":"Id of the frame that has scheduled a navigation.","$ref":"FrameId"},{"name":"delay","description":"Delay (in seconds) until the navigation is scheduled to begin. The navigation is not\\nguaranteed to start.","type":"number"},{"name":"reason","description":"The reason for the navigation.","type":"string","enum":["formSubmissionGet","formSubmissionPost","httpHeaderRefresh","scriptInitiated","metaTagRefresh","pageBlockInterstitial","reload"]},{"name":"url","description":"The destination URL for the scheduled navigation.","type":"string"}]},{"name":"frameStartedLoading","description":"Fired when frame has started loading.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame that has started loading.","$ref":"FrameId"}]},{"name":"frameStoppedLoading","description":"Fired when frame has stopped loading.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame that has stopped loading.","$ref":"FrameId"}]},{"name":"downloadWillBegin","description":"Fired when page is about to start a download.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame that caused download to begin.","$ref":"FrameId"},{"name":"url","description":"URL of the resource being downloaded.","type":"string"}]},{"name":"interstitialHidden","description":"Fired when interstitial page was hidden"},{"name":"interstitialShown","description":"Fired when interstitial page was shown"},{"name":"javascriptDialogClosed","description":"Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload) has been\\nclosed.","parameters":[{"name":"result","description":"Whether dialog was confirmed.","type":"boolean"},{"name":"userInput","description":"User input in case of prompt.","type":"string"}]},{"name":"javascriptDialogOpening","description":"Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload) is about to\\nopen.","parameters":[{"name":"url","description":"Frame url.","type":"string"},{"name":"message","description":"Message that will be displayed by the dialog.","type":"string"},{"name":"type","description":"Dialog type.","$ref":"DialogType"},{"name":"hasBrowserHandler","description":"True iff browser is capable showing or acting on the given dialog. When browser has no\\ndialog handler for given target, calling alert while Page domain is engaged will stall\\nthe page execution. Execution can be resumed via calling Page.handleJavaScriptDialog.","type":"boolean"},{"name":"defaultPrompt","description":"Default dialog prompt.","optional":true,"type":"string"}]},{"name":"lifecycleEvent","description":"Fired for top level page lifecycle events such as navigation, load, paint, etc.","parameters":[{"name":"frameId","description":"Id of the frame.","$ref":"FrameId"},{"name":"loaderId","description":"Loader identifier. Empty string if the request is fetched from worker.","$ref":"Network.LoaderId"},{"name":"name","type":"string"},{"name":"timestamp","$ref":"Network.MonotonicTime"}]},{"name":"loadEventFired","parameters":[{"name":"timestamp","$ref":"Network.MonotonicTime"}]},{"name":"navigatedWithinDocument","description":"Fired when same-document navigation happens, e.g. due to history API usage or anchor navigation.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame.","$ref":"FrameId"},{"name":"url","description":"Frame\'s new url.","type":"string"}]},{"name":"screencastFrame","description":"Compressed image data requested by the `startScreencast`.","experimental":true,"parameters":[{"name":"data","description":"Base64-encoded compressed image.","type":"string"},{"name":"metadata","description":"Screencast frame metadata.","$ref":"ScreencastFrameMetadata"},{"name":"sessionId","description":"Frame number.","type":"integer"}]},{"name":"screencastVisibilityChanged","description":"Fired when the page with currently enabled screencast was shown or hidden `.","experimental":true,"parameters":[{"name":"visible","description":"True if the page is visible.","type":"boolean"}]},{"name":"windowOpen","description":"Fired when a new window is going to be opened, via window.open(), link click, form submission,\\netc.","parameters":[{"name":"url","description":"The URL for the new window.","type":"string"},{"name":"windowName","description":"Window name.","type":"string"},{"name":"windowFeatures","description":"An array of enabled window features.","type":"array","items":{"type":"string"}},{"name":"userGesture","description":"Whether or not it was triggered by user gesture.","type":"boolean"}]},{"name":"compilationCacheProduced","description":"Issued for every compilation cache generated. Is only available\\nif Page.setGenerateCompilationCache is enabled.","experimental":true,"parameters":[{"name":"url","type":"string"},{"name":"data","description":"Base64-encoded data","type":"string"}]}]},{"domain":"Performance","types":[{"id":"Metric","description":"Run-time execution metric.","type":"object","properties":[{"name":"name","description":"Metric name.","type":"string"},{"name":"value","description":"Metric value.","type":"number"}]}],"commands":[{"name":"disable","description":"Disable collecting and reporting metrics."},{"name":"enable","description":"Enable collecting and reporting metrics."},{"name":"setTimeDomain","description":"Sets time domain to use for collecting and reporting duration metrics.\\nNote that this must be called before enabling metrics collection. Calling\\nthis method while metrics collection is enabled returns an error.","experimental":true,"parameters":[{"name":"timeDomain","description":"Time domain","type":"string","enum":["timeTicks","threadTicks"]}]},{"name":"getMetrics","description":"Retrieve current values of run-time metrics.","returns":[{"name":"metrics","description":"Current values for run-time metrics.","type":"array","items":{"$ref":"Metric"}}]}],"events":[{"name":"metrics","description":"Current values of the metrics.","parameters":[{"name":"metrics","description":"Current values of the metrics.","type":"array","items":{"$ref":"Metric"}},{"name":"title","description":"Timestamp title.","type":"string"}]}]},{"domain":"Security","description":"Security","types":[{"id":"CertificateId","description":"An internal certificate ID value.","type":"integer"},{"id":"MixedContentType","description":"A description of mixed content (HTTP resources on HTTPS pages), as defined by\\nhttps://www.w3.org/TR/mixed-content/#categories","type":"string","enum":["blockable","optionally-blockable","none"]},{"id":"SecurityState","description":"The security level of a page or resource.","type":"string","enum":["unknown","neutral","insecure","secure","info"]},{"id":"SecurityStateExplanation","description":"An explanation of an factor contributing to the security state.","type":"object","properties":[{"name":"securityState","description":"Security state representing the severity of the factor being explained.","$ref":"SecurityState"},{"name":"title","description":"Title describing the type of factor.","type":"string"},{"name":"summary","description":"Short phrase describing the type of factor.","type":"string"},{"name":"description","description":"Full text explanation of the factor.","type":"string"},{"name":"mixedContentType","description":"The type of mixed content described by the explanation.","$ref":"MixedContentType"},{"name":"certificate","description":"Page certificate.","type":"array","items":{"type":"string"}},{"name":"recommendations","description":"Recommendations to fix any issues.","optional":true,"type":"array","items":{"type":"string"}}]},{"id":"InsecureContentStatus","description":"Information about insecure content on the page.","deprecated":true,"type":"object","properties":[{"name":"ranMixedContent","description":"Always false.","type":"boolean"},{"name":"displayedMixedContent","description":"Always false.","type":"boolean"},{"name":"containedMixedForm","description":"Always false.","type":"boolean"},{"name":"ranContentWithCertErrors","description":"Always false.","type":"boolean"},{"name":"displayedContentWithCertErrors","description":"Always false.","type":"boolean"},{"name":"ranInsecureContentStyle","description":"Always set to unknown.","$ref":"SecurityState"},{"name":"displayedInsecureContentStyle","description":"Always set to unknown.","$ref":"SecurityState"}]},{"id":"CertificateErrorAction","description":"The action to take when a certificate error occurs. continue will continue processing the\\nrequest and cancel will cancel the request.","type":"string","enum":["continue","cancel"]}],"commands":[{"name":"disable","description":"Disables tracking security state changes."},{"name":"enable","description":"Enables tracking security state changes."},{"name":"setIgnoreCertificateErrors","description":"Enable/disable whether all certificate errors should be ignored.","experimental":true,"parameters":[{"name":"ignore","description":"If true, all certificate errors will be ignored.","type":"boolean"}]},{"name":"handleCertificateError","description":"Handles a certificate error that fired a certificateError event.","deprecated":true,"parameters":[{"name":"eventId","description":"The ID of the event.","type":"integer"},{"name":"action","description":"The action to take on the certificate error.","$ref":"CertificateErrorAction"}]},{"name":"setOverrideCertificateErrors","description":"Enable/disable overriding certificate errors. If enabled, all certificate error events need to\\nbe handled by the DevTools client and should be answered with `handleCertificateError` commands.","deprecated":true,"parameters":[{"name":"override","description":"If true, certificate errors will be overridden.","type":"boolean"}]}],"events":[{"name":"certificateError","description":"There is a certificate error. If overriding certificate errors is enabled, then it should be\\nhandled with the `handleCertificateError` command. Note: this event does not fire if the\\ncertificate error has been allowed internally. Only one client per target should override\\ncertificate errors at the same time.","deprecated":true,"parameters":[{"name":"eventId","description":"The ID of the event.","type":"integer"},{"name":"errorType","description":"The type of the error.","type":"string"},{"name":"requestURL","description":"The url that was requested.","type":"string"}]},{"name":"securityStateChanged","description":"The security state of the page changed.","parameters":[{"name":"securityState","description":"Security state.","$ref":"SecurityState"},{"name":"schemeIsCryptographic","description":"True if the page was loaded over cryptographic transport such as HTTPS.","deprecated":true,"type":"boolean"},{"name":"explanations","description":"List of explanations for the security state. If the overall security state is `insecure` or\\n`warning`, at least one corresponding explanation should be included.","type":"array","items":{"$ref":"SecurityStateExplanation"}},{"name":"insecureContentStatus","description":"Information about insecure content on the page.","deprecated":true,"$ref":"InsecureContentStatus"},{"name":"summary","description":"Overrides user-visible description of the state.","optional":true,"type":"string"}]}]},{"domain":"ServiceWorker","experimental":true,"types":[{"id":"RegistrationID","type":"string"},{"id":"ServiceWorkerRegistration","description":"ServiceWorker registration.","type":"object","properties":[{"name":"registrationId","$ref":"RegistrationID"},{"name":"scopeURL","type":"string"},{"name":"isDeleted","type":"boolean"}]},{"id":"ServiceWorkerVersionRunningStatus","type":"string","enum":["stopped","starting","running","stopping"]},{"id":"ServiceWorkerVersionStatus","type":"string","enum":["new","installing","installed","activating","activated","redundant"]},{"id":"ServiceWorkerVersion","description":"ServiceWorker version.","type":"object","properties":[{"name":"versionId","type":"string"},{"name":"registrationId","$ref":"RegistrationID"},{"name":"scriptURL","type":"string"},{"name":"runningStatus","$ref":"ServiceWorkerVersionRunningStatus"},{"name":"status","$ref":"ServiceWorkerVersionStatus"},{"name":"scriptLastModified","description":"The Last-Modified header value of the main script.","optional":true,"type":"number"},{"name":"scriptResponseTime","description":"The time at which the response headers of the main script were received from the server.\\nFor cached script it is the last time the cache entry was validated.","optional":true,"type":"number"},{"name":"controlledClients","optional":true,"type":"array","items":{"$ref":"Target.TargetID"}},{"name":"targetId","optional":true,"$ref":"Target.TargetID"}]},{"id":"ServiceWorkerErrorMessage","description":"ServiceWorker error message.","type":"object","properties":[{"name":"errorMessage","type":"string"},{"name":"registrationId","$ref":"RegistrationID"},{"name":"versionId","type":"string"},{"name":"sourceURL","type":"string"},{"name":"lineNumber","type":"integer"},{"name":"columnNumber","type":"integer"}]}],"commands":[{"name":"deliverPushMessage","parameters":[{"name":"origin","type":"string"},{"name":"registrationId","$ref":"RegistrationID"},{"name":"data","type":"string"}]},{"name":"disable"},{"name":"dispatchSyncEvent","parameters":[{"name":"origin","type":"string"},{"name":"registrationId","$ref":"RegistrationID"},{"name":"tag","type":"string"},{"name":"lastChance","type":"boolean"}]},{"name":"enable"},{"name":"inspectWorker","parameters":[{"name":"versionId","type":"string"}]},{"name":"setForceUpdateOnPageLoad","parameters":[{"name":"forceUpdateOnPageLoad","type":"boolean"}]},{"name":"skipWaiting","parameters":[{"name":"scopeURL","type":"string"}]},{"name":"startWorker","parameters":[{"name":"scopeURL","type":"string"}]},{"name":"stopAllWorkers"},{"name":"stopWorker","parameters":[{"name":"versionId","type":"string"}]},{"name":"unregister","parameters":[{"name":"scopeURL","type":"string"}]},{"name":"updateRegistration","parameters":[{"name":"scopeURL","type":"string"}]}],"events":[{"name":"workerErrorReported","parameters":[{"name":"errorMessage","$ref":"ServiceWorkerErrorMessage"}]},{"name":"workerRegistrationUpdated","parameters":[{"name":"registrations","type":"array","items":{"$ref":"ServiceWorkerRegistration"}}]},{"name":"workerVersionUpdated","parameters":[{"name":"versions","type":"array","items":{"$ref":"ServiceWorkerVersion"}}]}]},{"domain":"Storage","experimental":true,"types":[{"id":"StorageType","description":"Enum of possible storage types.","type":"string","enum":["appcache","cookies","file_systems","indexeddb","local_storage","shader_cache","websql","service_workers","cache_storage","all","other"]},{"id":"UsageForType","description":"Usage for a storage type.","type":"object","properties":[{"name":"storageType","description":"Name of storage type.","$ref":"StorageType"},{"name":"usage","description":"Storage usage (bytes).","type":"number"}]}],"commands":[{"name":"clearDataForOrigin","description":"Clears storage for origin.","parameters":[{"name":"origin","description":"Security origin.","type":"string"},{"name":"storageTypes","description":"Comma separated list of StorageType to clear.","type":"string"}]},{"name":"getUsageAndQuota","description":"Returns usage and quota in bytes.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}],"returns":[{"name":"usage","description":"Storage usage (bytes).","type":"number"},{"name":"quota","description":"Storage quota (bytes).","type":"number"},{"name":"usageBreakdown","description":"Storage usage per type (bytes).","type":"array","items":{"$ref":"UsageForType"}}]},{"name":"trackCacheStorageForOrigin","description":"Registers origin to be notified when an update occurs to its cache storage list.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}]},{"name":"trackIndexedDBForOrigin","description":"Registers origin to be notified when an update occurs to its IndexedDB.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}]},{"name":"untrackCacheStorageForOrigin","description":"Unregisters origin from receiving notifications for cache storage.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}]},{"name":"untrackIndexedDBForOrigin","description":"Unregisters origin from receiving notifications for IndexedDB.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}]}],"events":[{"name":"cacheStorageContentUpdated","description":"A cache\'s contents have been modified.","parameters":[{"name":"origin","description":"Origin to update.","type":"string"},{"name":"cacheName","description":"Name of cache in origin.","type":"string"}]},{"name":"cacheStorageListUpdated","description":"A cache has been added/deleted.","parameters":[{"name":"origin","description":"Origin to update.","type":"string"}]},{"name":"indexedDBContentUpdated","description":"The origin\'s IndexedDB object store has been modified.","parameters":[{"name":"origin","description":"Origin to update.","type":"string"},{"name":"databaseName","description":"Database to update.","type":"string"},{"name":"objectStoreName","description":"ObjectStore to update.","type":"string"}]},{"name":"indexedDBListUpdated","description":"The origin\'s IndexedDB database list has been modified.","parameters":[{"name":"origin","description":"Origin to update.","type":"string"}]}]},{"domain":"SystemInfo","description":"The SystemInfo domain defines methods and events for querying low-level system information.","experimental":true,"types":[{"id":"GPUDevice","description":"Describes a single graphics processor (GPU).","type":"object","properties":[{"name":"vendorId","description":"PCI ID of the GPU vendor, if available; 0 otherwise.","type":"number"},{"name":"deviceId","description":"PCI ID of the GPU device, if available; 0 otherwise.","type":"number"},{"name":"vendorString","description":"String description of the GPU vendor, if the PCI ID is not available.","type":"string"},{"name":"deviceString","description":"String description of the GPU device, if the PCI ID is not available.","type":"string"},{"name":"driverVendor","description":"String description of the GPU driver vendor.","type":"string"},{"name":"driverVersion","description":"String description of the GPU driver version.","type":"string"}]},{"id":"Size","description":"Describes the width and height dimensions of an entity.","type":"object","properties":[{"name":"width","description":"Width in pixels.","type":"integer"},{"name":"height","description":"Height in pixels.","type":"integer"}]},{"id":"VideoDecodeAcceleratorCapability","description":"Describes a supported video decoding profile with its associated minimum and\\nmaximum resolutions.","type":"object","properties":[{"name":"profile","description":"Video codec profile that is supported, e.g. VP9 Profile 2.","type":"string"},{"name":"maxResolution","description":"Maximum video dimensions in pixels supported for this |profile|.","$ref":"Size"},{"name":"minResolution","description":"Minimum video dimensions in pixels supported for this |profile|.","$ref":"Size"}]},{"id":"VideoEncodeAcceleratorCapability","description":"Describes a supported video encoding profile with its associated maximum\\nresolution and maximum framerate.","type":"object","properties":[{"name":"profile","description":"Video codec profile that is supported, e.g H264 Main.","type":"string"},{"name":"maxResolution","description":"Maximum video dimensions in pixels supported for this |profile|.","$ref":"Size"},{"name":"maxFramerateNumerator","description":"Maximum encoding framerate in frames per second supported for this\\n|profile|, as fraction\'s numerator and denominator, e.g. 24/1 fps,\\n24000/1001 fps, etc.","type":"integer"},{"name":"maxFramerateDenominator","type":"integer"}]},{"id":"SubsamplingFormat","description":"YUV subsampling type of the pixels of a given image.","type":"string","enum":["yuv420","yuv422","yuv444"]},{"id":"ImageDecodeAcceleratorCapability","description":"Describes a supported image decoding profile with its associated minimum and\\nmaximum resolutions and subsampling.","type":"object","properties":[{"name":"imageType","description":"Image coded, e.g. Jpeg.","type":"string"},{"name":"maxDimensions","description":"Maximum supported dimensions of the image in pixels.","$ref":"Size"},{"name":"minDimensions","description":"Minimum supported dimensions of the image in pixels.","$ref":"Size"},{"name":"subsamplings","description":"Optional array of supported subsampling formats, e.g. 4:2:0, if known.","type":"array","items":{"$ref":"SubsamplingFormat"}}]},{"id":"GPUInfo","description":"Provides information about the GPU(s) on the system.","type":"object","properties":[{"name":"devices","description":"The graphics devices on the system. Element 0 is the primary GPU.","type":"array","items":{"$ref":"GPUDevice"}},{"name":"auxAttributes","description":"An optional dictionary of additional GPU related attributes.","optional":true,"type":"object"},{"name":"featureStatus","description":"An optional dictionary of graphics features and their status.","optional":true,"type":"object"},{"name":"driverBugWorkarounds","description":"An optional array of GPU driver bug workarounds.","type":"array","items":{"type":"string"}},{"name":"videoDecoding","description":"Supported accelerated video decoding capabilities.","type":"array","items":{"$ref":"VideoDecodeAcceleratorCapability"}},{"name":"videoEncoding","description":"Supported accelerated video encoding capabilities.","type":"array","items":{"$ref":"VideoEncodeAcceleratorCapability"}},{"name":"imageDecoding","description":"Supported accelerated image decoding capabilities.","type":"array","items":{"$ref":"ImageDecodeAcceleratorCapability"}}]},{"id":"ProcessInfo","description":"Represents process info.","type":"object","properties":[{"name":"type","description":"Specifies process type.","type":"string"},{"name":"id","description":"Specifies process id.","type":"integer"},{"name":"cpuTime","description":"Specifies cumulative CPU usage in seconds across all threads of the\\nprocess since the process start.","type":"number"}]}],"commands":[{"name":"getInfo","description":"Returns information about the system.","returns":[{"name":"gpu","description":"Information about the GPUs on the system.","$ref":"GPUInfo"},{"name":"modelName","description":"A platform-dependent description of the model of the machine. On Mac OS, this is, for\\nexample, \'MacBookPro\'. Will be the empty string if not supported.","type":"string"},{"name":"modelVersion","description":"A platform-dependent description of the version of the machine. On Mac OS, this is, for\\nexample, \'10.1\'. Will be the empty string if not supported.","type":"string"},{"name":"commandLine","description":"The command line string used to launch the browser. Will be the empty string if not\\nsupported.","type":"string"}]},{"name":"getProcessInfo","description":"Returns information about all running processes.","returns":[{"name":"processInfo","description":"An array of process info blocks.","type":"array","items":{"$ref":"ProcessInfo"}}]}]},{"domain":"Target","description":"Supports additional targets discovery and allows to attach to them.","types":[{"id":"TargetID","type":"string"},{"id":"SessionID","description":"Unique identifier of attached debugging session.","type":"string"},{"id":"BrowserContextID","experimental":true,"type":"string"},{"id":"TargetInfo","type":"object","properties":[{"name":"targetId","$ref":"TargetID"},{"name":"type","type":"string"},{"name":"title","type":"string"},{"name":"url","type":"string"},{"name":"attached","description":"Whether the target has an attached client.","type":"boolean"},{"name":"openerId","description":"Opener target Id","optional":true,"$ref":"TargetID"},{"name":"browserContextId","experimental":true,"optional":true,"$ref":"BrowserContextID"}]},{"id":"RemoteLocation","experimental":true,"type":"object","properties":[{"name":"host","type":"string"},{"name":"port","type":"integer"}]}],"commands":[{"name":"activateTarget","description":"Activates (focuses) the target.","parameters":[{"name":"targetId","$ref":"TargetID"}]},{"name":"attachToTarget","description":"Attaches to the target with given id.","parameters":[{"name":"targetId","$ref":"TargetID"},{"name":"flatten","description":"Enables \\"flat\\" access to the session via specifying sessionId attribute in the commands.","experimental":true,"optional":true,"type":"boolean"}],"returns":[{"name":"sessionId","description":"Id assigned to the session.","$ref":"SessionID"}]},{"name":"attachToBrowserTarget","description":"Attaches to the browser target, only uses flat sessionId mode.","experimental":true,"returns":[{"name":"sessionId","description":"Id assigned to the session.","$ref":"SessionID"}]},{"name":"closeTarget","description":"Closes the target. If the target is a page that gets closed too.","parameters":[{"name":"targetId","$ref":"TargetID"}],"returns":[{"name":"success","type":"boolean"}]},{"name":"exposeDevToolsProtocol","description":"Inject object to the target\'s main frame that provides a communication\\nchannel with browser target.\\n\\nInjected object will be available as `window[bindingName]`.\\n\\nThe object has the follwing API:\\n- `binding.send(json)` - a method to send messages over the remote debugging protocol\\n- `binding.onmessage = json => handleMessage(json)` - a callback that will be called for the protocol notifications and command responses.","experimental":true,"parameters":[{"name":"targetId","$ref":"TargetID"},{"name":"bindingName","description":"Binding name, \'cdp\' if not specified.","optional":true,"type":"string"}]},{"name":"createBrowserContext","description":"Creates a new empty BrowserContext. Similar to an incognito profile but you can have more than\\none.","experimental":true,"returns":[{"name":"browserContextId","description":"The id of the context created.","$ref":"BrowserContextID"}]},{"name":"getBrowserContexts","description":"Returns all browser contexts created with `Target.createBrowserContext` method.","experimental":true,"returns":[{"name":"browserContextIds","description":"An array of browser context ids.","type":"array","items":{"$ref":"BrowserContextID"}}]},{"name":"createTarget","description":"Creates a new page.","parameters":[{"name":"url","description":"The initial URL the page will be navigated to.","type":"string"},{"name":"width","description":"Frame width in DIP (headless chrome only).","optional":true,"type":"integer"},{"name":"height","description":"Frame height in DIP (headless chrome only).","optional":true,"type":"integer"},{"name":"browserContextId","description":"The browser context to create the page in.","optional":true,"$ref":"BrowserContextID"},{"name":"enableBeginFrameControl","description":"Whether BeginFrames for this target will be controlled via DevTools (headless chrome only,\\nnot supported on MacOS yet, false by default).","experimental":true,"optional":true,"type":"boolean"},{"name":"newWindow","description":"Whether to create a new Window or Tab (chrome-only, false by default).","optional":true,"type":"boolean"},{"name":"background","description":"Whether to create the target in background or foreground (chrome-only,\\nfalse by default).","optional":true,"type":"boolean"}],"returns":[{"name":"targetId","description":"The id of the page opened.","$ref":"TargetID"}]},{"name":"detachFromTarget","description":"Detaches session with given id.","parameters":[{"name":"sessionId","description":"Session to detach.","optional":true,"$ref":"SessionID"},{"name":"targetId","description":"Deprecated.","deprecated":true,"optional":true,"$ref":"TargetID"}]},{"name":"disposeBrowserContext","description":"Deletes a BrowserContext. All the belonging pages will be closed without calling their\\nbeforeunload hooks.","experimental":true,"parameters":[{"name":"browserContextId","$ref":"BrowserContextID"}]},{"name":"getTargetInfo","description":"Returns information about a target.","experimental":true,"parameters":[{"name":"targetId","optional":true,"$ref":"TargetID"}],"returns":[{"name":"targetInfo","$ref":"TargetInfo"}]},{"name":"getTargets","description":"Retrieves a list of available targets.","returns":[{"name":"targetInfos","description":"The list of targets.","type":"array","items":{"$ref":"TargetInfo"}}]},{"name":"sendMessageToTarget","description":"Sends protocol message over session with given id.","parameters":[{"name":"message","type":"string"},{"name":"sessionId","description":"Identifier of the session.","optional":true,"$ref":"SessionID"},{"name":"targetId","description":"Deprecated.","deprecated":true,"optional":true,"$ref":"TargetID"}]},{"name":"setAutoAttach","description":"Controls whether to automatically attach to new targets which are considered to be related to\\nthis one. When turned on, attaches to all existing related targets as well. When turned off,\\nautomatically detaches from all currently attached targets.","experimental":true,"parameters":[{"name":"autoAttach","description":"Whether to auto-attach to related targets.","type":"boolean"},{"name":"waitForDebuggerOnStart","description":"Whether to pause new targets when attaching to them. Use `Runtime.runIfWaitingForDebugger`\\nto run paused targets.","type":"boolean"},{"name":"flatten","description":"Enables \\"flat\\" access to the session via specifying sessionId attribute in the commands.","experimental":true,"optional":true,"type":"boolean"}]},{"name":"setDiscoverTargets","description":"Controls whether to discover available targets and notify via\\n`targetCreated/targetInfoChanged/targetDestroyed` events.","parameters":[{"name":"discover","description":"Whether to discover available targets.","type":"boolean"}]},{"name":"setRemoteLocations","description":"Enables target discovery for the specified locations, when `setDiscoverTargets` was set to\\n`true`.","experimental":true,"parameters":[{"name":"locations","description":"List of remote locations.","type":"array","items":{"$ref":"RemoteLocation"}}]}],"events":[{"name":"attachedToTarget","description":"Issued when attached to target because of auto-attach or `attachToTarget` command.","experimental":true,"parameters":[{"name":"sessionId","description":"Identifier assigned to the session used to send/receive messages.","$ref":"SessionID"},{"name":"targetInfo","$ref":"TargetInfo"},{"name":"waitingForDebugger","type":"boolean"}]},{"name":"detachedFromTarget","description":"Issued when detached from target for any reason (including `detachFromTarget` command). Can be\\nissued multiple times per target if multiple sessions have been attached to it.","experimental":true,"parameters":[{"name":"sessionId","description":"Detached session identifier.","$ref":"SessionID"},{"name":"targetId","description":"Deprecated.","deprecated":true,"optional":true,"$ref":"TargetID"}]},{"name":"receivedMessageFromTarget","description":"Notifies about a new protocol message received from the session (as reported in\\n`attachedToTarget` event).","parameters":[{"name":"sessionId","description":"Identifier of a session which sends a message.","$ref":"SessionID"},{"name":"message","type":"string"},{"name":"targetId","description":"Deprecated.","deprecated":true,"optional":true,"$ref":"TargetID"}]},{"name":"targetCreated","description":"Issued when a possible inspection target is created.","parameters":[{"name":"targetInfo","$ref":"TargetInfo"}]},{"name":"targetDestroyed","description":"Issued when a target is destroyed.","parameters":[{"name":"targetId","$ref":"TargetID"}]},{"name":"targetCrashed","description":"Issued when a target has crashed.","parameters":[{"name":"targetId","$ref":"TargetID"},{"name":"status","description":"Termination status type.","type":"string"},{"name":"errorCode","description":"Termination error code.","type":"integer"}]},{"name":"targetInfoChanged","description":"Issued when some information about a target has changed. This only happens between\\n`targetCreated` and `targetDestroyed`.","parameters":[{"name":"targetInfo","$ref":"TargetInfo"}]}]},{"domain":"Tethering","description":"The Tethering domain defines methods and events for browser port binding.","experimental":true,"commands":[{"name":"bind","description":"Request browser port binding.","parameters":[{"name":"port","description":"Port number to bind.","type":"integer"}]},{"name":"unbind","description":"Request browser port unbinding.","parameters":[{"name":"port","description":"Port number to unbind.","type":"integer"}]}],"events":[{"name":"accepted","description":"Informs that port was successfully bound and got a specified connection id.","parameters":[{"name":"port","description":"Port number that was successfully bound.","type":"integer"},{"name":"connectionId","description":"Connection id to be used.","type":"string"}]}]},{"domain":"Tracing","experimental":true,"dependencies":["IO"],"types":[{"id":"MemoryDumpConfig","description":"Configuration for memory dump. Used only when \\"memory-infra\\" category is enabled.","type":"object"},{"id":"TraceConfig","type":"object","properties":[{"name":"recordMode","description":"Controls how the trace buffer stores data.","optional":true,"type":"string","enum":["recordUntilFull","recordContinuously","recordAsMuchAsPossible","echoToConsole"]},{"name":"enableSampling","description":"Turns on JavaScript stack sampling.","optional":true,"type":"boolean"},{"name":"enableSystrace","description":"Turns on system tracing.","optional":true,"type":"boolean"},{"name":"enableArgumentFilter","description":"Turns on argument filter.","optional":true,"type":"boolean"},{"name":"includedCategories","description":"Included category filters.","optional":true,"type":"array","items":{"type":"string"}},{"name":"excludedCategories","description":"Excluded category filters.","optional":true,"type":"array","items":{"type":"string"}},{"name":"syntheticDelays","description":"Configuration to synthesize the delays in tracing.","optional":true,"type":"array","items":{"type":"string"}},{"name":"memoryDumpConfig","description":"Configuration for memory dump triggers. Used only when \\"memory-infra\\" category is enabled.","optional":true,"$ref":"MemoryDumpConfig"}]},{"id":"StreamFormat","description":"Data format of a trace. Can be either the legacy JSON format or the\\nprotocol buffer format. Note that the JSON format will be deprecated soon.","type":"string","enum":["json","proto"]},{"id":"StreamCompression","description":"Compression type to use for traces returned via streams.","type":"string","enum":["none","gzip"]}],"commands":[{"name":"end","description":"Stop trace events collection."},{"name":"getCategories","description":"Gets supported tracing categories.","returns":[{"name":"categories","description":"A list of supported tracing categories.","type":"array","items":{"type":"string"}}]},{"name":"recordClockSyncMarker","description":"Record a clock sync marker in the trace.","parameters":[{"name":"syncId","description":"The ID of this clock sync marker","type":"string"}]},{"name":"requestMemoryDump","description":"Request a global memory dump.","returns":[{"name":"dumpGuid","description":"GUID of the resulting global memory dump.","type":"string"},{"name":"success","description":"True iff the global memory dump succeeded.","type":"boolean"}]},{"name":"start","description":"Start trace events collection.","parameters":[{"name":"categories","description":"Category/tag filter","deprecated":true,"optional":true,"type":"string"},{"name":"options","description":"Tracing options","deprecated":true,"optional":true,"type":"string"},{"name":"bufferUsageReportingInterval","description":"If set, the agent will issue bufferUsage events at this interval, specified in milliseconds","optional":true,"type":"number"},{"name":"transferMode","description":"Whether to report trace events as series of dataCollected events or to save trace to a\\nstream (defaults to `ReportEvents`).","optional":true,"type":"string","enum":["ReportEvents","ReturnAsStream"]},{"name":"streamFormat","description":"Trace data format to use. This only applies when using `ReturnAsStream`\\ntransfer mode (defaults to `json`).","optional":true,"$ref":"StreamFormat"},{"name":"streamCompression","description":"Compression format to use. This only applies when using `ReturnAsStream`\\ntransfer mode (defaults to `none`)","optional":true,"$ref":"StreamCompression"},{"name":"traceConfig","optional":true,"$ref":"TraceConfig"}]}],"events":[{"name":"bufferUsage","parameters":[{"name":"percentFull","description":"A number in range [0..1] that indicates the used size of event buffer as a fraction of its\\ntotal size.","optional":true,"type":"number"},{"name":"eventCount","description":"An approximate number of events in the trace log.","optional":true,"type":"number"},{"name":"value","description":"A number in range [0..1] that indicates the used size of event buffer as a fraction of its\\ntotal size.","optional":true,"type":"number"}]},{"name":"dataCollected","description":"Contains an bucket of collected trace events. When tracing is stopped collected events will be\\nsend as a sequence of dataCollected events followed by tracingComplete event.","parameters":[{"name":"value","type":"array","items":{"type":"object"}}]},{"name":"tracingComplete","description":"Signals that tracing is stopped and there is no trace buffers pending flush, all data were\\ndelivered via dataCollected events.","parameters":[{"name":"dataLossOccurred","description":"Indicates whether some trace data is known to have been lost, e.g. because the trace ring\\nbuffer wrapped around.","type":"boolean"},{"name":"stream","description":"A handle of the stream that holds resulting trace data.","optional":true,"$ref":"IO.StreamHandle"},{"name":"traceFormat","description":"Trace data format of returned stream.","optional":true,"$ref":"StreamFormat"},{"name":"streamCompression","description":"Compression format of returned stream.","optional":true,"$ref":"StreamCompression"}]}]},{"domain":"Fetch","description":"A domain for letting clients substitute browser\'s network layer with client code.","experimental":true,"dependencies":["Network","IO","Page"],"types":[{"id":"RequestId","description":"Unique request identifier.","type":"string"},{"id":"RequestStage","description":"Stages of the request to handle. Request will intercept before the request is\\nsent. Response will intercept after the response is received (but before response\\nbody is received.","experimental":true,"type":"string","enum":["Request","Response"]},{"id":"RequestPattern","experimental":true,"type":"object","properties":[{"name":"urlPattern","description":"Wildcards (\'*\' -> zero or more, \'?\' -> exactly one) are allowed. Escape character is\\nbackslash. Omitting is equivalent to \\"*\\".","optional":true,"type":"string"},{"name":"resourceType","description":"If set, only requests for matching resource types will be intercepted.","optional":true,"$ref":"Network.ResourceType"},{"name":"requestStage","description":"Stage at wich to begin intercepting requests. Default is Request.","optional":true,"$ref":"RequestStage"}]},{"id":"HeaderEntry","description":"Response HTTP header entry","type":"object","properties":[{"name":"name","type":"string"},{"name":"value","type":"string"}]},{"id":"AuthChallenge","description":"Authorization challenge for HTTP status code 401 or 407.","experimental":true,"type":"object","properties":[{"name":"source","description":"Source of the authentication challenge.","optional":true,"type":"string","enum":["Server","Proxy"]},{"name":"origin","description":"Origin of the challenger.","type":"string"},{"name":"scheme","description":"The authentication scheme used, such as basic or digest","type":"string"},{"name":"realm","description":"The realm of the challenge. May be empty.","type":"string"}]},{"id":"AuthChallengeResponse","description":"Response to an AuthChallenge.","experimental":true,"type":"object","properties":[{"name":"response","description":"The decision on what to do in response to the authorization challenge. Default means\\ndeferring to the default behavior of the net stack, which will likely either the Cancel\\nauthentication or display a popup dialog box.","type":"string","enum":["Default","CancelAuth","ProvideCredentials"]},{"name":"username","description":"The username to provide, possibly empty. Should only be set if response is\\nProvideCredentials.","optional":true,"type":"string"},{"name":"password","description":"The password to provide, possibly empty. Should only be set if response is\\nProvideCredentials.","optional":true,"type":"string"}]}],"commands":[{"name":"disable","description":"Disables the fetch domain."},{"name":"enable","description":"Enables issuing of requestPaused events. A request will be paused until client\\ncalls one of failRequest, fulfillRequest or continueRequest/continueWithAuth.","parameters":[{"name":"patterns","description":"If specified, only requests matching any of these patterns will produce\\nfetchRequested event and will be paused until clients response. If not set,\\nall requests will be affected.","optional":true,"type":"array","items":{"$ref":"RequestPattern"}},{"name":"handleAuthRequests","description":"If true, authRequired events will be issued and requests will be paused\\nexpecting a call to continueWithAuth.","optional":true,"type":"boolean"}]},{"name":"failRequest","description":"Causes the request to fail with specified reason.","parameters":[{"name":"requestId","description":"An id the client received in requestPaused event.","$ref":"RequestId"},{"name":"errorReason","description":"Causes the request to fail with the given reason.","$ref":"Network.ErrorReason"}]},{"name":"fulfillRequest","description":"Provides response to the request.","parameters":[{"name":"requestId","description":"An id the client received in requestPaused event.","$ref":"RequestId"},{"name":"responseCode","description":"An HTTP response code.","type":"integer"},{"name":"responseHeaders","description":"Response headers.","type":"array","items":{"$ref":"HeaderEntry"}},{"name":"body","description":"A response body.","optional":true,"type":"string"},{"name":"responsePhrase","description":"A textual representation of responseCode.\\nIf absent, a standard phrase mathcing responseCode is used.","optional":true,"type":"string"}]},{"name":"continueRequest","description":"Continues the request, optionally modifying some of its parameters.","parameters":[{"name":"requestId","description":"An id the client received in requestPaused event.","$ref":"RequestId"},{"name":"url","description":"If set, the request url will be modified in a way that\'s not observable by page.","optional":true,"type":"string"},{"name":"method","description":"If set, the request method is overridden.","optional":true,"type":"string"},{"name":"postData","description":"If set, overrides the post data in the request.","optional":true,"type":"string"},{"name":"headers","description":"If set, overrides the request headrts.","optional":true,"type":"array","items":{"$ref":"HeaderEntry"}}]},{"name":"continueWithAuth","description":"Continues a request supplying authChallengeResponse following authRequired event.","parameters":[{"name":"requestId","description":"An id the client received in authRequired event.","$ref":"RequestId"},{"name":"authChallengeResponse","description":"Response to with an authChallenge.","$ref":"AuthChallengeResponse"}]},{"name":"getResponseBody","description":"Causes the body of the response to be received from the server and\\nreturned as a single string. May only be issued for a request that\\nis paused in the Response stage and is mutually exclusive with\\ntakeResponseBodyForInterceptionAsStream. Calling other methods that\\naffect the request or disabling fetch domain before body is received\\nresults in an undefined behavior.","parameters":[{"name":"requestId","description":"Identifier for the intercepted request to get body for.","$ref":"RequestId"}],"returns":[{"name":"body","description":"Response body.","type":"string"},{"name":"base64Encoded","description":"True, if content was sent as base64.","type":"boolean"}]},{"name":"takeResponseBodyAsStream","description":"Returns a handle to the stream representing the response body.\\nThe request must be paused in the HeadersReceived stage.\\nNote that after this command the request can\'t be continued\\nas is -- client either needs to cancel it or to provide the\\nresponse body.\\nThe stream only supports sequential read, IO.read will fail if the position\\nis specified.\\nThis method is mutually exclusive with getResponseBody.\\nCalling other methods that affect the request or disabling fetch\\ndomain before body is received results in an undefined behavior.","parameters":[{"name":"requestId","$ref":"RequestId"}],"returns":[{"name":"stream","$ref":"IO.StreamHandle"}]}],"events":[{"name":"requestPaused","description":"Issued when the domain is enabled and the request URL matches the\\nspecified filter. The request is paused until the client responds\\nwith one of continueRequest, failRequest or fulfillRequest.\\nThe stage of the request can be determined by presence of responseErrorReason\\nand responseStatusCode -- the request is at the response stage if either\\nof these fields is present and in the request stage otherwise.","parameters":[{"name":"requestId","description":"Each request the page makes will have a unique id.","$ref":"RequestId"},{"name":"request","description":"The details of the request.","$ref":"Network.Request"},{"name":"frameId","description":"The id of the frame that initiated the request.","$ref":"Page.FrameId"},{"name":"resourceType","description":"How the requested resource will be used.","$ref":"Network.ResourceType"},{"name":"responseErrorReason","description":"Response error if intercepted at response stage.","optional":true,"$ref":"Network.ErrorReason"},{"name":"responseStatusCode","description":"Response code if intercepted at response stage.","optional":true,"type":"integer"},{"name":"responseHeaders","description":"Response headers if intercepted at the response stage.","optional":true,"type":"array","items":{"$ref":"HeaderEntry"}},{"name":"networkId","description":"If the intercepted request had a corresponding Network.requestWillBeSent event fired for it,\\nthen this networkId will be the same as the requestId present in the requestWillBeSent event.","optional":true,"$ref":"RequestId"}]},{"name":"authRequired","description":"Issued when the domain is enabled with handleAuthRequests set to true.\\nThe request is paused until client responds with continueWithAuth.","parameters":[{"name":"requestId","description":"Each request the page makes will have a unique id.","$ref":"RequestId"},{"name":"request","description":"The details of the request.","$ref":"Network.Request"},{"name":"frameId","description":"The id of the frame that initiated the request.","$ref":"Page.FrameId"},{"name":"resourceType","description":"How the requested resource will be used.","$ref":"Network.ResourceType"},{"name":"authChallenge","description":"Details of the Authorization Challenge encountered.\\nIf this is set, client should respond with continueRequest that\\ncontains AuthChallengeResponse.","$ref":"AuthChallenge"}]}]},{"domain":"WebAudio","description":"This domain allows inspection of Web Audio API.\\nhttps://webaudio.github.io/web-audio-api/","experimental":true,"types":[{"id":"ContextId","description":"Context\'s UUID in string","type":"string"},{"id":"ContextType","description":"Enum of BaseAudioContext types","type":"string","enum":["realtime","offline"]},{"id":"ContextState","description":"Enum of AudioContextState from the spec","type":"string","enum":["suspended","running","closed"]},{"id":"ContextRealtimeData","description":"Fields in AudioContext that change in real-time.","type":"object","properties":[{"name":"currentTime","description":"The current context time in second in BaseAudioContext.","type":"number"},{"name":"renderCapacity","description":"The time spent on rendering graph divided by render qunatum duration,\\nand multiplied by 100. 100 means the audio renderer reached the full\\ncapacity and glitch may occur.","type":"number"},{"name":"callbackIntervalMean","description":"A running mean of callback interval.","type":"number"},{"name":"callbackIntervalVariance","description":"A running variance of callback interval.","type":"number"}]},{"id":"BaseAudioContext","description":"Protocol object for BaseAudioContext","type":"object","properties":[{"name":"contextId","$ref":"ContextId"},{"name":"contextType","$ref":"ContextType"},{"name":"contextState","$ref":"ContextState"},{"name":"realtimeData","optional":true,"$ref":"ContextRealtimeData"},{"name":"callbackBufferSize","description":"Platform-dependent callback buffer size.","type":"number"},{"name":"maxOutputChannelCount","description":"Number of output channels supported by audio hardware in use.","type":"number"},{"name":"sampleRate","description":"Context sample rate.","type":"number"}]}],"commands":[{"name":"enable","description":"Enables the WebAudio domain and starts sending context lifetime events."},{"name":"disable","description":"Disables the WebAudio domain."},{"name":"getRealtimeData","description":"Fetch the realtime data from the registered contexts.","parameters":[{"name":"contextId","$ref":"ContextId"}],"returns":[{"name":"realtimeData","$ref":"ContextRealtimeData"}]}],"events":[{"name":"contextCreated","description":"Notifies that a new BaseAudioContext has been created.","parameters":[{"name":"context","$ref":"BaseAudioContext"}]},{"name":"contextDestroyed","description":"Notifies that existing BaseAudioContext has been destroyed.","parameters":[{"name":"contextId","$ref":"ContextId"}]},{"name":"contextChanged","description":"Notifies that existing BaseAudioContext has changed some properties (id stays the same)..","parameters":[{"name":"context","$ref":"BaseAudioContext"}]}]},{"domain":"WebAuthn","description":"This domain allows configuring virtual authenticators to test the WebAuthn\\nAPI.","experimental":true,"types":[{"id":"AuthenticatorId","type":"string"},{"id":"AuthenticatorProtocol","type":"string","enum":["u2f","ctap2"]},{"id":"AuthenticatorTransport","type":"string","enum":["usb","nfc","ble","cable","internal"]},{"id":"VirtualAuthenticatorOptions","type":"object","properties":[{"name":"protocol","$ref":"AuthenticatorProtocol"},{"name":"transport","$ref":"AuthenticatorTransport"},{"name":"hasResidentKey","type":"boolean"},{"name":"hasUserVerification","type":"boolean"},{"name":"automaticPresenceSimulation","description":"If set to true, tests of user presence will succeed immediately.\\nOtherwise, they will not be resolved. Defaults to true.","optional":true,"type":"boolean"}]},{"id":"Credential","type":"object","properties":[{"name":"credentialId","type":"string"},{"name":"rpIdHash","description":"SHA-256 hash of the Relying Party ID the credential is scoped to. Must\\nbe 32 bytes long.\\nSee https://w3c.github.io/webauthn/#rpidhash","type":"string"},{"name":"privateKey","description":"The private key in PKCS#8 format.","type":"string"},{"name":"signCount","description":"Signature counter. This is incremented by one for each successful\\nassertion.\\nSee https://w3c.github.io/webauthn/#signature-counter","type":"integer"}]}],"commands":[{"name":"enable","description":"Enable the WebAuthn domain and start intercepting credential storage and\\nretrieval with a virtual authenticator."},{"name":"disable","description":"Disable the WebAuthn domain."},{"name":"addVirtualAuthenticator","description":"Creates and adds a virtual authenticator.","parameters":[{"name":"options","$ref":"VirtualAuthenticatorOptions"}],"returns":[{"name":"authenticatorId","$ref":"AuthenticatorId"}]},{"name":"removeVirtualAuthenticator","description":"Removes the given authenticator.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"}]},{"name":"addCredential","description":"Adds the credential to the specified authenticator.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"credential","$ref":"Credential"}]},{"name":"getCredentials","description":"Returns all the credentials stored in the given virtual authenticator.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"}],"returns":[{"name":"credentials","type":"array","items":{"$ref":"Credential"}}]},{"name":"clearCredentials","description":"Clears all the credentials from the specified device.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"}]},{"name":"setUserVerified","description":"Sets whether User Verification succeeds or fails for an authenticator.\\nThe default is true.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"isUserVerified","type":"boolean"}]}]},{"domain":"Console","description":"This domain is deprecated - use Runtime or Log instead.","deprecated":true,"dependencies":["Runtime"],"types":[{"id":"ConsoleMessage","description":"Console message.","type":"object","properties":[{"name":"source","description":"Message source.","type":"string","enum":["xml","javascript","network","console-api","storage","appcache","rendering","security","other","deprecation","worker"]},{"name":"level","description":"Message severity.","type":"string","enum":["log","warning","error","debug","info"]},{"name":"text","description":"Message text.","type":"string"},{"name":"url","description":"URL of the message origin.","optional":true,"type":"string"},{"name":"line","description":"Line number in the resource that generated this message (1-based).","optional":true,"type":"integer"},{"name":"column","description":"Column number in the resource that generated this message (1-based).","optional":true,"type":"integer"}]}],"commands":[{"name":"clearMessages","description":"Does nothing."},{"name":"disable","description":"Disables console domain, prevents further console messages from being reported to the client."},{"name":"enable","description":"Enables console domain, sends the messages collected so far to the client by means of the\\n`messageAdded` notification."}],"events":[{"name":"messageAdded","description":"Issued when new console message is added.","parameters":[{"name":"message","description":"Console message that has been added.","$ref":"ConsoleMessage"}]}]},{"domain":"Debugger","description":"Debugger domain exposes JavaScript debugging capabilities. It allows setting and removing\\nbreakpoints, stepping through execution, exploring stack traces, etc.","dependencies":["Runtime"],"types":[{"id":"BreakpointId","description":"Breakpoint identifier.","type":"string"},{"id":"CallFrameId","description":"Call frame identifier.","type":"string"},{"id":"Location","description":"Location in the source code.","type":"object","properties":[{"name":"scriptId","description":"Script identifier as reported in the `Debugger.scriptParsed`.","$ref":"Runtime.ScriptId"},{"name":"lineNumber","description":"Line number in the script (0-based).","type":"integer"},{"name":"columnNumber","description":"Column number in the script (0-based).","optional":true,"type":"integer"}]},{"id":"ScriptPosition","description":"Location in the source code.","experimental":true,"type":"object","properties":[{"name":"lineNumber","type":"integer"},{"name":"columnNumber","type":"integer"}]},{"id":"CallFrame","description":"JavaScript call frame. Array of call frames form the call stack.","type":"object","properties":[{"name":"callFrameId","description":"Call frame identifier. This identifier is only valid while the virtual machine is paused.","$ref":"CallFrameId"},{"name":"functionName","description":"Name of the JavaScript function called on this call frame.","type":"string"},{"name":"functionLocation","description":"Location in the source code.","optional":true,"$ref":"Location"},{"name":"location","description":"Location in the source code.","$ref":"Location"},{"name":"url","description":"JavaScript script name or url.","type":"string"},{"name":"scopeChain","description":"Scope chain for this call frame.","type":"array","items":{"$ref":"Scope"}},{"name":"this","description":"`this` object for this call frame.","$ref":"Runtime.RemoteObject"},{"name":"returnValue","description":"The value being returned, if the function is at return point.","optional":true,"$ref":"Runtime.RemoteObject"}]},{"id":"Scope","description":"Scope description.","type":"object","properties":[{"name":"type","description":"Scope type.","type":"string","enum":["global","local","with","closure","catch","block","script","eval","module"]},{"name":"object","description":"Object representing the scope. For `global` and `with` scopes it represents the actual\\nobject; for the rest of the scopes, it is artificial transient object enumerating scope\\nvariables as its properties.","$ref":"Runtime.RemoteObject"},{"name":"name","optional":true,"type":"string"},{"name":"startLocation","description":"Location in the source code where scope starts","optional":true,"$ref":"Location"},{"name":"endLocation","description":"Location in the source code where scope ends","optional":true,"$ref":"Location"}]},{"id":"SearchMatch","description":"Search match for resource.","type":"object","properties":[{"name":"lineNumber","description":"Line number in resource content.","type":"number"},{"name":"lineContent","description":"Line with match content.","type":"string"}]},{"id":"BreakLocation","type":"object","properties":[{"name":"scriptId","description":"Script identifier as reported in the `Debugger.scriptParsed`.","$ref":"Runtime.ScriptId"},{"name":"lineNumber","description":"Line number in the script (0-based).","type":"integer"},{"name":"columnNumber","description":"Column number in the script (0-based).","optional":true,"type":"integer"},{"name":"type","optional":true,"type":"string","enum":["debuggerStatement","call","return"]}]}],"commands":[{"name":"continueToLocation","description":"Continues execution until specific location is reached.","parameters":[{"name":"location","description":"Location to continue to.","$ref":"Location"},{"name":"targetCallFrames","optional":true,"type":"string","enum":["any","current"]}]},{"name":"disable","description":"Disables debugger for given page."},{"name":"enable","description":"Enables debugger for the given page. Clients should not assume that the debugging has been\\nenabled until the result for this command is received.","parameters":[{"name":"maxScriptsCacheSize","description":"The maximum size in bytes of collected scripts (not referenced by other heap objects)\\nthe debugger can hold. Puts no limit if paramter is omitted.","experimental":true,"optional":true,"type":"number"}],"returns":[{"name":"debuggerId","description":"Unique identifier of the debugger.","experimental":true,"$ref":"Runtime.UniqueDebuggerId"}]},{"name":"evaluateOnCallFrame","description":"Evaluates expression on a given call frame.","parameters":[{"name":"callFrameId","description":"Call frame identifier to evaluate on.","$ref":"CallFrameId"},{"name":"expression","description":"Expression to evaluate.","type":"string"},{"name":"objectGroup","description":"String object group name to put result into (allows rapid releasing resulting object handles\\nusing `releaseObjectGroup`).","optional":true,"type":"string"},{"name":"includeCommandLineAPI","description":"Specifies whether command line API should be available to the evaluated expression, defaults\\nto false.","optional":true,"type":"boolean"},{"name":"silent","description":"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.","optional":true,"type":"boolean"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object that should be sent by value.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","experimental":true,"optional":true,"type":"boolean"},{"name":"throwOnSideEffect","description":"Whether to throw an exception if side effect cannot be ruled out during evaluation.","optional":true,"type":"boolean"},{"name":"timeout","description":"Terminate execution after timing out (number of milliseconds).","experimental":true,"optional":true,"$ref":"Runtime.TimeDelta"}],"returns":[{"name":"result","description":"Object wrapper for the evaluation result.","$ref":"Runtime.RemoteObject"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"Runtime.ExceptionDetails"}]},{"name":"getPossibleBreakpoints","description":"Returns possible locations for breakpoint. scriptId in start and end range locations should be\\nthe same.","parameters":[{"name":"start","description":"Start of range to search possible breakpoint locations in.","$ref":"Location"},{"name":"end","description":"End of range to search possible breakpoint locations in (excluding). When not specified, end\\nof scripts is used as end of range.","optional":true,"$ref":"Location"},{"name":"restrictToFunction","description":"Only consider locations which are in the same (non-nested) function as start.","optional":true,"type":"boolean"}],"returns":[{"name":"locations","description":"List of the possible breakpoint locations.","type":"array","items":{"$ref":"BreakLocation"}}]},{"name":"getScriptSource","description":"Returns source for the script with given id.","parameters":[{"name":"scriptId","description":"Id of the script to get source for.","$ref":"Runtime.ScriptId"}],"returns":[{"name":"scriptSource","description":"Script source.","type":"string"}]},{"name":"getStackTrace","description":"Returns stack trace with given `stackTraceId`.","experimental":true,"parameters":[{"name":"stackTraceId","$ref":"Runtime.StackTraceId"}],"returns":[{"name":"stackTrace","$ref":"Runtime.StackTrace"}]},{"name":"pause","description":"Stops on the next JavaScript statement."},{"name":"pauseOnAsyncCall","experimental":true,"parameters":[{"name":"parentStackTraceId","description":"Debugger will pause when async call with given stack trace is started.","$ref":"Runtime.StackTraceId"}]},{"name":"removeBreakpoint","description":"Removes JavaScript breakpoint.","parameters":[{"name":"breakpointId","$ref":"BreakpointId"}]},{"name":"restartFrame","description":"Restarts particular call frame from the beginning.","parameters":[{"name":"callFrameId","description":"Call frame identifier to evaluate on.","$ref":"CallFrameId"}],"returns":[{"name":"callFrames","description":"New stack trace.","type":"array","items":{"$ref":"CallFrame"}},{"name":"asyncStackTrace","description":"Async stack trace, if any.","optional":true,"$ref":"Runtime.StackTrace"},{"name":"asyncStackTraceId","description":"Async stack trace, if any.","experimental":true,"optional":true,"$ref":"Runtime.StackTraceId"}]},{"name":"resume","description":"Resumes JavaScript execution."},{"name":"searchInContent","description":"Searches for given string in script content.","parameters":[{"name":"scriptId","description":"Id of the script to search in.","$ref":"Runtime.ScriptId"},{"name":"query","description":"String to search for.","type":"string"},{"name":"caseSensitive","description":"If true, search is case sensitive.","optional":true,"type":"boolean"},{"name":"isRegex","description":"If true, treats string parameter as regex.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"List of search matches.","type":"array","items":{"$ref":"SearchMatch"}}]},{"name":"setAsyncCallStackDepth","description":"Enables or disables async call stacks tracking.","parameters":[{"name":"maxDepth","description":"Maximum depth of async call stacks. Setting to `0` will effectively disable collecting async\\ncall stacks (default).","type":"integer"}]},{"name":"setBlackboxPatterns","description":"Replace previous blackbox patterns with passed ones. Forces backend to skip stepping/pausing in\\nscripts with url matching one of the patterns. VM will try to leave blackboxed script by\\nperforming \'step in\' several times, finally resorting to \'step out\' if unsuccessful.","experimental":true,"parameters":[{"name":"patterns","description":"Array of regexps that will be used to check script url for blackbox state.","type":"array","items":{"type":"string"}}]},{"name":"setBlackboxedRanges","description":"Makes backend skip steps in the script in blackboxed ranges. VM will try leave blacklisted\\nscripts by performing \'step in\' several times, finally resorting to \'step out\' if unsuccessful.\\nPositions array contains positions where blackbox state is changed. First interval isn\'t\\nblackboxed. Array should be sorted.","experimental":true,"parameters":[{"name":"scriptId","description":"Id of the script.","$ref":"Runtime.ScriptId"},{"name":"positions","type":"array","items":{"$ref":"ScriptPosition"}}]},{"name":"setBreakpoint","description":"Sets JavaScript breakpoint at a given location.","parameters":[{"name":"location","description":"Location to set breakpoint in.","$ref":"Location"},{"name":"condition","description":"Expression to use as a breakpoint condition. When specified, debugger will only stop on the\\nbreakpoint if this expression evaluates to true.","optional":true,"type":"string"}],"returns":[{"name":"breakpointId","description":"Id of the created breakpoint for further reference.","$ref":"BreakpointId"},{"name":"actualLocation","description":"Location this breakpoint resolved into.","$ref":"Location"}]},{"name":"setInstrumentationBreakpoint","description":"Sets instrumentation breakpoint.","parameters":[{"name":"instrumentation","description":"Instrumentation name.","type":"string","enum":["beforeScriptExecution","beforeScriptWithSourceMapExecution"]}],"returns":[{"name":"breakpointId","description":"Id of the created breakpoint for further reference.","$ref":"BreakpointId"}]},{"name":"setBreakpointByUrl","description":"Sets JavaScript breakpoint at given location specified either by URL or URL regex. Once this\\ncommand is issued, all existing parsed scripts will have breakpoints resolved and returned in\\n`locations` property. Further matching script parsing will result in subsequent\\n`breakpointResolved` events issued. This logical breakpoint will survive page reloads.","parameters":[{"name":"lineNumber","description":"Line number to set breakpoint at.","type":"integer"},{"name":"url","description":"URL of the resources to set breakpoint on.","optional":true,"type":"string"},{"name":"urlRegex","description":"Regex pattern for the URLs of the resources to set breakpoints on. Either `url` or\\n`urlRegex` must be specified.","optional":true,"type":"string"},{"name":"scriptHash","description":"Script hash of the resources to set breakpoint on.","optional":true,"type":"string"},{"name":"columnNumber","description":"Offset in the line to set breakpoint at.","optional":true,"type":"integer"},{"name":"condition","description":"Expression to use as a breakpoint condition. When specified, debugger will only stop on the\\nbreakpoint if this expression evaluates to true.","optional":true,"type":"string"}],"returns":[{"name":"breakpointId","description":"Id of the created breakpoint for further reference.","$ref":"BreakpointId"},{"name":"locations","description":"List of the locations this breakpoint resolved into upon addition.","type":"array","items":{"$ref":"Location"}}]},{"name":"setBreakpointOnFunctionCall","description":"Sets JavaScript breakpoint before each call to the given function.\\nIf another function was created from the same source as a given one,\\ncalling it will also trigger the breakpoint.","experimental":true,"parameters":[{"name":"objectId","description":"Function object id.","$ref":"Runtime.RemoteObjectId"},{"name":"condition","description":"Expression to use as a breakpoint condition. When specified, debugger will\\nstop on the breakpoint if this expression evaluates to true.","optional":true,"type":"string"}],"returns":[{"name":"breakpointId","description":"Id of the created breakpoint for further reference.","$ref":"BreakpointId"}]},{"name":"setBreakpointsActive","description":"Activates / deactivates all breakpoints on the page.","parameters":[{"name":"active","description":"New value for breakpoints active state.","type":"boolean"}]},{"name":"setPauseOnExceptions","description":"Defines pause on exceptions state. Can be set to stop on all exceptions, uncaught exceptions or\\nno exceptions. Initial pause on exceptions state is `none`.","parameters":[{"name":"state","description":"Pause on exceptions mode.","type":"string","enum":["none","uncaught","all"]}]},{"name":"setReturnValue","description":"Changes return value in top frame. Available only at return break position.","experimental":true,"parameters":[{"name":"newValue","description":"New return value.","$ref":"Runtime.CallArgument"}]},{"name":"setScriptSource","description":"Edits JavaScript source live.","parameters":[{"name":"scriptId","description":"Id of the script to edit.","$ref":"Runtime.ScriptId"},{"name":"scriptSource","description":"New content of the script.","type":"string"},{"name":"dryRun","description":"If true the change will not actually be applied. Dry run may be used to get result\\ndescription without actually modifying the code.","optional":true,"type":"boolean"}],"returns":[{"name":"callFrames","description":"New stack trace in case editing has happened while VM was stopped.","optional":true,"type":"array","items":{"$ref":"CallFrame"}},{"name":"stackChanged","description":"Whether current call stack was modified after applying the changes.","optional":true,"type":"boolean"},{"name":"asyncStackTrace","description":"Async stack trace, if any.","optional":true,"$ref":"Runtime.StackTrace"},{"name":"asyncStackTraceId","description":"Async stack trace, if any.","experimental":true,"optional":true,"$ref":"Runtime.StackTraceId"},{"name":"exceptionDetails","description":"Exception details if any.","optional":true,"$ref":"Runtime.ExceptionDetails"}]},{"name":"setSkipAllPauses","description":"Makes page not interrupt on any pauses (breakpoint, exception, dom exception etc).","parameters":[{"name":"skip","description":"New value for skip pauses state.","type":"boolean"}]},{"name":"setVariableValue","description":"Changes value of variable in a callframe. Object-based scopes are not supported and must be\\nmutated manually.","parameters":[{"name":"scopeNumber","description":"0-based number of scope as was listed in scope chain. Only \'local\', \'closure\' and \'catch\'\\nscope types are allowed. Other scopes could be manipulated manually.","type":"integer"},{"name":"variableName","description":"Variable name.","type":"string"},{"name":"newValue","description":"New variable value.","$ref":"Runtime.CallArgument"},{"name":"callFrameId","description":"Id of callframe that holds variable.","$ref":"CallFrameId"}]},{"name":"stepInto","description":"Steps into the function call.","parameters":[{"name":"breakOnAsyncCall","description":"Debugger will issue additional Debugger.paused notification if any async task is scheduled\\nbefore next pause.","experimental":true,"optional":true,"type":"boolean"}]},{"name":"stepOut","description":"Steps out of the function call."},{"name":"stepOver","description":"Steps over the statement."}],"events":[{"name":"breakpointResolved","description":"Fired when breakpoint is resolved to an actual script and location.","parameters":[{"name":"breakpointId","description":"Breakpoint unique identifier.","$ref":"BreakpointId"},{"name":"location","description":"Actual breakpoint location.","$ref":"Location"}]},{"name":"paused","description":"Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria.","parameters":[{"name":"callFrames","description":"Call stack the virtual machine stopped on.","type":"array","items":{"$ref":"CallFrame"}},{"name":"reason","description":"Pause reason.","type":"string","enum":["ambiguous","assert","debugCommand","DOM","EventListener","exception","instrumentation","OOM","other","promiseRejection","XHR"]},{"name":"data","description":"Object containing break-specific auxiliary properties.","optional":true,"type":"object"},{"name":"hitBreakpoints","description":"Hit breakpoints IDs","optional":true,"type":"array","items":{"type":"string"}},{"name":"asyncStackTrace","description":"Async stack trace, if any.","optional":true,"$ref":"Runtime.StackTrace"},{"name":"asyncStackTraceId","description":"Async stack trace, if any.","experimental":true,"optional":true,"$ref":"Runtime.StackTraceId"},{"name":"asyncCallStackTraceId","description":"Just scheduled async call will have this stack trace as parent stack during async execution.\\nThis field is available only after `Debugger.stepInto` call with `breakOnAsynCall` flag.","experimental":true,"optional":true,"$ref":"Runtime.StackTraceId"}]},{"name":"resumed","description":"Fired when the virtual machine resumed execution."},{"name":"scriptFailedToParse","description":"Fired when virtual machine fails to parse the script.","parameters":[{"name":"scriptId","description":"Identifier of the script parsed.","$ref":"Runtime.ScriptId"},{"name":"url","description":"URL or name of the script parsed (if any).","type":"string"},{"name":"startLine","description":"Line offset of the script within the resource with given URL (for script tags).","type":"integer"},{"name":"startColumn","description":"Column offset of the script within the resource with given URL.","type":"integer"},{"name":"endLine","description":"Last line of the script.","type":"integer"},{"name":"endColumn","description":"Length of the last line of the script.","type":"integer"},{"name":"executionContextId","description":"Specifies script creation context.","$ref":"Runtime.ExecutionContextId"},{"name":"hash","description":"Content hash of the script.","type":"string"},{"name":"executionContextAuxData","description":"Embedder-specific auxiliary data.","optional":true,"type":"object"},{"name":"sourceMapURL","description":"URL of source map associated with script (if any).","optional":true,"type":"string"},{"name":"hasSourceURL","description":"True, if this script has sourceURL.","optional":true,"type":"boolean"},{"name":"isModule","description":"True, if this script is ES6 module.","optional":true,"type":"boolean"},{"name":"length","description":"This script length.","optional":true,"type":"integer"},{"name":"stackTrace","description":"JavaScript top stack frame of where the script parsed event was triggered if available.","experimental":true,"optional":true,"$ref":"Runtime.StackTrace"}]},{"name":"scriptParsed","description":"Fired when virtual machine parses script. This event is also fired for all known and uncollected\\nscripts upon enabling debugger.","parameters":[{"name":"scriptId","description":"Identifier of the script parsed.","$ref":"Runtime.ScriptId"},{"name":"url","description":"URL or name of the script parsed (if any).","type":"string"},{"name":"startLine","description":"Line offset of the script within the resource with given URL (for script tags).","type":"integer"},{"name":"startColumn","description":"Column offset of the script within the resource with given URL.","type":"integer"},{"name":"endLine","description":"Last line of the script.","type":"integer"},{"name":"endColumn","description":"Length of the last line of the script.","type":"integer"},{"name":"executionContextId","description":"Specifies script creation context.","$ref":"Runtime.ExecutionContextId"},{"name":"hash","description":"Content hash of the script.","type":"string"},{"name":"executionContextAuxData","description":"Embedder-specific auxiliary data.","optional":true,"type":"object"},{"name":"isLiveEdit","description":"True, if this script is generated as a result of the live edit operation.","experimental":true,"optional":true,"type":"boolean"},{"name":"sourceMapURL","description":"URL of source map associated with script (if any).","optional":true,"type":"string"},{"name":"hasSourceURL","description":"True, if this script has sourceURL.","optional":true,"type":"boolean"},{"name":"isModule","description":"True, if this script is ES6 module.","optional":true,"type":"boolean"},{"name":"length","description":"This script length.","optional":true,"type":"integer"},{"name":"stackTrace","description":"JavaScript top stack frame of where the script parsed event was triggered if available.","experimental":true,"optional":true,"$ref":"Runtime.StackTrace"}]}]},{"domain":"HeapProfiler","experimental":true,"dependencies":["Runtime"],"types":[{"id":"HeapSnapshotObjectId","description":"Heap snapshot object id.","type":"string"},{"id":"SamplingHeapProfileNode","description":"Sampling Heap Profile node. Holds callsite information, allocation statistics and child nodes.","type":"object","properties":[{"name":"callFrame","description":"Function location.","$ref":"Runtime.CallFrame"},{"name":"selfSize","description":"Allocations size in bytes for the node excluding children.","type":"number"},{"name":"id","description":"Node id. Ids are unique across all profiles collected between startSampling and stopSampling.","type":"integer"},{"name":"children","description":"Child nodes.","type":"array","items":{"$ref":"SamplingHeapProfileNode"}}]},{"id":"SamplingHeapProfileSample","description":"A single sample from a sampling profile.","type":"object","properties":[{"name":"size","description":"Allocation size in bytes attributed to the sample.","type":"number"},{"name":"nodeId","description":"Id of the corresponding profile tree node.","type":"integer"},{"name":"ordinal","description":"Time-ordered sample ordinal number. It is unique across all profiles retrieved\\nbetween startSampling and stopSampling.","type":"number"}]},{"id":"SamplingHeapProfile","description":"Sampling profile.","type":"object","properties":[{"name":"head","$ref":"SamplingHeapProfileNode"},{"name":"samples","type":"array","items":{"$ref":"SamplingHeapProfileSample"}}]}],"commands":[{"name":"addInspectedHeapObject","description":"Enables console to refer to the node with given id via $x (see Command Line API for more details\\n$x functions).","parameters":[{"name":"heapObjectId","description":"Heap snapshot object id to be accessible by means of $x command line API.","$ref":"HeapSnapshotObjectId"}]},{"name":"collectGarbage"},{"name":"disable"},{"name":"enable"},{"name":"getHeapObjectId","parameters":[{"name":"objectId","description":"Identifier of the object to get heap object id for.","$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"heapSnapshotObjectId","description":"Id of the heap snapshot object corresponding to the passed remote object id.","$ref":"HeapSnapshotObjectId"}]},{"name":"getObjectByHeapObjectId","parameters":[{"name":"objectId","$ref":"HeapSnapshotObjectId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects.","optional":true,"type":"string"}],"returns":[{"name":"result","description":"Evaluation result.","$ref":"Runtime.RemoteObject"}]},{"name":"getSamplingProfile","returns":[{"name":"profile","description":"Return the sampling profile being collected.","$ref":"SamplingHeapProfile"}]},{"name":"startSampling","parameters":[{"name":"samplingInterval","description":"Average sample interval in bytes. Poisson distribution is used for the intervals. The\\ndefault value is 32768 bytes.","optional":true,"type":"number"}]},{"name":"startTrackingHeapObjects","parameters":[{"name":"trackAllocations","optional":true,"type":"boolean"}]},{"name":"stopSampling","returns":[{"name":"profile","description":"Recorded sampling heap profile.","$ref":"SamplingHeapProfile"}]},{"name":"stopTrackingHeapObjects","parameters":[{"name":"reportProgress","description":"If true \'reportHeapSnapshotProgress\' events will be generated while snapshot is being taken\\nwhen the tracking is stopped.","optional":true,"type":"boolean"}]},{"name":"takeHeapSnapshot","parameters":[{"name":"reportProgress","description":"If true \'reportHeapSnapshotProgress\' events will be generated while snapshot is being taken.","optional":true,"type":"boolean"}]}],"events":[{"name":"addHeapSnapshotChunk","parameters":[{"name":"chunk","type":"string"}]},{"name":"heapStatsUpdate","description":"If heap objects tracking has been started then backend may send update for one or more fragments","parameters":[{"name":"statsUpdate","description":"An array of triplets. Each triplet describes a fragment. The first integer is the fragment\\nindex, the second integer is a total count of objects for the fragment, the third integer is\\na total size of the objects for the fragment.","type":"array","items":{"type":"integer"}}]},{"name":"lastSeenObjectId","description":"If heap objects tracking has been started then backend regularly sends a current value for last\\nseen object id and corresponding timestamp. If the were changes in the heap since last event\\nthen one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event.","parameters":[{"name":"lastSeenObjectId","type":"integer"},{"name":"timestamp","type":"number"}]},{"name":"reportHeapSnapshotProgress","parameters":[{"name":"done","type":"integer"},{"name":"total","type":"integer"},{"name":"finished","optional":true,"type":"boolean"}]},{"name":"resetProfiles"}]},{"domain":"Profiler","dependencies":["Runtime","Debugger"],"types":[{"id":"ProfileNode","description":"Profile node. Holds callsite information, execution statistics and child nodes.","type":"object","properties":[{"name":"id","description":"Unique id of the node.","type":"integer"},{"name":"callFrame","description":"Function location.","$ref":"Runtime.CallFrame"},{"name":"hitCount","description":"Number of samples where this node was on top of the call stack.","optional":true,"type":"integer"},{"name":"children","description":"Child node ids.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"deoptReason","description":"The reason of being not optimized. The function may be deoptimized or marked as don\'t\\noptimize.","optional":true,"type":"string"},{"name":"positionTicks","description":"An array of source position ticks.","optional":true,"type":"array","items":{"$ref":"PositionTickInfo"}}]},{"id":"Profile","description":"Profile.","type":"object","properties":[{"name":"nodes","description":"The list of profile nodes. First item is the root node.","type":"array","items":{"$ref":"ProfileNode"}},{"name":"startTime","description":"Profiling start timestamp in microseconds.","type":"number"},{"name":"endTime","description":"Profiling end timestamp in microseconds.","type":"number"},{"name":"samples","description":"Ids of samples top nodes.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"timeDeltas","description":"Time intervals between adjacent samples in microseconds. The first delta is relative to the\\nprofile startTime.","optional":true,"type":"array","items":{"type":"integer"}}]},{"id":"PositionTickInfo","description":"Specifies a number of samples attributed to a certain source position.","type":"object","properties":[{"name":"line","description":"Source line number (1-based).","type":"integer"},{"name":"ticks","description":"Number of samples attributed to the source line.","type":"integer"}]},{"id":"CoverageRange","description":"Coverage data for a source range.","type":"object","properties":[{"name":"startOffset","description":"JavaScript script source offset for the range start.","type":"integer"},{"name":"endOffset","description":"JavaScript script source offset for the range end.","type":"integer"},{"name":"count","description":"Collected execution count of the source range.","type":"integer"}]},{"id":"FunctionCoverage","description":"Coverage data for a JavaScript function.","type":"object","properties":[{"name":"functionName","description":"JavaScript function name.","type":"string"},{"name":"ranges","description":"Source ranges inside the function with coverage data.","type":"array","items":{"$ref":"CoverageRange"}},{"name":"isBlockCoverage","description":"Whether coverage data for this function has block granularity.","type":"boolean"}]},{"id":"ScriptCoverage","description":"Coverage data for a JavaScript script.","type":"object","properties":[{"name":"scriptId","description":"JavaScript script id.","$ref":"Runtime.ScriptId"},{"name":"url","description":"JavaScript script name or url.","type":"string"},{"name":"functions","description":"Functions contained in the script that has coverage data.","type":"array","items":{"$ref":"FunctionCoverage"}}]},{"id":"TypeObject","description":"Describes a type collected during runtime.","experimental":true,"type":"object","properties":[{"name":"name","description":"Name of a type collected with type profiling.","type":"string"}]},{"id":"TypeProfileEntry","description":"Source offset and types for a parameter or return value.","experimental":true,"type":"object","properties":[{"name":"offset","description":"Source offset of the parameter or end of function for return values.","type":"integer"},{"name":"types","description":"The types for this parameter or return value.","type":"array","items":{"$ref":"TypeObject"}}]},{"id":"ScriptTypeProfile","description":"Type profile data collected during runtime for a JavaScript script.","experimental":true,"type":"object","properties":[{"name":"scriptId","description":"JavaScript script id.","$ref":"Runtime.ScriptId"},{"name":"url","description":"JavaScript script name or url.","type":"string"},{"name":"entries","description":"Type profile entries for parameters and return values of the functions in the script.","type":"array","items":{"$ref":"TypeProfileEntry"}}]}],"commands":[{"name":"disable"},{"name":"enable"},{"name":"getBestEffortCoverage","description":"Collect coverage data for the current isolate. The coverage data may be incomplete due to\\ngarbage collection.","returns":[{"name":"result","description":"Coverage data for the current isolate.","type":"array","items":{"$ref":"ScriptCoverage"}}]},{"name":"setSamplingInterval","description":"Changes CPU profiler sampling interval. Must be called before CPU profiles recording started.","parameters":[{"name":"interval","description":"New sampling interval in microseconds.","type":"integer"}]},{"name":"start"},{"name":"startPreciseCoverage","description":"Enable precise code coverage. Coverage data for JavaScript executed before enabling precise code\\ncoverage may be incomplete. Enabling prevents running optimized code and resets execution\\ncounters.","parameters":[{"name":"callCount","description":"Collect accurate call counts beyond simple \'covered\' or \'not covered\'.","optional":true,"type":"boolean"},{"name":"detailed","description":"Collect block-based coverage.","optional":true,"type":"boolean"}]},{"name":"startTypeProfile","description":"Enable type profile.","experimental":true},{"name":"stop","returns":[{"name":"profile","description":"Recorded profile.","$ref":"Profile"}]},{"name":"stopPreciseCoverage","description":"Disable precise code coverage. Disabling releases unnecessary execution count records and allows\\nexecuting optimized code."},{"name":"stopTypeProfile","description":"Disable type profile. Disabling releases type profile data collected so far.","experimental":true},{"name":"takePreciseCoverage","description":"Collect coverage data for the current isolate, and resets execution counters. Precise code\\ncoverage needs to have started.","returns":[{"name":"result","description":"Coverage data for the current isolate.","type":"array","items":{"$ref":"ScriptCoverage"}}]},{"name":"takeTypeProfile","description":"Collect type profile.","experimental":true,"returns":[{"name":"result","description":"Type profile for all scripts since startTypeProfile() was turned on.","type":"array","items":{"$ref":"ScriptTypeProfile"}}]}],"events":[{"name":"consoleProfileFinished","parameters":[{"name":"id","type":"string"},{"name":"location","description":"Location of console.profileEnd().","$ref":"Debugger.Location"},{"name":"profile","$ref":"Profile"},{"name":"title","description":"Profile title passed as an argument to console.profile().","optional":true,"type":"string"}]},{"name":"consoleProfileStarted","description":"Sent when new profile recording is started using console.profile() call.","parameters":[{"name":"id","type":"string"},{"name":"location","description":"Location of console.profile().","$ref":"Debugger.Location"},{"name":"title","description":"Profile title passed as an argument to console.profile().","optional":true,"type":"string"}]}]},{"domain":"Runtime","description":"Runtime domain exposes JavaScript runtime by means of remote evaluation and mirror objects.\\nEvaluation results are returned as mirror object that expose object type, string representation\\nand unique identifier that can be used for further object reference. Original objects are\\nmaintained in memory unless they are either explicitly released or are released along with the\\nother objects in their object group.","types":[{"id":"ScriptId","description":"Unique script identifier.","type":"string"},{"id":"RemoteObjectId","description":"Unique object identifier.","type":"string"},{"id":"UnserializableValue","description":"Primitive value which cannot be JSON-stringified. Includes values `-0`, `NaN`, `Infinity`,\\n`-Infinity`, and bigint literals.","type":"string"},{"id":"RemoteObject","description":"Mirror object referencing original JavaScript object.","type":"object","properties":[{"name":"type","description":"Object type.","type":"string","enum":["object","function","undefined","string","number","boolean","symbol","bigint"]},{"name":"subtype","description":"Object subtype hint. Specified for `object` type values only.","optional":true,"type":"string","enum":["array","null","node","regexp","date","map","set","weakmap","weakset","iterator","generator","error","proxy","promise","typedarray","arraybuffer","dataview"]},{"name":"className","description":"Object class (constructor) name. Specified for `object` type values only.","optional":true,"type":"string"},{"name":"value","description":"Remote object value in case of primitive values or JSON values (if it was requested).","optional":true,"type":"any"},{"name":"unserializableValue","description":"Primitive value which can not be JSON-stringified does not have `value`, but gets this\\nproperty.","optional":true,"$ref":"UnserializableValue"},{"name":"description","description":"String representation of the object.","optional":true,"type":"string"},{"name":"objectId","description":"Unique object identifier (for non-primitive values).","optional":true,"$ref":"RemoteObjectId"},{"name":"preview","description":"Preview containing abbreviated property values. Specified for `object` type values only.","experimental":true,"optional":true,"$ref":"ObjectPreview"},{"name":"customPreview","experimental":true,"optional":true,"$ref":"CustomPreview"}]},{"id":"CustomPreview","experimental":true,"type":"object","properties":[{"name":"header","description":"The JSON-stringified result of formatter.header(object, config) call.\\nIt contains json ML array that represents RemoteObject.","type":"string"},{"name":"bodyGetterId","description":"If formatter returns true as a result of formatter.hasBody call then bodyGetterId will\\ncontain RemoteObjectId for the function that returns result of formatter.body(object, config) call.\\nThe result value is json ML array.","optional":true,"$ref":"RemoteObjectId"}]},{"id":"ObjectPreview","description":"Object containing abbreviated remote object value.","experimental":true,"type":"object","properties":[{"name":"type","description":"Object type.","type":"string","enum":["object","function","undefined","string","number","boolean","symbol","bigint"]},{"name":"subtype","description":"Object subtype hint. Specified for `object` type values only.","optional":true,"type":"string","enum":["array","null","node","regexp","date","map","set","weakmap","weakset","iterator","generator","error"]},{"name":"description","description":"String representation of the object.","optional":true,"type":"string"},{"name":"overflow","description":"True iff some of the properties or entries of the original object did not fit.","type":"boolean"},{"name":"properties","description":"List of the properties.","type":"array","items":{"$ref":"PropertyPreview"}},{"name":"entries","description":"List of the entries. Specified for `map` and `set` subtype values only.","optional":true,"type":"array","items":{"$ref":"EntryPreview"}}]},{"id":"PropertyPreview","experimental":true,"type":"object","properties":[{"name":"name","description":"Property name.","type":"string"},{"name":"type","description":"Object type. Accessor means that the property itself is an accessor property.","type":"string","enum":["object","function","undefined","string","number","boolean","symbol","accessor","bigint"]},{"name":"value","description":"User-friendly property value string.","optional":true,"type":"string"},{"name":"valuePreview","description":"Nested value preview.","optional":true,"$ref":"ObjectPreview"},{"name":"subtype","description":"Object subtype hint. Specified for `object` type values only.","optional":true,"type":"string","enum":["array","null","node","regexp","date","map","set","weakmap","weakset","iterator","generator","error"]}]},{"id":"EntryPreview","experimental":true,"type":"object","properties":[{"name":"key","description":"Preview of the key. Specified for map-like collection entries.","optional":true,"$ref":"ObjectPreview"},{"name":"value","description":"Preview of the value.","$ref":"ObjectPreview"}]},{"id":"PropertyDescriptor","description":"Object property descriptor.","type":"object","properties":[{"name":"name","description":"Property name or symbol description.","type":"string"},{"name":"value","description":"The value associated with the property.","optional":true,"$ref":"RemoteObject"},{"name":"writable","description":"True if the value associated with the property may be changed (data descriptors only).","optional":true,"type":"boolean"},{"name":"get","description":"A function which serves as a getter for the property, or `undefined` if there is no getter\\n(accessor descriptors only).","optional":true,"$ref":"RemoteObject"},{"name":"set","description":"A function which serves as a setter for the property, or `undefined` if there is no setter\\n(accessor descriptors only).","optional":true,"$ref":"RemoteObject"},{"name":"configurable","description":"True if the type of this property descriptor may be changed and if the property may be\\ndeleted from the corresponding object.","type":"boolean"},{"name":"enumerable","description":"True if this property shows up during enumeration of the properties on the corresponding\\nobject.","type":"boolean"},{"name":"wasThrown","description":"True if the result was thrown during the evaluation.","optional":true,"type":"boolean"},{"name":"isOwn","description":"True if the property is owned for the object.","optional":true,"type":"boolean"},{"name":"symbol","description":"Property symbol object, if the property is of the `symbol` type.","optional":true,"$ref":"RemoteObject"}]},{"id":"InternalPropertyDescriptor","description":"Object internal property descriptor. This property isn\'t normally visible in JavaScript code.","type":"object","properties":[{"name":"name","description":"Conventional property name.","type":"string"},{"name":"value","description":"The value associated with the property.","optional":true,"$ref":"RemoteObject"}]},{"id":"PrivatePropertyDescriptor","description":"Object private field descriptor.","experimental":true,"type":"object","properties":[{"name":"name","description":"Private property name.","type":"string"},{"name":"value","description":"The value associated with the private property.","$ref":"RemoteObject"}]},{"id":"CallArgument","description":"Represents function call argument. Either remote object id `objectId`, primitive `value`,\\nunserializable primitive value or neither of (for undefined) them should be specified.","type":"object","properties":[{"name":"value","description":"Primitive value or serializable javascript object.","optional":true,"type":"any"},{"name":"unserializableValue","description":"Primitive value which can not be JSON-stringified.","optional":true,"$ref":"UnserializableValue"},{"name":"objectId","description":"Remote object handle.","optional":true,"$ref":"RemoteObjectId"}]},{"id":"ExecutionContextId","description":"Id of an execution context.","type":"integer"},{"id":"ExecutionContextDescription","description":"Description of an isolated world.","type":"object","properties":[{"name":"id","description":"Unique id of the execution context. It can be used to specify in which execution context\\nscript evaluation should be performed.","$ref":"ExecutionContextId"},{"name":"origin","description":"Execution context origin.","type":"string"},{"name":"name","description":"Human readable name describing given context.","type":"string"},{"name":"auxData","description":"Embedder-specific auxiliary data.","optional":true,"type":"object"}]},{"id":"ExceptionDetails","description":"Detailed information about exception (or error) that was thrown during script compilation or\\nexecution.","type":"object","properties":[{"name":"exceptionId","description":"Exception id.","type":"integer"},{"name":"text","description":"Exception text, which should be used together with exception object when available.","type":"string"},{"name":"lineNumber","description":"Line number of the exception location (0-based).","type":"integer"},{"name":"columnNumber","description":"Column number of the exception location (0-based).","type":"integer"},{"name":"scriptId","description":"Script ID of the exception location.","optional":true,"$ref":"ScriptId"},{"name":"url","description":"URL of the exception location, to be used when the script was not reported.","optional":true,"type":"string"},{"name":"stackTrace","description":"JavaScript stack trace if available.","optional":true,"$ref":"StackTrace"},{"name":"exception","description":"Exception object if available.","optional":true,"$ref":"RemoteObject"},{"name":"executionContextId","description":"Identifier of the context where exception happened.","optional":true,"$ref":"ExecutionContextId"}]},{"id":"Timestamp","description":"Number of milliseconds since epoch.","type":"number"},{"id":"TimeDelta","description":"Number of milliseconds.","type":"number"},{"id":"CallFrame","description":"Stack entry for runtime errors and assertions.","type":"object","properties":[{"name":"functionName","description":"JavaScript function name.","type":"string"},{"name":"scriptId","description":"JavaScript script id.","$ref":"ScriptId"},{"name":"url","description":"JavaScript script name or url.","type":"string"},{"name":"lineNumber","description":"JavaScript script line number (0-based).","type":"integer"},{"name":"columnNumber","description":"JavaScript script column number (0-based).","type":"integer"}]},{"id":"StackTrace","description":"Call frames for assertions or error messages.","type":"object","properties":[{"name":"description","description":"String label of this stack trace. For async traces this may be a name of the function that\\ninitiated the async call.","optional":true,"type":"string"},{"name":"callFrames","description":"JavaScript function name.","type":"array","items":{"$ref":"CallFrame"}},{"name":"parent","description":"Asynchronous JavaScript stack trace that preceded this stack, if available.","optional":true,"$ref":"StackTrace"},{"name":"parentId","description":"Asynchronous JavaScript stack trace that preceded this stack, if available.","experimental":true,"optional":true,"$ref":"StackTraceId"}]},{"id":"UniqueDebuggerId","description":"Unique identifier of current debugger.","experimental":true,"type":"string"},{"id":"StackTraceId","description":"If `debuggerId` is set stack trace comes from another debugger and can be resolved there. This\\nallows to track cross-debugger calls. See `Runtime.StackTrace` and `Debugger.paused` for usages.","experimental":true,"type":"object","properties":[{"name":"id","type":"string"},{"name":"debuggerId","optional":true,"$ref":"UniqueDebuggerId"}]}],"commands":[{"name":"awaitPromise","description":"Add handler to promise with given promise object id.","parameters":[{"name":"promiseObjectId","description":"Identifier of the promise.","$ref":"RemoteObjectId"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object that should be sent by value.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"Promise result. Will contain rejected value if promise was rejected.","$ref":"RemoteObject"},{"name":"exceptionDetails","description":"Exception details if stack strace is available.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"callFunctionOn","description":"Calls function with given declaration on the given object. Object group of the result is\\ninherited from the target object.","parameters":[{"name":"functionDeclaration","description":"Declaration of the function to call.","type":"string"},{"name":"objectId","description":"Identifier of the object to call function on. Either objectId or executionContextId should\\nbe specified.","optional":true,"$ref":"RemoteObjectId"},{"name":"arguments","description":"Call arguments. All call arguments must belong to the same JavaScript world as the target\\nobject.","optional":true,"type":"array","items":{"$ref":"CallArgument"}},{"name":"silent","description":"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.","optional":true,"type":"boolean"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object which should be sent by value.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","experimental":true,"optional":true,"type":"boolean"},{"name":"userGesture","description":"Whether execution should be treated as initiated by user in the UI.","optional":true,"type":"boolean"},{"name":"awaitPromise","description":"Whether execution should `await` for resulting value and return once awaited promise is\\nresolved.","optional":true,"type":"boolean"},{"name":"executionContextId","description":"Specifies execution context which global object will be used to call function on. Either\\nexecutionContextId or objectId should be specified.","optional":true,"$ref":"ExecutionContextId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects. If objectGroup is not\\nspecified and objectId is, objectGroup will be inherited from object.","optional":true,"type":"string"}],"returns":[{"name":"result","description":"Call result.","$ref":"RemoteObject"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"compileScript","description":"Compiles expression.","parameters":[{"name":"expression","description":"Expression to compile.","type":"string"},{"name":"sourceURL","description":"Source url to be set for the script.","type":"string"},{"name":"persistScript","description":"Specifies whether the compiled script should be persisted.","type":"boolean"},{"name":"executionContextId","description":"Specifies in which execution context to perform script run. If the parameter is omitted the\\nevaluation will be performed in the context of the inspected page.","optional":true,"$ref":"ExecutionContextId"}],"returns":[{"name":"scriptId","description":"Id of the script.","optional":true,"$ref":"ScriptId"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"disable","description":"Disables reporting of execution contexts creation."},{"name":"discardConsoleEntries","description":"Discards collected exceptions and console API calls."},{"name":"enable","description":"Enables reporting of execution contexts creation by means of `executionContextCreated` event.\\nWhen the reporting gets enabled the event will be sent immediately for each existing execution\\ncontext."},{"name":"evaluate","description":"Evaluates expression on global object.","parameters":[{"name":"expression","description":"Expression to evaluate.","type":"string"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects.","optional":true,"type":"string"},{"name":"includeCommandLineAPI","description":"Determines whether Command Line API should be available during the evaluation.","optional":true,"type":"boolean"},{"name":"silent","description":"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.","optional":true,"type":"boolean"},{"name":"contextId","description":"Specifies in which execution context to perform evaluation. If the parameter is omitted the\\nevaluation will be performed in the context of the inspected page.","optional":true,"$ref":"ExecutionContextId"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object that should be sent by value.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","experimental":true,"optional":true,"type":"boolean"},{"name":"userGesture","description":"Whether execution should be treated as initiated by user in the UI.","optional":true,"type":"boolean"},{"name":"awaitPromise","description":"Whether execution should `await` for resulting value and return once awaited promise is\\nresolved.","optional":true,"type":"boolean"},{"name":"throwOnSideEffect","description":"Whether to throw an exception if side effect cannot be ruled out during evaluation.","experimental":true,"optional":true,"type":"boolean"},{"name":"timeout","description":"Terminate execution after timing out (number of milliseconds).","experimental":true,"optional":true,"$ref":"TimeDelta"}],"returns":[{"name":"result","description":"Evaluation result.","$ref":"RemoteObject"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"getIsolateId","description":"Returns the isolate id.","experimental":true,"returns":[{"name":"id","description":"The isolate id.","type":"string"}]},{"name":"getHeapUsage","description":"Returns the JavaScript heap usage.\\nIt is the total usage of the corresponding isolate not scoped to a particular Runtime.","experimental":true,"returns":[{"name":"usedSize","description":"Used heap size in bytes.","type":"number"},{"name":"totalSize","description":"Allocated heap size in bytes.","type":"number"}]},{"name":"getProperties","description":"Returns properties of a given object. Object group of the result is inherited from the target\\nobject.","parameters":[{"name":"objectId","description":"Identifier of the object to return properties for.","$ref":"RemoteObjectId"},{"name":"ownProperties","description":"If true, returns properties belonging only to the element itself, not to its prototype\\nchain.","optional":true,"type":"boolean"},{"name":"accessorPropertiesOnly","description":"If true, returns accessor properties (with getter/setter) only; internal properties are not\\nreturned either.","experimental":true,"optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the results.","experimental":true,"optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"Object properties.","type":"array","items":{"$ref":"PropertyDescriptor"}},{"name":"internalProperties","description":"Internal object properties (only of the element itself).","optional":true,"type":"array","items":{"$ref":"InternalPropertyDescriptor"}},{"name":"privateProperties","description":"Object private properties.","experimental":true,"optional":true,"type":"array","items":{"$ref":"PrivatePropertyDescriptor"}},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"globalLexicalScopeNames","description":"Returns all let, const and class variables from global scope.","parameters":[{"name":"executionContextId","description":"Specifies in which execution context to lookup global scope variables.","optional":true,"$ref":"ExecutionContextId"}],"returns":[{"name":"names","type":"array","items":{"type":"string"}}]},{"name":"queryObjects","parameters":[{"name":"prototypeObjectId","description":"Identifier of the prototype to return objects for.","$ref":"RemoteObjectId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release the results.","optional":true,"type":"string"}],"returns":[{"name":"objects","description":"Array with objects.","$ref":"RemoteObject"}]},{"name":"releaseObject","description":"Releases remote object with given id.","parameters":[{"name":"objectId","description":"Identifier of the object to release.","$ref":"RemoteObjectId"}]},{"name":"releaseObjectGroup","description":"Releases all remote objects that belong to a given group.","parameters":[{"name":"objectGroup","description":"Symbolic object group name.","type":"string"}]},{"name":"runIfWaitingForDebugger","description":"Tells inspected instance to run if it was waiting for debugger to attach."},{"name":"runScript","description":"Runs script with given id in a given context.","parameters":[{"name":"scriptId","description":"Id of the script to run.","$ref":"ScriptId"},{"name":"executionContextId","description":"Specifies in which execution context to perform script run. If the parameter is omitted the\\nevaluation will be performed in the context of the inspected page.","optional":true,"$ref":"ExecutionContextId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects.","optional":true,"type":"string"},{"name":"silent","description":"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.","optional":true,"type":"boolean"},{"name":"includeCommandLineAPI","description":"Determines whether Command Line API should be available during the evaluation.","optional":true,"type":"boolean"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object which should be sent by value.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","optional":true,"type":"boolean"},{"name":"awaitPromise","description":"Whether execution should `await` for resulting value and return once awaited promise is\\nresolved.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"Run result.","$ref":"RemoteObject"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"setAsyncCallStackDepth","description":"Enables or disables async call stacks tracking.","redirect":"Debugger","parameters":[{"name":"maxDepth","description":"Maximum depth of async call stacks. Setting to `0` will effectively disable collecting async\\ncall stacks (default).","type":"integer"}]},{"name":"setCustomObjectFormatterEnabled","experimental":true,"parameters":[{"name":"enabled","type":"boolean"}]},{"name":"setMaxCallStackSizeToCapture","experimental":true,"parameters":[{"name":"size","type":"integer"}]},{"name":"terminateExecution","description":"Terminate current or next JavaScript execution.\\nWill cancel the termination when the outer-most script execution ends.","experimental":true},{"name":"addBinding","description":"If executionContextId is empty, adds binding with the given name on the\\nglobal objects of all inspected contexts, including those created later,\\nbindings survive reloads.\\nIf executionContextId is specified, adds binding only on global object of\\ngiven execution context.\\nBinding function takes exactly one argument, this argument should be string,\\nin case of any other input, function throws an exception.\\nEach binding function call produces Runtime.bindingCalled notification.","experimental":true,"parameters":[{"name":"name","type":"string"},{"name":"executionContextId","optional":true,"$ref":"ExecutionContextId"}]},{"name":"removeBinding","description":"This method does not remove binding function from global object but\\nunsubscribes current runtime agent from Runtime.bindingCalled notifications.","experimental":true,"parameters":[{"name":"name","type":"string"}]}],"events":[{"name":"bindingCalled","description":"Notification is issued every time when binding is called.","experimental":true,"parameters":[{"name":"name","type":"string"},{"name":"payload","type":"string"},{"name":"executionContextId","description":"Identifier of the context where the call was made.","$ref":"ExecutionContextId"}]},{"name":"consoleAPICalled","description":"Issued when console API was called.","parameters":[{"name":"type","description":"Type of the call.","type":"string","enum":["log","debug","info","error","warning","dir","dirxml","table","trace","clear","startGroup","startGroupCollapsed","endGroup","assert","profile","profileEnd","count","timeEnd"]},{"name":"args","description":"Call arguments.","type":"array","items":{"$ref":"RemoteObject"}},{"name":"executionContextId","description":"Identifier of the context where the call was made.","$ref":"ExecutionContextId"},{"name":"timestamp","description":"Call timestamp.","$ref":"Timestamp"},{"name":"stackTrace","description":"Stack trace captured when the call was made. The async stack chain is automatically reported for\\nthe following call types: `assert`, `error`, `trace`, `warning`. For other types the async call\\nchain can be retrieved using `Debugger.getStackTrace` and `stackTrace.parentId` field.","optional":true,"$ref":"StackTrace"},{"name":"context","description":"Console context descriptor for calls on non-default console context (not console.*):\\n\'anonymous#unique-logger-id\' for call on unnamed context, \'name#unique-logger-id\' for call\\non named context.","experimental":true,"optional":true,"type":"string"}]},{"name":"exceptionRevoked","description":"Issued when unhandled exception was revoked.","parameters":[{"name":"reason","description":"Reason describing why exception was revoked.","type":"string"},{"name":"exceptionId","description":"The id of revoked exception, as reported in `exceptionThrown`.","type":"integer"}]},{"name":"exceptionThrown","description":"Issued when exception was thrown and unhandled.","parameters":[{"name":"timestamp","description":"Timestamp of the exception.","$ref":"Timestamp"},{"name":"exceptionDetails","$ref":"ExceptionDetails"}]},{"name":"executionContextCreated","description":"Issued when new execution context is created.","parameters":[{"name":"context","description":"A newly created execution context.","$ref":"ExecutionContextDescription"}]},{"name":"executionContextDestroyed","description":"Issued when execution context is destroyed.","parameters":[{"name":"executionContextId","description":"Id of the destroyed context","$ref":"ExecutionContextId"}]},{"name":"executionContextsCleared","description":"Issued when all executionContexts were cleared in browser"},{"name":"inspectRequested","description":"Issued when object should be inspected (for example, as a result of inspect() command line API\\ncall).","parameters":[{"name":"object","$ref":"RemoteObject"},{"name":"hints","type":"object"}]}]},{"domain":"Schema","description":"This domain is deprecated.","deprecated":true,"types":[{"id":"Domain","description":"Description of the protocol domain.","type":"object","properties":[{"name":"name","description":"Domain name.","type":"string"},{"name":"version","description":"Domain version.","type":"string"}]}],"commands":[{"name":"getDomains","description":"Returns supported domains.","returns":[{"name":"domains","description":"List of supported domains.","type":"array","items":{"$ref":"Domain"}}]}]}]}')},function(e,t,n){"use strict";(function(t){const r=n(56),i=n(379),o=n(75).format,a=n(75).parse,s=n(381),c=n(382),p=n(151),d=n(139);class u extends Error{constructor(e,t){let{message:n}=t;t.data&&(n+=` (${t.data})`),super(n),this.request=e,this.response=t}}e.exports=class extends r{constructor(e,t){super(),e=e||{},this.host=e.host||p.HOST,this.port=e.port||p.PORT,this.secure=!!e.secure,this.useHostName=!!e.useHostName,this.alterPath=e.alterPath||(e=>e),this.protocol=e.protocol,this.local=!!e.local,this.target=e.target||(e=>{let t,n=e.find(e=>!!e.webSocketDebuggerUrl&&(t=t||e,"page"===e.type));if(n=n||t)return n;throw new Error("No inspectable targets")}),this._notifier=t,this._callbacks={},this._nextCommandId=1,this.webSocketUrl=void 0,this._start()}inspect(e,t){return t.customInspect=!1,i.inspect(this,t)}send(e,t,n){return"function"==typeof t&&(n=t,t=void 0),"function"==typeof n?void this._enqueueCommand(e,t,n):new Promise((n,r)=>{this._enqueueCommand(e,t,(i,o)=>{if(i){const n={method:e,params:t};r(i instanceof Error?i:new u(n,o))}else n(o)})})}close(e){const t=e=>{3===this._ws.readyState?e():(this._ws.removeAllListeners("close"),this._ws.once("close",()=>{this._ws.removeAllListeners(),e()}),this._ws.close())};return"function"==typeof e?void t(e):new Promise((e,n)=>{t(e)})}async _start(){const e={host:this.host,port:this.port,secure:this.secure,useHostName:this.useHostName,alterPath:this.alterPath};try{const n=await this._fetchDebuggerURL(e),r=a(n);r.pathname=e.alterPath(r.pathname),this.webSocketUrl=o(r),e.host=r.hostname,e.port=r.port||e.port;const i=await this._fetchProtocol(e);c.prepare(this,i),await this._connectToWebSocket(),t.nextTick(()=>{this._notifier.emit("connect",this)})}catch(e){this._notifier.emit("error",e)}}async _fetchDebuggerURL(e){const t=this.target;switch(typeof t){case"string":{let n=t;return n.startsWith("/")&&(n=`ws://${this.host}:${this.port}${n}`),n.match(/^wss?:/i)?n:(await d.List(e)).find(e=>e.id===n).webSocketDebuggerUrl}case"object":return t.webSocketDebuggerUrl;case"function":{const n=t,r=await d.List(e),i=n(r);return("number"==typeof i?r[i]:i).webSocketDebuggerUrl}default:throw new Error(`Invalid target argument "${this.target}"`)}}async _fetchProtocol(e){return this.protocol?this.protocol:(e.local=this.local,await d.Protocol(e))}_connectToWebSocket(){return new Promise((e,t)=>{try{this.secure&&(this.webSocketUrl=this.webSocketUrl.replace(/^ws:/i,"wss:")),this._ws=new s(this.webSocketUrl)}catch(e){return void t(e)}this._ws.on("open",()=>{e()}),this._ws.on("message",e=>{const t=JSON.parse(e);this._handleMessage(t)}),this._ws.on("close",e=>{this.emit("disconnect")}),this._ws.on("error",e=>{t(e)})})}_handleMessage(e){if(e.id){const t=this._callbacks[e.id];if(!t)return;e.error?t(!0,e.error):t(!1,e.result||{}),delete this._callbacks[e.id],0===Object.keys(this._callbacks).length&&this.emit("ready")}else e.method&&(this.emit("event",e),this.emit(e.method,e.params))}_enqueueCommand(e,t,n){const r=this._nextCommandId++,i={id:r,method:e,params:t||{}};this._ws.send(JSON.stringify(i),e=>{e?"function"==typeof n&&n(e):this._callbacks[r]=n})}}}).call(this,n(30))},function(e,t,n){(function(e){var r=Object.getOwnPropertyDescriptors||function(e){for(var t=Object.keys(e),n={},r=0;r<t.length;r++)n[t[r]]=Object.getOwnPropertyDescriptor(e,t[r]);return n},i=/%[sdj%]/g;t.format=function(e){if(!y(e)){for(var t=[],n=0;n<arguments.length;n++)t.push(s(arguments[n]));return t.join(" ")}n=1;for(var r=arguments,o=r.length,a=String(e).replace(i,function(e){if("%%"===e)return"%";if(n>=o)return e;switch(e){case"%s":return String(r[n++]);case"%d":return Number(r[n++]);case"%j":try{return JSON.stringify(r[n++])}catch(e){return"[Circular]"}default:return e}}),c=r[n];n<o;c=r[++n])h(c)||!w(c)?a+=" "+c:a+=" "+s(c);return a},t.deprecate=function(n,r){if(void 0!==e&&!0===e.noDeprecation)return n;if(void 0===e)return function(){return t.deprecate(n,r).apply(this,arguments)};var i=!1;return function(){if(!i){if(e.throwDeprecation)throw new Error(r);e.traceDeprecation?console.trace(r):console.error(r),i=!0}return n.apply(this,arguments)}};var o,a={};function s(e,n){var r={seen:[],stylize:p};return arguments.length>=3&&(r.depth=arguments[2]),arguments.length>=4&&(r.colors=arguments[3]),f(n)?r.showHidden=n:n&&t._extend(r,n),b(r.showHidden)&&(r.showHidden=!1),b(r.depth)&&(r.depth=2),b(r.colors)&&(r.colors=!1),b(r.customInspect)&&(r.customInspect=!0),r.colors&&(r.stylize=c),d(r,e,r.depth)}function c(e,t){var n=s.styles[t];return n?"["+s.colors[n][0]+"m"+e+"["+s.colors[n][1]+"m":e}function p(e,t){return e}function d(e,n,r){if(e.customInspect&&n&&I(n.inspect)&&n.inspect!==t.inspect&&(!n.constructor||n.constructor.prototype!==n)){var i=n.inspect(r,e);return y(i)||(i=d(e,i,r)),i}var o=function(e,t){if(b(t))return e.stylize("undefined","undefined");if(y(t)){var n="'"+JSON.stringify(t).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return e.stylize(n,"string")}if(g(t))return e.stylize(""+t,"number");if(f(t))return e.stylize(""+t,"boolean");if(h(t))return e.stylize("null","null")}(e,n);if(o)return o;var a=Object.keys(n),s=function(e){var t={};return e.forEach(function(e,n){t[e]=!0}),t}(a);if(e.showHidden&&(a=Object.getOwnPropertyNames(n)),x(n)&&(a.indexOf("message")>=0||a.indexOf("description")>=0))return u(n);if(0===a.length){if(I(n)){var c=n.name?": "+n.name:"";return e.stylize("[Function"+c+"]","special")}if(v(n))return e.stylize(RegExp.prototype.toString.call(n),"regexp");if(S(n))return e.stylize(Date.prototype.toString.call(n),"date");if(x(n))return u(n)}var p,w="",T=!1,R=["{","}"];(m(n)&&(T=!0,R=["[","]"]),I(n))&&(w=" [Function"+(n.name?": "+n.name:"")+"]");return v(n)&&(w=" "+RegExp.prototype.toString.call(n)),S(n)&&(w=" "+Date.prototype.toUTCString.call(n)),x(n)&&(w=" "+u(n)),0!==a.length||T&&0!=n.length?r<0?v(n)?e.stylize(RegExp.prototype.toString.call(n),"regexp"):e.stylize("[Object]","special"):(e.seen.push(n),p=T?function(e,t,n,r,i){for(var o=[],a=0,s=t.length;a<s;++a)O(t,String(a))?o.push(l(e,t,n,r,String(a),!0)):o.push("");return i.forEach(function(i){i.match(/^\d+$/)||o.push(l(e,t,n,r,i,!0))}),o}(e,n,r,s,a):a.map(function(t){return l(e,n,r,s,t,T)}),e.seen.pop(),function(e,t,n){if(e.reduce(function(e,t){return 0,t.indexOf("\n")>=0&&0,e+t.replace(/\u001b\[\d\d?m/g,"").length+1},0)>60)return n[0]+(""===t?"":t+"\n ")+" "+e.join(",\n ")+" "+n[1];return n[0]+t+" "+e.join(", ")+" "+n[1]}(p,w,R)):R[0]+w+R[1]}function u(e){return"["+Error.prototype.toString.call(e)+"]"}function l(e,t,n,r,i,o){var a,s,c;if((c=Object.getOwnPropertyDescriptor(t,i)||{value:t[i]}).get?s=c.set?e.stylize("[Getter/Setter]","special"):e.stylize("[Getter]","special"):c.set&&(s=e.stylize("[Setter]","special")),O(r,i)||(a="["+i+"]"),s||(e.seen.indexOf(c.value)<0?(s=h(n)?d(e,c.value,null):d(e,c.value,n-1)).indexOf("\n")>-1&&(s=o?s.split("\n").map(function(e){return" "+e}).join("\n").substr(2):"\n"+s.split("\n").map(function(e){return" "+e}).join("\n")):s=e.stylize("[Circular]","special")),b(a)){if(o&&i.match(/^\d+$/))return s;(a=JSON.stringify(""+i)).match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(a=a.substr(1,a.length-2),a=e.stylize(a,"name")):(a=a.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),a=e.stylize(a,"string"))}return a+": "+s}function m(e){return Array.isArray(e)}function f(e){return"boolean"==typeof e}function h(e){return null===e}function g(e){return"number"==typeof e}function y(e){return"string"==typeof e}function b(e){return void 0===e}function v(e){return w(e)&&"[object RegExp]"===T(e)}function w(e){return"object"==typeof e&&null!==e}function S(e){return w(e)&&"[object Date]"===T(e)}function x(e){return w(e)&&("[object Error]"===T(e)||e instanceof Error)}function I(e){return"function"==typeof e}function T(e){return Object.prototype.toString.call(e)}function R(e){return e<10?"0"+e.toString(10):e.toString(10)}t.debuglog=function(n){if(b(o)&&(o=e.env.NODE_DEBUG||""),n=n.toUpperCase(),!a[n])if(new RegExp("\\b"+n+"\\b","i").test(o)){var r=e.pid;a[n]=function(){var e=t.format.apply(t,arguments);console.error("%s %d: %s",n,r,e)}}else a[n]=function(){};return a[n]},t.inspect=s,s.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]},s.styles={special:"cyan",number:"yellow",boolean:"yellow",undefined:"grey",null:"bold",string:"green",date:"magenta",regexp:"red"},t.isArray=m,t.isBoolean=f,t.isNull=h,t.isNullOrUndefined=function(e){return null==e},t.isNumber=g,t.isString=y,t.isSymbol=function(e){return"symbol"==typeof e},t.isUndefined=b,t.isRegExp=v,t.isObject=w,t.isDate=S,t.isError=x,t.isFunction=I,t.isPrimitive=function(e){return null===e||"boolean"==typeof e||"number"==typeof e||"string"==typeof e||"symbol"==typeof e||void 0===e},t.isBuffer=n(380);var k=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function C(){var e=new Date,t=[R(e.getHours()),R(e.getMinutes()),R(e.getSeconds())].join(":");return[e.getDate(),k[e.getMonth()],t].join(" ")}function O(e,t){return Object.prototype.hasOwnProperty.call(e,t)}t.log=function(){console.log("%s - %s",C(),t.format.apply(t,arguments))},t.inherits=n(34),t._extend=function(e,t){if(!t||!w(t))return e;for(var n=Object.keys(t),r=n.length;r--;)e[n[r]]=t[n[r]];return e};var $="undefined"!=typeof Symbol?Symbol("util.promisify.custom"):void 0;function E(e,t){if(!e){var n=new Error("Promise was rejected with a falsy value");n.reason=e,e=n}return t(e)}t.promisify=function(e){if("function"!=typeof e)throw new TypeError('The "original" argument must be of type Function');if($&&e[$]){var t;if("function"!=typeof(t=e[$]))throw new TypeError('The "util.promisify.custom" argument must be of type Function');return Object.defineProperty(t,$,{value:t,enumerable:!1,writable:!1,configurable:!0}),t}function t(){for(var t,n,r=new Promise(function(e,r){t=e,n=r}),i=[],o=0;o<arguments.length;o++)i.push(arguments[o]);i.push(function(e,r){e?n(e):t(r)});try{e.apply(this,i)}catch(e){n(e)}return r}return Object.setPrototypeOf(t,Object.getPrototypeOf(e)),$&&Object.defineProperty(t,$,{value:t,enumerable:!1,writable:!1,configurable:!0}),Object.defineProperties(t,r(e))},t.promisify.custom=$,t.callbackify=function(t){if("function"!=typeof t)throw new TypeError('The "original" argument must be of type Function');function n(){for(var n=[],r=0;r<arguments.length;r++)n.push(arguments[r]);var i=n.pop();if("function"!=typeof i)throw new TypeError("The last argument must be of type Function");var o=this,a=function(){return i.apply(o,arguments)};t.apply(this,n).then(function(t){e.nextTick(a,null,t)},function(t){e.nextTick(E,t,a)})}return Object.setPrototypeOf(n,Object.getPrototypeOf(t)),Object.defineProperties(n,r(t)),n}}).call(this,n(30))},function(e,t){e.exports=function(e){return e&&"object"==typeof e&&"function"==typeof e.copy&&"function"==typeof e.fill&&"function"==typeof e.readUInt8}},function(e,t,n){"use strict";const r=n(56);e.exports=class extends r{constructor(e){super(),this._ws=new WebSocket(e),this._ws.onopen=()=>{this.emit("open")},this._ws.onclose=()=>{this.emit("close")},this._ws.onmessage=e=>{this.emit("message",e.data)},this._ws.onerror=()=>{this.emit("error",new Error("WebSocket error"))}}close(){this._ws.close()}send(e,t){try{this._ws.send(e),t()}catch(e){t(e)}}}},function(e,t,n){"use strict";function r(e,t,n){e.category=t,Object.keys(n).forEach(r=>{"name"!==r&&(e[r]="type"===t&&"properties"===r||"parameters"===r?function(e){const t={};return e.forEach(e=>{const n=e.name;delete e.name,t[n]=e}),t}(n[r]):n[r])})}e.exports.prepare=function(e,t){e.protocol=t,t.domains.forEach(t=>{const n=t.domain;e[n]={},(t.commands||[]).forEach(t=>{!function(e,t,n){const i=(r,i)=>e.send(`${t}.${n.name}`,r,i);r(i,"command",n),e[t][n.name]=i}(e,n,t)}),(t.events||[]).forEach(t=>{!function(e,t,n){const i=`${t}.${n.name}`,o=t=>"function"==typeof t?(e.on(i,t),()=>e.removeListener(i,t)):new Promise((t,n)=>{e.once(i,t)});r(o,"event",n),e[t][n.name]=o}(e,n,t)}),(t.types||[]).forEach(t=>{!function(e,t,n){const i={};r(i,"type",n),e[t][n.id]=i}(e,n,t)})})}}]);
\ No newline at end of file diff --git a/remote/test/browser/dom/browser.ini b/remote/test/browser/dom/browser.ini new file mode 100644 index 0000000000..77f9558308 --- /dev/null +++ b/remote/test/browser/dom/browser.ini @@ -0,0 +1,12 @@ +[DEFAULT] +tags = remote +subsuite = remote +prefs = + remote.enabled=true +support-files = + !/remote/test/browser/chrome-remote-interface.js + !/remote/test/browser/head.js + head.js + +[browser_describeNode.js] +[browser_resolveNode.js] diff --git a/remote/test/browser/dom/browser_describeNode.js b/remote/test/browser/dom/browser_describeNode.js new file mode 100644 index 0000000000..931b908dfd --- /dev/null +++ b/remote/test/browser/dom/browser_describeNode.js @@ -0,0 +1,175 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div id='content'><p>foo</p><p>bar</p></div>"); +const DOC_FRAME = toDataURL(`<iframe src="${DOC}"></iframe>`); + +add_task(async function objectIdInvalidTypes({ client }) { + const { DOM } = client; + + for (const objectId of [null, true, 1, [], {}]) { + let errorThrown = ""; + try { + await DOM.describeNode({ objectId }); + } catch (e) { + errorThrown = e.message; + } + ok( + errorThrown.match(/objectId: string value expected/), + `Fails with invalid type: ${objectId}` + ); + } +}); + +add_task(async function objectIdUnknownValue({ client }) { + const { DOM } = client; + + let errorThrown = ""; + try { + await DOM.describeNode({ objectId: "foo" }); + } catch (e) { + errorThrown = e.message; + } + ok( + errorThrown.match(/Could not find object with given id/), + "Fails with unknown objectId" + ); +}); + +add_task(async function objectIdIsNotANode({ client }) { + const { DOM, Runtime } = client; + + await Runtime.enable(); + const { result } = await Runtime.evaluate({ + expression: "[42]", + }); + + let errorThrown = ""; + try { + await DOM.describeNode({ objectId: result.objectId }); + } catch (e) { + errorThrown = e.message; + } + ok( + errorThrown.match(/Object id doesn't reference a Node/), + "Fails if objectId doesn't reference a DOM node" + ); +}); + +add_task(async function objectIdAllProperties({ client }) { + const { DOM, Page, Runtime } = client; + + await Page.enable(); + const { frameId } = await Page.navigate({ url: DOC }); + await Page.loadEventFired(); + + await Runtime.enable(); + const { result } = await Runtime.evaluate({ + expression: `document.getElementById('content')`, + }); + const { node } = await DOM.describeNode({ + objectId: result.objectId, + }); + + ok(!!node.nodeId, "The node has a node id"); + ok(!!node.backendNodeId, "The node has a backend node id"); + is(node.nodeName, "DIV", "Found expected node name"); + is(node.localName, "div", "Found expected local name"); + is(node.nodeType, 1, "Found expected node type"); + is(node.nodeValue, "", "Found expected node value"); + is(node.childNodeCount, 2, "Expected number of child nodes found"); + is(node.attributes.length, 2, "Found expected attribute's name and value"); + is(node.attributes[0], "id", "Found expected attribute name"); + is(node.attributes[1], "content", "Found expected attribute value"); + is(node.frameId, frameId, "Found expected frame id"); +}); + +add_task(async function objectIdNoAttributes({ client }) { + const { DOM, Runtime } = client; + + await Runtime.enable(); + const { result } = await Runtime.evaluate({ + expression: "document", + }); + const { node } = await DOM.describeNode({ + objectId: result.objectId, + }); + + is(node.attributes, undefined, "No attributes returned"); +}); + +add_task(async function objectIdDiffersForDifferentNodes({ client }) { + const { DOM, Runtime } = client; + + await loadURL(DOC); + + await Runtime.enable(); + const { result: doc } = await Runtime.evaluate({ + expression: "document", + }); + const { node: node1 } = await DOM.describeNode({ + objectId: doc.objectId, + }); + + const { result: body } = await Runtime.evaluate({ + expression: `document.getElementById('content')`, + }); + const { node: node2 } = await DOM.describeNode({ + objectId: body.objectId, + }); + + for (const prop in node1) { + if (["nodeValue", "frameId"].includes(prop)) { + is(node1[prop], node2[prop], `Values of ${prop} are equal`); + } else { + isnot(node1[prop], node2[prop], `Values of ${prop} are different`); + } + } +}); + +add_task(async function objectIdDoesNotChangeForTheSameNode({ client }) { + const { DOM, Runtime } = client; + + await Runtime.enable(); + const { result } = await Runtime.evaluate({ + expression: "document", + }); + const { node: node1 } = await DOM.describeNode({ + objectId: result.objectId, + }); + const { node: node2 } = await DOM.describeNode({ + objectId: result.objectId, + }); + + for (const prop in node1) { + is(node1[prop], node2[prop], `Values of ${prop} are equal`); + } +}); + +add_task(async function frameIdForFrameElement({ client }) { + const { DOM, Page, Runtime } = client; + + await Page.enable(); + + const frameAttached = Page.frameAttached(); + await loadURL(DOC_FRAME); + const { frameId, parentFrameId } = await frameAttached; + + await Runtime.enable(); + + const { result: frameObj } = await Runtime.evaluate({ + expression: "document.getElementsByTagName('iframe')[0]", + }); + const { node: frame } = await DOM.describeNode({ + objectId: frameObj.objectId, + }); + + is(frame.frameId, frameId, "Reported frameId is from the frame itself"); + isnot( + frame.frameId, + parentFrameId, + "Reported frameId is not the parentFrameId" + ); +}); diff --git a/remote/test/browser/dom/browser_resolveNode.js b/remote/test/browser/dom/browser_resolveNode.js new file mode 100644 index 0000000000..553c42f5b4 --- /dev/null +++ b/remote/test/browser/dom/browser_resolveNode.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function backendNodeIdInvalidTypes({ client }) { + const { DOM } = client; + + // Bug 1625417 - CDP expects the id as number + for (const backendNodeId of [null, true, 1, /* "foo", */ [], {}]) { + let errorThrown = ""; + try { + await DOM.resolveNode({ backendNodeId }); + } catch (e) { + errorThrown = e.message; + } + ok( + errorThrown.match(/backendNodeId: string value expected/), + `Fails for invalid type: ${backendNodeId}` + ); + } +}); + +add_task(async function backendNodeIdInvalidValue({ client }) { + const { DOM } = client; + + let errorThrown = ""; + try { + await DOM.resolveNode({ backendNodeId: "-1" }); + } catch (e) { + errorThrown = e.message; + } + ok( + errorThrown.match(/No node with given id found/), + "Fails for unknown backendNodeId" + ); +}); + +add_task(async function backendNodeIdResultProperties({ client }) { + const { DOM, Runtime } = client; + + await Runtime.enable(); + const { result } = await Runtime.evaluate({ expression: "document" }); + const { node } = await DOM.describeNode({ objectId: result.objectId }); + + const { object } = await DOM.resolveNode({ + backendNodeId: node.backendNodeId, + }); + + ok(!!object, "Javascript node object returned"); + is(object.type, result.type, "Expected type returned"); + is(object.subtype, result.subtype, "Expected subtype returned"); + isnot(object.objectId, result.objectId, "Object has been duplicated"); +}); + +add_task(async function executionContextIdInvalidTypes({ client }) { + const { DOM, Runtime } = client; + + await Runtime.enable(); + const { result } = await Runtime.evaluate({ expression: "document" }); + const { node } = await DOM.describeNode({ objectId: result.objectId }); + + for (const executionContextId of [null, true, "foo", [], {}]) { + let errorThrown = ""; + try { + await DOM.resolveNode({ + backendNodeId: node.backendNodeId, + executionContextId, + }); + } catch (e) { + errorThrown = e.message; + } + ok( + errorThrown.match(/executionContextId: integer value expected/), + `Fails for invalid type: ${executionContextId}` + ); + } +}); + +add_task(async function executionContextIdInvalidValue({ client }) { + const { DOM, Runtime } = client; + + await Runtime.enable(); + const { result } = await Runtime.evaluate({ expression: "document" }); + const { node } = await DOM.describeNode({ objectId: result.objectId }); + + let errorThrown = ""; + try { + await DOM.resolveNode({ + backendNodeId: node.backendNodeId, + executionContextId: -1, + }); + } catch (e) { + errorThrown = e.message; + } + ok( + errorThrown.match(/Node with given id does not belong to the document/), + "Fails for unknown executionContextId" + ); +}); diff --git a/remote/test/browser/dom/head.js b/remote/test/browser/dom/head.js new file mode 100644 index 0000000000..7131e98b6f --- /dev/null +++ b/remote/test/browser/dom/head.js @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../head.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/test/browser/head.js", + this +); diff --git a/remote/test/browser/emulation/browser.ini b/remote/test/browser/emulation/browser.ini new file mode 100644 index 0000000000..c604dd98b3 --- /dev/null +++ b/remote/test/browser/emulation/browser.ini @@ -0,0 +1,13 @@ +[DEFAULT] +tags = remote +subsuite = remote +prefs = + remote.enabled=true +support-files = + !/remote/test/browser/chrome-remote-interface.js + !/remote/test/browser/head.js + head.js + +[browser_setDeviceMetricsOverride.js] +[browser_setTouchEmulationEnabled.js] +[browser_setUserAgentOverride.js] diff --git a/remote/test/browser/emulation/browser_setDeviceMetricsOverride.js b/remote/test/browser/emulation/browser_setDeviceMetricsOverride.js new file mode 100644 index 0000000000..7e7d278b4f --- /dev/null +++ b/remote/test/browser/emulation/browser_setDeviceMetricsOverride.js @@ -0,0 +1,418 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC_SMALL = toDataURL("<div>Hello world"); +const DOC_LARGE = toDataURL("<div style='margin: 150vh 0 0 150vw'>Hello world"); + +const MAX_WINDOW_SIZE = 10000000; + +add_task(async function dimensionsSmallerThanWindow({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + + const overrideSettings = { + width: Math.floor(layoutViewport.clientWidth / 2), + height: Math.floor(layoutViewport.clientHeight / 3), + deviceScaleFactor: 1.0, + }; + + await Emulation.setDeviceMetricsOverride(overrideSettings); + await loadURL(DOC_SMALL); + + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + overrideSettings.width, + "Expected layout viewport width set" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + overrideSettings.height, + "Expected layout viewport height set" + ); + is( + updatedLayoutMetrics.contentSize.width, + overrideSettings.width, + "Expected content size width set" + ); + is( + updatedLayoutMetrics.contentSize.height, + overrideSettings.height, + "Expected content size height set" + ); +}); + +add_task(async function dimensionsLargerThanWindow({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_LARGE); + const { layoutViewport } = await Page.getLayoutMetrics(); + + const overrideSettings = { + width: layoutViewport.clientWidth * 2, + height: layoutViewport.clientHeight * 2, + deviceScaleFactor: 1.0, + }; + + await Emulation.setDeviceMetricsOverride(overrideSettings); + await loadURL(DOC_LARGE); + + const scrollbarSize = await getScrollbarSize(); + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + overrideSettings.width - scrollbarSize.width, + "Expected layout viewport width set" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + overrideSettings.height - scrollbarSize.height, + "Expected layout viewport height set" + ); +}); + +add_task(async function noSizeChangeForSameWidthAndHeight({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + + const overrideSettings = { + width: layoutViewport.clientWidth, + height: layoutViewport.clientHeight, + deviceScaleFactor: 1.0, + }; + + await Emulation.setDeviceMetricsOverride(overrideSettings); + await loadURL(DOC_SMALL); + + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + layoutViewport.clientWidth, + "Expected layout viewport width set" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + layoutViewport.clientHeight, + "Expected layout viewport height set" + ); +}); + +add_task(async function noWidthChangeWithZeroWidth({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + + const overrideSettings = { + width: 0, + height: Math.floor(layoutViewport.clientHeight / 3), + deviceScaleFactor: 1.0, + }; + + await Emulation.setDeviceMetricsOverride(overrideSettings); + await loadURL(DOC_SMALL); + + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + layoutViewport.clientWidth, + "Expected layout viewport width set" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + overrideSettings.height, + "Expected layout viewport height set" + ); +}); + +add_task(async function noHeightChangeWithZeroHeight({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + + const overrideSettings = { + width: Math.floor(layoutViewport.clientWidth / 2), + height: 0, + deviceScaleFactor: 1.0, + }; + + await Emulation.setDeviceMetricsOverride(overrideSettings); + await loadURL(DOC_SMALL); + + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + overrideSettings.width, + "Expected layout viewport width set" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + layoutViewport.clientHeight, + "Expected layout viewport height set" + ); +}); + +add_task(async function nosizeChangeWithZeroWidthAndHeight({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + + const overrideSettings = { + width: 0, + height: 0, + deviceScaleFactor: 1.0, + }; + + await Emulation.setDeviceMetricsOverride(overrideSettings); + await loadURL(DOC_SMALL); + + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + layoutViewport.clientWidth, + "Expected layout viewport width set" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + layoutViewport.clientHeight, + "Expected layout viewport height set" + ); +}); + +add_task(async function failsWithNegativeWidth({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + const ratio = await getContentProperty("devicePixelRatio"); + + const overrideSettings = { + width: -1, + height: Math.floor(layoutViewport.clientHeight / 3), + deviceScaleFactor: 1.0, + }; + + let errorThrown = false; + try { + await Emulation.setDeviceMetricsOverride(overrideSettings); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "Negative width raised error"); + + await loadURL(DOC_SMALL); + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + layoutViewport.clientWidth, + "Visible layout width hasn't been changed" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + layoutViewport.clientHeight, + "Visible layout height hasn't been changed" + ); + is( + await getContentProperty("devicePixelRatio"), + ratio, + "Device pixel ratio hasn't been changed" + ); +}); + +add_task(async function failsWithTooLargeWidth({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + const ratio = await getContentProperty("devicePixelRatio"); + + const overrideSettings = { + width: MAX_WINDOW_SIZE + 1, + height: Math.floor(layoutViewport.clientHeight / 3), + deviceScaleFactor: 1.0, + }; + + let errorThrown = false; + try { + await Emulation.setDeviceMetricsOverride(overrideSettings); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "Too large width raised error"); + + await loadURL(DOC_SMALL); + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + layoutViewport.clientWidth, + "Visible layout width hasn't been changed" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + layoutViewport.clientHeight, + "Visible layout height hasn't been changed" + ); + is( + await getContentProperty("devicePixelRatio"), + ratio, + "Device pixel ratio hasn't been changed" + ); +}); + +add_task(async function failsWithNegativeHeight({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + const ratio = await getContentProperty("devicePixelRatio"); + + const overrideSettings = { + width: Math.floor(layoutViewport.clientWidth / 2), + height: -1, + deviceScaleFactor: 1.0, + }; + + let errorThrown = false; + try { + await Emulation.setDeviceMetricsOverride(overrideSettings); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "Negative height raised error"); + + await loadURL(DOC_SMALL); + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + layoutViewport.clientWidth, + "Visible layout width hasn't been changed" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + layoutViewport.clientHeight, + "Visible layout height hasn't been changed" + ); + is( + await getContentProperty("devicePixelRatio"), + ratio, + "Device pixel ratio hasn't been changed" + ); +}); + +add_task(async function failsWithTooLargeHeight({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + const ratio = await getContentProperty("devicePixelRatio"); + + const overrideSettings = { + width: Math.floor(layoutViewport.clientWidth / 2), + height: MAX_WINDOW_SIZE + 1, + deviceScaleFactor: 1.0, + }; + + let errorThrown = false; + try { + await Emulation.setDeviceMetricsOverride(overrideSettings); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "Too large height raised error"); + + await loadURL(DOC_SMALL); + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + layoutViewport.clientWidth, + "Visible layout width hasn't been changed" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + layoutViewport.clientHeight, + "Visible layout height hasn't been changed" + ); + is( + await getContentProperty("devicePixelRatio"), + ratio, + "Device pixel ratio hasn't been changed" + ); +}); + +add_task(async function setDevicePixelRatio({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + const ratio_orig = await getContentProperty("devicePixelRatio"); + + const overrideSettings = { + width: layoutViewport.clientWidth, + height: layoutViewport.clientHeight, + deviceScaleFactor: ratio_orig * 2, + }; + + // Set a custom pixel ratio + await Emulation.setDeviceMetricsOverride(overrideSettings); + await loadURL(DOC_SMALL); + + is( + await getContentProperty("devicePixelRatio"), + ratio_orig * 2, + "Expected device pixel ratio set" + ); +}); + +add_task(async function failsWithNegativeRatio({ client }) { + const { Emulation, Page } = client; + + await loadURL(DOC_SMALL); + const { layoutViewport } = await Page.getLayoutMetrics(); + const ratio = await getContentProperty("devicePixelRatio"); + + const overrideSettings = { + width: Math.floor(layoutViewport.clientHeight / 2), + height: Math.floor(layoutViewport.clientHeight / 3), + deviceScaleFactor: -1, + }; + + let errorThrown = false; + try { + await Emulation.setDeviceMetricsOverride(overrideSettings); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "Negative device scale factor raised error"); + + await loadURL(DOC_SMALL); + const updatedLayoutMetrics = await Page.getLayoutMetrics(); + + is( + updatedLayoutMetrics.layoutViewport.clientWidth, + layoutViewport.clientWidth, + "Visible layout width hasn't been changed" + ); + is( + updatedLayoutMetrics.layoutViewport.clientHeight, + layoutViewport.clientHeight, + "Visible layout height hasn't been changed" + ); + is( + await getContentProperty("devicePixelRatio"), + ratio, + "Device pixel ratio hasn't been changed" + ); +}); diff --git a/remote/test/browser/emulation/browser_setTouchEmulationEnabled.js b/remote/test/browser/emulation/browser_setTouchEmulationEnabled.js new file mode 100644 index 0000000000..48315ed0e9 --- /dev/null +++ b/remote/test/browser/emulation/browser_setTouchEmulationEnabled.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC_TOUCH = toDataURL("<div>Hello world"); + +add_task(async function invalidEnabledType({ client }) { + const { Emulation } = client; + + for (const enabled of [null, "", 1, [], {}]) { + let errorThrown = ""; + try { + await Emulation.setTouchEmulationEnabled({ enabled }); + } catch (e) { + errorThrown = e.message; + } + ok( + errorThrown.match(/enabled: boolean value expected/), + `Fails with invalid type: ${enabled}` + ); + } +}); + +add_task(async function enableAndDisable({ client }) { + const { Emulation, Runtime } = client; + const url = toDataURL("<p>foo"); + + await enableRuntime(client); + + let contextCreated = Runtime.executionContextCreated(); + await loadURL(url); + let result = await contextCreated; + await assertTouchEnabled(client, result.context, false); + + await Emulation.setTouchEmulationEnabled({ enabled: true }); + contextCreated = Runtime.executionContextCreated(); + await loadURL(url); + result = await contextCreated; + await assertTouchEnabled(client, result.context, true); + + await Emulation.setTouchEmulationEnabled({ enabled: false }); + contextCreated = Runtime.executionContextCreated(); + await loadURL(url); + result = await contextCreated; + await assertTouchEnabled(client, result.context, false); +}); + +add_task(async function receiveTouchEventsWhenEnabled({ client }) { + const { Emulation, Runtime } = client; + + await enableRuntime(client); + + await Emulation.setTouchEmulationEnabled({ enabled: true }); + const contextCreated = Runtime.executionContextCreated(); + await loadURL(DOC_TOUCH); + const { context } = await contextCreated; + + await assertTouchEnabled(client, context, true); + + const { result } = await evaluate(client, context.id, () => { + return new Promise(resolve => { + window.ontouchstart = () => { + resolve(true); + }; + window.dispatchEvent(new Event("touchstart")); + resolve(false); + }); + }); + is(result.value, true, "Received touch event"); +}); + +async function assertTouchEnabled(client, context, expectedStatus) { + const { result } = await evaluate(client, context.id, () => { + return "ontouchstart" in window; + }); + + if (expectedStatus) { + ok(result.value, "Touch emulation enabled"); + } else { + ok(!result.value, "Touch emulation disabled"); + } +} diff --git a/remote/test/browser/emulation/browser_setUserAgentOverride.js b/remote/test/browser/emulation/browser_setUserAgentOverride.js new file mode 100644 index 0000000000..9275675452 --- /dev/null +++ b/remote/test/browser/emulation/browser_setUserAgentOverride.js @@ -0,0 +1,147 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL(`<script>document.write(navigator.userAgent);</script>`); + +add_task(async function invalidPlatform({ client }) { + const { Emulation } = client; + const userAgent = "Mozilla/5.0 (rv: 23) Romanesco/42.0\n"; + + for (const platform of [null, true, 1, [], {}]) { + let errorThrown = ""; + try { + await Emulation.setUserAgentOverride({ userAgent, platform }); + } catch (e) { + errorThrown = e.message; + } + ok( + errorThrown.match(/platform: string value expected/), + `Fails with invalid type: ${platform}` + ); + } +}); + +add_task(async function setAndResetUserAgent({ client }) { + const { Emulation } = client; + const userAgent = "Mozilla/5.0 (rv: 23) Romanesco/42.0"; + + await loadURL(DOC); + const originalUserAgent = await getNavigatorProperty("userAgent"); + + isnot(originalUserAgent, userAgent, "Custom user agent hasn't been set"); + + await Emulation.setUserAgentOverride({ userAgent }); + await loadURL(DOC); + is( + await getNavigatorProperty("userAgent"), + userAgent, + "Custom user agent has been set" + ); + + await Emulation.setUserAgentOverride({ userAgent: "" }); + await loadURL(DOC); + is( + await getNavigatorProperty("userAgent"), + originalUserAgent, + "Custom user agent has been reset" + ); +}); + +add_task(async function invalidUserAgent({ Emulation }) { + const userAgent = "Mozilla/5.0 (rv: 23) Romanesco/42.0\n"; + + await loadURL(DOC); + isnot( + await getNavigatorProperty("userAgent"), + userAgent, + "Custom user agent hasn't been set" + ); + + let errorThrown = false; + try { + await Emulation.setUserAgentOverride({ userAgent }); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "Invalid user agent format raised error"); +}); + +add_task(async function setAndResetPlatform({ client }) { + const { Emulation } = client; + const userAgent = "Mozilla/5.0 (rv: 23) Romanesco/42.0"; + const platform = "foobar"; + + await loadURL(DOC); + const originalUserAgent = await getNavigatorProperty("userAgent"); + const originalPlatform = await getNavigatorProperty("platform"); + + isnot(userAgent, originalUserAgent, "Custom user agent hasn't been set"); + isnot(platform, originalPlatform, "Custom platform hasn't been set"); + + await Emulation.setUserAgentOverride({ userAgent, platform }); + await loadURL(DOC); + is( + await getNavigatorProperty("userAgent"), + userAgent, + "Custom user agent has been set" + ); + is( + await getNavigatorProperty("platform"), + platform, + "Custom platform has been set" + ); + + await Emulation.setUserAgentOverride({ userAgent: "", platform: "" }); + await loadURL(DOC); + is( + await getNavigatorProperty("userAgent"), + originalUserAgent, + "Custom user agent has been reset" + ); + is( + await getNavigatorProperty("platform"), + originalPlatform, + "Custom platform has been reset" + ); +}); + +add_task(async function notSetForNewContext({ client }) { + const { Emulation, Target } = client; + const userAgent = "Mozilla/5.0 (rv: 23) Romanesco/42.0"; + const platform = "foobar"; + + await Emulation.setUserAgentOverride({ userAgent, platform }); + await loadURL(DOC); + is( + await getNavigatorProperty("userAgent"), + userAgent, + "Custom user agent has been set" + ); + is( + await getNavigatorProperty("platform"), + platform, + "Custom platform has been set" + ); + + const { targetInfo } = await openTab(Target); + await Target.activateTarget({ targetId: targetInfo.targetId }); + + isnot( + await getNavigatorProperty("userAgent"), + userAgent, + "Custom user agent has not been set" + ); + isnot( + await getNavigatorProperty("platform"), + platform, + "Custom platform has not been set" + ); +}); + +async function getNavigatorProperty(prop) { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [prop], _prop => { + return content.navigator[_prop]; + }); +} diff --git a/remote/test/browser/emulation/head.js b/remote/test/browser/emulation/head.js new file mode 100644 index 0000000000..7131e98b6f --- /dev/null +++ b/remote/test/browser/emulation/head.js @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../head.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/test/browser/head.js", + this +); diff --git a/remote/test/browser/head.js b/remote/test/browser/head.js new file mode 100644 index 0000000000..d91fc67bba --- /dev/null +++ b/remote/test/browser/head.js @@ -0,0 +1,644 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +const { RemoteAgentError } = ChromeUtils.import( + "chrome://remote/content/Error.jsm" +); +const { RemoteAgent } = ChromeUtils.import( + "chrome://remote/content/RemoteAgent.jsm" +); + +const TIMEOUT_MULTIPLIER = SpecialPowers.isDebugBuild ? 4 : 1; +const TIMEOUT_EVENTS = 1000 * TIMEOUT_MULTIPLIER; + +/* +add_task() is overriden to setup and teardown a test environment +making it easier to write browser-chrome tests for the remote +debugger. + +Before the task is run, the nsIRemoteAgent listener is started and +a CDP client is connected to it. A new tab is also added. These +three things are exposed to the provided task like this: + + add_task(async function testName(client, CDP, tab) { + // client is an instance of the CDP class + // CDP is ./chrome-remote-interface.js + // tab is a fresh tab, destroyed after the test + }); + +Also target discovery is getting enabled, which means that targetCreated, +targetDestroyed, and targetInfoChanged events will be received by the client. + +add_plain_task() may be used to write test tasks without the implicit +setup and teardown described above. +*/ + +const add_plain_task = add_task.bind(this); + +this.add_task = function(taskFn, opts = {}) { + const { + createTab = true, // By default run each test in its own tab + } = opts; + + const fn = async function() { + let client, tab, target; + + await RemoteAgent.listen(Services.io.newURI("http://localhost:9222")); + info("CDP server started"); + + try { + const CDP = await getCDP(); + + if (createTab) { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + const browsingContextId = tab.linkedBrowser.browsingContext.id; + + const targets = await CDP.List(); + target = targets.find( + target => target.browsingContextId === browsingContextId + ); + } + + client = await CDP({ target }); + info("CDP client instantiated"); + + // Bug 1605722 - Workaround to not hang when waiting for Target events + await getDiscoveredTargets(client.Target); + + await taskFn({ client, CDP, tab }); + + if (createTab) { + // taskFn may resolve within a tick after opening a new tab. + // We shouldn't remove the newly opened tab in the same tick. + // Wait for the next tick here. + await TestUtils.waitForTick(); + BrowserTestUtils.removeTab(tab); + } + } catch (e) { + // Display better error message with the server side stacktrace + // if an error happened on the server side: + if (e.response) { + throw RemoteAgentError.fromJSON(e.response); + } else { + throw e; + } + } finally { + if (client) { + await client.close(); + info("CDP client closed"); + } + + await RemoteAgent.close(); + info("CDP server stopped"); + + // Close any additional tabs, so that only a single tab remains open + while (gBrowser.tabs.length > 1) { + gBrowser.removeCurrentTab(); + } + } + }; + + Object.defineProperty(fn, "name", { value: taskFn.name, writable: false }); + add_plain_task(fn); +}; + +/** + * Create a test document in an invisible window. + * This window will be automatically closed on test teardown. + */ +function createTestDocument() { + const browser = Services.appShell.createWindowlessBrowser(true); + registerCleanupFunction(() => browser.close()); + + // Create a system principal content viewer to ensure there is a valid + // empty document using system principal and avoid any wrapper issues + // when using document's JS Objects. + const webNavigation = browser.docShell.QueryInterface(Ci.nsIWebNavigation); + const system = Services.scriptSecurityManager.getSystemPrincipal(); + webNavigation.createAboutBlankContentViewer(system, system); + + return webNavigation.document; +} + +/** + * Retrieve an intance of CDP object from chrome-remote-interface library + */ +async function getCDP() { + // Instantiate a background test document in order to load the library + // as in a web page + const document = createTestDocument(); + + const window = document.defaultView.wrappedJSObject; + Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/test/browser/chrome-remote-interface.js", + window + ); + + // Implements `criRequest` to be called by chrome-remote-interface + // library in order to do the cross-domain http request, which, + // in a regular Web page, is impossible. + window.criRequest = (options, callback) => { + const { host, port, path } = options; + const url = `http://${host}:${port}${path}`; + const xhr = new XMLHttpRequest(); + xhr.open("GET", url, true); + + // Prevent "XML Parsing Error: syntax error" error messages + xhr.overrideMimeType("text/plain"); + + xhr.send(null); + xhr.onload = () => callback(null, xhr.responseText); + xhr.onerror = e => callback(e, null); + }; + + return window.CDP; +} + +async function getScrollbarSize() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const scrollbarHeight = {}; + const scrollbarWidth = {}; + + content.windowUtils.getScrollbarSize( + false, + scrollbarWidth, + scrollbarHeight + ); + return { + width: scrollbarWidth.value, + height: scrollbarHeight.value, + }; + }); +} + +function getTargets(CDP) { + return new Promise((resolve, reject) => { + CDP.List(null, (err, targets) => { + if (err) { + reject(err); + return; + } + resolve(targets); + }); + }); +} + +// Wait for all Target.targetCreated events. One for each tab, plus the one +// for the main process target. +async function getDiscoveredTargets(Target) { + return new Promise(resolve => { + const targets = []; + + const unsubscribe = Target.targetCreated(target => { + targets.push(target); + + if (targets.length >= gBrowser.tabs.length + 1) { + unsubscribe(); + resolve(targets); + } + }); + + Target.setDiscoverTargets({ discover: true }); + }); +} + +async function openTab(Target, options = {}) { + const { activate = false } = options; + + info("Create a new tab and wait for the target to be created"); + const targetCreated = Target.targetCreated(); + const newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + const { targetInfo } = await targetCreated; + + is(targetInfo.type, "page"); + + if (activate) { + await Target.activateTarget({ + targetId: targetInfo.targetId, + }); + info(`New tab with target id ${targetInfo.targetId} created and activated`); + } else { + info(`New tab with target id ${targetInfo.targetId} created`); + } + + return { targetInfo, newTab }; +} + +async function openWindow(Target, options = {}) { + const { activate = false } = options; + + info("Create a new window and wait for the target to be created"); + const targetCreated = Target.targetCreated(); + const newWindow = await BrowserTestUtils.openNewBrowserWindow(); + const newTab = newWindow.gBrowser.selectedTab; + const { targetInfo } = await targetCreated; + is(targetInfo.type, "page"); + + if (activate) { + await Target.activateTarget({ + targetId: targetInfo.targetId, + }); + info( + `New window with target id ${targetInfo.targetId} created and activated` + ); + } else { + info(`New window with target id ${targetInfo.targetId} created`); + } + + return { targetInfo, newWindow, newTab }; +} + +/** Creates a data URL for the given source document. */ +function toDataURL(src, doctype = "html") { + let doc, mime; + switch (doctype) { + case "html": + mime = "text/html;charset=utf-8"; + doc = `<!doctype html>\n<meta charset=utf-8>\n${src}`; + break; + default: + throw new Error("Unexpected doctype: " + doctype); + } + + return `data:${mime},${encodeURIComponent(doc)}`; +} + +function convertArgument(arg) { + if (typeof arg === "bigint") { + return { unserializableValue: `${arg.toString()}n` }; + } + if (Object.is(arg, -0)) { + return { unserializableValue: "-0" }; + } + if (Object.is(arg, Infinity)) { + return { unserializableValue: "Infinity" }; + } + if (Object.is(arg, -Infinity)) { + return { unserializableValue: "-Infinity" }; + } + if (Object.is(arg, NaN)) { + return { unserializableValue: "NaN" }; + } + + return { value: arg }; +} + +async function evaluate(client, contextId, pageFunction, ...args) { + const { Runtime } = client; + + if (typeof pageFunction === "string") { + return Runtime.evaluate({ + expression: pageFunction, + contextId, + returnByValue: true, + awaitPromise: true, + }); + } else if (typeof pageFunction === "function") { + return Runtime.callFunctionOn({ + functionDeclaration: pageFunction.toString(), + executionContextId: contextId, + arguments: args.map(convertArgument), + returnByValue: true, + awaitPromise: true, + }); + } + throw new Error("pageFunction: expected 'string' or 'function'"); +} + +/** + * Load a given URL in the currently selected tab + */ +async function loadURL(url, expectedURL = undefined) { + expectedURL = expectedURL || url; + + const browser = gBrowser.selectedTab.linkedBrowser; + const loaded = BrowserTestUtils.browserLoaded(browser, true, expectedURL); + + BrowserTestUtils.loadURI(browser, url); + await loaded; +} + +/** + * Enable the Runtime domain + */ +async function enableRuntime(client) { + const { Runtime } = client; + + // Enable watching for new execution context + await Runtime.enable(); + info("Runtime domain has been enabled"); + + // Calling Runtime.enable will emit executionContextCreated for the existing contexts + const { context } = await Runtime.executionContextCreated(); + ok(!!context.id, "The execution context has an id"); + ok(context.auxData.isDefault, "The execution context is the default one"); + ok(!!context.auxData.frameId, "The execution context has a frame id set"); + + return context; +} + +/** + * Retrieve the value of a property on the content window. + */ +function getContentProperty(prop) { + info(`Retrieve ${prop} on the content window`); + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [prop], + _prop => content[_prop] + ); +} + +/** + * Retrieve all frames for the current tab as flattened list. + * + * @return {Map<number, Frame>} + * Flattened list of frames as Map + */ +async function getFlattenedFrameTree(client) { + const { Page } = client; + + function flatten(frames) { + return frames.reduce((result, current) => { + result.set(current.frame.id, current.frame); + if (current.childFrames) { + const frames = flatten(current.childFrames); + result = new Map([...result, ...frames]); + } + return result; + }, new Map()); + } + + const { frameTree } = await Page.getFrameTree(); + return flatten(Array(frameTree)); +} + +/** + * Return a new promise, which resolves after ms have been elapsed + */ +function timeoutPromise(ms) { + return new Promise(resolve => { + window.setTimeout(resolve, ms); + }); +} + +/** Fail a test. */ +function fail(message) { + ok(false, message); +} + +/** + * Create a file with the specified contents. + * + * @param {string} contents + * Contents of the file. + * @param {Object} options + * @param {string=} options.path + * Path of the file. Defaults to the temporary directory. + * @param {boolean=} options.remove + * If true, automatically remove the file after the test. Defaults to true. + * + * @return {Promise} + * @resolves {string} + * Returns the final path of the created file. + */ +async function createFile(contents, options = {}) { + let { path = null, remove = true } = options; + + if (!path) { + const basePath = OS.Path.join(OS.Constants.Path.tmpDir, "remote-agent.txt"); + const { file, path: tmpPath } = await OS.File.openUnique(basePath, { + humanReadable: true, + }); + await file.close(); + path = tmpPath; + } + + let encoder = new TextEncoder(); + let array = encoder.encode(contents); + + const count = await OS.File.writeAtomic(path, array, { + encoding: "utf-8", + tmpPath: path + ".tmp", + }); + is(count, contents.length, "All data has been written to file"); + + const file = await OS.File.open(path); + + // Automatically remove the file once the test has finished + if (remove) { + registerCleanupFunction(async () => { + await file.close(); + await OS.File.remove(path, { ignoreAbsent: true }); + }); + } + + return { file, path }; +} + +async function throwScriptError(options = {}) { + const { inContent = true } = options; + + const addScriptErrorInternal = ({ options }) => { + const { + flag = Ci.nsIScriptError.errorFlag, + innerWindowId = content.windowGlobalChild.innerWindowId, + } = options; + + const scriptError = Cc["@mozilla.org/scripterror;1"].createInstance( + Ci.nsIScriptError + ); + scriptError.initWithWindowID( + options.text, + options.sourceName || "sourceName", + null, + options.lineNumber || 0, + options.columnNumber || 0, + flag, + options.category || "javascript", + innerWindowId + ); + Services.console.logMessage(scriptError); + }; + + if (inContent) { + ContentTask.spawn( + gBrowser.selectedBrowser, + { options }, + addScriptErrorInternal + ); + } else { + options.innerWindowId = window.windowGlobalChild.innerWindowId; + addScriptErrorInternal({ options }); + } +} + +class RecordEvents { + /** + * A timeline of events chosen by calls to `addRecorder`. + * Call `configure`` for each client event you want to record. + * Then `await record(someTimeout)` to record a timeline that you + * can make assertions about. + * + * const history = new RecordEvents(expectedNumberOfEvents); + * + * history.addRecorder({ + * event: Runtime.executionContextDestroyed, + * eventName: "Runtime.executionContextDestroyed", + * messageFn: payload => { + * return `Received Runtime.executionContextDestroyed for id ${payload.executionContextId}`; + * }, + * }); + * + * + * @param {number} total + * Number of expected events. Stop recording when this number is exceeded. + * + */ + constructor(total) { + this.events = []; + this.promises = new Set(); + this.subscriptions = new Set(); + this.total = total; + } + + /** + * Configure an event to be recorded and logged. + * The recording stops once we accumulate more than the expected + * total of all configured events. + * + * @param {Object} options + * @param {CDPEvent} options.event + * https://github.com/cyrus-and/chrome-remote-interface#clientdomaineventcallback + * @param {string} options.eventName + * Name to use for reporting. + * @param {Function=} options.callback + * ({ eventName, payload }) => {} to be called when each event is received + * @param {function(payload):string=} options.messageFn + */ + addRecorder(options = {}) { + const { + event, + eventName, + messageFn = () => `Recorded ${eventName}`, + callback, + } = options; + + const promise = new Promise(resolve => { + const unsubscribe = event(payload => { + info(messageFn(payload)); + this.events.push({ eventName, payload, index: this.events.length }); + callback?.({ eventName, payload, index: this.events.length - 1 }); + if (this.events.length > this.total) { + this.subscriptions.delete(unsubscribe); + unsubscribe(); + resolve(this.events); + } + }); + this.subscriptions.add(unsubscribe); + }); + + this.promises.add(promise); + } + + /** + * Register a promise to await while recording the timeline. The returned + * callback resolves the registered promise and adds `step` + * to the timeline, along with an associated payload, if provided. + * + * @param {string} step + * @return {Function} callback + */ + addPromise(step) { + let callback; + const promise = new Promise(resolve => { + callback = value => { + resolve(); + info(`Recorded ${step}`); + this.events.push({ + eventName: step, + payload: value, + index: this.events.length, + }); + return value; + }; + }); + + this.promises.add(promise); + return callback; + } + + /** + * Record events until we hit the timeout or the expected total is exceeded. + * + * @param {number=} timeout + * Timeout in milliseconds. Defaults to 1000. + * + * @return {Array<{ eventName, payload, index }>} Recorded events + */ + async record(timeout = TIMEOUT_EVENTS) { + await Promise.race([Promise.all(this.promises), timeoutPromise(timeout)]); + for (const unsubscribe of this.subscriptions) { + unsubscribe(); + } + return this.events; + } + + /** + * Filter events based on predicate + * + * @param {Function} predicate + * + * @return {Array<{ eventName, payload, index }>} + * The list of events matching the filter. + */ + filter(predicate) { + return this.events.filter(predicate); + } + + /** + * Find first occurrence of the given event. + * + * @param {string} eventName + * + * @return {{ eventName, payload, index }} The event, if any. + */ + findEvent(eventName) { + const event = this.events.find(el => el.eventName == eventName); + if (event) { + return event; + } + return {}; + } + + /** + * Find given events. + * + * @param {string} eventName + * + * @return {Array<{ eventName, payload, index }>} + * The events, if any. + */ + findEvents(eventName) { + return this.events.filter(event => event.eventName == eventName); + } + + /** + * Find index of first occurrence of the given event. + * + * @param {string} eventName + * + * @return {number} The event index, -1 if not found. + */ + indexOf(eventName) { + const event = this.events.find(el => el.eventName == eventName); + if (event) { + return event.index; + } + return -1; + } +} diff --git a/remote/test/browser/input/browser.ini b/remote/test/browser/input/browser.ini new file mode 100644 index 0000000000..a2fb2f515e --- /dev/null +++ b/remote/test/browser/input/browser.ini @@ -0,0 +1,17 @@ +[DEFAULT] +tags = remote +subsuite = remote +prefs = + remote.enabled=true +support-files = + !/remote/test/browser/chrome-remote-interface.js + !/remote/test/browser/head.js + head.js + doc_events.html + doc_dispatchKeyEvent_race.html + +[browser_dispatchKeyEvent.js] +[browser_dispatchKeyEvent_events.js] +[browser_dispatchKeyEvent_race.js] +skip-if = socketprocess_networking +[browser_dispatchMouseEvent.js] diff --git a/remote/test/browser/input/browser_dispatchKeyEvent.js b/remote/test/browser/input/browser_dispatchKeyEvent.js new file mode 100644 index 0000000000..dc94bc57b3 --- /dev/null +++ b/remote/test/browser/input/browser_dispatchKeyEvent.js @@ -0,0 +1,169 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function testTypingPrintableCharacters({ client }) { + await setupForInput(toDataURL("<input>")); + const { Input } = client; + + info("Write 'h'"); + await sendTextKey(Input, "h"); + await checkInputContent("h", 1); + + info("Write 'H'"); + await sendTextKey(Input, "H"); + await checkInputContent("hH", 2); + + info("Send char type event for char [’]"); + await Input.dispatchKeyEvent({ + type: "char", + modifiers: 0, + key: "’", + }); + await checkInputContent("hH’", 3); +}); + +add_task(async function testArrowKeys({ client }) { + await setupForInput(toDataURL("<input>")); + const { Input } = client; + + await sendText(Input, "hH’"); + info("Send Left"); + await sendRawKey(Input, "ArrowLeft"); + await checkInputContent("hH’", 2); + + info("Write 'a'"); + await sendTextKey(Input, "a"); + await checkInputContent("hHa’", 3); + + info("Send Left"); + await sendRawKey(Input, "ArrowLeft"); + await checkInputContent("hHa’", 2); + + info("Send Left"); + await sendRawKey(Input, "ArrowLeft"); + await checkInputContent("hHa’", 1); + + info("Write 'a'"); + await sendTextKey(Input, "a"); + await checkInputContent("haHa’", 2); + + info("Send ALT/CONTROL + Right"); + let modCode = isMac ? alt : ctrl; + let modKey = isMac ? "Alt" : "Control"; + await dispatchKeyEvent(Input, modKey, "rawKeyDown", modCode); + await dispatchKeyEvent(Input, "ArrowRight", "rawKeyDown", modCode); + await dispatchKeyEvent(Input, "ArrowRight", "keyUp"); + await dispatchKeyEvent(Input, modKey, "keyUp"); + await checkInputContent("haHa’", 5); +}); + +add_task(async function testBackspace({ client }) { + await setupForInput(toDataURL("<input>")); + const { Input } = client; + + await sendText(Input, "haHa’"); + + info("Delete every character in the input"); + await checkBackspace(Input, "haHa"); + await checkBackspace(Input, "haH"); + await checkBackspace(Input, "ha"); + await checkBackspace(Input, "h"); + await checkBackspace(Input, ""); +}); + +add_task(async function testShiftSelect({ client }) { + await setupForInput(toDataURL("<input>")); + const { Input } = client; + await resetInput("word 2 word3"); + + info("Send Shift + Left (select one char to the left)"); + await dispatchKeyEvent(Input, "Shift", "rawKeyDown", shift); + await sendRawKey(Input, "ArrowLeft", shift); + await sendRawKey(Input, "ArrowLeft", shift); + await sendRawKey(Input, "ArrowLeft", shift); + info("(deleteContentBackward)"); + await checkBackspace(Input, "word 2 wo"); + await dispatchKeyEvent(Input, "Shift", "keyUp"); + + await resetInput("word 2 wo"); + info("Send Shift + Left (select one char to the left)"); + await dispatchKeyEvent(Input, "Shift", "rawKeyDown", shift); + await sendRawKey(Input, "ArrowLeft", shift); + await sendRawKey(Input, "ArrowLeft", shift); + await sendTextKey(Input, "H"); + await checkInputContent("word 2 H", 8); + await dispatchKeyEvent(Input, "Shift", "keyUp"); +}); + +add_task(async function testSelectWord({ client }) { + await setupForInput(toDataURL("<input>")); + const { Input } = client; + await resetInput("word 2 word3"); + + info("Send Shift + Ctrl/Alt + Left (select one word to the left)"); + const { primary, primaryKey } = keyForPlatform(); + const combined = shift | primary; + await dispatchKeyEvent(Input, "Shift", "rawKeyDown", shift); + await dispatchKeyEvent(Input, primaryKey, "rawKeyDown", combined); + await sendRawKey(Input, "ArrowLeft", combined); + await sendRawKey(Input, "ArrowLeft", combined); + await dispatchKeyEvent(Input, "Shift", "keyUp", primary); + await dispatchKeyEvent(Input, primaryKey, "keyUp"); + info("(deleteContentBackward)"); + await checkBackspace(Input, "word "); +}); + +add_task(async function testSelectDelete({ client }) { + await setupForInput(toDataURL("<input>")); + const { Input } = client; + await resetInput("word 2 word3"); + + info("Send Ctrl/Alt + Backspace (deleteWordBackward)"); + const { primary, primaryKey } = keyForPlatform(); + await dispatchKeyEvent(Input, primaryKey, "rawKeyDown", primary); + await checkBackspace(Input, "word 2 ", primary); + await dispatchKeyEvent(Input, primaryKey, "keyUp"); + + await resetInput("word 2 "); + await sendText(Input, "word4"); + await sendRawKey(Input, "ArrowLeft"); + await sendRawKey(Input, "ArrowLeft"); + await checkInputContent("word 2 word4", 10); + + if (isMac) { + info("Send Meta + Backspace (deleteSoftLineBackward)"); + await dispatchKeyEvent(Input, "Meta", "rawKeyDown", meta); + await sendRawKey(Input, "Backspace", meta); + await dispatchKeyEvent(Input, "Meta", "keyUp"); + await checkInputContent("d4", 0); + } +}); + +add_task(async function testCtrlShiftArrows({ client }) { + await loadURL( + toDataURL('<select multiple size="3"><option>a<option>b<option>c</select>') + ); + const { Input } = client; + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + const select = content.document.querySelector("select"); + select.selectedIndex = 0; + select.focus(); + }); + + const combined = shift | ctrl; + await dispatchKeyEvent(Input, "Control", "rawKeyDown", shift); + await dispatchKeyEvent(Input, "Shift", "rawKeyDown", combined); + await sendRawKey(Input, "ArrowDown", combined); + await dispatchKeyEvent(Input, "Control", "keyUp", shift); + await dispatchKeyEvent(Input, "Shift", "keyUp"); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + const select = content.document.querySelector("select"); + ok(select[0].selected, "First option should be selected"); + ok(select[1].selected, "Second option should be selected"); + ok(!select[2].selected, "Third option should not be selected"); + }); +}); diff --git a/remote/test/browser/input/browser_dispatchKeyEvent_events.js b/remote/test/browser/input/browser_dispatchKeyEvent_events.js new file mode 100644 index 0000000000..24a5efdcb6 --- /dev/null +++ b/remote/test/browser/input/browser_dispatchKeyEvent_events.js @@ -0,0 +1,129 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PAGE_URL = + "http://example.com/browser/remote/test/browser/input/doc_events.html"; + +add_task(async function testShiftEvents({ client }) { + await setupForInput(PAGE_URL); + const { Input } = client; + await resetEvents(); + + await withModifier(Input, "Shift", "shift", "A"); + await checkInputContent("A", 1); + let events = await getEvents(); + checkEvent(events[0], "keydown", "Shift", "shift", true); + checkEvent(events[1], "keydown", "A", "shift", true); + checkEvent(events[2], "keypress", "A", "shift", true); + checkProperties({ data: "A", inputType: "insertText" }, events[3]); + checkEvent(events[4], "keyup", "A", "shift", true); + checkEvent(events[5], "keyup", "Shift", "shift", false); + await resetEvents(); + + await withModifier(Input, "Shift", "shift", "Enter"); + events = await getEvents(); + checkEvent(events[2], "keypress", "Enter", "shift", true); + await resetEvents(); + + await withModifier(Input, "Shift", "shift", "Tab"); + events = await getEvents(); + checkEvent(events[1], "keydown", "Tab", "shift", true); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + const input = content.document.querySelector("input"); + isnot(input, content.document.activeElement, "input should lose focus"); + }); +}); + +add_task(async function testAltEvents({ client }) { + await setupForInput(PAGE_URL); + const { Input } = client; + + await withModifier(Input, "Alt", "alt", "a"); + if (isMac) { + await checkInputContent("a", 1); + } else { + await checkInputContent("", 0); + } + let events = await getEvents(); + checkEvent(events[1], "keydown", "a", "alt", true); + checkEvent(events[events.length - 1], "keyup", "Alt", "alt", false); +}); + +add_task(async function testControlEvents({ client }) { + await setupForInput(PAGE_URL); + const { Input } = client; + + await withModifier(Input, "Control", "ctrl", "`"); + let events = await getEvents(); + // no keypress or input event + checkEvent(events[1], "keydown", "`", "ctrl", true); + checkEvent(events[events.length - 1], "keyup", "Control", "ctrl", false); +}); + +add_task(async function testMetaEvents({ client }) { + if (!isMac) { + return; + } + await setupForInput(PAGE_URL); + const { Input } = client; + + await withModifier(Input, "Meta", "meta", "a"); + let events = await getEvents(); + // no keypress or input event + checkEvent(events[1], "keydown", "a", "meta", true); + checkEvent(events[events.length - 1], "keyup", "Meta", "meta", false); +}); + +add_task(async function testShiftClick({ client }) { + await loadURL(PAGE_URL); + const { Input } = client; + await resetEvents(); + + await dispatchKeyEvent(Input, "Shift", "rawKeyDown", shift); + info("Click the 'pointers' div."); + await Input.dispatchMouseEvent({ + type: "mousePressed", + x: 80, + y: 180, + modifiers: shift, + }); + await Input.dispatchMouseEvent({ + type: "mouseReleased", + x: 80, + y: 180, + modifiers: shift, + }); + await dispatchKeyEvent(Input, "Shift", "keyUp", shift); + let events = await getEvents(); + checkProperties({ type: "click", shiftKey: true, button: 0 }, events[2]); +}); + +async function getEvents() { + const events = await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + return content.eval("allEvents"); + }); + info(`Events: ${JSON.stringify(events)}`); + return events; +} + +async function resetEvents() { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.eval("resetEvents()"); + const events = content.eval("allEvents"); + is(events.length, 0, "List of events should be empty"); + }); +} + +function checkEvent(event, type, key, property, expectedValue) { + let expected = { type, key }; + expected[property] = expectedValue; + checkProperties(expected, event, "Event"); +} + +function checkProperties(expectedObj, targetObj, message = "Compare objects") { + for (const prop in expectedObj) { + is(targetObj[prop], expectedObj[prop], message + `: check ${prop}`); + } +} diff --git a/remote/test/browser/input/browser_dispatchKeyEvent_race.js b/remote/test/browser/input/browser_dispatchKeyEvent_race.js new file mode 100644 index 0000000000..97daccf0ee --- /dev/null +++ b/remote/test/browser/input/browser_dispatchKeyEvent_race.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Here we test that the `dispatchKeyEvent` API resolves after all the synchronous event +// handlers from the content page have been flushed. +// +// Say the content page has an event handler such as: +// +// el.addEventListener("keyup", () => { +// doSomeVeryLongProcessing(); // <- takes a long time but is synchronous! +// window.myVariable = "newValue"; +// }); +// +// And imagine this is tested via: +// +// await Input.dispatchKeyEvent(...); +// const myVariable = await Runtime.evaluate({ expression: "window.myVariable" }); +// equals(myVariable, "newValue"); +// +// In order for this to work, we need to be sure that `await Input.dispatchKeyEvent` +// resolves only after the content page flushed the event handlers (and +// `window.myVariable = "newValue"` was executed). +// +// This can be racy because Input.dispatchKeyEvent and window.myVariable = "newValue" run +// in different processes. + +const PAGE_URL = + "http://example.com/browser/remote/test/browser/input/doc_dispatchKeyEvent_race.html"; + +add_task(async function({ client }) { + await loadURL(PAGE_URL); + + const { Input, Runtime } = client; + + // Need an enabled Runtime domain to run evaluate. + info("Enable the Runtime domain"); + await Runtime.enable(); + const { context } = await Runtime.executionContextCreated(); + + info("Focus the input on the page"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + const input = content.document.querySelector("input"); + input.focus(); + is(input, content.document.activeElement, "Input should be focused"); + }); + + // See doc_input_dispatchKeyEvent_race.html + // The page listens to `input` events to update a property on window. + // We will check that the value is updated as soon dispatchKeyEvent has resolved. + await checkWindowTestValue("initial-value", context.id, Runtime); + + info("Write 'hhhhhh' ('h' times 6)"); + for (let i = 0; i < 6; i++) { + await dispatchKeyEvent(Input, "h", 72, "keyDown"); + await dispatchKeyEvent(Input, "h", 72, "keyUp"); + } + await checkWindowTestValue("hhhhhh", context.id, Runtime); + + info("Write 'aaaaaa' with 6 consecutive keydown and one keyup"); + await Promise.all([ + dispatchKeyEvent(Input, "a", 65, "keyDown"), + dispatchKeyEvent(Input, "a", 65, "keyDown"), + dispatchKeyEvent(Input, "a", 65, "keyDown"), + dispatchKeyEvent(Input, "a", 65, "keyDown"), + dispatchKeyEvent(Input, "a", 65, "keyDown"), + dispatchKeyEvent(Input, "a", 65, "keyDown"), + ]); + await dispatchKeyEvent(Input, "a", 65, "keyUp"); + await checkWindowTestValue("hhhhhhaaaaaa", context.id, Runtime); +}); + +function dispatchKeyEvent(Input, key, keyCode, type, modifiers = 0) { + info(`Send ${type} for key ${key}`); + return Input.dispatchKeyEvent({ + type, + modifiers, + windowsVirtualKeyCode: keyCode, + key, + }); +} + +async function checkWindowTestValue(expected, contextId, Runtime) { + info("Retrieve the value of `window.testValue` in the test page"); + const { result } = await Runtime.evaluate({ + contextId, + expression: "window.testValue", + }); + + is(result.value, expected, "Content window test value is correct"); +} diff --git a/remote/test/browser/input/browser_dispatchMouseEvent.js b/remote/test/browser/input/browser_dispatchMouseEvent.js new file mode 100644 index 0000000000..96adcd5a8a --- /dev/null +++ b/remote/test/browser/input/browser_dispatchMouseEvent.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function testDispatchMouseEvent({ client }) { + await loadURL(toDataURL("<div>foo</div>")); + + const { Input } = client; + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + const div = content.document.querySelector("div"); + content.mouseDownPromise = new Promise(resolve => { + div.addEventListener("mousedown", resolve, { once: true }); + }); + content.mouseUpPromise = new Promise(resolve => { + div.addEventListener("mouseup", resolve, { once: true }); + }); + content.clickPromise = new Promise(resolve => { + div.addEventListener("click", resolve, { once: true }); + }); + }); + + const { x, y } = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + return content.document.querySelector("div").getBoundingClientRect(); + } + ); + + await Input.dispatchMouseEvent({ + type: "mousePressed", + x, + y, + }); + + info("Waiting for DOM mousedown event on the div"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + await content.mouseDownPromise; + }); + + await Input.dispatchMouseEvent({ + type: "mouseReleased", + x, + y, + }); + + info("Waiting for DOM mouseup event on the div"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + await content.mouseUpPromise; + }); + + info("Waiting for DOM click event on the div"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + await content.clickPromise; + }); + + ok(true, "All events detected"); +}); diff --git a/remote/test/browser/input/doc_dispatchKeyEvent_race.html b/remote/test/browser/input/doc_dispatchKeyEvent_race.html new file mode 100644 index 0000000000..f8f45bae88 --- /dev/null +++ b/remote/test/browser/input/doc_dispatchKeyEvent_race.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test page for dispatchKeyEvent race test</title> +</head> +<body> + <input type="text"> + <script type="text/javascript"> + window.testValue = "initial-value"; + + // Small helper to synchronously pause for a given delay, in ms. + function sleep(delay) { + const start = Date.now(); + while (Date.now() - start < delay) { + // Wait for the condition to fail. + } + } + + document.querySelector("input").addEventListener("input", () => { + // Block the execution synchronously for one second. + sleep(1000); + // Update the value that will be checked by the test. + window.testValue = document.querySelector("input").value; + }); + </script> +</body> +</html> diff --git a/remote/test/browser/input/doc_events.html b/remote/test/browser/input/doc_events.html new file mode 100644 index 0000000000..505c784c13 --- /dev/null +++ b/remote/test/browser/input/doc_events.html @@ -0,0 +1,148 @@ +<!doctype html> +<meta charset=utf-8> +<html> +<head> + <title>Input Events</title> + <style> + div { padding:0px; margin: 0px; } + #trackPointer { position: fixed; } + #resultContainer { width: 600px; height: 60px; } + .area { width: 100px; height: 50px; background-color: #ccc; } + .block { width: 5px; height: 5px; border: solid 1px red; } + #dragArea { position: relative; } + #dragTarget { position: absolute; top:22px; left:47px;} + </style> + <script> + "use strict"; + var allEvents = []; + function makeParagraph(message) { + let paragraph = document.createElement("p"); + paragraph.textContent = message; + return paragraph; + } + function displayMessage(message) { + let eventNode = document.getElementById("events"); + eventNode.innerHTML = "" + eventNode.appendChild(makeParagraph(message)); + } + + function appendMessage(message) { + document.getElementById("events").appendChild(makeParagraph(message)); + } + + /** + * Escape |key| if it's in a surrogate-half character range. + * + * Example: given "\ud83d" return "U+d83d". + * + * Otherwise JSON.stringify will convert it to U+FFFD (REPLACEMENT CHARACTER) + * when returning a value from executeScript, for example. + */ + function escapeSurrogateHalf(key) { + if (typeof key !== "undefined" && key.length === 1) { + var charCode = key.charCodeAt(0); + var highSurrogate = charCode >= 0xD800 && charCode <= 0xDBFF; + var surrogate = highSurrogate || (charCode >= 0xDC00 && charCode <= 0xDFFF); + if (surrogate) { + key = "U+" + charCode.toString(16); + } + } + return key; + } + + function recordKeyboardEvent(event) { + var key = escapeSurrogateHalf(event.key); + allEvents.push({ + "code": event.code, + "key": key, + "which": event.which, + "location": event.location, + "alt": event.altKey, + "ctrl": event.ctrlKey, + "meta": event.metaKey, + "shift": event.shiftKey, + "repeat": event.repeat, + "type": event.type + }); + appendMessage(event.type + " " + + "code: " + event.code + ", " + + "key: " + key + ", " + + "which: " + event.which + ", " + + "keyCode: " + event.keyCode); + } + function recordInputEvent(event) { + allEvents.push({ + "data": event.data, + "inputType": event.inputType, + "isComposing": event.isComposing, + }); + appendMessage("InputEvent " + + "data: " + event.data + ", " + + "inputType: " + event.inputType + ", " + + "isComposing: " + event.isComposing); + } + function recordPointerEvent(event) { + if (event.type === "contextmenu") { + event.preventDefault(); + } + allEvents.push({ + "type": event.type, + "button": event.button, + "buttons": event.buttons, + "pageX": event.pageX, + "pageY": event.pageY, + "ctrlKey": event.ctrlKey, + "metaKey": event.metaKey, + "altKey": event.altKey, + "shiftKey": event.shiftKey, + "target": event.target.id + }); + appendMessage(event.type + " " + + "pageX: " + event.pageX + ", " + + "pageY: " + event.pageY + ", " + + "button: " + event.button + ", " + + "buttons: " + event.buttons + ", " + + "ctrlKey: " + event.ctrlKey + ", " + + "altKey: " + event.altKey + ", " + + "metaKey: " + event.metaKey + ", " + + "shiftKey: " + event.shiftKey + ", " + + "target id: " + event.target.id); + } + function resetEvents() { + allEvents.length = 0; + displayMessage(""); + } + + document.addEventListener("DOMContentLoaded", function() { + let keyReporter = document.getElementById("keys"); + keyReporter.addEventListener("keyup", recordKeyboardEvent); + keyReporter.addEventListener("keypress", recordKeyboardEvent); + keyReporter.addEventListener("keydown", recordKeyboardEvent); + keyReporter.addEventListener("input", recordInputEvent); + + let mouseReporter = document.getElementById("pointers"); + mouseReporter.addEventListener("click", recordPointerEvent); + mouseReporter.addEventListener("dblclick", recordPointerEvent); + mouseReporter.addEventListener("mousedown", recordPointerEvent); + mouseReporter.addEventListener("mouseup", recordPointerEvent); + mouseReporter.addEventListener("contextmenu", recordPointerEvent); + }); + </script> +</head> +<body> + <div id="trackPointer" class="block"></div> + <div> + <h2>KeyReporter</h2> + <input type="text" id="keys" size="80"> + </div> + <div> + <h2>ClickReporter</h2> + <div id="pointers" class="area"> + </div> + </div> + <div id="resultContainer"> + <h2>Events</h2> + <div id="events"></div> + </div> +</body> +</html> diff --git a/remote/test/browser/input/head.js b/remote/test/browser/input/head.js new file mode 100644 index 0000000000..7bdc526857 --- /dev/null +++ b/remote/test/browser/input/head.js @@ -0,0 +1,123 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../head.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/test/browser/head.js", + this +); + +const { Input: I } = ChromeUtils.import( + "chrome://remote/content/domains/parent/Input.jsm" +); + +const { alt, ctrl, meta, shift } = I.Modifier; + +const isMac = Services.appinfo.OS === "Darwin"; + +// Map of key codes used in Input tests. +const KEYCODES = { + a: KeyboardEvent.DOM_VK_A, + A: KeyboardEvent.DOM_VK_A, + b: KeyboardEvent.DOM_VK_B, + B: KeyboardEvent.DOM_VK_B, + c: KeyboardEvent.DOM_VK_C, + C: KeyboardEvent.DOM_VK_C, + h: KeyboardEvent.DOM_VK_H, + H: KeyboardEvent.DOM_VK_H, + Alt: KeyboardEvent.DOM_VK_ALT, + ArrowLeft: KeyboardEvent.DOM_VK_LEFT, + ArrowRight: KeyboardEvent.DOM_VK_RIGHT, + ArrowDown: KeyboardEvent.DOM_VK_DOWN, + Backspace: KeyboardEvent.DOM_VK_BACK_SPACE, + Control: KeyboardEvent.DOM_VK_CONTROL, + Meta: KeyboardEvent.DM_VK_META, + Shift: KeyboardEvent.DOM_VK_SHIFT, + Tab: KeyboardEvent.DOM_VK_TAB, +}; + +async function setupForInput(url) { + await loadURL(url); + info("Focus the input on the page"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + const input = content.document.querySelector("input"); + input.focus(); + is(input, content.document.activeElement, "Input should be focused"); + is(input.value, "", "Check input content"); + is(input.selectionStart, 0, "Check position of input caret"); + }); +} + +async function withModifier(Input, modKey, mod, key) { + await dispatchKeyEvent(Input, modKey, "rawKeyDown", I.Modifier[mod]); + await dispatchKeyEvent(Input, key, "keyDown", I.Modifier[mod]); + await dispatchKeyEvent(Input, key, "keyUp", I.Modifier[mod]); + await dispatchKeyEvent(Input, modKey, "keyUp"); +} + +function dispatchKeyEvent(Input, key, type, modifiers = 0) { + info(`Send ${type} for key ${key}`); + return Input.dispatchKeyEvent({ + type, + modifiers, + windowsVirtualKeyCode: KEYCODES[key], + key, + }); +} + +function getInputContent() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + const input = content.document.querySelector("input"); + return { value: input.value, caret: input.selectionStart }; + }); +} + +async function checkInputContent(expectedValue, expectedCaret) { + const { value, caret } = await getInputContent(); + is(value, expectedValue, "Check input content"); + is(caret, expectedCaret, "Check position of input caret"); +} + +function keyForPlatform() { + // TODO add cases for other key-combinations as the need arises + let primary = ctrl; + let primaryKey = "Control"; + if (isMac) { + primary = alt; + primaryKey = "Alt"; + } + return { primary, primaryKey }; +} + +async function sendTextKey(Input, key, modifiers = 0) { + await dispatchKeyEvent(Input, key, "keyDown", modifiers); + await dispatchKeyEvent(Input, key, "keyUp", modifiers); +} + +async function sendText(Input, text) { + for (const sym of text) { + await sendTextKey(Input, sym); + } +} + +async function sendRawKey(Input, key, modifiers = 0) { + await dispatchKeyEvent(Input, key, "rawKeyDown", modifiers); + await dispatchKeyEvent(Input, key, "keyUp", modifiers); +} + +async function checkBackspace(Input, expected, modifiers = 0) { + info("Send Backspace"); + await sendRawKey(Input, "Backspace", modifiers); + await checkInputContent(expected, expected.length); +} + +function resetInput(value = "") { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [value], function(arg) { + const input = content.document.querySelector("input"); + input.value = arg; + input.focus(); + }); +} diff --git a/remote/test/browser/io/browser.ini b/remote/test/browser/io/browser.ini new file mode 100644 index 0000000000..352fa38433 --- /dev/null +++ b/remote/test/browser/io/browser.ini @@ -0,0 +1,12 @@ +[DEFAULT] +tags = remote +subsuite = remote +prefs = + remote.enabled=true +support-files = + !/remote/test/browser/chrome-remote-interface.js + !/remote/test/browser/head.js + head.js + +[browser_close.js] +[browser_read.js] diff --git a/remote/test/browser/io/browser_close.js b/remote/test/browser/io/browser_close.js new file mode 100644 index 0000000000..fc3fc382d7 --- /dev/null +++ b/remote/test/browser/io/browser_close.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function fileRemovedAfterClose({ client }) { + const { IO } = client; + const contents = "Lorem ipsum"; + const { handle, path } = await registerFileStream(contents); + + await IO.close({ handle }); + ok(!(await OS.File.exists(path)), "Discarded the temporary backing storage"); +}); + +add_task(async function unknownHandle({ client }) { + const { IO } = client; + const handle = "1000000"; + + try { + await IO.close({ handle }); + ok(false, "Close shouldn't pass"); + } catch (e) { + ok( + e.message.startsWith(`Invalid stream handle`), + "Error contains expected message" + ); + } +}); + +add_task(async function invalidHandleTypes({ client }) { + const { IO } = client; + for (const handle of [null, true, 1, [], {}]) { + try { + await IO.close({ handle }); + ok(false, "Close shouldn't pass"); + } catch (e) { + ok( + e.message.startsWith(`handle: string value expected`), + "Error contains expected message" + ); + } + } +}); diff --git a/remote/test/browser/io/browser_read.js b/remote/test/browser/io/browser_read.js new file mode 100644 index 0000000000..1692e45e90 --- /dev/null +++ b/remote/test/browser/io/browser_read.js @@ -0,0 +1,155 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function seekByOffsets({ client }) { + const { IO } = client; + const contents = "Lorem ipsum"; + const { handle } = await registerFileStream(contents); + + for (const offset of [0, 5, 10, 100, 0, -1]) { + const result = await IO.read({ handle, offset }); + ok(result.base64Encoded, `Data for offset ${offset} is base64 encoded`); + ok(result.eof, `All data has been read for offset ${offset}`); + is( + atob(result.data), + contents.substring(offset >= 0 ? offset : 0), + `Found expected data for offset ${offset}` + ); + } +}); + +add_task(async function remembersOffsetAfterRead({ client }) { + const { IO } = client; + const contents = "Lorem ipsum"; + const { handle } = await registerFileStream(contents); + + let expectedOffset = 0; + const size = 3; + do { + const result = await IO.read({ handle, size }); + is( + atob(result.data), + contents.substring(expectedOffset, expectedOffset + size), + `Found expected data for expectedOffset ${expectedOffset}` + ); + ok( + result.base64Encoded, + `Data up to expected offset ${expectedOffset} is base64 encoded` + ); + + is( + result.eof, + expectedOffset + size >= contents.length, + `All data has been read up to expected offset ${expectedOffset}` + ); + + expectedOffset = Math.min(expectedOffset + size, contents.length); + } while (expectedOffset < contents.length); +}); + +add_task(async function readBySize({ client }) { + const { IO } = client; + const contents = "Lorem ipsum"; + const { handle } = await registerFileStream(contents); + + for (const size of [0, 5, 10, 100, 0, -1]) { + const result = await IO.read({ handle, offset: 0, size }); + ok(result.base64Encoded, `Data for size ${size} is base64 encoded`); + is( + result.eof, + size >= contents.length, + `All data has been read for size ${size}` + ); + is( + atob(result.data), + contents.substring(0, size), + `Found expected data for size ${size}` + ); + } +}); + +add_task(async function readAfterClose({ client }) { + const { IO } = client; + const contents = "Lorem ipsum"; + const { handle } = await registerFileStream(contents); + + await IO.close({ handle }); + + try { + await IO.read({ handle }); + ok(false, "Read shouldn't pass"); + } catch (e) { + ok( + e.message.startsWith(`Invalid stream handle`), + "Error contains expected message" + ); + } +}); + +add_task(async function unknownHandle({ client }) { + const { IO } = client; + const handle = "1000000"; + + try { + await IO.read({ handle }); + ok(false, "Read shouldn't pass"); + } catch (e) { + ok( + e.message.startsWith(`Invalid stream handle`), + "Error contains expected message" + ); + } +}); + +add_task(async function invalidHandleTypes({ client }) { + const { IO } = client; + for (const handle of [null, true, 1, [], {}]) { + try { + await IO.read({ handle }); + ok(false, "Read shouldn't pass"); + } catch (e) { + ok( + e.message.startsWith(`handle: string value expected`), + "Error contains expected message" + ); + } + } +}); + +add_task(async function invalidOffsetTypes({ client }) { + const { IO } = client; + const contents = "Lorem ipsum"; + const { handle } = await registerFileStream(contents); + + for (const offset of [null, true, "1", [], {}]) { + try { + await IO.read({ handle, offset }); + ok(false, "Read shouldn't pass"); + } catch (e) { + ok( + e.message.startsWith(`offset: integer value expected`), + "Error contains expected message" + ); + } + } +}); + +add_task(async function invalidSizeTypes({ client }) { + const { IO } = client; + const contents = "Lorem ipsum"; + const { handle } = await registerFileStream(contents); + + for (const size of [null, true, "1", [], {}]) { + try { + await IO.read({ handle, size }); + ok(false, "Read shouldn't pass"); + } catch (e) { + ok( + e.message.startsWith(`size: integer value expected`), + "Error contains expected message" + ); + } + } +}); diff --git a/remote/test/browser/io/head.js b/remote/test/browser/io/head.js new file mode 100644 index 0000000000..566ef690ee --- /dev/null +++ b/remote/test/browser/io/head.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../head.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/test/browser/head.js", + this +); + +const { streamRegistry } = ChromeUtils.import( + "chrome://remote/content/domains/parent/IO.jsm" +); + +async function registerFileStream(contents, options = {}) { + // Any file as registered with the stream registry will be automatically + // deleted during the shutdown of Firefox. + options.remove = false; + + const { file, path } = await createFile(contents, options); + const handle = streamRegistry.add(file); + + return { handle, path }; +} diff --git a/remote/test/browser/log/browser.ini b/remote/test/browser/log/browser.ini new file mode 100644 index 0000000000..421c01994f --- /dev/null +++ b/remote/test/browser/log/browser.ini @@ -0,0 +1,11 @@ +[DEFAULT] +tags = remote +subsuite = remote +prefs = + remote.enabled=true +support-files = + !/remote/test/browser/chrome-remote-interface.js + !/remote/test/browser/head.js + head.js + +[browser_entryAdded.js] diff --git a/remote/test/browser/log/browser_entryAdded.js b/remote/test/browser/log/browser_entryAdded.js new file mode 100644 index 0000000000..f5fb865e19 --- /dev/null +++ b/remote/test/browser/log/browser_entryAdded.js @@ -0,0 +1,138 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function noEventsWhenLogDomainDisabled({ client }) { + await runEntryAddedTest(client, 0, async () => { + await throwScriptError("foo"); + }); +}); + +add_task(async function noEventsAfterLogDomainDisabled({ client }) { + const { Log } = client; + + await Log.enable(); + await Log.disable(); + + await runEntryAddedTest(client, 0, async () => { + await throwScriptError("foo"); + }); +}); + +add_task(async function noEventsForConsoleMessageWithException({ client }) { + const { Log } = client; + + await Log.enable(); + + const context = await enableRuntime(client); + await runEntryAddedTest(client, 0, async () => { + evaluate(client, context.id, () => { + const foo = {}; + foo.bar(); + }); + }); +}); + +add_task(async function eventsForScriptErrorWithoutException({ client }) { + const { Log } = client; + + await Log.enable(); + + await enableRuntime(client); + const events = await runEntryAddedTest(client, 1, async () => { + throwScriptError({ + text: "foo", + sourceName: "http://foo.bar", + lineNumber: 7, + category: "javascript", + }); + }); + + is(events[0].source, "javascript", "Got expected source"); + is(events[0].level, "error", "Got expected level"); + is(events[0].text, "foo", "Got expected text"); + is(events[0].url, "http://foo.bar", "Got expected url"); + is(events[0].lineNumber, 7, "Got expected line number"); +}); + +add_task(async function eventsForScriptErrorLevels({ client }) { + const { Log } = client; + + await Log.enable(); + + const flags = { + info: Ci.nsIScriptError.infoFlag, + warning: Ci.nsIScriptError.warningFlag, + error: Ci.nsIScriptError.errorFlag, + }; + + await enableRuntime(client); + for (const [level, flag] of Object.entries(flags)) { + const events = await runEntryAddedTest(client, 1, async () => { + throwScriptError({ text: level, flag }); + }); + + is(events[0].text, level, "Got expected text"); + is(events[0].level, level, "Got expected level"); + } +}); + +add_task(async function eventsForScriptErrorContent({ client }) { + const { Log } = client; + + await Log.enable(); + + const context = await enableRuntime(client); + const events = await runEntryAddedTest(client, 1, async () => { + evaluate(client, context.id, () => { + document.execCommand("copy"); + }); + }); + + is(events[0].source, "other", "Got expected source"); + is(events[0].level, "warning", "Got expected level"); + ok( + events[0].text.includes("document.execCommand(‘cut’/‘copy’) was denied"), + "Got expected text" + ); + is(events[0].url, undefined, "Got undefined url"); + is(events[0].lineNumber, 2, "Got expected line number"); +}); + +async function runEntryAddedTest(client, eventCount, callback, options = {}) { + const { Log } = client; + + const EVENT_ENTRY_ADDED = "Log.entryAdded"; + + const history = new RecordEvents(eventCount); + history.addRecorder({ + event: Log.entryAdded, + eventName: EVENT_ENTRY_ADDED, + messageFn: payload => `Received "${EVENT_ENTRY_ADDED}"`, + }); + + const timeBefore = Date.now(); + await callback(); + + const entryAddedEvents = await history.record(); + is(entryAddedEvents.length, eventCount, "Got expected amount of events"); + + if (eventCount == 0) { + return []; + } + + const timeAfter = Date.now(); + + // Check basic details for entryAdded events + entryAddedEvents.forEach(event => { + const timestamp = event.payload.entry.timestamp; + + ok( + timestamp >= timeBefore && timestamp <= timeAfter, + "Got valid timestamp" + ); + }); + + return entryAddedEvents.map(event => event.payload.entry); +} diff --git a/remote/test/browser/log/head.js b/remote/test/browser/log/head.js new file mode 100644 index 0000000000..7131e98b6f --- /dev/null +++ b/remote/test/browser/log/head.js @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../head.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/test/browser/head.js", + this +); diff --git a/remote/test/browser/network/browser.ini b/remote/test/browser/network/browser.ini new file mode 100644 index 0000000000..274291c3f3 --- /dev/null +++ b/remote/test/browser/network/browser.ini @@ -0,0 +1,29 @@ +[DEFAULT] +tags = remote +subsuite = remote +prefs = + remote.enabled=true +support-files = + !/remote/test/browser/chrome-remote-interface.js + !/remote/test/browser/head.js + head.js + doc_empty.html + doc_frameset.html + doc_networkEvents.html + file_networkEvents.js + file_framesetEvents.js + sjs-cookies.sjs + +[browser_deleteCookies.js] +[browser_emulateNetworkConditions.js] +[browser_getAllCookies.js] +[browser_getCookies.js] +skip-if = (os == 'win' && os_version == '10.0' && ccov) # Bug 1605650 +[browser_navigationEvents.js] +[browser_responseReceived.js] +[browser_requestWillBeSent.js] +[browser_setCookie.js] +[browser_setCookies.js] +[browser_setCacheDisabled.js] +skip-if = true # Bug 1610382 +[browser_setUserAgentOverride.js] diff --git a/remote/test/browser/network/browser_deleteCookies.js b/remote/test/browser/network/browser_deleteCookies.js new file mode 100644 index 0000000000..70489cdd7d --- /dev/null +++ b/remote/test/browser/network/browser_deleteCookies.js @@ -0,0 +1,298 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const SJS_PATH = "/browser/remote/test/browser/network/sjs-cookies.sjs"; + +const DEFAULT_HOST = "http://example.org"; +const DEFAULT_HOSTNAME = "example.org"; +const ALT_HOST = "http://example.net"; +const ALT_HOSTNAME = "example.net"; +const SECURE_HOST = "https://example.com"; +const SECURE_HOSTNAME = "example.com"; + +const DEFAULT_URL = `${DEFAULT_HOST}${SJS_PATH}`; + +add_task(async function failureWithoutArguments({ client }) { + const { Network } = client; + + let errorThrown = false; + try { + await Network.deleteCookies(); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "Fails without any arguments"); +}); + +add_task(async function failureWithoutDomainOrURL({ client }) { + const { Network } = client; + + let errorThrown = false; + try { + await Network.deleteCookies({ name: "foo" }); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "Fails without domain or URL"); +}); + +add_task(async function failureWithInvalidProtocol({ client }) { + const { Network } = client; + + const FTP_URL = `ftp://${DEFAULT_HOSTNAME}`; + + let errorThrown = false; + try { + await Network.deleteCookies({ name: "foo", url: FTP_URL }); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "Fails for invalid protocol in URL"); +}); + +add_task(async function pristineContext({ client }) { + const { Network } = client; + + await loadURL(DEFAULT_URL); + + const { cookies } = await Network.getCookies(); + is(cookies.length, 0, "No cookies have been found"); + + await Network.deleteCookies({ name: "foo", url: DEFAULT_URL }); +}); + +add_task(async function fromHostWithPort({ client }) { + const { Network } = client; + + const PORT_URL = `${DEFAULT_HOST}:8000${SJS_PATH}`; + await loadURL(PORT_URL + "?name=id&value=1"); + + const cookie = { + name: "id", + value: "1", + }; + + try { + const { cookies: before } = await Network.getCookies(); + is(before.length, 1, "A cookie has been found"); + + await Network.deleteCookies({ name: cookie.name, url: PORT_URL }); + + const { cookies: after } = await Network.getCookies(); + is(after.length, 0, "No cookie has been found"); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function forSpecificDomain({ client }) { + const { Network } = client; + + const ALT_URL = ALT_HOST + SJS_PATH; + + await loadURL(`${ALT_URL}?name=foo&value=bar`); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar`); + + const cookie = { + name: "foo", + value: "bar", + domain: "example.net", + }; + + try { + const { cookies: before } = await Network.getCookies(); + is(before.length, 1, "A cookie has been found"); + + await Network.deleteCookies({ + name: cookie.name, + domain: DEFAULT_HOSTNAME, + }); + + const { cookies: after } = await Network.getCookies(); + is(after.length, 0, "No cookie has been found"); + + await loadURL(ALT_URL); + + const { cookies: other } = await Network.getCookies(); + is(other.length, 1, "A cookie has been found"); + assertCookie(other[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function forSpecificURL({ client }) { + const { Network } = client; + + const ALT_URL = ALT_HOST + SJS_PATH; + + await loadURL(`${ALT_URL}?name=foo&value=bar`); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar`); + + const cookie = { + name: "foo", + value: "bar", + domain: "example.net", + }; + + try { + const { cookies: before } = await Network.getCookies(); + is(before.length, 1, "A cookie has been found"); + + await Network.deleteCookies({ name: cookie.name, url: DEFAULT_URL }); + + const { cookies: after } = await Network.getCookies(); + is(after.length, 0, "No cookie has been found"); + + await loadURL(ALT_URL); + + const { cookies: other } = await Network.getCookies(); + is(other.length, 1, "A cookie has been found"); + assertCookie(other[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function forSecureURL({ client }) { + const { Network } = client; + + const SECURE_URL = `${SECURE_HOST}${SJS_PATH}`; + + await loadURL(`${DEFAULT_URL}?name=foo&value=bar`); + await loadURL(`${SECURE_URL}?name=foo&value=bar`); + + const cookie = { + name: "foo", + value: "bar", + domain: "example.org", + }; + + try { + const { cookies: before } = await Network.getCookies(); + is(before.length, 1, "A cookie has been found"); + + await Network.deleteCookies({ name: cookie.name, url: SECURE_URL }); + + const { cookies: after } = await Network.getCookies(); + is(after.length, 0, "No cookie has been found"); + + await loadURL(DEFAULT_URL); + + const { cookies: other } = await Network.getCookies(); + is(other.length, 1, "A cookie has been found"); + assertCookie(other[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function forSpecificDomainAndURL({ client }) { + const { Network } = client; + + const ALT_URL = ALT_HOST + SJS_PATH; + + await loadURL(`${ALT_URL}?name=foo&value=bar`); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar`); + + const cookie = { + name: "foo", + value: "bar", + domain: "example.net", + }; + + try { + const { cookies: before } = await Network.getCookies(); + is(before.length, 1, "A cookie has been found"); + + // Domain has precedence before URL + await Network.deleteCookies({ + name: cookie.name, + domain: DEFAULT_HOSTNAME, + url: ALT_URL, + }); + + const { cookies: after } = await Network.getCookies(); + is(after.length, 0, "No cookie has been found"); + + await loadURL(ALT_URL); + + const { cookies: other } = await Network.getCookies(); + is(other.length, 1, "A cookie has been found"); + assertCookie(other[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function path({ client }) { + const { Network } = client; + + const PATH = "/browser/remote/test/browser/"; + const PARENT_PATH = "/browser/remote/test/"; + const SUB_PATH = "/browser/remote/test/browser/network/"; + + const cookie = { + name: "foo", + value: "bar", + path: PATH, + }; + + try { + console.log("Check exact path"); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&path=${PATH}`); + let result = await Network.getCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + + await Network.deleteCookies({ + name: cookie.name, + path: PATH, + url: DEFAULT_URL, + }); + result = await Network.getCookies(); + is(result.cookies.length, 0, "No cookie has been found"); + + console.log("Check sub path"); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&path=${PATH}`); + result = await Network.getCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + + await Network.deleteCookies({ + name: cookie.name, + path: SUB_PATH, + url: DEFAULT_URL, + }); + result = await Network.getCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + + console.log("Check parent path"); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&path=${PATH}`); + result = await Network.getCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + + await Network.deleteCookies({ + name: cookie.name, + path: PARENT_PATH, + url: DEFAULT_URL, + }); + result = await Network.getCookies(); + is(result.cookies.length, 0, "No cookie has been found"); + + console.log("Check non matching path"); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&path=${PATH}`); + result = await Network.getCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + + await Network.deleteCookies({ + name: cookie.name, + path: "/foo/bar", + url: DEFAULT_URL, + }); + result = await Network.getCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + } finally { + Services.cookies.removeAll(); + } +}); diff --git a/remote/test/browser/network/browser_emulateNetworkConditions.js b/remote/test/browser/network/browser_emulateNetworkConditions.js new file mode 100644 index 0000000000..8eacdacd32 --- /dev/null +++ b/remote/test/browser/network/browser_emulateNetworkConditions.js @@ -0,0 +1,248 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const pageEmptyURL = + "http://example.com/browser/remote/test/browser/page/doc_empty.html"; + +/** + * Acts just as `add_task`, but does cleanup afterwards + * @param taskFn + */ +function add_networking_task(taskFn) { + add_task(async client => { + try { + await taskFn(client); + } finally { + Services.io.offline = false; + } + }); +} + +add_networking_task(async function offlineWithoutArguments({ client }) { + const { Network } = client; + + let errorThrown = ""; + try { + await Network.emulateNetworkConditions(); + } catch (e) { + errorThrown = e.message; + } + + ok( + errorThrown.match(/offline: boolean value expected/), + "Fails without any arguments" + ); +}); + +add_networking_task(async function offlineWithEmptyArguments({ client }) { + const { Network } = client; + + let errorThrown = ""; + try { + await Network.emulateNetworkConditions({}); + } catch (e) { + errorThrown = e.message; + } + + ok( + errorThrown.match(/offline: boolean value expected/), + "Fails with only empty arguments" + ); +}); + +add_networking_task(async function offlineWithInvalidArguments({ client }) { + const { Network } = client; + const testTable = [null, undefined, 1, "foo", [], {}]; + + for (const testCase of testTable) { + let errorThrown = ""; + try { + await Network.emulateNetworkConditions({ offline: testCase }); + } catch (e) { + errorThrown = e.message; + } + + const testType = typeof testCase; + + ok( + errorThrown.match(/offline: boolean value expected/), + `Fails with ${testType}-type argument for offline` + ); + } +}); + +add_networking_task(async function offlineWithUnsupportedArguments({ client }) { + const { Network } = client; + + // Random valid values for the Network.emulateNetworkConditions command, even though we don't support them yet + const args = { + offline: true, + latency: 500, + downloadThroughput: 500, + uploadThroughput: 500, + connectionType: "cellular2g", + someFutureArg: false, + }; + + await Network.emulateNetworkConditions(args); + + ok(true, "No errors should be thrown due to non-implemented arguments"); +}); + +add_networking_task(async function emulateOfflineWhileOnline({ client }) { + const { Network } = client; + + // Assert we're online to begin with + await assertOfflineStatus(false); + + // Assert for offline + await Network.emulateNetworkConditions({ offline: true }); + await assertOfflineStatus(true); + + // Assert we really can't navigate after setting offline + await assertOfflineNavigationFails(); +}); + +add_networking_task(async function emulateOfflineWhileOffline({ client }) { + const { Network } = client; + + // Assert we're online to begin with + await assertOfflineStatus(false); + + // Assert for offline + await Network.emulateNetworkConditions({ offline: true }); + await assertOfflineStatus(true); + + // Assert for no-offline event, because we're offline - and changing to offline - so nothing changes + await Network.emulateNetworkConditions({ offline: true }); + await assertOfflineStatus(true); + + // Assert we still can't navigate after setting offline twice + await assertOfflineNavigationFails(); +}); + +add_networking_task(async function emulateOnlineWhileOnline({ client }) { + const { Network } = client; + + // Assert we're online to begin with + await assertOfflineStatus(false); + + // Assert for no-offline event, because we're online - and changing to online - so nothing changes + await Network.emulateNetworkConditions({ offline: false }); + await assertOfflineStatus(false); +}); + +add_networking_task(async function emulateOnlineWhileOffline({ client }) { + const { Network } = client; + + // Assert we're online to begin with + await assertOfflineStatus(false); + + // Assert for offline event, because we're online - and changing to offline + const offlineChanged = Promise.race([ + BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "online", + true + ), + BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "offline", + true + ), + ]); + + await Network.emulateNetworkConditions({ offline: true }); + + info("Waiting for offline event on window"); + is(await offlineChanged, "offline", "Only the offline-event should fire"); + await assertOfflineStatus(true); + + // Assert for online event, because we're offline - and changing to online + const offlineChangedBack = Promise.race([ + BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "online", + true + ), + BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "offline", + true + ), + ]); + await Network.emulateNetworkConditions({ offline: false }); + + info("Waiting for online event on window"); + is(await offlineChangedBack, "online", "Only the online-event should fire"); + await assertOfflineStatus(false); +}); + +/** + * Navigates to a page, and asserting any status code to appear + */ +async function assertOfflineNavigationFails() { + // Tests always connect to localhost, and per bug 87717, localhost is now + // reachable in offline mode. To avoid this, disable any proxy. + const proxyPrefValue = SpecialPowers.getIntPref("network.proxy.type"); + const diskPrefValue = SpecialPowers.getBoolPref("browser.cache.disk.enable"); + const memoryPrefValue = SpecialPowers.getBoolPref( + "browser.cache.memory.enable" + ); + SpecialPowers.pushPrefEnv({ + set: [ + ["network.proxy.type", 0], + ["browser.cache.disk.enable", false], + ["browser.cache.memory.enable", false], + ], + }); + + try { + const browser = gBrowser.selectedTab.linkedBrowser; + let netErrorLoaded = BrowserTestUtils.waitForErrorPage(browser); + + BrowserTestUtils.loadURI(browser, pageEmptyURL); + await netErrorLoaded; + + await SpecialPowers.spawn(browser, [], () => { + ok( + content.document.documentURI.startsWith("about:neterror?e=netOffline"), + "Should be showing error page" + ); + }); + } finally { + // Re-enable the proxy so example.com is resolved to localhost, rather than + // the actual example.com. + SpecialPowers.pushPrefEnv({ + set: [ + ["network.proxy.type", proxyPrefValue], + ["browser.cache.disk.enable", diskPrefValue], + ["browser.cache.memory.enable", memoryPrefValue], + ], + }); + } +} + +/** + * Checks on the page what the value of window.navigator.onLine is on the currently navigated page + * + * @param {boolean} offline + * True if offline is expected + */ +function assertOfflineStatus(offline) { + is( + Services.io.offline, + offline, + "Services.io.offline should be " + (offline ? "true" : "false") + ); + + return SpecialPowers.spawn(gBrowser.selectedBrowser, [offline], offline => { + is( + content.navigator.onLine, + !offline, + "Page should be " + (offline ? "offline" : "online") + ); + }); +} diff --git a/remote/test/browser/network/browser_getAllCookies.js b/remote/test/browser/network/browser_getAllCookies.js new file mode 100644 index 0000000000..e3d07faba6 --- /dev/null +++ b/remote/test/browser/network/browser_getAllCookies.js @@ -0,0 +1,225 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const SJS_PATH = "/browser/remote/test/browser/network/sjs-cookies.sjs"; + +const DEFAULT_HOST = "http://example.org"; +const ALT_HOST = "http://example.net"; +const SECURE_HOST = "https://example.com"; + +const DEFAULT_URL = `${DEFAULT_HOST}${SJS_PATH}`; + +add_task(async function noCookiesWhenNoneAreSet({ client }) { + const { Network } = client; + const { cookies } = await Network.getAllCookies(); + is(cookies.length, 0, "No cookies have been found"); +}); + +add_task(async function noCookiesForPristineContext({ client }) { + const { Network } = client; + await loadURL(DEFAULT_URL); + + try { + const { cookies } = await Network.getAllCookies(); + is(cookies.length, 0, "No cookies have been found"); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function allCookiesFromHostWithPort({ client }) { + const { Network } = client; + const PORT_URL = `${DEFAULT_HOST}:8000${SJS_PATH}?name=id&value=1`; + await loadURL(PORT_URL); + + const cookie = { + name: "id", + value: "1", + }; + + try { + const { cookies } = await Network.getAllCookies(); + is(cookies.length, 1, "All cookies have been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function allCookiesFromMultipleOrigins({ client }) { + const { Network } = client; + await loadURL(`${ALT_HOST}${SJS_PATH}?name=users&value=password`); + await loadURL(`${SECURE_HOST}${SJS_PATH}?name=secure&value=password`); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar`); + + const cookie1 = { name: "foo", value: "bar", domain: "example.org" }; + const cookie2 = { name: "secure", value: "password", domain: "example.com" }; + const cookie3 = { name: "users", value: "password", domain: "example.net" }; + + try { + const { cookies } = await Network.getAllCookies(); + cookies.sort((a, b) => a.name.localeCompare(b.name)); + is(cookies.length, 3, "All cookies have been found"); + assertCookie(cookies[0], cookie1); + assertCookie(cookies[1], cookie2); + assertCookie(cookies[2], cookie3); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function secure({ client }) { + const { Network } = client; + await loadURL(`${SECURE_HOST}${SJS_PATH}?name=foo&value=bar&secure`); + + const cookie = { + name: "foo", + value: "bar", + domain: "example.com", + secure: true, + }; + + try { + // Cookie returned for secure protocols + let result = await Network.getAllCookies(); + is(result.cookies.length, 1, "The secure cookie has been found"); + assertCookie(result.cookies[0], cookie); + + // For unsecure protocols the secure cookies are also returned + await loadURL(DEFAULT_URL); + result = await Network.getAllCookies(); + is(result.cookies.length, 1, "The secure cookie has been found"); + assertCookie(result.cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function expiry({ client }) { + const { Network } = client; + const date = new Date(); + date.setDate(date.getDate() + 3); + + const encodedDate = encodeURI(date.toUTCString()); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&expiry=${encodedDate}`); + + const cookie = { + name: "foo", + value: "bar", + expires: Math.floor(date.getTime() / 1000), + session: false, + }; + + try { + const { cookies } = await Network.getAllCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function session({ client }) { + const { Network } = client; + await loadURL(`${DEFAULT_URL}?name=foo&value=bar`); + + const cookie = { + name: "foo", + value: "bar", + expiry: -1, + session: true, + }; + + try { + const { cookies } = await Network.getAllCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function path({ client }) { + const { Network } = client; + const PATH = "/browser/remote/test/browser/"; + const PARENT_PATH = "/browser/remote/test/"; + + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&path=${PATH}`); + + const cookie = { + name: "foo", + value: "bar", + path: PATH, + }; + + try { + console.log("Check exact path"); + await loadURL(`${DEFAULT_HOST}${PATH}`); + let result = await Network.getAllCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + assertCookie(result.cookies[0], cookie); + + console.log("Check sub path"); + await loadURL(`${DEFAULT_HOST}${SJS_PATH}`); + result = await Network.getAllCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + assertCookie(result.cookies[0], cookie); + + console.log("Check parent path"); + await loadURL(`${DEFAULT_HOST}${PARENT_PATH}`); + result = await Network.getAllCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + assertCookie(result.cookies[0], cookie); + + console.log("Check non matching path"); + await loadURL(`${DEFAULT_HOST}/foo/bar`); + result = await Network.getAllCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + assertCookie(result.cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function httpOnly({ client }) { + const { Network } = client; + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&httpOnly`); + + const cookie = { + name: "foo", + value: "bar", + httpOnly: true, + }; + + try { + const { cookies } = await Network.getAllCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function sameSite({ client }) { + const { Network } = client; + for (const value of ["Lax", "Strict"]) { + console.log(`Test cookie with SameSite=${value}`); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&sameSite=${value}`); + + const cookie = { + name: "foo", + value: "bar", + sameSite: value, + }; + + try { + const { cookies } = await Network.getAllCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } + } +}); diff --git a/remote/test/browser/network/browser_getCookies.js b/remote/test/browser/network/browser_getCookies.js new file mode 100644 index 0000000000..c53fe9f0cc --- /dev/null +++ b/remote/test/browser/network/browser_getCookies.js @@ -0,0 +1,220 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const SJS_PATH = "/browser/remote/test/browser/network/sjs-cookies.sjs"; + +const DEFAULT_HOST = "http://example.org"; +const ALT_HOST = "http://example.net"; +const SECURE_HOST = "https://example.com"; + +const DEFAULT_URL = `${DEFAULT_HOST}${SJS_PATH}`; + +add_task(async function noCookiesWhenNoneAreSet({ client }) { + const { Network } = client; + const { cookies } = await Network.getCookies({ urls: [DEFAULT_HOST] }); + is(cookies.length, 0, "No cookies have been found"); +}); + +add_task(async function noCookiesForPristineContext({ client }) { + const { Network } = client; + await loadURL(DEFAULT_URL); + + try { + const { cookies } = await Network.getCookies(); + is(cookies.length, 0, "No cookies have been found"); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function allCookiesFromHostWithPort({ client }) { + const { Network } = client; + const PORT_URL = `${DEFAULT_HOST}:8000${SJS_PATH}?name=id&value=1`; + await loadURL(PORT_URL); + + const cookie = { + name: "id", + value: "1", + }; + + try { + const { cookies } = await Network.getCookies(); + is(cookies.length, 1, "All cookies have been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function allCookiesFromCurrentURL({ client }) { + const { Network } = client; + await loadURL(`${ALT_HOST}${SJS_PATH}?name=user&value=password`); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar`); + await loadURL(`${DEFAULT_URL}?name=user&value=password`); + + const cookie1 = { name: "foo", value: "bar", domain: "example.org" }; + const cookie2 = { name: "user", value: "password", domain: "example.org" }; + + try { + const { cookies } = await Network.getCookies(); + cookies.sort((a, b) => a.name.localeCompare(b.name)); + is(cookies.length, 2, "All cookies have been found"); + assertCookie(cookies[0], cookie1); + assertCookie(cookies[1], cookie2); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function secure({ client }) { + const { Network } = client; + await loadURL(`${SECURE_HOST}${SJS_PATH}?name=foo&value=bar&secure`); + + const cookie = { + name: "foo", + value: "bar", + domain: "example.com", + secure: true, + }; + + try { + // Cookie returned for secure protocols + let result = await Network.getCookies(); + is(result.cookies.length, 1, "The secure cookie has been found"); + assertCookie(result.cookies[0], cookie); + + // For unsecure protocols no secure cookies are returned + await loadURL(DEFAULT_URL); + result = await Network.getCookies(); + is(result.cookies.length, 0, "No secure cookies have been found"); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function expiry({ client }) { + const { Network } = client; + const date = new Date(); + date.setDate(date.getDate() + 3); + + const encodedDate = encodeURI(date.toUTCString()); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&expiry=${encodedDate}`); + + const cookie = { + name: "foo", + value: "bar", + expires: Math.floor(date.getTime() / 1000), + session: false, + }; + + try { + const { cookies } = await Network.getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function session({ client }) { + const { Network } = client; + await loadURL(`${DEFAULT_URL}?name=foo&value=bar`); + + const cookie = { + name: "foo", + value: "bar", + expiry: -1, + session: true, + }; + + try { + const { cookies } = await Network.getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function path({ client }) { + const { Network } = client; + const PATH = "/browser/remote/test/browser/"; + const PARENT_PATH = "/browser/remote/test/"; + + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&path=${PATH}`); + + const cookie = { + name: "foo", + value: "bar", + path: PATH, + }; + + try { + console.log("Check exact path"); + await loadURL(`${DEFAULT_HOST}${PATH}`); + let result = await Network.getCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + assertCookie(result.cookies[0], cookie); + + console.log("Check sub path"); + await loadURL(`${DEFAULT_HOST}${SJS_PATH}`); + result = await Network.getCookies(); + is(result.cookies.length, 1, "A single cookie has been found"); + assertCookie(result.cookies[0], cookie); + + console.log("Check parent path"); + await loadURL(`${DEFAULT_HOST}${PARENT_PATH}`); + result = await Network.getCookies(); + is(result.cookies.length, 0, "No cookies have been found"); + + console.log("Check non matching path"); + await loadURL(`${DEFAULT_HOST}/foo/bar`); + result = await Network.getCookies(); + is(result.cookies.length, 0, "No cookies have been found"); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function httpOnly({ client }) { + const { Network } = client; + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&httpOnly`); + + const cookie = { + name: "foo", + value: "bar", + httpOnly: true, + }; + + try { + const { cookies } = await Network.getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function sameSite({ client }) { + const { Network } = client; + for (const value of ["Lax", "Strict"]) { + console.log(`Test cookie with SameSite=${value}`); + await loadURL(`${DEFAULT_URL}?name=foo&value=bar&sameSite=${value}`); + + const cookie = { + name: "foo", + value: "bar", + sameSite: value, + }; + + try { + const { cookies } = await Network.getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } + } +}); diff --git a/remote/test/browser/network/browser_navigationEvents.js b/remote/test/browser/network/browser_navigationEvents.js new file mode 100644 index 0000000000..4f08821418 --- /dev/null +++ b/remote/test/browser/network/browser_navigationEvents.js @@ -0,0 +1,202 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test order and consistency of Network/Page events as a whole. +// Details of specific events are checked in event-specific test files. + +const BASE_PATH = "http://example.com/browser/remote/test/browser/network"; +const FRAMESET_URL = `${BASE_PATH}/doc_frameset.html`; +const FRAMESET_JS_URL = `${BASE_PATH}/file_framesetEvents.js`; +const PAGE_URL = `${BASE_PATH}/doc_networkEvents.html`; +const PAGE_JS_URL = `${BASE_PATH}/file_networkEvents.js`; + +add_task(async function eventsForTopFrameNavigation({ client }) { + const { history, frameId: frameIdNav } = await prepareTest( + client, + FRAMESET_URL, + 10 + ); + + const documentEvents = filterEventsByType(history, "Document"); + const scriptEvents = filterEventsByType(history, "Script"); + const subdocumentEvents = filterEventsByType(history, "Subdocument"); + + is(documentEvents.length, 2, "Expected number of Document events"); + is(subdocumentEvents.length, 2, "Expected number of Subdocument events"); + is(scriptEvents.length, 4, "Expected number of Script events"); + + const navigatedEvents = history.findEvents("Page.navigate"); + is(navigatedEvents.length, 1, "Expected number of navigate done events"); + + const frameAttachedEvents = history.findEvents("Page.frameAttached"); + is(frameAttachedEvents.length, 1, "Expected number of frame attached events"); + + // network events for document and script + assertEventOrder(documentEvents[0], documentEvents[1]); + assertEventOrder(documentEvents[1], navigatedEvents[0], { + ignoreTimestamps: true, + }); + assertEventOrder(navigatedEvents[0], scriptEvents[0], { + ignoreTimestamps: true, + }); + assertEventOrder(scriptEvents[0], scriptEvents[1]); + + const docRequest = documentEvents[0].payload; + is(docRequest.documentURL, FRAMESET_URL, "documenURL matches target url"); + is(docRequest.frameId, frameIdNav, "Got the expected frame id"); + is(docRequest.request.url, FRAMESET_URL, "Got the Document request"); + + const docResponse = documentEvents[1].payload; + is(docResponse.frameId, frameIdNav, "Got the expected frame id"); + is(docResponse.response.url, FRAMESET_URL, "Got the Document response"); + ok(!!docResponse.response.headers.server, "Document response has headers"); + // TODO? response reports extra request header "upgrade-insecure-requests":"1" + // Assert.deepEqual( + // docResponse.response.requestHeaders, + // docRequest.request.headers, + // "Response event reports same request headers as request event" + // ); + + const scriptRequest = scriptEvents[0].payload; + is( + scriptRequest.documentURL, + FRAMESET_URL, + "documentURL is trigger document" + ); + is(scriptRequest.frameId, frameIdNav, "Got the expected frame id"); + is(scriptRequest.request.url, FRAMESET_JS_URL, "Got the Script request"); + + const scriptResponse = scriptEvents[1].payload; + is(scriptResponse.frameId, frameIdNav, "Got the expected frame id"); + todo( + scriptResponse.loaderId === docRequest.loaderId, + "The same loaderId is used for dependent responses (Bug 1637838)" + ); + is(scriptResponse.response.url, FRAMESET_JS_URL, "Got the Script response"); + Assert.deepEqual( + scriptResponse.response.requestHeaders, + scriptRequest.request.headers, + "Response event reports same request headers as request event" + ); + + // frame is attached after all resources of the document have been loaded + // and before sub document starts loading + assertEventOrder(scriptEvents[1], frameAttachedEvents[0], { + ignoreTimestamps: true, + }); + assertEventOrder(frameAttachedEvents[0], subdocumentEvents[0], { + ignoreTimestamps: true, + }); + + const { + frameId: frameIdSubFrame, + parentFrameId, + } = frameAttachedEvents[0].payload; + is(parentFrameId, frameIdNav, "Got expected parent frame id"); + + // network events for subdocument and script + assertEventOrder(subdocumentEvents[0], subdocumentEvents[1]); + assertEventOrder(subdocumentEvents[1], scriptEvents[2]); + assertEventOrder(scriptEvents[2], scriptEvents[3]); + + const subdocRequest = subdocumentEvents[0].payload; + is( + subdocRequest.documentURL, + FRAMESET_URL, + "documentURL is trigger document" + ); + is(subdocRequest.frameId, frameIdSubFrame, "Got the expected frame id"); + is(subdocRequest.request.url, PAGE_URL, "Got the Subdocument request"); + + const subdocResponse = subdocumentEvents[1].payload; + is(subdocResponse.frameId, frameIdSubFrame, "Got the expected frame id"); + is(subdocResponse.response.url, PAGE_URL, "Got the Subdocument response"); + + const subscriptRequest = scriptEvents[2].payload; + is(subscriptRequest.documentURL, PAGE_URL, "documentURL is trigger document"); + is(subscriptRequest.frameId, frameIdSubFrame, "Got the expected frame id"); + is(subscriptRequest.request.url, PAGE_JS_URL, "Got the Script request"); + + const subscriptResponse = scriptEvents[3].payload; + is(subscriptResponse.frameId, frameIdSubFrame, "Got the expected frame id"); + is(subscriptResponse.response.url, PAGE_JS_URL, "Got the Script response"); + todo( + subscriptResponse.loaderId === subdocRequest.loaderId, + "The same loaderId is used for dependent responses (Bug 1637838)" + ); + Assert.deepEqual( + subscriptResponse.response.requestHeaders, + subscriptRequest.request.headers, + "Response event reports same request headers as request event" + ); + + const lifeCycleEvents = history + .findEvents("Page.lifecycleEvent") + .map(event => event.payload); + for (const { name, loaderId } of lifeCycleEvents) { + is( + loaderId, + docRequest.loaderId, + `${name} lifecycle event has same loaderId as Document request` + ); + } +}); + +async function prepareTest(client, url, totalCount) { + const REQUEST = "Network.requestWillBeSent"; + const RESPONSE = "Network.responseReceived"; + const FRAMEATTACHED = "Page.frameAttached"; + const LIFECYCLE = "Page.livecycleEvent"; + + const { Network, Page } = client; + const history = new RecordEvents(totalCount); + + history.addRecorder({ + event: Network.requestWillBeSent, + eventName: REQUEST, + messageFn: payload => { + return `Received ${REQUEST} for ${payload.request?.url}`; + }, + }); + + history.addRecorder({ + event: Network.responseReceived, + eventName: RESPONSE, + messageFn: payload => { + return `Received ${RESPONSE} for ${payload.response?.url}`; + }, + }); + + history.addRecorder({ + event: Page.frameAttached, + eventName: FRAMEATTACHED, + messageFn: ({ frameId, parentFrameId: parentId }) => { + return `Received ${FRAMEATTACHED} frame=${frameId} parent=${parentId}`; + }, + }); + + history.addRecorder({ + event: Page.lifecycleEvent, + eventName: LIFECYCLE, + messageFn: payload => { + return `Received ${LIFECYCLE} ${payload.name}`; + }, + }); + + await Network.enable(); + await Page.enable(); + + const navigateDone = history.addPromise("Page.navigate"); + const { frameId } = await Page.navigate({ url }).then(navigateDone); + ok(frameId, "Page.navigate returned a frameId"); + + info("Wait for events"); + const events = await history.record(); + + info(`Received events: ${events.map(getDescriptionForEvent)}`); + is(events.length, totalCount, "Received expected number of events"); + + return { history, frameId }; +} diff --git a/remote/test/browser/network/browser_requestWillBeSent.js b/remote/test/browser/network/browser_requestWillBeSent.js new file mode 100644 index 0000000000..274bf50e51 --- /dev/null +++ b/remote/test/browser/network/browser_requestWillBeSent.js @@ -0,0 +1,154 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const BASE_PATH = "http://example.com/browser/remote/test/browser/network"; +const FRAMESET_URL = `${BASE_PATH}/doc_frameset.html`; +const FRAMESET_JS_URL = `${BASE_PATH}/file_framesetEvents.js`; +const PAGE_URL = `${BASE_PATH}/doc_networkEvents.html`; +const PAGE_JS_URL = `${BASE_PATH}/file_networkEvents.js`; + +add_task(async function noEventsWhenNetworkDomainDisabled({ client }) { + const history = configureHistory(client, 0); + await loadURL(PAGE_URL); + + const events = await history.record(); + is(events.length, 0, "Expected no Network.responseReceived events"); +}); + +add_task(async function noEventsAfterNetworkDomainDisabled({ client }) { + const { Network } = client; + + const history = configureHistory(client, 0); + await Network.enable(); + await Network.disable(); + await loadURL(PAGE_URL); + + const events = await history.record(); + is(events.length, 0, "Expected no Network.responseReceived events"); +}); + +add_task(async function documentNavigationWithResource({ client }) { + const { Page, Network } = client; + + await Network.enable(); + await Page.enable(); + + const history = configureHistory(client, 4); + + const frameAttached = Page.frameAttached(); + const { frameId: frameIdNav } = await Page.navigate({ url: FRAMESET_URL }); + const { frameId: frameIdSubFrame } = await frameAttached; + ok(frameIdNav, "Page.navigate returned a frameId"); + + info("Wait for Network events"); + const events = await history.record(); + is(events.length, 4, "Expected number of Network.requestWillBeSent events"); + + // Check top-level document request + const docRequest = events[0].payload; + is(docRequest.type, "Document", "Document request has the expected type"); + is(docRequest.documentURL, FRAMESET_URL, "documentURL matches requested url"); + is(docRequest.frameId, frameIdNav, "Got the expected frame id"); + is(docRequest.request.url, FRAMESET_URL, "Got the Document request"); + is(docRequest.request.method, "GET", "Has the expected request method"); + is( + docRequest.requestId, + docRequest.loaderId, + "The request id is equal to the loader id" + ); + is( + docRequest.request.headers.host, + "example.com", + "Document request has headers" + ); + + // Check top-level script request + const scriptRequest = events[1].payload; + is(scriptRequest.type, "Script", "Script request has the expected type"); + is( + scriptRequest.documentURL, + FRAMESET_URL, + "documentURL is trigger document for the script request" + ); + is(scriptRequest.frameId, frameIdNav, "Got the expected frame id"); + is(scriptRequest.request.url, FRAMESET_JS_URL, "Got the Script request"); + is(scriptRequest.request.method, "GET", "Has the expected request method"); + is( + scriptRequest.request.headers.host, + "example.com", + "Script request has headers" + ); + todo( + scriptRequest.loaderId === docRequest.loaderId, + "The same loaderId is used for dependent requests (Bug 1637838)" + ); + assertEventOrder(events[0], events[1]); + + // Check subdocument request + const subdocRequest = events[2].payload; + is( + subdocRequest.type, + "Subdocument", + "Subdocument request has the expected type" + ); + is(subdocRequest.documentURL, FRAMESET_URL, "documenURL matches request url"); + is(subdocRequest.frameId, frameIdSubFrame, "Got the expected frame id"); + is( + subdocRequest.requestId, + subdocRequest.loaderId, + "The request id is equal to the loader id" + ); + is(subdocRequest.request.url, PAGE_URL, "Got the Subdocument request"); + is(subdocRequest.request.method, "GET", "Has the expected request method"); + is( + subdocRequest.request.headers.host, + "example.com", + "Subdocument request has headers" + ); + assertEventOrder(events[1], events[2]); + + // Check script request (frame) + const subscriptRequest = events[3].payload; + is(subscriptRequest.type, "Script", "Script request has the expected type"); + is( + subscriptRequest.documentURL, + PAGE_URL, + "documentURL is trigger document for the script request" + ); + is(subscriptRequest.frameId, frameIdSubFrame, "Got the expected frame id"); + todo( + subscriptRequest.loaderId === docRequest.loaderId, + "The same loaderId is used for dependent requests (Bug 1637838)" + ); + is(subscriptRequest.request.url, PAGE_JS_URL, "Got the Script request"); + is( + subscriptRequest.request.method, + "GET", + "Script request has the expected method" + ); + is( + subscriptRequest.request.headers.host, + "example.com", + "Script request has headers" + ); + assertEventOrder(events[2], events[3]); +}); + +function configureHistory(client, total) { + const REQUEST = "Network.requestWillBeSent"; + + const { Network } = client; + const history = new RecordEvents(total); + + history.addRecorder({ + event: Network.requestWillBeSent, + eventName: REQUEST, + messageFn: payload => { + return `Received ${REQUEST} for ${payload.request.url}`; + }, + }); + + return history; +} diff --git a/remote/test/browser/network/browser_responseReceived.js b/remote/test/browser/network/browser_responseReceived.js new file mode 100644 index 0000000000..a258c95010 --- /dev/null +++ b/remote/test/browser/network/browser_responseReceived.js @@ -0,0 +1,236 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const BASE_PATH = "http://example.com/browser/remote/test/browser/network"; +const FRAMESET_URL = `${BASE_PATH}/doc_frameset.html`; +const FRAMESET_JS_URL = `${BASE_PATH}/file_framesetEvents.js`; +const PAGE_URL = `${BASE_PATH}/doc_networkEvents.html`; +const PAGE_JS_URL = `${BASE_PATH}/file_networkEvents.js`; + +add_task(async function noEventsWhenNetworkDomainDisabled({ client }) { + const history = configureHistory(client, 0); + await loadURL(PAGE_URL); + + const events = await history.record(); + is(events.length, 0, "Expected no Network.responseReceived events"); +}); + +add_task(async function noEventsAfterNetworkDomainDisabled({ client }) { + const { Network } = client; + + const history = configureHistory(client, 0); + await Network.enable(); + await Network.disable(); + await loadURL(PAGE_URL); + + const events = await history.record(); + is(events.length, 0, "Expected no Network.responseReceived events"); +}); + +add_task(async function documentNavigationWithResource({ client }) { + const { Page, Network } = client; + + await Network.enable(); + await Page.enable(); + + const history = configureHistory(client, 4); + + const frameAttached = Page.frameAttached(); + const { frameId: frameIdNav } = await Page.navigate({ url: FRAMESET_URL }); + const { frameId: frameIdSubframe } = await frameAttached; + ok(frameIdNav, "Page.navigate returned a frameId"); + + info("Wait for Network events"); + const events = await history.record(); + is(events.length, 4, "Expected number of Network.responseReceived events"); + + // Check top-level document response + const docResponse = events[0].payload; + is(docResponse.type, "Document", "Document response has expected type"); + is(docResponse.frameId, frameIdNav, "Got the expected frame id"); + is( + docResponse.requestId, + docResponse.loaderId, + "The response id is equal to the loader id" + ); + is(docResponse.response.url, FRAMESET_URL, "Got the Document response"); + is( + docResponse.response.mimeType, + "text/html", + "Document response has expected mimeType" + ); + ok(!!docResponse.response.headers.server, "Document response has headers"); + is(docResponse.response.status, 200, "Document response has expected status"); + is( + docResponse.response.statusText, + "OK", + "Document response has expected status text" + ); + if (docResponse.response.fromDiskCache === false) { + is( + docResponse.response.remoteIPAddress, + "127.0.0.1", + "Document response has the expected IP address" + ); + ok( + typeof docResponse.response.remotePort == "number", + "Document response has a remotePort" + ); + } + is( + docResponse.response.protocol, + "http/1.1", + "Document response has expected protocol" + ); + + // Check top-level script response + const scriptResponse = events[1].payload; + is(scriptResponse.type, "Script", "Script response has expected type"); + is(scriptResponse.frameId, frameIdNav, "Got the expected frame id"); + is(scriptResponse.response.url, FRAMESET_JS_URL, "Got the Script response"); + is( + scriptResponse.response.mimeType, + "application/x-javascript", + "Script response has expected mimeType" + ); + ok(!!scriptResponse.response.headers.server, "Script response has headers"); + is( + scriptResponse.response.status, + 200, + "Script response has the expected status" + ); + is( + scriptResponse.response.statusText, + "OK", + "Script response has the expected status text" + ); + if (scriptResponse.response.fromDiskCache === false) { + is( + scriptResponse.response.remoteIPAddress, + docResponse.response.remoteIPAddress, + "Script response has same IP address as document response" + ); + ok( + typeof scriptResponse.response.remotePort == "number", + "Script response has a remotePort" + ); + } + is( + scriptResponse.response.protocol, + "http/1.1", + "Script response has the expected protocol" + ); + + // Check subdocument response + const frameDocResponse = events[2].payload; + is( + frameDocResponse.type, + "Subdocument", + "Subdocument response has expected type" + ); + is(frameDocResponse.frameId, frameIdSubframe, "Got the expected frame id"); + is( + frameDocResponse.requestId, + frameDocResponse.loaderId, + "The response id is equal to the loader id" + ); + is( + frameDocResponse.response.url, + PAGE_URL, + "Got the expected Document response" + ); + is( + frameDocResponse.response.mimeType, + "text/html", + "Document response has expected mimeType" + ); + ok( + !!frameDocResponse.response.headers.server, + "Subdocument response has headers" + ); + is( + frameDocResponse.response.status, + 200, + "Subdocument response has expected status" + ); + is( + frameDocResponse.response.statusText, + "OK", + "Subdocument response has expected status text" + ); + if (frameDocResponse.response.fromDiskCache === false) { + is( + frameDocResponse.response.remoteIPAddress, + "127.0.0.1", + "Subdocument response has the expected IP address" + ); + ok( + typeof frameDocResponse.response.remotePort == "number", + "Subdocument response has a remotePort" + ); + } + is( + frameDocResponse.response.protocol, + "http/1.1", + "Subdocument response has expected protocol" + ); + + // Check frame script response + const frameScriptResponse = events[3].payload; + is(frameScriptResponse.type, "Script", "Script response has expected type"); + is(frameScriptResponse.frameId, frameIdSubframe, "Got the expected frame id"); + is(frameScriptResponse.response.url, PAGE_JS_URL, "Got the Script response"); + is( + frameScriptResponse.response.mimeType, + "application/x-javascript", + "Script response has expected mimeType" + ); + ok( + !!frameScriptResponse.response.headers.server, + "Script response has headers" + ); + is( + frameScriptResponse.response.status, + 200, + "Script response has the expected status" + ); + is( + frameScriptResponse.response.statusText, + "OK", + "Script response has the expected status text" + ); + if (frameScriptResponse.response.fromDiskCache === false) { + is( + frameScriptResponse.response.remoteIPAddress, + docResponse.response.remoteIPAddress, + "Script response has same IP address as document response" + ); + ok( + typeof frameScriptResponse.response.remotePort == "number", + "Script response has a remotePort" + ); + } + is( + frameScriptResponse.response.protocol, + "http/1.1", + "Script response has the expected protocol" + ); +}); + +function configureHistory(client, total) { + const RESPONSE = "Network.responseReceived"; + + const { Network } = client; + const history = new RecordEvents(total); + + history.addRecorder({ + event: Network.responseReceived, + eventName: RESPONSE, + messageFn: payload => { + return `Received ${RESPONSE} for ${payload.response.url}`; + }, + }); + return history; +} diff --git a/remote/test/browser/network/browser_setCacheDisabled.js b/remote/test/browser/network/browser_setCacheDisabled.js new file mode 100644 index 0000000000..610c4b7531 --- /dev/null +++ b/remote/test/browser/network/browser_setCacheDisabled.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { INHIBIT_CACHING, LOAD_BYPASS_CACHE, LOAD_NORMAL } = Ci.nsIRequest; + +const TEST_PAGE = + "http://example.com/browser/remote/test/browser/network/doc_empty.html"; + +add_task(async function cacheEnabledAfterDisabled({ client }) { + const { Network } = client; + await Network.setCacheDisabled({ cacheDisabled: true }); + await Network.setCacheDisabled({ cacheDisabled: false }); + + const checkPromise = checkLoadFlags(LOAD_NORMAL, TEST_PAGE); + await loadURL(TEST_PAGE); + await checkPromise; +}); + +add_task(async function cacheEnabledByDefault({ Network }) { + const checkPromise = checkLoadFlags(LOAD_NORMAL, TEST_PAGE); + await loadURL(TEST_PAGE); + await checkPromise; +}); + +add_task(async function cacheDisabled({ client }) { + const { Network } = client; + await Network.setCacheDisabled({ cacheDisabled: true }); + + const checkPromise = checkLoadFlags( + LOAD_BYPASS_CACHE | INHIBIT_CACHING, + TEST_PAGE + ); + await loadURL(TEST_PAGE); + await checkPromise; +}); + +function checkLoadFlags(flags, url) { + return ContentTask.spawn( + gBrowser.selectedBrowser, + { flags, url }, + async (options = {}) => { + const { flags, url } = options; + + // an nsIWebProgressListener that checks all requests made by the docShell + // have the flags we expect. + var RequestWatcher = { + init(docShell, expectedLoadFlags, url, callback) { + this.callback = callback; + this.docShell = docShell; + this.expectedLoadFlags = expectedLoadFlags; + this.url = url; + + this.requestCount = 0; + + const { + NOTIFY_STATE_DOCUMENT, + NOTIFY_STATE_REQUEST, + } = Ci.nsIWebProgress; + + this.docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress) + .addProgressListener( + this, + NOTIFY_STATE_DOCUMENT | NOTIFY_STATE_REQUEST + ); + }, + + onStateChange(webProgress, request, flags, status) { + // We are checking requests - if there isn't one, ignore it. + if (!request) { + return; + } + + // We will usually see requests for 'about:document-onload-blocker' not + // have the flag, so we just ignore them. + // We also see, eg, resource://gre-resources/loading-image.png, so + // skip resource:// URLs too. + // We may also see, eg, chrome://global/skin/icons/chevron.svg, so + // skip chrome:// URLs too. + if ( + request.name.startsWith("about:") || + request.name.startsWith("resource:") || + request.name.startsWith("chrome:") + ) { + return; + } + is( + request.loadFlags & this.expectedLoadFlags, + this.expectedLoadFlags, + "request " + request.name + " has the expected flags" + ); + this.requestCount += 1; + + var stopFlags = + Ci.nsIWebProgressListener.STATE_STOP | + Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; + + if (request.name == this.url && (flags & stopFlags) == stopFlags) { + this.docShell.removeProgressListener(this); + ok( + this.requestCount > 1, + this.url + " saw " + this.requestCount + " requests" + ); + this.callback(); + } + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + }; + + await new Promise(resolve => { + RequestWatcher.init(docShell, flags, url, resolve); + }); + } + ); +} diff --git a/remote/test/browser/network/browser_setCookie.js b/remote/test/browser/network/browser_setCookie.js new file mode 100644 index 0000000000..bb16e65c56 --- /dev/null +++ b/remote/test/browser/network/browser_setCookie.js @@ -0,0 +1,298 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const SJS_PATH = "/browser/remote/test/browser/network/sjs-cookies.sjs"; + +const DEFAULT_HOST = "example.org"; +const ALT_HOST = "foo.example.org"; +const SECURE_HOST = "example.com"; + +const DEFAULT_URL = `http://${DEFAULT_HOST}`; + +add_task(async function failureWithoutArguments({ client }) { + const { Network } = client; + + let errorThrown = false; + try { + await Network.setCookie(); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "Fails without any arguments"); +}); + +add_task(async function failureWithMissingNameAndValue({ client }) { + const { Network } = client; + + let errorThrown = false; + try { + await Network.setCookie({ + value: "bar", + domain: "example.org", + }); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "Fails without name specified"); + + errorThrown = false; + try { + await Network.setCookie({ + name: "foo", + domain: "example.org", + }); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "Fails without value specified"); +}); + +add_task(async function failureWithMissingDomainAndURL({ client }) { + const { Network } = client; + + let errorThrown = false; + try { + await Network.setCookie({ name: "foo", value: "bar" }); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "Fails without domain and URL specified"); +}); + +add_task(async function setCookieWithDomain({ client }) { + const { Network } = client; + + const cookie = { + name: "foo", + value: "bar", + domain: ALT_HOST, + }; + + try { + const { success } = await Network.setCookie(cookie); + ok(success, "Cookie has been set"); + + const cookies = getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function setCookieWithEmptyDomain({ client }) { + const { Network } = client; + + try { + const { success } = await Network.setCookie({ + name: "foo", + value: "bar", + url: "", + }); + ok(!success, "Cookie has not been set"); + + const cookies = getCookies(); + is(cookies.length, 0, "No cookie has been found"); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function setCookieWithURL({ client }) { + const { Network } = client; + + const cookie = { + name: "foo", + value: "bar", + domain: ALT_HOST, + }; + + try { + const { success } = await Network.setCookie({ + name: cookie.name, + value: cookie.value, + url: `http://${ALT_HOST}`, + }); + ok(success, "Cookie has been set"); + + const cookies = getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function setCookieWithEmptyURL({ client }) { + const { Network } = client; + + try { + const { success } = await Network.setCookie({ + name: "foo", + value: "bar", + url: "", + }); + ok(!success, "No cookie has been set"); + + const cookies = getCookies(); + is(cookies.length, 0, "No cookie has been found"); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function setCookieWithDomainAndURL({ client }) { + const { Network } = client; + + const cookie = { + name: "foo", + value: "bar", + domain: ALT_HOST, + }; + + try { + const { success } = await Network.setCookie({ + name: cookie.name, + value: cookie.value, + domain: cookie.domain, + url: `http://${DEFAULT_HOST}`, + }); + ok(success, "Cookie has been set"); + + const cookies = getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function setCookieWithHttpOnly({ client }) { + const { Network } = client; + + const cookie = { + name: "foo", + value: "bar", + domain: DEFAULT_HOST, + httpOnly: true, + }; + + try { + const { success } = await Network.setCookie(cookie); + ok(success, "Cookie has been set"); + + const cookies = getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function setCookieWithExpiry({ client }) { + const { Network } = client; + + const tomorrow = Math.floor(Date.now() / 1000) + 60 * 60 * 24; + + const cookie = { + name: "foo", + value: "bar", + domain: DEFAULT_HOST, + expires: tomorrow, + session: false, + }; + + try { + const { success } = await Network.setCookie(cookie); + ok(success, "Cookie has been set"); + + const cookies = getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function setCookieWithPath({ client }) { + const { Network } = client; + + const cookie = { + name: "foo", + value: "bar", + domain: ALT_HOST, + path: SJS_PATH, + }; + + try { + const { success } = await Network.setCookie(cookie); + ok(success, "Cookie has been set"); + + const cookies = getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function testAddSameSiteCookie({ client }) { + const { Network } = client; + + for (const sameSite of ["None", "Lax", "Strict"]) { + console.log(`Check same site value: ${sameSite}`); + const cookie = { + name: "foo", + value: "bar", + domain: DEFAULT_HOST, + }; + if (sameSite != "None") { + cookie.sameSite = sameSite; + } + + try { + const { success } = await Network.setCookie({ + name: cookie.name, + value: cookie.value, + domain: cookie.domain, + sameSite, + }); + ok(success, "Cookie has been set"); + + const cookies = getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + } finally { + Services.cookies.removeAll(); + } + } +}); + +add_task(async function testAddSecureCookie({ client }) { + const { Network } = client; + + const cookie = { + name: "foo", + value: "bar", + domain: "example.com", + secure: true, + }; + + try { + const { success } = await Network.setCookie({ + name: cookie.name, + value: cookie.value, + url: `https://${SECURE_HOST}`, + }); + ok(success, "Cookie has been set"); + + const cookies = getCookies(); + is(cookies.length, 1, "A single cookie has been found"); + assertCookie(cookies[0], cookie); + ok(cookies[0].secure, `Cookie for HTTPS is secure`); + } finally { + Services.cookies.removeAll(); + } +}); diff --git a/remote/test/browser/network/browser_setCookies.js b/remote/test/browser/network/browser_setCookies.js new file mode 100644 index 0000000000..c89a68ca16 --- /dev/null +++ b/remote/test/browser/network/browser_setCookies.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ALT_HOST = "foo.example.org"; +const DEFAULT_HOST = "example.org"; + +add_task(async function failureWithoutArguments({ client }) { + const { Network } = client; + + let errorThrown = false; + try { + await Network.setCookies(); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "Fails without any arguments"); +}); + +add_task(async function setCookies({ client }) { + const { Network } = client; + + const expected_cookies = [ + { + name: "foo", + value: "bar", + domain: DEFAULT_HOST, + }, + { + name: "user", + value: "password", + domain: ALT_HOST, + }, + ]; + + try { + await Network.setCookies({ cookies: expected_cookies }); + + const cookies = getCookies(); + cookies.sort((a, b) => a.name.localeCompare(b.name)); + is(cookies.length, expected_cookies.length, "Correct number of cookies"); + assertCookie(cookies[0], expected_cookies[0]); + assertCookie(cookies[1], expected_cookies[1]); + } finally { + Services.cookies.removeAll(); + } +}); + +add_task(async function setCookiesWithInvalidField({ client }) { + const { Network } = client; + + const cookies = [ + { + name: "foo", + value: "bar", + domain: "", + }, + ]; + + let errorThrown = false; + try { + await Network.setCookies({ cookies }); + } catch (e) { + errorThrown = true; + } finally { + Services.cookies.removeAll(); + } + ok(errorThrown, "Fails with an invalid field"); +}); diff --git a/remote/test/browser/network/browser_setUserAgentOverride.js b/remote/test/browser/network/browser_setUserAgentOverride.js new file mode 100644 index 0000000000..911a0a296c --- /dev/null +++ b/remote/test/browser/network/browser_setUserAgentOverride.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL(`<script>document.write(navigator.userAgent);</script>`); + +// Network.setUserAgentOverride is a redirect to Emulation.setUserAgentOverride. +// Run at least one test for setting and resetting the user agent to make sure +// that the redirect works. + +add_task(async function forwardToEmulation({ client }) { + const { Network } = client; + const userAgent = "Mozilla/5.0 (rv: 23) Romanesco/42.0"; + const platform = "foobar"; + + await loadURL(DOC); + const originalUserAgent = await getNavigatorProperty("userAgent"); + const originalPlatform = await getNavigatorProperty("platform"); + + isnot(originalUserAgent, userAgent, "Custom user agent hasn't been set"); + isnot(originalPlatform, platform, "Custom platform hasn't been set"); + + await Network.setUserAgentOverride({ userAgent, platform }); + await loadURL(DOC); + is( + await getNavigatorProperty("userAgent"), + userAgent, + "Custom user agent has been set" + ); + is( + await getNavigatorProperty("platform"), + platform, + "Custom platform has been set" + ); + + await Network.setUserAgentOverride({ userAgent: "", platform: "" }); + await loadURL(DOC); + is( + await getNavigatorProperty("userAgent"), + originalUserAgent, + "Custom user agent has been reset" + ); + is( + await getNavigatorProperty("platform"), + originalPlatform, + "Custom platform has been reset" + ); + + await Network.setUserAgentOverride({ userAgent, platform }); + await loadURL(DOC); + is( + await getNavigatorProperty("userAgent"), + userAgent, + "Custom user agent has been set" + ); + is( + await getNavigatorProperty("platform"), + platform, + "Custom platform has been set" + ); +}); + +async function getNavigatorProperty(prop) { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [prop], _prop => { + return content.navigator[_prop]; + }); +} diff --git a/remote/test/browser/network/doc_empty.html b/remote/test/browser/network/doc_empty.html new file mode 100644 index 0000000000..effbcb5fac --- /dev/null +++ b/remote/test/browser/network/doc_empty.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test page</title> +</head> +<body> + <div id="content">Example page</div> +</body> +</html> diff --git a/remote/test/browser/network/doc_frameset.html b/remote/test/browser/network/doc_frameset.html new file mode 100644 index 0000000000..9fa4fb80e3 --- /dev/null +++ b/remote/test/browser/network/doc_frameset.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <title>Frameset for Network events</title> + <script type="text/javascript" src="file_framesetEvents.js"></script> +</head> +<body> + <iframe src="doc_networkEvents.html"></iframe> +</body> +</html> diff --git a/remote/test/browser/network/doc_networkEvents.html b/remote/test/browser/network/doc_networkEvents.html new file mode 100644 index 0000000000..51482954cc --- /dev/null +++ b/remote/test/browser/network/doc_networkEvents.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test page for Network events</title> + <script type="text/javascript" src="file_networkEvents.js"></script> +</head> +<body> +</body> +</html> diff --git a/remote/test/browser/network/file_framesetEvents.js b/remote/test/browser/network/file_framesetEvents.js new file mode 100644 index 0000000000..3e146d265d --- /dev/null +++ b/remote/test/browser/network/file_framesetEvents.js @@ -0,0 +1,2 @@ +// Test file to emit Network events. +var foo = true; diff --git a/remote/test/browser/network/file_networkEvents.js b/remote/test/browser/network/file_networkEvents.js new file mode 100644 index 0000000000..3e146d265d --- /dev/null +++ b/remote/test/browser/network/file_networkEvents.js @@ -0,0 +1,2 @@ +// Test file to emit Network events. +var foo = true; diff --git a/remote/test/browser/network/head.js b/remote/test/browser/network/head.js new file mode 100644 index 0000000000..d3a9786a4b --- /dev/null +++ b/remote/test/browser/network/head.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../head.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/test/browser/head.js", + this +); + +function assertCookie(cookie, expected = {}) { + const { + name = "", + value = "", + domain = "example.org", + path = "/", + expires = -1, + size = name.length + value.length, + httpOnly = false, + secure = false, + session = true, + sameSite, + } = expected; + + const expectedCookie = { + name, + value, + domain, + path, + expires, + size, + httpOnly, + secure, + session, + }; + + if (sameSite) { + expectedCookie.sameSite = sameSite; + } + + Assert.deepEqual(cookie, expectedCookie); +} + +function assertEventOrder(first, second, options = {}) { + const { ignoreTimestamps = false } = options; + + const firstDescription = getDescriptionForEvent(first); + const secondDescription = getDescriptionForEvent(second); + + ok( + first.index < second.index, + `${firstDescription} received before ${secondDescription})` + ); + + if (!ignoreTimestamps) { + ok( + first.payload.timestamp <= second.payload.timestamp, + `Timestamp of ${firstDescription}) is earlier than ${secondDescription})` + ); + } +} + +function filterEventsByType(history, type) { + return history.filter(event => event.payload.type == type); +} + +function getCookies() { + return Services.cookies.cookies.map(cookie => { + const data = { + name: cookie.name, + value: cookie.value, + domain: cookie.host, + path: cookie.path, + expires: cookie.isSession ? -1 : cookie.expiry, + // The size is the combined length of both the cookie name and value + size: cookie.name.length + cookie.value.length, + httpOnly: cookie.isHttpOnly, + secure: cookie.isSecure, + session: cookie.isSession, + }; + + if (cookie.sameSite) { + const sameSiteMap = new Map([ + [Ci.nsICookie.SAMESITE_LAX, "Lax"], + [Ci.nsICookie.SAMESITE_STRICT, "Strict"], + ]); + + data.sameSite = sameSiteMap.get(cookie.sameSite); + } + + return data; + }); +} + +function getDescriptionForEvent(event) { + const { eventName, payload } = event; + + return `${eventName}(${payload.type || payload.name || payload.frameId})`; +} diff --git a/remote/test/browser/network/sjs-cookies.sjs b/remote/test/browser/network/sjs-cookies.sjs new file mode 100644 index 0000000000..345a66880d --- /dev/null +++ b/remote/test/browser/network/sjs-cookies.sjs @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// eslint-disable-next-line mozilla/reject-importGlobalProperties +Cu.importGlobalProperties(["URLSearchParams"]); + +function handleRequest(request, response) { + const queryString = new URLSearchParams(request.queryString); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain; charset=utf-8", false); + + if (queryString.has("name") && queryString.has("value")) { + const name = queryString.get("name"); + const value = queryString.get("value"); + const path = queryString.get("path") || "/"; + + const expiry = queryString.get("expiry"); + const httpOnly = queryString.has("httpOnly"); + const secure = queryString.has("secure"); + const sameSite = queryString.get("sameSite"); + + let cookie = `${name}=${value}; Path=${path}`; + + if (expiry) { + cookie += `; Expires=${expiry}`; + } + + if (httpOnly) { + cookie += "; HttpOnly"; + } + + if (sameSite != undefined) { + cookie += `; sameSite=${sameSite}`; + } + + if (secure) { + cookie += "; Secure"; + } + + response.setHeader("Set-Cookie", cookie, true); + response.write(`Set cookie: ${cookie}`); + } +} diff --git a/remote/test/browser/page/browser.ini b/remote/test/browser/page/browser.ini new file mode 100644 index 0000000000..30f5638e7e --- /dev/null +++ b/remote/test/browser/page/browser.ini @@ -0,0 +1,39 @@ +[DEFAULT] +tags = remote +subsuite = remote +prefs = + remote.enabled=true +support-files = + !/remote/test/browser/chrome-remote-interface.js + !/remote/test/browser/head.js + head.js + doc_empty.html + sjs_redirect.sjs + +[browser_bringToFront.js] +[browser_captureScreenshot.js] +[browser_createIsolatedWorld.js] +[browser_domContentEventFired.js] +[browser_frameAttached.js] +[browser_frameDetached.js] +[browser_frameNavigated.js] +[browser_frameStartedLoading.js] +[browser_frameStoppedLoading.js] +[browser_getFrameTree.js] +[browser_getLayoutMetrics.js] +[browser_getNavigationHistory.js] +[browser_javascriptDialog_alert.js] +[browser_javascriptDialog_beforeunload.js] +[browser_javascriptDialog_confirm.js] +[browser_javascriptDialog_otherTarget.js] +[browser_javascriptDialog_prompt.js] +[browser_lifecycleEvent.js] +[browser_loadEventFired.js] +[browser_navigate.js] +[browser_navigateToHistoryEntry.js] +[browser_navigationEvents.js] +[browser_printToPDF.js] +[browser_reload.js] +[browser_runtimeEvents.js] +[browser_scriptToEvaluateOnNewDocument.js] +skip-if = socketprocess_networking diff --git a/remote/test/browser/page/browser_bringToFront.js b/remote/test/browser/page/browser_bringToFront.js new file mode 100644 index 0000000000..ed00071922 --- /dev/null +++ b/remote/test/browser/page/browser_bringToFront.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const FIRST_DOC = toDataURL("first"); +const SECOND_DOC = toDataURL("second"); + +add_task(async function testBringToFrontUpdatesSelectedTab({ client }) { + const tab = gBrowser.selectedTab; + + await loadURL(FIRST_DOC); + + info("Open another tab that should become the front tab"); + const otherTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + SECOND_DOC + ); + + try { + is(gBrowser.selectedTab, otherTab, "Selected tab is now the new tab"); + + const { Page } = client; + info( + "Call Page.bringToFront() and check that the test tab becomes the selected tab" + ); + await Page.bringToFront(); + is(gBrowser.selectedTab, tab, "Selected tab is the target tab again"); + is(tab.ownerGlobal, getFocusedNavigator(), "The initial window is focused"); + } finally { + BrowserTestUtils.removeTab(otherTab); + } +}); + +add_task(async function testBringToFrontUpdatesFocusedWindow({ client }) { + const tab = gBrowser.selectedTab; + + await loadURL(FIRST_DOC); + + is(tab.ownerGlobal, getFocusedNavigator(), "The initial window is focused"); + + const otherWindow = await BrowserTestUtils.openNewBrowserWindow(); + + try { + is(otherWindow, getFocusedNavigator(), "The new window is focused"); + + const { Page } = client; + info( + "Call Page.bringToFront() and check that the tab window is focused again" + ); + await Page.bringToFront(); + is( + tab.ownerGlobal, + getFocusedNavigator(), + "The initial window is focused again" + ); + } finally { + await BrowserTestUtils.closeWindow(otherWindow); + } +}); + +function getFocusedNavigator() { + return Services.wm.getMostRecentWindow("navigator:browser"); +} diff --git a/remote/test/browser/page/browser_captureScreenshot.js b/remote/test/browser/page/browser_captureScreenshot.js new file mode 100644 index 0000000000..075456955d --- /dev/null +++ b/remote/test/browser/page/browser_captureScreenshot.js @@ -0,0 +1,563 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function documentSmallerThanViewport({ client }) { + const { Page } = client; + + await loadURLWithElement(); + + info("Check that captureScreenshot() captures the viewport by default"); + const { data } = await Page.captureScreenshot(); + ok(!!data, "Screenshot data is not empty"); + + const scale = await getDevicePixelRatio(); + const viewport = await getViewportSize(); + const { mimeType, width, height } = await getImageDetails(data); + + is(mimeType, "image/png", "Screenshot has correct MIME type"); + is(width, (viewport.width - viewport.x) * scale, "Image has expected width"); + is( + height, + (viewport.height - viewport.y) * scale, + "Image has expected height" + ); +}); + +add_task(async function documentLargerThanViewport({ client }) { + const { Page } = client; + + await loadURL(toDataURL("<div style='margin: 100vh 100vw'>Hello world")); + + info("Check that captureScreenshot() captures the viewport by default"); + const { data } = await Page.captureScreenshot(); + ok(!!data, "Screenshot data is not empty"); + + const scale = await getDevicePixelRatio(); + const scrollbarSize = await getScrollbarSize(); + const viewport = await getViewportSize(); + const { mimeType, width, height } = await getImageDetails(data); + + is(mimeType, "image/png", "Screenshot has correct MIME type"); + is( + width, + (viewport.width - viewport.x - scrollbarSize.width) * scale, + "Image has expected width" + ); + is( + height, + (viewport.height - viewport.y - scrollbarSize.height) * scale, + "Image has expected height" + ); +}); + +add_task(async function invalidFormat({ client }) { + const { Page } = client; + await loadURL(toDataURL("<div>Hello world")); + + let errorThrown = false; + try { + await Page.captureScreenshot({ format: "foo" }); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "captureScreenshot raised error for invalid image format"); +}); + +add_task(async function asJPEGFormat({ client }) { + const { Page } = client; + await loadURL(toDataURL("<div>Hello world")); + + info("Check that captureScreenshot() captures as JPEG format"); + const { data } = await Page.captureScreenshot({ format: "jpeg" }); + ok(!!data, "Screenshot data is not empty"); + + const scale = await getDevicePixelRatio(); + const viewport = await getViewportSize(); + const { mimeType, height, width } = await getImageDetails(data); + + is(mimeType, "image/jpeg", "Screenshot has correct MIME type"); + is(width, (viewport.width - viewport.x) * scale); + is(height, (viewport.height - viewport.y) * scale); +}); + +add_task(async function asJPEGFormatAndQuality({ client }) { + const { Page } = client; + await loadURL(toDataURL("<div>Hello world")); + + info("Check that captureScreenshot() captures as JPEG format"); + const imageDefault = await Page.captureScreenshot({ format: "jpeg" }); + ok(!!imageDefault, "Screenshot data with default quality is not empty"); + + const image100 = await Page.captureScreenshot({ + format: "jpeg", + quality: 100, + }); + ok(!!image100, "Screenshot data with quality 100 is not empty"); + + const image10 = await Page.captureScreenshot({ + format: "jpeg", + quality: 10, + }); + ok(!!image10, "Screenshot data with quality 10 is not empty"); + + const infoDefault = await getImageDetails(imageDefault.data); + const info100 = await getImageDetails(image100.data); + const info10 = await getImageDetails(image10.data); + + // All screenshots are of mimeType JPEG + is( + infoDefault.mimeType, + "image/jpeg", + "Screenshot with default quality has correct MIME type" + ); + is( + info100.mimeType, + "image/jpeg", + "Screenshot with quality 100 has correct MIME type" + ); + is( + info10.mimeType, + "image/jpeg", + "Screenshot with quality 10 has correct MIME type" + ); + + const scale = await getDevicePixelRatio(); + const viewport = await getViewportSize(); + + // Images are all of the same dimension + is(infoDefault.width, (viewport.width - viewport.x) * scale); + is(infoDefault.height, (viewport.height - viewport.y) * scale); + + is(info100.width, (viewport.width - viewport.x) * scale); + is(info100.height, (viewport.height - viewport.y) * scale); + + is(info10.width, (viewport.width - viewport.x) * scale); + is(info10.height, (viewport.height - viewport.y) * scale); + + // Images of different quality result in different content sizes + ok( + info100.length > infoDefault.length, + "Size of quality 100 is larger than default" + ); + ok( + info10.length < infoDefault.length, + "Size of quality 10 is smaller than default" + ); +}); + +add_task(async function clipMissingProperties({ client }) { + const { Page } = client; + const contentSize = await getContentSize(); + + for (const prop of ["x", "y", "width", "height", "scale"]) { + console.info(`Check for missing ${prop}`); + + const clip = { + x: 0, + y: 0, + width: contentSize.width, + height: contentSize.height, + }; + clip[prop] = undefined; + + let errorThrown = false; + try { + await Page.captureScreenshot({ clip }); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, `raised error for missing clip.${prop} property`); + } +}); + +add_task(async function clipOutOfBoundsXAndY({ client }) { + const { Page } = client; + + const ratio = await getDevicePixelRatio(); + const size = 50; + + await loadURLWithElement(); + const contentSize = await getContentSize(); + + var { data: refData } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width: size, + height: size, + scale: 1, + }, + }); + + for (const x of [-1, contentSize.width]) { + console.info(`Check out-of-bounds x for ${x}`); + const { data } = await Page.captureScreenshot({ + clip: { + x, + y: 0, + width: size, + height: size, + scale: 1, + }, + }); + const { width, height } = await getImageDetails(data); + + is(width, size * ratio, "Image has expected width"); + is(height, size * ratio, "Image has expected height"); + is(data, refData, "Image is equal"); + } + + for (const y of [-1, contentSize.height]) { + console.info(`Check out-of-bounds y for ${y}`); + const { data } = await Page.captureScreenshot({ + clip: { + x: 0, + y, + width: size, + height: size, + scale: 1, + }, + }); + const { width, height } = await getImageDetails(data); + + is(width, size * ratio, "Image has expected width"); + is(height, size * ratio, "Image has expected height"); + is(data, refData, "Image is equal"); + } +}); + +add_task(async function clipOutOfBoundsWidthAndHeight({ client }) { + const { Page } = client; + const ratio = await getDevicePixelRatio(); + + await loadURL(toDataURL("<div style='margin: 100vh 100vw'>Hello world")); + const contentSize = await getContentSize(); + + var { data: refData } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width: contentSize.width, + height: contentSize.height, + scale: 1, + }, + }); + + for (const value of [-1, 0]) { + console.info(`Check out-of-bounds width for ${value}`); + const clip = { + x: 0, + y: 0, + width: value, + height: contentSize.height, + scale: 1, + }; + + const { data } = await Page.captureScreenshot({ clip }); + const { width, height } = await getImageDetails(data); + is(width, contentSize.width * ratio, "Image has expected width"); + is(height, contentSize.height * ratio, "Image has expected height"); + is(data, refData, "Image is equal"); + } + + for (const value of [-1, 0]) { + console.info(`Check out-of-bounds height for ${value}`); + const clip = { + x: 0, + y: 0, + width: contentSize.width, + height: value, + scale: 1, + }; + + const { data } = await Page.captureScreenshot({ clip }); + const { width, height } = await getImageDetails(data); + is(width, contentSize.width * ratio, "Image has expected width"); + is(height, contentSize.height * ratio, "Image has expected height"); + is(data, refData, "Image is equal"); + } +}); + +add_task(async function clipOutOfBoundsScale({ client }) { + const { Page } = client; + const ratio = await getDevicePixelRatio(); + + await loadURLWithElement(); + const contentSize = await getContentSize(); + + var { data: refData } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width: contentSize.width, + height: contentSize.height, + scale: 1, + }, + }); + + for (const value of [-1, 0]) { + console.info(`Check out-of-bounds scale for ${value}`); + var { data } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width: 50, + height: 50, + scale: value, + }, + }); + + const { width, height } = await getImageDetails(data); + is(width, contentSize.width * ratio, "Image has expected width"); + is(height, contentSize.height * ratio, "Image has expected height"); + is(data, refData, "Image is equal"); + } +}); + +add_task(async function clipScale({ client }) { + const { Page } = client; + const ratio = await getDevicePixelRatio(); + + for (const scale of [1.5, 2]) { + console.info(`Check scale for ${scale}`); + await loadURLWithElement({ width: 100 * scale, height: 100 * scale }); + var { data: refData } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width: 100 * scale, + height: 100 * scale, + scale: 1, + }, + }); + + await loadURLWithElement({ width: 100, height: 100 }); + var { data } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width: 100, + height: 100, + scale, + }, + }); + + const { width, height } = await getImageDetails(data); + is(width, 100 * ratio * scale, "Image has expected width"); + is(height, 100 * ratio * scale, "Image has expected height"); + is(data, refData, "Image is equal"); + } +}); + +add_task(async function clipScaleAndDevicePixelRatio({ client }) { + const { Page } = client; + + const originalRatio = await getDevicePixelRatio(); + + const ratio = 2; + const scale = 1.5; + const size = 100; + + const expectedSize = size * ratio * scale; + + console.info(`Create reference screenshot: ${expectedSize}x${expectedSize}`); + await loadURLWithElement({ + width: expectedSize, + height: expectedSize, + }); + var { data: refData } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width: expectedSize, + height: expectedSize, + scale: 1, + }, + }); + + await setDevicePixelRatio(originalRatio * ratio); + + await loadURLWithElement({ width: size, height: size }); + var { data } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width: size, + height: size, + scale, + }, + }); + + const { width, height } = await getImageDetails(data); + is(width, expectedSize * originalRatio, "Image has expected width"); + is(height, expectedSize * originalRatio, "Image has expected height"); + is(data, refData, "Image is equal"); +}); + +add_task(async function clipPosition({ client }) { + const { Page } = client; + const ratio = await getDevicePixelRatio(); + + await loadURLWithElement(); + var { data: refData } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width: 100, + height: 100, + scale: 1, + }, + }); + + for (const [x, y] of [ + [10, 20], + [20, 10], + [20, 20], + ]) { + console.info(`Check postion for ${x} and ${y}`); + await loadURLWithElement({ x, y }); + var { data } = await Page.captureScreenshot({ + clip: { + x, + y, + width: 100, + height: 100, + scale: 1, + }, + }); + + const { width, height } = await getImageDetails(data); + is(width, 100 * ratio, "Image has expected width"); + is(height, 100 * ratio, "Image has expected height"); + is(data, refData, "Image is equal"); + } +}); + +add_task(async function clipDimension({ client }) { + const { Page } = client; + const ratio = await getDevicePixelRatio(); + + for (const [width, height] of [ + [10, 20], + [20, 10], + [20, 20], + ]) { + console.info(`Check width and height for ${width} and ${height}`); + + // Get reference image as section from a larger image + await loadURLWithElement({ width: 50, height: 50 }); + var { data: refData } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width, + height, + scale: 1, + }, + }); + + await loadURLWithElement({ width, height }); + var { data } = await Page.captureScreenshot({ + clip: { + x: 0, + y: 0, + width, + height, + scale: 1, + }, + }); + + const dimension = await getImageDetails(data); + is(dimension.width, width * ratio, "Image has expected width"); + is(dimension.height, height * ratio, "Image has expected height"); + is(data, refData, "Image is equal"); + } +}); + +async function loadURLWithElement(options = {}) { + const { x = 0, y = 0, width = 100, height = 100 } = options; + + const doc = ` + <style> + body { + margin: 0; + } + div { + margin-left: ${x}px; + margin-top: ${y}px; + width: ${width}px; + height: ${height}px; + background: green; + } + </style> + <body> + <div></div> + `; + + await loadURL(toDataURL(doc)); +} + +async function getDevicePixelRatio() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + return content.devicePixelRatio; + }); +} + +async function setDevicePixelRatio(dppx) { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [dppx], function(dppx) { + content.docShell.contentViewer.overrideDPPX = dppx; + is(content.devicePixelRatio, dppx, "devicePixelRatio override set"); + }); +} + +async function getImageDetails(image) { + const mimeType = getMimeType(image); + + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [{ mimeType, image }], + async function({ mimeType, image }) { + return new Promise(resolve => { + const img = new content.Image(); + img.addEventListener( + "load", + () => { + resolve({ + mimeType, + width: img.width, + height: img.height, + length: image.length, + }); + }, + { once: true } + ); + + img.src = `data:${mimeType};base64,${image}`; + }); + } + ); +} + +function getMimeType(image) { + // Decode from base64 and convert the first 4 bytes to hex + const raw = atob(image).slice(0, 4); + let magicBytes = ""; + for (let i = 0; i < raw.length; i++) { + magicBytes += raw + .charCodeAt(i) + .toString(16) + .toUpperCase(); + } + + switch (magicBytes) { + case "89504E47": + return "image/png"; + case "FFD8FFDB": + case "FFD8FFE0": + return "image/jpeg"; + default: + throw new Error("Unknown MIME type"); + } +} diff --git a/remote/test/browser/page/browser_createIsolatedWorld.js b/remote/test/browser/page/browser_createIsolatedWorld.js new file mode 100644 index 0000000000..8272beb044 --- /dev/null +++ b/remote/test/browser/page/browser_createIsolatedWorld.js @@ -0,0 +1,494 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test Page.createIsolatedWorld + +const DOC = toDataURL("default-test-page"); +const DOC_IFRAME = toDataURL(`<iframe src="data:text/html,${DOC}"></iframe>`); + +const WORLD_NAME_1 = "testWorld1"; +const WORLD_NAME_2 = "testWorld2"; + +const DESTROYED = "Runtime.executionContextDestroyed"; +const CREATED = "Runtime.executionContextCreated"; +const CLEARED = "Runtime.executionContextsCleared"; + +add_task(async function frameIdMissing({ client }) { + const { Page } = client; + + let errorThrown = ""; + try { + await Page.createIsolatedWorld({ + worldName: WORLD_NAME_1, + grantUniversalAccess: true, + }); + } catch (e) { + errorThrown = e.message; + } + ok( + errorThrown.match(/frameId: string value expected/), + `Fails with missing frameId` + ); +}); + +add_task(async function frameIdInvalidTypes({ client }) { + const { Page } = client; + + for (const frameId of [null, true, 1, [], {}]) { + let errorThrown = ""; + try { + await Page.createIsolatedWorld({ + frameId, + }); + } catch (e) { + errorThrown = e.message; + } + ok( + errorThrown.match(/frameId: string value expected/), + `Fails with invalid type: ${frameId}` + ); + } +}); + +add_task(async function worldNameInvalidTypes({ client }) { + const { Page } = client; + + await Page.enable(); + info("Page notifications are enabled"); + + const loadEvent = Page.loadEventFired(); + const { frameId } = await Page.navigate({ url: DOC }); + await loadEvent; + + for (const worldName of [null, true, 1, [], {}]) { + let errorThrown = ""; + try { + await Page.createIsolatedWorld({ + frameId, + worldName, + }); + } catch (e) { + errorThrown = e.message; + } + ok( + errorThrown.match(/worldName: string value expected/), + `Fails with invalid type: ${worldName}` + ); + } +}); + +add_task(async function noEventsWhenRuntimeDomainDisabled({ client }) { + const { Page, Runtime } = client; + + await Page.enable(); + info("Page notifications are enabled"); + + const history = recordEvents(Runtime, 0); + const loadEvent = Page.loadEventFired(); + const { frameId } = await Page.navigate({ url: DOC }); + await loadEvent; + + let errorThrown = ""; + try { + await Page.createIsolatedWorld({ + frameId, + worldName: WORLD_NAME_1, + grantUniversalAccess: true, + }); + await assertEventOrder({ history, expectedEvents: [] }); + } catch (e) { + errorThrown = e.message; + } + todo( + errorThrown === "", + "No contexts tracked internally without Runtime enabled (Bug 1623482)" + ); +}); + +add_task(async function noEventsAfterRuntimeDomainDisabled({ client }) { + const { Page, Runtime } = client; + + await Page.enable(); + info("Page notifications are enabled"); + + await enableRuntime(client); + await Runtime.disable(); + info("Runtime notifications are disabled"); + + const history = recordEvents(Runtime, 0); + const loadEvent = Page.loadEventFired(); + const { frameId } = await Page.navigate({ url: DOC }); + await loadEvent; + + await Page.createIsolatedWorld({ + frameId, + worldName: WORLD_NAME_2, + grantUniversalAccess: true, + }); + await assertEventOrder({ history, expectedEvents: [] }); +}); + +add_task(async function contextCreatedAfterNavigation({ client }) { + const { Page, Runtime } = client; + + await Page.enable(); + info("Page notifications are enabled"); + + await enableRuntime(client); + + const history = recordEvents(Runtime, 3); + const loadEvent = Page.loadEventFired(); + const { frameId } = await Page.navigate({ url: DOC }); + await loadEvent; + + const { executionContextId: isolatedId } = await Page.createIsolatedWorld({ + frameId, + worldName: WORLD_NAME_1, + grantUniversalAccess: true, + }); + await assertEventOrder({ + history, + expectedEvents: [ + DESTROYED, // default, about:blank + CREATED, // default, DOC + CREATED, // isolated, DOC + ], + }); + + const contexts = history + .findEvents(CREATED) + .map(event => event.payload.context); + const defaultContext = contexts[0]; + const isolatedContext = contexts[1]; + is(defaultContext.auxData.isDefault, true, "Default context is default"); + is( + defaultContext.auxData.type, + "default", + "Default context has type 'default'" + ); + is(defaultContext.origin, DOC, "Default context has expected origin"); + checkIsolated(isolatedContext, isolatedId, WORLD_NAME_1, frameId); + compareContexts(isolatedContext, defaultContext); +}); + +add_task(async function contextDestroyedForNavigation({ client }) { + const { Page, Runtime } = client; + + const defaultContext = await enableRuntime(client); + const isolatedContext = await createIsolatedContext(client, defaultContext); + + await Page.enable(); + + const history = recordEvents(Runtime, 4, true); + const frameNavigated = Page.frameNavigated(); + await Page.navigate({ url: DOC }); + await frameNavigated; + + await assertEventOrder({ + history, + expectedEvents: [ + DESTROYED, // default, about:blank + DESTROYED, // isolated, about:blank + CLEARED, + CREATED, // default, DOC + ], + }); + + const destroyed = history + .findEvents(DESTROYED) + .map(event => event.payload.executionContextId); + ok(destroyed.includes(isolatedContext.id), "Isolated context destroyed"); + ok(destroyed.includes(defaultContext.id), "Default context destroyed"); + + const { context: newContext } = history.findEvent(CREATED).payload; + is(newContext.auxData.isDefault, true, "The new context is a default one"); + ok(!!newContext.id, "The new context has an id"); + ok( + ![defaultContext.id, isolatedContext.id].includes(newContext.id), + "The new context has a new id" + ); +}); + +add_task(async function contextsForFramesetNavigation({ client }) { + const { Page, Runtime } = client; + + await Page.enable(); + info("Page notifications are enabled"); + + await enableRuntime(client); + + // check creation when navigating to a frameset + const historyTo = recordEvents(Runtime, 5); + const loadEventTo = Page.loadEventFired(); + const { frameId: frameIdTo } = await Page.navigate({ url: DOC_IFRAME }); + await loadEventTo; + + const { frameTree } = await Page.getFrameTree(); + const subFrame = frameTree.childFrames[0].frame; + + const { + executionContextId: contextIdParent, + } = await Page.createIsolatedWorld({ + frameId: frameIdTo, + worldName: WORLD_NAME_1, + grantUniversalAccess: true, + }); + const { + executionContextId: contextIdSubFrame, + } = await Page.createIsolatedWorld({ + frameId: subFrame.id, + worldName: WORLD_NAME_2, + grantUniversalAccess: true, + }); + + await assertEventOrder({ + history: historyTo, + expectedEvents: [ + DESTROYED, // default, about:blank + CREATED, // default, DOC_IFRAME + CREATED, // default, DOC + CREATED, // isolated, DOC_IFRAME + CREATED, // isolated, DOC + ], + }); + + const contextsCreated = historyTo + .findEvents(CREATED) + .map(event => event.payload.context); + const parentDefaultContextCreated = contextsCreated[0]; + const frameDefaultContextCreated = contextsCreated[1]; + const parentIsolatedContextCreated = contextsCreated[2]; + const frameIsolatedContextCreated = contextsCreated[3]; + + checkIsolated( + parentIsolatedContextCreated, + contextIdParent, + WORLD_NAME_1, + frameIdTo + ); + compareContexts(parentIsolatedContextCreated, parentDefaultContextCreated); + + checkIsolated( + frameIsolatedContextCreated, + contextIdSubFrame, + WORLD_NAME_2, + subFrame.id + ); + compareContexts(frameIsolatedContextCreated, frameDefaultContextCreated); + + // check destroying when navigating away from a frameset + const historyFrom = recordEvents(Runtime, 6); + const loadEventFrom = Page.loadEventFired(); + await Page.navigate({ url: DOC }); + await loadEventFrom; + + await assertEventOrder({ + history: historyFrom, + expectedEvents: [ + DESTROYED, // default, DOC + DESTROYED, // isolated, DOC + DESTROYED, // default, DOC_IFRAME + DESTROYED, // isolated, DOC_IFRAME + CREATED, // default, DOC + ], + }); + + const contextsDestroyed = historyFrom + .findEvents(DESTROYED) + .map(event => event.payload.executionContextId); + contextsCreated.forEach(context => { + ok( + contextsDestroyed.includes(context.id), + `Context with id ${context.id} destroyed` + ); + }); + + const { context: newContext } = historyFrom.findEvent(CREATED).payload; + is(newContext.auxData.isDefault, true, "The new context is a default one"); + ok(!!newContext.id, "The new context has an id"); + ok( + ![parentDefaultContextCreated.id, frameDefaultContextCreated.id].includes( + newContext.id + ), + "The new context has a new id" + ); +}); + +add_task(async function evaluateInIsolatedAndDefault({ client }) { + const { Runtime } = client; + + const defaultContext = await enableRuntime(client); + const isolatedContext = await createIsolatedContext(client, defaultContext); + + const { result: objDefault } = await Runtime.evaluate({ + contextId: defaultContext.id, + expression: "({ foo: 1 })", + }); + const { result: objIsolated } = await Runtime.evaluate({ + contextId: isolatedContext.id, + expression: "({ foo: 10 })", + }); + const { result: result1 } = await Runtime.callFunctionOn({ + executionContextId: isolatedContext.id, + functionDeclaration: "arg => ++arg.foo", + arguments: [{ objectId: objIsolated.objectId }], + }); + is(result1.value, 11, "Isolated context incremented the expected value"); + + let errorThrown = ""; + try { + await Runtime.callFunctionOn({ + executionContextId: isolatedContext.id, + functionDeclaration: "arg => ++arg.foo", + arguments: [{ objectId: objDefault.objectId }], + }); + } catch (e) { + errorThrown = e.message; + } + ok( + errorThrown.match(/Could not find object with given id/), + "Contexts do not share objects" + ); +}); + +add_task(async function contextEvaluationIsIsolated({ client }) { + const { Runtime } = client; + + // If a document makes changes to standard global object, an isolated + // world should not be affected + await loadURL(toDataURL("<script>window.Node = null</script>")); + + const defaultContext = await enableRuntime(client); + const isolatedContext = await createIsolatedContext(client, defaultContext); + + const { result: result1 } = await Runtime.callFunctionOn({ + executionContextId: defaultContext.id, + functionDeclaration: "arg => window.Node", + }); + const { result: result2 } = await Runtime.callFunctionOn({ + executionContextId: isolatedContext.id, + functionDeclaration: "arg => window.Node", + }); + is(result1.value, null, "Default context sees content changes to global"); + todo_isnot( + result2.value, + null, + "Isolated context is not affected by changes to global, Bug 1601421" + ); +}); + +function checkIsolated(context, expectedId, expectedName, expectedFrameId) { + is( + expectedId, + context.id, + "createIsolatedWorld returns id of isolated context" + ); + is( + context.auxData.frameId, + expectedFrameId, + "Isolated context has expected frameId" + ); + is(context.auxData.isDefault, false, "Isolated context is not default"); + is(context.auxData.type, "isolated", "Isolated context has type 'isolated'"); + is(context.name, expectedName, "Isolated context is named as requested"); + ok(!!context.origin, "Isolated context has an origin"); +} + +function compareContexts(isolatedContext, defaultContext) { + isnot( + defaultContext.name, + isolatedContext.name, + "The contexts have different names" + ); + isnot( + defaultContext.id, + isolatedContext.id, + "The contexts have different ids" + ); + is( + defaultContext.origin, + isolatedContext.origin, + "The contexts have same origin" + ); + is( + defaultContext.auxData.frameId, + isolatedContext.auxData.frameId, + "The contexts have same frameId" + ); +} + +async function createIsolatedContext( + client, + defaultContext, + worldName = WORLD_NAME_1 +) { + const { Page, Runtime } = client; + + const frameId = defaultContext.auxData.frameId; + + const isolatedContextCreated = Runtime.executionContextCreated(); + const { executionContextId: isolatedId } = await Page.createIsolatedWorld({ + frameId, + worldName, + grantUniversalAccess: true, + }); + const { context: isolatedContext } = await isolatedContextCreated; + info("Isolated world created"); + + checkIsolated(isolatedContext, isolatedId, worldName, frameId); + compareContexts(isolatedContext, defaultContext); + + return isolatedContext; +} + +function recordEvents(Runtime, total, cleared = false) { + const history = new RecordEvents(total); + + history.addRecorder({ + event: Runtime.executionContextDestroyed, + eventName: DESTROYED, + messageFn: payload => { + return `Received ${DESTROYED} for id ${payload.executionContextId}`; + }, + }); + history.addRecorder({ + event: Runtime.executionContextCreated, + eventName: CREATED, + messageFn: payload => { + return ( + `Received ${CREATED} for id ${payload.context.id}` + + ` type: ${payload.context.auxData.type}` + + ` name: ${payload.context.name}` + + ` origin: ${payload.context.origin}` + ); + }, + }); + if (cleared) { + history.addRecorder({ + event: Runtime.executionContextsCleared, + eventName: CLEARED, + }); + } + + return history; +} + +async function assertEventOrder(options = {}) { + const { history, expectedEvents, timeout = 1000 } = options; + const events = await history.record(timeout); + const eventNames = events.map(item => item.eventName); + info(`Expected events: ${expectedEvents}`); + info(`Received events: ${eventNames}`); + is( + events.length, + expectedEvents.length, + "Received expected number of Runtime context events" + ); + Assert.deepEqual( + eventNames, + expectedEvents, + "Received Runtime context events in expected order" + ); +} diff --git a/remote/test/browser/page/browser_domContentEventFired.js b/remote/test/browser/page/browser_domContentEventFired.js new file mode 100644 index 0000000000..9959f711be --- /dev/null +++ b/remote/test/browser/page/browser_domContentEventFired.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div>foo</div>"); +const DOC_IFRAME_MULTI = toDataURL(` + <iframe src='data:text/html,foo'></iframe> + <iframe src='data:text/html,bar'></iframe> +`); +const DOC_IFRAME_NESTED = toDataURL(` + <iframe src="${DOC_IFRAME_MULTI}"></iframe> +`); + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runContentEventFiredTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.disable(); + + await runContentEventFiredTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function eventWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runContentEventFiredTest(client, 1, async () => { + info("Navigate to a page with no iframes"); + await loadURL(DOC); + }); +}); + +add_task(async function eventWhenNavigatingWithFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runContentEventFiredTest(client, 1, async () => { + info("Navigate to a page with iframes"); + await loadURL(DOC_IFRAME_MULTI); + }); +}); + +add_task(async function eventWhenNavigatingWithNestedFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runContentEventFiredTest(client, 1, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +async function runContentEventFiredTest(client, expectedEventCount, callback) { + const { Page } = client; + + if (![0, 1].includes(expectedEventCount)) { + throw new Error(`Invalid value for expectedEventCount`); + } + + const DOM_CONTENT_EVENT_FIRED = "Page.domContentEventFired"; + + const history = new RecordEvents(expectedEventCount); + history.addRecorder({ + event: Page.domContentEventFired, + eventName: DOM_CONTENT_EVENT_FIRED, + messageFn: payload => { + return `Received ${DOM_CONTENT_EVENT_FIRED} at time ${payload.timestamp}`; + }, + }); + + const timeStart = Date.now() / 1000; + await callback(); + const domContentEventFiredEvents = await history.record(); + const timeEnd = Date.now() / 1000; + + is( + domContentEventFiredEvents.length, + expectedEventCount, + "Got expected amount of domContentEventFired events" + ); + if (expectedEventCount == 0) { + return; + } + + const timestamp = domContentEventFiredEvents[0].payload.timestamp; + ok( + timestamp >= timeStart && timestamp <= timeEnd, + `Timestamp in expected range [${timeStart} - ${timeEnd}]` + ); +} diff --git a/remote/test/browser/page/browser_frameAttached.js b/remote/test/browser/page/browser_frameAttached.js new file mode 100644 index 0000000000..bb181feadb --- /dev/null +++ b/remote/test/browser/page/browser_frameAttached.js @@ -0,0 +1,147 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div>foo</div>"); +const DOC_IFRAME_MULTI = toDataURL(` + <iframe src='data:text/html,foo'></iframe> + <iframe src='data:text/html,bar'></iframe> +`); +const DOC_IFRAME_NESTED = toDataURL(` + <iframe src="${DOC_IFRAME_MULTI}"></iframe> +`); + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runFrameAttachedTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.disable(); + + await runFrameAttachedTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function noEventWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + info("Navigate to a page with iframes"); + await loadURL(DOC_IFRAME_NESTED); + + await Page.enable(); + + await runFrameAttachedTest(client, 0, async () => { + info("Navigate to a page with no iframes"); + await loadURL(DOC); + }); +}); + +add_task(async function eventWhenNavigatingWithFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameAttachedTest(client, 2, async () => { + info("Navigate to a page with iframes"); + await loadURL(DOC_IFRAME_MULTI); + }); +}); + +add_task(async function eventWhenNavigatingWithNestedFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameAttachedTest(client, 3, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function eventWhenAttachingFrame({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameAttachedTest(client, 1, async () => { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + const frame = content.document.createElement("iframe"); + frame.src = "data:text/html,frame content"; + const loaded = new Promise(resolve => (frame.onload = resolve)); + content.document.body.appendChild(frame); + await loaded; + }); + }); +}); + +async function runFrameAttachedTest(client, expectedEventCount, callback) { + const { Page } = client; + + const ATTACHED = "Page.frameAttached"; + + const history = new RecordEvents(expectedEventCount); + history.addRecorder({ + event: Page.frameAttached, + eventName: ATTACHED, + messageFn: payload => { + return `Received ${ATTACHED} for frame id ${payload.frameId}`; + }, + }); + + const framesBefore = await getFlattenedFrameTree(client); + await callback(); + const framesAfter = await getFlattenedFrameTree(client); + + const frameAttachedEvents = await history.record(); + + if (expectedEventCount == 0) { + is(frameAttachedEvents.length, 0, "Got no frame attached event"); + return; + } + + // check how many frames were attached or detached + const count = Math.abs(framesBefore.size - framesAfter.size); + + is(count, expectedEventCount, "Expected amount of frames attached"); + is( + frameAttachedEvents.length, + count, + "Received the expected amount of frameAttached events" + ); + + // extract the new or removed frames + const framesAll = new Map([...framesBefore, ...framesAfter]); + const expectedFrames = new Map( + [...framesAll].filter(([key, _value]) => { + return !framesBefore.has(key) && framesAfter.has(key); + }) + ); + + frameAttachedEvents.forEach(({ payload }) => { + const { frameId, parentFrameId } = payload; + + info(`Check frame id ${frameId}`); + const expectedFrame = expectedFrames.get(frameId); + + ok(expectedFrame, `Found expected frame with id ${frameId}`); + is( + frameId, + expectedFrame.id, + "Got expected frame id for frameAttached event" + ); + is( + parentFrameId, + expectedFrame.parentId, + "Got expected parent frame id for frameAttached event" + ); + }); +} diff --git a/remote/test/browser/page/browser_frameDetached.js b/remote/test/browser/page/browser_frameDetached.js new file mode 100644 index 0000000000..659898c283 --- /dev/null +++ b/remote/test/browser/page/browser_frameDetached.js @@ -0,0 +1,180 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div>foo</div>"); +const DOC_IFRAME_MULTI = toDataURL(` + <iframe src='data:text/html,foo'></iframe> + <iframe src='data:text/html,bar'></iframe> +`); +const DOC_IFRAME_NESTED = toDataURL(` + <iframe src="${DOC_IFRAME_MULTI}"></iframe> +`); + +// Disable bfcache to force documents to be destroyed on navigation +Services.prefs.setIntPref("browser.sessionhistory.max_total_viewers", 0); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.sessionhistory.max_total_viewers"); +}); + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + + await runFrameDetachedTest(client, 0, async () => { + info("Navigate away from a page with an iframe"); + await loadURL(DOC); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + + await Page.enable(); + await Page.disable(); + + await runFrameDetachedTest(client, 0, async () => { + info("Navigate away to a page with no iframes"); + await loadURL(DOC); + }); +}); + +add_task(async function noEventWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + info("Navigate to a page with no iframes"); + await loadURL(DOC); + + await runFrameDetachedTest(client, 0, async () => { + info("Navigate away to a page with no iframes"); + await loadURL(DOC); + }); +}); + +add_task(async function eventWhenNavigatingWithFrames({ client }) { + const { Page } = client; + + info("Navigate to a page with iframes"); + await loadURL(DOC_IFRAME_MULTI); + + await Page.enable(); + + await runFrameDetachedTest(client, 2, async () => { + info("Navigate away to a page with no iframes"); + await loadURL(DOC); + }); +}); + +add_task(async function eventWhenNavigatingWithNestedFrames({ client }) { + const { Page } = client; + + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + + await Page.enable(); + + await runFrameDetachedTest(client, 3, async () => { + info("Navigate away to a page with no iframes"); + await loadURL(DOC); + }); +}); + +add_task(async function eventWhenDetachingFrame({ client }) { + const { Page } = client; + + info("Navigate to a page with iframes"); + await loadURL(DOC_IFRAME_MULTI); + + await Page.enable(); + + await runFrameDetachedTest(client, 1, async () => { + // Remove the single frame from the page + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const frame = content.document.getElementsByTagName("iframe")[0]; + frame.remove(); + }); + }); +}); + +add_task(async function eventWhenDetachingNestedFrames({ client }) { + const { Page, Runtime } = client; + + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + + await Page.enable(); + await Runtime.enable(); + + const { context } = await Runtime.executionContextCreated(); + + await runFrameDetachedTest(client, 3, async () => { + // Remove top-frame, which also removes any nested frames + await evaluate(client, context.id, async () => { + const frame = document.getElementsByTagName("iframe")[0]; + frame.remove(); + }); + }); +}); + +async function runFrameDetachedTest(client, expectedEventCount, callback) { + const { Page } = client; + + const DETACHED = "Page.frameDetached"; + + const history = new RecordEvents(expectedEventCount); + history.addRecorder({ + event: Page.frameDetached, + eventName: DETACHED, + messageFn: payload => { + return `Received ${DETACHED} for frame id ${payload.frameId}`; + }, + }); + + const framesBefore = await getFlattenedFrameTree(client); + await callback(); + const framesAfter = await getFlattenedFrameTree(client); + + const frameDetachedEvents = await history.record(); + + if (expectedEventCount == 0) { + is(frameDetachedEvents.length, 0, "Got no frame detached event"); + return; + } + + // check how many frames were attached or detached + const count = Math.abs(framesBefore.size - framesAfter.size); + + is(count, expectedEventCount, "Expected amount of frames detached"); + is( + frameDetachedEvents.length, + count, + "Received the expected amount of frameDetached events" + ); + + // extract the new or removed frames + const framesAll = new Map([...framesBefore, ...framesAfter]); + const expectedFrames = new Map( + [...framesAll].filter(([key, _value]) => { + return framesBefore.has(key) && !framesAfter.has(key); + }) + ); + + frameDetachedEvents.forEach(({ payload }) => { + const { frameId } = payload; + + info(`Check frame id ${frameId}`); + const expectedFrame = expectedFrames.get(frameId); + + is( + frameId, + expectedFrame.id, + "Got expected frame id for frameDetached event" + ); + }); +} diff --git a/remote/test/browser/page/browser_frameNavigated.js b/remote/test/browser/page/browser_frameNavigated.js new file mode 100644 index 0000000000..a74bb2449f --- /dev/null +++ b/remote/test/browser/page/browser_frameNavigated.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div>foo</div>"); +const DOC_IFRAME_MULTI = toDataURL(` + <iframe src='data:text/html,foo'></iframe> + <iframe src='data:text/html,bar'></iframe> +`); +const DOC_IFRAME_NESTED = toDataURL(` + <iframe src="${DOC_IFRAME_MULTI}"></iframe> +`); + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runFrameNavigatedTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.disable(); + + await runFrameNavigatedTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function eventWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameNavigatedTest(client, 1, async () => { + info("Navigate to a page with no iframes"); + await loadURL(DOC); + }); +}); + +add_task(async function eventsWhenNavigatingWithFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameNavigatedTest(client, 3, async () => { + info("Navigate to a page with iframes"); + await loadURL(DOC_IFRAME_MULTI); + }); +}); + +add_task(async function eventWhenNavigatingWithNestedFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameNavigatedTest(client, 4, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +async function runFrameNavigatedTest(client, expectedEventCount, callback) { + const { Page } = client; + + const NAVIGATED = "Page.frameNavigated"; + + const history = new RecordEvents(expectedEventCount); + history.addRecorder({ + event: Page.frameNavigated, + eventName: NAVIGATED, + messageFn: payload => { + return `Received ${NAVIGATED} for frame id ${payload.frame.id}`; + }, + }); + + await callback(); + + const frameNavigatedEvents = await history.record(); + + is( + frameNavigatedEvents.length, + expectedEventCount, + "Got expected amount of frameNavigated events" + ); + if (expectedEventCount == 0) { + return; + } + + const frames = await getFlattenedFrameTree(client); + + frameNavigatedEvents.forEach(({ payload }) => { + const { frame } = payload; + + const expectedFrame = frames.get(frame.id); + Assert.deepEqual(frame, expectedFrame, "Got expected frame details"); + }); +} diff --git a/remote/test/browser/page/browser_frameStartedLoading.js b/remote/test/browser/page/browser_frameStartedLoading.js new file mode 100644 index 0000000000..e8de1339b2 --- /dev/null +++ b/remote/test/browser/page/browser_frameStartedLoading.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div>foo</div>"); +const DOC_IFRAME_MULTI = toDataURL(` + <iframe src='data:text/html,foo'></iframe> + <iframe src='data:text/html,bar'></iframe> +`); +const DOC_IFRAME_NESTED = toDataURL(` + <iframe src="${DOC_IFRAME_MULTI}"></iframe> +`); + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runFrameStartedLoadingTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.disable(); + + await runFrameStartedLoadingTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function eventWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameStartedLoadingTest(client, 1, async () => { + info("Navigate to a page with no iframes"); + await loadURL(DOC); + }); +}); + +add_task(async function eventsWhenNavigatingWithFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameStartedLoadingTest(client, 3, async () => { + info("Navigate to a page with iframes"); + await loadURL(DOC_IFRAME_MULTI); + }); +}); + +add_task(async function eventsWhenNavigatingWithNestedFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameStartedLoadingTest(client, 4, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +async function runFrameStartedLoadingTest( + client, + expectedEventCount, + callback +) { + const { Page } = client; + + const STARTED_LOADING = "Page.frameStartedLoading"; + + const history = new RecordEvents(expectedEventCount); + history.addRecorder({ + event: Page.frameStartedLoading, + eventName: STARTED_LOADING, + messageFn: payload => { + return `Received ${STARTED_LOADING} for frame id ${payload.frameId}`; + }, + }); + + await callback(); + + const frameStartedLoadingEvents = await history.record(); + + is( + frameStartedLoadingEvents.length, + expectedEventCount, + "Got expected amount of frameStartedLoading events" + ); + if (expectedEventCount == 0) { + return; + } + + const frames = await getFlattenedFrameTree(client); + + frameStartedLoadingEvents.forEach(({ payload }) => { + const { frameId } = payload; + + info(`Check frame id ${frameId}`); + const expectedFrame = frames.get(frameId); + + ok(expectedFrame, `Found expected frame with id ${frameId}`); + is( + frameId, + expectedFrame.id, + "Got expected frame id for frameStartedLoading event" + ); + }); +} diff --git a/remote/test/browser/page/browser_frameStoppedLoading.js b/remote/test/browser/page/browser_frameStoppedLoading.js new file mode 100644 index 0000000000..31287eadc7 --- /dev/null +++ b/remote/test/browser/page/browser_frameStoppedLoading.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div>foo</div>"); +const DOC_IFRAME_MULTI = toDataURL(` + <iframe src='data:text/html,foo'></iframe> + <iframe src='data:text/html,bar'></iframe> +`); +const DOC_IFRAME_NESTED = toDataURL(` + <iframe src="${DOC_IFRAME_MULTI}"></iframe> +`); + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runFrameStoppedLoadingTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.disable(); + + await runFrameStoppedLoadingTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function eventWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameStoppedLoadingTest(client, 1, async () => { + info("Navigate to a page with no iframes"); + await loadURL(DOC); + }); +}); + +add_task(async function eventsWhenNavigatingWithFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameStoppedLoadingTest(client, 3, async () => { + info("Navigate to a page with iframes"); + await loadURL(DOC_IFRAME_MULTI); + }); +}); + +add_task(async function eventsWhenNavigatingWithNestedFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameStoppedLoadingTest(client, 4, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +async function runFrameStoppedLoadingTest( + client, + expectedEventCount, + callback +) { + const { Page } = client; + + const STOPPED_LOADING = "Page.frameStoppedLoading"; + + const history = new RecordEvents(expectedEventCount); + history.addRecorder({ + event: Page.frameStoppedLoading, + eventName: STOPPED_LOADING, + messageFn: payload => { + return `Received ${STOPPED_LOADING} for frame id ${payload.frameId}`; + }, + }); + + await callback(); + + const frameStoppedLoadingEvents = await history.record(); + + is( + frameStoppedLoadingEvents.length, + expectedEventCount, + "Got expected amount of frameStoppedLoading events" + ); + if (expectedEventCount == 0) { + return; + } + + const frames = await getFlattenedFrameTree(client); + + frameStoppedLoadingEvents.forEach(({ payload }) => { + const { frameId } = payload; + + info(`Check frame id ${frameId}`); + const expectedFrame = frames.get(frameId); + + ok(expectedFrame, `Found expected frame with id ${frameId}`); + is( + frameId, + expectedFrame.id, + "Got expected frame id for frameStartedLoading event" + ); + }); +} diff --git a/remote/test/browser/page/browser_getFrameTree.js b/remote/test/browser/page/browser_getFrameTree.js new file mode 100644 index 0000000000..b7055b8d57 --- /dev/null +++ b/remote/test/browser/page/browser_getFrameTree.js @@ -0,0 +1,158 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div>foo</div>"); +const DOC_IFRAME_MULTI = toDataURL(` + <iframe src='data:text/html,foo'></iframe> + <iframe src='data:text/html,bar'></iframe> +`); +const DOC_IFRAME_NESTED = toDataURL(` + <iframe src="${DOC_IFRAME_MULTI}"></iframe> +`); + +add_task(async function pageWithoutFrames({ client }) { + const { Page } = client; + + info("Navigate to a page without a frame"); + await loadURL(DOC); + + const { frameTree } = await Page.getFrameTree(); + ok(!!frameTree.frame, "Expected frame details found"); + + const expectedFrames = await getFlattenedFrameList(); + + // Check top-level frame + const expectedFrame = expectedFrames.get(frameTree.frame.id); + is(frameTree.frame.id, expectedFrame.id, "Expected frame id found"); + is(frameTree.frame.parentId, undefined, "Parent frame doesn't exist"); + is(frameTree.name, undefined, "Top frame doens't contain name property"); + is(frameTree.frame.url, expectedFrame.url, "Expected url found"); + is(frameTree.childFrames, undefined, "No sub frames found"); +}); + +add_task(async function PageWithFrames({ client }) { + const { Page } = client; + + info("Navigate to a page with frames"); + await loadURL(DOC_IFRAME_MULTI); + + const { frameTree } = await Page.getFrameTree(); + ok(!!frameTree.frame, "Expected frame details found"); + + const expectedFrames = await getFlattenedFrameList(); + + let frame = frameTree.frame; + let expectedFrame = expectedFrames.get(frame.id); + + info(`Check top frame with id: ${frame.id}`); + is(frame.id, expectedFrame.id, "Expected frame id found"); + is(frame.parentId, undefined, "Parent frame doesn't exist"); + is(frame.name, undefined, "Top frame doesn't contain name property"); + is(frame.url, expectedFrame.url, "Expected URL found"); + + is(frameTree.childFrames.length, 2, "Expected two sub frames"); + for (const childFrameTree of frameTree.childFrames) { + let frame = childFrameTree.frame; + let expectedFrame = expectedFrames.get(frame.id); + + info(`Check sub frame with id: ${frame.id}`); + is(frame.id, expectedFrame.id, "Expected frame id found"); + is(frame.parentId, expectedFrame.parentId, "Expected parent id found"); + is(frame.name, expectedFrame.name, "Frame has expected name set"); + is(frame.url, expectedFrame.url, "Expected URL found"); + is(childFrameTree.childFrames, undefined, "No sub frames found"); + } +}); + +add_task(async function pageWithNestedFrames({ client }) { + const { Page } = client; + + info("Navigate to a page with nested frames"); + await loadURL(DOC_IFRAME_NESTED); + + const { frameTree } = await Page.getFrameTree(); + ok(!!frameTree.frame, "Expected frame details found"); + + const expectedFrames = await getFlattenedFrameList(); + + let frame = frameTree.frame; + let expectedFrame = expectedFrames.get(frame.id); + + info(`Check top frame with id: ${frame.id}`); + is(frame.id, expectedFrame.id, "Expected frame id found"); + is(frame.parentId, undefined, "Parent frame doesn't exist"); + is(frame.name, undefined, "Top frame doesn't contain name property"); + is(frame.url, expectedFrame.url, "Expected URL found"); + is(frameTree.childFrames.length, 1, "Expected a single sub frame"); + + const childFrameTree = frameTree.childFrames[0]; + frame = childFrameTree.frame; + expectedFrame = expectedFrames.get(frame.id); + + info(`Check sub frame with id: ${frame.id}`); + is(frame.id, expectedFrame.id, "Expected frame id found"); + is(frame.parentId, expectedFrame.parentId, "Expected parent id found"); + is(frame.name, expectedFrame.name, "Frame has expected name set"); + is(frame.url, expectedFrame.url, "Expected URL found"); + is(childFrameTree.childFrames.length, 2, "Expected two sub frames"); + + let nestedChildFrameTree = childFrameTree.childFrames[0]; + frame = nestedChildFrameTree.frame; + expectedFrame = expectedFrames.get(frame.id); + + info(`Check first nested frame with id: ${frame.id}`); + is(frame.id, expectedFrame.id, "Expected frame id found"); + is(frame.parentId, expectedFrame.parentId, "Expected parent id found"); + is(frame.name, expectedFrame.name, "Frame has expected name set"); + is(frame.url, expectedFrame.url, "Expected URL found"); + is(nestedChildFrameTree.childFrames, undefined, "No sub frames found"); + + nestedChildFrameTree = childFrameTree.childFrames[1]; + frame = nestedChildFrameTree.frame; + expectedFrame = expectedFrames.get(frame.id); + + info(`Check second nested frame with id: ${frame.id}`); + is(frame.id, expectedFrame.id, "Expected frame id found"); + is(frame.parentId, expectedFrame.parentId, "Expected parent id found"); + is(frame.name, expectedFrame.name, "Frame has expected name set"); + is(frame.url, expectedFrame.url, "Expected URL found"); + is(nestedChildFrameTree.childFrames, undefined, "No sub frames found"); +}); + +/** + * Retrieve all frames for the current tab as flattened list. + */ +function getFlattenedFrameList() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const frames = new Map(); + + function getFrameDetails(context) { + const frameElement = context.embedderElement; + + const frame = { + id: context.id.toString(), + parentId: context.parent ? context.parent.id.toString() : null, + loaderId: null, + name: frameElement?.id || frameElement?.name, + url: context.docShell.domWindow.location.href, + securityOrigin: null, + mimeType: null, + }; + + if (context.parent) { + frame.parentId = context.parent.id.toString(); + } + + frames.set(context.id.toString(), frame); + + for (const childContext of context.children) { + getFrameDetails(childContext); + } + } + + getFrameDetails(content.docShell.browsingContext); + return frames; + }); +} diff --git a/remote/test/browser/page/browser_getLayoutMetrics.js b/remote/test/browser/page/browser_getLayoutMetrics.js new file mode 100644 index 0000000000..db8b3e8f3c --- /dev/null +++ b/remote/test/browser/page/browser_getLayoutMetrics.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function documentSmallerThanViewport({ client }) { + const { Page } = client; + await loadURL(toDataURL("<div>Hello world")); + + const { contentSize, layoutViewport } = await Page.getLayoutMetrics(); + await checkContentSize(contentSize); + await checkLayoutViewport(layoutViewport); + + is( + contentSize.x, + layoutViewport.pageX, + "X position of content is equal to layout viewport" + ); + is( + contentSize.y, + layoutViewport.pageY, + "Y position of content is equal to layout viewport" + ); + ok( + contentSize.width <= layoutViewport.clientWidth, + "Width of content is smaller than the layout viewport" + ); + ok( + contentSize.height <= layoutViewport.clientHeight, + "Height of content is smaller than the layout viewport" + ); +}); + +add_task(async function documentLargerThanViewport({ client }) { + const { Page } = client; + await loadURL(toDataURL("<div style='margin: 150vh 0 0 150vw'>Hello world")); + + const { contentSize, layoutViewport } = await Page.getLayoutMetrics(); + await checkContentSize(contentSize); + await checkLayoutViewport(layoutViewport, { scrollbars: true }); + + is( + contentSize.x, + layoutViewport.pageX, + "X position of content is equal to layout viewport" + ); + is( + contentSize.y, + layoutViewport.pageY, + "Y position of content is equal to layout viewport" + ); + ok( + contentSize.width > layoutViewport.clientWidth, + "Width of content is larger than the layout viewport" + ); + ok( + contentSize.height > layoutViewport.clientHeight, + "Height of content is larger than the layout viewport" + ); +}); + +add_task(async function documentLargerThanViewportScrolledXY({ client }) { + const { Page } = client; + await loadURL(toDataURL("<div style='margin: 150vh 0 0 150vw'>Hello world")); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.scrollTo(50, 100); + }); + + const { contentSize, layoutViewport } = await Page.getLayoutMetrics(); + await checkContentSize(contentSize); + await checkLayoutViewport(layoutViewport, { scrollbars: true }); + + is( + layoutViewport.pageX, + contentSize.x + 50, + "X position of content is equal to layout viewport" + ); + is( + layoutViewport.pageY, + contentSize.y + 100, + "Y position of content is equal to layout viewport" + ); + ok( + contentSize.width > layoutViewport.clientWidth, + "Width of content is larger than the layout viewport" + ); + ok( + contentSize.height > layoutViewport.clientHeight, + "Height of content is larger than the layout viewport" + ); +}); + +async function checkContentSize(rect) { + const expected = await getContentSize(); + + is(rect.x, expected.x, "Expected x position returned"); + is(rect.y, expected.y, "Expected y position returned"); + is(rect.width, expected.width, "Expected width returned"); + is(rect.height, expected.height, "Expected height returned"); +} + +async function checkLayoutViewport(viewport, options = {}) { + const { scrollbars = false } = options; + + const expected = await getViewportSize(); + + if (scrollbars) { + const { width, height } = await getScrollbarSize(); + expected.width -= width; + expected.height -= height; + } + + is(viewport.pageX, expected.x, "Expected x position returned"); + is(viewport.pageY, expected.y, "Expected y position returned"); + is(viewport.clientWidth, expected.width, "Expected width returned"); + is(viewport.clientHeight, expected.height, "Expected height returned"); +} diff --git a/remote/test/browser/page/browser_getNavigationHistory.js b/remote/test/browser/page/browser_getNavigationHistory.js new file mode 100644 index 0000000000..27f9e30c94 --- /dev/null +++ b/remote/test/browser/page/browser_getNavigationHistory.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SessionStore } = ChromeUtils.import( + "resource:///modules/sessionstore/SessionStore.jsm" +); + +add_task(async function singleEntry({ client }) { + const { Page } = client; + + const data = generateHistoryData(1); + for (const entry of data) { + await loadURL(entry.userTypedURL); + } + + const history = await Page.getNavigationHistory(); + assertHistoryEntries(history, data, 0); +}); + +add_task(async function multipleEntriesWithLastIndex({ client }) { + const { Page } = client; + + const data = generateHistoryData(3); + for (const entry of data) { + await loadURL(entry.userTypedURL); + } + + const history = await Page.getNavigationHistory(); + assertHistoryEntries(history, data, data.length - 1); +}); + +add_task(async function multipleEntriesWithFirstIndex({ client }) { + const { Page } = client; + + const data = generateHistoryData(3); + for (const entry of data) { + await loadURL(entry.userTypedURL); + } + + await gotoHistoryIndex(0); + + const history = await Page.getNavigationHistory(); + assertHistoryEntries(history, data, 0); +}); + +add_task(async function locationRedirect({ client }) { + const { Page } = client; + + const pageEmptyURL = + "http://example.com/browser/remote/test/browser/page/doc_empty.html"; + const sjsURL = + "http://example.com/browser/remote/test/browser/page/sjs_redirect.sjs"; + const redirectURL = `${sjsURL}?${pageEmptyURL}`; + + const data = [ + { + url: pageEmptyURL, + userTypedURL: redirectURL, + title: "Empty page", + }, + ]; + + await loadURL(redirectURL, pageEmptyURL); + + const history = await Page.getNavigationHistory(); + assertHistoryEntries(history, data, 0); +}); diff --git a/remote/test/browser/page/browser_javascriptDialog_alert.js b/remote/test/browser/page/browser_javascriptDialog_alert.js new file mode 100644 index 0000000000..51cb764e7c --- /dev/null +++ b/remote/test/browser/page/browser_javascriptDialog_alert.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test a browser alert is detected via Page.javascriptDialogOpening and can be +// closed with Page.handleJavaScriptDialog +add_task(async function({ client }) { + const { Page } = client; + + info("Enable the page domain"); + await Page.enable(); + + info("Set window.alertIsClosed to false in the content page"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + // This boolean will be flipped after closing the dialog + content.alertIsClosed = false; + }); + + info("Create an alert dialog again"); + const { message, type } = await createAlertDialog(Page); + is(type, "alert", "dialog event contains the correct type"); + is(message, "test-1234", "dialog event contains the correct text"); + + info("Close the dialog with accept:false"); + await Page.handleJavaScriptDialog({ accept: false }); + + info("Retrieve the alertIsClosed boolean on the content window"); + let alertIsClosed = await getContentProperty("alertIsClosed"); + ok(alertIsClosed, "The content process is no longer blocked on the alert"); + + info("Reset window.alertIsClosed to false in the content page"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.alertIsClosed = false; + }); + + info("Create an alert dialog again"); + await createAlertDialog(Page); + + info("Close the dialog with accept:true"); + await Page.handleJavaScriptDialog({ accept: true }); + + alertIsClosed = await getContentProperty("alertIsClosed"); + ok(alertIsClosed, "The content process is no longer blocked on the alert"); +}); + +function createAlertDialog(Page) { + const onDialogOpen = Page.javascriptDialogOpening(); + + info("Trigger an alert in the test page"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.alert("test-1234"); + // Flip a boolean in the content page to check if the content process resumed + // after the alert was opened. + content.alertIsClosed = true; + }); + + return onDialogOpen; +} diff --git a/remote/test/browser/page/browser_javascriptDialog_beforeunload.js b/remote/test/browser/page/browser_javascriptDialog_beforeunload.js new file mode 100644 index 0000000000..18bfcba201 --- /dev/null +++ b/remote/test/browser/page/browser_javascriptDialog_beforeunload.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test beforeunload dialog events. +add_task(async function({ client, tab }) { + info("Allow to trigger onbeforeunload without user interaction"); + await new Promise(resolve => { + const options = { + set: [["dom.require_user_interaction_for_beforeunload", false]], + }; + SpecialPowers.pushPrefEnv(options, resolve); + }); + + const { Page } = client; + + info("Enable the page domain"); + await Page.enable(); + + info("Attach a valid onbeforeunload handler"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.onbeforeunload = () => true; + }); + + info("Trigger the beforeunload again but reject the prompt"); + const { type } = await triggerBeforeUnload(Page, tab, false); + is(type, "beforeunload", "dialog event contains the correct type"); + + info("Trigger the beforeunload again and accept the prompt"); + const onTabClose = BrowserTestUtils.waitForEvent(tab, "TabClose"); + await triggerBeforeUnload(Page, tab, true); + + info("Wait for the TabClose event"); + await onTabClose; +}); + +function triggerBeforeUnload(Page, tab, accept) { + // We use then here because after clicking on the close button, nothing + // in the main block of the function will be executed until the prompt + // is accepted or rejected. Attaching a then to this promise still works. + + const onDialogOpen = Page.javascriptDialogOpening().then( + async dialogEvent => { + await Page.handleJavaScriptDialog({ accept }); + return dialogEvent; + } + ); + + info("Click on the tab close icon"); + tab.closeButton.click(); + + return onDialogOpen; +} diff --git a/remote/test/browser/page/browser_javascriptDialog_confirm.js b/remote/test/browser/page/browser_javascriptDialog_confirm.js new file mode 100644 index 0000000000..da126ecc71 --- /dev/null +++ b/remote/test/browser/page/browser_javascriptDialog_confirm.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for window.confirm(). Check that the dialog is correctly detected and that it can +// be rejected or accepted. +add_task(async function({ client }) { + const { Page } = client; + + info("Enable the page domain"); + await Page.enable(); + + info("Create a confirm dialog to open"); + const { message, type } = await createConfirmDialog(Page); + + is(type, "confirm", "dialog event contains the correct type"); + is(message, "confirm-1234?", "dialog event contains the correct text"); + + info("Accept the dialog"); + await Page.handleJavaScriptDialog({ accept: true }); + let isConfirmed = await getContentProperty("isConfirmed"); + ok(isConfirmed, "The confirm dialog was accepted"); + + await createConfirmDialog(Page); + info("Trigger another confirm in the test page"); + + info("Reject the dialog"); + await Page.handleJavaScriptDialog({ accept: false }); + isConfirmed = await getContentProperty("isConfirmed"); + ok(!isConfirmed, "The confirm dialog was rejected"); +}); + +function createConfirmDialog(Page) { + const onDialogOpen = Page.javascriptDialogOpening(); + + info("Trigger a confirm in the test page"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.isConfirmed = content.confirm("confirm-1234?"); + }); + + return onDialogOpen; +} diff --git a/remote/test/browser/page/browser_javascriptDialog_otherTarget.js b/remote/test/browser/page/browser_javascriptDialog_otherTarget.js new file mode 100644 index 0000000000..60b49719b1 --- /dev/null +++ b/remote/test/browser/page/browser_javascriptDialog_otherTarget.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that javascript dialog events are emitted by the page domain only if +// the dialog is created for the window of the target. +add_task(async function({ client }) { + const { Page } = client; + + info("Enable the page domain"); + await Page.enable(); + + // Add a listener for dialogs on the test page. + Page.javascriptDialogOpening(() => { + ok(false, "Should never receive this event"); + }); + + info("Open another tab"); + const otherTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + toDataURL("test-page") + ); + is(gBrowser.selectedTab, otherTab, "Selected tab is now the new tab"); + + // Create a promise that resolve when dialog prompt is created. + // It will also take care of closing the dialog. + const onOtherPageDialog = new Promise(r => { + Services.obs.addObserver(function onDialogLoaded(promptContainer) { + Services.obs.removeObserver(onDialogLoaded, "tabmodal-dialog-loaded"); + promptContainer.querySelector(".tabmodalprompt-button0").click(); + r(); + }, "tabmodal-dialog-loaded"); + }); + + info("Trigger an alert in the second page"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.alert("test"); + }); + + info("Wait for the alert to be detected and closed"); + await onOtherPageDialog; + + info("Call bringToFront on the test page to make sure we received"); + await Page.bringToFront(); + + BrowserTestUtils.removeTab(otherTab); +}); diff --git a/remote/test/browser/page/browser_javascriptDialog_prompt.js b/remote/test/browser/page/browser_javascriptDialog_prompt.js new file mode 100644 index 0000000000..5cf8cff58a --- /dev/null +++ b/remote/test/browser/page/browser_javascriptDialog_prompt.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for window.prompt(). Check that the dialog is correctly detected and that it can +// be rejected or accepted, with a custom prompt text. +add_task(async function({ client }) { + const { Page } = client; + + info("Enable the page domain"); + await Page.enable(); + + info("Create a prompt dialog to open"); + const { message, type } = await createPromptDialog(Page); + + is(type, "prompt", "dialog event contains the correct type"); + is(message, "prompt-1234", "dialog event contains the correct text"); + + info("Accept the prompt"); + await Page.handleJavaScriptDialog({ accept: true, promptText: "some-text" }); + + let promptResult = await getContentProperty("promptResult"); + is(promptResult, "some-text", "The prompt text was correctly applied"); + + await createPromptDialog(Page); + info("Trigger another prompt in the test page"); + + info("Reject the prompt"); + await Page.handleJavaScriptDialog({ accept: false, promptText: "new-text" }); + + promptResult = await getContentProperty("promptResult"); + ok(!promptResult, "The prompt dialog was rejected"); +}); + +function createPromptDialog(Page) { + const onDialogOpen = Page.javascriptDialogOpening(); + + info("Trigger a prompt in the test page"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.promptResult = content.prompt("prompt-1234"); + }); + + return onDialogOpen; +} diff --git a/remote/test/browser/page/browser_lifecycleEvent.js b/remote/test/browser/page/browser_lifecycleEvent.js new file mode 100644 index 0000000000..42e80583b3 --- /dev/null +++ b/remote/test/browser/page/browser_lifecycleEvent.js @@ -0,0 +1,206 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div>foo</div>"); +const DOC_IFRAME_MULTI = toDataURL(` + <iframe src='data:text/html,foo'></iframe> + <iframe src='data:text/html,bar'></iframe> +`); +const DOC_IFRAME_NESTED = toDataURL(` + <iframe src="${DOC_IFRAME_MULTI}"></iframe> +`); + +const PAGE_URL = + "http://example.com/browser/remote/test/browser/page/doc_empty.html"; + +add_task(async function noEventsWhenPageDomainDisabled({ client }) { + await runPageLifecycleTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.disable(); + + await runPageLifecycleTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function noEventWhenLifeCycleDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + + await runPageLifecycleTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function noEventAfterLifeCycleDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.setLifecycleEventsEnabled({ enabled: true }); + await Page.setLifecycleEventsEnabled({ enabled: false }); + + await runPageLifecycleTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function eventsWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.setLifecycleEventsEnabled({ enabled: true }); + + await runPageLifecycleTest(client, 1, async () => { + info("Navigate to a page with no iframes"); + await loadURL(DOC); + }); +}); + +add_task(async function eventsWhenNavigatingToURLWithNoFrames({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.setLifecycleEventsEnabled({ enabled: true }); + + await runPageLifecycleTest(client, 1, async () => { + info("Navigate to a URL with no frames"); + await loadURL(PAGE_URL); + }); +}); + +add_task(async function eventsWhenNavigatingToSameURLWithNoFrames({ client }) { + const { Page } = client; + + info("Navigate to a page with no iframes"); + await loadURL(PAGE_URL); + + await Page.enable(); + await Page.setLifecycleEventsEnabled({ enabled: true }); + + await runPageLifecycleTest(client, 1, async () => { + info("Navigate to the same page with no iframes"); + await loadURL(PAGE_URL); + }); +}); + +add_task(async function eventsWhenReloadingSameURLWithNoFrames({ client }) { + const { Page } = client; + + info("Navigate to a page with no iframes"); + await loadURL(PAGE_URL); + + await Page.enable(); + await Page.setLifecycleEventsEnabled({ enabled: true }); + + await runPageLifecycleTest(client, 1, async () => { + info("Reload page with no iframes"); + const pageLoaded = Page.loadEventFired(); + await Page.reload(); + await pageLoaded; + }); +}); + +add_task(async function eventsWhenNavigatingWithFrames({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.setLifecycleEventsEnabled({ enabled: true }); + + await runPageLifecycleTest(client, 3, async () => { + info("Navigate to a page with iframes"); + await loadURL(DOC_IFRAME_MULTI); + }); +}); + +add_task(async function eventsWhenNavigatingWithNestedFrames({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.setLifecycleEventsEnabled({ enabled: true }); + + await runPageLifecycleTest(client, 4, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +async function runPageLifecycleTest(client, expectedEventSets, callback) { + const { Page } = client; + + const LIFECYCLE = "Page.lifecycleEvent"; + const LIFECYCLE_EVENTS = ["init", "DOMContentLoaded", "load"]; + + const expectedEventCount = expectedEventSets * LIFECYCLE_EVENTS.length; + const history = new RecordEvents(expectedEventCount); + history.addRecorder({ + event: Page.lifecycleEvent, + eventName: LIFECYCLE, + messageFn: payload => { + return ( + `Received "${payload.name}" ${LIFECYCLE} ` + + `for frame id ${payload.frameId}` + ); + }, + }); + + await callback(); + + const flattenedFrameTree = await getFlattenedFrameTree(client); + + const lifecycleEvents = await history.record(); + is( + lifecycleEvents.length, + expectedEventCount, + "Got expected amount of lifecycle events" + ); + + if (expectedEventCount == 0) { + return; + } + + // Check lifecycle events for each frame + for (const frame of flattenedFrameTree.values()) { + info(`Check frame id ${frame.id}`); + + const frameEvents = lifecycleEvents.filter(({ payload }) => { + return payload.frameId == frame.id; + }); + + Assert.deepEqual( + frameEvents.map(event => event.payload.name), + LIFECYCLE_EVENTS, + "Received various lifecycle events in the expected order" + ); + + // Check data as exposed by each of these events + let lastTimestamp = lifecycleEvents[0].payload.timestamp; + lifecycleEvents.forEach(({ payload }, index) => { + ok( + payload.timestamp >= lastTimestamp, + "timestamp succeeds the one from the former event" + ); + lastTimestamp = payload.timestamp; + + // Bug 1632007 return a loaderId for data: url + if (frame.url.startsWith("data:")) { + todo(!!payload.loaderId, "Event has a loaderId"); + } else { + is(payload.loaderId, frame.loaderId, `event has expected loaderId`); + } + }); + } +} diff --git a/remote/test/browser/page/browser_loadEventFired.js b/remote/test/browser/page/browser_loadEventFired.js new file mode 100644 index 0000000000..48ba83eefe --- /dev/null +++ b/remote/test/browser/page/browser_loadEventFired.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div>foo</div>"); +const DOC_IFRAME_MULTI = toDataURL(` + <iframe src='data:text/html,foo'></iframe> + <iframe src='data:text/html,bar'></iframe> +`); +const DOC_IFRAME_NESTED = toDataURL(` + <iframe src="${DOC_IFRAME_MULTI}"></iframe> +`); + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runLoadEventFiredTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.disable(); + + await runLoadEventFiredTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function eventWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runLoadEventFiredTest(client, 1, async () => { + info("Navigate to a page with no iframes"); + await loadURL(DOC); + }); +}); + +add_task(async function eventWhenNavigatingWithFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runLoadEventFiredTest(client, 1, async () => { + info("Navigate to a page with iframes"); + await loadURL(DOC_IFRAME_MULTI); + }); +}); + +add_task(async function eventWhenNavigatingWithNestedFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runLoadEventFiredTest(client, 1, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +async function runLoadEventFiredTest(client, expectedEventCount, callback) { + const { Page } = client; + + if (![0, 1].includes(expectedEventCount)) { + throw new Error(`Invalid value for expectedEventCount`); + } + + const LOAD_EVENT_FIRED = "Page.loadEventFired"; + + const history = new RecordEvents(expectedEventCount); + history.addRecorder({ + event: Page.loadEventFired, + eventName: LOAD_EVENT_FIRED, + messageFn: payload => { + return `Received ${LOAD_EVENT_FIRED} at time ${payload.timestamp}`; + }, + }); + + const timeStart = Date.now() / 1000; + await callback(); + const loadEventFiredEvents = await history.record(); + const timeEnd = Date.now() / 1000; + + is( + loadEventFiredEvents.length, + expectedEventCount, + "Got expected amount of loadEventFired events" + ); + if (expectedEventCount == 0) { + return; + } + + const timestamp = loadEventFiredEvents[0].payload.timestamp; + ok( + timestamp >= timeStart && timestamp <= timeEnd, + `Timestamp in expected range [${timeStart} - ${timeEnd}]` + ); +} diff --git a/remote/test/browser/page/browser_navigate.js b/remote/test/browser/page/browser_navigate.js new file mode 100644 index 0000000000..d377f353d5 --- /dev/null +++ b/remote/test/browser/page/browser_navigate.js @@ -0,0 +1,264 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const pageEmptyURL = + "http://example.com/browser/remote/test/browser/page/doc_empty.html"; + +add_task(async function testBasicNavigation({ client }) { + const { Page, Network } = client; + await Page.enable(); + await Network.enable(); + const loadEventFired = Page.loadEventFired(); + const requestEvent = Network.requestWillBeSent(); + const { frameId, loaderId, errorText } = await Page.navigate({ + url: pageEmptyURL, + }); + const { loaderId: requestLoaderId } = await requestEvent; + + ok(!!loaderId, "Page.navigate returns loaderId"); + is( + loaderId, + requestLoaderId, + "Page.navigate returns same loaderId as corresponding request" + ); + is(errorText, undefined, "No errorText on a successful navigation"); + + await loadEventFired; + const currentFrame = await getTopFrame(client); + is(frameId, currentFrame.id, "Page.navigate returns expected frameId"); + + is( + gBrowser.selectedBrowser.currentURI.spec, + pageEmptyURL, + "Expected URL loaded" + ); +}); + +add_task(async function testTwoNavigations({ client }) { + const { Page, Network } = client; + await Page.enable(); + await Network.enable(); + let requestEvent = Network.requestWillBeSent(); + let loadEventFired = Page.loadEventFired(); + const { frameId, loaderId, errorText } = await Page.navigate({ + url: pageEmptyURL, + }); + const { loaderId: requestLoaderId } = await requestEvent; + await loadEventFired; + is( + gBrowser.selectedBrowser.currentURI.spec, + pageEmptyURL, + "Expected URL loaded" + ); + + loadEventFired = Page.loadEventFired(); + requestEvent = Network.requestWillBeSent(); + const { + frameId: frameId2, + loaderId: loaderId2, + errorText: errorText2, + } = await Page.navigate({ + url: pageEmptyURL, + }); + const { loaderId: requestLoaderId2 } = await requestEvent; + ok(!!loaderId, "Page.navigate returns loaderId"); + ok(!!loaderId2, "Page.navigate returns loaderId"); + isnot(loaderId, loaderId2, "Page.navigate returns different loaderIds"); + is( + loaderId, + requestLoaderId, + "Page.navigate returns same loaderId as corresponding request" + ); + is( + loaderId2, + requestLoaderId2, + "Page.navigate returns same loaderId as corresponding request" + ); + is(errorText, undefined, "No errorText on a successful navigation"); + is(errorText2, undefined, "No errorText on a successful navigation"); + is(frameId, frameId2, "Page.navigate return same frameId"); + + await loadEventFired; + is( + gBrowser.selectedBrowser.currentURI.spec, + pageEmptyURL, + "Expected URL loaded" + ); +}); + +add_task(async function testRedirect({ client }) { + const { Page, Network } = client; + const sjsURL = + "http://example.com/browser/remote/test/browser/page/sjs_redirect.sjs"; + const redirectURL = `${sjsURL}?${pageEmptyURL}`; + await Page.enable(); + await Network.enable(); + const requestEvent = Network.requestWillBeSent(); + const loadEventFired = Page.loadEventFired(); + + const { frameId, loaderId, errorText } = await Page.navigate({ + url: redirectURL, + }); + const { loaderId: requestLoaderId } = await requestEvent; + ok(!!loaderId, "Page.navigate returns loaderId"); + is( + loaderId, + requestLoaderId, + "Page.navigate returns same loaderId as original request" + ); + is(errorText, undefined, "No errorText on a successful navigation"); + ok(!!frameId, "Page.navigate returns frameId"); + + await loadEventFired; + is( + gBrowser.selectedBrowser.currentURI.spec, + pageEmptyURL, + "Expected URL loaded" + ); +}); + +add_task(async function testUnknownHost({ client }) { + const { Page } = client; + const { frameId, loaderId, errorText } = await Page.navigate({ + url: "http://example-does-not-exist.com", + }); + ok(!!frameId, "Page.navigate returns frameId"); + ok(!!loaderId, "Page.navigate returns loaderId"); + is(errorText, "NS_ERROR_UNKNOWN_HOST", "Failed navigation returns errorText"); +}); + +add_task(async function testExpiredCertificate({ client }) { + const { Page } = client; + const { frameId, loaderId, errorText } = await Page.navigate({ + url: "https://expired.example.com", + }); + ok(!!frameId, "Page.navigate returns frameId"); + ok(!!loaderId, "Page.navigate returns loaderId"); + is( + errorText, + "SEC_ERROR_EXPIRED_CERTIFICATE", + "Failed navigation returns errorText" + ); +}); + +add_task(async function testUnknownCertificate({ client }) { + const { Page, Network } = client; + await Network.enable(); + const requestEvent = Network.requestWillBeSent(); + const { frameId, loaderId, errorText } = await Page.navigate({ + url: "https://self-signed.example.com", + }); + const { loaderId: requestLoaderId } = await requestEvent; + ok(!!frameId, "Page.navigate returns frameId"); + ok(!!loaderId, "Page.navigate returns loaderId"); + is( + loaderId, + requestLoaderId, + "Page.navigate returns same loaderId as original request" + ); + is(errorText, "SSL_ERROR_UNKNOWN", "Failed navigation returns errorText"); +}); + +add_task(async function testNotFound({ client }) { + const { Page } = client; + const { frameId, loaderId, errorText } = await Page.navigate({ + url: "http://example.com/browser/remote/doesnotexist.html", + }); + ok(!!frameId, "Page.navigate returns frameId"); + ok(!!loaderId, "Page.navigate returns loaderId"); + is(errorText, undefined, "No errorText on a 404"); +}); + +add_task(async function testInvalidURL({ client }) { + const { Page } = client; + let message = ""; + for (let url of ["blah.com", "foo", "https\n//", "http", ""]) { + message = ""; + try { + await Page.navigate({ url }); + } catch (e) { + message = e.response.message; + } + ok(message.includes("invalid URL"), `Invalid url ${url} causes error`); + } + + for (let url of [2, {}, true]) { + message = ""; + try { + await Page.navigate({ url }); + } catch (e) { + message = e.response.message; + } + ok( + message.includes("string value expected"), + `Invalid url ${url} causes error` + ); + } +}); + +add_task(async function testDataURL({ client }) { + const { Page } = client; + const url = toDataURL("first"); + await Page.enable(); + const loadEventFired = Page.loadEventFired(); + const { frameId, loaderId, errorText } = await Page.navigate({ url }); + is(errorText, undefined, "No errorText on a successful navigation"); + todo(!!loaderId, "Page.navigate returns loaderId"); + + await loadEventFired; + const currentFrame = await getTopFrame(client); + is(frameId, currentFrame.id, "Page.navigate returns expected frameId"); + is(gBrowser.selectedBrowser.currentURI.spec, url, "Expected URL loaded"); +}); + +add_task(async function testFileURL({ client }) { + const { Page } = client; + const dir = getChromeDir(getResolvedURI(gTestPath)); + dir.append("doc_empty.html"); + + // The file can be a symbolic link on local build. Normalize it to make sure + // the path matches to the actual URI opened in the new tab. + dir.normalize(); + const url = Services.io.newFileURI(dir).spec; + const browser = gBrowser.selectedTab.linkedBrowser; + const loaded = BrowserTestUtils.browserLoaded(browser, false, url); + + const { /* frameId, */ loaderId, errorText } = await Page.navigate({ url }); + is(errorText, undefined, "No errorText on a successful navigation"); + todo(!!loaderId, "Page.navigate returns loaderId"); + + // Bug 1634693 Page.loadEventFired isn't emitted after file: navigation + await loaded; + is(browser.currentURI.spec, url, "Expected URL loaded"); + // Bug 1634695 Navigating to file: returns wrong frame id and hangs + // content page domain methods + // const currentFrame = await getTopFrame(client); + // ok(frameId === currentFrame.id, "Page.navigate returns expected frameId"); +}); + +add_task(async function testAbout({ client }) { + const { Page } = client; + await Page.enable(); + let loadEventFired = Page.loadEventFired(); + const { frameId, loaderId, errorText } = await Page.navigate({ + url: "about:blank", + }); + todo(!!loaderId, "Page.navigate returns loaderId"); + is(errorText, undefined, "No errorText on a successful navigation"); + + await loadEventFired; + const currentFrame = await getTopFrame(client); + is(frameId, currentFrame.id, "Page.navigate returns expected frameId"); + is( + gBrowser.selectedBrowser.currentURI.spec, + "about:blank", + "Expected URL loaded" + ); +}); + +async function getTopFrame(client) { + const frames = await getFlattenedFrameTree(client); + return Array.from(frames.values())[0]; +} diff --git a/remote/test/browser/page/browser_navigateToHistoryEntry.js b/remote/test/browser/page/browser_navigateToHistoryEntry.js new file mode 100644 index 0000000000..4a6e1b4fb6 --- /dev/null +++ b/remote/test/browser/page/browser_navigateToHistoryEntry.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function toUnknownEntryId({ client }) { + const { Page } = client; + + const { entries } = await Page.getNavigationHistory(); + const ids = entries.map(entry => entry.id); + + let errorThrown = ""; + try { + await Page.navigateToHistoryEntry({ entryId: Math.max(...ids) + 1 }); + } catch (e) { + errorThrown = e.message; + } + ok( + errorThrown.match(/No entry with passed id/), + "Unknown entry id raised error" + ); +}); + +add_task(async function toSameEntry({ client }) { + const { Page } = client; + + const data = generateHistoryData(1); + for (const entry of data) { + await loadURL(entry.userTypedURL); + } + + const { currentIndex, entries } = await Page.getNavigationHistory(); + await Page.navigateToHistoryEntry({ entryId: entries[currentIndex].id }); + + const history = await Page.getNavigationHistory(); + assertHistoryEntries(history, data, 0); + + is( + gBrowser.selectedBrowser.currentURI.spec, + data[0].url, + "Expected URL loaded" + ); +}); + +add_task(async function oneEntryBackInHistory({ client }) { + const { Page } = client; + + const data = generateHistoryData(3); + for (const entry of data) { + await loadURL(entry.userTypedURL); + } + + const { currentIndex, entries } = await Page.getNavigationHistory(); + await Page.navigateToHistoryEntry({ entryId: entries[currentIndex - 1].id }); + + const history = await Page.getNavigationHistory(); + assertHistoryEntries(history, data, currentIndex - 1); + + is( + gBrowser.selectedBrowser.currentURI.spec, + data[currentIndex - 1].url, + "Expected URL loaded" + ); +}); + +add_task(async function oneEntryForwardInHistory({ client }) { + const { Page } = client; + + const data = generateHistoryData(3); + for (const entry of data) { + await loadURL(entry.userTypedURL); + } + + await gotoHistoryIndex(0); + + const { currentIndex, entries } = await Page.getNavigationHistory(); + await Page.navigateToHistoryEntry({ entryId: entries[currentIndex + 1].id }); + + const history = await Page.getNavigationHistory(); + assertHistoryEntries(history, data, currentIndex + 1); + + is( + gBrowser.selectedBrowser.currentURI.spec, + data[currentIndex + 1].url, + "Expected URL loaded" + ); +}); + +add_task(async function toFirstEntryInHistory({ client }) { + const { Page } = client; + + const data = generateHistoryData(3); + for (const entry of data) { + await loadURL(entry.userTypedURL); + } + + const { entries } = await Page.getNavigationHistory(); + await Page.navigateToHistoryEntry({ entryId: entries[0].id }); + + const history = await Page.getNavigationHistory(); + assertHistoryEntries(history, data, 0); + + is( + gBrowser.selectedBrowser.currentURI.spec, + data[0].url, + "Expected URL loaded" + ); +}); + +add_task(async function toLastEntryInHistory({ client }) { + const { Page } = client; + + const data = generateHistoryData(3); + for (const entry of data) { + await loadURL(entry.userTypedURL); + } + + await gotoHistoryIndex(0); + + const { entries } = await Page.getNavigationHistory(); + await Page.navigateToHistoryEntry({ + entryId: entries[entries.length - 1].id, + }); + + const history = await Page.getNavigationHistory(); + assertHistoryEntries(history, data, data.length - 1); + + is( + gBrowser.selectedBrowser.currentURI.spec, + data[data.length - 1].url, + "Expected URL loaded" + ); +}); diff --git a/remote/test/browser/page/browser_navigationEvents.js b/remote/test/browser/page/browser_navigationEvents.js new file mode 100644 index 0000000000..ab79d209f2 --- /dev/null +++ b/remote/test/browser/page/browser_navigationEvents.js @@ -0,0 +1,189 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the Page navigation events + +const INITIAL_DOC = toDataURL("default-test-page"); +const IFRAME_DOC = toDataURL( + `<iframe src="data:text/html,somecontent"></iframe>` +); +const RANDOM_ID_DOC = toDataURL( + `<script>window.randomId = Math.random() + "-" + Date.now();</script>` +); + +const promises = new Set(); +const resolutions = new Map(); + +add_task(async function pageWithoutFrame({ client }) { + await loadURL(INITIAL_DOC); + + const { Page } = client; + + // turn on navigation related events, such as DOMContentLoaded et al. + await Page.enable(); + info("Page domain has been enabled"); + + const { frameTree } = await Page.getFrameTree(); + + // Save the given `promise` resolution into the `promises` global Set + function recordPromise(name, promise) { + promise.then(event => { + info(`Received Page.${name}`); + resolutions.set(name, event); + }); + promises.add(promise); + } + // Record all Page events that we assert in this test + function recordPromises() { + recordPromise("frameStartedLoading", Page.frameStartedLoading()); + recordPromise("frameNavigated", Page.frameNavigated()); + recordPromise("domContentEventFired", Page.domContentEventFired()); + recordPromise("loadEventFired", Page.loadEventFired()); + recordPromise("navigatedWithinDocument", Page.navigatedWithinDocument()); + recordPromise("frameStoppedLoading", Page.frameStoppedLoading()); + } + + info("Test Page.navigate"); + recordPromises(); + + const url = RANDOM_ID_DOC; + const { frameId } = await Page.navigate({ url }); + info("A new page has been requested"); + + ok(frameId, "Page.navigate returned a frameId"); + is( + frameId, + frameTree.frame.id, + "The Page.navigate's frameId is the same than getFrameTree's one" + ); + + await assertNavigationEvents({ url, frameId }); + + const randomId1 = await getTestTabRandomId(); + ok(!!randomId1, "Test tab has a valid randomId"); + + info("Test Page.reload"); + recordPromises(); + + await Page.reload(); + info("The page has been reloaded"); + + await assertNavigationEvents({ url, frameId }); + + const randomId2 = await getTestTabRandomId(); + ok(!!randomId2, "Test tab has a valid randomId"); + isnot( + randomId2, + randomId1, + "Test tab randomId has been updated after reload" + ); + + info("Test Page.navigate with the same URL still reloads the current page"); + recordPromises(); + + await Page.navigate({ url }); + info("The page has been reloaded"); + + await assertNavigationEvents({ url, frameId }); + + const randomId3 = await getTestTabRandomId(); + ok(!!randomId3, "Test tab has a valid randomId"); + isnot( + randomId3, + randomId2, + "Test tab randomId has been updated after reload" + ); +}); + +add_task(async function pageWithSingleFrame({ client }) { + const { Page } = client; + + await Page.enable(); + + // Store all frameNavigated events in an array + const frameNavigatedEvents = []; + Page.frameNavigated(e => frameNavigatedEvents.push(e)); + + info("Navigate to a page containing an iframe"); + const onStoppedLoading = Page.frameStoppedLoading(); + const { frameId } = await Page.navigate({ url: IFRAME_DOC }); + await onStoppedLoading; + + is(frameNavigatedEvents.length, 2, "Received 2 frameNavigated events"); + is( + frameNavigatedEvents[0].frame.id, + frameId, + "Received the correct frameId for the frameNavigated event" + ); +}); + +async function assertNavigationEvents({ url, frameId }) { + // Wait for all the promises to resolve + await Promise.all(promises); + + // Assert the order in which they resolved + const expectedResolutions = [ + "frameStartedLoading", + "frameNavigated", + "domContentEventFired", + "loadEventFired", + "navigatedWithinDocument", + "frameStoppedLoading", + ]; + Assert.deepEqual( + [...resolutions.keys()], + expectedResolutions, + "Received various Page navigation events in the expected order" + ); + + // Now assert the data exposed by each of these events + const frameStartedLoading = resolutions.get("frameStartedLoading"); + is( + frameStartedLoading.frameId, + frameId, + "frameStartedLoading frameId is the same one" + ); + + const frameNavigated = resolutions.get("frameNavigated"); + ok( + !frameNavigated.frame.parentId, + "frameNavigated is for the top level document and has a null parentId" + ); + is(frameNavigated.frame.id, frameId, "frameNavigated id is the right one"); + is( + frameNavigated.frame.name, + undefined, + "frameNavigated name isn't implemented yet" + ); + is(frameNavigated.frame.url, url, "frameNavigated url is the right one"); + + const navigatedWithinDocument = resolutions.get("navigatedWithinDocument"); + is( + navigatedWithinDocument.frameId, + frameId, + "navigatedWithinDocument frameId is the same one" + ); + is( + navigatedWithinDocument.url, + url, + "navigatedWithinDocument url is the same one" + ); + + const frameStoppedLoading = resolutions.get("frameStoppedLoading"); + is( + frameStoppedLoading.frameId, + frameId, + "frameStoppedLoading frameId is the same one" + ); + + promises.clear(); + resolutions.clear(); +} + +async function getTestTabRandomId() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + return content.wrappedJSObject.randomId; + }); +} diff --git a/remote/test/browser/page/browser_printToPDF.js b/remote/test/browser/page/browser_printToPDF.js new file mode 100644 index 0000000000..fed1a6162e --- /dev/null +++ b/remote/test/browser/page/browser_printToPDF.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div style='background-color: green'>Hello world</div>"); + +add_task(async function transferModes({ client }) { + const { IO, Page } = client; + await loadURL(DOC); + + // as base64 encoded data + const base64 = await Page.printToPDF({ transferMode: "ReturnAsBase64" }); + is(base64.stream, null, "No stream handle is returned"); + ok(!!base64.data, "Base64 encoded data is returned"); + verifyPDF(atob(base64.data).trimEnd()); + + // defaults to base64 encoded data + const defaults = await Page.printToPDF(); + is(defaults.stream, null, "By default no stream handle is returned"); + ok(!!defaults.data, "By default base64 encoded data is returned"); + verifyPDF(atob(defaults.data).trimEnd()); + + // unknown transfer modes default to base64 + const fallback = await Page.printToPDF({ transferMode: "ReturnAsFoo" }); + is(fallback.stream, null, "Unknown mode doesn't return a stream"); + ok(!!fallback.data, "Unknown mode defaults to base64 encoded data"); + verifyPDF(atob(fallback.data).trimEnd()); + + // as stream handle + const stream = await Page.printToPDF({ transferMode: "ReturnAsStream" }); + ok(!!stream.stream, "Stream handle is returned"); + is(stream.data, null, "No base64 encoded data is returned"); + let streamData = ""; + + while (true) { + const { data, base64Encoded, eof } = await IO.read({ + handle: stream.stream, + }); + streamData += base64Encoded ? atob(data) : data; + if (eof) { + await IO.close({ handle: stream.stream }); + break; + } + } + + verifyPDF(streamData.trimEnd()); +}); + +function verifyPDF(data) { + is(data.slice(0, 5), "%PDF-", "Decoded data starts with the PDF signature"); + is(data.slice(-5), "%%EOF", "Decoded data ends with the EOF flag"); +} diff --git a/remote/test/browser/page/browser_reload.js b/remote/test/browser/page/browser_reload.js new file mode 100644 index 0000000000..0872337551 --- /dev/null +++ b/remote/test/browser/page/browser_reload.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function testReload({ client }) { + const { Page } = client; + await loadURL(toDataURL("halløj")); + + info("Reloading document"); + await Page.enable(); + const loaded = Page.loadEventFired(); + await Page.reload(); + await loaded; + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + ok(!content.docShell.isForceReloading, "Document is not force-reloaded"); + }); +}); + +add_task(async function testReloadIgnoreCache({ client }) { + const { Page } = client; + await loadURL(toDataURL("halløj")); + + info("Force-reloading document"); + await Page.enable(); + const loaded = Page.loadEventFired(); + await Page.reload({ ignoreCache: true }); + await loaded; + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + ok(content.docShell.isForceReloading, "Document is force-reloaded"); + }); +}); diff --git a/remote/test/browser/page/browser_runtimeEvents.js b/remote/test/browser/page/browser_runtimeEvents.js new file mode 100644 index 0000000000..8869ddad59 --- /dev/null +++ b/remote/test/browser/page/browser_runtimeEvents.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* global getCDP */ + +// Assert the order of Runtime.executionContextDestroyed, +// Page.frameNavigated, and Runtime.executionContextCreated + +const TEST_DOC = toDataURL("default-test-page"); + +add_task(async function testCDP({ client }) { + await loadURL(TEST_DOC); + + const { Page, Runtime } = client; + + const events = []; + function assertReceivedEvents(expected, message) { + Assert.deepEqual(events, expected, message); + // Empty the list of received events + events.splice(0); + } + Page.frameNavigated(() => { + events.push("frameNavigated"); + }); + Runtime.executionContextCreated(() => { + events.push("executionContextCreated"); + }); + Runtime.executionContextDestroyed(() => { + events.push("executionContextDestroyed"); + }); + + // turn on navigation related events, such as DOMContentLoaded et al. + await Page.enable(); + info("Page domain has been enabled"); + + const onExecutionContextCreated = Runtime.executionContextCreated(); + await Runtime.enable(); + info("Runtime domain has been enabled"); + + // Runtime.enable will dispatch `executionContextCreated` for the existing document + let { context } = await onExecutionContextCreated; + ok(!!context.id, `The execution context has an id ${context.id}`); + ok(context.auxData.isDefault, "The execution context is the default one"); + ok(!!context.auxData.frameId, "The execution context has a frame id set"); + + assertReceivedEvents( + ["executionContextCreated"], + "Received only executionContextCreated event after Runtime.enable call" + ); + + const { frameTree } = await Page.getFrameTree(); + is( + frameTree.frame.id, + context.auxData.frameId, + "getFrameTree and executionContextCreated refers about the same frame Id" + ); + + const onFrameNavigated = Page.frameNavigated(); + const onExecutionContextDestroyed = Runtime.executionContextDestroyed(); + const onExecutionContextCreated2 = Runtime.executionContextCreated(); + const url = toDataURL("test-page"); + const { frameId } = await Page.navigate({ url }); + info("A new page has been requested"); + ok(frameId, "Page.navigate returned a frameId"); + is( + frameId, + frameTree.frame.id, + "The Page.navigate's frameId is the same than getFrameTree's one" + ); + + const frameNavigated = await onFrameNavigated; + ok( + !frameNavigated.frame.parentId, + "frameNavigated is for the top level document and has a null parentId" + ); + is( + frameNavigated.frame.id, + frameId, + "frameNavigated id is the same than the one returned by Page.navigate" + ); + is( + frameNavigated.frame.name, + undefined, + "frameNavigated name isn't implemented yet" + ); + is( + frameNavigated.frame.url, + url, + "frameNavigated url is the same being given to Page.navigate" + ); + + const { executionContextId } = await onExecutionContextDestroyed; + ok(executionContextId, "The destroyed event reports an id"); + is( + executionContextId, + context.id, + "The destroyed event is for the first reported execution context" + ); + + ({ context } = await onExecutionContextCreated2); + ok(!!context.id, "The execution context has an id"); + ok(context.auxData.isDefault, "The execution context is the default one"); + is( + context.auxData.frameId, + frameId, + "The execution context frame id is the same " + + "the one returned by Page.navigate" + ); + + isnot( + executionContextId, + context.id, + "The destroyed id is different from the created one" + ); + + assertReceivedEvents( + ["executionContextDestroyed", "frameNavigated", "executionContextCreated"], + "Received frameNavigated between the two execution context events during navigation to another URL" + ); +}); diff --git a/remote/test/browser/page/browser_scriptToEvaluateOnNewDocument.js b/remote/test/browser/page/browser_scriptToEvaluateOnNewDocument.js new file mode 100644 index 0000000000..39feb77ffc --- /dev/null +++ b/remote/test/browser/page/browser_scriptToEvaluateOnNewDocument.js @@ -0,0 +1,172 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test Page.addScriptToEvaluateOnNewDocument and Page.removeScriptToEvaluateOnNewDocument +// +// TODO Bug 1601695 - Schedule script evaluation and check for correct frame id + +const DOC = toDataURL("default-test-page"); +const WORLD = "testWorld"; + +add_task(async function uniqueIdForAddedScripts({ client }) { + const { Page, Runtime } = client; + + await loadURL(DOC); + + const { identifier: id1 } = await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 1;", + }); + is(typeof id1, "string", "Script id should be a string"); + ok(id1.length > 0, "Script id is non-empty"); + + const { identifier: id2 } = await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 1;", + }); + ok(id2.length > 0, "Script id is non-empty"); + isnot(id1, id2, "Two scripts should have different ids"); + + await Runtime.enable(); + + // flush event for DOC default context + await Runtime.executionContextCreated(); + await checkIsolatedContextAfterLoad(client, toDataURL("<p>Hello"), []); +}); + +add_task(async function addScriptAfterNavigation({ client }) { + const { Page } = client; + + await loadURL(DOC); + + const { identifier: id1 } = await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 1;", + }); + is(typeof id1, "string", "Script id should be a string"); + ok(id1.length > 0, "Script id is non-empty"); + + await loadURL(toDataURL("<p>Hello")); + + const { identifier: id2 } = await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 2;", + }); + ok(id2.length > 0, "Script id is non-empty"); + isnot(id1, id2, "Two scripts should have different ids"); +}); + +add_task(async function addWithIsolatedWorldAndNavigate({ client }) { + const { Page, Runtime } = client; + + await Page.enable(); + await Runtime.enable(); + + const contextsCreated = recordContextCreated(Runtime, 3); + + const loadEventFired = Page.loadEventFired(); + const { frameId } = await Page.navigate({ url: DOC }); + await loadEventFired; + + // flush context-created events for the steps above + await contextsCreated; + + await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 1;", + worldName: WORLD, + }); + + const isolatedId = await Page.createIsolatedWorld({ + frameId, + worldName: WORLD, + grantUniversalAccess: true, + }); + + const contexts = await checkIsolatedContextAfterLoad( + client, + toDataURL("<p>Next") + ); + + isnot(contexts[1].id, isolatedId, "The context has a new id"); +}); + +add_task(async function addWithIsolatedWorldNavigateTwice({ client }) { + const { Page, Runtime } = client; + + await Runtime.enable(); + + await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 1;", + worldName: WORLD, + }); + + await checkIsolatedContextAfterLoad(client, DOC); + await checkIsolatedContextAfterLoad(client, toDataURL("<p>Hello")); +}); + +add_task(async function addTwoScriptsWithIsolatedWorld({ client }) { + const { Page, Runtime } = client; + + await Runtime.enable(); + + const names = [WORLD, "A_whole_new_world"]; + await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 1;", + worldName: names[0], + }); + await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 8;", + worldName: names[1], + }); + + await checkIsolatedContextAfterLoad(client, DOC, names); +}); + +function recordContextCreated(Runtime, expectedCount) { + return new Promise(resolve => { + const ctx = []; + const unsubscribe = Runtime.executionContextCreated(payload => { + ctx.push(payload.context); + info( + `Runtime.executionContextCreated: ${payload.context.auxData.type}` + + `\n\turl ${payload.context.origin}` + ); + if (ctx.length > expectedCount) { + unsubscribe(); + resolve(ctx); + } + }); + timeoutPromise(1000).then(() => { + unsubscribe(); + resolve(ctx); + }); + }); +} + +async function checkIsolatedContextAfterLoad(client, url, names = [WORLD]) { + const { Page, Runtime } = client; + + await Page.enable(); + + // At least the default context will get created + const expected = names.length + 1; + + const contextsCreated = recordContextCreated(Runtime, expected); + const frameNavigated = Page.frameNavigated(); + const { frameId } = await Page.navigate({ url }); + await frameNavigated; + const contexts = await contextsCreated; + + is(contexts.length, expected, "Expected number of contexts got created"); + is(contexts[0].auxData.frameId, frameId, "Expected frame id found"); + is(contexts[0].auxData.isDefault, true, "Got default context"); + is(contexts[0].auxData.type, "default", "Got default context"); + is(contexts[0].name, "", "Get context with empty name"); + + names.forEach((name, index) => { + is(contexts[index + 1].name, name, "Get context with expected name"); + is(contexts[index + 1].auxData.frameId, frameId, "Expected frame id found"); + is(contexts[index + 1].auxData.isDefault, false, "Got isolated context"); + is(contexts[index + 1].auxData.type, "isolated", "Got isolated context"); + }); + + return contexts; +} diff --git a/remote/test/browser/page/doc_empty.html b/remote/test/browser/page/doc_empty.html new file mode 100644 index 0000000000..f8a2a9d02b --- /dev/null +++ b/remote/test/browser/page/doc_empty.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> +<head> + <title>Empty page</title> +</head> +<body> +</body> +</html> diff --git a/remote/test/browser/page/head.js b/remote/test/browser/page/head.js new file mode 100644 index 0000000000..311a8148f1 --- /dev/null +++ b/remote/test/browser/page/head.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../head.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/test/browser/head.js", + this +); + +const { + clearInterval, + clearTimeout, + setInterval, + setTimeout, +} = ChromeUtils.import("resource://gre/modules/Timer.jsm"); + +const { PollPromise } = ChromeUtils.import("chrome://remote/content/Sync.jsm"); + +const TIMEOUT_SET_HISTORY_INDEX = 1000; + +function assertHistoryEntries(history, expectedData, expectedIndex) { + const { currentIndex, entries } = history; + + is(currentIndex, expectedIndex, "Got expected current index"); + is( + entries.length, + expectedData.length, + "Found expected count of history entries" + ); + + entries.forEach((entry, index) => { + ok(!!entry.id, "History entry has an id set"); + is( + entry.url, + expectedData[index].url, + "History entry has the correct URL set" + ); + is( + entry.userTypedURL, + expectedData[index].userTypedURL, + "History entry has the correct user typed URL set" + ); + is( + entry.title, + expectedData[index].title, + "History entry has the correct title set" + ); + }); +} + +function generateHistoryData(count) { + const data = []; + + for (let index = 0; index < count; index++) { + const url = toDataURL(`<head><title>Test ${index + 1}</title></head>`); + data.push({ + url, + userTypedURL: url, + title: `Test ${index + 1}`, + }); + } + + return data; +} + +async function getContentSize() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const docEl = content.document.documentElement; + + return { + x: 0, + y: 0, + width: docEl.scrollWidth, + height: docEl.scrollHeight, + }; + }); +} + +async function getViewportSize() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + return { + x: content.pageXOffset, + y: content.pageYOffset, + width: content.innerWidth, + height: content.innerHeight, + }; + }); +} + +function getCurrentHistoryIndex() { + return new Promise(resolve => { + SessionStore.getSessionHistory(window.gBrowser.selectedTab, history => { + resolve(history.index); + }); + }); +} + +async function gotoHistoryIndex(index) { + gBrowser.gotoIndex(index); + + // On some platforms the requested index isn't set immediately. + await PollPromise( + async (resolve, reject) => { + const currentIndex = await getCurrentHistoryIndex(); + if (currentIndex == index) { + resolve(); + } else { + reject(); + } + }, + { timeout: TIMEOUT_SET_HISTORY_INDEX } + ); +} diff --git a/remote/test/browser/page/sjs_redirect.sjs b/remote/test/browser/page/sjs_redirect.sjs new file mode 100644 index 0000000000..b3dbf44f53 --- /dev/null +++ b/remote/test/browser/page/sjs_redirect.sjs @@ -0,0 +1,7 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 301, "Moved Permanently"); + response.setHeader("Location", request.queryString, false); +} diff --git a/remote/test/browser/runtime/browser.ini b/remote/test/browser/runtime/browser.ini new file mode 100644 index 0000000000..4e6f884ec4 --- /dev/null +++ b/remote/test/browser/runtime/browser.ini @@ -0,0 +1,20 @@ +[DEFAULT] +tags = remote +subsuite = remote +prefs = + remote.enabled=true +support-files = + !/remote/test/browser/chrome-remote-interface.js + !/remote/test/browser/head.js + doc_console_events.html + head.js + +[browser_callFunctionOn.js] +[browser_callFunctionOn_returnByValue.js] +[browser_consoleAPICalled.js] +[browser_evaluate.js] +[browser_exceptionThrown.js] +[browser_executionContextEvents.js] +skip-if = os == "mac" || os == "win" # bug 1586503,1590930 +[browser_getProperties.js] +[browser_remoteObjects.js] diff --git a/remote/test/browser/runtime/browser_callFunctionOn.js b/remote/test/browser/runtime/browser_callFunctionOn.js new file mode 100644 index 0000000000..7977631961 --- /dev/null +++ b/remote/test/browser/runtime/browser_callFunctionOn.js @@ -0,0 +1,460 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_DOC = toDataURL("default-test-page"); + +add_task(async function FunctionDeclarationMissing({ client }) { + const { Runtime } = client; + + let errorThrown = ""; + try { + await Runtime.callFunctionOn(); + } catch (e) { + errorThrown = e.message; + } + ok(errorThrown.includes("functionDeclaration: string value expected")); +}); + +add_task(async function functionDeclarationInvalidTypes({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + for (const functionDeclaration of [null, true, 1, [], {}]) { + let errorThrown = ""; + try { + await Runtime.callFunctionOn({ functionDeclaration, executionContextId }); + } catch (e) { + errorThrown = e.message; + } + ok(errorThrown.includes("functionDeclaration: string value expected")); + } +}); + +add_task(async function functionDeclarationGetCurrentLocation({ client }) { + const { Runtime } = client; + + await loadURL(TEST_DOC); + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => location.href", + executionContextId, + }); + is(result.value, TEST_DOC, "Works against the test page"); +}); + +add_task(async function argumentsInvalidTypes({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + for (const args of [null, true, 1, "foo", {}]) { + let errorThrown = ""; + try { + await Runtime.callFunctionOn({ + functionDeclaration: "", + arguments: args, + executionContextId, + }); + } catch (e) { + errorThrown = e.message; + } + ok(errorThrown.includes("arguments: array value expected")); + } +}); + +add_task(async function argumentsPrimitiveTypes({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + for (const args of [null, true, 1, "foo", {}]) { + let errorThrown = ""; + try { + await Runtime.callFunctionOn({ + functionDeclaration: "", + arguments: args, + executionContextId, + }); + } catch (e) { + errorThrown = e.message; + } + ok(errorThrown.includes("arguments: array value expected")); + } +}); + +add_task(async function awaitPromiseInvalidTypes({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + for (const awaitPromise of [null, 1, "foo", [], {}]) { + let errorThrown = ""; + try { + await Runtime.callFunctionOn({ + functionDeclaration: "", + awaitPromise, + executionContextId, + }); + } catch (e) { + errorThrown = e.message; + } + ok(errorThrown.includes("awaitPromise: boolean value expected")); + } +}); + +add_task(async function awaitPromiseResolve({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => Promise.resolve(42)", + awaitPromise: true, + executionContextId, + }); + + is(result.type, "number", "The type is correct"); + is(result.subtype, undefined, "The subtype is undefined for numbers"); + is(result.value, 42, "The result is the promise's resolution"); +}); + +add_task(async function awaitPromiseDelayedResolve({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => new Promise(r => setTimeout(() => r(42), 0))", + awaitPromise: true, + executionContextId, + }); + is(result.type, "number", "The type is correct"); + is(result.subtype, undefined, "The subtype is undefined for numbers"); + is(result.value, 42, "The result is the promise's resolution"); +}); + +add_task(async function awaitPromiseReject({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { exceptionDetails } = await Runtime.callFunctionOn({ + functionDeclaration: "() => Promise.reject(42)", + awaitPromise: true, + executionContextId, + }); + // TODO: Implement all values for exceptionDetails (bug 1548480) + is( + exceptionDetails.exception.value, + 42, + "The result is the promise's rejection" + ); +}); + +add_task(async function awaitPromiseDelayedReject({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { exceptionDetails } = await Runtime.callFunctionOn({ + functionDeclaration: + "() => new Promise((_,r) => setTimeout(() => r(42), 0))", + awaitPromise: true, + executionContextId, + }); + is( + exceptionDetails.exception.value, + 42, + "The result is the promise's rejection" + ); +}); + +add_task(async function awaitPromiseResolveWithoutWait({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => Promise.resolve(42)", + awaitPromise: false, + executionContextId, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, "promise", "The subtype is promise"); + ok(!!result.objectId, "We got the object id for the promise"); + ok(!result.value, "We do not receive any value"); +}); + +add_task(async function awaitPromiseDelayedResolveWithoutWait({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => new Promise(r => setTimeout(() => r(42), 0))", + awaitPromise: false, + executionContextId, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, "promise", "The subtype is promise"); + ok(!!result.objectId, "We got the object id for the promise"); + ok(!result.value, "We do not receive any value"); +}); + +add_task(async function awaitPromiseRejectWithoutWait({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => Promise.reject(42)", + awaitPromise: false, + executionContextId, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, "promise", "The subtype is promise"); + ok(!!result.objectId, "We got the object id for the promise"); + ok(!result.exceptionDetails, "We do not receive any exception"); +}); + +add_task(async function awaitPromiseDelayedRejectWithoutWait({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: + "() => new Promise((_,r) => setTimeout(() => r(42), 0))", + awaitPromise: false, + executionContextId, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, "promise", "The subtype is promise"); + ok(!!result.objectId, "We got the object id for the promise"); + ok(!result.exceptionDetails, "We do not receive any exception"); +}); + +add_task(async function executionContextIdNorObjectIdSpecified({ client }) { + const { Runtime } = client; + + let errorThrown = ""; + try { + await Runtime.callFunctionOn({ + functionDeclaration: "", + }); + } catch (e) { + errorThrown = e.message; + } + ok( + errorThrown.includes( + "Either objectId or executionContextId must be specified" + ) + ); +}); + +add_task(async function executionContextIdInvalidTypes({ client }) { + const { Runtime } = client; + + for (const executionContextId of [null, true, "foo", [], {}]) { + let errorThrown = ""; + try { + await Runtime.callFunctionOn({ + functionDeclaration: "", + executionContextId, + }); + } catch (e) { + errorThrown = e.message; + } + ok(errorThrown.includes("executionContextId: number value expected")); + } +}); + +add_task(async function executionContextIdInvalidValue({ client }) { + const { Runtime } = client; + + let errorThrown = ""; + try { + await Runtime.callFunctionOn({ + functionDeclaration: "", + executionContextId: -1, + }); + } catch (e) { + errorThrown = e.message; + } + ok(errorThrown.includes("Cannot find context with specified id")); +}); + +add_task(async function objectIdInvalidTypes({ client }) { + const { Runtime } = client; + + for (const objectId of [null, true, 1, [], {}]) { + let errorThrown = ""; + try { + await Runtime.callFunctionOn({ functionDeclaration: "", objectId }); + } catch (e) { + errorThrown = e.message; + } + ok(errorThrown.includes("objectId: string value expected")); + } +}); + +add_task(async function objectId({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + // First create an object + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => ({ foo: 42 })", + executionContextId, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, undefined, "The subtype is undefined for objects"); + ok(!!result.objectId, "Got an object id"); + + // Then apply a method on this object + const { result: result2 } = await Runtime.callFunctionOn({ + functionDeclaration: "function () { return this.foo; }", + executionContextId, + objectId: result.objectId, + }); + + is(result2.type, "number", "The type is correct"); + is(result2.subtype, undefined, "The subtype is undefined for numbers"); + is(result2.value, 42, "Expected value returned"); +}); + +add_task(async function objectIdArgumentReference({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + // First create a remote JS object + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => ({ foo: 1 })", + executionContextId, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, undefined, "The subtype is undefined for objects"); + ok(!!result.objectId, "Got an object id"); + + // Then increment the `foo` attribute of this JS object, + // while returning this attribute value + const { result: result2 } = await Runtime.callFunctionOn({ + functionDeclaration: "arg => ++arg.foo", + arguments: [{ objectId: result.objectId }], + executionContextId, + }); + + Assert.deepEqual( + result2, + { + type: "number", + value: 2, + }, + "The result has the expected type and value" + ); + + // Finally, try to pass this JS object and get it back. Ensure that it + // returns the same object id. Also increment the attribute again. + const { result: result3 } = await Runtime.callFunctionOn({ + functionDeclaration: "arg => { arg.foo++; return arg; }", + arguments: [{ objectId: result.objectId }], + executionContextId, + }); + + is(result3.type, "object", "The type is correct"); + is(result3.subtype, undefined, "The subtype is undefined for objects"); + // Remote objects don't have unique ids. So you may have multiple object ids + // that reference the same remote object + ok(!!result3.objectId, "Got an object id"); + isnot(result3.objectId, result.objectId, "The object id is different"); + + // Assert that we can still access this object and that its foo attribute + // has been incremented. Use the second object id we got from previous call + // to callFunctionOn. + const { result: result4 } = await Runtime.callFunctionOn({ + functionDeclaration: "arg => arg.foo", + arguments: [{ objectId: result3.objectId }], + executionContextId, + }); + + Assert.deepEqual( + result4, + { + type: "number", + value: 3, + }, + "The result has the expected type and value" + ); +}); + +add_task(async function exceptionDetailsJavascriptError({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { exceptionDetails } = await Runtime.callFunctionOn({ + functionDeclaration: "doesNotExists()", + executionContextId, + }); + + Assert.deepEqual( + exceptionDetails, + { + text: "doesNotExists is not defined", + }, + "Javascript error is passed to the client" + ); +}); + +add_task(async function exceptionDetailsThrowError({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { exceptionDetails } = await Runtime.callFunctionOn({ + functionDeclaration: "() => { throw new Error('foo') }", + executionContextId, + }); + + Assert.deepEqual( + exceptionDetails, + { + text: "foo", + }, + "Exception details are passed to the client" + ); +}); + +add_task(async function exceptionDetailsThrowValue({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { exceptionDetails } = await Runtime.callFunctionOn({ + functionDeclaration: "() => { throw 'foo' }", + executionContextId, + }); + + Assert.deepEqual( + exceptionDetails, + { + exception: { + type: "string", + value: "foo", + }, + }, + "Exception details are passed as a RemoteObject" + ); +}); diff --git a/remote/test/browser/runtime/browser_callFunctionOn_returnByValue.js b/remote/test/browser/runtime/browser_callFunctionOn_returnByValue.js new file mode 100644 index 0000000000..3647599116 --- /dev/null +++ b/remote/test/browser/runtime/browser_callFunctionOn_returnByValue.js @@ -0,0 +1,369 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function returnAsObjectTypes({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const expressions = [ + { expression: "({foo:true})", type: "object", subtype: undefined }, + { expression: "Symbol('foo')", type: "symbol", subtype: undefined }, + { expression: "new Promise(()=>{})", type: "object", subtype: "promise" }, + { expression: "new Int8Array(8)", type: "object", subtype: "typedarray" }, + { expression: "new WeakMap()", type: "object", subtype: "weakmap" }, + { expression: "new WeakSet()", type: "object", subtype: "weakset" }, + { expression: "new Map()", type: "object", subtype: "map" }, + { expression: "new Set()", type: "object", subtype: "set" }, + { expression: "/foo/", type: "object", subtype: "regexp" }, + { expression: "[1, 2]", type: "object", subtype: "array" }, + { expression: "new Proxy({}, {})", type: "object", subtype: "proxy" }, + { expression: "new Date()", type: "object", subtype: "date" }, + { + expression: "document", + type: "object", + subtype: "node", + className: "HTMLDocument", + description: "#document", + }, + { + expression: `{{ + const div = document.createElement('div'); + div.id = "foo"; + return div; + }}`, + type: "object", + subtype: "node", + className: "HTMLDivElement", + description: "div#foo", + }, + ]; + + for (const entry of expressions) { + const { expression, type, subtype, className, description } = entry; + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: `() => ${expression}`, + executionContextId, + }); + + is(result.type, type, "The type is correct"); + is(result.subtype, subtype, "The subtype is correct"); + ok(!!result.objectId, "Got an object id"); + if (className) { + is(result.className, className, "The className is correct"); + } + if (description) { + is(result.description, description, "The description is correct"); + } + } +}); + +add_task(async function returnAsObjectDifferentObjectIds({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const expressions = [{}, "document"]; + for (const expression of expressions) { + const { result: result1 } = await Runtime.callFunctionOn({ + functionDeclaration: `() => ${JSON.stringify(expression)}`, + executionContextId, + }); + const { result: result2 } = await Runtime.callFunctionOn({ + functionDeclaration: `() => ${JSON.stringify(expression)}`, + executionContextId, + }); + is( + result1.objectId, + result2.objectId, + `Different object ids returned for ${expression}` + ); + } +}); + +add_task(async function returnAsObjectPrimitiveTypes({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const expressions = [42, "42", true, 4.2]; + for (const expression of expressions) { + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: `() => ${JSON.stringify(expression)}`, + executionContextId, + }); + is(result.value, expression, `Evaluating primitive '${expression}' works`); + is(result.type, typeof expression, `${expression} type is correct`); + } +}); + +add_task(async function returnAsObjectNotSerializable({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const notSerializableNumbers = { + number: ["-0", "NaN", "Infinity", "-Infinity"], + bigint: ["42n"], + }; + + for (const type in notSerializableNumbers) { + for (const expression of notSerializableNumbers[type]) { + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: `() => ${expression}`, + executionContextId, + }); + Assert.deepEqual( + result, + { + type, + unserializableValue: expression, + description: expression, + }, + `Evaluating unserializable '${expression}' works` + ); + } + } +}); + +// `null` is special as it has its own subtype, is of type 'object' +// but is returned as a value, without an `objectId` attribute +add_task(async function returnAsObjectNull({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => null", + executionContextId, + }); + Assert.deepEqual( + result, + { + type: "object", + subtype: "null", + value: null, + }, + "Null type is correct" + ); +}); + +// undefined doesn't work with JSON.stringify, so test it independently +add_task(async function returnAsObjectUndefined({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => undefined", + executionContextId, + }); + Assert.deepEqual( + result, + { + type: "undefined", + }, + "Undefined type is correct" + ); +}); + +add_task(async function returnByValueInvalidTypes({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + for (const returnByValue of [null, 1, "foo", [], {}]) { + let errorThrown = ""; + try { + await Runtime.callFunctionOn({ + functionDeclaration: "", + executionContextId, + returnByValue, + }); + } catch (e) { + errorThrown = e.message; + } + ok(errorThrown.includes("returnByValue: boolean value expected")); + } +}); + +add_task(async function returnByValue({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const values = [ + null, + 42, + 42.0, + "42", + true, + false, + { foo: true }, + { foo: { bar: 42, str: "str", array: [1, 2, 3] } }, + [42, "42", true], + [{ foo: true }], + ]; + + for (const value of values) { + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: `() => (${JSON.stringify(value)})`, + executionContextId, + returnByValue: true, + }); + + Assert.deepEqual( + result, + { + type: typeof value, + value, + description: value != null ? value.toString() : value, + }, + "The returned value is the same than the input value" + ); + } +}); + +add_task(async function returnByValueNotSerializable({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const notSerializableNumbers = { + number: ["-0", "NaN", "Infinity", "-Infinity"], + bigint: ["42n"], + }; + + for (const type in notSerializableNumbers) { + for (const unserializableValue of notSerializableNumbers[type]) { + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: `() => (${unserializableValue})`, + executionContextId, + returnByValue: true, + }); + + Assert.deepEqual( + result, + { + type, + unserializableValue, + description: unserializableValue, + }, + "The returned value is the same than the input value" + ); + } + } +}); + +// Test undefined individually as JSON.stringify doesn't return a string +add_task(async function returnByValueUndefined({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "() => {}", + executionContextId, + returnByValue: true, + }); + + Assert.deepEqual( + result, + { + type: "undefined", + }, + "Undefined type is correct" + ); +}); + +add_task(async function returnByValueArguments({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const values = [ + 42, + 42.0, + "42", + true, + false, + null, + { foo: true }, + { foo: { bar: 42, str: "str", array: [1, 2, 3] } }, + [42, "42", true], + [{ foo: true }], + ]; + + for (const value of values) { + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "a => a", + arguments: [{ value }], + executionContextId, + returnByValue: true, + }); + + Assert.deepEqual( + result, + { + type: typeof value, + value, + description: value != null ? value.toString() : value, + }, + "The returned value is the same than the input value" + ); + } +}); + +add_task(async function returnByValueArgumentsNotSerializable({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + const notSerializableNumbers = { + number: ["-0", "NaN", "Infinity", "-Infinity"], + bigint: ["42n"], + }; + + for (const type in notSerializableNumbers) { + for (const unserializableValue of notSerializableNumbers[type]) { + const { result } = await Runtime.callFunctionOn({ + functionDeclaration: "a => a", + arguments: [{ unserializableValue }], + executionContextId, + returnByValue: true, + }); + + Assert.deepEqual( + result, + { + type, + unserializableValue, + description: unserializableValue, + }, + "The returned value is the same than the input value" + ); + } + } +}); + +add_task(async function returnByValueArgumentsSymbol({ client }) { + const { Runtime } = client; + + const { id: executionContextId } = await enableRuntime(client); + + let errorThrown = ""; + try { + await Runtime.callFunctionOn({ + functionDeclaration: "a => a", + arguments: [{ unserializableValue: "Symbol('42')" }], + executionContextId, + returnByValue: true, + }); + } catch (e) { + errorThrown = e.message; + } + ok(errorThrown, "Symbol cannot be returned as value"); +}); diff --git a/remote/test/browser/runtime/browser_consoleAPICalled.js b/remote/test/browser/runtime/browser_consoleAPICalled.js new file mode 100644 index 0000000000..f203399ed0 --- /dev/null +++ b/remote/test/browser/runtime/browser_consoleAPICalled.js @@ -0,0 +1,186 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PAGE_CONSOLE_EVENTS = + "http://example.com/browser/remote/test/browser/runtime/doc_console_events.html"; + +add_task(async function noEventsWhenRuntimeDomainDisabled({ client }) { + await runConsoleTest(client, 0, async () => { + ContentTask.spawn(gBrowser.selectedBrowser, {}, () => console.log("foo")); + }); +}); + +add_task(async function noEventsAfterRuntimeDomainDisabled({ client }) { + const { Runtime } = client; + + await Runtime.enable(); + await Runtime.disable(); + + await runConsoleTest(client, 0, async () => { + ContentTask.spawn(gBrowser.selectedBrowser, {}, () => console.log("foo")); + }); +}); + +add_task(async function noEventsForJavascriptErrors({ client }) { + await loadURL(PAGE_CONSOLE_EVENTS); + const context = await enableRuntime(client); + + await runConsoleTest(client, 0, async () => { + evaluate(client, context.id, () => { + document.getElementById("js-error").click(); + }); + }); +}); + +add_task(async function consoleAPI({ client }) { + const context = await enableRuntime(client); + + const events = await runConsoleTest(client, 1, async () => { + await evaluate(client, context.id, () => { + console.log("foo"); + }); + }); + + is(events[0].type, "log", "Got expected type"); + is(events[0].args[0].value, "foo", "Got expected argument value"); + is( + events[0].executionContextId, + context.id, + "Got event from current execution context" + ); +}); + +add_task(async function consoleAPITypes({ client }) { + const expectedEvents = ["dir", "error", "log", "timeEnd", "trace", "warning"]; + const levels = ["dir", "error", "log", "time", "timeEnd", "trace", "warn"]; + + const context = await enableRuntime(client); + + const events = await runConsoleTest( + client, + expectedEvents.length, + async () => { + for (const level of levels) { + await evaluate( + client, + context.id, + level => { + console[level]("foo"); + }, + level + ); + } + } + ); + + events.forEach((event, index) => { + console.log(`Check for event type "${expectedEvents[index]}"`); + is(event.type, expectedEvents[index], "Got expected type"); + }); +}); + +add_task(async function consoleAPIArgumentsCount({ client }) { + const argumentList = [[], ["foo"], ["foo", "bar", "cheese"]]; + + const context = await enableRuntime(client); + + const events = await runConsoleTest(client, argumentList.length, async () => { + for (const args of argumentList) { + await evaluate( + client, + context.id, + args => { + console.log(...args); + }, + args + ); + } + }); + + events.forEach((event, index) => { + console.log(`Check for event args "${argumentList[index]}"`); + + const argValues = event.args.map(arg => arg.value); + Assert.deepEqual(argValues, argumentList[index], "Got expected args"); + }); +}); + +add_task(async function consoleAPIArgumentsAsRemoteObject({ client }) { + const context = await enableRuntime(client); + + const events = await runConsoleTest(client, 1, async () => { + await evaluate(client, context.id, () => { + console.log("foo", Symbol("foo")); + }); + }); + + Assert.equal(events[0].args.length, 2, "Got expected amount of arguments"); + + is(events[0].args[0].type, "string", "Got expected argument type"); + is(events[0].args[0].value, "foo", "Got expected argument value"); + + is(events[0].args[1].type, "symbol", "Got expected argument type"); + is( + events[0].args[1].description, + "Symbol(foo)", + "Got expected argument description" + ); + ok(!!events[0].args[1].objectId, "Got objectId for argument"); +}); + +add_task(async function consoleAPIByContentInteraction({ client }) { + await loadURL(PAGE_CONSOLE_EVENTS); + const context = await enableRuntime(client); + + const events = await runConsoleTest(client, 1, async () => { + evaluate(client, context.id, () => { + document.getElementById("console-error").click(); + }); + }); + + is(events[0].type, "error", "Got expected type"); + is(events[0].args[0].value, "foo", "Got expected argument value"); + is( + events[0].executionContextId, + context.id, + "Got event from current execution context" + ); +}); + +async function runConsoleTest(client, eventCount, callback, options = {}) { + const { Runtime } = client; + + const EVENT_CONSOLE_API_CALLED = "Runtime.consoleAPICalled"; + + const history = new RecordEvents(eventCount); + history.addRecorder({ + event: Runtime.consoleAPICalled, + eventName: EVENT_CONSOLE_API_CALLED, + messageFn: payload => + `Received ${EVENT_CONSOLE_API_CALLED} for ${payload.type}`, + }); + + const timeBefore = Date.now(); + await callback(); + + const consoleAPIentries = await history.record(); + is(consoleAPIentries.length, eventCount, "Got expected amount of events"); + + if (eventCount == 0) { + return []; + } + + const timeAfter = Date.now(); + + // Check basic details for consoleAPICalled events + consoleAPIentries.forEach(({ payload }) => { + ok( + payload.timestamp >= timeBefore && payload.timestamp <= timeAfter, + "Got valid timestamp" + ); + }); + + return consoleAPIentries.map(event => event.payload); +} diff --git a/remote/test/browser/runtime/browser_evaluate.js b/remote/test/browser/runtime/browser_evaluate.js new file mode 100644 index 0000000000..61ab7b9dbe --- /dev/null +++ b/remote/test/browser/runtime/browser_evaluate.js @@ -0,0 +1,506 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_DOC = toDataURL("default-test-page"); + +add_task(async function awaitPromiseInvalidTypes({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + for (const awaitPromise of [null, 1, "foo", [], {}]) { + let errorThrown = ""; + try { + await Runtime.evaluate({ + expression: "", + awaitPromise, + }); + } catch (e) { + errorThrown = e.message; + } + ok(errorThrown.includes("awaitPromise: boolean value expected")); + } +}); + +add_task(async function awaitPromiseResolve({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "Promise.resolve(42)", + awaitPromise: true, + }); + + is(result.type, "number", "The type is correct"); + is(result.subtype, undefined, "The subtype is undefined for numbers"); + is(result.value, 42, "The result is the promise's resolution"); +}); + +add_task(async function awaitPromiseReject({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { exceptionDetails } = await Runtime.evaluate({ + expression: "Promise.reject(42)", + awaitPromise: true, + }); + // TODO: Implement all values for exceptionDetails (bug 1548480) + is( + exceptionDetails.exception.value, + 42, + "The result is the promise's rejection" + ); +}); + +add_task(async function awaitPromiseDelayedResolve({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "new Promise(r => setTimeout(() => r(42), 0))", + awaitPromise: true, + }); + is(result.type, "number", "The type is correct"); + is(result.subtype, undefined, "The subtype is undefined for numbers"); + is(result.value, 42, "The result is the promise's resolution"); +}); + +add_task(async function awaitPromiseDelayedReject({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { exceptionDetails } = await Runtime.evaluate({ + expression: "new Promise((_,r) => setTimeout(() => r(42), 0))", + awaitPromise: true, + }); + is( + exceptionDetails.exception.value, + 42, + "The result is the promise's rejection" + ); +}); + +add_task(async function awaitPromiseResolveWithoutWait({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "Promise.resolve(42)", + awaitPromise: false, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, "promise", "The subtype is promise"); + ok(!!result.objectId, "We got the object id for the promise"); + ok(!result.value, "We do not receive any value"); +}); + +add_task(async function awaitPromiseDelayedResolveWithoutWait({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "new Promise(r => setTimeout(() => r(42), 0))", + awaitPromise: false, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, "promise", "The subtype is promise"); + ok(!!result.objectId, "We got the object id for the promise"); + ok(!result.value, "We do not receive any value"); +}); + +add_task(async function awaitPromiseRejectWithoutWait({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "Promise.reject(42)", + awaitPromise: false, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, "promise", "The subtype is promise"); + ok(!!result.objectId, "We got the object id for the promise"); + ok(!result.exceptionDetails, "We do not receive any exception"); +}); + +add_task(async function awaitPromiseDelayedRejectWithoutWait({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "new Promise((_,r) => setTimeout(() => r(42), 0))", + awaitPromise: false, + }); + + is(result.type, "object", "The type is correct"); + is(result.subtype, "promise", "The subtype is promise"); + ok(!!result.objectId, "We got the object id for the promise"); + ok(!result.exceptionDetails, "We do not receive any exception"); +}); + +add_task(async function contextIdInvalidValue({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + let errorThrown = ""; + try { + await Runtime.evaluate({ expression: "", contextId: -1 }); + } catch (e) { + errorThrown = e.message; + } + ok(errorThrown.includes("Cannot find context with specified id")); +}); + +add_task(async function contextIdNotSpecified({ client }) { + const { Runtime } = client; + + await loadURL(TEST_DOC); + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ expression: "location.href" }); + is(result.value, TEST_DOC, "Works against the current document"); +}); + +add_task(async function contextIdSpecified({ client }) { + const { Runtime } = client; + + await loadURL(TEST_DOC); + const { id: contextId } = await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "location.href", + contextId, + }); + is(result.value, TEST_DOC, "Works against the targetted document"); +}); + +add_task(async function returnAsObjectTypes({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const expressions = [ + { expression: "({foo:true})", type: "object", subtype: undefined }, + { expression: "Symbol('foo')", type: "symbol", subtype: undefined }, + { expression: "new Promise(()=>{})", type: "object", subtype: "promise" }, + { expression: "new Int8Array(8)", type: "object", subtype: "typedarray" }, + { expression: "new WeakMap()", type: "object", subtype: "weakmap" }, + { expression: "new WeakSet()", type: "object", subtype: "weakset" }, + { expression: "new Map()", type: "object", subtype: "map" }, + { expression: "new Set()", type: "object", subtype: "set" }, + { expression: "/foo/", type: "object", subtype: "regexp" }, + { expression: "[1, 2]", type: "object", subtype: "array" }, + { expression: "new Proxy({}, {})", type: "object", subtype: "proxy" }, + { expression: "new Date()", type: "object", subtype: "date" }, + { + expression: "document", + type: "object", + subtype: "node", + className: "HTMLDocument", + description: "#document", + }, + { + expression: `(() => {{ + const div = document.createElement('div'); + div.id = "foo"; + return div; + }})()`, + type: "object", + subtype: "node", + className: "HTMLDivElement", + description: "div#foo", + }, + ]; + + for (const entry of expressions) { + const { expression, type, subtype, className, description } = entry; + + const { result } = await Runtime.evaluate({ expression }); + + is(result.type, type, "The type is correct"); + is(result.subtype, subtype, "The subtype is correct"); + ok(!!result.objectId, "Got an object id"); + if (className) { + is(result.className, className, "The className is correct"); + } + if (description) { + is(result.description, description, "The description is correct"); + } + } +}); + +add_task(async function returnAsObjectDifferentObjectIds({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const expressions = [{}, "document"]; + for (const expression of expressions) { + const { result: result1 } = await Runtime.evaluate({ + expression: JSON.stringify(expression), + }); + const { result: result2 } = await Runtime.evaluate({ + expression: JSON.stringify(expression), + }); + is( + result1.objectId, + result2.objectId, + `Different object ids returned for ${expression}` + ); + } +}); + +add_task(async function returnAsObjectPrimitiveTypes({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const expressions = [42, "42", true, 4.2]; + for (const expression of expressions) { + const { result } = await Runtime.evaluate({ + expression: JSON.stringify(expression), + }); + is(result.value, expression, `Evaluating primitive '${expression}' works`); + is(result.type, typeof expression, `${expression} type is correct`); + } +}); + +add_task(async function returnAsObjectNotSerializable({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const notSerializableNumbers = { + number: ["-0", "NaN", "Infinity", "-Infinity"], + bigint: ["42n"], + }; + + for (const type in notSerializableNumbers) { + for (const expression of notSerializableNumbers[type]) { + const { result } = await Runtime.evaluate({ expression }); + Assert.deepEqual( + result, + { + type, + unserializableValue: expression, + description: expression, + }, + `Evaluating unserializable '${expression}' works` + ); + } + } +}); + +// `null` is special as it has its own subtype, is of type 'object' +// but is returned as a value, without an `objectId` attribute +add_task(async function returnAsObjectNull({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "null", + }); + Assert.deepEqual( + result, + { + type: "object", + subtype: "null", + value: null, + }, + "Null type is correct" + ); +}); + +// undefined doesn't work with JSON.stringify, so test it independently +add_task(async function returnAsObjectUndefined({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "undefined", + }); + Assert.deepEqual( + result, + { + type: "undefined", + }, + "Undefined type is correct" + ); +}); + +add_task(async function returnByValueInvalidTypes({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + for (const returnByValue of [null, 1, "foo", [], {}]) { + let errorThrown = ""; + try { + await Runtime.evaluate({ + expression: "", + returnByValue, + }); + } catch (e) { + errorThrown = e.message; + } + ok(errorThrown.includes("returnByValue: boolean value expected")); + } +}); + +add_task(async function returnByValue({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const values = [ + null, + 42, + 42.0, + "42", + true, + false, + { foo: true }, + { foo: { bar: 42, str: "str", array: [1, 2, 3] } }, + [42, "42", true], + [{ foo: true }], + ]; + + for (const value of values) { + const { result } = await Runtime.evaluate({ + expression: `(${JSON.stringify(value)})`, + returnByValue: true, + }); + + Assert.deepEqual( + result, + { + type: typeof value, + value, + description: value != null ? value.toString() : value, + }, + `Returned expected value for ${JSON.stringify(value)}` + ); + } +}); + +add_task(async function returnByValueNotSerializable({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const notSerializableNumbers = { + number: ["-0", "NaN", "Infinity", "-Infinity"], + bigint: ["42n"], + }; + + for (const type in notSerializableNumbers) { + for (const unserializableValue of notSerializableNumbers[type]) { + const { result } = await Runtime.evaluate({ + expression: `(${unserializableValue})`, + returnByValue: true, + }); + + Assert.deepEqual( + result, + { + type, + unserializableValue, + description: unserializableValue, + }, + `Returned expected value for ${JSON.stringify(unserializableValue)}` + ); + } + } +}); + +// Test undefined individually as JSON.stringify doesn't return a string +add_task(async function returnByValueUndefined({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { result } = await Runtime.evaluate({ + expression: "undefined", + returnByValue: true, + }); + + Assert.deepEqual( + result, + { + type: "undefined", + }, + "Undefined type is correct" + ); +}); + +add_task(async function exceptionDetailsJavascriptError({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { exceptionDetails } = await Runtime.evaluate({ + expression: "doesNotExists()", + }); + + Assert.deepEqual( + exceptionDetails, + { + text: "doesNotExists is not defined", + }, + "Javascript error is passed to the client" + ); +}); + +add_task(async function exceptionDetailsThrowError({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { exceptionDetails } = await Runtime.evaluate({ + expression: "throw new Error('foo')", + }); + + Assert.deepEqual( + exceptionDetails, + { + text: "foo", + }, + "Exception details are passed to the client" + ); +}); + +add_task(async function exceptionDetailsThrowValue({ client }) { + const { Runtime } = client; + + await enableRuntime(client); + + const { exceptionDetails } = await Runtime.evaluate({ + expression: "throw 'foo'", + }); + + Assert.deepEqual( + exceptionDetails, + { + exception: { + type: "string", + value: "foo", + }, + }, + "Exception details are passed as a RemoteObject" + ); +}); diff --git a/remote/test/browser/runtime/browser_exceptionThrown.js b/remote/test/browser/runtime/browser_exceptionThrown.js new file mode 100644 index 0000000000..b573f0cffd --- /dev/null +++ b/remote/test/browser/runtime/browser_exceptionThrown.js @@ -0,0 +1,120 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PAGE_CONSOLE_EVENTS = + "http://example.com/browser/remote/test/browser/runtime/doc_console_events.html"; + +add_task(async function noEventsWhenRuntimeDomainDisabled({ client }) { + await runExceptionThrownTest(client, 0, async () => { + await throwScriptError({ text: "foo" }); + }); +}); + +add_task(async function noEventsAfterRuntimeDomainDisabled({ client }) { + const { Runtime } = client; + + await Runtime.enable(); + await Runtime.disable(); + + await runExceptionThrownTest(client, 0, async () => { + await throwScriptError({ text: "foo" }); + }); +}); + +add_task(async function noEventsForScriptErrorWithoutException({ client }) { + const { Runtime } = client; + + await Runtime.enable(); + + await runExceptionThrownTest(client, 0, async () => { + await throwScriptError({ text: "foo" }); + }); +}); + +add_task(async function eventsForScriptErrorWithException({ client }) { + await loadURL(PAGE_CONSOLE_EVENTS); + + const context = await enableRuntime(client); + + const events = await runExceptionThrownTest(client, 1, async () => { + evaluate(client, context.id, () => { + document.getElementById("js-error").click(); + }); + }); + + is( + typeof events[0].exceptionId, + "number", + "Got expected type for exception id" + ); + is( + events[0].text, + "TypeError: foo.click is not a function", + "Got expected text" + ); + is(events[0].lineNumber, 9, "Got expected line number"); + is(events[0].columnNumber, 11, "Got expected column number"); + is(events[0].url, PAGE_CONSOLE_EVENTS, "Got expected url"); + is( + events[0].executionContextId, + context.id, + "Got event from current execution context" + ); + + const callFrames = events[0].stackTrace.callFrames; + is(callFrames.length, 2, "Got expected amount of call frames"); + + is(callFrames[0].functionName, "throwError", "Got expected function name"); + is(callFrames[0].url, PAGE_CONSOLE_EVENTS, "Got expected url"); + is(callFrames[0].lineNumber, 9, "Got expected line number"); + is(callFrames[0].columnNumber, 11, "Got expected column number"); + + is(callFrames[1].functionName, "onclick", "Got expected function name"); + is(callFrames[1].url, PAGE_CONSOLE_EVENTS, "Got expected url"); +}); + +async function runExceptionThrownTest( + client, + eventCount, + callback, + options = {} +) { + const { Runtime } = client; + + const EVENT_EXCEPTION_THROWN = "Runtime.exceptionThrown"; + + const history = new RecordEvents(eventCount); + history.addRecorder({ + event: Runtime.exceptionThrown, + eventName: EVENT_EXCEPTION_THROWN, + messageFn: payload => `Received "${payload.name}"`, + }); + + const timeBefore = Date.now(); + await callback(); + + const exceptionThrownEvents = await history.record(); + is(exceptionThrownEvents.length, eventCount, "Got expected amount of events"); + + if (eventCount == 0) { + return []; + } + + const timeAfter = Date.now(); + + // Check basic details for entryAdded events + exceptionThrownEvents.forEach(event => { + const details = event.payload.exceptionDetails; + const timestamp = event.payload.timestamp; + + is(typeof details, "object", "Got expected 'exceptionDetails' property"); + ok( + timestamp >= timeBefore && timestamp <= timeAfter, + "Got valid timestamp" + ); + }); + + return exceptionThrownEvents.map(event => event.payload.exceptionDetails); +} diff --git a/remote/test/browser/runtime/browser_executionContextEvents.js b/remote/test/browser/runtime/browser_executionContextEvents.js new file mode 100644 index 0000000000..f0f15c5516 --- /dev/null +++ b/remote/test/browser/runtime/browser_executionContextEvents.js @@ -0,0 +1,339 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the Runtime execution context events + +const DOC = toDataURL("default-test-page"); +const DOC_IFRAME = toDataURL(`<iframe src='${DOC}'></iframe>`); + +const DESTROYED = "Runtime.executionContextDestroyed"; +const CREATED = "Runtime.executionContextCreated"; +const CLEARED = "Runtime.executionContextsCleared"; + +add_task(async function noEventsWhenRuntimeDomainDisabled({ client }) { + const { Runtime } = client; + + const history = recordContextEvents(Runtime, 0); + await loadURL(DOC); + await assertEventOrder({ history, expectedEvents: [] }); +}); + +add_task(async function noEventsAfterRuntimeDomainDisabled({ client }) { + const { Runtime } = client; + + await Runtime.enable(); + await Runtime.disable(); + + const history = recordContextEvents(Runtime, 0); + await loadURL(DOC); + await assertEventOrder({ history, expectedEvents: [] }); +}); + +add_task(async function eventsWhenNavigatingWithNoFrames({ client }) { + const { Page, Runtime } = client; + + const previousContext = await enableRuntime(client); + const history = recordContextEvents(Runtime, 3); + + const { frameId } = await Page.navigate({ url: DOC }); + await assertEventOrder({ history }); + + const { executionContextId: destroyedId } = history.findEvent( + DESTROYED + ).payload; + is( + destroyedId, + previousContext.id, + "The destroyed event reports the previous context id" + ); + + const { context: contextCreated } = history.findEvent(CREATED).payload; + checkDefaultContext(contextCreated); + isnot( + contextCreated.id, + previousContext.id, + "The new execution context has a different id" + ); + is( + contextCreated.auxData.frameId, + frameId, + "The execution context frame id is the same " + + "than the one returned by Page.navigate" + ); +}); + +add_task(async function eventsWhenNavigatingFrameSet({ client }) { + const { Runtime } = client; + + const previousContext = await enableRuntime(client); + + // Check navigation to a frameset + const historyTo = recordContextEvents(Runtime, 4); + await loadURL(DOC_IFRAME); + await assertEventOrder({ + history: historyTo, + expectedEvents: [DESTROYED, CLEARED, CREATED, CREATED], + }); + + const { executionContextId: destroyedId } = historyTo.findEvent( + DESTROYED + ).payload; + is( + destroyedId, + previousContext.id, + "The destroyed event reports the previous context id" + ); + + const contexts = historyTo.findEvents(CREATED); + const createdTopContext = contexts[0].payload.context; + const createdFrameContext = contexts[1].payload.context; + + checkDefaultContext(createdTopContext); + isnot( + createdTopContext.id, + previousContext.id, + "The new execution context has a different id" + ); + is( + createdTopContext.origin, + DOC_IFRAME, + "The execution context origin is the frameset" + ); + + checkDefaultContext(createdFrameContext); + isnot( + createdFrameContext.id, + createdTopContext.id, + "The new frame's execution context has a different id" + ); + is( + createdFrameContext.origin, + DOC, + "The frame's execution context origin is the frame" + ); + + // Check navigation from a frameset + const historyFrom = recordContextEvents(Runtime, 4); + await loadURL(DOC); + await assertEventOrder({ + history: historyFrom, + // Bug 1644657: The cleared event should come last but we emit destroy events + // for the top-level context and for frames afterward. Chrome only sends out + // the cleared event on navigation. + expectedEvents: [DESTROYED, CLEARED, DESTROYED, CREATED], + }); + + const destroyedContextIds = historyFrom.findEvents(DESTROYED); + is( + destroyedContextIds[0].payload.executionContextId, + createdTopContext.id, + "The destroyed event reports the previous context id" + ); + is( + destroyedContextIds[1].payload.executionContextId, + createdFrameContext.id, + "The destroyed event reports the previous frame's context id" + ); + + const { context: contextCreated } = historyFrom.findEvent(CREATED).payload; + checkDefaultContext(contextCreated); + isnot( + contextCreated.id, + createdTopContext.id, + "The new execution context has a different id" + ); + is( + contextCreated.origin, + DOC, + "The execution context origin is not the frameset" + ); +}); + +add_task(async function eventsWhenNavigatingBackWithNoFrames({ client }) { + const { Runtime } = client; + + // Load an initial URL so that navigating back will work + await loadURL(DOC); + const previousContext = await enableRuntime(client); + + const executionContextCreated = Runtime.executionContextCreated(); + await loadURL(toDataURL("other-test-page")); + const { context: createdContext } = await executionContextCreated; + + const history = recordContextEvents(Runtime, 3); + gBrowser.selectedBrowser.goBack(); + await assertEventOrder({ history }); + + const { executionContextId: destroyedId } = history.findEvent( + DESTROYED + ).payload; + is( + destroyedId, + createdContext.id, + "The destroyed event reports the current context id" + ); + + const { context } = history.findEvent(CREATED).payload; + checkDefaultContext(context); + is( + context.origin, + previousContext.origin, + "The new execution context has the same origin as the previous one." + ); + isnot( + context.id, + previousContext.id, + "The new execution context has a different id" + ); + ok(context.auxData.isDefault, "The execution context is the default one"); + is( + context.auxData.frameId, + previousContext.auxData.frameId, + "The execution context frame id is always the same" + ); + is(context.auxData.type, "default", "Execution context has 'default' type"); + is(context.name, "", "The default execution context is named ''"); + + const { result } = await Runtime.evaluate({ + contextId: context.id, + expression: "location.href", + }); + is( + result.value, + DOC, + "Runtime.evaluate works and is against the page we just navigated to" + ); +}); + +add_task(async function eventsWhenReloadingPageWithNoFrames({ client }) { + const { Page, Runtime } = client; + + // Load an initial URL so that reload will work + await loadURL(DOC); + const previousContext = await enableRuntime(client); + + await Page.enable(); + + const history = recordContextEvents(Runtime, 3); + const frameNavigated = Page.frameNavigated(); + gBrowser.selectedBrowser.reload(); + await frameNavigated; + + await assertEventOrder({ history }); + + const { executionContextId } = history.findEvent(DESTROYED).payload; + is( + executionContextId, + previousContext.id, + "The destroyed event reports the previous context id" + ); + + const { context } = history.findEvent(CREATED).payload; + checkDefaultContext(context); + is( + context.auxData.frameId, + previousContext.auxData.frameId, + "The execution context frame id is the same as before reloading" + ); + + isnot( + executionContextId, + context.id, + "The destroyed id is different from the created one" + ); +}); + +add_task(async function eventsWhenNavigatingByLocationWithNoFrames({ client }) { + const { Runtime } = client; + + const previousContext = await enableRuntime(client); + const history = recordContextEvents(Runtime, 3); + + await Runtime.evaluate({ + contextId: previousContext.id, + expression: `window.location = '${DOC}';`, + }); + await assertEventOrder({ history }); + + const { executionContextId: destroyedId } = history.findEvent( + DESTROYED + ).payload; + is( + destroyedId, + previousContext.id, + "The destroyed event reports the previous context id" + ); + + const { context: createdContext } = history.findEvent(CREATED).payload; + checkDefaultContext(createdContext); + is( + createdContext.auxData.frameId, + previousContext.auxData.frameId, + "The execution context frame id is identical " + + "to the one from before before setting the window's location" + ); + isnot( + destroyedId, + createdContext.id, + "The destroyed id is different from the created one" + ); +}); + +function recordContextEvents(Runtime, total) { + const history = new RecordEvents(total); + + history.addRecorder({ + event: Runtime.executionContextDestroyed, + eventName: DESTROYED, + messageFn: payload => { + return `Received ${DESTROYED} for id ${payload.executionContextId}`; + }, + }); + history.addRecorder({ + event: Runtime.executionContextCreated, + eventName: CREATED, + messageFn: ({ context }) => { + return ( + `Received ${CREATED} for id ${context.id}` + + ` type: ${context.auxData.type}` + + ` name: ${context.name}` + + ` origin: ${context.origin}` + ); + }, + }); + history.addRecorder({ + event: Runtime.executionContextsCleared, + eventName: CLEARED, + }); + + return history; +} + +async function assertEventOrder(options = {}) { + const { history, expectedEvents = [DESTROYED, CLEARED, CREATED] } = options; + const events = await history.record(); + const eventNames = events.map(item => item.eventName); + info(`Expected events: ${expectedEvents}`); + info(`Received events: ${eventNames}`); + + is( + events.length, + expectedEvents.length, + "Received expected number of Runtime context events" + ); + Assert.deepEqual( + events.map(item => item.eventName), + expectedEvents, + "Received Runtime context events in expected order" + ); +} + +function checkDefaultContext(context) { + ok(!!context.id, "The execution context has an id"); + ok(context.auxData.isDefault, "The execution context is the default one"); + is(context.auxData.type, "default", "Execution context has 'default' type"); + ok(!!context.origin, "The execution context has an origin"); + is(context.name, "", "The default execution context is named ''"); +} diff --git a/remote/test/browser/runtime/browser_getProperties.js b/remote/test/browser/runtime/browser_getProperties.js new file mode 100644 index 0000000000..84229db10e --- /dev/null +++ b/remote/test/browser/runtime/browser_getProperties.js @@ -0,0 +1,184 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the Runtime remote object + +add_task(async function({ client }) { + const firstContext = await testRuntimeEnable(client); + const contextId = firstContext.id; + + await testGetOwnSimpleProperties(client, contextId); + await testGetCustomProperty(client, contextId); + await testGetPrototypeProperties(client, contextId); + await testGetGetterSetterProperties(client, contextId); +}); + +async function testRuntimeEnable({ Runtime }) { + // Enable watching for new execution context + await Runtime.enable(); + info("Runtime domain has been enabled"); + + // Calling Runtime.enable will emit executionContextCreated for the existing contexts + const { context } = await Runtime.executionContextCreated(); + ok(!!context.id, "The execution context has an id"); + ok(context.auxData.isDefault, "The execution context is the default one"); + ok(!!context.auxData.frameId, "The execution context has a frame id set"); + + return context; +} + +async function testGetOwnSimpleProperties({ Runtime }, contextId) { + const { result } = await Runtime.evaluate({ + contextId, + expression: "({ bool: true, fun() {}, int: 1, object: {}, string: 'foo' })", + }); + is(result.subtype, undefined, "JS Object has no subtype"); + is(result.type, "object", "The type is correct"); + ok(!!result.objectId, "Got an object id"); + + const { result: result2 } = await Runtime.getProperties({ + objectId: result.objectId, + ownProperties: true, + }); + is( + result2.length, + 5, + "ownProperties=true allows to iterate only over direct object properties (i.e. ignore prototype)" + ); + result2.sort((a, b) => a.name > b.name); + is(result2[0].name, "bool"); + is(result2[0].configurable, true); + is(result2[0].enumerable, true); + is(result2[0].writable, true); + is(result2[0].value.type, "boolean"); + is(result2[0].value.value, true); + is(result2[0].isOwn, true); + + is(result2[1].name, "fun"); + is(result2[1].configurable, true); + is(result2[1].enumerable, true); + is(result2[1].writable, true); + is(result2[1].value.type, "function"); + ok(!!result2[1].value.objectId); + is(result2[1].isOwn, true); + + is(result2[2].name, "int"); + is(result2[2].configurable, true); + is(result2[2].enumerable, true); + is(result2[2].writable, true); + is(result2[2].value.type, "number"); + is(result2[2].value.value, 1); + is(result2[2].isOwn, true); + + is(result2[3].name, "object"); + is(result2[3].configurable, true); + is(result2[3].enumerable, true); + is(result2[3].writable, true); + is(result2[3].value.type, "object"); + ok(!!result2[3].value.objectId); + is(result2[3].isOwn, true); + + is(result2[4].name, "string"); + is(result2[4].configurable, true); + is(result2[4].enumerable, true); + is(result2[4].writable, true); + is(result2[4].value.type, "string"); + is(result2[4].value.value, "foo"); + is(result2[4].isOwn, true); +} + +async function testGetPrototypeProperties({ Runtime }, contextId) { + const { result } = await Runtime.evaluate({ + contextId, + expression: "({ foo: 42 })", + }); + is(result.subtype, undefined, "JS Object has no subtype"); + is(result.type, "object", "The type is correct"); + ok(!!result.objectId, "Got an object id"); + + const { result: result2 } = await Runtime.getProperties({ + objectId: result.objectId, + ownProperties: false, + }); + ok(result2.length > 1, "We have more properties than just the object one"); + const foo = result2.find(p => p.name == "foo"); + ok(foo, "The object property is described"); + ok(foo.isOwn, "and is reported as 'own' property"); + + const toString = result2.find(p => p.name == "toString"); + ok( + toString, + "Function from Object's prototype are also described like toString" + ); + ok(!toString.isOwn, "but are reported as not being an 'own' property"); +} + +async function testGetGetterSetterProperties({ Runtime }, contextId) { + const { result } = await Runtime.evaluate({ + contextId, + expression: + "({ get prop() { return this.x; }, set prop(v) { this.x = v; } })", + }); + is(result.subtype, undefined, "JS Object has no subtype"); + is(result.type, "object", "The type is correct"); + ok(!!result.objectId, "Got an object id"); + + const { result: result2 } = await Runtime.getProperties({ + objectId: result.objectId, + ownProperties: true, + }); + is(result2.length, 1); + + is(result2[0].name, "prop"); + is(result2[0].configurable, true); + is(result2[0].enumerable, true); + is( + result2[0].writable, + undefined, + "writable is only set for data properties" + ); + + is(result2[0].get.type, "function"); + ok(!!result2[0].get.objectId); + is(result2[0].set.type, "function"); + ok(!!result2[0].set.objectId); + + is(result2[0].isOwn, true); + + const { result: result3 } = await Runtime.callFunctionOn({ + executionContextId: contextId, + functionDeclaration: "(set, get) => { set(42); return get(); }", + arguments: [ + { objectId: result2[0].set.objectId }, + { objectId: result2[0].get.objectId }, + ], + }); + is(result3.type, "number", "The type is correct"); + is(result3.subtype, undefined, "The subtype is undefined for numbers"); + is(result3.value, 42, "The getter returned the value set by the setter"); +} + +async function testGetCustomProperty({ Runtime }, contextId) { + const { result } = await Runtime.evaluate({ + contextId, + expression: `const obj = {}; Object.defineProperty(obj, "prop", { value: 42 }); obj`, + }); + is(result.subtype, undefined, "JS Object has no subtype"); + is(result.type, "object", "The type is correct"); + ok(!!result.objectId, "Got an object id"); + + const { result: result2 } = await Runtime.getProperties({ + objectId: result.objectId, + ownProperties: true, + }); + is(result2.length, 1, "We only get the one object's property"); + is(result2[0].name, "prop"); + is(result2[0].configurable, false); + is(result2[0].enumerable, false); + is(result2[0].writable, false); + is(result2[0].value.type, "number"); + is(result2[0].value.value, 42); + is(result2[0].isOwn, true); +} diff --git a/remote/test/browser/runtime/browser_remoteObjects.js b/remote/test/browser/runtime/browser_remoteObjects.js new file mode 100644 index 0000000000..07453ffa59 --- /dev/null +++ b/remote/test/browser/runtime/browser_remoteObjects.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the Runtime remote object + +add_task(async function({ client }) { + const firstContext = await testRuntimeEnable(client); + const contextId = firstContext.id; + + await testObjectRelease(client, contextId); +}); + +async function testRuntimeEnable({ Runtime }) { + // Enable watching for new execution context + await Runtime.enable(); + info("Runtime domain has been enabled"); + + // Calling Runtime.enable will emit executionContextCreated for the existing contexts + const { context } = await Runtime.executionContextCreated(); + ok(!!context.id, "The execution context has an id"); + ok(context.auxData.isDefault, "The execution context is the default one"); + ok(!!context.auxData.frameId, "The execution context has a frame id set"); + + return context; +} + +async function testObjectRelease({ Runtime }, contextId) { + const { result } = await Runtime.evaluate({ + contextId, + expression: "({ foo: 42 })", + }); + is(result.subtype, undefined, "JS Object has no subtype"); + is(result.type, "object", "The type is correct"); + ok(!!result.objectId, "Got an object id"); + + const { result: result2 } = await Runtime.callFunctionOn({ + executionContextId: contextId, + functionDeclaration: "obj => JSON.stringify(obj)", + arguments: [{ objectId: result.objectId }], + }); + is(result2.type, "string", "The type is correct"); + is(result2.value, JSON.stringify({ foo: 42 }), "Got the object's JSON"); + + const { result: result3 } = await Runtime.callFunctionOn({ + objectId: result.objectId, + functionDeclaration: "function () { return this.foo; }", + }); + is(result3.type, "number", "The type is correct"); + is(result3.value, 42, "Got the object's foo attribute"); + + await Runtime.releaseObject({ + objectId: result.objectId, + }); + info("Object is released"); + + try { + await Runtime.callFunctionOn({ + executionContextId: contextId, + functionDeclaration: "() => {}", + arguments: [{ objectId: result.objectId }], + }); + ok(false, "callFunctionOn with a released object as argument should throw"); + } catch (e) { + ok( + e.message.includes("Could not find object with given id"), + "callFunctionOn throws on released argument" + ); + } + try { + await Runtime.callFunctionOn({ + objectId: result.objectId, + functionDeclaration: "() => {}", + }); + ok(false, "callFunctionOn with a released object as target should throw"); + } catch (e) { + ok( + e.message.includes("Unable to get the context for object with id"), + "callFunctionOn throws on released target" + ); + } +} diff --git a/remote/test/browser/runtime/doc_console_events.html b/remote/test/browser/runtime/doc_console_events.html new file mode 100644 index 0000000000..63b37ce592 --- /dev/null +++ b/remote/test/browser/runtime/doc_console_events.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> + <title>Empty page</title> + + <script> + function throwError() { + let foo = {}; + foo.click(); + } + </script> +</head> +<body> + <a id="console-log" href="javascript:console.log('foo')">console.log()</a><br/> + <a id="console-error" href="javascript:console.error('foo')">console.error()</a><br/> + <a id="js-error" onclick="throwError()">Javascript Error</a><br/> +</body> +</html> diff --git a/remote/test/browser/runtime/head.js b/remote/test/browser/runtime/head.js new file mode 100644 index 0000000000..7131e98b6f --- /dev/null +++ b/remote/test/browser/runtime/head.js @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../head.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/test/browser/head.js", + this +); diff --git a/remote/test/browser/security/browser.ini b/remote/test/browser/security/browser.ini new file mode 100644 index 0000000000..5a8065e015 --- /dev/null +++ b/remote/test/browser/security/browser.ini @@ -0,0 +1,11 @@ +[DEFAULT] +tags = remote +subsuite = remote +prefs = + remote.enabled=true +support-files = + !/remote/test/browser/chrome-remote-interface.js + !/remote/test/browser/head.js + head.js + +[browser_setIgnoreCertificateErrors.js] diff --git a/remote/test/browser/security/browser_setIgnoreCertificateErrors.js b/remote/test/browser/security/browser_setIgnoreCertificateErrors.js new file mode 100644 index 0000000000..36056ad528 --- /dev/null +++ b/remote/test/browser/security/browser_setIgnoreCertificateErrors.js @@ -0,0 +1,162 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + STATE_IS_SECURE, + STATE_IS_BROKEN, + STATE_IS_INSECURE, +} = Ci.nsIWebProgressListener; + +// from ../../../build/pgo/server-locations.txt +const NO_CERT = "https://nocert.example.com:443"; +const SELF_SIGNED = "https://self-signed.example.com:443"; +const UNTRUSTED = "https://untrusted.example.com:443"; +const EXPIRED = "https://expired.example.com:443"; +const MISMATCH_EXPIRED = "https://mismatch.expired.example.com:443"; +const MISMATCH_UNTRUSTED = "https://mismatch.untrusted.example.com:443"; +const UNTRUSTED_EXPIRED = "https://untrusted-expired.example.com:443"; +const MISMATCH_UNTRUSTED_EXPIRED = + "https://mismatch.untrusted-expired.example.com:443"; + +const BAD_CERTS = [ + NO_CERT, + SELF_SIGNED, + UNTRUSTED, + EXPIRED, + MISMATCH_EXPIRED, + MISMATCH_UNTRUSTED, + UNTRUSTED_EXPIRED, + MISMATCH_UNTRUSTED_EXPIRED, +]; + +function getConnectionState() { + // prevents items that are being lazy loaded causing issues + document.getElementById("identity-box").click(); + gIdentityHandler.refreshIdentityPopup(); + return document.getElementById("identity-popup").getAttribute("connection"); +} + +/** + * Compares the security state of the page with what is expected. + * Returns one of "secure", "broken", "insecure", or "unknown". + */ +function isSecurityState(browser, expectedState) { + const ui = browser.securityUI; + if (!ui) { + ok(false, "No security UI to get the security state"); + return; + } + + const isSecure = ui.state & STATE_IS_SECURE; + const isBroken = ui.state & STATE_IS_BROKEN; + const isInsecure = ui.state & STATE_IS_INSECURE; + + let actualState; + if (isSecure && !(isBroken || isInsecure)) { + actualState = "secure"; + } else if (isBroken && !(isSecure || isInsecure)) { + actualState = "broken"; + } else if (isInsecure && !(isSecure || isBroken)) { + actualState = "insecure"; + } else { + actualState = "unknown"; + } + + is( + expectedState, + actualState, + `Expected state is ${expectedState} and actual state is ${actualState}` + ); +} + +add_task(async function testDefault({ Security }) { + for (const url of BAD_CERTS) { + info(`Navigating to ${url}`); + const loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url); + await loaded; + + is( + getConnectionState(), + "cert-error-page", + "Security error page is present" + ); + isSecurityState(gBrowser, "insecure"); + } +}); + +add_task(async function testIgnore({ client }) { + const { Security } = client; + info("Enable security certificate override"); + await Security.setIgnoreCertificateErrors({ ignore: true }); + + for (const url of BAD_CERTS) { + info(`Navigating to ${url}`); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + is( + getConnectionState(), + "secure-cert-user-overridden", + "Security certificate was overridden by user" + ); + isSecurityState(gBrowser, "secure"); + } +}); + +add_task(async function testUnignore({ client }) { + const { Security } = client; + info("Disable security certificate override"); + await Security.setIgnoreCertificateErrors({ ignore: false }); + + for (const url of BAD_CERTS) { + info(`Navigating to ${url}`); + const loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url); + await loaded; + + is( + getConnectionState(), + "cert-error-page", + "Security error page is present" + ); + isSecurityState(gBrowser, "insecure"); + } +}); + +// smoke test for unignored -> ignored -> unignored +add_task(async function testToggle({ client }) { + const { Security } = client; + let loaded; + + info("Enable security certificate override"); + await Security.setIgnoreCertificateErrors({ ignore: true }); + + info(`Navigating to ${UNTRUSTED} having set the override`); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, UNTRUSTED); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + is( + getConnectionState(), + "secure-cert-user-overridden", + "Security certificate was overridden by user" + ); + isSecurityState(gBrowser, "secure"); + + info("Disable security certificate override"); + await Security.setIgnoreCertificateErrors({ ignore: false }); + + info(`Navigating to ${UNTRUSTED} having unset the override`); + loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, UNTRUSTED); + await loaded; + + is( + getConnectionState(), + "cert-error-page", + "Security error page is present by default" + ); + isSecurityState(gBrowser, "insecure"); +}); diff --git a/remote/test/browser/security/head.js b/remote/test/browser/security/head.js new file mode 100644 index 0000000000..7131e98b6f --- /dev/null +++ b/remote/test/browser/security/head.js @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../head.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/test/browser/head.js", + this +); diff --git a/remote/test/browser/target/browser.ini b/remote/test/browser/target/browser.ini new file mode 100644 index 0000000000..d0f854decd --- /dev/null +++ b/remote/test/browser/target/browser.ini @@ -0,0 +1,22 @@ +[DEFAULT] +tags = remote +subsuite = remote +prefs = + remote.enabled=true +support-files = + !/remote/test/browser/chrome-remote-interface.js + !/remote/test/browser/head.js + head.js + doc_test.html + +[browser_activateTarget.js] +[browser_attachToTarget.js] +[browser_attachedToTarget.js] +[browser_browserContext.js] +[browser_closeTarget.js] +[browser_getTargets.js] +[browser_sendMessageToTarget.js] +skip-if = true # bug 1598468 - temporarily stop emitting event due to spamming +[browser_setDiscoverTargets.js] +[browser_targetCreated.js] +[browser_targetDestroyed.js] diff --git a/remote/test/browser/target/browser_activateTarget.js b/remote/test/browser/target/browser_activateTarget.js new file mode 100644 index 0000000000..481ea2e810 --- /dev/null +++ b/remote/test/browser/target/browser_activateTarget.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function raisesWithoutArguments({ client, tab }) { + const { Target } = client; + let errorThrown = false; + try { + await Target.activateTarget(); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "activateTarget raised error without an argument"); +}); + +add_task(async function raisesWithUnknownTargetId({ client, tab }) { + const { Target } = client; + let errorThrown = false; + try { + await Target.activateTarget({ targetId: "-1" }); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "activateTarget raised error with unkown target id"); +}); + +add_task(async function selectTabInOtherWindow({ client, tab }) { + const { Target, target } = client; + is( + target.browsingContextId, + tab.linkedBrowser.browsingContext.id, + "Current target has expected browsing context id" + ); + const currentTargetId = target.id; + const targets = await getDiscoveredTargets(Target); + const filtered_targets = targets.filter(target => { + return target.targetInfo.targetId == currentTargetId; + }); + is(filtered_targets.length, 1, "The current target has been found"); + const initialTarget = filtered_targets[0]; + + is(tab.ownerGlobal, getFocusedNavigator(), "Initial window is focused"); + + // open some more tabs in the initial window + await openTab(Target); + await openTab(Target); + const lastTabFirstWindow = await openTab(Target); + is( + gBrowser.selectedTab, + lastTabFirstWindow.newTab, + "Last openend tab in initial window is the selected tab" + ); + + const { newWindow } = await openWindow(Target); + + const lastTabSecondWindow = await openTab(Target); + is( + gBrowser.selectedTab, + lastTabSecondWindow.newTab, + "Last openend tab in new window is the selected tab" + ); + + try { + is(newWindow, getFocusedNavigator(), "The new window is focused"); + await Target.activateTarget({ + targetId: initialTarget.targetInfo.targetId, + }); + is( + tab.ownerGlobal, + getFocusedNavigator(), + "Initial window is focused again" + ); + is(gBrowser.selectedTab, tab, "Selected tab is the initial tab again"); + } finally { + await BrowserTestUtils.closeWindow(newWindow); + } +}); + +function getFocusedNavigator() { + return Services.wm.getMostRecentWindow("navigator:browser"); +} diff --git a/remote/test/browser/target/browser_attachToTarget.js b/remote/test/browser/target/browser_attachToTarget.js new file mode 100644 index 0000000000..6fa75431d1 --- /dev/null +++ b/remote/test/browser/target/browser_attachToTarget.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function raisesWithoutArguments({ client, tab }) { + const { Target } = client; + let errorThrown = false; + try { + await Target.attachToTarget(); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "attachToTarget raised error without an argument"); +}); + +add_task(async function raisesWithUnknownTargetId({ client, tab }) { + const { Target } = client; + let errorThrown = false; + try { + await Target.attachToTarget({ targetId: "-1" }); + } catch (e) { + errorThrown = true; + } + ok(errorThrown, "attachToTarget raised error with unkown target id"); +}); + +add_task( + async function attachPageTarget({ client }) { + const { Target } = client; + const { targetInfo } = await openTab(Target); + + info("Attach new target"); + const { sessionId } = await Target.attachToTarget({ + targetId: targetInfo.targetId, + }); + + is( + typeof sessionId, + "string", + "attachToTarget returns the session id as string" + ); + }, + { createTab: false } +); diff --git a/remote/test/browser/target/browser_attachedToTarget.js b/remote/test/browser/target/browser_attachedToTarget.js new file mode 100644 index 0000000000..27ffba30cf --- /dev/null +++ b/remote/test/browser/target/browser_attachedToTarget.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PAGE_TEST = + "http://example.com/browser/remote/test/browser/target/doc_test.html"; + +add_task( + async function attachedPageTarget({ client }) { + const { Target } = client; + const { targetInfo } = await openTab(Target); + + await loadURL(PAGE_TEST); + + info("Attach new page target"); + const attachedToTarget = Target.attachedToTarget(); + const { sessionId } = await Target.attachToTarget({ + targetId: targetInfo.targetId, + }); + const { + targetInfo: eventTargetInfo, + sessionId: eventSessionId, + waitingForDebugger: eventWaitingForDebugger, + } = await attachedToTarget; + + is(eventTargetInfo.targetId, targetInfo.targetId, "Got expected target id"); + is(eventTargetInfo.type, "page", "Got expected target type"); + is(eventTargetInfo.title, "Test Page", "Got expected target title"); + is(eventTargetInfo.url, PAGE_TEST, "Got expected target URL"); + todo(eventTargetInfo.attached, "Set correct attached status (bug 1680780)"); + + is( + eventSessionId, + sessionId, + "attachedToTarget and attachToTarget refer to the same session id" + ); + is( + typeof eventWaitingForDebugger, + "boolean", + "Got expected type for waitingForDebugger" + ); + }, + { createTab: false } +); diff --git a/remote/test/browser/target/browser_browserContext.js b/remote/test/browser/target/browser_browserContext.js new file mode 100644 index 0000000000..5c2c25d640 --- /dev/null +++ b/remote/test/browser/target/browser_browserContext.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function({ CDP }) { + // Connect to the server + const { webSocketDebuggerUrl } = await CDP.Version(); + const client = await CDP({ target: webSocketDebuggerUrl }); + info("CDP client has been instantiated"); + + const { Target } = client; + await getDiscoveredTargets(Target); + + const { browserContextId } = await Target.createBrowserContext(); + + const targetCreated = Target.targetCreated(); + const { targetId } = await Target.createTarget({ browserContextId }); + ok(!!targetId, "Target.createTarget returns a non-empty target id"); + + const { targetInfo } = await targetCreated; + is( + targetId, + targetInfo.targetId, + "targetCreated refers to the same target id" + ); + is( + browserContextId, + targetInfo.browserContextId, + "targetCreated refers to the same browser context" + ); + is(targetInfo.type, "page", "The target is a page"); + + // Releasing the browser context is going to remove the tab opened when calling createTarget + await Target.disposeBrowserContext({ browserContextId }); + + await client.close(); + info("The client is closed"); +}); diff --git a/remote/test/browser/target/browser_closeTarget.js b/remote/test/browser/target/browser_closeTarget.js new file mode 100644 index 0000000000..34d557c9d8 --- /dev/null +++ b/remote/test/browser/target/browser_closeTarget.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function raisesWithoutArguments({ client, tab }) { + const { Target } = client; + let exceptionThrown = false; + try { + await Target.closeTarget(); + } catch (e) { + exceptionThrown = true; + } + ok(exceptionThrown, "closeTarget raised error without an argument"); +}); + +add_task(async function raisesWithUnknownTargetId({ client, tab }) { + const { Target } = client; + let exceptionThrown = false; + try { + await Target.closeTarget({ targetId: "-1" }); + } catch (e) { + exceptionThrown = true; + } + ok(exceptionThrown, "closeTarget raised error with unkown target id"); +}); + +add_task(async function triggersTargetDestroyed({ client, tab }) { + const { Target } = client; + const { targetInfo, newTab } = await openTab(Target); + + const tabClosed = BrowserTestUtils.waitForEvent(newTab, "TabClose"); + const targetDestroyed = Target.targetDestroyed(); + + info("Closing the target"); + Target.closeTarget({ targetId: targetInfo.targetId }); + + await tabClosed; + info("Tab was closed"); + + await targetDestroyed; + info("Received the Target.targetDestroyed event"); +}); diff --git a/remote/test/browser/target/browser_getTargets.js b/remote/test/browser/target/browser_getTargets.js new file mode 100644 index 0000000000..a867a0d18d --- /dev/null +++ b/remote/test/browser/target/browser_getTargets.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PAGE_TEST = + "http://example.com/browser/remote/test/browser/target/doc_test.html"; + +add_task( + async function getTargetsDetails({ client }) { + const { Target, target } = client; + + await loadURL(PAGE_TEST); + + const { targetInfos } = await Target.getTargets(); + + Assert.equal(targetInfos.length, 1, "Got expected amount of targets"); + + const targetInfo = targetInfos[0]; + is(targetInfo.id, target.id, "Got expected target id"); + is(targetInfo.type, "page", "Got expected target type"); + is(targetInfo.title, "Test Page", "Got expected target title"); + is(targetInfo.url, PAGE_TEST, "Got expected target URL"); + }, + { createTab: false } +); + +add_task( + async function getTargetsCount({ client }) { + const { Target, target } = client; + const { targetInfo: newTabTargetInfo } = await openTab(Target); + + await loadURL(PAGE_TEST); + + const { targetInfos } = await Target.getTargets(); + + Assert.equal(targetInfos.length, 2, "Got expected amount of targets"); + const targetIds = targetInfos.map(info => info.id); + ok(targetIds.includes(target.id), "Got expected original target id"); + ok(targetIds.includes(newTabTargetInfo.id), "Got expected new target id"); + }, + { createTab: false } +); diff --git a/remote/test/browser/target/browser_sendMessageToTarget.js b/remote/test/browser/target/browser_sendMessageToTarget.js new file mode 100644 index 0000000000..b440066178 --- /dev/null +++ b/remote/test/browser/target/browser_sendMessageToTarget.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function sendToAttachedTarget({ client }) { + const { Target } = client; + const { targetInfo } = await openTab(Target); + + const attachedToTarget = Target.attachedToTarget(); + const { sessionId } = await Target.attachToTarget({ + targetId: targetInfo.targetId, + }); + await attachedToTarget; + info("Target attached"); + + const id = 1; + const message = JSON.stringify({ + id, + method: "Page.navigate", + params: { + url: toDataURL("new-page"), + }, + }); + + info("Calling Target.sendMessageToTarget"); + const onResponse = Target.receivedMessageFromTarget(); + await Target.sendMessageToTarget({ sessionId, message }); + const response = await onResponse; + info("Message from target received"); + + ok(!!response, "The response is not empty"); + is(response.sessionId, sessionId, "The response is from the same session"); + + const responseMessage = JSON.parse(response.message); + is(responseMessage.id, id, "The response is from the same session"); + ok( + !!responseMessage.result.frameId, + "received the `frameId` out of `Page.navigate` request" + ); +}); diff --git a/remote/test/browser/target/browser_setDiscoverTargets.js b/remote/test/browser/target/browser_setDiscoverTargets.js new file mode 100644 index 0000000000..9e3107eb8c --- /dev/null +++ b/remote/test/browser/target/browser_setDiscoverTargets.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + async function mainTarget({ client }) { + const { Target } = client; + const { targetInfo } = await new Promise(resolve => { + const unsubscribe = Target.targetCreated(target => { + if (target.targetInfo.type == "browser") { + unsubscribe(); + resolve(target); + } + }); + + // Calling `setDiscoverTargets` will dispatch `targetCreated` event for all + // already opened tabs and the browser target. + Target.setDiscoverTargets({ discover: true }); + }); + + ok(!!targetInfo, "Target info for main target has been found"); + ok(!!targetInfo.targetId, "Main target has a non-empty target id"); + is(targetInfo.type, "browser", "Type of target is browser"); + }, + { createTab: false } +); + +add_task(async function pageTargets({ client, tab }) { + const { Target, target } = client; + is( + target.browsingContextId, + tab.linkedBrowser.browsingContext.id, + "Current target has expected browsing context id" + ); + const currentTargetId = target.id; + const url = toDataURL("pageTargets"); + await loadURL(url); + + const targets = await new Promise(resolve => { + const targets = []; + + const unsubscribe = Target.targetCreated(target => { + if (target.targetInfo.type == "page") { + targets.push(target); + + // Return once all page targets have been discovered + if (targets.length == gBrowser.tabs.length) { + unsubscribe(); + resolve(targets); + } + } + }); + + // Calling `setDiscoverTargets` will dispatch `targetCreated` event for all + // already opened tabs and the browser target. + Target.setDiscoverTargets({ discover: true }); + }); + + // Get the current target + const filtered_targets = targets.filter(target => { + return target.targetInfo.targetId == currentTargetId; + }); + is(filtered_targets.length, 1, "The current target has been found"); + const { targetInfo } = filtered_targets[0]; + + ok(!!targetInfo, "Target info for current tab has been found"); + ok(!!targetInfo.targetId, "Page target has a non-empty target id"); + is(targetInfo.type, "page", "Type of current target is 'page'"); + is(targetInfo.url, url, "Page target has a non-empty target id"); +}); diff --git a/remote/test/browser/target/browser_targetCreated.js b/remote/test/browser/target/browser_targetCreated.js new file mode 100644 index 0000000000..bfb7287f87 --- /dev/null +++ b/remote/test/browser/target/browser_targetCreated.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function eventFiredWhenTabIsCreated({ client }) { + const { Target } = client; + + const targetCreated = Target.targetCreated(); + await BrowserTestUtils.openNewForegroundTab(gBrowser); + const { targetInfo } = await targetCreated; + + is(typeof targetInfo.targetId, "string", "Got expected type for target id"); + is(targetInfo.type, "page", "Got expected target type"); + is(targetInfo.title, "", "Got expected target title"); + is(targetInfo.url, "about:blank", "Got expected target URL"); + is(targetInfo.attached, false, "Got expected attached status"); +}); diff --git a/remote/test/browser/target/browser_targetDestroyed.js b/remote/test/browser/target/browser_targetDestroyed.js new file mode 100644 index 0000000000..2ad657b135 --- /dev/null +++ b/remote/test/browser/target/browser_targetDestroyed.js @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function eventFiredWhenTabIsClosed({ client, tab }) { + const { Target } = client; + const { newTab } = await openTab(Target); + + const tabClosed = BrowserTestUtils.waitForEvent(newTab, "TabClose"); + const targetDestroyed = Target.targetDestroyed(); + + info("Closing the tab"); + BrowserTestUtils.removeTab(newTab); + + await tabClosed; + info("Tab was closed"); + + await targetDestroyed; + info("Received the Target.targetDestroyed event"); +}); diff --git a/remote/test/browser/target/doc_test.html b/remote/test/browser/target/doc_test.html new file mode 100644 index 0000000000..c779ea0773 --- /dev/null +++ b/remote/test/browser/target/doc_test.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test Page</title> +</head> +<body> +</body> +</html> diff --git a/remote/test/browser/target/head.js b/remote/test/browser/target/head.js new file mode 100644 index 0000000000..7131e98b6f --- /dev/null +++ b/remote/test/browser/target/head.js @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../head.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/test/browser/head.js", + this +); diff --git a/remote/test/moz.build b/remote/test/moz.build new file mode 100644 index 0000000000..94bc3f985f --- /dev/null +++ b/remote/test/moz.build @@ -0,0 +1,18 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"] +BROWSER_CHROME_MANIFESTS += [ + "browser/browser.ini", + "browser/dom/browser.ini", + "browser/emulation/browser.ini", + "browser/input/browser.ini", + "browser/io/browser.ini", + "browser/log/browser.ini", + "browser/network/browser.ini", + "browser/page/browser.ini", + "browser/runtime/browser.ini", + "browser/security/browser.ini", + "browser/target/browser.ini", +] diff --git a/remote/test/puppeteer/.ci/node10/Dockerfile.linux b/remote/test/puppeteer/.ci/node10/Dockerfile.linux new file mode 100644 index 0000000000..47b1478a44 --- /dev/null +++ b/remote/test/puppeteer/.ci/node10/Dockerfile.linux @@ -0,0 +1,17 @@ +FROM node:10 + +RUN apt-get update && \ + apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \ + libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \ + libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \ + libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \ + libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget && \ + rm -rf /var/lib/apt/lists/* + +# Add user so we don't need --no-sandbox. +RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \ + && mkdir -p /home/pptruser/Downloads \ + && chown -R pptruser:pptruser /home/pptruser + +# Run everything after as non-privileged user. +USER pptruser diff --git a/remote/test/puppeteer/.ci/node12/Dockerfile.linux b/remote/test/puppeteer/.ci/node12/Dockerfile.linux new file mode 100644 index 0000000000..0a9a0acc93 --- /dev/null +++ b/remote/test/puppeteer/.ci/node12/Dockerfile.linux @@ -0,0 +1,17 @@ +FROM node:12 + +RUN apt-get update && \ + apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \ + libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \ + libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \ + libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \ + libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget && \ + rm -rf /var/lib/apt/lists/* + +# Add user so we don't need --no-sandbox. +RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \ + && mkdir -p /home/pptruser/Downloads \ + && chown -R pptruser:pptruser /home/pptruser + +# Run everything after as non-privileged user. +USER pptruser diff --git a/remote/test/puppeteer/.editorconfig b/remote/test/puppeteer/.editorconfig new file mode 100644 index 0000000000..c6c8b36219 --- /dev/null +++ b/remote/test/puppeteer/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/remote/test/puppeteer/.eslintignore b/remote/test/puppeteer/.eslintignore new file mode 100644 index 0000000000..f4c17fcc0f --- /dev/null +++ b/remote/test/puppeteer/.eslintignore @@ -0,0 +1,14 @@ +test/assets/modernizr.js +third_party/* +utils/browser/puppeteer-web.js +utils/doclint/check_public_api/test/ +node6/* +node6-test/* +experimental/ +lib/ +/index.d.ts +# We ignore this file because it uses ES imports which we don't yet use +# in the Puppeteer src, so it trips up the ESLint-TypeScript parser. +utils/doclint/generate_types/test/test.ts +vendor/ +web-test-runner.config.mjs diff --git a/remote/test/puppeteer/.eslintrc.js b/remote/test/puppeteer/.eslintrc.js new file mode 100644 index 0000000000..a1de9853ee --- /dev/null +++ b/remote/test/puppeteer/.eslintrc.js @@ -0,0 +1,183 @@ +module.exports = { + root: true, + env: { + node: true, + es6: true, + }, + + parser: '@typescript-eslint/parser', + + plugins: ['mocha', '@typescript-eslint', 'unicorn', 'import'], + + extends: ['plugin:prettier/recommended'], + + rules: { + // Error if files are not formatted with Prettier correctly. + 'prettier/prettier': 2, + // syntax preferences + quotes: [ + 2, + 'single', + { + avoidEscape: true, + allowTemplateLiterals: true, + }, + ], + 'spaced-comment': [ + 2, + 'always', + { + markers: ['*'], + }, + ], + eqeqeq: [2], + 'accessor-pairs': [ + 2, + { + getWithoutSet: false, + setWithoutGet: false, + }, + ], + 'new-parens': 2, + 'func-call-spacing': 2, + 'prefer-const': 2, + + 'max-len': [ + 2, + { + /* this setting doesn't impact things as we use Prettier to format + * our code and hence dictate the line length. + * Prettier aims for 80 but sometimes makes the decision to go just + * over 80 chars as it decides that's better than wrapping. ESLint's + * rule defaults to 80 but therefore conflicts with Prettier. So we + * set it to something far higher than Prettier would allow to avoid + * it causing issues and conflicting with Prettier. + */ + code: 200, + comments: 90, + ignoreTemplateLiterals: true, + ignoreUrls: true, + ignoreStrings: true, + ignoreRegExpLiterals: true, + }, + ], + // anti-patterns + 'no-var': 2, + 'no-with': 2, + 'no-multi-str': 2, + 'no-caller': 2, + 'no-implied-eval': 2, + 'no-labels': 2, + 'no-new-object': 2, + 'no-octal-escape': 2, + 'no-self-compare': 2, + 'no-shadow-restricted-names': 2, + 'no-cond-assign': 2, + 'no-debugger': 2, + 'no-dupe-keys': 2, + 'no-duplicate-case': 2, + 'no-empty-character-class': 2, + 'no-unreachable': 2, + 'no-unsafe-negation': 2, + radix: 2, + 'valid-typeof': 2, + 'no-unused-vars': [ + 2, + { + args: 'none', + vars: 'local', + varsIgnorePattern: + '([fx]?describe|[fx]?it|beforeAll|beforeEach|afterAll|afterEach)', + }, + ], + 'no-implicit-globals': [2], + + // es2015 features + 'require-yield': 2, + 'template-curly-spacing': [2, 'never'], + + // ensure we don't have any it.only or describe.only in prod + 'mocha/no-exclusive-tests': 'error', + + // enforce the variable in a catch block is named error + 'unicorn/catch-error-name': 'error', + + 'no-restricted-imports': [ + 'error', + { + patterns: ['*Events'], + paths: [ + { + name: 'mitt', + message: + 'Import Mitt from the vendored location: vendor/mitt/src/index.js', + }, + ], + }, + ], + 'import/extensions': ['error', 'ignorePackages'], + }, + overrides: [ + { + files: ['*.ts'], + extends: [ + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + ], + rules: { + 'no-unused-vars': 0, + '@typescript-eslint/no-unused-vars': 2, + 'func-call-spacing': 0, + '@typescript-eslint/func-call-spacing': 2, + semi: 0, + '@typescript-eslint/semi': 2, + '@typescript-eslint/no-empty-function': 0, + '@typescript-eslint/no-use-before-define': 0, + // We have to use any on some types so the warning isn't valuable. + '@typescript-eslint/no-explicit-any': 0, + // We don't require explicit return types on basic functions or + // dummy functions in tests, for example + '@typescript-eslint/explicit-function-return-type': 0, + // We know it's bad and use it very sparingly but it's needed :( + '@typescript-eslint/ban-ts-ignore': 0, + /** + * This is the default options (as per + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/ban-types.md), + * + * Unfortunately there's no way to + */ + '@typescript-eslint/ban-types': [ + 'error', + { + extendDefaults: true, + types: { + /* + * Puppeteer's API accepts generic functions in many places so it's + * not a useful linting rule to ban the `Function` type. This turns off + * the banning of the `Function` type which is a default rule. + */ + Function: false, + }, + }, + ], + '@typescript-eslint/array-type': [ + 2, + { + default: 'array-simple', + }, + ], + }, + }, + { + files: ['test-browser/**/*.js'], + parserOptions: { + sourceType: 'module', + }, + env: { + es6: true, + browser: true, + es2020: true, + }, + }, + ], +}; diff --git a/remote/test/puppeteer/.github/workflows/publish-on-tag.yml b/remote/test/puppeteer/.github/workflows/publish-on-tag.yml new file mode 100644 index 0000000000..b16c1be3fb --- /dev/null +++ b/remote/test/puppeteer/.github/workflows/publish-on-tag.yml @@ -0,0 +1,30 @@ +name: publish-on-tag + +on: + push: + tags: + - '*' + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install dependencies + run: npm install + - name: Build + run: npm run build + - name: Publish puppeteer + env: + NPM_TOKEN: ${{secrets.NPM_TOKEN_PUPPETEER}} + run: | + npm config set '//wombat-dressing-room.appspot.com/:_authToken' '${NPM_TOKEN}' + npm publish + - name: Publish puppeteer-core + env: + NPM_TOKEN: ${{secrets.NPM_TOKEN_PUPPETEER_CORE}} + run: | + utils/prepare_puppeteer_core.js + npm config set '//wombat-dressing-room.appspot.com/:_authToken' '${NPM_TOKEN}' + npm publish diff --git a/remote/test/puppeteer/.npmrc b/remote/test/puppeteer/.npmrc new file mode 100644 index 0000000000..f4335b67c1 --- /dev/null +++ b/remote/test/puppeteer/.npmrc @@ -0,0 +1,2 @@ +registry=https://wombat-dressing-room.appspot.com/ +access=public diff --git a/remote/test/puppeteer/.travis.yml b/remote/test/puppeteer/.travis.yml new file mode 100644 index 0000000000..3dccb9aced --- /dev/null +++ b/remote/test/puppeteer/.travis.yml @@ -0,0 +1,118 @@ +language: node_js +services: xvfb + +# Throughout this file, the following `node_js` versions are being used: +# +# - node_js: '10' # The maintenance LTS version. +# - node_js: '12' # The oldest major active LTS version. +# - node_js: '14' # The newest major active LTS version. + +jobs: + include: + - os: 'osx' + name: 'Unit tests: macOS/Chromium' + node_js: '10' # The maintenance LTS version. + osx_image: xcode11.4 + env: + - CHROMIUM=true + before_install: + - PUPPETEER_PRODUCT=firefox npm install + script: + - ls .local-chromium .local-firefox + - npm run tsc + - npm run unit + + - os: 'windows' + name: 'Unit tests: Windows/Chromium' + node_js: '10' # The maintenance LTS version. + env: + - CHROMIUM=true + before_install: + - PUPPETEER_PRODUCT=firefox npm install + script: + - ls .local-chromium .local-firefox + - npm run tsc + - travis_retry npm run unit + + # Node <10.17's fs.promises module was experimental and doesn't behave as + # expected. This problem was fixed in Node 10.19, but we run the unit tests + # through on 10.15 to make sure we don't cause any regressions when using + # fs.promises. See https://github.com/puppeteer/puppeteer/issues/6548 for an + # example. + - node_js: '10.15.0' + name: 'Node 10.15 Unit tests: Linux/Chromium' + env: + - CHROMIUM=true + before_install: + - PUPPETEER_PRODUCT=firefox npm install + script: + - npm run unit + + - node_js: '10' # The maintenance LTS version. + name: 'Unit tests [with coverage]: Linux/Chromium' + env: + - CHROMIUM=true + before_install: + - PUPPETEER_PRODUCT=firefox npm install + script: + - travis_retry npm run unit-with-coverage + - npm run assert-unit-coverage + + - node_js: '12' # The oldest major active LTS version. + name: 'Unit tests [Node 12]: Linux/Chromium' + env: + - CHROMIUM=true + before_install: + - PUPPETEER_PRODUCT=firefox npm install + script: + - npm run unit + + - node_js: '14' # The newest major active LTS version. + name: 'Unit tests [Node 14]: Linux/Chromium' + env: + - CHROMIUM=true + before_install: + - PUPPETEER_PRODUCT=firefox npm install + script: + - npm run unit + + - node_js: '12' # The oldest major active LTS version. + name: 'Browser tests: Linux/Chromium' + addons: + chrome: stable + env: + - CHROMIUM=true + script: + - npm run test-browser + + # This bot runs all the extra checks that aren't the main Puppeteer unit tests. + - node_js: '10' # The maintenance LTS version. + name: 'Extra tests: Linux/Chromium' + env: + - CHROMIUM=true + script: + - npm run lint + # Ensure that we can generate the new docs without erroring + - npm run generate-docs + - npm run ensure-correct-devtools-protocol-revision + + # This bot runs separately as it changes package.json to test puppeteer-core + # and we don't want that leaking into other bots and causing issues. + - node_js: '10' # The maintenance LTS version. + name: 'Test bundling and install of packages' + env: + - CHROMIUM=true + script: + - npm run test-install + + - node_js: '10' # The maintenance LTS version. + name: 'Unit tests: Linux/Firefox' + env: + - FIREFOX=true + before_install: + - PUPPETEER_PRODUCT=firefox npm install + script: + - npm run funit + +notifications: + email: false diff --git a/remote/test/puppeteer/.versionrc b/remote/test/puppeteer/.versionrc new file mode 100644 index 0000000000..a0247ed4ab --- /dev/null +++ b/remote/test/puppeteer/.versionrc @@ -0,0 +1,3 @@ +{ + "releaseCommitMessageFormat": "chore(release): mark v{{currentTag}}" +} diff --git a/remote/test/puppeteer/CHANGELOG.md b/remote/test/puppeteer/CHANGELOG.md new file mode 100644 index 0000000000..35e3a06ddb --- /dev/null +++ b/remote/test/puppeteer/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + +## [5.5.0](https://github.com/puppeteer/puppeteer/compare/v5.4.1...v5.5.0) (2020-11-16) + + +### Features + +* **chromium:** roll Chromium to r818858 ([#6526](https://github.com/puppeteer/puppeteer/issues/6526)) ([b549256](https://github.com/puppeteer/puppeteer/commit/b54925695200cad32f470f8eb407259606447a85)) + + +### Bug Fixes + +* **common:** fix generic type of `_isClosedPromise` ([#6579](https://github.com/puppeteer/puppeteer/issues/6579)) ([122f074](https://github.com/puppeteer/puppeteer/commit/122f074f92f47a7b9aa08091851e51a07632d23b)) +* **domworld:** fix missing binding for waittasks ([#6562](https://github.com/puppeteer/puppeteer/issues/6562)) ([67da1cf](https://github.com/puppeteer/puppeteer/commit/67da1cf866703f5f581c9cce4923697ac38129ef)) + +# Changelog + +All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. diff --git a/remote/test/puppeteer/CONTRIBUTING.md b/remote/test/puppeteer/CONTRIBUTING.md new file mode 100644 index 0000000000..54bfb306ed --- /dev/null +++ b/remote/test/puppeteer/CONTRIBUTING.md @@ -0,0 +1,290 @@ +<!-- gen:toc --> +- [How to Contribute](#how-to-contribute) + * [Contributor License Agreement](#contributor-license-agreement) + * [Getting Code](#getting-code) + * [Code reviews](#code-reviews) + * [Code Style](#code-style) + * [TypeScript guidelines](#typescript-guidelines) + * [Project structure and TypeScript compilation](#project-structure-and-typescript-compilation) + - [Shipping CJS and ESM bundles](#shipping-cjs-and-esm-bundles) + - [tsconfig for the tests](#tsconfig-for-the-tests) + - [Root `tsconfig.json`](#root-tsconfigjson) + * [API guidelines](#api-guidelines) + * [Commit Messages](#commit-messages) + * [Writing Documentation](#writing-documentation) + * [Adding New Dependencies](#adding-new-dependencies) + * [Running & Writing Tests](#running--writing-tests) + * [Public API Coverage](#public-api-coverage) + * [Debugging Puppeteer](#debugging-puppeteer) +- [For Project Maintainers](#for-project-maintainers) + * [Rolling new Chromium version](#rolling-new-chromium-version) + - [Bisecting upstream changes](#bisecting-upstream-changes) + * [Releasing to npm](#releasing-to-npm) +<!-- gen:stop --> + +# How to Contribute + +First of all, thank you for your interest in Puppeteer! +We'd love to accept your patches and contributions! + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution, +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to <https://cla.developers.google.com/> to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Getting Code + +1. Clone this repository + +```bash +git clone https://github.com/puppeteer/puppeteer +cd puppeteer +``` + +2. Install dependencies + +```bash +npm install +``` + +3. Run Puppeteer tests locally. For more information about tests, read [Running & Writing Tests](#running--writing-tests). + +```bash +npm run unit +``` + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +## Code Style + +- Coding style is fully defined in [`.eslintrc`](https://github.com/puppeteer/puppeteer/blob/main/.eslintrc.js) and we automatically format our code with [Prettier](https://prettier.io). +- It's recommended to set-up Prettier into your editor, or you can run `npm run eslint-fix` to automatically format any files. +- If you're working in a JS file, code should be annotated with [closure annotations](https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler). +- If you're working in a TS file, you should explicitly type all variables and return types. You'll get ESLint warnings if you don't so if you're not sure use them as guidelines, and feel free to ask us for help! + +To run ESLint, use: + +```bash +npm run eslint +``` + +You can check your code (both JS & TS) type-checks by running: + +```bash +npm run tsc +``` + +## TypeScript guidelines + +- Try to avoid the use of `any` when possible. Consider `unknown` as a better alternative. You are able to use `any` if needbe, but it will generate an ESLint warning. + +## Project structure and TypeScript compilation + +The code in Puppeteer is split primarily into two folders: + +- `src` contains all source code +- `vendor` contains all dependencies that we've vendored into the codebase. See the [`vendor/README.md`](https://github.com/puppeteer/puppeteer/blob/main/vendor/README.md) for details. + +We structure these using TypeScript's project references, which lets us treat each folder like a standalone TypeScript project. + +### Shipping CJS and ESM bundles + +Currently Puppeteer ships two bundles; a CommonJS version for Node and an ESM bundle for the browser. Therefore we maintain two `tsconfig` files for each project; `tsconfig.esm.json` and `tsconfig.cjs.json`. At build time we compile twice, once outputting to CJS and another time to output to ESM. + +We compile into the `lib` directory which is what we publish on the npm repository and it's structured like so: + +``` +lib +- cjs + - puppeteer <== the output of compiling `src/tsconfig.cjs.json` + - vendor <== the output of compiling `vendor/tsconfig.cjs.json` +- esm + - puppeteer <== the output of compiling `src/tsconfig.esm.json` + - vendor <== the output of compiling `vendor/tsconfig.esm.json` +``` + +The main entry point for the Node module Puppeteer is `cjs-entry.js`. This imports `lib/cjs/puppeteer/index.js` and exposes it to Node users. + +### tsconfig for the tests + +We also maintain `test/tsconfig.test.json`. This is **only used to compile the unit test `*.spec.ts` files**. When the tests are run, we first compile Puppeteer as normal before running the unit tests **against the compiled output**. Doing this lets the test run against the compiled code we ship to users so it gives us more confidence in our compiled output being correct. + +### Root `tsconfig.json` + +The root `tsconfig.json` exists for the API Extractor; it has to find a `tsconfig.json` in the project's root directory. It is _not_ used for anything else. + + +## API guidelines + +When authoring new API methods, consider the following: + +- Expose as little information as needed. When in doubt, don’t expose new information. +- Methods are used in favor of getters/setters. + - The only exception is namespaces, e.g. `page.keyboard` and `page.coverage` +- All string literals must be small case. This includes event names and option values. +- Avoid adding "sugar" API (API that is trivially implementable in user-space) unless they're **very** demanded. + +## Commit Messages + +Commit messages should follow [the Conventional Commits format](https://www.conventionalcommits.org/en/v1.0.0/#summary). This is enforced via `npm run lint`. + +In particular, breaking changes should clearly be noted as “BREAKING CHANGE:” in the commit message footer. Example: + +``` +fix(page): fix page.pizza method + +This patch fixes page.pizza so that it works with iframes. + +Issues: #123, #234 + +BREAKING CHANGE: page.pizza now delivers pizza at home by default. +To deliver to a different location, use the "deliver" option: + `page.pizza({deliver: 'work'})`. +``` + +## Writing Documentation + +All public API should have a descriptive entry in [`docs/api.md`](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md). There's a [documentation linter](https://github.com/puppeteer/puppeteer/tree/main/utils/doclint) which makes sure documentation is aligned with the codebase. + +To run the documentation linter, use: + +```bash +npm run doc +``` + +## Adding New Dependencies + +For all dependencies (both installation and development): +- **Do not add** a dependency if the desired functionality is easily implementable. +- If adding a dependency, it should be well-maintained and trustworthy. + +A barrier for introducing new installation dependencies is especially high: +- **Do not add** installation dependency unless it's critical to project success. + +There are additional considerations for dependencies that are environment agonistic. See the [`vendor/README.md`](https://github.com/puppeteer/puppeteer/blob/main/vendor/README.md) for details. + + +## Running & Writing Tests + +- Every feature should be accompanied by a test. +- Every public api event/method should be accompanied by a test. +- Tests should not depend on external services. +- Tests should work on all three platforms: Mac, Linux and Win. This is especially important for screenshot tests. + +Puppeteer tests are located in the test directory ([`test`](https://github.com/puppeteer/puppeteer/blob/main/test/) and are written using Mocha. See [`test/README.md`](https://github.com/puppeteer/puppeteer/blob/main/test/) for more details. + +Despite being named 'unit', these are integration tests, making sure public API methods and events work as expected. + +- To run all tests: + +```bash +npm run unit +``` + +- To run a specific test, substitute the `it` with `it.only`: + +```js + ... + it.only('should work', async function({server, page}) { + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok).toBe(true); + }); +``` + +- To disable a specific test, substitute the `it` with `xit` (mnemonic rule: '*cross it*'): + +```js + ... + // Using "xit" to skip specific test + xit('should work', async function({server, page}) { + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok).toBe(true); + }); +``` + +- To run tests in non-headless mode: + +```bash +HEADLESS=false npm run unit +``` + +- To run Firefox tests, firstly ensure you have Firefox installed locally (you only need to do this once, not on every test run) and then you can run the tests: + +```bash +PUPPETEER_PRODUCT=firefox node install.js +PUPPETEER_PRODUCT=firefox npm run unit +``` + +- To run tests with custom browser executable: + +```bash +BINARY=<path-to-executable> npm run unit +``` + +## Public API Coverage + +Every public API method or event should be called at least once in tests. To ensure this, there's a `coverage` command which tracks calls to public API and reports back if some methods/events were not called. + +Run coverage: + +```bash +npm run coverage +``` + +## Debugging Puppeteer + +See [Debugging Tips](README.md#debugging-tips) in the readme. + +# For Project Maintainers + +## Rolling new Chromium version + +The following steps are needed to update the Chromium version. + +1. Find a suitable Chromium revision + Not all revisions have builds for all platforms, so we need to find one that does. + To do so, run `utils/check_availability.js -rb` to find the latest suitable beta Chromium revision (see `utils/check_availability.js -help` for more options). +1. Update `src/revisions.ts` with the found revision number. +1. Run `npm run ensure-correct-devtools-protocol-revision`. + If it fails, update `package.json` with the expected `devtools-protocol` version. +1. Run `npm run tsc` and `npm install` and ensure that all tests pass. If a test fails, [bisect](#bisecting-upstream-changes) the upstream cause of the failure, and either update the test expectations accordingly (if it was an intended change) or work around the changes in Puppeteer (if it’s not desirable to change Puppeteer’s observable behavior). +1. Update `versions.js` with the new Chromium-to-Puppeteer version mapping. +1. Commit and push your changes and open a pull request. + +### Bisecting upstream changes + +Sometimes, performing a Chromium roll causes tests to fail. To figure out the cause, you need to bisect Chromium revisions to figure out the earliest possible revision that changed the behavior. The script in `utils/bisect.js` can be helpful here. Given a Node.js script that calls `process.exit(1)` for bad revisions, run this from the Puppeteer repository’s root directory: + +```sh +node utils/bisect.js --good 686378 --bad 706915 script.js +``` + +## Releasing to npm + +Releasing to npm consists of the following phases: + +1. Source Code: mark a release. + 1. Run `npm run release` to bump the version number in `package.json` and populate the changelog. + 1. Run `npm run doc` to update the docs accordingly. + 1. Send a PR titled `'chore(release): mark vXXX.YYY.ZZZ'` ([example](https://github.com/puppeteer/puppeteer/pull/5078)). + 1. Make sure the PR passes **all checks**. + - **WHY**: there are linters in place that help to avoid unnecessary errors, e.g. [like this](https://github.com/puppeteer/puppeteer/pull/2446) + 1. Merge the PR. + 1. Once merged, publish the release notes from `CHANGELOG.md` using [GitHub’s “draft new release tag” option](https://github.com/puppeteer/puppeteer/releases/new). + - **NOTE**: tag names are prefixed with `'v'`, e.g. for version `1.4.0` the tag is `v1.4.0`. + 1. As soon as the Git tag is created by completing the previous step, our CI automatically `npm publish`es the new releases for both the `puppeteer` and `puppeteer-core` packages. +1. Source Code: mark post-release. + 1. Bump `package.json` version to the `-post` version, run `npm run doc` to update the “released APIs” section at the top of `docs/api.md` accordingly, and send a PR titled `'chore: bump version to vXXX.YYY.ZZZ-post'` ([example](https://github.com/puppeteer/puppeteer/commit/d02440d1eac98028e29f4e1cf55413062a259156)) + - **NOTE**: no other commits should be landed in-between release commit and bump commit. diff --git a/remote/test/puppeteer/LICENSE b/remote/test/puppeteer/LICENSE new file mode 100644 index 0000000000..d2c171df74 --- /dev/null +++ b/remote/test/puppeteer/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/remote/test/puppeteer/README.md b/remote/test/puppeteer/README.md new file mode 100644 index 0000000000..d50b8245e0 --- /dev/null +++ b/remote/test/puppeteer/README.md @@ -0,0 +1,453 @@ +# Puppeteer + +<!-- [START badges] --> +[![Build status](https://img.shields.io/travis/com/puppeteer/puppeteer/main.svg)](https://travis-ci.com/puppeteer/puppeteer) [![npm puppeteer package](https://img.shields.io/npm/v/puppeteer.svg)](https://npmjs.org/package/puppeteer) [![Issue resolution status](https://isitmaintained.com/badge/resolution/puppeteer/puppeteer.svg)](https://github.com/puppeteer/puppeteer/issues) +<!-- [END badges] --> + +<img src="https://user-images.githubusercontent.com/10379601/29446482-04f7036a-841f-11e7-9872-91d1fc2ea683.png" height="200" align="right"> + +###### [API](https://github.com/puppeteer/puppeteer/blob/v5.5.0/docs/api.md) | [FAQ](#faq) | [Contributing](https://github.com/puppeteer/puppeteer/blob/main/CONTRIBUTING.md) | [Troubleshooting](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md) + +> Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). Puppeteer runs [headless](https://developers.google.com/web/updates/2017/04/headless-chrome) by default, but can be configured to run full (non-headless) Chrome or Chromium. + +<!-- [START usecases] --> +###### What can I do? + +Most things that you can do manually in the browser can be done using Puppeteer! Here are a few examples to get you started: + +* Generate screenshots and PDFs of pages. +* Crawl a SPA (Single-Page Application) and generate pre-rendered content (i.e. "SSR" (Server-Side Rendering)). +* Automate form submission, UI testing, keyboard input, etc. +* Create an up-to-date, automated testing environment. Run your tests directly in the latest version of Chrome using the latest JavaScript and browser features. +* Capture a [timeline trace](https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference) of your site to help diagnose performance issues. +* Test Chrome Extensions. +<!-- [END usecases] --> + +Give it a spin: https://try-puppeteer.appspot.com/ + +<!-- [START getstarted] --> +## Getting Started + +### Installation + +To use Puppeteer in your project, run: + +```bash +npm i puppeteer +# or "yarn add puppeteer" +``` + +Note: When you install Puppeteer, it downloads a recent version of Chromium (~170MB Mac, ~282MB Linux, ~280MB Win) that is guaranteed to work with the API. To skip the download, or to download a different browser, see [Environment variables](https://github.com/puppeteer/puppeteer/blob/v5.5.0/docs/api.md#environment-variables). + + +### puppeteer-core + +Since version 1.7.0 we publish the [`puppeteer-core`](https://www.npmjs.com/package/puppeteer-core) package, +a version of Puppeteer that doesn't download any browser by default. + +```bash +npm i puppeteer-core +# or "yarn add puppeteer-core" +``` + +`puppeteer-core` is intended to be a lightweight version of Puppeteer for launching an existing browser installation or for connecting to a remote one. Be sure that the version of puppeteer-core you install is compatible with the +browser you intend to connect to. + +See [puppeteer vs puppeteer-core](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#puppeteer-vs-puppeteer-core). + +### Usage + +Puppeteer follows the latest [maintenance LTS](https://github.com/nodejs/Release#release-schedule) version of Node. + +Note: Prior to v1.18.1, Puppeteer required at least Node v6.4.0. Versions from v1.18.1 to v2.1.0 rely on +Node 8.9.0+. Starting from v3.0.0 Puppeteer starts to rely on Node 10.18.1+. All examples below use async/await which is only supported in Node v7.6.0 or greater. + +Puppeteer will be familiar to people using other browser testing frameworks. You create an instance +of `Browser`, open pages, and then manipulate them with [Puppeteer's API](https://github.com/puppeteer/puppeteer/blob/v5.5.0/docs/api.md#). + +**Example** - navigating to https://example.com and saving a screenshot as *example.png*: + +Save file as **example.js** + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + await page.screenshot({path: 'example.png'}); + + await browser.close(); +})(); +``` + +Execute script on the command line + +```bash +node example.js +``` + +Puppeteer sets an initial page size to 800×600px, which defines the screenshot size. The page size can be customized with [`Page.setViewport()`](https://github.com/puppeteer/puppeteer/blob/v5.5.0/docs/api.md#pagesetviewportviewport). + +**Example** - create a PDF. + +Save file as **hn.js** + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://news.ycombinator.com', {waitUntil: 'networkidle2'}); + await page.pdf({path: 'hn.pdf', format: 'A4'}); + + await browser.close(); +})(); +``` + +Execute script on the command line + +```bash +node hn.js +``` + +See [`Page.pdf()`](https://github.com/puppeteer/puppeteer/blob/v5.5.0/docs/api.md#pagepdfoptions) for more information about creating pdfs. + +**Example** - evaluate script in the context of the page + +Save file as **get-dimensions.js** + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + + // Get the "viewport" of the page, as reported by the page. + const dimensions = await page.evaluate(() => { + return { + width: document.documentElement.clientWidth, + height: document.documentElement.clientHeight, + deviceScaleFactor: window.devicePixelRatio + }; + }); + + console.log('Dimensions:', dimensions); + + await browser.close(); +})(); +``` + +Execute script on the command line + +```bash +node get-dimensions.js +``` + +See [`Page.evaluate()`](https://github.com/puppeteer/puppeteer/blob/v5.5.0/docs/api.md#pageevaluatepagefunction-args) for more information on `evaluate` and related methods like `evaluateOnNewDocument` and `exposeFunction`. + +<!-- [END getstarted] --> + +<!-- [START runtimesettings] --> +## Default runtime settings + +**1. Uses Headless mode** + +Puppeteer launches Chromium in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). To launch a full version of Chromium, set the [`headless` option](https://github.com/puppeteer/puppeteer/blob/v5.5.0/docs/api.md#puppeteerlaunchoptions) when launching a browser: + +```js +const browser = await puppeteer.launch({headless: false}); // default is true +``` + +**2. Runs a bundled version of Chromium** + +By default, Puppeteer downloads and uses a specific version of Chromium so its API +is guaranteed to work out of the box. To use Puppeteer with a different version of Chrome or Chromium, +pass in the executable's path when creating a `Browser` instance: + +```js +const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'}); +``` + +You can also use Puppeteer with Firefox Nightly (experimental support). See [`Puppeteer.launch()`](https://github.com/puppeteer/puppeteer/blob/v5.5.0/docs/api.md#puppeteerlaunchoptions) for more information. + +See [`this article`](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for a description of the differences between Chromium and Chrome. [`This article`](https://chromium.googlesource.com/chromium/src/+/master/docs/chromium_browser_vs_google_chrome.md) describes some differences for Linux users. + +**3. Creates a fresh user profile** + +Puppeteer creates its own browser user profile which it **cleans up on every run**. + +<!-- [END runtimesettings] --> + +## Resources + +- [API Documentation](https://github.com/puppeteer/puppeteer/blob/v5.5.0/docs/api.md) +- [Examples](https://github.com/puppeteer/puppeteer/tree/main/examples/) +- [Community list of Puppeteer resources](https://github.com/transitive-bullshit/awesome-puppeteer) + + +<!-- [START debugging] --> + +## Debugging tips + +1. Turn off headless mode - sometimes it's useful to see what the browser is + displaying. Instead of launching in headless mode, launch a full version of + the browser using `headless: false`: + + ```js + const browser = await puppeteer.launch({headless: false}); + ``` + +2. Slow it down - the `slowMo` option slows down Puppeteer operations by the + specified amount of milliseconds. It's another way to help see what's going on. + + ```js + const browser = await puppeteer.launch({ + headless: false, + slowMo: 250 // slow down by 250ms + }); + ``` + +3. Capture console output - You can listen for the `console` event. + This is also handy when debugging code in `page.evaluate()`: + + ```js + page.on('console', msg => console.log('PAGE LOG:', msg.text())); + + await page.evaluate(() => console.log(`url is ${location.href}`)); + ``` + +4. Use debugger in application code browser + + There are two execution context: node.js that is running test code, and the browser + running application code being tested. This lets you debug code in the + application code browser; ie code inside `evaluate()`. + + - Use `{devtools: true}` when launching Puppeteer: + + ```js + const browser = await puppeteer.launch({devtools: true}); + ``` + + - Change default test timeout: + + jest: `jest.setTimeout(100000);` + + jasmine: `jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000;` + + mocha: `this.timeout(100000);` (don't forget to change test to use [function and not '=>'](https://stackoverflow.com/a/23492442)) + + - Add an evaluate statement with `debugger` inside / add `debugger` to an existing evaluate statement: + + ```js + await page.evaluate(() => {debugger;}); + ``` + + The test will now stop executing in the above evaluate statement, and chromium will stop in debug mode. + +5. Use debugger in node.js + + This will let you debug test code. For example, you can step over `await page.click()` in the node.js script and see the click happen in the application code browser. + + Note that you won't be able to run `await page.click()` in + DevTools console due to this [Chromium bug](https://bugs.chromium.org/p/chromium/issues/detail?id=833928). So if + you want to try something out, you have to add it to your test file. + + - Add `debugger;` to your test, eg: + + ```js + debugger; + await page.click('a[target=_blank]'); + ``` + + - Set `headless` to `false` + - Run `node --inspect-brk`, eg `node --inspect-brk node_modules/.bin/jest tests` + - In Chrome open `chrome://inspect/#devices` and click `inspect` + - In the newly opened test browser, type `F8` to resume test execution + - Now your `debugger` will be hit and you can debug in the test browser + + +6. Enable verbose logging - internal DevTools protocol traffic + will be logged via the [`debug`](https://github.com/visionmedia/debug) module under the `puppeteer` namespace. + + # Basic verbose logging + env DEBUG="puppeteer:*" node script.js + + # Protocol traffic can be rather noisy. This example filters out all Network domain messages + env DEBUG="puppeteer:*" env DEBUG_COLORS=true node script.js 2>&1 | grep -v '"Network' + +7. Debug your Puppeteer (node) code easily, using [ndb](https://github.com/GoogleChromeLabs/ndb) + + - `npm install -g ndb` (or even better, use [npx](https://github.com/zkat/npx)!) + + - add a `debugger` to your Puppeteer (node) code + + - add `ndb` (or `npx ndb`) before your test command. For example: + + `ndb jest` or `ndb mocha` (or `npx ndb jest` / `npx ndb mocha`) + + - debug your test inside chromium like a boss! + +<!-- [END debugging] --> + + +<!-- [START typescript] --> +## Usage with TypeScript + +We have recently completed a migration to move the Puppeteer source code from JavaScript to TypeScript and we're currently working on shipping type definitions for TypeScript with Puppeteer's npm package. + +Until this work is complete we recommend installing the Puppeteer type definitions from the [DefinitelyTyped](https://definitelytyped.org/) repository: + +```bash +npm install --save-dev @types/puppeteer +``` + +The types that you'll see appearing in the Puppeteer source code are based off the great work of those who have contributed to the `@types/puppeteer` package. We really appreciate the hard work those people put in to providing high quality TypeScript definitions for Puppeteer's users. + +<!-- [END typescript] --> + + +## Contributing to Puppeteer + +Check out [contributing guide](https://github.com/puppeteer/puppeteer/blob/main/CONTRIBUTING.md) to get an overview of Puppeteer development. + +<!-- [START faq] --> + +# FAQ + +#### Q: Who maintains Puppeteer? + +The Chrome DevTools team maintains the library, but we'd love your help and expertise on the project! +See [Contributing](https://github.com/puppeteer/puppeteer/blob/main/CONTRIBUTING.md). + +#### Q: What is the status of cross-browser support? + +Official Firefox support is currently experimental. The ongoing collaboration with Mozilla aims to support common end-to-end testing use cases, for which developers expect cross-browser coverage. The Puppeteer team needs input from users to stabilize Firefox support and to bring missing APIs to our attention. + +From Puppeteer v2.1.0 onwards you can specify [`puppeteer.launch({product: 'firefox'})`](https://github.com/puppeteer/puppeteer/blob/v5.5.0/docs/api.md#puppeteerlaunchoptions) to run your Puppeteer scripts in Firefox Nightly, without any additional custom patches. While [an older experiment](https://www.npmjs.com/package/puppeteer-firefox) required a patched version of Firefox, [the current approach](https://wiki.mozilla.org/Remote) works with “stock” Firefox. + +We will continue to collaborate with other browser vendors to bring Puppeteer support to browsers such as Safari. +This effort includes exploration of a standard for executing cross-browser commands (instead of relying on the non-standard DevTools Protocol used by Chrome). + +#### Q: What are Puppeteer’s goals and principles? + +The goals of the project are: + +- Provide a slim, canonical library that highlights the capabilities of the [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). +- Provide a reference implementation for similar testing libraries. Eventually, these other frameworks could adopt Puppeteer as their foundational layer. +- Grow the adoption of headless/automated browser testing. +- Help dogfood new DevTools Protocol features...and catch bugs! +- Learn more about the pain points of automated browser testing and help fill those gaps. + +We adapt [Chromium principles](https://www.chromium.org/developers/core-principles) to help us drive product decisions: +- **Speed**: Puppeteer has almost zero performance overhead over an automated page. +- **Security**: Puppeteer operates off-process with respect to Chromium, making it safe to automate potentially malicious pages. +- **Stability**: Puppeteer should not be flaky and should not leak memory. +- **Simplicity**: Puppeteer provides a high-level API that’s easy to use, understand, and debug. + +#### Q: Is Puppeteer replacing Selenium/WebDriver? + +**No**. Both projects are valuable for very different reasons: +- Selenium/WebDriver focuses on cross-browser automation; its value proposition is a single standard API that works across all major browsers. +- Puppeteer focuses on Chromium; its value proposition is richer functionality and higher reliability. + +That said, you **can** use Puppeteer to run tests against Chromium, e.g. using the community-driven [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer). While this probably shouldn’t be your only testing solution, it does have a few good points compared to WebDriver: + +- Puppeteer requires zero setup and comes bundled with the Chromium version it works best with, making it [very easy to start with](https://github.com/puppeteer/puppeteer/#getting-started). At the end of the day, it’s better to have a few tests running chromium-only, than no tests at all. +- Puppeteer has event-driven architecture, which removes a lot of potential flakiness. There’s no need for evil “sleep(1000)” calls in puppeteer scripts. +- Puppeteer runs headless by default, which makes it fast to run. Puppeteer v1.5.0 also exposes browser contexts, making it possible to efficiently parallelize test execution. +- Puppeteer shines when it comes to debugging: flip the “headless” bit to false, add “slowMo”, and you’ll see what the browser is doing. You can even open Chrome DevTools to inspect the test environment. + +#### Q: Why doesn’t Puppeteer v.XXX work with Chromium v.YYY? + +We see Puppeteer as an **indivisible entity** with Chromium. Each version of Puppeteer bundles a specific version of Chromium – **the only** version it is guaranteed to work with. + +This is not an artificial constraint: A lot of work on Puppeteer is actually taking place in the Chromium repository. Here’s a typical story: +- A Puppeteer bug is reported: https://github.com/puppeteer/puppeteer/issues/2709 +- It turned out this is an issue with the DevTools protocol, so we’re fixing it in Chromium: https://chromium-review.googlesource.com/c/chromium/src/+/1102154 +- Once the upstream fix is landed, we roll updated Chromium into Puppeteer: https://github.com/puppeteer/puppeteer/pull/2769 + +However, oftentimes it is desirable to use Puppeteer with the official Google Chrome rather than Chromium. For this to work, you should install a `puppeteer-core` version that corresponds to the Chrome version. + +For example, in order to drive Chrome 71 with puppeteer-core, use `chrome-71` npm tag: +```bash +npm install puppeteer-core@chrome-71 +``` + +#### Q: Which Chromium version does Puppeteer use? + +Look for the `chromium` entry in [revisions.ts](https://github.com/puppeteer/puppeteer/blob/main/src/revisions.ts). To find the corresponding Chromium commit and version number, search for the revision prefixed by an `r` in [OmahaProxy](https://omahaproxy.appspot.com/)'s "Find Releases" section. + + +#### Q: Which Firefox version does Puppeteer use? + +Since Firefox support is experimental, Puppeteer downloads the latest [Firefox Nightly](https://wiki.mozilla.org/Nightly) when the `PUPPETEER_PRODUCT` environment variable is set to `firefox`. That's also why the value of `firefox` in [revisions.ts](https://github.com/puppeteer/puppeteer/blob/main/src/revisions.ts) is `latest` -- Puppeteer isn't tied to a particular Firefox version. + +To fetch Firefox Nightly as part of Puppeteer installation: + +```bash +PUPPETEER_PRODUCT=firefox npm i puppeteer +# or "yarn add puppeteer" +``` + +#### Q: What’s considered a “Navigation”? + +From Puppeteer’s standpoint, **“navigation” is anything that changes a page’s URL**. +Aside from regular navigation where the browser hits the network to fetch a new document from the web server, this includes [anchor navigations](https://www.w3.org/TR/html5/single-page.html#scroll-to-fragid) and [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) usage. + +With this definition of “navigation,” **Puppeteer works seamlessly with single-page applications.** + +#### Q: What’s the difference between a “trusted" and "untrusted" input event? + +In browsers, input events could be divided into two big groups: trusted vs. untrusted. + +- **Trusted events**: events generated by users interacting with the page, e.g. using a mouse or keyboard. +- **Untrusted event**: events generated by Web APIs, e.g. `document.createEvent` or `element.click()` methods. + +Websites can distinguish between these two groups: +- using an [`Event.isTrusted`](https://developer.mozilla.org/en-US/docs/Web/API/Event/isTrusted) event flag +- sniffing for accompanying events. For example, every trusted `'click'` event is preceded by `'mousedown'` and `'mouseup'` events. + +For automation purposes it’s important to generate trusted events. **All input events generated with Puppeteer are trusted and fire proper accompanying events.** If, for some reason, one needs an untrusted event, it’s always possible to hop into a page context with `page.evaluate` and generate a fake event: + +```js +await page.evaluate(() => { + document.querySelector('button[type=submit]').click(); +}); +``` + +#### Q: What features does Puppeteer not support? + +You may find that Puppeteer does not behave as expected when controlling pages that incorporate audio and video. (For example, [video playback/screenshots is likely to fail](https://github.com/puppeteer/puppeteer/issues/291).) There are two reasons for this: + +* Puppeteer is bundled with Chromium — not Chrome — and so by default, it inherits all of [Chromium's media-related limitations](https://www.chromium.org/audio-video). This means that Puppeteer does not support licensed formats such as AAC or H.264. (However, it is possible to force Puppeteer to use a separately-installed version Chrome instead of Chromium via the [`executablePath` option to `puppeteer.launch`](https://github.com/puppeteer/puppeteer/blob/v5.5.0/docs/api.md#puppeteerlaunchoptions). You should only use this configuration if you need an official release of Chrome that supports these media formats.) +* Since Puppeteer (in all configurations) controls a desktop version of Chromium/Chrome, features that are only supported by the mobile version of Chrome are not supported. This means that Puppeteer [does not support HTTP Live Streaming (HLS)](https://caniuse.com/#feat=http-live-streaming). + +#### Q: I am having trouble installing / running Puppeteer in my test environment. Where should I look for help? +We have a [troubleshooting](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md) guide for various operating systems that lists the required dependencies. + +#### Q: How do I try/test a prerelease version of Puppeteer? + +You can check out this repo or install the latest prerelease from npm: + +```bash +npm i --save puppeteer@next +``` + +Please note that prerelease may be unstable and contain bugs. + +#### Q: I have more questions! Where do I ask? + +There are many ways to get help on Puppeteer: +- [bugtracker](https://github.com/puppeteer/puppeteer/issues) +- [Stack Overflow](https://stackoverflow.com/questions/tagged/puppeteer) +- [slack channel](https://join.slack.com/t/puppeteer/shared_invite/enQtMzU4MjIyMDA5NTM4LWI0YTE0MjM0NWQzYmE2MTRmNjM1ZTBkN2MxNmJmNTIwNTJjMmFhOWFjMGExMDViYjk2YjU2ZmYzMmE1NmExYzc) + +Make sure to search these channels before posting your question. + + +<!-- [END faq] --> diff --git a/remote/test/puppeteer/api-extractor.json b/remote/test/puppeteer/api-extractor.json new file mode 100644 index 0000000000..3e7b9e66f6 --- /dev/null +++ b/remote/test/puppeteer/api-extractor.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "<projectFolder>/lib/cjs/puppeteer/api-docs-entry.d.ts", + "bundledPackages": [ "devtools-protocol" ], + + "apiReport": { + "enabled": false + }, + + "docModel": { + "enabled": true + }, + + "dtsRollup": { + "enabled": false + }, + + "tsdocMetadata": { + "enabled": false + }, + + "messages": { + "compilerMessageReporting": { + "default": { + "logLevel": "warning" + } + }, + + "extractorMessageReporting": { + "ae-internal-missing-underscore": { + "logLevel": "none" + }, + "default": { + "logLevel": "warning" + } + }, + + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + } + } + } + +} diff --git a/remote/test/puppeteer/cjs-entry-core.js b/remote/test/puppeteer/cjs-entry-core.js new file mode 100644 index 0000000000..446726fafa --- /dev/null +++ b/remote/test/puppeteer/cjs-entry-core.js @@ -0,0 +1,29 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * We use `export default puppeteer` in `src/index.ts` to expose the library But + * TypeScript in CJS mode compiles that to `exports.default = `. This means that + * our CJS Node users would have to use `require('puppeteer').default` which + * isn't very nice. + * + * So instead we expose this file as our entry point. This requires the compiled + * Puppeteer output and re-exports the `default` export via `module.exports.` + * This means that we can publish to CJS and ESM whilst maintaining the expected + * import behaviour for CJS and ESM users. + */ +const puppeteerExport = require('./lib/cjs/puppeteer/node-puppeteer-core'); +module.exports = puppeteerExport.default; diff --git a/remote/test/puppeteer/cjs-entry.js b/remote/test/puppeteer/cjs-entry.js new file mode 100644 index 0000000000..d1840a9bea --- /dev/null +++ b/remote/test/puppeteer/cjs-entry.js @@ -0,0 +1,29 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * We use `export default puppeteer` in `src/index.ts` to expose the library But + * TypeScript in CJS mode compiles that to `exports.default = `. This means that + * our CJS Node users would have to use `require('puppeteer').default` which + * isn't very nice. + * + * So instead we expose this file as our entry point. This requires the compiled + * Puppeteer output and re-exports the `default` export via `module.exports.` + * This means that we can publish to CJS and ESM whilst maintaining the expected + * import behaviour for CJS and ESM users. + */ +const puppeteerExport = require('./lib/cjs/puppeteer/node'); +module.exports = puppeteerExport.default; diff --git a/remote/test/puppeteer/commitlint.config.js b/remote/test/puppeteer/commitlint.config.js new file mode 100644 index 0000000000..2e216cd897 --- /dev/null +++ b/remote/test/puppeteer/commitlint.config.js @@ -0,0 +1,22 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'body-max-line-length': [0, 'always', 100], + 'footer-max-line-length': [0, 'always', 100], + }, +}; diff --git a/remote/test/puppeteer/examples/README.md b/remote/test/puppeteer/examples/README.md new file mode 100644 index 0000000000..8afb08555f --- /dev/null +++ b/remote/test/puppeteer/examples/README.md @@ -0,0 +1,38 @@ +# Running the examples + +Assuming you have a checkout of the Puppeteer repo and have run npm i (or yarn) to install the dependencies, the examples can be run from the root folder like so: + +```sh +NODE_PATH=../ node examples/search.js +``` + +## Larger examples + +More complex and use case driven examples can be found at [github.com/GoogleChromeLabs/puppeteer-examples](https://github.com/GoogleChromeLabs/puppeteer-examples). + +# Other resources + +> Other useful tools, articles, and projects that use Puppeteer. + +## Rendering and web scraping + +- [Puppetron](https://github.com/cheeaun/puppetron) - Demo site that shows how to use Puppeteer and Headless Chrome to render pages. Inspired by [GoogleChrome/rendertron](https://github.com/GoogleChrome/rendertron). +- [Thal](https://medium.com/@e_mad_ehsan/getting-started-with-puppeteer-and-chrome-headless-for-web-scrapping-6bf5979dee3e "An article on medium") - Getting started with Puppeteer and Chrome Headless for Web Scraping. +- [pupperender](https://github.com/LasaleFamine/pupperender) - Express middleware that checks the User-Agent header of incoming requests, and if it matches one of a configurable set of bots, render the page using Puppeteer. Useful for PWA rendering. +- [headless-chrome-crawler](https://github.com/yujiosaka/headless-chrome-crawler) - Crawler that provides simple APIs to manipulate Headless Chrome and allows you to crawl dynamic websites. +- [puppeteer-examples](https://github.com/checkly/puppeteer-examples) - Puppeteer Headless Chrome examples for real life use cases such as getting useful info from the web pages or common login scenarios. +- [browserless](https://github.com/joelgriffith/browserless) - Headless Chrome as a service letting you execute Puppeteer scripts remotely. Provides a docker image with configuration for concurrency, launch arguments and more. +- [Puppeteer Sandbox](https://puppeteersandbox.com) - Puppeteer sandbox environment as a service. Runs Puppeteer scripts and allows saving and embedding them in external sites and markdown files. + +## Testing + +- [angular-puppeteer-demo](https://github.com/Quramy/angular-puppeteer-demo) - Demo repository explaining how to use Puppeteer in Karma. +- [mocha-headless-chrome](https://github.com/direct-adv-interfaces/mocha-headless-chrome) - Tool which runs client-side **mocha** tests in the command line through headless Chrome. +- [puppeteer-to-istanbul-example](https://github.com/bcoe/puppeteer-to-istanbul-example) - Demo repository demonstrating how to output Puppeteer coverage in Istanbul format. +- [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer) - (almost) Zero configuration tool for setting up and running Jest and Puppeteer easily. Also includes an assertion library for Puppeteer. +- [puppeteer-har](https://github.com/Everettss/puppeteer-har) - Generate HAR file with puppeteer. +- [puppetry](https://puppetry.app/) - A desktop app to build Puppeteer/Jest driven tests without coding. +- [cucumber-puppeteer-example](https://github.com/mlampedx/cucumber-puppeteer-example) - Example repository demonstrating how to use Puppeeteer and Cucumber for integration testing. + +## Services +- [Checkly](https://checklyhq.com) - Monitoring SaaS that uses Puppeteer to check availability and correctness of web pages and apps. diff --git a/remote/test/puppeteer/examples/block-images.js b/remote/test/puppeteer/examples/block-images.js new file mode 100644 index 0000000000..298e473e82 --- /dev/null +++ b/remote/test/puppeteer/examples/block-images.js @@ -0,0 +1,33 @@ +/** + * Copyright 2017 Google Inc., PhantomJS Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.setRequestInterception(true); + page.on('request', (request) => { + if (request.resourceType() === 'image') request.abort(); + else request.continue(); + }); + await page.goto('https://news.google.com/news/'); + await page.screenshot({ path: 'news.png', fullPage: true }); + + await browser.close(); +})(); diff --git a/remote/test/puppeteer/examples/cross-browser.js b/remote/test/puppeteer/examples/cross-browser.js new file mode 100644 index 0000000000..f709abf5bb --- /dev/null +++ b/remote/test/puppeteer/examples/cross-browser.js @@ -0,0 +1,48 @@ +const puppeteer = require('puppeteer'); + +/** + * To have Puppeteer fetch a Firefox binary for you, first run: + * + * PUPPETEER_PRODUCT=firefox npm install + * + * To get additional logging about which browser binary is executed, + * run this example as: + * + * DEBUG=puppeteer:launcher NODE_PATH=../ node examples/cross-browser.js + * + * You can set a custom binary with the `executablePath` launcher option. + * + * + */ + +const firefoxOptions = { + product: 'firefox', + extraPrefsFirefox: { + // Enable additional Firefox logging from its protocol implementation + // 'remote.log.level': 'Trace', + }, + // Make browser logs visible + dumpio: true, +}; + +(async () => { + const browser = await puppeteer.launch(firefoxOptions); + + const page = await browser.newPage(); + console.log(await browser.version()); + + await page.goto('https://news.ycombinator.com/'); + + // Extract articles from the page. + const resultsSelector = '.storylink'; + const links = await page.evaluate((resultsSelector) => { + const anchors = Array.from(document.querySelectorAll(resultsSelector)); + return anchors.map((anchor) => { + const title = anchor.textContent.trim(); + return `${title} - ${anchor.href}`; + }); + }, resultsSelector); + console.log(links.join('\n')); + + await browser.close(); +})(); diff --git a/remote/test/puppeteer/examples/custom-event.js b/remote/test/puppeteer/examples/custom-event.js new file mode 100644 index 0000000000..bf196345ba --- /dev/null +++ b/remote/test/puppeteer/examples/custom-event.js @@ -0,0 +1,50 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + + // Define a window.onCustomEvent function on the page. + await page.exposeFunction('onCustomEvent', (e) => { + console.log(`${e.type} fired`, e.detail || ''); + }); + + /** + * Attach an event listener to page to capture a custom event on page load/navigation. + * @param {string} type Event name. + * @returns {!Promise} + */ + function listenFor(type) { + return page.evaluateOnNewDocument((type) => { + document.addEventListener(type, (e) => { + window.onCustomEvent({ type, detail: e.detail }); + }); + }, type); + } + + await listenFor('app-ready'); // Listen for "app-ready" custom event on page load. + + await page.goto('https://www.chromestatus.com/features', { + waitUntil: 'networkidle0', + }); + + await browser.close(); +})(); diff --git a/remote/test/puppeteer/examples/detect-sniff.js b/remote/test/puppeteer/examples/detect-sniff.js new file mode 100644 index 0000000000..76bb75b386 --- /dev/null +++ b/remote/test/puppeteer/examples/detect-sniff.js @@ -0,0 +1,44 @@ +/** + * Copyright 2017 Google Inc., PhantomJS Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const puppeteer = require('puppeteer'); + +function sniffDetector() { + const userAgent = window.navigator.userAgent; + const platform = window.navigator.platform; + + window.navigator.__defineGetter__('userAgent', function () { + window.navigator.sniffed = true; + return userAgent; + }); + + window.navigator.__defineGetter__('platform', function () { + window.navigator.sniffed = true; + return platform; + }); +} + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.evaluateOnNewDocument(sniffDetector); + await page.goto('https://www.google.com', { waitUntil: 'networkidle2' }); + console.log('Sniffed: ' + (await page.evaluate(() => !!navigator.sniffed))); + + await browser.close(); +})(); diff --git a/remote/test/puppeteer/examples/oopif.js b/remote/test/puppeteer/examples/oopif.js new file mode 100644 index 0000000000..52e2047b31 --- /dev/null +++ b/remote/test/puppeteer/examples/oopif.js @@ -0,0 +1,47 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const puppeteer = require('puppeteer'); + +async function attachFrame(frameId, url) { + const frame = document.createElement('iframe'); + frame.src = url; + frame.id = frameId; + document.body.appendChild(frame); + await new Promise((x) => (frame.onload = x)); + return frame; +} + +(async () => { + // Launch browser in non-headless mode. + const browser = await puppeteer.launch({ headless: false }); + const page = await browser.newPage(); + + // Load a page from one origin: + await page.goto('http://example.org/'); + + // Inject iframe with the another origin. + await page.evaluateHandle(attachFrame, 'frame1', 'https://example.com/'); + + // At this point there should be a message in the output: + // puppeteer:frame The frame '...' moved to another session. Out-of-proccess + // iframes (OOPIF) are not supported by Puppeteer yet. + // https://github.com/puppeteer/puppeteer/issues/2548 + + await browser.close(); +})(); diff --git a/remote/test/puppeteer/examples/pdf.js b/remote/test/puppeteer/examples/pdf.js new file mode 100644 index 0000000000..9f7ac92e0a --- /dev/null +++ b/remote/test/puppeteer/examples/pdf.js @@ -0,0 +1,35 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://news.ycombinator.com', { + waitUntil: 'networkidle2', + }); + // page.pdf() is currently supported only in headless mode. + // @see https://bugs.chromium.org/p/chromium/issues/detail?id=753118 + await page.pdf({ + path: 'hn.pdf', + format: 'letter', + }); + + await browser.close(); +})(); diff --git a/remote/test/puppeteer/examples/proxy.js b/remote/test/puppeteer/examples/proxy.js new file mode 100644 index 0000000000..5490822b8b --- /dev/null +++ b/remote/test/puppeteer/examples/proxy.js @@ -0,0 +1,35 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch({ + // Launch chromium using a proxy server on port 9876. + // More on proxying: + // https://www.chromium.org/developers/design-documents/network-settings + args: [ + '--proxy-server=127.0.0.1:9876', + // Use proxy for localhost URLs + '--proxy-bypass-list=<-loopback>', + ], + }); + const page = await browser.newPage(); + await page.goto('https://google.com'); + await browser.close(); +})(); diff --git a/remote/test/puppeteer/examples/screenshot-fullpage.js b/remote/test/puppeteer/examples/screenshot-fullpage.js new file mode 100644 index 0000000000..8844cbcfee --- /dev/null +++ b/remote/test/puppeteer/examples/screenshot-fullpage.js @@ -0,0 +1,28 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.emulate(puppeteer.devices['iPhone 6']); + await page.goto('https://www.nytimes.com/'); + await page.screenshot({ path: 'full.png', fullPage: true }); + await browser.close(); +})(); diff --git a/remote/test/puppeteer/examples/screenshot.js b/remote/test/puppeteer/examples/screenshot.js new file mode 100644 index 0000000000..28b4dbb274 --- /dev/null +++ b/remote/test/puppeteer/examples/screenshot.js @@ -0,0 +1,27 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('http://example.com'); + await page.screenshot({ path: 'example.png' }); + await browser.close(); +})(); diff --git a/remote/test/puppeteer/examples/search.js b/remote/test/puppeteer/examples/search.js new file mode 100644 index 0000000000..828d155438 --- /dev/null +++ b/remote/test/puppeteer/examples/search.js @@ -0,0 +1,55 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Search developers.google.com/web for articles tagged + * "Headless Chrome" and scrape results from the results page. + */ + +'use strict'; + +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + + await page.goto('https://developers.google.com/web/'); + + // Type into search box. + await page.type('.devsite-searchbox input', 'Headless Chrome'); + + // Wait for suggest overlay to appear and click "show all results". + const allResultsSelector = '.devsite-suggest-all-results'; + await page.waitForSelector(allResultsSelector); + await page.click(allResultsSelector); + + // Wait for the results page to load and display the results. + const resultsSelector = '.gsc-results .gsc-thumbnail-inside a.gs-title'; + await page.waitForSelector(resultsSelector); + + // Extract the results from the page. + const links = await page.evaluate((resultsSelector) => { + const anchors = Array.from(document.querySelectorAll(resultsSelector)); + return anchors.map((anchor) => { + const title = anchor.textContent.split('|')[0].trim(); + return `${title} - ${anchor.href}`; + }); + }, resultsSelector); + console.log(links.join('\n')); + + await browser.close(); +})(); diff --git a/remote/test/puppeteer/install.js b/remote/test/puppeteer/install.js new file mode 100644 index 0000000000..11518a595f --- /dev/null +++ b/remote/test/puppeteer/install.js @@ -0,0 +1,89 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This file is part of public API. + * + * By default, the `puppeteer` package runs this script during the installation + * process unless one of the env flags is provided. + * `puppeteer-core` package doesn't include this step at all. However, it's + * still possible to install a supported browser using this script when + * necessary. + */ + +const compileTypeScriptIfRequired = require('./typescript-if-required'); + +async function download() { + await compileTypeScriptIfRequired(); + // need to ensure TS is compiled before loading the installer + const { + downloadBrowser, + logPolitely, + } = require('./lib/cjs/puppeteer/node/install'); + + if (process.env.PUPPETEER_SKIP_DOWNLOAD) { + logPolitely( + '**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" environment variable was found.' + ); + return; + } + if ( + process.env.NPM_CONFIG_PUPPETEER_SKIP_DOWNLOAD || + process.env.npm_config_puppeteer_skip_download + ) { + logPolitely( + '**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" was set in npm config.' + ); + return; + } + if ( + process.env.NPM_PACKAGE_CONFIG_PUPPETEER_SKIP_DOWNLOAD || + process.env.npm_package_config_puppeteer_skip_download + ) { + logPolitely( + '**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" was set in project config.' + ); + return; + } + if (process.env.PUPPETEER_SKIP_CHROMIUM_DOWNLOAD) { + logPolitely( + '**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" environment variable was found.' + ); + return; + } + if ( + process.env.NPM_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD || + process.env.npm_config_puppeteer_skip_chromium_download + ) { + logPolitely( + '**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in npm config.' + ); + return; + } + if ( + process.env.NPM_PACKAGE_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD || + process.env.npm_package_config_puppeteer_skip_chromium_download + ) { + logPolitely( + '**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in project config.' + ); + return; + } + + downloadBrowser(); +} + +download(); diff --git a/remote/test/puppeteer/json-mocha-reporter.js b/remote/test/puppeteer/json-mocha-reporter.js new file mode 100644 index 0000000000..47fc74a9c5 --- /dev/null +++ b/remote/test/puppeteer/json-mocha-reporter.js @@ -0,0 +1,68 @@ +const mocha = require('mocha'); +module.exports = JSONExtra; + +const constants = mocha.Runner.constants; + +/* + +This is a copy of +https://github.com/mochajs/mocha/blob/master/lib/reporters/json-stream.js +with more event hooks. mocha does not support extending reporters or using +multiple reporters so a custom reporter is needed and it must be local +to the project. + +*/ + +function JSONExtra(runner, options) { + mocha.reporters.Base.call(this, runner, options); + const self = this; + + runner.once(constants.EVENT_RUN_BEGIN, function () { + writeEvent(['start', { total: runner.total }]); + }); + + runner.on(constants.EVENT_TEST_PASS, function (test) { + writeEvent(['pass', clean(test)]); + }); + + runner.on(constants.EVENT_TEST_FAIL, function (test, err) { + test = clean(test); + test.err = err.message; + test.stack = err.stack || null; + writeEvent(['fail', test]); + }); + + runner.once(constants.EVENT_RUN_END, function () { + writeEvent(['end', self.stats]); + }); + + runner.on(constants.EVENT_TEST_BEGIN, function (test) { + writeEvent(['test-start', clean(test)]); + }); + + runner.on(constants.EVENT_TEST_PENDING, function (test) { + writeEvent(['pending', clean(test)]); + }); +} + +function writeEvent(event) { + process.stdout.write(JSON.stringify(event) + '\n'); +} + +/** + * Returns an object literal representation of `test` + * free of cyclic properties, etc. + * + * @private + * @param {Object} test - Instance used as data source. + * @return {Object} object containing pared-down test instance data + */ +function clean(test) { + return { + title: test.title, + fullTitle: test.fullTitle(), + file: test.file, + duration: test.duration, + currentRetry: test.currentRetry(), + }; +} diff --git a/remote/test/puppeteer/mocha-config/base.js b/remote/test/puppeteer/mocha-config/base.js new file mode 100644 index 0000000000..dc0b04d73f --- /dev/null +++ b/remote/test/puppeteer/mocha-config/base.js @@ -0,0 +1,6 @@ +module.exports = { + reporter: 'dot', + // Allow `console.log`s to show up during test execution + logLevel: 'debug', + exit: !!process.env.CI, +}; diff --git a/remote/test/puppeteer/mocha-config/coverage-tests.js b/remote/test/puppeteer/mocha-config/coverage-tests.js new file mode 100644 index 0000000000..6814f404cf --- /dev/null +++ b/remote/test/puppeteer/mocha-config/coverage-tests.js @@ -0,0 +1,6 @@ +const base = require('./base'); + +module.exports = { + ...base, + spec: 'test/assert-coverage-test.js', +}; diff --git a/remote/test/puppeteer/mocha-config/doclint-tests.js b/remote/test/puppeteer/mocha-config/doclint-tests.js new file mode 100644 index 0000000000..279e28eb17 --- /dev/null +++ b/remote/test/puppeteer/mocha-config/doclint-tests.js @@ -0,0 +1,6 @@ +const base = require('./base'); + +module.exports = { + ...base, + spec: 'utils/doclint/**/*.spec.js', +}; diff --git a/remote/test/puppeteer/mocha-config/puppeteer-unit-tests.js b/remote/test/puppeteer/mocha-config/puppeteer-unit-tests.js new file mode 100644 index 0000000000..cb1bb7cae9 --- /dev/null +++ b/remote/test/puppeteer/mocha-config/puppeteer-unit-tests.js @@ -0,0 +1,28 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const base = require('./base'); + +module.exports = { + ...base, + require: ['./test/mocha-ts-require', './test/mocha-utils.ts'], + spec: 'test/*.spec.ts', + extension: ['js', 'ts'], + retries: process.env.CI ? 2 : 0, + parallel: !!process.env.PARALLEL, + timeout: 25 * 1000, + reporter: process.env.CI ? 'spec' : 'dot', +}; diff --git a/remote/test/puppeteer/moz.yaml b/remote/test/puppeteer/moz.yaml new file mode 100644 index 0000000000..3ebe8c1838 --- /dev/null +++ b/remote/test/puppeteer/moz.yaml @@ -0,0 +1,10 @@ +bugzilla: + component: Agent + product: Remote Protocol +origin: + description: Headless Chrome Node API + license: Apache-2.0 + name: puppeteer + release: a5dedb7 + url: https://github.com/mjzffr/puppeteer.git +schema: 1 diff --git a/remote/test/puppeteer/package-lock.json b/remote/test/puppeteer/package-lock.json new file mode 100644 index 0000000000..684b7f8733 --- /dev/null +++ b/remote/test/puppeteer/package-lock.json @@ -0,0 +1,9746 @@ +{ + "name": "puppeteer", + "version": "5.5.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/generator": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz", + "integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==", + "dev": true, + "requires": { + "@babel/types": "^7.12.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", + "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", + "dev": true + }, + "@babel/runtime": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", + "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/traverse": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.5.tgz", + "integrity": "sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.5", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.12.5", + "@babel/types": "^7.12.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + }, + "dependencies": { + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "@commitlint/cli": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-11.0.0.tgz", + "integrity": "sha512-YWZWg1DuqqO5Zjh7vUOeSX76vm0FFyz4y0cpGMFhrhvUi5unc4IVfCXZ6337R9zxuBtmveiRuuhQqnRRer+13g==", + "dev": true, + "requires": { + "@babel/runtime": "^7.11.2", + "@commitlint/format": "^11.0.0", + "@commitlint/lint": "^11.0.0", + "@commitlint/load": "^11.0.0", + "@commitlint/read": "^11.0.0", + "chalk": "4.1.0", + "core-js": "^3.6.1", + "get-stdin": "8.0.0", + "lodash": "^4.17.19", + "resolve-from": "5.0.0", + "resolve-global": "1.0.0", + "yargs": "^15.1.0" + } + }, + "@commitlint/config-conventional": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-11.0.0.tgz", + "integrity": "sha512-SNDRsb5gLuDd2PL83yCOQX6pE7gevC79UPFx+GLbLfw6jGnnbO9/tlL76MLD8MOViqGbo7ZicjChO9Gn+7tHhA==", + "dev": true, + "requires": { + "conventional-changelog-conventionalcommits": "^4.3.1" + } + }, + "@commitlint/ensure": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-11.0.0.tgz", + "integrity": "sha512-/T4tjseSwlirKZdnx4AuICMNNlFvRyPQimbZIOYujp9DSO6XRtOy9NrmvWujwHsq9F5Wb80QWi4WMW6HMaENug==", + "dev": true, + "requires": { + "@commitlint/types": "^11.0.0", + "lodash": "^4.17.19" + } + }, + "@commitlint/execute-rule": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-11.0.0.tgz", + "integrity": "sha512-g01p1g4BmYlZ2+tdotCavrMunnPFPhTzG1ZiLKTCYrooHRbmvqo42ZZn4QMStUEIcn+jfLb6BRZX3JzIwA1ezQ==", + "dev": true + }, + "@commitlint/format": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-11.0.0.tgz", + "integrity": "sha512-bpBLWmG0wfZH/svzqD1hsGTpm79TKJWcf6EXZllh2J/LSSYKxGlv967lpw0hNojme0sZd4a/97R3qA2QHWWSLg==", + "dev": true, + "requires": { + "@commitlint/types": "^11.0.0", + "chalk": "^4.0.0" + } + }, + "@commitlint/is-ignored": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-11.0.0.tgz", + "integrity": "sha512-VLHOUBN+sOlkYC4tGuzE41yNPO2w09sQnOpfS+pSPnBFkNUUHawEuA44PLHtDvQgVuYrMAmSWFQpWabMoP5/Xg==", + "dev": true, + "requires": { + "@commitlint/types": "^11.0.0", + "semver": "7.3.2" + } + }, + "@commitlint/lint": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-11.0.0.tgz", + "integrity": "sha512-Q8IIqGIHfwKr8ecVZyYh6NtXFmKw4YSEWEr2GJTB/fTZXgaOGtGFZDWOesCZllQ63f1s/oWJYtVv5RAEuwN8BQ==", + "dev": true, + "requires": { + "@commitlint/is-ignored": "^11.0.0", + "@commitlint/parse": "^11.0.0", + "@commitlint/rules": "^11.0.0", + "@commitlint/types": "^11.0.0" + } + }, + "@commitlint/load": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-11.0.0.tgz", + "integrity": "sha512-t5ZBrtgvgCwPfxmG811FCp39/o3SJ7L+SNsxFL92OR4WQxPcu6c8taD0CG2lzOHGuRyuMxZ7ps3EbngT2WpiCg==", + "dev": true, + "requires": { + "@commitlint/execute-rule": "^11.0.0", + "@commitlint/resolve-extends": "^11.0.0", + "@commitlint/types": "^11.0.0", + "chalk": "4.1.0", + "cosmiconfig": "^7.0.0", + "lodash": "^4.17.19", + "resolve-from": "^5.0.0" + } + }, + "@commitlint/message": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-11.0.0.tgz", + "integrity": "sha512-01ObK/18JL7PEIE3dBRtoMmU6S3ecPYDTQWWhcO+ErA3Ai0KDYqV5VWWEijdcVafNpdeUNrEMigRkxXHQLbyJA==", + "dev": true + }, + "@commitlint/parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-11.0.0.tgz", + "integrity": "sha512-DekKQAIYWAXIcyAZ6/PDBJylWJ1BROTfDIzr9PMVxZRxBPc1gW2TG8fLgjZfBP5mc0cuthPkVi91KQQKGri/7A==", + "dev": true, + "requires": { + "conventional-changelog-angular": "^5.0.0", + "conventional-commits-parser": "^3.0.0" + } + }, + "@commitlint/read": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-11.0.0.tgz", + "integrity": "sha512-37V0V91GSv0aDzMzJioKpCoZw6l0shk7+tRG8RkW1GfZzUIytdg3XqJmM+IaIYpaop0m6BbZtfq+idzUwJnw7g==", + "dev": true, + "requires": { + "@commitlint/top-level": "^11.0.0", + "fs-extra": "^9.0.0", + "git-raw-commits": "^2.0.0" + } + }, + "@commitlint/resolve-extends": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-11.0.0.tgz", + "integrity": "sha512-WinU6Uv6L7HDGLqn/To13KM1CWvZ09VHZqryqxXa1OY+EvJkfU734CwnOEeNlSCK7FVLrB4kmodLJtL1dkEpXw==", + "dev": true, + "requires": { + "import-fresh": "^3.0.0", + "lodash": "^4.17.19", + "resolve-from": "^5.0.0", + "resolve-global": "^1.0.0" + } + }, + "@commitlint/rules": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-11.0.0.tgz", + "integrity": "sha512-2hD9y9Ep5ZfoNxDDPkQadd2jJeocrwC4vJ98I0g8pNYn/W8hS9+/FuNpolREHN8PhmexXbkjrwyQrWbuC0DVaA==", + "dev": true, + "requires": { + "@commitlint/ensure": "^11.0.0", + "@commitlint/message": "^11.0.0", + "@commitlint/to-lines": "^11.0.0", + "@commitlint/types": "^11.0.0" + } + }, + "@commitlint/to-lines": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-11.0.0.tgz", + "integrity": "sha512-TIDTB0Y23jlCNubDROUVokbJk6860idYB5cZkLWcRS9tlb6YSoeLn1NLafPlrhhkkkZzTYnlKYzCVrBNVes1iw==", + "dev": true + }, + "@commitlint/top-level": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-11.0.0.tgz", + "integrity": "sha512-O0nFU8o+Ws+py5pfMQIuyxOtfR/kwtr5ybqTvR+C2lUPer2x6lnQU+OnfD7hPM+A+COIUZWx10mYQvkR3MmtAA==", + "dev": true, + "requires": { + "find-up": "^5.0.0" + }, + "dependencies": { + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "p-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", + "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + } + } + }, + "@commitlint/types": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-11.0.0.tgz", + "integrity": "sha512-VoNqai1vR5anRF5Tuh/+SWDFk7xi7oMwHrHrbm1BprYXjB2RJsWLhUrStMssDxEl5lW/z3EUdg8RvH/IUBccSQ==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.1.tgz", + "integrity": "sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + } + } + }, + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@mdn/browser-compat-data": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-2.0.7.tgz", + "integrity": "sha512-GeeM827DlzFFidn1eKkMBiqXFD2oLsnZbaiGhByPl0vcapsRzUL+t9hDoov1swc9rB2jw64R+ihtzC8qOE9wXw==", + "dev": true, + "requires": { + "extend": "3.0.2" + } + }, + "@microsoft/api-documenter": { + "version": "7.9.7", + "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.9.7.tgz", + "integrity": "sha512-wSlBDGbUd4Ld7YB8wmvsZ2yCwVdUh8zXmJHmggpZ13/TF/sForhhjd3MXfj0348h1RpVKE3foPxfxsDP6ON1Eg==", + "dev": true, + "requires": { + "@microsoft/api-extractor-model": "7.10.3", + "@microsoft/tsdoc": "0.12.19", + "@rushstack/node-core-library": "3.34.3", + "@rushstack/ts-command-line": "4.7.3", + "colors": "~1.2.1", + "js-yaml": "~3.13.1", + "resolve": "~1.17.0" + }, + "dependencies": { + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + } + } + }, + "@microsoft/api-extractor": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.10.4.tgz", + "integrity": "sha512-vod9Y8IHhBtB3hcKiOe4OLi/hNeWtgRh/mxGrye5SeHaJpu5urAAA9CxLxBT8fDe+pyr1ipzlaiM/eMm2vXKgw==", + "dev": true, + "requires": { + "@microsoft/api-extractor-model": "7.10.3", + "@microsoft/tsdoc": "0.12.19", + "@rushstack/node-core-library": "3.34.3", + "@rushstack/rig-package": "0.2.4", + "@rushstack/ts-command-line": "4.7.3", + "colors": "~1.2.1", + "lodash": "~4.17.15", + "resolve": "~1.17.0", + "semver": "~7.3.0", + "source-map": "~0.6.1", + "typescript": "~3.9.7" + }, + "dependencies": { + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "typescript": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", + "dev": true + } + } + }, + "@microsoft/api-extractor-model": { + "version": "7.10.3", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.10.3.tgz", + "integrity": "sha512-etP4NbZpj+zPCuO3YYigIYXkXq5zYhfE3vo/hrCj1OOd/159HDbSHnEQrNWRVy5TR79RAzHvkYAwtLYKeYP8Ag==", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.12.19", + "@rushstack/node-core-library": "3.34.3" + } + }, + "@microsoft/tsdoc": { + "version": "0.12.19", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.12.19.tgz", + "integrity": "sha512-IpgPxHrNxZiMNUSXqR1l/gePKPkfAmIKoDRP9hp7OwjU29ZR8WCJsOJ8iBKgw0Qk+pFwR+8Y1cy8ImLY6e9m4A==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", + "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.3", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", + "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", + "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.3", + "fastq": "^1.6.0" + } + }, + "@rollup/plugin-node-resolve": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-8.4.0.tgz", + "integrity": "sha512-LFqKdRLn0ShtQyf6SBYO69bGE1upV6wUhBX0vFOUnLAyzx5cwp8svA0eHUnu8+YU57XOkrMtfG63QOpQx25pHQ==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deep-freeze": "^0.0.1", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.17.0" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + } + }, + "@rushstack/node-core-library": { + "version": "3.34.3", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.34.3.tgz", + "integrity": "sha512-WNXHEk5/uoZsbrKzGpYUzDDymJvZarRkByab4uS1fbEcTSDFSVB9e0rREzCkU9yDAQlRutbFwiTXLu3LVR5F6w==", + "dev": true, + "requires": { + "@types/node": "10.17.13", + "colors": "~1.2.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.17.0", + "semver": "~7.3.0", + "timsort": "~0.3.0", + "z-schema": "~3.18.3" + }, + "dependencies": { + "@types/node": { + "version": "10.17.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz", + "integrity": "sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==", + "dev": true + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + } + } + }, + "@rushstack/rig-package": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.2.4.tgz", + "integrity": "sha512-/UXb6N0m0l+5kU4mLmRxyw3we+/w1fesgvfg96xllty5LyQxKDvkscmjlvCU/Yx55WO1tVxN4/7YlNEB2DcHyA==", + "dev": true, + "requires": { + "@types/node": "10.17.13", + "resolve": "~1.17.0", + "strip-json-comments": "~3.1.1" + }, + "dependencies": { + "@types/node": { + "version": "10.17.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz", + "integrity": "sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==", + "dev": true + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + } + } + }, + "@rushstack/ts-command-line": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.7.3.tgz", + "integrity": "sha512-8FNrUSbMgKLgRVcsg1STsIC2xAdyes7qJtVwg36hSnBAMZgCCIM+Z36nnxyrnYTS/6qwiXv7fwVaUxXH+SyiAQ==", + "dev": true, + "requires": { + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "colors": "~1.2.1", + "string-argv": "~0.3.1" + } + }, + "@sindresorhus/is": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", + "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", + "dev": true + }, + "@sinonjs/commons": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", + "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/formatio": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-5.0.1.tgz", + "integrity": "sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^5.0.2" + } + }, + "@sinonjs/samsam": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.0.tgz", + "integrity": "sha512-hXpcfx3aq+ETVBwPlRFICld5EnrkexXuXDwqUNhDdr5L8VjvMeSRwyOa0qL7XFmR+jVWR4rUZtnxlG7RX72sBg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "@szmarczak/http-timer": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz", + "integrity": "sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==", + "dev": true, + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "@types/accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", + "dev": true + }, + "@types/babel__code-frame": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/babel__code-frame/-/babel__code-frame-7.0.2.tgz", + "integrity": "sha512-imO+jT/yjOKOAS5GQZ8SDtwiIloAGGr6OaZDKB0V5JVaSfGZLat5K5/ZRtyKW6R60XHV3RHYPTFfhYb+wDKyKg==", + "dev": true + }, + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/cacheable-request": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz", + "integrity": "sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==", + "dev": true, + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } + }, + "@types/connect": { + "version": "3.4.33", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", + "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-P1bffQfhD3O4LW0ioENXUhZ9OIa0Zn+P7M+pWgkCKaT53wVLSq0mrKksCID/FGHpFhRSxRGhgrQmfhRuzwtKdg==", + "dev": true + }, + "@types/cookies": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.5.tgz", + "integrity": "sha512-3+TAFSm78O7/bAeYdB8FoYGntuT87vVP9JKuQRL8sRhv9313LP2SpHHL50VeFtnyjIcb3UELddMk5Yt0eOSOkg==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, + "@types/debug": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-0.0.31.tgz", + "integrity": "sha512-LS1MCPaQKqspg7FvexuhmDbWUhE2yIJ+4AgVIyObfc06/UKZ8REgxGNjZc82wPLWmbeOm7S+gSsLgo75TanG4A==", + "dev": true + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/express": { + "version": "4.17.9", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.9.tgz", + "integrity": "sha512-SDzEIZInC4sivGIFY4Sz1GG6J9UObPwCInYJjko2jzOf/Imx/dlpume6Xxwj1ORL82tBbmN4cPDIDkLbWHk9hw==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz", + "integrity": "sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/http-assert": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.1.tgz", + "integrity": "sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ==", + "dev": true + }, + "@types/http-cache-semantics": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", + "integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==", + "dev": true + }, + "@types/http-errors": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-2aoSC4UUbHDj2uCsCxcG/vRMXey/m17bC7UwitVm5hn22nI8O8Y9iDpA76Orc+DWkQ4zZrOKEshCqR/jSuXAHA==", + "dev": true + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "@types/json-schema": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", + "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, + "@types/keygrip": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz", + "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==", + "dev": true + }, + "@types/keyv": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz", + "integrity": "sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/koa": { + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.11.6.tgz", + "integrity": "sha512-BhyrMj06eQkk04C97fovEDQMpLpd2IxCB4ecitaXwOKGq78Wi2tooaDOWOFGajPk8IkQOAtMppApgSVkYe1F/A==", + "dev": true, + "requires": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "@types/koa-compose": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz", + "integrity": "sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==", + "dev": true, + "requires": { + "@types/koa": "*" + } + }, + "@types/mime": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", + "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==", + "dev": true + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/minimist": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz", + "integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==", + "dev": true + }, + "@types/mocha": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-7.0.2.tgz", + "integrity": "sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==", + "dev": true + }, + "@types/node": { + "version": "14.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.9.tgz", + "integrity": "sha512-JsoLXFppG62tWTklIoO4knA+oDTYsmqWxHRvd4lpmfQRNhX6osheUOWETP2jMoV/2bEHuMra8Pp3Dmo/stBFcw==" + }, + "@types/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", + "dev": true + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "@types/parse5": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz", + "integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==", + "dev": true + }, + "@types/proxy-from-env": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/proxy-from-env/-/proxy-from-env-1.0.1.tgz", + "integrity": "sha512-luG++TFHyS61eKcfkR1CVV6a1GMNXDjtqEQIIfaSHax75xp0HU3SlezjOi1yqubJwrG8e9DeW59n6wTblIDwFg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/puppeteer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-5.4.0.tgz", + "integrity": "sha512-zTYDLjnHjgzokrwKt7N0rgn7oZPYo1J0m8Ghu+gXqzLCEn8RWbELa2uprE2UFJ0jU/Sk0x9jXXdOH/5QQLFHhQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/puppeteer-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/puppeteer-core/-/puppeteer-core-2.0.0.tgz", + "integrity": "sha512-JvoEb7KgEkUet009ZDrtpUER3hheXoHgQByuYpJZ5WWT7LWwMH+0NTqGQXGgoOKzs+G5NA1T4DZwXK79Bhnejw==", + "dev": true, + "requires": { + "@types/puppeteer": "*" + } + }, + "@types/qs": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz", + "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true + }, + "@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/rimraf": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-2.0.4.tgz", + "integrity": "sha512-8gBudvllD2A/c0CcEX/BivIDorHFt5UI5m46TsNj8DjWCCTTZT74kEe4g+QsY7P/B9WdO98d82zZgXO/RQzu2Q==", + "dev": true, + "requires": { + "@types/glob": "*", + "@types/node": "*" + } + }, + "@types/serve-static": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz", + "integrity": "sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA==", + "dev": true, + "requires": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "@types/sinon": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-9.0.8.tgz", + "integrity": "sha512-IVnI820FZFMGI+u1R+2VdRaD/82YIQTdqLYC9DLPszZuynAJDtCvCtCs3bmyL66s7FqRM3+LPX7DhHnVTaagDw==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz", + "integrity": "sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg==", + "dev": true + }, + "@types/stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", + "dev": true + }, + "@types/tar-fs": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/@types/tar-fs/-/tar-fs-1.16.3.tgz", + "integrity": "sha512-Y+fdeg11tb9J3UNIatNtrTPM1i8U+WLv2mMhZ3W13mtU19stCgrXJ4iXLkTpoF8jqHi3T/qTS8+fQ3IPzXxpuA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/tar-stream": "*" + } + }, + "@types/tar-stream": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/tar-stream/-/tar-stream-2.1.0.tgz", + "integrity": "sha512-s1UQxQUVMHbSkCC0X4qdoiWgHF8DoyY1JjQouFsnk/8ysoTdBaiCHud/exoAZzKDbzAXVc+ah6sczxGVMAohFw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/ws": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.0.tgz", + "integrity": "sha512-Y29uQ3Uy+58bZrFLhX36hcI3Np37nqWE7ky5tjiDoy1GDZnIwVxS0CgF+s+1bXMzjKBFy+fqaRfb708iNzdinw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/yargs": { + "version": "15.0.10", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.10.tgz", + "integrity": "sha512-z8PNtlhrj7eJNLmrAivM7rjBESG6JwC5xP3RVk12i/8HVP7Xnx/sEmERnRImyEuUaJfO942X0qMOYsoupaJbZQ==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", + "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==", + "dev": true + }, + "@types/yauzl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", + "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.8.1.tgz", + "integrity": "sha512-d7LeQ7dbUrIv5YVFNzGgaW3IQKMmnmKFneRWagRlGYOSfLJVaRbj/FrBNOBC1a3tVO+TgNq1GbHvRtg1kwL0FQ==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "4.8.1", + "@typescript-eslint/scope-manager": "4.8.1", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/experimental-utils": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.8.1.tgz", + "integrity": "sha512-WigyLn144R3+lGATXW4nNcDJ9JlTkG8YdBWHkDlN0lC3gUGtDi7Pe3h5GPvFKMcRz8KbZpm9FJV9NTW8CpRHpg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.8.1", + "@typescript-eslint/types": "4.8.1", + "@typescript-eslint/typescript-estree": "4.8.1", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.8.1.tgz", + "integrity": "sha512-QND8XSVetATHK9y2Ltc/XBl5Ro7Y62YuZKnPEwnNPB8E379fDsvzJ1dMJ46fg/VOmk0hXhatc+GXs5MaXuL5Uw==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "4.8.1", + "@typescript-eslint/types": "4.8.1", + "@typescript-eslint/typescript-estree": "4.8.1", + "debug": "^4.1.1" + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.8.1.tgz", + "integrity": "sha512-r0iUOc41KFFbZdPAdCS4K1mXivnSZqXS5D9oW+iykQsRlTbQRfuFRSW20xKDdYiaCoH+SkSLeIF484g3kWzwOQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.8.1", + "@typescript-eslint/visitor-keys": "4.8.1" + } + }, + "@typescript-eslint/types": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.8.1.tgz", + "integrity": "sha512-ave2a18x2Y25q5K05K/U3JQIe2Av4+TNi/2YuzyaXLAsDx6UZkz1boZ7nR/N6Wwae2PpudTZmHFXqu7faXfHmA==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.8.1.tgz", + "integrity": "sha512-bJ6Fn/6tW2g7WIkCWh3QRlaSU7CdUUK52shx36/J7T5oTQzANvi6raoTsbwGM11+7eBbeem8hCCKbyvAc0X3sQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.8.1", + "@typescript-eslint/visitor-keys": "4.8.1", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.8.1.tgz", + "integrity": "sha512-3nrwXFdEYALQh/zW8rFwP4QltqsanCDz4CwWMPiIZmwlk9GlvBeueEIbq05SEq4ganqM0g9nh02xXgv5XI3PeQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.8.1", + "eslint-visitor-keys": "^2.0.0" + } + }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "@web/browser-logs": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-0.1.5.tgz", + "integrity": "sha512-WXIvoXtugpSYQuredglsbbqsLRXpclFG4nLRebhIRARUNpfOK+1xTglh8HJjdk7bEHTX9jIbnyhtQVxHuo97fg==", + "dev": true + }, + "@web/config-loader": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@web/config-loader/-/config-loader-0.1.2.tgz", + "integrity": "sha512-uh+VL79V/TJjrleAQM66Rke8jKu/J/sqzhiJ08iwGcUKV5lqUbLxGoC4RiNRwcEh8dna9HLBPpi935EuAD5tnw==", + "dev": true, + "requires": { + "semver": "^7.3.2" + } + }, + "@web/dev-server-core": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.2.17.tgz", + "integrity": "sha512-fBxj5cqf+PjkBUMxnMZ4YS9vfARxVfl4cB20nc0y8jwy5zGAE7RPUcVSRoKP+1uWHnXos0oEqth0hphzyJxBbw==", + "dev": true, + "requires": { + "@types/koa": "^2.11.6", + "@types/ws": "^7.2.6", + "@web/parse5-utils": "^1.0.0", + "chokidar": "^3.4.0", + "clone": "^2.1.2", + "es-module-lexer": "^0.3.24", + "get-stream": "^6.0.0", + "is-stream": "^2.0.0", + "isbinaryfile": "^4.0.6", + "koa": "^2.13.0", + "koa-etag": "^3.0.0", + "koa-static": "^5.0.0", + "lru-cache": "^5.1.1", + "mime-types": "^2.1.27", + "parse5": "^6.0.0", + "picomatch": "^2.2.2", + "ws": "^7.3.1" + }, + "dependencies": { + "get-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", + "integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "@web/dev-server-esbuild": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@web/dev-server-esbuild/-/dev-server-esbuild-0.2.6.tgz", + "integrity": "sha512-nOtQEI63kPux9vPRcQb2P5323RxuWdSu9GMMiVVRqPqDVxSa8IJHhnzV5yRidGnea9TsgV+caFeTTW0leod34g==", + "dev": true, + "requires": { + "@mdn/browser-compat-data": "^2.0.2", + "@web/dev-server-core": "^0.2.2", + "esbuild": "^0.7.15", + "parse5": "^6.0.1", + "ua-parser-js": "^0.7.21" + } + }, + "@web/dev-server-rollup": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@web/dev-server-rollup/-/dev-server-rollup-0.2.11.tgz", + "integrity": "sha512-cqeXC93BElaKIV0Bel05M86Or5HQKem7FuBjfoMMlgp0irt6ZYCovYe7l2rG0Hw+/MCYaNs+uxjMK5UYLuWMBw==", + "dev": true, + "requires": { + "@web/dev-server-core": "^0.2.17", + "chalk": "^4.1.0", + "parse5": "^6.0.1", + "rollup": "^2.33.2", + "whatwg-url": "^8.1.0" + } + }, + "@web/parse5-utils": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-1.1.2.tgz", + "integrity": "sha512-/JQHbK53BmYiFK2igr2B+Psl2Ivp2ju75Nx1InZweTbxLQNGG9yUBaudER85aqebIH6smkPkKwVtpdBXBiwy1A==", + "dev": true, + "requires": { + "@types/parse5": "^5.0.3", + "parse5": "^6.0.1" + } + }, + "@web/test-runner": { + "version": "0.9.12", + "resolved": "https://registry.npmjs.org/@web/test-runner/-/test-runner-0.9.12.tgz", + "integrity": "sha512-ufpYHVN7GQU3ZN6qAXnstlSVnZ0QAj/gIKdiBwl8eOK1Ucg9wxknpaidTouX/nqdey9MI8qCYIl5wRtfo5iHMg==", + "dev": true, + "requires": { + "@rollup/plugin-node-resolve": "^8.4.0", + "@web/config-loader": "^0.1.2", + "@web/dev-server-rollup": "^0.2.9", + "@web/test-runner-chrome": "^0.7.3", + "@web/test-runner-cli": "^0.6.13", + "@web/test-runner-commands": "^0.2.1", + "@web/test-runner-core": "^0.8.11", + "@web/test-runner-mocha": "^0.5.1", + "@web/test-runner-saucelabs": "^0.1.1", + "command-line-args": "^5.1.1", + "deepmerge": "^4.2.2", + "globby": "^11.0.1" + } + }, + "@web/test-runner-chrome": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@web/test-runner-chrome/-/test-runner-chrome-0.7.3.tgz", + "integrity": "sha512-wf4SZ8nPTontLML/QwAmsR/+e9+rcuPiirbz63ulsBpX939Kn6LuOPPF4axL66gxJJ/BP++PJeCWtsCnowzveA==", + "dev": true, + "requires": { + "@types/puppeteer-core": "^2.0.0", + "@web/test-runner-core": "^0.8.11", + "@web/test-runner-coverage-v8": "^0.2.3", + "chrome-launcher": "^0.13.4", + "puppeteer-core": "^5.3.1" + } + }, + "@web/test-runner-cli": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@web/test-runner-cli/-/test-runner-cli-0.6.13.tgz", + "integrity": "sha512-c2Hm+n0CxpyHoch+17ODmpepYmTWZ1YleG3J7nCwLGzvnI3umv5pi/82AFcuMYJnW+9R2YYfhW+8bHKX7Mq6Cw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@types/babel__code-frame": "^7.0.2", + "@web/browser-logs": "^0.1.2", + "@web/config-loader": "^0.1.1", + "@web/test-runner-chrome": "^0.7.3", + "@web/test-runner-core": "^0.8.11", + "camelcase": "^6.0.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "command-line-args": "^5.1.1", + "command-line-usage": "^6.1.0", + "convert-source-map": "^1.7.0", + "deepmerge": "^4.2.2", + "diff": "^4.0.2", + "globby": "^11.0.1", + "ip": "^1.1.5", + "istanbul-lib-report": "^3.0.0", + "istanbul-reports": "^3.0.2", + "log-update": "^4.0.0", + "open": "^7.0.4", + "portfinder": "^1.0.28", + "source-map": "^0.7.3" + }, + "dependencies": { + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "@web/test-runner-commands": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@web/test-runner-commands/-/test-runner-commands-0.2.1.tgz", + "integrity": "sha512-usbgVGIihxfHIPv1FPDueEzET51SM8X6ZVZY+k8MgFzab018JyhqvkDVEgRVmSfg1gWKvqRdA5Qpr11b+GGBeA==", + "dev": true, + "requires": { + "@web/test-runner-core": "^0.8.4" + } + }, + "@web/test-runner-core": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.8.11.tgz", + "integrity": "sha512-NyCm9tNaUFkO9P7sI9/z/E7CSP4sxlEnygHmo9mqbWOGW3aNsasqEcYHxQgGMW1HJrohDCigWRJZhMSFNOYd1Q==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@web/browser-logs": "^0.1.5", + "@web/dev-server-core": "^0.2.11", + "co-body": "^6.0.0", + "debounce": "^1.2.0", + "dependency-graph": "^0.9.0", + "globby": "^11.0.1", + "istanbul-lib-coverage": "^3.0.0", + "picomatch": "^2.2.2", + "uuid": "^8.1.0" + } + }, + "@web/test-runner-coverage-v8": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@web/test-runner-coverage-v8/-/test-runner-coverage-v8-0.2.3.tgz", + "integrity": "sha512-vfDjbENCJsDxY2NwAclOKC5y9MkI+KIx0U/JSPZsowzx2dYZHOqKQfRknZa2nyC9wf3pqbBUrgy0g231t5nkyQ==", + "dev": true, + "requires": { + "@web/test-runner-core": "^0.8.11", + "istanbul-lib-coverage": "^3.0.0", + "v8-to-istanbul": "^7.0.0" + } + }, + "@web/test-runner-mocha": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@web/test-runner-mocha/-/test-runner-mocha-0.5.1.tgz", + "integrity": "sha512-/8VBh3nP2sE/a320CfRI/J1TLT8ChPy0xGBafh3gg59+jOTtHCfZzHet7nre/VTOOvu1H24XEZiyhMFud6WVXQ==", + "dev": true, + "requires": { + "@types/mocha": "^8.0.1", + "@web/test-runner-core": "^0.8.4" + }, + "dependencies": { + "@types/mocha": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.0.4.tgz", + "integrity": "sha512-M4BwiTJjHmLq6kjON7ZoI2JMlBvpY3BYSdiP6s/qCT3jb1s9/DeJF0JELpAxiVSIxXDzfNKe+r7yedMIoLbknQ==", + "dev": true + } + } + }, + "@web/test-runner-saucelabs": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@web/test-runner-saucelabs/-/test-runner-saucelabs-0.1.3.tgz", + "integrity": "sha512-f0i3vn96WKs9zwpghUCkuYztALaNv5m5on8NTj7JoLY00FhVpdxbeanpB4Ej5i9xTu6aelmKoILPK88KIE+dAw==", + "dev": true, + "requires": { + "@web/dev-server-esbuild": "^0.2.4", + "@web/test-runner-selenium": "^0.3.3", + "ip": "^1.1.5", + "saucelabs": "^4.5.0", + "selenium-webdriver": "^4.0.0-alpha.7", + "uuid": "^8.3.0" + } + }, + "@web/test-runner-selenium": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@web/test-runner-selenium/-/test-runner-selenium-0.3.3.tgz", + "integrity": "sha512-KyHH7ZZ0SkyAXmwpx+lTCuDs6/YXnqHn5mSdhNw2oYA6ZHrPWOdw8bvLXJItfLBGkJc0nn6hs9bqYp9qdyHHVA==", + "dev": true, + "requires": { + "@web/test-runner-core": "^0.8.4", + "selenium-webdriver": "^4.0.0-alpha.7" + } + }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", + "dev": true + }, + "add-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", + "integrity": "sha1-anmQQ3ynNtXhKI25K9MmbV9csqo=", + "dev": true + }, + "agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==" + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", + "dev": true + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true + }, + "archive-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", + "integrity": "sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA=", + "dev": true, + "requires": { + "file-type": "^4.2.0" + }, + "dependencies": { + "file-type": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", + "integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU=", + "dev": true + } + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=", + "dev": true + }, + "array-includes": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", + "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "is-string": "^1.0.5" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array.prototype.flat": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz", + "integrity": "sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, + "babel-eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", + "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bin-check": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bin-check/-/bin-check-4.1.0.tgz", + "integrity": "sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==", + "dev": true, + "requires": { + "execa": "^0.7.0", + "executable": "^4.1.0" + } + }, + "bin-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-3.1.0.tgz", + "integrity": "sha512-Mkfm4iE1VFt4xd4vH+gx+0/71esbfus2LsnCGe8Pi4mndSPyT+NGES/Eg99jx8/lUGWfu3z2yuB/bt5UB+iVbQ==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "find-versions": "^3.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "bin-version-check": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-4.0.0.tgz", + "integrity": "sha512-sR631OrhC+1f8Cvs8WyVWOA33Y8tgwjETNPyyD/myRBXLkfS/vl74FmH/lFcRl9KY3zwGh7jFhvyk9vV3/3ilQ==", + "dev": true, + "requires": { + "bin-version": "^3.0.0", + "semver": "^5.6.0", + "semver-truncate": "^1.1.2" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "bin-wrapper": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bin-wrapper/-/bin-wrapper-4.1.0.tgz", + "integrity": "sha512-hfRmo7hWIXPkbpi0ZltboCMVrU+0ClXR/JgbCKKjlDjQf6igXa7OwdqNcFWQZPZTgiY7ZpzE3+LjjkLiTN2T7Q==", + "dev": true, + "requires": { + "bin-check": "^4.1.0", + "bin-version-check": "^4.0.0", + "download": "^7.1.0", + "import-lazy": "^3.1.0", + "os-filter-obj": "^2.0.0", + "pify": "^4.0.1" + }, + "dependencies": { + "import-lazy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-3.1.0.tgz", + "integrity": "sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ==", + "dev": true + } + } + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true + }, + "bl": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "builtin-modules": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz", + "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==", + "dev": true + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "dev": true, + "requires": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + } + }, + "cacheable-lookup": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz", + "integrity": "sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w==", + "dev": true + }, + "cacheable-request": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", + "integrity": "sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=", + "dev": true, + "requires": { + "clone-response": "1.0.2", + "get-stream": "3.0.0", + "http-cache-semantics": "3.8.1", + "keyv": "3.0.0", + "lowercase-keys": "1.0.0", + "normalize-url": "2.0.1", + "responselike": "1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", + "dev": true + } + } + }, + "call-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", + "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camel-case": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.1.tgz", + "integrity": "sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q==", + "dev": true, + "requires": { + "pascal-case": "^3.1.1", + "tslib": "^1.10.0" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + } + }, + "capital-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.3.tgz", + "integrity": "sha512-OlUSJpUr7SY0uZFOxcwnDOU7/MpHlKTZx2mqnDYQFrDudXLFm0JJ9wr/l4csB+rh2Ug0OPuoSO53PqiZBqno9A==", + "dev": true, + "requires": { + "no-case": "^3.0.3", + "tslib": "^1.10.0", + "upper-case-first": "^2.0.1" + } + }, + "caw": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", + "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", + "dev": true, + "requires": { + "get-proxy": "^2.0.0", + "isurl": "^1.0.0-alpha5", + "tunnel-agent": "^0.6.0", + "url-to-options": "^1.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "change-case": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.1.tgz", + "integrity": "sha512-qRlUWn/hXnX1R1LBDF/RelJLiqNjKjUqlmuBVSEIyye8kq49CXqkZWKmi8XeUAdDXWFOcGLUMZ+aHn3Q5lzUXw==", + "dev": true, + "requires": { + "camel-case": "^4.1.1", + "capital-case": "^1.0.3", + "constant-case": "^3.0.3", + "dot-case": "^3.0.3", + "header-case": "^2.0.3", + "no-case": "^3.0.3", + "param-case": "^3.0.3", + "pascal-case": "^3.1.1", + "path-case": "^3.0.3", + "sentence-case": "^3.0.3", + "snake-case": "^3.0.3", + "tslib": "^1.10.0" + } + }, + "chokidar": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", + "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "chrome-launcher": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.13.4.tgz", + "integrity": "sha512-nnzXiDbGKjDSK6t2I+35OAPBy5Pw/39bgkb/ZAFwMhwJbdYBp6aH+vW28ZgtjdU890Q7D+3wN/tB8N66q5Gi2A==", + "dev": true, + "requires": { + "@types/node": "*", + "escape-string-regexp": "^1.0.5", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^0.5.3", + "rimraf": "^3.0.2" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha1-jffHquUf02h06PjQW5GAvBGj/tc=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "co-body": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.1.0.tgz", + "integrity": "sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==", + "dev": true, + "requires": { + "inflation": "^2.0.0", + "qs": "^6.5.2", + "raw-body": "^2.3.3", + "type-is": "^1.6.16" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "colors": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", + "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "command-line-args": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.1.1.tgz", + "integrity": "sha512-hL/eG8lrll1Qy1ezvkant+trihbGnaKaeEjj6Scyr3DN+RC7iQ5Rz84IeLERfAWDGo0HBSNAakczwgCilDXnWg==", + "dev": true, + "requires": { + "array-back": "^3.0.1", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + } + }, + "command-line-usage": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.1.tgz", + "integrity": "sha512-F59pEuAR9o1SF/bD0dQBDluhpT4jJQNWUHEuVBqpDmCUo6gPjCi+m9fCWnWZVR/oG6cMTUms4h+3NPl74wGXvA==", + "dev": true, + "requires": { + "array-back": "^4.0.1", + "chalk": "^2.4.2", + "table-layout": "^1.0.1", + "typical": "^5.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "array-back": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.1.tgz", + "integrity": "sha512-Z/JnaVEXv+A9xabHzN43FiiiWEE7gPCRXMrVmRm00tWbjZRul1iHm7ECzlyNq1p4a4ATXz+G9FJ3GqGOkOV3fg==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true + } + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "commonmark": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.28.1.tgz", + "integrity": "sha1-Buq41SM4uDn6Gi11rwCF7tGxvq4=", + "dev": true, + "requires": { + "entities": "~ 1.1.1", + "mdurl": "~ 1.0.1", + "minimist": "~ 1.2.0", + "string.prototype.repeat": "^0.2.0" + } + }, + "compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "requires": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "compare-versions": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", + "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "config-chain": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", + "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", + "dev": true, + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "constant-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.3.tgz", + "integrity": "sha512-FXtsSnnrFYpzDmvwDGQW+l8XK3GV1coLyBN0eBz16ZUzGaZcT2ANVCJmLeuw2GQgxKHQIe9e0w2dzkSfaRlUmA==", + "dev": true, + "requires": { + "no-case": "^3.0.3", + "tslib": "^1.10.0", + "upper-case": "^2.0.1" + } + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "conventional-changelog": { + "version": "3.1.23", + "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-3.1.23.tgz", + "integrity": "sha512-sScUu2NHusjRC1dPc5p8/b3kT78OYr95/Bx7Vl8CPB8tF2mG1xei5iylDTRjONV5hTlzt+Cn/tBWrKdd299b7A==", + "dev": true, + "requires": { + "conventional-changelog-angular": "^5.0.11", + "conventional-changelog-atom": "^2.0.7", + "conventional-changelog-codemirror": "^2.0.7", + "conventional-changelog-conventionalcommits": "^4.4.0", + "conventional-changelog-core": "^4.2.0", + "conventional-changelog-ember": "^2.0.8", + "conventional-changelog-eslint": "^3.0.8", + "conventional-changelog-express": "^2.0.5", + "conventional-changelog-jquery": "^3.0.10", + "conventional-changelog-jshint": "^2.0.8", + "conventional-changelog-preset-loader": "^2.3.4" + } + }, + "conventional-changelog-angular": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.12.tgz", + "integrity": "sha512-5GLsbnkR/7A89RyHLvvoExbiGbd9xKdKqDTrArnPbOqBqG/2wIosu0fHwpeIRI8Tl94MhVNBXcLJZl92ZQ5USw==", + "dev": true, + "requires": { + "compare-func": "^2.0.0", + "q": "^1.5.1" + } + }, + "conventional-changelog-atom": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/conventional-changelog-atom/-/conventional-changelog-atom-2.0.8.tgz", + "integrity": "sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==", + "dev": true, + "requires": { + "q": "^1.5.1" + } + }, + "conventional-changelog-codemirror": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/conventional-changelog-codemirror/-/conventional-changelog-codemirror-2.0.8.tgz", + "integrity": "sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==", + "dev": true, + "requires": { + "q": "^1.5.1" + } + }, + "conventional-changelog-config-spec": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-config-spec/-/conventional-changelog-config-spec-2.1.0.tgz", + "integrity": "sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ==", + "dev": true + }, + "conventional-changelog-conventionalcommits": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.5.0.tgz", + "integrity": "sha512-buge9xDvjjOxJlyxUnar/+6i/aVEVGA7EEh4OafBCXPlLUQPGbRUBhBUveWRxzvR8TEjhKEP4BdepnpG2FSZXw==", + "dev": true, + "requires": { + "compare-func": "^2.0.0", + "lodash": "^4.17.15", + "q": "^1.5.1" + } + }, + "conventional-changelog-core": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-4.2.1.tgz", + "integrity": "sha512-8cH8/DEoD3e5Q6aeogdR5oaaKs0+mG6+f+Om0ZYt3PNv7Zo0sQhu4bMDRsqAF+UTekTAtP1W/C41jH/fkm8Jtw==", + "dev": true, + "requires": { + "add-stream": "^1.0.0", + "conventional-changelog-writer": "^4.0.18", + "conventional-commits-parser": "^3.2.0", + "dateformat": "^3.0.0", + "get-pkg-repo": "^1.0.0", + "git-raw-commits": "2.0.0", + "git-remote-origin-url": "^2.0.0", + "git-semver-tags": "^4.1.1", + "lodash": "^4.17.15", + "normalize-package-data": "^3.0.0", + "q": "^1.5.1", + "read-pkg": "^3.0.0", + "read-pkg-up": "^3.0.0", + "shelljs": "^0.8.3", + "through2": "^4.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "requires": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + } + }, + "dargs": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", + "integrity": "sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "git-raw-commits": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.0.tgz", + "integrity": "sha512-w4jFEJFgKXMQJ0H0ikBk2S+4KP2VEjhCvLCNqbNRQC8BgGWgLKNCO7a9K9LI+TVT7Gfoloje502sEnctibffgg==", + "dev": true, + "requires": { + "dargs": "^4.0.1", + "lodash.template": "^4.0.2", + "meow": "^4.0.0", + "split2": "^2.0.0", + "through2": "^2.0.0" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true + }, + "meow": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", + "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", + "dev": true, + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist": "^1.1.3", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0" + }, + "dependencies": { + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + } + } + }, + "minimist-options": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", + "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "quick-lru": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", + "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "dependencies": { + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + } + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "requires": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true + } + } + }, + "conventional-changelog-ember": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/conventional-changelog-ember/-/conventional-changelog-ember-2.0.9.tgz", + "integrity": "sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==", + "dev": true, + "requires": { + "q": "^1.5.1" + } + }, + "conventional-changelog-eslint": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/conventional-changelog-eslint/-/conventional-changelog-eslint-3.0.9.tgz", + "integrity": "sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==", + "dev": true, + "requires": { + "q": "^1.5.1" + } + }, + "conventional-changelog-express": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/conventional-changelog-express/-/conventional-changelog-express-2.0.6.tgz", + "integrity": "sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==", + "dev": true, + "requires": { + "q": "^1.5.1" + } + }, + "conventional-changelog-jquery": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/conventional-changelog-jquery/-/conventional-changelog-jquery-3.0.11.tgz", + "integrity": "sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==", + "dev": true, + "requires": { + "q": "^1.5.1" + } + }, + "conventional-changelog-jshint": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/conventional-changelog-jshint/-/conventional-changelog-jshint-2.0.9.tgz", + "integrity": "sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==", + "dev": true, + "requires": { + "compare-func": "^2.0.0", + "q": "^1.5.1" + } + }, + "conventional-changelog-preset-loader": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz", + "integrity": "sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==", + "dev": true + }, + "conventional-changelog-writer": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.18.tgz", + "integrity": "sha512-mAQDCKyB9HsE8Ko5cCM1Jn1AWxXPYV0v8dFPabZRkvsiWUul2YyAqbIaoMKF88Zf2ffnOPSvKhboLf3fnjo5/A==", + "dev": true, + "requires": { + "compare-func": "^2.0.0", + "conventional-commits-filter": "^2.0.7", + "dateformat": "^3.0.0", + "handlebars": "^4.7.6", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "semver": "^6.0.0", + "split": "^1.0.0", + "through2": "^4.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "conventional-commits-filter": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz", + "integrity": "sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==", + "dev": true, + "requires": { + "lodash.ismatch": "^4.4.0", + "modify-values": "^1.0.0" + } + }, + "conventional-commits-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.0.tgz", + "integrity": "sha512-XmJiXPxsF0JhAKyfA2Nn+rZwYKJ60nanlbSWwwkGwLQFbugsc0gv1rzc7VbbUWAzJfR1qR87/pNgv9NgmxtBMQ==", + "dev": true, + "requires": { + "JSONStream": "^1.0.4", + "is-text-path": "^1.0.1", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^2.0.0", + "through2": "^4.0.0", + "trim-off-newlines": "^1.0.0" + } + }, + "conventional-recommended-bump": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-6.0.10.tgz", + "integrity": "sha512-2ibrqAFMN3ZA369JgVoSbajdD/BHN6zjY7DZFKTHzyzuQejDUCjQ85S5KHxCRxNwsbDJhTPD5hOKcis/jQhRgg==", + "dev": true, + "requires": { + "concat-stream": "^2.0.0", + "conventional-changelog-preset-loader": "^2.3.4", + "conventional-commits-filter": "^2.0.6", + "conventional-commits-parser": "^3.1.0", + "git-raw-commits": "2.0.0", + "git-semver-tags": "^4.1.0", + "meow": "^7.0.0", + "q": "^1.5.1" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "requires": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + } + }, + "dargs": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", + "integrity": "sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "git-raw-commits": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.0.tgz", + "integrity": "sha512-w4jFEJFgKXMQJ0H0ikBk2S+4KP2VEjhCvLCNqbNRQC8BgGWgLKNCO7a9K9LI+TVT7Gfoloje502sEnctibffgg==", + "dev": true, + "requires": { + "dargs": "^4.0.1", + "lodash.template": "^4.0.2", + "meow": "^4.0.0", + "split2": "^2.0.0", + "through2": "^2.0.0" + }, + "dependencies": { + "meow": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", + "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", + "dev": true, + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist": "^1.1.3", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0" + } + } + } + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true + }, + "meow": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-7.1.1.tgz", + "integrity": "sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA==", + "dev": true, + "requires": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^2.5.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.13.1", + "yargs-parser": "^18.1.3" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "map-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", + "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==", + "dev": true + }, + "minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, + "trim-newlines": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz", + "integrity": "sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==", + "dev": true + } + } + }, + "minimist-options": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", + "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "quick-lru": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", + "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "requires": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true + }, + "type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } + } + } + } + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "cookies": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", + "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "dev": true, + "requires": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + } + } + }, + "core-js": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.7.0.tgz", + "integrity": "sha512-NwS7fI5M5B85EwpWuIwJN4i/fbisQUwLwiSNUWeXlkAZ0sbBjLEvLvFLf1uzAUV66PcEPt4xCGCmOZSxVf3xzA==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cosmiconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "cross-env": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.2.tgz", + "integrity": "sha512-KZP/bMEOJEDCkDQAyRhu3RL2ZO/SUVrxQVI0G3YEQ+OLbRA3c6zgixe8Mq8a/z7+HKlNEjo8oiLUs8iRijY2Rw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, + "dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", + "dev": true + }, + "dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "dev": true + }, + "debounce": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz", + "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==", + "dev": true + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "dev": true, + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + } + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "decompress": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "dev": true, + "requires": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, + "decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "requires": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "dependencies": { + "bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dev": true, + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + } + } + } + }, + "decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "requires": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "dependencies": { + "file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + } + } + }, + "decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "requires": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "dependencies": { + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + } + } + }, + "decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", + "dev": true, + "requires": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "dependencies": { + "file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", + "dev": true + }, + "get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, + "deep-freeze": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz", + "integrity": "sha1-OgsABd4YZygZ39OM0x+RF5yJPoQ=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, + "defer-to-connect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz", + "integrity": "sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg==", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "dependency-graph": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.9.0.tgz", + "integrity": "sha512-9YLIBURXj4DJMFALxXw9K3Y3rwb5Fk0X5/8ipCzaN84+gKxoHK43tVKRNakCQbiEx07E8Uwhuq21BpUagFhZ8w==", + "dev": true + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-indent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.0.0.tgz", + "integrity": "sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==", + "dev": true + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "devtools-protocol": { + "version": "0.0.818844", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.818844.tgz", + "integrity": "sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg==" + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "diff-sequences": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", + "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dot-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.3.tgz", + "integrity": "sha512-7hwEmg6RiSQfm/GwPL4AAWXKy3YNNZA3oFv2Pdiey0mwkRCPZ9x6SZbkLcn8Ma5PYeVokzoD4Twv2n7LKp5WeA==", + "dev": true, + "requires": { + "no-case": "^3.0.3", + "tslib": "^1.10.0" + } + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "dotgitignore": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/dotgitignore/-/dotgitignore-2.1.0.tgz", + "integrity": "sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "minimatch": "^3.0.4" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "download": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/download/-/download-7.1.0.tgz", + "integrity": "sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ==", + "dev": true, + "requires": { + "archive-type": "^4.0.0", + "caw": "^2.0.1", + "content-disposition": "^0.5.2", + "decompress": "^4.2.0", + "ext-name": "^5.0.0", + "file-type": "^8.1.0", + "filenamify": "^2.0.0", + "get-stream": "^3.0.0", + "got": "^8.3.1", + "make-dir": "^1.2.0", + "p-event": "^2.1.0", + "pify": "^3.0.0" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "got": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", + "integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==", + "dev": true, + "requires": { + "@sindresorhus/is": "^0.7.0", + "cacheable-request": "^2.1.1", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "into-stream": "^3.1.0", + "is-retry-allowed": "^1.1.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "mimic-response": "^1.0.0", + "p-cancelable": "^0.4.0", + "p-timeout": "^2.0.1", + "pify": "^3.0.0", + "safe-buffer": "^5.1.1", + "timed-out": "^4.0.1", + "url-parse-lax": "^3.0.0", + "url-to-options": "^1.0.1" + } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-module-lexer": { + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.3.26.tgz", + "integrity": "sha512-Va0Q/xqtrss45hWzP8CZJwzGSZJjDM5/MJRE3IXXnUCcVLElR9BRaE9F62BopysASyc4nM3uwhSW7FFB9nlWAA==", + "dev": true + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "esbuild": { + "version": "0.7.22", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.7.22.tgz", + "integrity": "sha512-B43SYg8LGWYTCv9Gs0RnuLNwjzpuWOoCaZHTWEDEf5AfrnuDMerPVMdCEu7xOdhFvQ+UqfP2MGU9lxEy0JzccA==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.13.0.tgz", + "integrity": "sha512-uCORMuOO8tUzJmsdRtrvcGq5qposf7Rw0LwkTJkoDbOycVQtQjmnhZSuLQnozLE4TmAzlMVV45eCHmQ1OpDKUQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.2.1", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.0", + "esquery": "^1.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "eslint-ast-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-ast-utils/-/eslint-ast-utils-1.1.0.tgz", + "integrity": "sha512-otzzTim2/1+lVrlH19EfQQJEhVJSu0zOb9ygb3iapN6UlyaDtyRq4b5U1FuW0v1lRa9Fp/GJyHkSwm6NqABgCA==", + "dev": true, + "requires": { + "lodash.get": "^4.4.2", + "lodash.zip": "^4.2.0" + } + }, + "eslint-config-prettier": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz", + "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + }, + "dependencies": { + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + } + } + }, + "eslint-import-resolver-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", + "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.13.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-module-utils": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", + "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "pkg-dir": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + } + } + }, + "eslint-plugin-import": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz", + "integrity": "sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "array.prototype.flat": "^1.2.3", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.4", + "eslint-module-utils": "^2.6.0", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.1", + "read-pkg-up": "^2.0.0", + "resolve": "^1.17.0", + "tsconfig-paths": "^3.9.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "eslint-plugin-mocha": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-8.0.0.tgz", + "integrity": "sha512-n67etbWDz6NQM+HnTwZHyBwz/bLlYPOxUbw7bPuCyFujv7ZpaT/Vn6KTAbT02gf7nRljtYIjWcTxK/n8a57rQQ==", + "dev": true, + "requires": { + "eslint-utils": "^2.1.0", + "ramda": "^0.27.1" + } + }, + "eslint-plugin-prettier": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz", + "integrity": "sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-plugin-unicorn": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-22.0.0.tgz", + "integrity": "sha512-jXPOauNiVFYLr+AeU3l21Ao+iDl/G08vUWui21RCI2L1TJIIoJvAMjMR6I+QPKr8FgIumzuR6gzDKCtEx2IkzA==", + "dev": true, + "requires": { + "ci-info": "^2.0.0", + "clean-regexp": "^1.0.0", + "eslint-ast-utils": "^1.1.0", + "eslint-template-visitor": "^2.2.1", + "eslint-utils": "^2.1.0", + "import-modules": "^2.0.0", + "lodash": "^4.17.20", + "pluralize": "^8.0.0", + "read-pkg-up": "^7.0.1", + "regexp-tree": "^0.1.21", + "reserved-words": "^0.1.2", + "safe-regex": "^2.1.1", + "semver": "^7.3.2" + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-template-visitor": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/eslint-template-visitor/-/eslint-template-visitor-2.2.1.tgz", + "integrity": "sha512-q3SxoBXz0XjPGkUpwGVAwIwIPIxzCAJX1uwfVc8tW3v7u/zS7WXNH3I2Mu2MDz2NgSITAyKLRaQFPHu/iyKxDQ==", + "dev": true, + "requires": { + "babel-eslint": "^10.1.0", + "eslint-visitor-keys": "^1.3.0", + "esquery": "^1.3.1", + "multimap": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + }, + "espree": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", + "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + } + } + }, + "executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "requires": { + "pify": "^2.2.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "expect": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-25.5.0.tgz", + "integrity": "sha512-w7KAXo0+6qqZZhovCaBVPSIqQp7/UTcx4M9uKt2m6pd2VB1voyC8JizLRqeEqud3AAVP02g+hbErDu5gu64tlA==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-styles": "^4.0.0", + "jest-get-type": "^25.2.6", + "jest-matcher-utils": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-regex-util": "^25.2.6" + } + }, + "ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "dev": true, + "requires": { + "mime-db": "^1.28.0" + } + }, + "ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "dev": true, + "requires": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "fast-glob": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", + "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fastq": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.9.0.tgz", + "integrity": "sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "requires": { + "pend": "~1.2.0" + } + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "file-type": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-8.1.0.tgz", + "integrity": "sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ==", + "dev": true + }, + "filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", + "dev": true + }, + "filenamify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", + "integrity": "sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==", + "dev": true, + "requires": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "dev": true, + "requires": { + "array-back": "^3.0.1" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "find-versions": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz", + "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==", + "dev": true, + "requires": { + "semver-regex": "^2.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "form-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "fs-access": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz", + "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=", + "dev": true, + "requires": { + "null-check": "^1.0.0" + } + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs-extra": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", + "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-pkg-repo": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz", + "integrity": "sha1-xztInAbYDMVTbCyFP54FIyBWly0=", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "meow": "^3.3.0", + "normalize-package-data": "^2.3.0", + "parse-github-repo-url": "^1.3.0", + "through2": "^2.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + } + } + }, + "get-proxy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", + "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", + "dev": true, + "requires": { + "npm-conf": "^1.1.0" + } + }, + "get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "dev": true + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "git-raw-commits": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.8.tgz", + "integrity": "sha512-6Gk7tQHGMLEL1bSnrMJTCVt2AQl4EmCcJDtzs/JJacCb2+TNEyHM67Gp7Ri9faF7OcGpjGGRjHLvs/AG7QKZ2Q==", + "dev": true, + "requires": { + "dargs": "^7.0.0", + "lodash.template": "^4.0.2", + "meow": "^8.0.0", + "split2": "^2.0.0", + "through2": "^4.0.0" + } + }, + "git-remote-origin-url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", + "integrity": "sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=", + "dev": true, + "requires": { + "gitconfiglocal": "^1.0.0", + "pify": "^2.3.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "git-semver-tags": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-4.1.1.tgz", + "integrity": "sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==", + "dev": true, + "requires": { + "meow": "^8.0.0", + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "gitconfiglocal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", + "integrity": "sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=", + "dev": true, + "requires": { + "ini": "^1.3.2" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "dev": true, + "requires": { + "ini": "^1.3.4" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "globby": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", + "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "got": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.0.tgz", + "integrity": "sha512-k9noyoIIY9EejuhaBNLyZ31D5328LeqnyPNXJQb2XlJZcKakLqN5m6O/ikhq/0lw56kUYS54fVm+D1x57YC9oQ==", + "dev": true, + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.1", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "dependencies": { + "@sindresorhus/is": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.0.tgz", + "integrity": "sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ==", + "dev": true + }, + "cacheable-request": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.1.tgz", + "integrity": "sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^2.0.0" + } + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "requires": { + "mimic-response": "^3.1.0" + } + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "keyv": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.3.tgz", + "integrity": "sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true + }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", + "dev": true + }, + "p-cancelable": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz", + "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==", + "dev": true + }, + "responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "dev": true, + "requires": { + "lowercase-keys": "^2.0.0" + } + } + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "handlebars": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "dev": true, + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, + "hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-symbol-support-x": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", + "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", + "dev": true + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "has-to-string-tag-x": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", + "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", + "dev": true, + "requires": { + "has-symbol-support-x": "^1.4.1" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "header-case": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.3.tgz", + "integrity": "sha512-LChe/V32mnUQnTwTxd3aAlNMk8ia9tjCDb/LjYtoMrdAPApxLB+azejUk5ERZIZdIqvinwv6BAUuFXH/tQPdZA==", + "dev": true, + "requires": { + "capital-case": "^1.0.3", + "tslib": "^1.10.0" + } + }, + "hosted-git-info": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.7.tgz", + "integrity": "sha512-fWqc0IcuXs+BmE9orLDyVykAG9GJtGLGuZAAqgcckPgv5xad4AcXGIv8galtQvlwutxSlaMcdw7BUtq2EIvqCQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "http-assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.1.tgz", + "integrity": "sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw==", + "dev": true, + "requires": { + "deep-equal": "~1.0.1", + "http-errors": "~1.7.2" + }, + "dependencies": { + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + } + } + }, + "http-cache-semantics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", + "dev": true + }, + "http-errors": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + } + } + }, + "http2-wrapper": { + "version": "1.0.0-beta.5.2", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz", + "integrity": "sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ==", + "dev": true, + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "dependencies": { + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true + } + } + }, + "https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "requires": { + "agent-base": "5", + "debug": "4" + } + }, + "husky": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.0.tgz", + "integrity": "sha512-tTMeLCLqSBqnflBZnlVDhpaIMucSGaYyX6855jM4AguGeWCeSzNdb1mfyWduTZ3pe3SJVvVWGL0jO1iKZVPfTA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "ci-info": "^2.0.0", + "compare-versions": "^3.6.0", + "cosmiconfig": "^7.0.0", + "find-versions": "^3.2.0", + "opencollective-postinstall": "^2.0.2", + "pkg-dir": "^4.2.0", + "please-upgrade-node": "^3.2.0", + "slash": "^3.0.0", + "which-pm-runs": "^1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", + "dev": true + }, + "import-fresh": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz", + "integrity": "sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, + "import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true + }, + "import-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-modules/-/import-modules-2.0.0.tgz", + "integrity": "sha512-iczM/v9drffdNnABOKwj0f9G3cFDon99VcG1mxeBsdqnbd+vnQ5c2uAiCHNQITqFTOPaEvwg3VjoWCur0uHLEw==", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "inflation": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", + "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true + }, + "into-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", + "integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=", + "dev": true, + "requires": { + "from2": "^2.1.1", + "p-is-promise": "^1.1.0" + } + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true + }, + "is-core-module": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz", + "integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-docker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", + "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, + "is-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", + "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", + "dev": true + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "dev": true + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", + "dev": true, + "requires": { + "text-extensions": "^1.0.0" + } + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isbinaryfile": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.6.tgz", + "integrity": "sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "isurl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", + "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", + "dev": true, + "requires": { + "has-to-string-tag-x": "^1.2.0", + "is-object": "^1.0.1" + } + }, + "jest-diff": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", + "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "diff-sequences": "^25.2.6", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + }, + "dependencies": { + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-get-type": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", + "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", + "dev": true + }, + "jest-matcher-utils": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz", + "integrity": "sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "jest-diff": "^25.5.0", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + }, + "dependencies": { + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-message-util": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.5.0.tgz", + "integrity": "sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^25.5.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "slash": "^3.0.0", + "stack-utils": "^1.0.1" + }, + "dependencies": { + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-regex-util": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", + "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", + "dev": true + }, + "jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=", + "dev": true + }, + "jpeg-js": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.3.7.tgz", + "integrity": "sha512-9IXdWudL61npZjvLuVe/ktHiA41iE8qFyLB+4VDTblEsWBzeg8WQTlktdUK4CdncUqtUgUg0bbOmTE2bKBKaBQ==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + }, + "dependencies": { + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } + } + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "jszip": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.5.0.tgz", + "integrity": "sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==", + "dev": true, + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "just-extend": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.1.tgz", + "integrity": "sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==", + "dev": true + }, + "keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dev": true, + "requires": { + "tsscmp": "1.0.6" + } + }, + "keyv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", + "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", + "dev": true, + "requires": { + "json-buffer": "3.0.0" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "koa": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.13.0.tgz", + "integrity": "sha512-i/XJVOfPw7npbMv67+bOeXr3gPqOAw6uh5wFyNs3QvJ47tUx3M3V9rIE0//WytY42MKz4l/MXKyGkQ2LQTfLUQ==", + "dev": true, + "requires": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.8.0", + "debug": "~3.1.0", + "delegates": "^1.0.0", + "depd": "^1.1.2", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^1.2.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "dev": true + }, + "koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "dev": true, + "requires": { + "co": "^4.6.0", + "koa-compose": "^3.0.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "dev": true, + "requires": { + "any-promise": "^1.1.0" + } + } + } + }, + "koa-etag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/koa-etag/-/koa-etag-3.0.0.tgz", + "integrity": "sha1-nvc4Ld1agqsN6xU0FckVg293HT8=", + "dev": true, + "requires": { + "etag": "^1.3.0", + "mz": "^2.1.0" + } + }, + "koa-send": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", + "integrity": "sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "http-errors": "^1.7.3", + "resolve-path": "^1.4.0" + } + }, + "koa-static": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", + "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "koa-send": "^5.0.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "requires": { + "immediate": "~3.0.5" + } + }, + "lighthouse-logger": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.2.0.tgz", + "integrity": "sha512-wzUvdIeJZhRsG6gpZfmSCfysaxNEr43i+QT+Hie94wvHDKFLi4n7C2GqZ4sTC+PH5b5iktmXJvU87rWvhP3lHw==", + "dev": true, + "requires": { + "debug": "^2.6.8", + "marky": "^1.2.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + }, + "lodash.ismatch": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", + "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0" + } + }, + "lodash.zip": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", + "integrity": "sha1-7GZi5IlkCO1KtsVCo5kLcswIACA=", + "dev": true + }, + "log-symbols": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "dev": true, + "requires": { + "chalk": "^4.0.0" + } + }, + "log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "requires": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lower-case": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz", + "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "map-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", + "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==", + "dev": true + }, + "marky": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.1.tgz", + "integrity": "sha512-md9k+Gxa3qLH6sUKpeC2CNkJK/Ld+bEz5X96nYwloqphQE0CKCVEKco/6jxEZixinqNdz5RFi/KaCyfbMDMAXQ==", + "dev": true + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "meow": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.0.0.tgz", + "integrity": "sha512-nbsTRz2fwniJBFgUkcdISq8y/q9n9VbiHYbfwklFh5V4V2uAcxtKQkDc0yCLPM/kP0d+inZBewn3zJqewHE7kg==", + "dev": true, + "requires": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + } + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "mime": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", + "dev": true + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "dev": true + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "dev": true, + "requires": { + "mime-db": "1.44.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true + }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "mocha": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.2.1.tgz", + "integrity": "sha512-cuLBVfyFfFqbNR0uUKbDGXKGk+UDFe6aR4os78XIrMQpZl/nv7JYHcvP5MFIAb374b2zFXsdgEGwmzMtP0Xg8w==", + "dev": true, + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.4.3", + "debug": "4.2.0", + "diff": "4.0.2", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.6", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.14.0", + "log-symbols": "4.0.0", + "minimatch": "3.0.4", + "ms": "2.1.2", + "nanoid": "3.1.12", + "serialize-javascript": "5.0.1", + "strip-json-comments": "3.1.1", + "supports-color": "7.2.0", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.0.2", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "p-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", + "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "modify-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", + "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "multimap": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multimap/-/multimap-1.1.0.tgz", + "integrity": "sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw==", + "dev": true + }, + "mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "requires": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "nanoid": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.12.tgz", + "integrity": "sha512-1qstj9z5+x491jfiC4Nelk+f8XBad7LN20PmyWINJEMRSf3wcAjAWysw1qaA8z6NSKe2sjq1hRSDpBH5paCb6A==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "dev": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "nise": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz", + "integrity": "sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "no-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", + "integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==", + "dev": true, + "requires": { + "lower-case": "^2.0.1", + "tslib": "^1.10.0" + } + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, + "normalize-package-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.0.tgz", + "integrity": "sha512-6lUjEI0d3v6kFrtgA/lOx4zHCWULXsFNIjHolnZCKCTLA6m/G625cdn3O7eNmT0iD3jfo6HZ9cdImGZwf21prw==", + "dev": true, + "requires": { + "hosted-git-info": "^3.0.6", + "resolve": "^1.17.0", + "semver": "^7.3.2", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-url": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", + "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", + "dev": true, + "requires": { + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" + }, + "dependencies": { + "sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + } + } + }, + "npm-conf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", + "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", + "dev": true, + "requires": { + "config-chain": "^1.1.11", + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "null-check": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz", + "integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.values": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", + "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=", + "dev": true + }, + "open": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/open/-/open-7.3.0.tgz", + "integrity": "sha512-mgLwQIx2F/ye9SmbrUkurZCnkoXyXyu9EbHtJZrICjVAJfyMArdHp3KkixGdZx1ZHFPNIwl0DDM1dFFqXbTLZw==", + "dev": true, + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + } + }, + "opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", + "dev": true + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "os-filter-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz", + "integrity": "sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==", + "dev": true, + "requires": { + "arch": "^2.1.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-cancelable": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", + "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", + "dev": true + }, + "p-event": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-2.3.1.tgz", + "integrity": "sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA==", + "dev": true, + "requires": { + "p-timeout": "^2.0.1" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-is-promise": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", + "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-timeout": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", + "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", + "dev": true, + "requires": { + "p-finally": "^1.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "param-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.3.tgz", + "integrity": "sha512-VWBVyimc1+QrzappRs7waeN2YmoZFCGXWASRYX1/rGHtXqEcrGEIDm+jqIwFa2fRXNgQEwrxaYuIrX0WcAguTA==", + "dev": true, + "requires": { + "dot-case": "^3.0.3", + "tslib": "^1.10.0" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-github-repo-url": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz", + "integrity": "sha1-nn2LslKmy2ukJZUGC3v23z28H1A=", + "dev": true + }, + "parse-json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "pascal-case": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.1.tgz", + "integrity": "sha512-XIeHKqIrsquVTQL2crjq3NfJUxmdLasn3TYOU0VBM+UX2a6ztAWBlJQBePLGY7VHW8+2dRadeIPK5+KImwTxQA==", + "dev": true, + "requires": { + "no-case": "^3.0.3", + "tslib": "^1.10.0" + } + }, + "path-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.3.tgz", + "integrity": "sha512-UMFU6UETFpCNWbIWNczshPrnK/7JAXBP2NYw80ojElbQ2+JYxdqWDBkvvqM93u4u6oLmuJ/tPOf2tM8KtXv4eg==", + "dev": true, + "requires": { + "dot-case": "^3.0.3", + "tslib": "^1.10.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pixelmatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", + "integrity": "sha1-j0fc7FARtHe2fbA8JDvB8wheiFQ=", + "dev": true, + "requires": { + "pngjs": "^3.0.0" + }, + "dependencies": { + "pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "dev": true + } + } + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "requires": { + "find-up": "^4.0.0" + } + }, + "please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "dev": true, + "requires": { + "semver-compare": "^1.0.0" + } + }, + "pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true + }, + "pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "dev": true + }, + "portfinder": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", + "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", + "dev": true, + "requires": { + "async": "^2.6.2", + "debug": "^3.1.1", + "mkdirp": "^0.5.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true + }, + "prettier": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.0.tgz", + "integrity": "sha512-yYerpkvseM4iKD/BXLYUkQV5aKt4tQPqaGW6EsZjzyu0r7sVZZNPJW4Y8MyKmicp6t42XUPcBVA+H6sB3gqndw==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", + "dev": true + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "puppeteer-core": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-5.5.0.tgz", + "integrity": "sha512-tlA+1n+ziW/Db03hVV+bAecDKse8ihFRXYiEypBe9IlLRvOCzYFG6qrCMBYK34HO/Q/Ecjc+tvkHRAfLVH+NgQ==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "devtools-protocol": "0.0.818844", + "extract-zip": "^2.0.0", + "https-proxy-agent": "^4.0.0", + "node-fetch": "^2.6.1", + "pkg-dir": "^4.2.0", + "progress": "^2.0.1", + "proxy-from-env": "^1.0.0", + "rimraf": "^3.0.2", + "tar-fs": "^2.0.0", + "unbzip2-stream": "^1.3.3", + "ws": "^7.2.3" + } + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==", + "dev": true + }, + "query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "dev": true, + "requires": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true + }, + "ramda": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", + "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "raw-body": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", + "integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.3", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + } + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, + "reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", + "dev": true + }, + "regexp-tree": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.21.tgz", + "integrity": "sha512-kUUXjX4AnqnR8KRTCrayAo9PzYMRKmVoGgaz2tBuz0MF3g1ZbGebmtW0yFHfFK9CmBjQKeYIgoL22pFLBJY7sw==", + "dev": true + }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "reserved-words": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/reserved-words/-/reserved-words-0.1.2.tgz", + "integrity": "sha1-AKCUD5jNUBrqqsMWQR2a3FKzGrE=", + "dev": true + }, + "resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } + }, + "resolve-alpn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.0.0.tgz", + "integrity": "sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA==", + "dev": true + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve-global": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz", + "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==", + "dev": true, + "requires": { + "global-dirs": "^0.1.1" + } + }, + "resolve-path": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", + "integrity": "sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc=", + "dev": true, + "requires": { + "http-errors": "~1.6.2", + "path-is-absolute": "1.0.1" + }, + "dependencies": { + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + } + } + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "dev": true, + "requires": { + "lowercase-keys": "^1.0.0" + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "rollup": { + "version": "2.33.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.33.3.tgz", + "integrity": "sha512-RpayhPTe4Gu/uFGCmk7Gp5Z9Qic2VsqZ040G+KZZvsZYdcuWaJg678JeDJJvJeEQXminu24a2au+y92CUWVd+w==", + "dev": true, + "requires": { + "fsevents": "~2.1.2" + } + }, + "run-parallel": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz", + "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "dev": true, + "requires": { + "regexp-tree": "~0.1.1" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "saucelabs": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-4.5.2.tgz", + "integrity": "sha512-D5T+KMFMi2PFS64Qhsjc/ibO9hSGRTC2VDi0D4MXvuNkbEc9vT8yx+l7PwrLlnDoN8jfJdpKiCrTe4Of3FpRvw==", + "dev": true, + "requires": { + "bin-wrapper": "^4.1.0", + "change-case": "^4.1.1", + "form-data": "^3.0.0", + "got": "^11.7.0", + "hash.js": "^1.1.7", + "tunnel": "0.0.6", + "yargs": "^16.0.3" + }, + "dependencies": { + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", + "integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==", + "dev": true + }, + "yargs": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.1.1.tgz", + "integrity": "sha512-hAD1RcFP/wfgfxgMVswPE+z3tlPFtxG8/yWUrG2i17sTWGCGqWnxKcLTF4cUKDUK8fzokwsmO9H0TDkRbMHy8w==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + } + } + }, + "seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "dev": true, + "requires": { + "commander": "^2.8.1" + } + }, + "selenium-webdriver": { + "version": "4.0.0-alpha.7", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.7.tgz", + "integrity": "sha512-D4qnTsyTr91jT8f7MfN+OwY0IlU5+5FmlO5xlgRUV6hDEV8JyYx2NerdTEqDDkNq7RZDYc4VoPALk8l578RBHw==", + "dev": true, + "requires": { + "jszip": "^3.2.2", + "rimraf": "^2.7.1", + "tmp": "0.0.30" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", + "dev": true + }, + "semver-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", + "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", + "dev": true + }, + "semver-truncate": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-1.1.2.tgz", + "integrity": "sha1-V/Qd5pcHpicJp+AQS6IRcQnqR+g=", + "dev": true, + "requires": { + "semver": "^5.3.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "sentence-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.3.tgz", + "integrity": "sha512-ZPr4dgTcNkEfcGOMFQyDdJrTU9uQO1nb1cjf+nuzb6FxgMDgKddZOM29qEsB7jvsZSMruLRcL2KfM4ypKpa0LA==", + "dev": true, + "requires": { + "no-case": "^3.0.3", + "tslib": "^1.10.0", + "upper-case-first": "^2.0.1" + } + }, + "serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shelljs": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz", + "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "sinon": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.1.tgz", + "integrity": "sha512-naPfsamB5KEE1aiioaoqJ6MEhdUs/2vtI5w1hPAXX/UwvoPjXcwh1m5HiKx0HGgKR8lQSoFIgY5jM6KK8VrS9w==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.1", + "@sinonjs/fake-timers": "^6.0.1", + "@sinonjs/formatio": "^5.0.1", + "@sinonjs/samsam": "^5.2.0", + "diff": "^4.0.2", + "nise": "^4.0.4", + "supports-color": "^7.1.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "snake-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.3.tgz", + "integrity": "sha512-WM1sIXEO+rsAHBKjGf/6R1HBBcgbncKS08d2Aqec/mrDSpU80SiOU41hO7ny6DToHSyrlwTYzQBIK1FPSx4Y3Q==", + "dev": true, + "requires": { + "dot-case": "^3.0.3", + "tslib": "^1.10.0" + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", + "dev": true, + "requires": { + "sort-keys": "^1.0.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz", + "integrity": "sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==", + "dev": true + }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "requires": { + "through": "2" + } + }, + "split2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", + "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", + "dev": true, + "requires": { + "through2": "^2.0.2" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "stack-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.3.tgz", + "integrity": "sha512-WldO+YmqhEpjp23eHZRhOT1NQF51STsbxZ+/AdpFD+EhheFxAe5d0WoK4DQVJkSHacPrJJX3OqRAl9CgHf78pg==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, + "standard-version": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/standard-version/-/standard-version-9.0.0.tgz", + "integrity": "sha512-eRR04IscMP3xW9MJTykwz13HFNYs8jS33AGuDiBKgfo5YrO0qX0Nxb4rjupVwT5HDYL/aR+MBEVLjlmVFmFEDQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "conventional-changelog": "3.1.23", + "conventional-changelog-config-spec": "2.1.0", + "conventional-changelog-conventionalcommits": "4.4.0", + "conventional-recommended-bump": "6.0.10", + "detect-indent": "^6.0.0", + "detect-newline": "^3.1.0", + "dotgitignore": "^2.1.0", + "figures": "^3.1.0", + "find-up": "^4.1.0", + "fs-access": "^1.0.1", + "git-semver-tags": "^4.0.0", + "semver": "^7.1.1", + "stringify-package": "^1.0.1", + "yargs": "^15.3.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "conventional-changelog-conventionalcommits": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.4.0.tgz", + "integrity": "sha512-ybvx76jTh08tpaYrYn/yd0uJNLt5yMrb1BphDe4WBredMlvPisvMghfpnJb6RmRNcqXeuhR6LfGZGewbkRm9yA==", + "dev": true, + "requires": { + "compare-func": "^2.0.0", + "lodash": "^4.17.15", + "q": "^1.5.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + }, + "string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "string.prototype.repeat": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz", + "integrity": "sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8=", + "dev": true + }, + "string.prototype.trimend": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz", + "integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "string.prototype.trimstart": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz", + "integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "stringify-package": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stringify-package/-/stringify-package-1.0.1.tgz", + "integrity": "sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "requires": { + "is-natural-number": "^4.0.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "table-layout": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.1.tgz", + "integrity": "sha512-dEquqYNJiGwY7iPfZ3wbXDI944iqanTSchrACLL2nOB+1r+h1Nzu2eH+DuPPvWvm5Ry7iAPeFlgEtP5bIp5U7Q==", + "dev": true, + "requires": { + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" + }, + "dependencies": { + "array-back": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.1.tgz", + "integrity": "sha512-Z/JnaVEXv+A9xabHzN43FiiiWEE7gPCRXMrVmRm00tWbjZRul1iHm7ECzlyNq1p4a4ATXz+G9FJ3GqGOkOV3fg==", + "dev": true + }, + "typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true + } + } + }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz", + "integrity": "sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "text-diff": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/text-diff/-/text-diff-1.0.1.tgz", + "integrity": "sha1-bBBZBUNeM3hXN1ydL2ymPkU/9WU=", + "dev": true + }, + "text-extensions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", + "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", + "dev": true + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "requires": { + "any-promise": "^1.0.0" + } + }, + "thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", + "dev": true, + "requires": { + "thenify": ">= 3.1.0 < 4" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "requires": { + "readable-stream": "3" + } + }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "dev": true + }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", + "dev": true + }, + "tmp": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz", + "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.1" + } + }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, + "tr46": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz", + "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "trim-newlines": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz", + "integrity": "sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==", + "dev": true + }, + "trim-off-newlines": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", + "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=", + "dev": true + }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "ts-node": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz", + "integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + }, + "tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "dev": true + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typescript": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.5.tgz", + "integrity": "sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==", + "dev": true + }, + "typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "dev": true + }, + "ua-parser-js": { + "version": "0.7.22", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.22.tgz", + "integrity": "sha512-YUxzMjJ5T71w6a8WWVcMGM6YWOTX27rCoIQgLXiWaxqXSx9D7DNjiGWn1aJIRSQ5qr0xuhra77bSIh6voR/46Q==", + "dev": true + }, + "uglify-js": { + "version": "3.11.6", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.11.6.tgz", + "integrity": "sha512-oASI1FOJ7BBFkSCNDZ446EgkSuHkOZBuqRFrwXIKWCoXw8ZXQETooTQjkAcBS03Acab7ubCKsXnwuV2svy061g==", + "dev": true, + "optional": true + }, + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "upper-case": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.1.tgz", + "integrity": "sha512-laAsbea9SY5osxrv7S99vH9xAaJKrw5Qpdh4ENRLcaxipjKsiaBwiAsxfa8X5mObKNTQPsupSq0J/VIxsSJe3A==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + }, + "upper-case-first": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.1.tgz", + "integrity": "sha512-105J8XqQ+9RxW3l9gHZtgve5oaiR9TIwvmZAMAIZWRHe00T21cdvewKORTlOJf/zXW6VukuTshM+HXZNWz7N5w==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + }, + "uri-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "dev": true, + "requires": { + "prepend-http": "^2.0.0" + } + }, + "url-to-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", + "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==", + "dev": true + }, + "v8-compile-cache": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", + "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", + "dev": true + }, + "v8-to-istanbul": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-7.0.0.tgz", + "integrity": "sha512-fLL2rFuQpMtm9r8hrAV2apXX/WqHJ6+IC4/eQVdMDGBUgH/YMV4Gv3duk3kjmyg6uiQWBAA9nJwue4iJUOkHeA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validator": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-8.2.0.tgz", + "integrity": "sha512-Yw5wW34fSv5spzTXNkokD6S6/Oq92d8q/t14TqsS3fAiA1RYnxSFSIZ+CY3n6PGGRCq5HhJTSepQvFUS2QUDxA==", + "dev": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true + }, + "whatwg-url": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz", + "integrity": "sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^2.0.2", + "webidl-conversions": "^6.1.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "which-pm-runs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wordwrapjs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.0.tgz", + "integrity": "sha512-Svqw723a3R34KvsMgpjFBYCgNOSdcW3mQFK4wIfhGQhtaFVOJmdYoXgi63ne3dTlWgatVcUc7t4HtQ/+bUVIzQ==", + "dev": true, + "requires": { + "reduce-flatten": "^2.0.0", + "typical": "^5.0.0" + }, + "dependencies": { + "typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true + } + } + }, + "workerpool": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.2.tgz", + "integrity": "sha512-DSNyvOpFKrNusaaUwk+ej6cBj1bmhLcBfj80elGk+ZIo5JSkq+unB1dLKEOcNfJDZgjGICfhQ0Q5TbP0PvF4+Q==", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "ws": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz", + "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yaml": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", + "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==", + "dev": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "dependencies": { + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + } + } + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "ylru": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", + "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==", + "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, + "z-schema": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.18.4.tgz", + "integrity": "sha512-DUOKC/IhbkdLKKiV89gw9DUauTV8U/8yJl1sjf6MtDmzevLKOF2duNJ495S3MFVjqZarr+qNGCPbkg4mu4PpLw==", + "dev": true, + "requires": { + "commander": "^2.7.1", + "lodash.get": "^4.0.0", + "lodash.isequal": "^4.0.0", + "validator": "^8.0.0" + } + } + } +} diff --git a/remote/test/puppeteer/package.json b/remote/test/puppeteer/package.json new file mode 100644 index 0000000000..f124546778 --- /dev/null +++ b/remote/test/puppeteer/package.json @@ -0,0 +1,115 @@ +{ + "name": "puppeteer", + "version": "5.5.0", + "description": "A high-level API to control headless Chrome over the DevTools Protocol", + "main": "./cjs-entry.js", + "repository": "github:puppeteer/puppeteer", + "engines": { + "node": ">=10.18.1" + }, + "scripts": { + "test-browser": "wtr", + "test-browser-watch": "wtr --watch", + "unit": "npm run tsc-cjs && mocha --config mocha-config/puppeteer-unit-tests.js", + "unit-debug": "npm run tsc-cjs && mocha --inspect-brk --config mocha-config/puppeteer-unit-tests.js", + "unit-with-coverage": "cross-env COVERAGE=1 npm run unit", + "assert-unit-coverage": "cross-env COVERAGE=1 mocha --config mocha-config/coverage-tests.js", + "funit": "PUPPETEER_PRODUCT=firefox npm run unit", + "test": "npm run tsc && npm run lint --silent && npm run unit-with-coverage && npm run test-browser", + "prepare": "node typescript-if-required.js", + "prepublishOnly": "npm run tsc", + "dev-install": "npm run tsc && node install.js", + "install": "node install.js", + "eslint": "([ \"$CI\" = true ] && eslint --ext js --ext ts --quiet -f codeframe . || eslint --ext js --ext ts .)", + "eslint-fix": "eslint --ext js --ext ts --fix .", + "commitlint": "commitlint --from=HEAD~1", + "lint": "npm run eslint && npm run tsc && npm run doc && npm run commitlint", + "doc": "node utils/doclint/cli.js", + "clean-lib": "rm -rf lib", + "build": "npm run tsc", + "tsc": "npm run clean-lib && tsc --version && npm run tsc-cjs && npm run tsc-esm", + "tsc-cjs": "tsc -b src/tsconfig.cjs.json", + "tsc-esm": "tsc -b src/tsconfig.esm.json", + "apply-next-version": "node utils/apply_next_version.js", + "test-install": "scripts/test-install.sh", + "generate-docs": "npm run tsc && api-extractor run --local --verbose && api-documenter markdown -i temp -o new-docs", + "ensure-correct-devtools-protocol-revision": "ts-node -s scripts/ensure-correct-devtools-protocol-package", + "release": "standard-version" + }, + "files": [ + "lib/", + "install.js", + "typescript-if-required.js", + "cjs-entry.js", + "cjs-entry-core.js" + ], + "author": "The Chromium Authors", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0", + "devtools-protocol": "0.0.818844", + "extract-zip": "^2.0.0", + "https-proxy-agent": "^4.0.0", + "node-fetch": "^2.6.1", + "pkg-dir": "^4.2.0", + "progress": "^2.0.1", + "proxy-from-env": "^1.0.0", + "rimraf": "^3.0.2", + "tar-fs": "^2.0.0", + "unbzip2-stream": "^1.3.3", + "ws": "^7.2.3" + }, + "devDependencies": { + "@commitlint/cli": "^11.0.0", + "@commitlint/config-conventional": "^11.0.0", + "@microsoft/api-documenter": "7.9.7", + "@microsoft/api-extractor": "7.10.4", + "@types/debug": "0.0.31", + "@types/mime": "^2.0.0", + "@types/mocha": "^7.0.2", + "@types/node": "^14.0.13", + "@types/proxy-from-env": "^1.0.1", + "@types/rimraf": "^2.0.2", + "@types/sinon": "^9.0.4", + "@types/tar-fs": "^1.16.2", + "@types/ws": "^7.2.4", + "@typescript-eslint/eslint-plugin": "^4.4.0", + "@typescript-eslint/parser": "^4.4.0", + "@web/test-runner": "^0.9.2", + "commonmark": "^0.28.1", + "cross-env": "^7.0.2", + "eslint": "^7.10.0", + "eslint-config-prettier": "^6.12.0", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-mocha": "^8.0.0", + "eslint-plugin-prettier": "^3.1.4", + "eslint-plugin-unicorn": "^22.0.0", + "esprima": "^4.0.0", + "expect": "^25.2.7", + "husky": "^4.3.0", + "jpeg-js": "^0.3.7", + "mime": "^2.0.3", + "minimist": "^1.2.0", + "mocha": "^8.2.0", + "ncp": "^2.0.0", + "pixelmatch": "^4.0.2", + "pngjs": "^5.0.0", + "prettier": "^2.1.2", + "sinon": "^9.0.2", + "standard-version": "^9.0.0", + "text-diff": "^1.0.1", + "ts-node": "^9.0.0", + "typescript": "3.9.5" + }, + "husky": { + "hooks": { + "commit-msg": "commitlint --env HUSKY_GIT_PARAMS" + } + }, + "standard-version": { + "skip": { + "commit": true, + "tag": true + } + } +} diff --git a/remote/test/puppeteer/prettier.config.js b/remote/test/puppeteer/prettier.config.js new file mode 100644 index 0000000000..4c5c41c687 --- /dev/null +++ b/remote/test/puppeteer/prettier.config.js @@ -0,0 +1,5 @@ +module.exports = { + semi: true, + trailingComma: 'es5', + singleQuote: true, +}; diff --git a/remote/test/puppeteer/scripts/ensure-correct-devtools-protocol-package.ts b/remote/test/puppeteer/scripts/ensure-correct-devtools-protocol-package.ts new file mode 100644 index 0000000000..2d6a4a1d1b --- /dev/null +++ b/remote/test/puppeteer/scripts/ensure-correct-devtools-protocol-package.ts @@ -0,0 +1,84 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This script ensures that the pinned version of devtools-protocol in + * package.json is the right version for the current revision of Chromium that + * Puppeteer ships with. + * + * The devtools-protocol package publisher runs every hour and checks if there + * are protocol changes. If there are, it will be versioned with the revision + * number of the commit that last changed the .pdl files. + * + * Chromium branches/releases are figured out at a later point in time, so it's + * not true that each Chromium revision will have an exact matching revision + * version of devtools-protocol. To ensure we're using a devtools-protocol that + * is aligned with our revision, we want to find the largest package number + * that's <= the revision that Puppeteer is using. + * + * This script uses npm's `view` function to list all versions in a range and + * find the one closest to our Chromium revision. + */ + +// eslint-disable-next-line import/extensions +import { PUPPETEER_REVISIONS } from '../src/revisions'; +import { execSync } from 'child_process'; + +import packageJson from '../package.json'; + +const currentProtocolPackageInstalledVersion = + packageJson.dependencies['devtools-protocol']; + +/** + * Ensure that the devtools-protocol version is pinned. + */ +if (/^[^0-9]/.test(currentProtocolPackageInstalledVersion)) { + console.log( + `ERROR: devtools-protocol package is not pinned to a specific version.\n` + ); + process.exit(1); +} + +// find the right revision for our Chromium revision + +const command = `npm view "devtools-protocol@<=0.0.${PUPPETEER_REVISIONS.chromium}" version | tail -1`; + +console.log( + 'Checking npm for devtools-protocol revisions:\n', + `'${command}'`, + '\n' +); + +const output = execSync(command, { + encoding: 'utf8', +}); + +const bestRevisionFromNpm = output.split(' ')[1].replace(/'|\n/g, ''); + +if (currentProtocolPackageInstalledVersion !== bestRevisionFromNpm) { + console.log(`ERROR: bad devtools-protocol revision detected: + + Current Puppeteer Chromium revision: ${PUPPETEER_REVISIONS.chromium} + Current devtools-protocol version in package.json: ${currentProtocolPackageInstalledVersion} + Expected devtools-protocol version: ${bestRevisionFromNpm}`); + + process.exit(1); +} + +console.log( + `Correct devtools-protocol version found (${bestRevisionFromNpm}).` +); +process.exit(0); diff --git a/remote/test/puppeteer/scripts/test-install.sh b/remote/test/puppeteer/scripts/test-install.sh new file mode 100755 index 0000000000..e59aa39d00 --- /dev/null +++ b/remote/test/puppeteer/scripts/test-install.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env sh +set -e + +ROOTDIR="$(pwd)" +# Pack the module into a tarball +npm pack +tarball="$(realpath puppeteer-*.tgz)" +TMPDIR="$(mktemp -d)" +cd $TMPDIR +# Check we can install from the tarball. +# This emulates installing from npm and ensures that: +# 1. we publish the right files in the `files` list from package.json +# 2. The install script works and correctly exits without errors +# 3. Requiring Puppeteer from Node works. +npm install --loglevel silent "${tarball}" +node --eval="require('puppeteer')" +ls $TMPDIR/node_modules/puppeteer/.local-chromium/ + +# Again for Firefox +TMPDIR="$(mktemp -d)" +cd $TMPDIR +PUPPETEER_PRODUCT=firefox npm install --loglevel silent "${tarball}" +node --eval="require('puppeteer')" +rm "${tarball}" +ls $TMPDIR/node_modules/puppeteer/.local-firefox/linux-*/firefox/firefox + +# Again for puppeteer-core +cd $ROOTDIR +node ./utils/prepare_puppeteer_core.js +npm pack +tarball="$(realpath puppeteer-core-*.tgz)" +TMPDIR="$(mktemp -d)" +cd $TMPDIR +# Check we can install from the tarball. +# This emulates installing from npm and ensures that: +# 1. we publish the right files in the `files` list from package.json +# 2. The install script works and correctly exits without errors +# 3. Requiring Puppeteer Core from Node works. +npm install --loglevel silent "${tarball}" +node --eval="require('puppeteer-core')" + diff --git a/remote/test/puppeteer/scripts/tsconfig.json b/remote/test/puppeteer/scripts/tsconfig.json new file mode 100644 index 0000000000..c8d842173c --- /dev/null +++ b/remote/test/puppeteer/scripts/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "module": "CommonJS" + } +} diff --git a/remote/test/puppeteer/src/.eslintrc.js b/remote/test/puppeteer/src/.eslintrc.js new file mode 100644 index 0000000000..4ebb9bb1ec --- /dev/null +++ b/remote/test/puppeteer/src/.eslintrc.js @@ -0,0 +1,19 @@ +module.exports = { + extends: '../.eslintrc.js', + /** + * ESLint rules + * + * All available rules: http://eslint.org/docs/rules/ + * + * Rules take the following form: + * "rule-name", [severity, { opts }] + * Severity: 2 == error, 1 == warning, 0 == off. + */ + rules: { + 'no-console': [ + 2, + { allow: ['warn', 'error', 'assert', 'timeStamp', 'time', 'timeEnd'] }, + ], + 'no-debugger': 0, + }, +}; diff --git a/remote/test/puppeteer/src/api-docs-entry.ts b/remote/test/puppeteer/src/api-docs-entry.ts new file mode 100644 index 0000000000..d033a3ed7c --- /dev/null +++ b/remote/test/puppeteer/src/api-docs-entry.ts @@ -0,0 +1,65 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * This file re-exports any APIs that we want to have documentation generated + * for. It is used by API Extractor to determine what parts of the system to + * document. + * + * The legacy DocLint system and the unit test coverage system use the list of + * modules defined in coverage-utils.js. src/api-docs-entry.ts is ONLY used by + * API Extractor. + * + * Once we have migrated to API Extractor and removed DocLint we can remove the + * duplication and use this file. + */ +export * from './common/Accessibility.js'; +export * from './common/Browser.js'; +export * from './node/BrowserFetcher.js'; +export * from './node/Puppeteer.js'; +export * from './common/Connection.js'; +export * from './common/ConsoleMessage.js'; +export * from './common/Coverage.js'; +export * from './common/DeviceDescriptors.js'; +export * from './common/Dialog.js'; +export * from './common/DOMWorld.js'; +export * from './common/JSHandle.js'; +export * from './common/ExecutionContext.js'; +export * from './common/EventEmitter.js'; +export * from './common/FileChooser.js'; +export * from './common/FrameManager.js'; +export * from './common/Input.js'; +export * from './common/Page.js'; +export * from './common/Product.js'; +export * from './common/Puppeteer.js'; +export * from './common/BrowserConnector.js'; +export * from './node/Launcher.js'; +export * from './node/LaunchOptions.js'; +export * from './common/HTTPRequest.js'; +export * from './common/HTTPResponse.js'; +export * from './common/SecurityDetails.js'; +export * from './common/Target.js'; +export * from './common/Errors.js'; +export * from './common/Tracing.js'; +export * from './common/NetworkManager.js'; +export * from './common/WebWorker.js'; +export * from './common/USKeyboardLayout.js'; +export * from './common/EvalTypes.js'; +export * from './common/PDFOptions.js'; +export * from './common/TimeoutSettings.js'; +export * from './common/LifecycleWatcher.js'; +export * from './common/QueryHandler.js'; +export * from 'devtools-protocol/types/protocol'; diff --git a/remote/test/puppeteer/src/common/Accessibility.ts b/remote/test/puppeteer/src/common/Accessibility.ts new file mode 100644 index 0000000000..69684a4770 --- /dev/null +++ b/remote/test/puppeteer/src/common/Accessibility.ts @@ -0,0 +1,502 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CDPSession } from './Connection.js'; +import { ElementHandle } from './JSHandle.js'; +import { Protocol } from 'devtools-protocol'; + +/** + * Represents a Node and the properties of it that are relevant to Accessibility. + * @public + */ +export interface SerializedAXNode { + /** + * The {@link https://www.w3.org/TR/wai-aria/#usage_intro | role} of the node. + */ + role: string; + /** + * A human readable name for the node. + */ + name?: string; + /** + * The current value of the node. + */ + value?: string | number; + /** + * An additional human readable description of the node. + */ + description?: string; + /** + * Any keyboard shortcuts associated with this node. + */ + keyshortcuts?: string; + /** + * A human readable alternative to the role. + */ + roledescription?: string; + /** + * A description of the current value. + */ + valuetext?: string; + disabled?: boolean; + expanded?: boolean; + focused?: boolean; + modal?: boolean; + multiline?: boolean; + /** + * Whether more than one child can be selected. + */ + multiselectable?: boolean; + readonly?: boolean; + required?: boolean; + selected?: boolean; + /** + * Whether the checkbox is checked, or in a + * {@link https://www.w3.org/TR/wai-aria-practices/examples/checkbox/checkbox-2/checkbox-2.html | mixed state}. + */ + checked?: boolean | 'mixed'; + /** + * Whether the node is checked or in a mixed state. + */ + pressed?: boolean | 'mixed'; + /** + * The level of a heading. + */ + level?: number; + valuemin?: number; + valuemax?: number; + autocomplete?: string; + haspopup?: string; + /** + * Whether and in what way this node's value is invalid. + */ + invalid?: string; + orientation?: string; + /** + * Children of this node, if there are any. + */ + children?: SerializedAXNode[]; +} + +/** + * @public + */ +export interface SnapshotOptions { + /** + * Prune uninteresting nodes from the tree. + * @defaultValue true + */ + interestingOnly?: boolean; + /** + * Root node to get the accessibility tree for + * @defaultValue The root node of the entire page. + */ + root?: ElementHandle; +} + +/** + * The Accessibility class provides methods for inspecting Chromium's + * accessibility tree. The accessibility tree is used by assistive technology + * such as {@link https://en.wikipedia.org/wiki/Screen_reader | screen readers} or + * {@link https://en.wikipedia.org/wiki/Switch_access | switches}. + * + * @remarks + * + * Accessibility is a very platform-specific thing. On different platforms, + * there are different screen readers that might have wildly different output. + * + * Blink - Chrome's rendering engine - has a concept of "accessibility tree", + * which is then translated into different platform-specific APIs. Accessibility + * namespace gives users access to the Blink Accessibility Tree. + * + * Most of the accessibility tree gets filtered out when converting from Blink + * AX Tree to Platform-specific AX-Tree or by assistive technologies themselves. + * By default, Puppeteer tries to approximate this filtering, exposing only + * the "interesting" nodes of the tree. + * + * @public + */ +export class Accessibility { + private _client: CDPSession; + + /** + * @internal + */ + constructor(client: CDPSession) { + this._client = client; + } + + /** + * Captures the current state of the accessibility tree. + * The returned object represents the root accessible node of the page. + * + * @remarks + * + * **NOTE** The Chromium accessibility tree contains nodes that go unused on + * most platforms and by most screen readers. Puppeteer will discard them as + * well for an easier to process tree, unless `interestingOnly` is set to + * `false`. + * + * @example + * An example of dumping the entire accessibility tree: + * ```js + * const snapshot = await page.accessibility.snapshot(); + * console.log(snapshot); + * ``` + * + * @example + * An example of logging the focused node's name: + * ```js + * const snapshot = await page.accessibility.snapshot(); + * const node = findFocusedNode(snapshot); + * console.log(node && node.name); + * + * function findFocusedNode(node) { + * if (node.focused) + * return node; + * for (const child of node.children || []) { + * const foundNode = findFocusedNode(child); + * return foundNode; + * } + * return null; + * } + * ``` + * + * @returns An AXNode object representing the snapshot. + * + */ + public async snapshot( + options: SnapshotOptions = {} + ): Promise<SerializedAXNode> { + const { interestingOnly = true, root = null } = options; + const { nodes } = await this._client.send('Accessibility.getFullAXTree'); + let backendNodeId = null; + if (root) { + const { node } = await this._client.send('DOM.describeNode', { + objectId: root._remoteObject.objectId, + }); + backendNodeId = node.backendNodeId; + } + const defaultRoot = AXNode.createTree(nodes); + let needle = defaultRoot; + if (backendNodeId) { + needle = defaultRoot.find( + (node) => node.payload.backendDOMNodeId === backendNodeId + ); + if (!needle) return null; + } + if (!interestingOnly) return this.serializeTree(needle)[0]; + + const interestingNodes = new Set<AXNode>(); + this.collectInterestingNodes(interestingNodes, defaultRoot, false); + if (!interestingNodes.has(needle)) return null; + return this.serializeTree(needle, interestingNodes)[0]; + } + + private serializeTree( + node: AXNode, + interestingNodes?: Set<AXNode> + ): SerializedAXNode[] { + const children: SerializedAXNode[] = []; + for (const child of node.children) + children.push(...this.serializeTree(child, interestingNodes)); + + if (interestingNodes && !interestingNodes.has(node)) return children; + + const serializedNode = node.serialize(); + if (children.length) serializedNode.children = children; + return [serializedNode]; + } + + private collectInterestingNodes( + collection: Set<AXNode>, + node: AXNode, + insideControl: boolean + ): void { + if (node.isInteresting(insideControl)) collection.add(node); + if (node.isLeafNode()) return; + insideControl = insideControl || node.isControl(); + for (const child of node.children) + this.collectInterestingNodes(collection, child, insideControl); + } +} + +class AXNode { + public payload: Protocol.Accessibility.AXNode; + public children: AXNode[] = []; + + private _richlyEditable = false; + private _editable = false; + private _focusable = false; + private _hidden = false; + private _name: string; + private _role: string; + private _ignored: boolean; + private _cachedHasFocusableChild?: boolean; + + constructor(payload: Protocol.Accessibility.AXNode) { + this.payload = payload; + this._name = this.payload.name ? this.payload.name.value : ''; + this._role = this.payload.role ? this.payload.role.value : 'Unknown'; + this._ignored = this.payload.ignored; + + for (const property of this.payload.properties || []) { + if (property.name === 'editable') { + this._richlyEditable = property.value.value === 'richtext'; + this._editable = true; + } + if (property.name === 'focusable') this._focusable = property.value.value; + if (property.name === 'hidden') this._hidden = property.value.value; + } + } + + private _isPlainTextField(): boolean { + if (this._richlyEditable) return false; + if (this._editable) return true; + return this._role === 'textbox' || this._role === 'searchbox'; + } + + private _isTextOnlyObject(): boolean { + const role = this._role; + return role === 'LineBreak' || role === 'text' || role === 'InlineTextBox'; + } + + private _hasFocusableChild(): boolean { + if (this._cachedHasFocusableChild === undefined) { + this._cachedHasFocusableChild = false; + for (const child of this.children) { + if (child._focusable || child._hasFocusableChild()) { + this._cachedHasFocusableChild = true; + break; + } + } + } + return this._cachedHasFocusableChild; + } + + public find(predicate: (x: AXNode) => boolean): AXNode | null { + if (predicate(this)) return this; + for (const child of this.children) { + const result = child.find(predicate); + if (result) return result; + } + return null; + } + + public isLeafNode(): boolean { + if (!this.children.length) return true; + + // These types of objects may have children that we use as internal + // implementation details, but we want to expose them as leaves to platform + // accessibility APIs because screen readers might be confused if they find + // any children. + if (this._isPlainTextField() || this._isTextOnlyObject()) return true; + + // Roles whose children are only presentational according to the ARIA and + // HTML5 Specs should be hidden from screen readers. + // (Note that whilst ARIA buttons can have only presentational children, HTML5 + // buttons are allowed to have content.) + switch (this._role) { + case 'doc-cover': + case 'graphics-symbol': + case 'img': + case 'Meter': + case 'scrollbar': + case 'slider': + case 'separator': + case 'progressbar': + return true; + default: + break; + } + + // Here and below: Android heuristics + if (this._hasFocusableChild()) return false; + if (this._focusable && this._name) return true; + if (this._role === 'heading' && this._name) return true; + return false; + } + + public isControl(): boolean { + switch (this._role) { + case 'button': + case 'checkbox': + case 'ColorWell': + case 'combobox': + case 'DisclosureTriangle': + case 'listbox': + case 'menu': + case 'menubar': + case 'menuitem': + case 'menuitemcheckbox': + case 'menuitemradio': + case 'radio': + case 'scrollbar': + case 'searchbox': + case 'slider': + case 'spinbutton': + case 'switch': + case 'tab': + case 'textbox': + case 'tree': + case 'treeitem': + return true; + default: + return false; + } + } + + public isInteresting(insideControl: boolean): boolean { + const role = this._role; + if (role === 'Ignored' || this._hidden || this._ignored) return false; + + if (this._focusable || this._richlyEditable) return true; + + // If it's not focusable but has a control role, then it's interesting. + if (this.isControl()) return true; + + // A non focusable child of a control is not interesting + if (insideControl) return false; + + return this.isLeafNode() && !!this._name; + } + + public serialize(): SerializedAXNode { + const properties = new Map<string, number | string | boolean>(); + for (const property of this.payload.properties || []) + properties.set(property.name.toLowerCase(), property.value.value); + if (this.payload.name) properties.set('name', this.payload.name.value); + if (this.payload.value) properties.set('value', this.payload.value.value); + if (this.payload.description) + properties.set('description', this.payload.description.value); + + const node: SerializedAXNode = { + role: this._role, + }; + + type UserStringProperty = + | 'name' + | 'value' + | 'description' + | 'keyshortcuts' + | 'roledescription' + | 'valuetext'; + + const userStringProperties: UserStringProperty[] = [ + 'name', + 'value', + 'description', + 'keyshortcuts', + 'roledescription', + 'valuetext', + ]; + const getUserStringPropertyValue = (key: UserStringProperty): string => + properties.get(key) as string; + + for (const userStringProperty of userStringProperties) { + if (!properties.has(userStringProperty)) continue; + + node[userStringProperty] = getUserStringPropertyValue(userStringProperty); + } + + type BooleanProperty = + | 'disabled' + | 'expanded' + | 'focused' + | 'modal' + | 'multiline' + | 'multiselectable' + | 'readonly' + | 'required' + | 'selected'; + const booleanProperties: BooleanProperty[] = [ + 'disabled', + 'expanded', + 'focused', + 'modal', + 'multiline', + 'multiselectable', + 'readonly', + 'required', + 'selected', + ]; + const getBooleanPropertyValue = (key: BooleanProperty): boolean => + properties.get(key) as boolean; + + for (const booleanProperty of booleanProperties) { + // WebArea's treat focus differently than other nodes. They report whether + // their frame has focus, not whether focus is specifically on the root + // node. + if (booleanProperty === 'focused' && this._role === 'WebArea') continue; + const value = getBooleanPropertyValue(booleanProperty); + if (!value) continue; + node[booleanProperty] = getBooleanPropertyValue(booleanProperty); + } + + type TristateProperty = 'checked' | 'pressed'; + const tristateProperties: TristateProperty[] = ['checked', 'pressed']; + for (const tristateProperty of tristateProperties) { + if (!properties.has(tristateProperty)) continue; + const value = properties.get(tristateProperty); + node[tristateProperty] = + value === 'mixed' ? 'mixed' : value === 'true' ? true : false; + } + + type NumbericalProperty = 'level' | 'valuemax' | 'valuemin'; + const numericalProperties: NumbericalProperty[] = [ + 'level', + 'valuemax', + 'valuemin', + ]; + const getNumericalPropertyValue = (key: NumbericalProperty): number => + properties.get(key) as number; + for (const numericalProperty of numericalProperties) { + if (!properties.has(numericalProperty)) continue; + node[numericalProperty] = getNumericalPropertyValue(numericalProperty); + } + + type TokenProperty = + | 'autocomplete' + | 'haspopup' + | 'invalid' + | 'orientation'; + const tokenProperties: TokenProperty[] = [ + 'autocomplete', + 'haspopup', + 'invalid', + 'orientation', + ]; + const getTokenPropertyValue = (key: TokenProperty): string => + properties.get(key) as string; + for (const tokenProperty of tokenProperties) { + const value = getTokenPropertyValue(tokenProperty); + if (!value || value === 'false') continue; + node[tokenProperty] = getTokenPropertyValue(tokenProperty); + } + return node; + } + + public static createTree(payloads: Protocol.Accessibility.AXNode[]): AXNode { + const nodeById = new Map<string, AXNode>(); + for (const payload of payloads) + nodeById.set(payload.nodeId, new AXNode(payload)); + for (const node of nodeById.values()) { + for (const childId of node.payload.childIds || []) + node.children.push(nodeById.get(childId)); + } + return nodeById.values().next().value; + } +} diff --git a/remote/test/puppeteer/src/common/AriaQueryHandler.ts b/remote/test/puppeteer/src/common/AriaQueryHandler.ts new file mode 100644 index 0000000000..9b563e9e02 --- /dev/null +++ b/remote/test/puppeteer/src/common/AriaQueryHandler.ts @@ -0,0 +1,140 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InternalQueryHandler } from './QueryHandler.js'; +import { ElementHandle, JSHandle } from './JSHandle.js'; +import { Protocol } from 'devtools-protocol'; +import { CDPSession } from './Connection.js'; +import { DOMWorld, PageBinding, WaitForSelectorOptions } from './DOMWorld.js'; + +async function queryAXTree( + client: CDPSession, + element: ElementHandle, + accessibleName?: string, + role?: string +): Promise<Protocol.Accessibility.AXNode[]> { + const { nodes } = await client.send('Accessibility.queryAXTree', { + objectId: element._remoteObject.objectId, + accessibleName, + role, + }); + const filteredNodes: Protocol.Accessibility.AXNode[] = nodes.filter( + (node: Protocol.Accessibility.AXNode) => node.role.value !== 'text' + ); + return filteredNodes; +} + +/* + * The selectors consist of an accessible name to query for and optionally + * further aria attributes on the form `[<attribute>=<value>]`. + * Currently, we only support the `name` and `role` attribute. + * The following examples showcase how the syntax works wrt. querying: + * - 'title[role="heading"]' queries for elements with name 'title' and role 'heading'. + * - '[role="img"]' queries for elements with role 'img' and any name. + * - 'label' queries for elements with name 'label' and any role. + * - '[name=""][role="button"]' queries for elements with no name and role 'button'. + */ +type ariaQueryOption = { name?: string; role?: string }; +function parseAriaSelector(selector: string): ariaQueryOption { + const normalize = (value: string): string => value.replace(/ +/g, ' ').trim(); + const knownAttributes = new Set(['name', 'role']); + const queryOptions: ariaQueryOption = {}; + const attributeRegexp = /\[\s*(?<attribute>\w+)\s*=\s*"(?<value>\\.|[^"\\]*)"\s*\]/; + const defaultName = selector.replace( + attributeRegexp, + (_, attribute: string, value: string) => { + attribute = attribute.trim(); + if (!knownAttributes.has(attribute)) + throw new Error( + 'Unkown aria attribute "${groups.attribute}" in selector' + ); + queryOptions[attribute] = normalize(value); + return ''; + } + ); + if (defaultName && !queryOptions.name) + queryOptions.name = normalize(defaultName); + return queryOptions; +} + +const queryOne = async ( + element: ElementHandle, + selector: string +): Promise<ElementHandle | null> => { + const exeCtx = element.executionContext(); + const { name, role } = parseAriaSelector(selector); + const res = await queryAXTree(exeCtx._client, element, name, role); + if (res.length < 1) { + return null; + } + return exeCtx._adoptBackendNodeId(res[0].backendDOMNodeId); +}; + +const waitFor = async ( + domWorld: DOMWorld, + selector: string, + options: WaitForSelectorOptions +): Promise<ElementHandle<Element>> => { + const binding: PageBinding = { + name: 'ariaQuerySelector', + pptrFunction: async (selector: string) => { + const document = await domWorld._document(); + const element = await queryOne(document, selector); + return element; + }, + }; + return domWorld.waitForSelectorInPage( + (_: Element, selector: string) => globalThis.ariaQuerySelector(selector), + selector, + options, + binding + ); +}; + +const queryAll = async ( + element: ElementHandle, + selector: string +): Promise<ElementHandle[]> => { + const exeCtx = element.executionContext(); + const { name, role } = parseAriaSelector(selector); + const res = await queryAXTree(exeCtx._client, element, name, role); + return Promise.all( + res.map((axNode) => exeCtx._adoptBackendNodeId(axNode.backendDOMNodeId)) + ); +}; + +const queryAllArray = async ( + element: ElementHandle, + selector: string +): Promise<JSHandle> => { + const elementHandles = await queryAll(element, selector); + const exeCtx = element.executionContext(); + const jsHandle = exeCtx.evaluateHandle( + (...elements) => elements, + ...elementHandles + ); + return jsHandle; +}; + +/** + * @internal + */ +export const ariaHandler: InternalQueryHandler = { + queryOne, + waitFor, + queryAll, + queryAllArray, +}; diff --git a/remote/test/puppeteer/src/common/Browser.ts b/remote/test/puppeteer/src/common/Browser.ts new file mode 100644 index 0000000000..e3b6a24119 --- /dev/null +++ b/remote/test/puppeteer/src/common/Browser.ts @@ -0,0 +1,734 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assert } from './assert.js'; +import { helper } from './helper.js'; +import { Target } from './Target.js'; +import { EventEmitter } from './EventEmitter.js'; +import { Connection, ConnectionEmittedEvents } from './Connection.js'; +import { Protocol } from 'devtools-protocol'; +import { Page } from './Page.js'; +import { ChildProcess } from 'child_process'; +import { Viewport } from './PuppeteerViewport.js'; + +type BrowserCloseCallback = () => Promise<void> | void; + +/** + * @public + */ +export interface WaitForTargetOptions { + /** + * Maximum wait time in milliseconds. Pass `0` to disable the timeout. + * @defaultValue 30 seconds. + */ + timeout?: number; +} + +/** + * All the events a {@link Browser | browser instance} may emit. + * + * @public + */ +export const enum BrowserEmittedEvents { + /** + * Emitted when Puppeteer gets disconnected from the Chromium instance. This + * might happen because of one of the following: + * + * - Chromium is closed or crashed + * + * - The {@link Browser.disconnect | browser.disconnect } method was called. + */ + Disconnected = 'disconnected', + + /** + * Emitted when the url of a target changes. Contains a {@link Target} instance. + * + * @remarks + * + * Note that this includes target changes in incognito browser contexts. + */ + TargetChanged = 'targetchanged', + + /** + * Emitted when a target is created, for example when a new page is opened by + * {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open} + * or by {@link Browser.newPage | browser.newPage} + * + * Contains a {@link Target} instance. + * + * @remarks + * + * Note that this includes target creations in incognito browser contexts. + */ + TargetCreated = 'targetcreated', + /** + * Emitted when a target is destroyed, for example when a page is closed. + * Contains a {@link Target} instance. + * + * @remarks + * + * Note that this includes target destructions in incognito browser contexts. + */ + TargetDestroyed = 'targetdestroyed', +} + +/** + * A Browser is created when Puppeteer connects to a Chromium instance, either through + * {@link PuppeteerNode.launch} or {@link Puppeteer.connect}. + * + * @remarks + * + * The Browser class extends from Puppeteer's {@link EventEmitter} class and will + * emit various events which are documented in the {@link BrowserEmittedEvents} enum. + * + * @example + * + * An example of using a {@link Browser} to create a {@link Page}: + * ```js + * const puppeteer = require('puppeteer'); + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.goto('https://example.com'); + * await browser.close(); + * })(); + * ``` + * + * @example + * + * An example of disconnecting from and reconnecting to a {@link Browser}: + * ```js + * const puppeteer = require('puppeteer'); + * + * (async () => { + * const browser = await puppeteer.launch(); + * // Store the endpoint to be able to reconnect to Chromium + * const browserWSEndpoint = browser.wsEndpoint(); + * // Disconnect puppeteer from Chromium + * browser.disconnect(); + * + * // Use the endpoint to reestablish a connection + * const browser2 = await puppeteer.connect({browserWSEndpoint}); + * // Close Chromium + * await browser2.close(); + * })(); + * ``` + * + * @public + */ +export class Browser extends EventEmitter { + /** + * @internal + */ + static async create( + connection: Connection, + contextIds: string[], + ignoreHTTPSErrors: boolean, + defaultViewport?: Viewport, + process?: ChildProcess, + closeCallback?: BrowserCloseCallback + ): Promise<Browser> { + const browser = new Browser( + connection, + contextIds, + ignoreHTTPSErrors, + defaultViewport, + process, + closeCallback + ); + await connection.send('Target.setDiscoverTargets', { discover: true }); + return browser; + } + private _ignoreHTTPSErrors: boolean; + private _defaultViewport?: Viewport; + private _process?: ChildProcess; + private _connection: Connection; + private _closeCallback: BrowserCloseCallback; + private _defaultContext: BrowserContext; + private _contexts: Map<string, BrowserContext>; + /** + * @internal + * Used in Target.ts directly so cannot be marked private. + */ + _targets: Map<string, Target>; + + /** + * @internal + */ + constructor( + connection: Connection, + contextIds: string[], + ignoreHTTPSErrors: boolean, + defaultViewport?: Viewport, + process?: ChildProcess, + closeCallback?: BrowserCloseCallback + ) { + super(); + this._ignoreHTTPSErrors = ignoreHTTPSErrors; + this._defaultViewport = defaultViewport; + this._process = process; + this._connection = connection; + this._closeCallback = closeCallback || function (): void {}; + + this._defaultContext = new BrowserContext(this._connection, this, null); + this._contexts = new Map(); + for (const contextId of contextIds) + this._contexts.set( + contextId, + new BrowserContext(this._connection, this, contextId) + ); + + this._targets = new Map(); + this._connection.on(ConnectionEmittedEvents.Disconnected, () => + this.emit(BrowserEmittedEvents.Disconnected) + ); + this._connection.on('Target.targetCreated', this._targetCreated.bind(this)); + this._connection.on( + 'Target.targetDestroyed', + this._targetDestroyed.bind(this) + ); + this._connection.on( + 'Target.targetInfoChanged', + this._targetInfoChanged.bind(this) + ); + } + + /** + * The spawned browser process. Returns `null` if the browser instance was created with + * {@link Puppeteer.connect}. + */ + process(): ChildProcess | null { + return this._process; + } + + /** + * Creates a new incognito browser context. This won't share cookies/cache with other + * browser contexts. + * + * @example + * ```js + * (async () => { + * const browser = await puppeteer.launch(); + * // Create a new incognito browser context. + * const context = await browser.createIncognitoBrowserContext(); + * // Create a new page in a pristine context. + * const page = await context.newPage(); + * // Do stuff + * await page.goto('https://example.com'); + * })(); + * ``` + */ + async createIncognitoBrowserContext(): Promise<BrowserContext> { + const { browserContextId } = await this._connection.send( + 'Target.createBrowserContext' + ); + const context = new BrowserContext( + this._connection, + this, + browserContextId + ); + this._contexts.set(browserContextId, context); + return context; + } + + /** + * Returns an array of all open browser contexts. In a newly created browser, this will + * return a single instance of {@link BrowserContext}. + */ + browserContexts(): BrowserContext[] { + return [this._defaultContext, ...Array.from(this._contexts.values())]; + } + + /** + * Returns the default browser context. The default browser context cannot be closed. + */ + defaultBrowserContext(): BrowserContext { + return this._defaultContext; + } + + /** + * @internal + * Used by BrowserContext directly so cannot be marked private. + */ + async _disposeContext(contextId?: string): Promise<void> { + await this._connection.send('Target.disposeBrowserContext', { + browserContextId: contextId || undefined, + }); + this._contexts.delete(contextId); + } + + private async _targetCreated( + event: Protocol.Target.TargetCreatedEvent + ): Promise<void> { + const targetInfo = event.targetInfo; + const { browserContextId } = targetInfo; + const context = + browserContextId && this._contexts.has(browserContextId) + ? this._contexts.get(browserContextId) + : this._defaultContext; + + const target = new Target( + targetInfo, + context, + () => this._connection.createSession(targetInfo), + this._ignoreHTTPSErrors, + this._defaultViewport + ); + assert( + !this._targets.has(event.targetInfo.targetId), + 'Target should not exist before targetCreated' + ); + this._targets.set(event.targetInfo.targetId, target); + + if (await target._initializedPromise) { + this.emit(BrowserEmittedEvents.TargetCreated, target); + context.emit(BrowserContextEmittedEvents.TargetCreated, target); + } + } + + private async _targetDestroyed(event: { targetId: string }): Promise<void> { + const target = this._targets.get(event.targetId); + target._initializedCallback(false); + this._targets.delete(event.targetId); + target._closedCallback(); + if (await target._initializedPromise) { + this.emit(BrowserEmittedEvents.TargetDestroyed, target); + target + .browserContext() + .emit(BrowserContextEmittedEvents.TargetDestroyed, target); + } + } + + private _targetInfoChanged( + event: Protocol.Target.TargetInfoChangedEvent + ): void { + const target = this._targets.get(event.targetInfo.targetId); + assert(target, 'target should exist before targetInfoChanged'); + const previousURL = target.url(); + const wasInitialized = target._isInitialized; + target._targetInfoChanged(event.targetInfo); + if (wasInitialized && previousURL !== target.url()) { + this.emit(BrowserEmittedEvents.TargetChanged, target); + target + .browserContext() + .emit(BrowserContextEmittedEvents.TargetChanged, target); + } + } + + /** + * The browser websocket endpoint which can be used as an argument to + * {@link Puppeteer.connect}. + * + * @returns The Browser websocket url. + * + * @remarks + * + * The format is `ws://${host}:${port}/devtools/browser/<id>`. + * + * You can find the `webSocketDebuggerUrl` from `http://${host}:${port}/json/version`. + * Learn more about the + * {@link https://chromedevtools.github.io/devtools-protocol | devtools protocol} and + * the {@link + * https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target + * | browser endpoint}. + */ + wsEndpoint(): string { + return this._connection.url(); + } + + /** + * Creates a {@link Page} in the default browser context. + */ + async newPage(): Promise<Page> { + return this._defaultContext.newPage(); + } + + /** + * @internal + * Used by BrowserContext directly so cannot be marked private. + */ + async _createPageInContext(contextId?: string): Promise<Page> { + const { targetId } = await this._connection.send('Target.createTarget', { + url: 'about:blank', + browserContextId: contextId || undefined, + }); + const target = await this._targets.get(targetId); + assert( + await target._initializedPromise, + 'Failed to create target for page' + ); + const page = await target.page(); + return page; + } + + /** + * All active targets inside the Browser. In case of multiple browser contexts, returns + * an array with all the targets in all browser contexts. + */ + targets(): Target[] { + return Array.from(this._targets.values()).filter( + (target) => target._isInitialized + ); + } + + /** + * The target associated with the browser. + */ + target(): Target { + return this.targets().find((target) => target.type() === 'browser'); + } + + /** + * Searches for a target in all browser contexts. + * + * @param predicate - A function to be run for every target. + * @returns The first target found that matches the `predicate` function. + * + * @example + * + * An example of finding a target for a page opened via `window.open`: + * ```js + * await page.evaluate(() => window.open('https://www.example.com/')); + * const newWindowTarget = await browser.waitForTarget(target => target.url() === 'https://www.example.com/'); + * ``` + */ + async waitForTarget( + predicate: (x: Target) => boolean, + options: WaitForTargetOptions = {} + ): Promise<Target> { + const { timeout = 30000 } = options; + const existingTarget = this.targets().find(predicate); + if (existingTarget) return existingTarget; + let resolve; + const targetPromise = new Promise<Target>((x) => (resolve = x)); + this.on(BrowserEmittedEvents.TargetCreated, check); + this.on(BrowserEmittedEvents.TargetChanged, check); + try { + if (!timeout) return await targetPromise; + return await helper.waitWithTimeout<Target>( + targetPromise, + 'target', + timeout + ); + } finally { + this.removeListener(BrowserEmittedEvents.TargetCreated, check); + this.removeListener(BrowserEmittedEvents.TargetChanged, check); + } + + function check(target: Target): void { + if (predicate(target)) resolve(target); + } + } + + /** + * An array of all open pages inside the Browser. + * + * @remarks + * + * In case of multiple browser contexts, returns an array with all the pages in all + * browser contexts. Non-visible pages, such as `"background_page"`, will not be listed + * here. You can find them using {@link Target.page}. + */ + async pages(): Promise<Page[]> { + const contextPages = await Promise.all( + this.browserContexts().map((context) => context.pages()) + ); + // Flatten array. + return contextPages.reduce((acc, x) => acc.concat(x), []); + } + + /** + * A string representing the browser name and version. + * + * @remarks + * + * For headless Chromium, this is similar to `HeadlessChrome/61.0.3153.0`. For + * non-headless, this is similar to `Chrome/61.0.3153.0`. + * + * The format of browser.version() might change with future releases of Chromium. + */ + async version(): Promise<string> { + const version = await this._getVersion(); + return version.product; + } + + /** + * The browser's original user agent. Pages can override the browser user agent with + * {@link Page.setUserAgent}. + */ + async userAgent(): Promise<string> { + const version = await this._getVersion(); + return version.userAgent; + } + + /** + * Closes Chromium and all of its pages (if any were opened). The {@link Browser} object + * itself is considered to be disposed and cannot be used anymore. + */ + async close(): Promise<void> { + await this._closeCallback.call(null); + this.disconnect(); + } + + /** + * Disconnects Puppeteer from the browser, but leaves the Chromium process running. + * After calling `disconnect`, the {@link Browser} object is considered disposed and + * cannot be used anymore. + */ + disconnect(): void { + this._connection.dispose(); + } + + /** + * Indicates that the browser is connected. + */ + isConnected(): boolean { + return !this._connection._closed; + } + + private _getVersion(): Promise<Protocol.Browser.GetVersionResponse> { + return this._connection.send('Browser.getVersion'); + } +} + +export const enum BrowserContextEmittedEvents { + /** + * Emitted when the url of a target inside the browser context changes. + * Contains a {@link Target} instance. + */ + TargetChanged = 'targetchanged', + + /** + * Emitted when a target is created within the browser context, for example + * when a new page is opened by + * {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open} + * or by {@link BrowserContext.newPage | browserContext.newPage} + * + * Contains a {@link Target} instance. + */ + TargetCreated = 'targetcreated', + /** + * Emitted when a target is destroyed within the browser context, for example + * when a page is closed. Contains a {@link Target} instance. + */ + TargetDestroyed = 'targetdestroyed', +} + +/** + * BrowserContexts provide a way to operate multiple independent browser + * sessions. When a browser is launched, it has a single BrowserContext used by + * default. The method {@link Browser.newPage | Browser.newPage} creates a page + * in the default browser context. + * + * @remarks + * + * The Browser class extends from Puppeteer's {@link EventEmitter} class and + * will emit various events which are documented in the + * {@link BrowserContextEmittedEvents} enum. + * + * If a page opens another page, e.g. with a `window.open` call, the popup will + * belong to the parent page's browser context. + * + * Puppeteer allows creation of "incognito" browser contexts with + * {@link Browser.createIncognitoBrowserContext | Browser.createIncognitoBrowserContext} + * method. "Incognito" browser contexts don't write any browsing data to disk. + * + * @example + * ```js + * // Create a new incognito browser context + * const context = await browser.createIncognitoBrowserContext(); + * // Create a new page inside context. + * const page = await context.newPage(); + * // ... do stuff with page ... + * await page.goto('https://example.com'); + * // Dispose context once it's no longer needed. + * await context.close(); + * ``` + */ +export class BrowserContext extends EventEmitter { + private _connection: Connection; + private _browser: Browser; + private _id?: string; + + /** + * @internal + */ + constructor(connection: Connection, browser: Browser, contextId?: string) { + super(); + this._connection = connection; + this._browser = browser; + this._id = contextId; + } + + /** + * An array of all active targets inside the browser context. + */ + targets(): Target[] { + return this._browser + .targets() + .filter((target) => target.browserContext() === this); + } + + /** + * This searches for a target in this specific browser context. + * + * @example + * An example of finding a target for a page opened via `window.open`: + * ```js + * await page.evaluate(() => window.open('https://www.example.com/')); + * const newWindowTarget = await browserContext.waitForTarget(target => target.url() === 'https://www.example.com/'); + * ``` + * + * @param predicate - A function to be run for every target + * @param options - An object of options. Accepts a timout, + * which is the maximum wait time in milliseconds. + * Pass `0` to disable the timeout. Defaults to 30 seconds. + * @returns Promise which resolves to the first target found + * that matches the `predicate` function. + */ + waitForTarget( + predicate: (x: Target) => boolean, + options: { timeout?: number } = {} + ): Promise<Target> { + return this._browser.waitForTarget( + (target) => target.browserContext() === this && predicate(target), + options + ); + } + + /** + * An array of all pages inside the browser context. + * + * @returns Promise which resolves to an array of all open pages. + * Non visible pages, such as `"background_page"`, will not be listed here. + * You can find them using {@link Target.page | the target page}. + */ + async pages(): Promise<Page[]> { + const pages = await Promise.all( + this.targets() + .filter((target) => target.type() === 'page') + .map((target) => target.page()) + ); + return pages.filter((page) => !!page); + } + + /** + * Returns whether BrowserContext is incognito. + * The default browser context is the only non-incognito browser context. + * + * @remarks + * The default browser context cannot be closed. + */ + isIncognito(): boolean { + return !!this._id; + } + + /** + * @example + * ```js + * const context = browser.defaultBrowserContext(); + * await context.overridePermissions('https://html5demos.com', ['geolocation']); + * ``` + * + * @param origin - The origin to grant permissions to, e.g. "https://example.com". + * @param permissions - An array of permissions to grant. + * All permissions that are not listed here will be automatically denied. + */ + async overridePermissions( + origin: string, + permissions: string[] + ): Promise<void> { + const webPermissionToProtocol = new Map< + string, + Protocol.Browser.PermissionType + >([ + ['geolocation', 'geolocation'], + ['midi', 'midi'], + ['notifications', 'notifications'], + // TODO: push isn't a valid type? + // ['push', 'push'], + ['camera', 'videoCapture'], + ['microphone', 'audioCapture'], + ['background-sync', 'backgroundSync'], + ['ambient-light-sensor', 'sensors'], + ['accelerometer', 'sensors'], + ['gyroscope', 'sensors'], + ['magnetometer', 'sensors'], + ['accessibility-events', 'accessibilityEvents'], + ['clipboard-read', 'clipboardReadWrite'], + ['clipboard-write', 'clipboardReadWrite'], + ['payment-handler', 'paymentHandler'], + ['idle-detection', 'idleDetection'], + // chrome-specific permissions we have. + ['midi-sysex', 'midiSysex'], + ]); + const protocolPermissions = permissions.map((permission) => { + const protocolPermission = webPermissionToProtocol.get(permission); + if (!protocolPermission) + throw new Error('Unknown permission: ' + permission); + return protocolPermission; + }); + await this._connection.send('Browser.grantPermissions', { + origin, + browserContextId: this._id || undefined, + permissions: protocolPermissions, + }); + } + + /** + * Clears all permission overrides for the browser context. + * + * @example + * ```js + * const context = browser.defaultBrowserContext(); + * context.overridePermissions('https://example.com', ['clipboard-read']); + * // do stuff .. + * context.clearPermissionOverrides(); + * ``` + */ + async clearPermissionOverrides(): Promise<void> { + await this._connection.send('Browser.resetPermissions', { + browserContextId: this._id || undefined, + }); + } + + /** + * Creates a new page in the browser context. + */ + newPage(): Promise<Page> { + return this._browser._createPageInContext(this._id); + } + + /** + * The browser this browser context belongs to. + */ + browser(): Browser { + return this._browser; + } + + /** + * Closes the browser context. All the targets that belong to the browser context + * will be closed. + * + * @remarks + * Only incognito browser contexts can be closed. + */ + async close(): Promise<void> { + assert(this._id, 'Non-incognito profiles cannot be closed!'); + await this._browser._disposeContext(this._id); + } +} diff --git a/remote/test/puppeteer/src/common/BrowserConnector.ts b/remote/test/puppeteer/src/common/BrowserConnector.ts new file mode 100644 index 0000000000..5da9cbc144 --- /dev/null +++ b/remote/test/puppeteer/src/common/BrowserConnector.ts @@ -0,0 +1,120 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConnectionTransport } from './ConnectionTransport.js'; +import { Browser } from './Browser.js'; +import { assert } from './assert.js'; +import { debugError } from '../common/helper.js'; +import { Connection } from './Connection.js'; +import { getFetch } from './fetch.js'; +import { Viewport } from './PuppeteerViewport.js'; +import { isNode } from '../environment.js'; + +/** + * Generic browser options that can be passed when launching any browser. + * @public + */ +export interface BrowserOptions { + ignoreHTTPSErrors?: boolean; + defaultViewport?: Viewport; + slowMo?: number; +} + +const getWebSocketTransportClass = async () => { + return isNode + ? (await import('../node/NodeWebSocketTransport.js')).NodeWebSocketTransport + : (await import('./BrowserWebSocketTransport.js')) + .BrowserWebSocketTransport; +}; + +/** + * Users should never call this directly; it's called when calling + * `puppeteer.connect`. + * @internal + */ +export const connectToBrowser = async ( + options: BrowserOptions & { + browserWSEndpoint?: string; + browserURL?: string; + transport?: ConnectionTransport; + } +): Promise<Browser> => { + const { + browserWSEndpoint, + browserURL, + ignoreHTTPSErrors = false, + defaultViewport = { width: 800, height: 600 }, + transport, + slowMo = 0, + } = options; + + assert( + Number(!!browserWSEndpoint) + Number(!!browserURL) + Number(!!transport) === + 1, + 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect' + ); + + let connection = null; + if (transport) { + connection = new Connection('', transport, slowMo); + } else if (browserWSEndpoint) { + const WebSocketClass = await getWebSocketTransportClass(); + const connectionTransport: ConnectionTransport = await WebSocketClass.create( + browserWSEndpoint + ); + connection = new Connection(browserWSEndpoint, connectionTransport, slowMo); + } else if (browserURL) { + const connectionURL = await getWSEndpoint(browserURL); + const WebSocketClass = await getWebSocketTransportClass(); + const connectionTransport: ConnectionTransport = await WebSocketClass.create( + connectionURL + ); + connection = new Connection(connectionURL, connectionTransport, slowMo); + } + + const { browserContextIds } = await connection.send( + 'Target.getBrowserContexts' + ); + return Browser.create( + connection, + browserContextIds, + ignoreHTTPSErrors, + defaultViewport, + null, + () => connection.send('Browser.close').catch(debugError) + ); +}; + +async function getWSEndpoint(browserURL: string): Promise<string> { + const endpointURL = new URL('/json/version', browserURL); + + const fetch = await getFetch(); + try { + const result = await fetch(endpointURL.toString(), { + method: 'GET', + }); + if (!result.ok) { + throw new Error(`HTTP ${result.statusText}`); + } + const data = await result.json(); + return data.webSocketDebuggerUrl; + } catch (error) { + error.message = + `Failed to fetch browser webSocket URL from ${endpointURL}: ` + + error.message; + throw error; + } +} diff --git a/remote/test/puppeteer/src/common/BrowserWebSocketTransport.ts b/remote/test/puppeteer/src/common/BrowserWebSocketTransport.ts new file mode 100644 index 0000000000..9d0e5c4592 --- /dev/null +++ b/remote/test/puppeteer/src/common/BrowserWebSocketTransport.ts @@ -0,0 +1,55 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConnectionTransport } from './ConnectionTransport.js'; + +export class BrowserWebSocketTransport implements ConnectionTransport { + static create(url: string): Promise<BrowserWebSocketTransport> { + return new Promise((resolve, reject) => { + const ws = new WebSocket(url); + + ws.addEventListener('open', () => + resolve(new BrowserWebSocketTransport(ws)) + ); + ws.addEventListener('error', reject); + }); + } + + private _ws: WebSocket; + onmessage?: (message: string) => void; + onclose?: () => void; + + constructor(ws: WebSocket) { + this._ws = ws; + this._ws.addEventListener('message', (event) => { + if (this.onmessage) this.onmessage.call(null, event.data); + }); + this._ws.addEventListener('close', () => { + if (this.onclose) this.onclose.call(null); + }); + // Silently ignore all errors - we don't know what to do with them. + this._ws.addEventListener('error', () => {}); + this.onmessage = null; + this.onclose = null; + } + + send(message: string): void { + this._ws.send(message); + } + + close(): void { + this._ws.close(); + } +} diff --git a/remote/test/puppeteer/src/common/Connection.ts b/remote/test/puppeteer/src/common/Connection.ts new file mode 100644 index 0000000000..d1383157fa --- /dev/null +++ b/remote/test/puppeteer/src/common/Connection.ts @@ -0,0 +1,347 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { assert } from './assert.js'; +import { debug } from './Debug.js'; +const debugProtocolSend = debug('puppeteer:protocol:SEND ►'); +const debugProtocolReceive = debug('puppeteer:protocol:RECV ◀'); + +import { Protocol } from 'devtools-protocol'; +import { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js'; +import { ConnectionTransport } from './ConnectionTransport.js'; +import { EventEmitter } from './EventEmitter.js'; + +interface ConnectionCallback { + resolve: Function; + reject: Function; + error: Error; + method: string; +} + +/** + * Internal events that the Connection class emits. + * + * @internal + */ +export const ConnectionEmittedEvents = { + Disconnected: Symbol('Connection.Disconnected'), +} as const; + +/** + * @internal + */ +export class Connection extends EventEmitter { + _url: string; + _transport: ConnectionTransport; + _delay: number; + _lastId = 0; + _sessions: Map<string, CDPSession> = new Map(); + _closed = false; + + _callbacks: Map<number, ConnectionCallback> = new Map(); + + constructor(url: string, transport: ConnectionTransport, delay = 0) { + super(); + this._url = url; + this._delay = delay; + + this._transport = transport; + this._transport.onmessage = this._onMessage.bind(this); + this._transport.onclose = this._onClose.bind(this); + } + + static fromSession(session: CDPSession): Connection { + return session._connection; + } + + /** + * @param {string} sessionId + * @returns {?CDPSession} + */ + session(sessionId: string): CDPSession | null { + return this._sessions.get(sessionId) || null; + } + + url(): string { + return this._url; + } + + send<T extends keyof ProtocolMapping.Commands>( + method: T, + ...paramArgs: ProtocolMapping.Commands[T]['paramsType'] + ): Promise<ProtocolMapping.Commands[T]['returnType']> { + // There is only ever 1 param arg passed, but the Protocol defines it as an + // array of 0 or 1 items See this comment: + // https://github.com/ChromeDevTools/devtools-protocol/pull/113#issuecomment-412603285 + // which explains why the protocol defines the params this way for better + // type-inference. + // So now we check if there are any params or not and deal with them accordingly. + const params = paramArgs.length ? paramArgs[0] : undefined; + const id = this._rawSend({ method, params }); + return new Promise((resolve, reject) => { + this._callbacks.set(id, { resolve, reject, error: new Error(), method }); + }); + } + + _rawSend(message: Record<string, unknown>): number { + const id = ++this._lastId; + const stringifiedMessage = JSON.stringify( + Object.assign({}, message, { id }) + ); + debugProtocolSend(stringifiedMessage); + this._transport.send(stringifiedMessage); + return id; + } + + async _onMessage(message: string): Promise<void> { + if (this._delay) await new Promise((f) => setTimeout(f, this._delay)); + debugProtocolReceive(message); + const object = JSON.parse(message); + if (object.method === 'Target.attachedToTarget') { + const sessionId = object.params.sessionId; + const session = new CDPSession( + this, + object.params.targetInfo.type, + sessionId + ); + this._sessions.set(sessionId, session); + } else if (object.method === 'Target.detachedFromTarget') { + const session = this._sessions.get(object.params.sessionId); + if (session) { + session._onClosed(); + this._sessions.delete(object.params.sessionId); + } + } + if (object.sessionId) { + const session = this._sessions.get(object.sessionId); + if (session) session._onMessage(object); + } else if (object.id) { + const callback = this._callbacks.get(object.id); + // Callbacks could be all rejected if someone has called `.dispose()`. + if (callback) { + this._callbacks.delete(object.id); + if (object.error) + callback.reject( + createProtocolError(callback.error, callback.method, object) + ); + else callback.resolve(object.result); + } + } else { + this.emit(object.method, object.params); + } + } + + _onClose(): void { + if (this._closed) return; + this._closed = true; + this._transport.onmessage = null; + this._transport.onclose = null; + for (const callback of this._callbacks.values()) + callback.reject( + rewriteError( + callback.error, + `Protocol error (${callback.method}): Target closed.` + ) + ); + this._callbacks.clear(); + for (const session of this._sessions.values()) session._onClosed(); + this._sessions.clear(); + this.emit(ConnectionEmittedEvents.Disconnected); + } + + dispose(): void { + this._onClose(); + this._transport.close(); + } + + /** + * @param {Protocol.Target.TargetInfo} targetInfo + * @returns {!Promise<!CDPSession>} + */ + async createSession( + targetInfo: Protocol.Target.TargetInfo + ): Promise<CDPSession> { + const { sessionId } = await this.send('Target.attachToTarget', { + targetId: targetInfo.targetId, + flatten: true, + }); + return this._sessions.get(sessionId); + } +} + +interface CDPSessionOnMessageObject { + id?: number; + method: string; + params: Record<string, unknown>; + error: { message: string; data: any }; + result?: any; +} + +/** + * Internal events that the CDPSession class emits. + * + * @internal + */ +export const CDPSessionEmittedEvents = { + Disconnected: Symbol('CDPSession.Disconnected'), +} as const; + +/** + * The `CDPSession` instances are used to talk raw Chrome Devtools Protocol. + * + * @remarks + * + * Protocol methods can be called with {@link CDPSession.send} method and protocol + * events can be subscribed to with `CDPSession.on` method. + * + * Useful links: {@link https://chromedevtools.github.io/devtools-protocol/ | DevTools Protocol Viewer} + * and {@link https://github.com/aslushnikov/getting-started-with-cdp/blob/master/README.md | Getting Started with DevTools Protocol}. + * + * @example + * ```js + * const client = await page.target().createCDPSession(); + * await client.send('Animation.enable'); + * client.on('Animation.animationCreated', () => console.log('Animation created!')); + * const response = await client.send('Animation.getPlaybackRate'); + * console.log('playback rate is ' + response.playbackRate); + * await client.send('Animation.setPlaybackRate', { + * playbackRate: response.playbackRate / 2 + * }); + * ``` + * + * @public + */ +export class CDPSession extends EventEmitter { + /** + * @internal + */ + _connection: Connection; + private _sessionId: string; + private _targetType: string; + private _callbacks: Map<number, ConnectionCallback> = new Map(); + + /** + * @internal + */ + constructor(connection: Connection, targetType: string, sessionId: string) { + super(); + this._connection = connection; + this._targetType = targetType; + this._sessionId = sessionId; + } + + send<T extends keyof ProtocolMapping.Commands>( + method: T, + ...paramArgs: ProtocolMapping.Commands[T]['paramsType'] + ): Promise<ProtocolMapping.Commands[T]['returnType']> { + if (!this._connection) + return Promise.reject( + new Error( + `Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.` + ) + ); + + // See the comment in Connection#send explaining why we do this. + const params = paramArgs.length ? paramArgs[0] : undefined; + + const id = this._connection._rawSend({ + sessionId: this._sessionId, + method, + /* TODO(jacktfranklin@): once this Firefox bug is solved + * we no longer need the `|| {}` check + * https://bugzilla.mozilla.org/show_bug.cgi?id=1631570 + */ + params: params || {}, + }); + + return new Promise((resolve, reject) => { + this._callbacks.set(id, { resolve, reject, error: new Error(), method }); + }); + } + + /** + * @internal + */ + _onMessage(object: CDPSessionOnMessageObject): void { + if (object.id && this._callbacks.has(object.id)) { + const callback = this._callbacks.get(object.id); + this._callbacks.delete(object.id); + if (object.error) + callback.reject( + createProtocolError(callback.error, callback.method, object) + ); + else callback.resolve(object.result); + } else { + assert(!object.id); + this.emit(object.method, object.params); + } + } + + /** + * Detaches the cdpSession from the target. Once detached, the cdpSession object + * won't emit any events and can't be used to send messages. + */ + async detach(): Promise<void> { + if (!this._connection) + throw new Error( + `Session already detached. Most likely the ${this._targetType} has been closed.` + ); + await this._connection.send('Target.detachFromTarget', { + sessionId: this._sessionId, + }); + } + + /** + * @internal + */ + _onClosed(): void { + for (const callback of this._callbacks.values()) + callback.reject( + rewriteError( + callback.error, + `Protocol error (${callback.method}): Target closed.` + ) + ); + this._callbacks.clear(); + this._connection = null; + this.emit(CDPSessionEmittedEvents.Disconnected); + } +} + +/** + * @param {!Error} error + * @param {string} method + * @param {{error: {message: string, data: any}}} object + * @returns {!Error} + */ +function createProtocolError( + error: Error, + method: string, + object: { error: { message: string; data: any } } +): Error { + let message = `Protocol error (${method}): ${object.error.message}`; + if ('data' in object.error) message += ` ${object.error.data}`; + return rewriteError(error, message); +} + +/** + * @param {!Error} error + * @param {string} message + * @returns {!Error} + */ +function rewriteError(error: Error, message: string): Error { + error.message = message; + return error; +} diff --git a/remote/test/puppeteer/src/common/ConnectionTransport.ts b/remote/test/puppeteer/src/common/ConnectionTransport.ts new file mode 100644 index 0000000000..fc3deddb40 --- /dev/null +++ b/remote/test/puppeteer/src/common/ConnectionTransport.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface ConnectionTransport { + send(string); + close(); + onmessage?: (message: string) => void; + onclose?: () => void; +} diff --git a/remote/test/puppeteer/src/common/ConsoleMessage.ts b/remote/test/puppeteer/src/common/ConsoleMessage.ts new file mode 100644 index 0000000000..3387dc59d0 --- /dev/null +++ b/remote/test/puppeteer/src/common/ConsoleMessage.ts @@ -0,0 +1,122 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JSHandle } from './JSHandle.js'; + +/** + * @public + */ +export interface ConsoleMessageLocation { + /** + * URL of the resource if known or `undefined` otherwise. + */ + url?: string; + + /** + * 0-based line number in the resource if known or `undefined` otherwise. + */ + lineNumber?: number; + + /** + * 0-based column number in the resource if known or `undefined` otherwise. + */ + columnNumber?: number; +} + +/** + * The supported types for console messages. + */ +export type ConsoleMessageType = + | 'log' + | 'debug' + | 'info' + | 'error' + | 'warning' + | 'dir' + | 'dirxml' + | 'table' + | 'trace' + | 'clear' + | 'startGroup' + | 'startGroupCollapsed' + | 'endGroup' + | 'assert' + | 'profile' + | 'profileEnd' + | 'count' + | 'timeEnd' + | 'verbose'; + +/** + * ConsoleMessage objects are dispatched by page via the 'console' event. + * @public + */ +export class ConsoleMessage { + private _type: ConsoleMessageType; + private _text: string; + private _args: JSHandle[]; + private _stackTraceLocations: ConsoleMessageLocation[]; + + /** + * @public + */ + constructor( + type: ConsoleMessageType, + text: string, + args: JSHandle[], + stackTraceLocations: ConsoleMessageLocation[] + ) { + this._type = type; + this._text = text; + this._args = args; + this._stackTraceLocations = stackTraceLocations; + } + + /** + * @returns The type of the console message. + */ + type(): ConsoleMessageType { + return this._type; + } + + /** + * @returns The text of the console message. + */ + text(): string { + return this._text; + } + + /** + * @returns An array of arguments passed to the console. + */ + args(): JSHandle[] { + return this._args; + } + + /** + * @returns The location of the console message. + */ + location(): ConsoleMessageLocation { + return this._stackTraceLocations.length ? this._stackTraceLocations[0] : {}; + } + + /** + * @returns The array of locations on the stack of the console message. + */ + stackTrace(): ConsoleMessageLocation[] { + return this._stackTraceLocations; + } +} diff --git a/remote/test/puppeteer/src/common/Coverage.ts b/remote/test/puppeteer/src/common/Coverage.ts new file mode 100644 index 0000000000..63060e656a --- /dev/null +++ b/remote/test/puppeteer/src/common/Coverage.ts @@ -0,0 +1,425 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assert } from './assert.js'; +import { helper, debugError, PuppeteerEventListener } from './helper.js'; +import { Protocol } from 'devtools-protocol'; +import { CDPSession } from './Connection.js'; + +import { EVALUATION_SCRIPT_URL } from './ExecutionContext.js'; + +/** + * The CoverageEntry class represents one entry of the coverage report. + * @public + */ +export interface CoverageEntry { + /** + * The URL of the style sheet or script. + */ + url: string; + /** + * The content of the style sheet or script. + */ + text: string; + /** + * The covered range as start and end positions. + */ + ranges: Array<{ start: number; end: number }>; +} + +/** + * Set of configurable options for JS coverage. + * @public + */ +export interface JSCoverageOptions { + /** + * Whether to reset coverage on every navigation. + */ + resetOnNavigation?: boolean; + /** + * Whether anonymous scripts generated by the page should be reported. + */ + reportAnonymousScripts?: boolean; +} + +/** + * Set of configurable options for CSS coverage. + * @public + */ +export interface CSSCoverageOptions { + /** + * Whether to reset coverage on every navigation. + */ + resetOnNavigation?: boolean; +} + +/** + * The Coverage class provides methods to gathers information about parts of + * JavaScript and CSS that were used by the page. + * + * @remarks + * To output coverage in a form consumable by {@link https://github.com/istanbuljs | Istanbul}, + * see {@link https://github.com/istanbuljs/puppeteer-to-istanbul | puppeteer-to-istanbul}. + * + * @example + * An example of using JavaScript and CSS coverage to get percentage of initially + * executed code: + * ```js + * // Enable both JavaScript and CSS coverage + * await Promise.all([ + * page.coverage.startJSCoverage(), + * page.coverage.startCSSCoverage() + * ]); + * // Navigate to page + * await page.goto('https://example.com'); + * // Disable both JavaScript and CSS coverage + * const [jsCoverage, cssCoverage] = await Promise.all([ + * page.coverage.stopJSCoverage(), + * page.coverage.stopCSSCoverage(), + * ]); + * let totalBytes = 0; + * let usedBytes = 0; + * const coverage = [...jsCoverage, ...cssCoverage]; + * for (const entry of coverage) { + * totalBytes += entry.text.length; + * for (const range of entry.ranges) + * usedBytes += range.end - range.start - 1; + * } + * console.log(`Bytes used: ${usedBytes / totalBytes * 100}%`); + * ``` + * @public + */ +export class Coverage { + /** + * @internal + */ + _jsCoverage: JSCoverage; + /** + * @internal + */ + _cssCoverage: CSSCoverage; + + constructor(client: CDPSession) { + this._jsCoverage = new JSCoverage(client); + this._cssCoverage = new CSSCoverage(client); + } + + /** + * @param options - defaults to + * `{ resetOnNavigation : true, reportAnonymousScripts : false }` + * @returns Promise that resolves when coverage is started. + * + * @remarks + * Anonymous scripts are ones that don't have an associated url. These are + * scripts that are dynamically created on the page using `eval` or + * `new Function`. If `reportAnonymousScripts` is set to `true`, anonymous + * scripts will have `__puppeteer_evaluation_script__` as their URL. + */ + async startJSCoverage(options: JSCoverageOptions = {}): Promise<void> { + return await this._jsCoverage.start(options); + } + + /** + * @returns Promise that resolves to the array of coverage reports for + * all scripts. + * + * @remarks + * JavaScript Coverage doesn't include anonymous scripts by default. + * However, scripts with sourceURLs are reported. + */ + async stopJSCoverage(): Promise<CoverageEntry[]> { + return await this._jsCoverage.stop(); + } + + /** + * @param options - defaults to `{ resetOnNavigation : true }` + * @returns Promise that resolves when coverage is started. + */ + async startCSSCoverage(options: CSSCoverageOptions = {}): Promise<void> { + return await this._cssCoverage.start(options); + } + + /** + * @returns Promise that resolves to the array of coverage reports + * for all stylesheets. + * @remarks + * CSS Coverage doesn't include dynamically injected style tags + * without sourceURLs. + */ + async stopCSSCoverage(): Promise<CoverageEntry[]> { + return await this._cssCoverage.stop(); + } +} + +class JSCoverage { + _client: CDPSession; + _enabled = false; + _scriptURLs = new Map<string, string>(); + _scriptSources = new Map<string, string>(); + _eventListeners: PuppeteerEventListener[] = []; + _resetOnNavigation = false; + _reportAnonymousScripts = false; + + constructor(client: CDPSession) { + this._client = client; + } + + async start( + options: { + resetOnNavigation?: boolean; + reportAnonymousScripts?: boolean; + } = {} + ): Promise<void> { + assert(!this._enabled, 'JSCoverage is already enabled'); + const { + resetOnNavigation = true, + reportAnonymousScripts = false, + } = options; + this._resetOnNavigation = resetOnNavigation; + this._reportAnonymousScripts = reportAnonymousScripts; + this._enabled = true; + this._scriptURLs.clear(); + this._scriptSources.clear(); + this._eventListeners = [ + helper.addEventListener( + this._client, + 'Debugger.scriptParsed', + this._onScriptParsed.bind(this) + ), + helper.addEventListener( + this._client, + 'Runtime.executionContextsCleared', + this._onExecutionContextsCleared.bind(this) + ), + ]; + await Promise.all([ + this._client.send('Profiler.enable'), + this._client.send('Profiler.startPreciseCoverage', { + callCount: false, + detailed: true, + }), + this._client.send('Debugger.enable'), + this._client.send('Debugger.setSkipAllPauses', { skip: true }), + ]); + } + + _onExecutionContextsCleared(): void { + if (!this._resetOnNavigation) return; + this._scriptURLs.clear(); + this._scriptSources.clear(); + } + + async _onScriptParsed( + event: Protocol.Debugger.ScriptParsedEvent + ): Promise<void> { + // Ignore puppeteer-injected scripts + if (event.url === EVALUATION_SCRIPT_URL) return; + // Ignore other anonymous scripts unless the reportAnonymousScripts option is true. + if (!event.url && !this._reportAnonymousScripts) return; + try { + const response = await this._client.send('Debugger.getScriptSource', { + scriptId: event.scriptId, + }); + this._scriptURLs.set(event.scriptId, event.url); + this._scriptSources.set(event.scriptId, response.scriptSource); + } catch (error) { + // This might happen if the page has already navigated away. + debugError(error); + } + } + + async stop(): Promise<CoverageEntry[]> { + assert(this._enabled, 'JSCoverage is not enabled'); + this._enabled = false; + + const result = await Promise.all< + Protocol.Profiler.TakePreciseCoverageResponse, + void, + void, + void + >([ + this._client.send('Profiler.takePreciseCoverage'), + this._client.send('Profiler.stopPreciseCoverage'), + this._client.send('Profiler.disable'), + this._client.send('Debugger.disable'), + ]); + + helper.removeEventListeners(this._eventListeners); + + const coverage = []; + const profileResponse = result[0]; + + for (const entry of profileResponse.result) { + let url = this._scriptURLs.get(entry.scriptId); + if (!url && this._reportAnonymousScripts) + url = 'debugger://VM' + entry.scriptId; + const text = this._scriptSources.get(entry.scriptId); + if (text === undefined || url === undefined) continue; + const flattenRanges = []; + for (const func of entry.functions) flattenRanges.push(...func.ranges); + const ranges = convertToDisjointRanges(flattenRanges); + coverage.push({ url, ranges, text }); + } + return coverage; + } +} + +class CSSCoverage { + _client: CDPSession; + _enabled = false; + _stylesheetURLs = new Map<string, string>(); + _stylesheetSources = new Map<string, string>(); + _eventListeners: PuppeteerEventListener[] = []; + _resetOnNavigation = false; + _reportAnonymousScripts = false; + + constructor(client: CDPSession) { + this._client = client; + } + + async start(options: { resetOnNavigation?: boolean } = {}): Promise<void> { + assert(!this._enabled, 'CSSCoverage is already enabled'); + const { resetOnNavigation = true } = options; + this._resetOnNavigation = resetOnNavigation; + this._enabled = true; + this._stylesheetURLs.clear(); + this._stylesheetSources.clear(); + this._eventListeners = [ + helper.addEventListener( + this._client, + 'CSS.styleSheetAdded', + this._onStyleSheet.bind(this) + ), + helper.addEventListener( + this._client, + 'Runtime.executionContextsCleared', + this._onExecutionContextsCleared.bind(this) + ), + ]; + await Promise.all([ + this._client.send('DOM.enable'), + this._client.send('CSS.enable'), + this._client.send('CSS.startRuleUsageTracking'), + ]); + } + + _onExecutionContextsCleared(): void { + if (!this._resetOnNavigation) return; + this._stylesheetURLs.clear(); + this._stylesheetSources.clear(); + } + + async _onStyleSheet(event: Protocol.CSS.StyleSheetAddedEvent): Promise<void> { + const header = event.header; + // Ignore anonymous scripts + if (!header.sourceURL) return; + try { + const response = await this._client.send('CSS.getStyleSheetText', { + styleSheetId: header.styleSheetId, + }); + this._stylesheetURLs.set(header.styleSheetId, header.sourceURL); + this._stylesheetSources.set(header.styleSheetId, response.text); + } catch (error) { + // This might happen if the page has already navigated away. + debugError(error); + } + } + + async stop(): Promise<CoverageEntry[]> { + assert(this._enabled, 'CSSCoverage is not enabled'); + this._enabled = false; + const ruleTrackingResponse = await this._client.send( + 'CSS.stopRuleUsageTracking' + ); + await Promise.all([ + this._client.send('CSS.disable'), + this._client.send('DOM.disable'), + ]); + helper.removeEventListeners(this._eventListeners); + + // aggregate by styleSheetId + const styleSheetIdToCoverage = new Map(); + for (const entry of ruleTrackingResponse.ruleUsage) { + let ranges = styleSheetIdToCoverage.get(entry.styleSheetId); + if (!ranges) { + ranges = []; + styleSheetIdToCoverage.set(entry.styleSheetId, ranges); + } + ranges.push({ + startOffset: entry.startOffset, + endOffset: entry.endOffset, + count: entry.used ? 1 : 0, + }); + } + + const coverage = []; + for (const styleSheetId of this._stylesheetURLs.keys()) { + const url = this._stylesheetURLs.get(styleSheetId); + const text = this._stylesheetSources.get(styleSheetId); + const ranges = convertToDisjointRanges( + styleSheetIdToCoverage.get(styleSheetId) || [] + ); + coverage.push({ url, ranges, text }); + } + + return coverage; + } +} + +function convertToDisjointRanges( + nestedRanges: Array<{ startOffset: number; endOffset: number; count: number }> +): Array<{ start: number; end: number }> { + const points = []; + for (const range of nestedRanges) { + points.push({ offset: range.startOffset, type: 0, range }); + points.push({ offset: range.endOffset, type: 1, range }); + } + // Sort points to form a valid parenthesis sequence. + points.sort((a, b) => { + // Sort with increasing offsets. + if (a.offset !== b.offset) return a.offset - b.offset; + // All "end" points should go before "start" points. + if (a.type !== b.type) return b.type - a.type; + const aLength = a.range.endOffset - a.range.startOffset; + const bLength = b.range.endOffset - b.range.startOffset; + // For two "start" points, the one with longer range goes first. + if (a.type === 0) return bLength - aLength; + // For two "end" points, the one with shorter range goes first. + return aLength - bLength; + }); + + const hitCountStack = []; + const results = []; + let lastOffset = 0; + // Run scanning line to intersect all ranges. + for (const point of points) { + if ( + hitCountStack.length && + lastOffset < point.offset && + hitCountStack[hitCountStack.length - 1] > 0 + ) { + const lastResult = results.length ? results[results.length - 1] : null; + if (lastResult && lastResult.end === lastOffset) + lastResult.end = point.offset; + else results.push({ start: lastOffset, end: point.offset }); + } + lastOffset = point.offset; + if (point.type === 0) hitCountStack.push(point.range.count); + else hitCountStack.pop(); + } + // Filter out empty ranges. + return results.filter((range) => range.end - range.start > 1); +} diff --git a/remote/test/puppeteer/src/common/DOMWorld.ts b/remote/test/puppeteer/src/common/DOMWorld.ts new file mode 100644 index 0000000000..ae74ab9caf --- /dev/null +++ b/remote/test/puppeteer/src/common/DOMWorld.ts @@ -0,0 +1,940 @@ +/** + * Copyright 2019 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assert } from './assert.js'; +import { helper, debugError } from './helper.js'; +import { + LifecycleWatcher, + PuppeteerLifeCycleEvent, +} from './LifecycleWatcher.js'; +import { TimeoutError } from './Errors.js'; +import { JSHandle, ElementHandle } from './JSHandle.js'; +import { ExecutionContext } from './ExecutionContext.js'; +import { TimeoutSettings } from './TimeoutSettings.js'; +import { MouseButton } from './Input.js'; +import { FrameManager, Frame } from './FrameManager.js'; +import { getQueryHandlerAndSelector } from './QueryHandler.js'; +import { + SerializableOrJSHandle, + EvaluateHandleFn, + WrapElementHandle, + EvaluateFn, + EvaluateFnReturnType, + UnwrapPromiseLike, +} from './EvalTypes.js'; +import { isNode } from '../environment.js'; +import { Protocol } from 'devtools-protocol'; + +// predicateQueryHandler and checkWaitForOptions are declared here so that +// TypeScript knows about them when used in the predicate function below. +declare const predicateQueryHandler: ( + element: Element | Document, + selector: string +) => Promise<Element | Element[] | NodeListOf<Element>>; +declare const checkWaitForOptions: ( + node: Node, + waitForVisible: boolean, + waitForHidden: boolean +) => Element | null | boolean; + +/** + * @public + */ +export interface WaitForSelectorOptions { + visible?: boolean; + hidden?: boolean; + timeout?: number; +} + +/** + * @internal + */ +export interface PageBinding { + name: string; + pptrFunction: Function; +} + +/** + * @internal + */ +export class DOMWorld { + private _frameManager: FrameManager; + private _frame: Frame; + private _timeoutSettings: TimeoutSettings; + private _documentPromise?: Promise<ElementHandle> = null; + private _contextPromise?: Promise<ExecutionContext> = null; + + private _contextResolveCallback?: (x?: ExecutionContext) => void = null; + + private _detached = false; + /** + * @internal + */ + _waitTasks = new Set<WaitTask>(); + + /** + * @internal + * Contains mapping from functions that should be bound to Puppeteer functions. + */ + _boundFunctions = new Map<string, Function>(); + // Set of bindings that have been registered in the current context. + private _ctxBindings = new Set<string>(); + private static bindingIdentifier = (name: string, contextId: number) => + `${name}_${contextId}`; + + constructor( + frameManager: FrameManager, + frame: Frame, + timeoutSettings: TimeoutSettings + ) { + this._frameManager = frameManager; + this._frame = frame; + this._timeoutSettings = timeoutSettings; + this._setContext(null); + frameManager._client.on('Runtime.bindingCalled', (event) => + this._onBindingCalled(event) + ); + } + + frame(): Frame { + return this._frame; + } + + async _setContext(context?: ExecutionContext): Promise<void> { + if (context) { + this._contextResolveCallback.call(null, context); + this._contextResolveCallback = null; + for (const waitTask of this._waitTasks) waitTask.rerun(); + } else { + this._documentPromise = null; + this._contextPromise = new Promise((fulfill) => { + this._contextResolveCallback = fulfill; + }); + } + } + + _hasContext(): boolean { + return !this._contextResolveCallback; + } + + _detach(): void { + this._detached = true; + for (const waitTask of this._waitTasks) + waitTask.terminate( + new Error('waitForFunction failed: frame got detached.') + ); + } + + executionContext(): Promise<ExecutionContext> { + if (this._detached) + throw new Error( + `Execution context is not available in detached frame "${this._frame.url()}" (are you trying to evaluate?)` + ); + return this._contextPromise; + } + + async evaluateHandle<HandlerType extends JSHandle = JSHandle>( + pageFunction: EvaluateHandleFn, + ...args: SerializableOrJSHandle[] + ): Promise<HandlerType> { + const context = await this.executionContext(); + return context.evaluateHandle(pageFunction, ...args); + } + + async evaluate<T extends EvaluateFn>( + pageFunction: T, + ...args: SerializableOrJSHandle[] + ): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> { + const context = await this.executionContext(); + return context.evaluate<UnwrapPromiseLike<EvaluateFnReturnType<T>>>( + pageFunction, + ...args + ); + } + + async $(selector: string): Promise<ElementHandle | null> { + const document = await this._document(); + const value = await document.$(selector); + return value; + } + + async _document(): Promise<ElementHandle> { + if (this._documentPromise) return this._documentPromise; + this._documentPromise = this.executionContext().then(async (context) => { + const document = await context.evaluateHandle('document'); + return document.asElement(); + }); + return this._documentPromise; + } + + async $x(expression: string): Promise<ElementHandle[]> { + const document = await this._document(); + const value = await document.$x(expression); + return value; + } + + async $eval<ReturnType>( + selector: string, + pageFunction: ( + element: Element, + ...args: unknown[] + ) => ReturnType | Promise<ReturnType>, + ...args: SerializableOrJSHandle[] + ): Promise<WrapElementHandle<ReturnType>> { + const document = await this._document(); + return document.$eval<ReturnType>(selector, pageFunction, ...args); + } + + async $$eval<ReturnType>( + selector: string, + pageFunction: ( + elements: Element[], + ...args: unknown[] + ) => ReturnType | Promise<ReturnType>, + ...args: SerializableOrJSHandle[] + ): Promise<WrapElementHandle<ReturnType>> { + const document = await this._document(); + const value = await document.$$eval<ReturnType>( + selector, + pageFunction, + ...args + ); + return value; + } + + async $$(selector: string): Promise<ElementHandle[]> { + const document = await this._document(); + const value = await document.$$(selector); + return value; + } + + async content(): Promise<string> { + return await this.evaluate(() => { + let retVal = ''; + if (document.doctype) + retVal = new XMLSerializer().serializeToString(document.doctype); + if (document.documentElement) + retVal += document.documentElement.outerHTML; + return retVal; + }); + } + + async setContent( + html: string, + options: { + timeout?: number; + waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; + } = {} + ): Promise<void> { + const { + waitUntil = ['load'], + timeout = this._timeoutSettings.navigationTimeout(), + } = options; + // We rely upon the fact that document.open() will reset frame lifecycle with "init" + // lifecycle event. @see https://crrev.com/608658 + await this.evaluate<(x: string) => void>((html) => { + document.open(); + document.write(html); + document.close(); + }, html); + const watcher = new LifecycleWatcher( + this._frameManager, + this._frame, + waitUntil, + timeout + ); + const error = await Promise.race([ + watcher.timeoutOrTerminationPromise(), + watcher.lifecyclePromise(), + ]); + watcher.dispose(); + if (error) throw error; + } + + /** + * Adds a script tag into the current context. + * + * @remarks + * + * You can pass a URL, filepath or string of contents. Note that when running Puppeteer + * in a browser environment you cannot pass a filepath and should use either + * `url` or `content`. + */ + async addScriptTag(options: { + url?: string; + path?: string; + content?: string; + type?: string; + }): Promise<ElementHandle> { + const { url = null, path = null, content = null, type = '' } = options; + if (url !== null) { + try { + const context = await this.executionContext(); + return ( + await context.evaluateHandle(addScriptUrl, url, type) + ).asElement(); + } catch (error) { + throw new Error(`Loading script from ${url} failed`); + } + } + + if (path !== null) { + if (!isNode) { + throw new Error( + 'Cannot pass a filepath to addScriptTag in the browser environment.' + ); + } + const fs = await helper.importFSModule(); + let contents = await fs.promises.readFile(path, 'utf8'); + contents += '//# sourceURL=' + path.replace(/\n/g, ''); + const context = await this.executionContext(); + return ( + await context.evaluateHandle(addScriptContent, contents, type) + ).asElement(); + } + + if (content !== null) { + const context = await this.executionContext(); + return ( + await context.evaluateHandle(addScriptContent, content, type) + ).asElement(); + } + + throw new Error( + 'Provide an object with a `url`, `path` or `content` property' + ); + + async function addScriptUrl( + url: string, + type: string + ): Promise<HTMLElement> { + const script = document.createElement('script'); + script.src = url; + if (type) script.type = type; + const promise = new Promise((res, rej) => { + script.onload = res; + script.onerror = rej; + }); + document.head.appendChild(script); + await promise; + return script; + } + + function addScriptContent( + content: string, + type = 'text/javascript' + ): HTMLElement { + const script = document.createElement('script'); + script.type = type; + script.text = content; + let error = null; + script.onerror = (e) => (error = e); + document.head.appendChild(script); + if (error) throw error; + return script; + } + } + + /** + * Adds a style tag into the current context. + * + * @remarks + * + * You can pass a URL, filepath or string of contents. Note that when running Puppeteer + * in a browser environment you cannot pass a filepath and should use either + * `url` or `content`. + * + */ + async addStyleTag(options: { + url?: string; + path?: string; + content?: string; + }): Promise<ElementHandle> { + const { url = null, path = null, content = null } = options; + if (url !== null) { + try { + const context = await this.executionContext(); + return (await context.evaluateHandle(addStyleUrl, url)).asElement(); + } catch (error) { + throw new Error(`Loading style from ${url} failed`); + } + } + + if (path !== null) { + if (!isNode) { + throw new Error( + 'Cannot pass a filepath to addStyleTag in the browser environment.' + ); + } + const fs = await helper.importFSModule(); + let contents = await fs.promises.readFile(path, 'utf8'); + contents += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/'; + const context = await this.executionContext(); + return ( + await context.evaluateHandle(addStyleContent, contents) + ).asElement(); + } + + if (content !== null) { + const context = await this.executionContext(); + return ( + await context.evaluateHandle(addStyleContent, content) + ).asElement(); + } + + throw new Error( + 'Provide an object with a `url`, `path` or `content` property' + ); + + async function addStyleUrl(url: string): Promise<HTMLElement> { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = url; + const promise = new Promise((res, rej) => { + link.onload = res; + link.onerror = rej; + }); + document.head.appendChild(link); + await promise; + return link; + } + + async function addStyleContent(content: string): Promise<HTMLElement> { + const style = document.createElement('style'); + style.type = 'text/css'; + style.appendChild(document.createTextNode(content)); + const promise = new Promise((res, rej) => { + style.onload = res; + style.onerror = rej; + }); + document.head.appendChild(style); + await promise; + return style; + } + } + + async click( + selector: string, + options: { delay?: number; button?: MouseButton; clickCount?: number } + ): Promise<void> { + const handle = await this.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.click(options); + await handle.dispose(); + } + + async focus(selector: string): Promise<void> { + const handle = await this.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.focus(); + await handle.dispose(); + } + + async hover(selector: string): Promise<void> { + const handle = await this.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.hover(); + await handle.dispose(); + } + + async select(selector: string, ...values: string[]): Promise<string[]> { + const handle = await this.$(selector); + assert(handle, 'No node found for selector: ' + selector); + const result = await handle.select(...values); + await handle.dispose(); + return result; + } + + async tap(selector: string): Promise<void> { + const handle = await this.$(selector); + await handle.tap(); + await handle.dispose(); + } + + async type( + selector: string, + text: string, + options?: { delay: number } + ): Promise<void> { + const handle = await this.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.type(text, options); + await handle.dispose(); + } + + async waitForSelector( + selector: string, + options: WaitForSelectorOptions + ): Promise<ElementHandle | null> { + const { updatedSelector, queryHandler } = getQueryHandlerAndSelector( + selector + ); + return queryHandler.waitFor(this, updatedSelector, options); + } + + // If multiple waitFor are set up asynchronously, we need to wait for the + // first one to set up the binding in the page before running the others. + private _settingUpBinding: Promise<void> | null = null; + /** + * @internal + */ + async addBindingToContext( + context: ExecutionContext, + name: string + ): Promise<void> { + // Previous operation added the binding so we are done. + if ( + this._ctxBindings.has( + DOMWorld.bindingIdentifier(name, context._contextId) + ) + ) { + return; + } + // Wait for other operation to finish + if (this._settingUpBinding) { + await this._settingUpBinding; + return this.addBindingToContext(context, name); + } + + const bind = async (name: string) => { + const expression = helper.pageBindingInitString('internal', name); + try { + await context._client.send('Runtime.addBinding', { + name, + executionContextId: context._contextId, + }); + await context.evaluate(expression); + } catch (error) { + // We could have tried to evaluate in a context which was already + // destroyed. This happens, for example, if the page is navigated while + // we are trying to add the binding + const ctxDestroyed = error.message.includes( + 'Execution context was destroyed' + ); + const ctxNotFound = error.message.includes( + 'Cannot find context with specified id' + ); + if (ctxDestroyed || ctxNotFound) { + return; + } else { + debugError(error); + return; + } + } + this._ctxBindings.add( + DOMWorld.bindingIdentifier(name, context._contextId) + ); + }; + + this._settingUpBinding = bind(name); + await this._settingUpBinding; + this._settingUpBinding = null; + } + + private async _onBindingCalled( + event: Protocol.Runtime.BindingCalledEvent + ): Promise<void> { + let payload: { type: string; name: string; seq: number; args: unknown[] }; + if (!this._hasContext()) return; + const context = await this.executionContext(); + try { + payload = JSON.parse(event.payload); + } catch { + // The binding was either called by something in the page or it was + // called before our wrapper was initialized. + return; + } + const { type, name, seq, args } = payload; + if ( + type !== 'internal' || + !this._ctxBindings.has( + DOMWorld.bindingIdentifier(name, context._contextId) + ) + ) + return; + if (context._contextId !== event.executionContextId) return; + try { + const result = await this._boundFunctions.get(name)(...args); + await context.evaluate(deliverResult, name, seq, result); + } catch (error) { + // The WaitTask may already have been resolved by timing out, or the + // exection context may have been destroyed. + // In both caes, the promises above are rejected with a protocol error. + // We can safely ignores these, as the WaitTask is re-installed in + // the next execution context if needed. + if (error.message.includes('Protocol error')) return; + debugError(error); + } + function deliverResult(name: string, seq: number, result: unknown): void { + globalThis[name].callbacks.get(seq).resolve(result); + globalThis[name].callbacks.delete(seq); + } + } + + /** + * @internal + */ + async waitForSelectorInPage( + queryOne: Function, + selector: string, + options: WaitForSelectorOptions, + binding?: PageBinding + ): Promise<ElementHandle | null> { + const { + visible: waitForVisible = false, + hidden: waitForHidden = false, + timeout = this._timeoutSettings.timeout(), + } = options; + const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation'; + const title = `selector \`${selector}\`${ + waitForHidden ? ' to be hidden' : '' + }`; + async function predicate( + selector: string, + waitForVisible: boolean, + waitForHidden: boolean + ): Promise<Node | null | boolean> { + const node = predicateQueryHandler + ? ((await predicateQueryHandler(document, selector)) as Element) + : document.querySelector(selector); + return checkWaitForOptions(node, waitForVisible, waitForHidden); + } + const waitTaskOptions: WaitTaskOptions = { + domWorld: this, + predicateBody: helper.makePredicateString(predicate, queryOne), + title, + polling, + timeout, + args: [selector, waitForVisible, waitForHidden], + binding, + }; + const waitTask = new WaitTask(waitTaskOptions); + const jsHandle = await waitTask.promise; + const elementHandle = jsHandle.asElement(); + if (!elementHandle) { + await jsHandle.dispose(); + return null; + } + return elementHandle; + } + + async waitForXPath( + xpath: string, + options: WaitForSelectorOptions + ): Promise<ElementHandle | null> { + const { + visible: waitForVisible = false, + hidden: waitForHidden = false, + timeout = this._timeoutSettings.timeout(), + } = options; + const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation'; + const title = `XPath \`${xpath}\`${waitForHidden ? ' to be hidden' : ''}`; + function predicate( + xpath: string, + waitForVisible: boolean, + waitForHidden: boolean + ): Node | null | boolean { + const node = document.evaluate( + xpath, + document, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null + ).singleNodeValue; + return checkWaitForOptions(node, waitForVisible, waitForHidden); + } + const waitTaskOptions: WaitTaskOptions = { + domWorld: this, + predicateBody: helper.makePredicateString(predicate), + title, + polling, + timeout, + args: [xpath, waitForVisible, waitForHidden], + }; + const waitTask = new WaitTask(waitTaskOptions); + const jsHandle = await waitTask.promise; + const elementHandle = jsHandle.asElement(); + if (!elementHandle) { + await jsHandle.dispose(); + return null; + } + return elementHandle; + } + + waitForFunction( + pageFunction: Function | string, + options: { polling?: string | number; timeout?: number } = {}, + ...args: SerializableOrJSHandle[] + ): Promise<JSHandle> { + const { + polling = 'raf', + timeout = this._timeoutSettings.timeout(), + } = options; + const waitTaskOptions: WaitTaskOptions = { + domWorld: this, + predicateBody: pageFunction, + title: 'function', + polling, + timeout, + args, + }; + const waitTask = new WaitTask(waitTaskOptions); + return waitTask.promise; + } + + async title(): Promise<string> { + return this.evaluate(() => document.title); + } +} + +/** + * @internal + */ +export interface WaitTaskOptions { + domWorld: DOMWorld; + predicateBody: Function | string; + title: string; + polling: string | number; + timeout: number; + binding?: PageBinding; + args: SerializableOrJSHandle[]; +} + +/** + * @internal + */ +export class WaitTask { + _domWorld: DOMWorld; + _polling: string | number; + _timeout: number; + _predicateBody: string; + _args: SerializableOrJSHandle[]; + _binding: PageBinding; + _runCount = 0; + promise: Promise<JSHandle>; + _resolve: (x: JSHandle) => void; + _reject: (x: Error) => void; + _timeoutTimer?: NodeJS.Timeout; + _terminated = false; + + constructor(options: WaitTaskOptions) { + if (helper.isString(options.polling)) + assert( + options.polling === 'raf' || options.polling === 'mutation', + 'Unknown polling option: ' + options.polling + ); + else if (helper.isNumber(options.polling)) + assert( + options.polling > 0, + 'Cannot poll with non-positive interval: ' + options.polling + ); + else throw new Error('Unknown polling options: ' + options.polling); + + function getPredicateBody(predicateBody: Function | string) { + if (helper.isString(predicateBody)) return `return (${predicateBody});`; + return `return (${predicateBody})(...args);`; + } + + this._domWorld = options.domWorld; + this._polling = options.polling; + this._timeout = options.timeout; + this._predicateBody = getPredicateBody(options.predicateBody); + this._args = options.args; + this._binding = options.binding; + this._runCount = 0; + this._domWorld._waitTasks.add(this); + if (this._binding) { + this._domWorld._boundFunctions.set( + this._binding.name, + this._binding.pptrFunction + ); + } + this.promise = new Promise<JSHandle>((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + // Since page navigation requires us to re-install the pageScript, we should track + // timeout on our end. + if (options.timeout) { + const timeoutError = new TimeoutError( + `waiting for ${options.title} failed: timeout ${options.timeout}ms exceeded` + ); + this._timeoutTimer = setTimeout( + () => this.terminate(timeoutError), + options.timeout + ); + } + this.rerun(); + } + + terminate(error: Error): void { + this._terminated = true; + this._reject(error); + this._cleanup(); + } + + async rerun(): Promise<void> { + const runCount = ++this._runCount; + let success: JSHandle = null; + let error: Error = null; + const context = await this._domWorld.executionContext(); + if (this._terminated || runCount !== this._runCount) return; + if (this._binding) { + await this._domWorld.addBindingToContext(context, this._binding.name); + } + if (this._terminated || runCount !== this._runCount) return; + try { + success = await context.evaluateHandle( + waitForPredicatePageFunction, + this._predicateBody, + this._polling, + this._timeout, + ...this._args + ); + } catch (error_) { + error = error_; + } + + if (this._terminated || runCount !== this._runCount) { + if (success) await success.dispose(); + return; + } + + // Ignore timeouts in pageScript - we track timeouts ourselves. + // If the frame's execution context has already changed, `frame.evaluate` will + // throw an error - ignore this predicate run altogether. + if ( + !error && + (await this._domWorld.evaluate((s) => !s, success).catch(() => true)) + ) { + await success.dispose(); + return; + } + if (error) { + if (error.message.includes('TypeError: binding is not a function')) { + return this.rerun(); + } + // When frame is detached the task should have been terminated by the DOMWorld. + // This can fail if we were adding this task while the frame was detached, + // so we terminate here instead. + if ( + error.message.includes( + 'Execution context is not available in detached frame' + ) + ) { + this.terminate( + new Error('waitForFunction failed: frame got detached.') + ); + return; + } + + // When the page is navigated, the promise is rejected. + // We will try again in the new execution context. + if (error.message.includes('Execution context was destroyed')) return; + + // We could have tried to evaluate in a context which was already + // destroyed. + if (error.message.includes('Cannot find context with specified id')) + return; + + this._reject(error); + } else { + this._resolve(success); + } + this._cleanup(); + } + + _cleanup(): void { + clearTimeout(this._timeoutTimer); + this._domWorld._waitTasks.delete(this); + } +} + +async function waitForPredicatePageFunction( + predicateBody: string, + polling: string, + timeout: number, + ...args: unknown[] +): Promise<unknown> { + const predicate = new Function('...args', predicateBody); + let timedOut = false; + if (timeout) setTimeout(() => (timedOut = true), timeout); + if (polling === 'raf') return await pollRaf(); + if (polling === 'mutation') return await pollMutation(); + if (typeof polling === 'number') return await pollInterval(polling); + + /** + * @returns {!Promise<*>} + */ + async function pollMutation(): Promise<unknown> { + const success = await predicate(...args); + if (success) return Promise.resolve(success); + + let fulfill; + const result = new Promise((x) => (fulfill = x)); + const observer = new MutationObserver(async () => { + if (timedOut) { + observer.disconnect(); + fulfill(); + } + const success = await predicate(...args); + if (success) { + observer.disconnect(); + fulfill(success); + } + }); + observer.observe(document, { + childList: true, + subtree: true, + attributes: true, + }); + return result; + } + + async function pollRaf(): Promise<unknown> { + let fulfill; + const result = new Promise((x) => (fulfill = x)); + await onRaf(); + return result; + + async function onRaf(): Promise<unknown> { + if (timedOut) { + fulfill(); + return; + } + const success = await predicate(...args); + if (success) fulfill(success); + else requestAnimationFrame(onRaf); + } + } + + async function pollInterval(pollInterval: number): Promise<unknown> { + let fulfill; + const result = new Promise((x) => (fulfill = x)); + await onTimeout(); + return result; + + async function onTimeout(): Promise<unknown> { + if (timedOut) { + fulfill(); + return; + } + const success = await predicate(...args); + if (success) fulfill(success); + else setTimeout(onTimeout, pollInterval); + } + } +} diff --git a/remote/test/puppeteer/src/common/Debug.ts b/remote/test/puppeteer/src/common/Debug.ts new file mode 100644 index 0000000000..8ff124b094 --- /dev/null +++ b/remote/test/puppeteer/src/common/Debug.ts @@ -0,0 +1,83 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isNode } from '../environment.js'; + +/** + * A debug function that can be used in any environment. + * + * @remarks + * + * If used in Node, it falls back to the + * {@link https://www.npmjs.com/package/debug | debug module}. In the browser it + * uses `console.log`. + * + * @param prefix - this will be prefixed to each log. + * @returns a function that can be called to log to that debug channel. + * + * In Node, use the `DEBUG` environment variable to control logging: + * + * ``` + * DEBUG=* // logs all channels + * DEBUG=foo // logs the `foo` channel + * DEBUG=foo* // logs any channels starting with `foo` + * ``` + * + * In the browser, set `window.__PUPPETEER_DEBUG` to a string: + * + * ``` + * window.__PUPPETEER_DEBUG='*'; // logs all channels + * window.__PUPPETEER_DEBUG='foo'; // logs the `foo` channel + * window.__PUPPETEER_DEBUG='foo*'; // logs any channels starting with `foo` + * ``` + * + * @example + * ``` + * const log = debug('Page'); + * + * log('new page created') + * // logs "Page: new page created" + * ``` + */ +export const debug = (prefix: string): ((...args: unknown[]) => void) => { + if (isNode) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require('debug')(prefix); + } + + return (...logArgs: unknown[]): void => { + const debugLevel = globalThis.__PUPPETEER_DEBUG as string; + if (!debugLevel) return; + + const everythingShouldBeLogged = debugLevel === '*'; + + const prefixMatchesDebugLevel = + everythingShouldBeLogged || + /** + * If the debug level is `foo*`, that means we match any prefix that + * starts with `foo`. If the level is `foo`, we match only the prefix + * `foo`. + */ + (debugLevel.endsWith('*') + ? prefix.startsWith(debugLevel) + : prefix === debugLevel); + + if (!prefixMatchesDebugLevel) return; + + // eslint-disable-next-line no-console + console.log(`${prefix}:`, ...logArgs); + }; +}; diff --git a/remote/test/puppeteer/src/common/DeviceDescriptors.ts b/remote/test/puppeteer/src/common/DeviceDescriptors.ts new file mode 100644 index 0000000000..a4a4960b94 --- /dev/null +++ b/remote/test/puppeteer/src/common/DeviceDescriptors.ts @@ -0,0 +1,964 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +interface Device { + name: string; + userAgent: string; + viewport: { + width: number; + height: number; + deviceScaleFactor: number; + isMobile: boolean; + hasTouch: boolean; + isLandscape: boolean; + }; +} + +const devices: Device[] = [ + { + name: 'Blackberry PlayBook', + userAgent: + 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+', + viewport: { + width: 600, + height: 1024, + deviceScaleFactor: 1, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Blackberry PlayBook landscape', + userAgent: + 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+', + viewport: { + width: 1024, + height: 600, + deviceScaleFactor: 1, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'BlackBerry Z30', + userAgent: + 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+', + viewport: { + width: 360, + height: 640, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'BlackBerry Z30 landscape', + userAgent: + 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+', + viewport: { + width: 640, + height: 360, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Galaxy Note 3', + userAgent: + 'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + viewport: { + width: 360, + height: 640, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Galaxy Note 3 landscape', + userAgent: + 'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + viewport: { + width: 640, + height: 360, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Galaxy Note II', + userAgent: + 'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + viewport: { + width: 360, + height: 640, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Galaxy Note II landscape', + userAgent: + 'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + viewport: { + width: 640, + height: 360, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Galaxy S III', + userAgent: + 'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + viewport: { + width: 360, + height: 640, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Galaxy S III landscape', + userAgent: + 'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + viewport: { + width: 640, + height: 360, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Galaxy S5', + userAgent: + 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 360, + height: 640, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Galaxy S5 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 640, + height: 360, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPad', + userAgent: + 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1', + viewport: { + width: 768, + height: 1024, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPad landscape', + userAgent: + 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1', + viewport: { + width: 1024, + height: 768, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPad Mini', + userAgent: + 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1', + viewport: { + width: 768, + height: 1024, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPad Mini landscape', + userAgent: + 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1', + viewport: { + width: 1024, + height: 768, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPad Pro', + userAgent: + 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1', + viewport: { + width: 1024, + height: 1366, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPad Pro landscape', + userAgent: + 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1', + viewport: { + width: 1366, + height: 1024, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 4', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53', + viewport: { + width: 320, + height: 480, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 4 landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53', + viewport: { + width: 480, + height: 320, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 5', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1', + viewport: { + width: 320, + height: 568, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 5 landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1', + viewport: { + width: 568, + height: 320, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 6', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 375, + height: 667, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 6 landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 667, + height: 375, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 6 Plus', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 414, + height: 736, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 6 Plus landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 736, + height: 414, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 7', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 375, + height: 667, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 7 landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 667, + height: 375, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 7 Plus', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 414, + height: 736, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 7 Plus landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 736, + height: 414, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 8', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 375, + height: 667, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 8 landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 667, + height: 375, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 8 Plus', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 414, + height: 736, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 8 Plus landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 736, + height: 414, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone SE', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1', + viewport: { + width: 320, + height: 568, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone SE landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1', + viewport: { + width: 568, + height: 320, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone X', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 375, + height: 812, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone X landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 812, + height: 375, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone XR', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1', + viewport: { + width: 414, + height: 896, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone XR landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1', + viewport: { + width: 896, + height: 414, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'JioPhone 2', + userAgent: + 'Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5', + viewport: { + width: 240, + height: 320, + deviceScaleFactor: 1, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'JioPhone 2 landscape', + userAgent: + 'Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5', + viewport: { + width: 320, + height: 240, + deviceScaleFactor: 1, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Kindle Fire HDX', + userAgent: + 'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true', + viewport: { + width: 800, + height: 1280, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Kindle Fire HDX landscape', + userAgent: + 'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true', + viewport: { + width: 1280, + height: 800, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'LG Optimus L70', + userAgent: + 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 384, + height: 640, + deviceScaleFactor: 1.25, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'LG Optimus L70 landscape', + userAgent: + 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 640, + height: 384, + deviceScaleFactor: 1.25, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Microsoft Lumia 550', + userAgent: + 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263', + viewport: { + width: 640, + height: 360, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Microsoft Lumia 950', + userAgent: + 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263', + viewport: { + width: 360, + height: 640, + deviceScaleFactor: 4, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Microsoft Lumia 950 landscape', + userAgent: + 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263', + viewport: { + width: 640, + height: 360, + deviceScaleFactor: 4, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Nexus 10', + userAgent: + 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36', + viewport: { + width: 800, + height: 1280, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Nexus 10 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36', + viewport: { + width: 1280, + height: 800, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Nexus 4', + userAgent: + 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 384, + height: 640, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Nexus 4 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 640, + height: 384, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Nexus 5', + userAgent: + 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 360, + height: 640, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Nexus 5 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 640, + height: 360, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Nexus 5X', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 412, + height: 732, + deviceScaleFactor: 2.625, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Nexus 5X landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 732, + height: 412, + deviceScaleFactor: 2.625, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Nexus 6', + userAgent: + 'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 412, + height: 732, + deviceScaleFactor: 3.5, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Nexus 6 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 732, + height: 412, + deviceScaleFactor: 3.5, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Nexus 6P', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 412, + height: 732, + deviceScaleFactor: 3.5, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Nexus 6P landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 732, + height: 412, + deviceScaleFactor: 3.5, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Nexus 7', + userAgent: + 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36', + viewport: { + width: 600, + height: 960, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Nexus 7 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36', + viewport: { + width: 960, + height: 600, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Nokia Lumia 520', + userAgent: + 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)', + viewport: { + width: 320, + height: 533, + deviceScaleFactor: 1.5, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Nokia Lumia 520 landscape', + userAgent: + 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)', + viewport: { + width: 533, + height: 320, + deviceScaleFactor: 1.5, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Nokia N9', + userAgent: + 'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13', + viewport: { + width: 480, + height: 854, + deviceScaleFactor: 1, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Nokia N9 landscape', + userAgent: + 'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13', + viewport: { + width: 854, + height: 480, + deviceScaleFactor: 1, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Pixel 2', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 411, + height: 731, + deviceScaleFactor: 2.625, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Pixel 2 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 731, + height: 411, + deviceScaleFactor: 2.625, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Pixel 2 XL', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 411, + height: 823, + deviceScaleFactor: 3.5, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Pixel 2 XL landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 823, + height: 411, + deviceScaleFactor: 3.5, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, +]; + +export type DevicesMap = { + [name: string]: Device; +}; + +const devicesMap: DevicesMap = {}; + +for (const device of devices) devicesMap[device.name] = device; + +export { devicesMap }; diff --git a/remote/test/puppeteer/src/common/Dialog.ts b/remote/test/puppeteer/src/common/Dialog.ts new file mode 100644 index 0000000000..48720239b6 --- /dev/null +++ b/remote/test/puppeteer/src/common/Dialog.ts @@ -0,0 +1,111 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assert } from './assert.js'; +import { CDPSession } from './Connection.js'; +import { Protocol } from 'devtools-protocol'; + +/** + * Dialog instances are dispatched by the {@link Page} via the `dialog` event. + * + * @remarks + * + * @example + * ```js + * const puppeteer = require('puppeteer'); + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * page.on('dialog', async dialog => { + * console.log(dialog.message()); + * await dialog.dismiss(); + * await browser.close(); + * }); + * page.evaluate(() => alert('1')); + * })(); + * ``` + */ +export class Dialog { + private _client: CDPSession; + private _type: Protocol.Page.DialogType; + private _message: string; + private _defaultValue: string; + private _handled = false; + + /** + * @internal + */ + constructor( + client: CDPSession, + type: Protocol.Page.DialogType, + message: string, + defaultValue = '' + ) { + this._client = client; + this._type = type; + this._message = message; + this._defaultValue = defaultValue; + } + + /** + * @returns The type of the dialog. + */ + type(): Protocol.Page.DialogType { + return this._type; + } + + /** + * @returns The message displayed in the dialog. + */ + message(): string { + return this._message; + } + + /** + * @returns The default value of the prompt, or an empty string if the dialog + * is not a `prompt`. + */ + defaultValue(): string { + return this._defaultValue; + } + + /** + * @param promptText - optional text that will be entered in the dialog + * prompt. Has no effect if the dialog's type is not `prompt`. + * + * @returns A promise that resolves when the dialog has been accepted. + */ + async accept(promptText?: string): Promise<void> { + assert(!this._handled, 'Cannot accept dialog which is already handled!'); + this._handled = true; + await this._client.send('Page.handleJavaScriptDialog', { + accept: true, + promptText: promptText, + }); + } + + /** + * @returns A promise which will resolve once the dialog has been dismissed + */ + async dismiss(): Promise<void> { + assert(!this._handled, 'Cannot dismiss dialog which is already handled!'); + this._handled = true; + await this._client.send('Page.handleJavaScriptDialog', { + accept: false, + }); + } +} diff --git a/remote/test/puppeteer/src/common/EmulationManager.ts b/remote/test/puppeteer/src/common/EmulationManager.ts new file mode 100644 index 0000000000..f3130ef3c5 --- /dev/null +++ b/remote/test/puppeteer/src/common/EmulationManager.ts @@ -0,0 +1,58 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { CDPSession } from './Connection.js'; +import { Viewport } from './PuppeteerViewport.js'; +import { Protocol } from 'devtools-protocol'; + +export class EmulationManager { + _client: CDPSession; + _emulatingMobile = false; + _hasTouch = false; + + constructor(client: CDPSession) { + this._client = client; + } + + async emulateViewport(viewport: Viewport): Promise<boolean> { + const mobile = viewport.isMobile || false; + const width = viewport.width; + const height = viewport.height; + const deviceScaleFactor = viewport.deviceScaleFactor || 1; + const screenOrientation: Protocol.Emulation.ScreenOrientation = viewport.isLandscape + ? { angle: 90, type: 'landscapePrimary' } + : { angle: 0, type: 'portraitPrimary' }; + const hasTouch = viewport.hasTouch || false; + + await Promise.all([ + this._client.send('Emulation.setDeviceMetricsOverride', { + mobile, + width, + height, + deviceScaleFactor, + screenOrientation, + }), + this._client.send('Emulation.setTouchEmulationEnabled', { + enabled: hasTouch, + }), + ]); + + const reloadNeeded = + this._emulatingMobile !== mobile || this._hasTouch !== hasTouch; + this._emulatingMobile = mobile; + this._hasTouch = hasTouch; + return reloadNeeded; + } +} diff --git a/remote/test/puppeteer/src/common/Errors.ts b/remote/test/puppeteer/src/common/Errors.ts new file mode 100644 index 0000000000..2ba151495d --- /dev/null +++ b/remote/test/puppeteer/src/common/Errors.ts @@ -0,0 +1,41 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +class CustomError extends Error { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +} + +/** + * TimeoutError is emitted whenever certain operations are terminated due to timeout. + * + * @remarks + * + * Example operations are {@link Page.waitForSelector | page.waitForSelector} + * or {@link PuppeteerNode.launch | puppeteer.launch}. + * + * @public + */ +export class TimeoutError extends CustomError {} + +export type PuppeteerErrors = Record<string, typeof CustomError>; + +export const puppeteerErrors: PuppeteerErrors = { + TimeoutError, +}; diff --git a/remote/test/puppeteer/src/common/EvalTypes.ts b/remote/test/puppeteer/src/common/EvalTypes.ts new file mode 100644 index 0000000000..7a9335942a --- /dev/null +++ b/remote/test/puppeteer/src/common/EvalTypes.ts @@ -0,0 +1,81 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JSHandle, ElementHandle } from './JSHandle.js'; + +/** + * @public + */ +export type EvaluateFn<T = unknown> = + | string + | ((arg1: T, ...args: unknown[]) => unknown); + +export type UnwrapPromiseLike<T> = T extends PromiseLike<infer U> ? U : T; + +/** + * @public + */ +export type EvaluateFnReturnType<T extends EvaluateFn> = T extends ( + ...args: unknown[] +) => infer R + ? R + : unknown; + +/** + * @public + */ +export type EvaluateHandleFn = string | ((...args: unknown[]) => unknown); + +/** + * @public + */ +export type Serializable = + | number + | string + | boolean + | null + | BigInt + | JSONArray + | JSONObject; + +/** + * @public + */ +export type JSONArray = Serializable[]; + +/** + * @public + */ +export interface JSONObject { + [key: string]: Serializable; +} + +/** + * @public + */ +export type SerializableOrJSHandle = Serializable | JSHandle; + +/** + * Wraps a DOM element into an ElementHandle instance + * @public + **/ +export type WrapElementHandle<X> = X extends Element ? ElementHandle<X> : X; + +/** + * Unwraps a DOM element out of an ElementHandle instance + * @public + **/ +export type UnwrapElementHandle<X> = X extends ElementHandle<infer E> ? E : X; diff --git a/remote/test/puppeteer/src/common/EventEmitter.ts b/remote/test/puppeteer/src/common/EventEmitter.ts new file mode 100644 index 0000000000..3588f1408b --- /dev/null +++ b/remote/test/puppeteer/src/common/EventEmitter.ts @@ -0,0 +1,144 @@ +import mitt, { + Emitter, + EventType, + Handler, +} from '../../vendor/mitt/src/index.js'; + +/** + * @internal + */ +export interface CommonEventEmitter { + on(event: EventType, handler: Handler): CommonEventEmitter; + off(event: EventType, handler: Handler): CommonEventEmitter; + /* To maintain parity with the built in NodeJS event emitter which uses removeListener + * rather than `off`. + * If you're implementing new code you should use `off`. + */ + addListener(event: EventType, handler: Handler): CommonEventEmitter; + removeListener(event: EventType, handler: Handler): CommonEventEmitter; + emit(event: EventType, eventData?: any): boolean; + once(event: EventType, handler: Handler): CommonEventEmitter; + listenerCount(event: string): number; + + removeAllListeners(event?: EventType): CommonEventEmitter; +} + +/** + * The EventEmitter class that many Puppeteer classes extend. + * + * @remarks + * + * This allows you to listen to events that Puppeteer classes fire and act + * accordingly. Therefore you'll mostly use {@link EventEmitter.on | on} and + * {@link EventEmitter.off | off} to bind + * and unbind to event listeners. + * + * @public + */ +export class EventEmitter implements CommonEventEmitter { + private emitter: Emitter; + private eventsMap = new Map<EventType, Handler[]>(); + + /** + * @internal + */ + constructor() { + this.emitter = mitt(this.eventsMap); + } + + /** + * Bind an event listener to fire when an event occurs. + * @param event - the event type you'd like to listen to. Can be a string or symbol. + * @param handler - the function to be called when the event occurs. + * @returns `this` to enable you to chain calls. + */ + on(event: EventType, handler: Handler): EventEmitter { + this.emitter.on(event, handler); + return this; + } + + /** + * Remove an event listener from firing. + * @param event - the event type you'd like to stop listening to. + * @param handler - the function that should be removed. + * @returns `this` to enable you to chain calls. + */ + off(event: EventType, handler: Handler): EventEmitter { + this.emitter.off(event, handler); + return this; + } + + /** + * Remove an event listener. + * @deprecated please use `off` instead. + */ + removeListener(event: EventType, handler: Handler): EventEmitter { + this.off(event, handler); + return this; + } + + /** + * Add an event listener. + * @deprecated please use `on` instead. + */ + addListener(event: EventType, handler: Handler): EventEmitter { + this.on(event, handler); + return this; + } + + /** + * Emit an event and call any associated listeners. + * + * @param event - the event you'd like to emit + * @param eventData - any data you'd like to emit with the event + * @returns `true` if there are any listeners, `false` if there are not. + */ + emit(event: EventType, eventData?: any): boolean { + this.emitter.emit(event, eventData); + return this.eventListenersCount(event) > 0; + } + + /** + * Like `on` but the listener will only be fired once and then it will be removed. + * @param event - the event you'd like to listen to + * @param handler - the handler function to run when the event occurs + * @returns `this` to enable you to chain calls. + */ + once(event: EventType, handler: Handler): EventEmitter { + const onceHandler: Handler = (eventData) => { + handler(eventData); + this.off(event, onceHandler); + }; + + return this.on(event, onceHandler); + } + + /** + * Gets the number of listeners for a given event. + * + * @param event - the event to get the listener count for + * @returns the number of listeners bound to the given event + */ + listenerCount(event: EventType): number { + return this.eventListenersCount(event); + } + + /** + * Removes all listeners. If given an event argument, it will remove only + * listeners for that event. + * @param event - the event to remove listeners for. + * @returns `this` to enable you to chain calls. + */ + removeAllListeners(event?: EventType): EventEmitter { + if (event) { + this.eventsMap.delete(event); + } else { + this.eventsMap.clear(); + } + return this; + } + + private eventListenersCount(event: EventType): number { + return this.eventsMap.has(event) ? this.eventsMap.get(event).length : 0; + } +} diff --git a/remote/test/puppeteer/src/common/Events.ts b/remote/test/puppeteer/src/common/Events.ts new file mode 100644 index 0000000000..90d8bae785 --- /dev/null +++ b/remote/test/puppeteer/src/common/Events.ts @@ -0,0 +1,97 @@ +/** + * Copyright 2019 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * IMPORTANT: we are mid-way through migrating away from this Events.ts file + * in favour of defining events next to the class that emits them. + * + * However we need to maintain this file for now because the legacy DocLint + * system relies on them. Be aware in the mean time if you make a change here + * you probably need to replicate it in the relevant class. For example if you + * add a new Page event, you should update the PageEmittedEvents enum in + * src/common/Page.ts. + * + * Chat to @jackfranklin if you're unsure. + */ + +export const Events = { + Page: { + Close: 'close', + Console: 'console', + Dialog: 'dialog', + DOMContentLoaded: 'domcontentloaded', + Error: 'error', + // Can't use just 'error' due to node.js special treatment of error events. + // @see https://nodejs.org/api/events.html#events_error_events + PageError: 'pageerror', + Request: 'request', + Response: 'response', + RequestFailed: 'requestfailed', + RequestFinished: 'requestfinished', + FrameAttached: 'frameattached', + FrameDetached: 'framedetached', + FrameNavigated: 'framenavigated', + Load: 'load', + Metrics: 'metrics', + Popup: 'popup', + WorkerCreated: 'workercreated', + WorkerDestroyed: 'workerdestroyed', + }, + + Browser: { + TargetCreated: 'targetcreated', + TargetDestroyed: 'targetdestroyed', + TargetChanged: 'targetchanged', + Disconnected: 'disconnected', + }, + + BrowserContext: { + TargetCreated: 'targetcreated', + TargetDestroyed: 'targetdestroyed', + TargetChanged: 'targetchanged', + }, + + NetworkManager: { + Request: Symbol('Events.NetworkManager.Request'), + Response: Symbol('Events.NetworkManager.Response'), + RequestFailed: Symbol('Events.NetworkManager.RequestFailed'), + RequestFinished: Symbol('Events.NetworkManager.RequestFinished'), + }, + + FrameManager: { + FrameAttached: Symbol('Events.FrameManager.FrameAttached'), + FrameNavigated: Symbol('Events.FrameManager.FrameNavigated'), + FrameDetached: Symbol('Events.FrameManager.FrameDetached'), + LifecycleEvent: Symbol('Events.FrameManager.LifecycleEvent'), + FrameNavigatedWithinDocument: Symbol( + 'Events.FrameManager.FrameNavigatedWithinDocument' + ), + ExecutionContextCreated: Symbol( + 'Events.FrameManager.ExecutionContextCreated' + ), + ExecutionContextDestroyed: Symbol( + 'Events.FrameManager.ExecutionContextDestroyed' + ), + }, + + Connection: { + Disconnected: Symbol('Events.Connection.Disconnected'), + }, + + CDPSession: { + Disconnected: Symbol('Events.CDPSession.Disconnected'), + }, +} as const; diff --git a/remote/test/puppeteer/src/common/ExecutionContext.ts b/remote/test/puppeteer/src/common/ExecutionContext.ts new file mode 100644 index 0000000000..4420410de0 --- /dev/null +++ b/remote/test/puppeteer/src/common/ExecutionContext.ts @@ -0,0 +1,387 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assert } from './assert.js'; +import { helper } from './helper.js'; +import { createJSHandle, JSHandle, ElementHandle } from './JSHandle.js'; +import { CDPSession } from './Connection.js'; +import { DOMWorld } from './DOMWorld.js'; +import { Frame } from './FrameManager.js'; +import { Protocol } from 'devtools-protocol'; +import { EvaluateHandleFn, SerializableOrJSHandle } from './EvalTypes.js'; + +export const EVALUATION_SCRIPT_URL = '__puppeteer_evaluation_script__'; +const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; + +/** + * This class represents a context for JavaScript execution. A [Page] might have + * many execution contexts: + * - each + * {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe | + * frame } has "default" execution context that is always created after frame is + * attached to DOM. This context is returned by the + * {@link frame.executionContext()} method. + * - {@link https://developer.chrome.com/extensions | Extension}'s content scripts + * create additional execution contexts. + * + * Besides pages, execution contexts can be found in + * {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API | + * workers }. + * + * @public + */ +export class ExecutionContext { + /** + * @internal + */ + _client: CDPSession; + /** + * @internal + */ + _world: DOMWorld; + /** + * @internal + */ + _contextId: number; + + /** + * @internal + */ + constructor( + client: CDPSession, + contextPayload: Protocol.Runtime.ExecutionContextDescription, + world: DOMWorld + ) { + this._client = client; + this._world = world; + this._contextId = contextPayload.id; + } + + /** + * @remarks + * + * Not every execution context is associated with a frame. For + * example, workers and extensions have execution contexts that are not + * associated with frames. + * + * @returns The frame associated with this execution context. + */ + frame(): Frame | null { + return this._world ? this._world.frame() : null; + } + + /** + * @remarks + * If the function passed to the `executionContext.evaluate` returns a + * Promise, then `executionContext.evaluate` would wait for the promise to + * resolve and return its value. If the function passed to the + * `executionContext.evaluate` returns a non-serializable value, then + * `executionContext.evaluate` resolves to `undefined`. DevTools Protocol also + * supports transferring some additional values that are not serializable by + * `JSON`: `-0`, `NaN`, `Infinity`, `-Infinity`, and bigint literals. + * + * + * @example + * ```js + * const executionContext = await page.mainFrame().executionContext(); + * const result = await executionContext.evaluate(() => Promise.resolve(8 * 7))* ; + * console.log(result); // prints "56" + * ``` + * + * @example + * A string can also be passed in instead of a function. + * + * ```js + * console.log(await executionContext.evaluate('1 + 2')); // prints "3" + * ``` + * + * @example + * {@link JSHandle} instances can be passed as arguments to the + * `executionContext.* evaluate`: + * ```js + * const oneHandle = await executionContext.evaluateHandle(() => 1); + * const twoHandle = await executionContext.evaluateHandle(() => 2); + * const result = await executionContext.evaluate( + * (a, b) => a + b, oneHandle, * twoHandle + * ); + * await oneHandle.dispose(); + * await twoHandle.dispose(); + * console.log(result); // prints '3'. + * ``` + * @param pageFunction a function to be evaluated in the `executionContext` + * @param args argument to pass to the page function + * + * @returns A promise that resolves to the return value of the given function. + */ + async evaluate<ReturnType extends any>( + pageFunction: Function | string, + ...args: unknown[] + ): Promise<ReturnType> { + return await this._evaluateInternal<ReturnType>( + true, + pageFunction, + ...args + ); + } + + /** + * @remarks + * The only difference between `executionContext.evaluate` and + * `executionContext.evaluateHandle` is that `executionContext.evaluateHandle` + * returns an in-page object (a {@link JSHandle}). + * If the function passed to the `executionContext.evaluateHandle` returns a + * Promise, then `executionContext.evaluateHandle` would wait for the + * promise to resolve and return its value. + * + * @example + * ```js + * const context = await page.mainFrame().executionContext(); + * const aHandle = await context.evaluateHandle(() => Promise.resolve(self)); + * aHandle; // Handle for the global object. + * ``` + * + * @example + * A string can also be passed in instead of a function. + * + * ```js + * // Handle for the '3' * object. + * const aHandle = await context.evaluateHandle('1 + 2'); + * ``` + * + * @example + * JSHandle instances can be passed as arguments + * to the `executionContext.* evaluateHandle`: + * + * ```js + * const aHandle = await context.evaluateHandle(() => document.body); + * const resultHandle = await context.evaluateHandle(body => body.innerHTML, * aHandle); + * console.log(await resultHandle.jsonValue()); // prints body's innerHTML + * await aHandle.dispose(); + * await resultHandle.dispose(); + * ``` + * + * @param pageFunction a function to be evaluated in the `executionContext` + * @param args argument to pass to the page function + * + * @returns A promise that resolves to the return value of the given function + * as an in-page object (a {@link JSHandle}). + */ + async evaluateHandle<HandleType extends JSHandle | ElementHandle = JSHandle>( + pageFunction: EvaluateHandleFn, + ...args: SerializableOrJSHandle[] + ): Promise<HandleType> { + return this._evaluateInternal<HandleType>(false, pageFunction, ...args); + } + + private async _evaluateInternal<ReturnType>( + returnByValue: boolean, + pageFunction: Function | string, + ...args: unknown[] + ): Promise<ReturnType> { + const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`; + + if (helper.isString(pageFunction)) { + const contextId = this._contextId; + const expression = pageFunction; + const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression) + ? expression + : expression + '\n' + suffix; + + const { exceptionDetails, result: remoteObject } = await this._client + .send('Runtime.evaluate', { + expression: expressionWithSourceUrl, + contextId, + returnByValue, + awaitPromise: true, + userGesture: true, + }) + .catch(rewriteError); + + if (exceptionDetails) + throw new Error( + 'Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails) + ); + + return returnByValue + ? helper.valueFromRemoteObject(remoteObject) + : createJSHandle(this, remoteObject); + } + + if (typeof pageFunction !== 'function') + throw new Error( + `Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.` + ); + + let functionText = pageFunction.toString(); + try { + new Function('(' + functionText + ')'); + } catch (error) { + // This means we might have a function shorthand. Try another + // time prefixing 'function '. + if (functionText.startsWith('async ')) + functionText = + 'async function ' + functionText.substring('async '.length); + else functionText = 'function ' + functionText; + try { + new Function('(' + functionText + ')'); + } catch (error) { + // We tried hard to serialize, but there's a weird beast here. + throw new Error('Passed function is not well-serializable!'); + } + } + let callFunctionOnPromise; + try { + callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', { + functionDeclaration: functionText + '\n' + suffix + '\n', + executionContextId: this._contextId, + arguments: args.map(convertArgument.bind(this)), + returnByValue, + awaitPromise: true, + userGesture: true, + }); + } catch (error) { + if ( + error instanceof TypeError && + error.message.startsWith('Converting circular structure to JSON') + ) + error.message += ' Are you passing a nested JSHandle?'; + throw error; + } + const { + exceptionDetails, + result: remoteObject, + } = await callFunctionOnPromise.catch(rewriteError); + if (exceptionDetails) + throw new Error( + 'Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails) + ); + return returnByValue + ? helper.valueFromRemoteObject(remoteObject) + : createJSHandle(this, remoteObject); + + /** + * @param {*} arg + * @returns {*} + * @this {ExecutionContext} + */ + function convertArgument(this: ExecutionContext, arg: unknown): unknown { + if (typeof arg === 'bigint') + // eslint-disable-line valid-typeof + return { unserializableValue: `${arg.toString()}n` }; + if (Object.is(arg, -0)) return { unserializableValue: '-0' }; + if (Object.is(arg, Infinity)) return { unserializableValue: 'Infinity' }; + if (Object.is(arg, -Infinity)) + return { unserializableValue: '-Infinity' }; + if (Object.is(arg, NaN)) return { unserializableValue: 'NaN' }; + const objectHandle = arg && arg instanceof JSHandle ? arg : null; + if (objectHandle) { + if (objectHandle._context !== this) + throw new Error( + 'JSHandles can be evaluated only in the context they were created!' + ); + if (objectHandle._disposed) throw new Error('JSHandle is disposed!'); + if (objectHandle._remoteObject.unserializableValue) + return { + unserializableValue: objectHandle._remoteObject.unserializableValue, + }; + if (!objectHandle._remoteObject.objectId) + return { value: objectHandle._remoteObject.value }; + return { objectId: objectHandle._remoteObject.objectId }; + } + return { value: arg }; + } + + function rewriteError(error: Error): Protocol.Runtime.EvaluateResponse { + if (error.message.includes('Object reference chain is too long')) + return { result: { type: 'undefined' } }; + if (error.message.includes("Object couldn't be returned by value")) + return { result: { type: 'undefined' } }; + + if ( + error.message.endsWith('Cannot find context with specified id') || + error.message.endsWith('Inspected target navigated or closed') + ) + throw new Error( + 'Execution context was destroyed, most likely because of a navigation.' + ); + throw error; + } + } + + /** + * This method iterates the JavaScript heap and finds all the objects with the + * given prototype. + * @remarks + * @example + * ```js + * // Create a Map object + * await page.evaluate(() => window.map = new Map()); + * // Get a handle to the Map object prototype + * const mapPrototype = await page.evaluateHandle(() => Map.prototype); + * // Query all map instances into an array + * const mapInstances = await page.queryObjects(mapPrototype); + * // Count amount of map objects in heap + * const count = await page.evaluate(maps => maps.length, mapInstances); + * await mapInstances.dispose(); + * await mapPrototype.dispose(); + * ``` + * + * @param prototypeHandle a handle to the object prototype + * + * @returns A handle to an array of objects with the given prototype. + */ + async queryObjects(prototypeHandle: JSHandle): Promise<JSHandle> { + assert(!prototypeHandle._disposed, 'Prototype JSHandle is disposed!'); + assert( + prototypeHandle._remoteObject.objectId, + 'Prototype JSHandle must not be referencing primitive value' + ); + const response = await this._client.send('Runtime.queryObjects', { + prototypeObjectId: prototypeHandle._remoteObject.objectId, + }); + return createJSHandle(this, response.objects); + } + + /** + * @internal + */ + async _adoptBackendNodeId( + backendNodeId: Protocol.DOM.BackendNodeId + ): Promise<ElementHandle> { + const { object } = await this._client.send('DOM.resolveNode', { + backendNodeId: backendNodeId, + executionContextId: this._contextId, + }); + return createJSHandle(this, object) as ElementHandle; + } + + /** + * @internal + */ + async _adoptElementHandle( + elementHandle: ElementHandle + ): Promise<ElementHandle> { + assert( + elementHandle.executionContext() !== this, + 'Cannot adopt handle that already belongs to this execution context' + ); + assert(this._world, 'Cannot adopt handle without DOMWorld'); + const nodeInfo = await this._client.send('DOM.describeNode', { + objectId: elementHandle._remoteObject.objectId, + }); + return this._adoptBackendNodeId(nodeInfo.node.backendNodeId); + } +} diff --git a/remote/test/puppeteer/src/common/FileChooser.ts b/remote/test/puppeteer/src/common/FileChooser.ts new file mode 100644 index 0000000000..3406918824 --- /dev/null +++ b/remote/test/puppeteer/src/common/FileChooser.ts @@ -0,0 +1,85 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ElementHandle } from './JSHandle.js'; +import { Protocol } from 'devtools-protocol'; +import { assert } from './assert.js'; + +/** + * File choosers let you react to the page requesting for a file. + * @remarks + * `FileChooser` objects are returned via the `page.waitForFileChooser` method. + * @example + * An example of using `FileChooser`: + * ```js + * const [fileChooser] = await Promise.all([ + * page.waitForFileChooser(), + * page.click('#upload-file-button'), // some button that triggers file selection + * ]); + * await fileChooser.accept(['/tmp/myfile.pdf']); + * ``` + * **NOTE** In browsers, only one file chooser can be opened at a time. + * All file choosers must be accepted or canceled. Not doing so will prevent + * subsequent file choosers from appearing. + */ +export class FileChooser { + private _element: ElementHandle; + private _multiple: boolean; + private _handled = false; + + /** + * @internal + */ + constructor( + element: ElementHandle, + event: Protocol.Page.FileChooserOpenedEvent + ) { + this._element = element; + this._multiple = event.mode !== 'selectSingle'; + } + + /** + * Whether file chooser allow for {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#attr-multiple | multiple} file selection. + */ + isMultiple(): boolean { + return this._multiple; + } + + /** + * Accept the file chooser request with given paths. + * @param filePaths - If some of the `filePaths` are relative paths, + * then they are resolved relative to the {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory}. + */ + async accept(filePaths: string[]): Promise<void> { + assert( + !this._handled, + 'Cannot accept FileChooser which is already handled!' + ); + this._handled = true; + await this._element.uploadFile(...filePaths); + } + + /** + * Closes the file chooser without selecting any files. + */ + async cancel(): Promise<void> { + assert( + !this._handled, + 'Cannot cancel FileChooser which is already handled!' + ); + this._handled = true; + } +} diff --git a/remote/test/puppeteer/src/common/FrameManager.ts b/remote/test/puppeteer/src/common/FrameManager.ts new file mode 100644 index 0000000000..e1017487d3 --- /dev/null +++ b/remote/test/puppeteer/src/common/FrameManager.ts @@ -0,0 +1,1309 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { debug } from '../common/Debug.js'; + +import { EventEmitter } from './EventEmitter.js'; +import { assert } from './assert.js'; +import { helper, debugError } from './helper.js'; +import { ExecutionContext, EVALUATION_SCRIPT_URL } from './ExecutionContext.js'; +import { + LifecycleWatcher, + PuppeteerLifeCycleEvent, +} from './LifecycleWatcher.js'; +import { DOMWorld, WaitForSelectorOptions } from './DOMWorld.js'; +import { NetworkManager } from './NetworkManager.js'; +import { TimeoutSettings } from './TimeoutSettings.js'; +import { CDPSession } from './Connection.js'; +import { JSHandle, ElementHandle } from './JSHandle.js'; +import { MouseButton } from './Input.js'; +import { Page } from './Page.js'; +import { HTTPResponse } from './HTTPResponse.js'; +import { Protocol } from 'devtools-protocol'; +import { + SerializableOrJSHandle, + EvaluateHandleFn, + WrapElementHandle, + EvaluateFn, + EvaluateFnReturnType, + UnwrapPromiseLike, +} from './EvalTypes.js'; + +const UTILITY_WORLD_NAME = '__puppeteer_utility_world__'; + +/** + * We use symbols to prevent external parties listening to these events. + * They are internal to Puppeteer. + * + * @internal + */ +export const FrameManagerEmittedEvents = { + FrameAttached: Symbol('FrameManager.FrameAttached'), + FrameNavigated: Symbol('FrameManager.FrameNavigated'), + FrameDetached: Symbol('FrameManager.FrameDetached'), + LifecycleEvent: Symbol('FrameManager.LifecycleEvent'), + FrameNavigatedWithinDocument: Symbol( + 'FrameManager.FrameNavigatedWithinDocument' + ), + ExecutionContextCreated: Symbol('FrameManager.ExecutionContextCreated'), + ExecutionContextDestroyed: Symbol('FrameManager.ExecutionContextDestroyed'), +}; + +/** + * @internal + */ +export class FrameManager extends EventEmitter { + _client: CDPSession; + private _page: Page; + private _networkManager: NetworkManager; + _timeoutSettings: TimeoutSettings; + private _frames = new Map<string, Frame>(); + private _contextIdToContext = new Map<number, ExecutionContext>(); + private _isolatedWorlds = new Set<string>(); + private _mainFrame: Frame; + + constructor( + client: CDPSession, + page: Page, + ignoreHTTPSErrors: boolean, + timeoutSettings: TimeoutSettings + ) { + super(); + this._client = client; + this._page = page; + this._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this); + this._timeoutSettings = timeoutSettings; + this._client.on('Page.frameAttached', (event) => + this._onFrameAttached(event.frameId, event.parentFrameId) + ); + this._client.on('Page.frameNavigated', (event) => + this._onFrameNavigated(event.frame) + ); + this._client.on('Page.navigatedWithinDocument', (event) => + this._onFrameNavigatedWithinDocument(event.frameId, event.url) + ); + this._client.on('Page.frameDetached', (event) => + this._onFrameDetached(event.frameId) + ); + this._client.on('Page.frameStoppedLoading', (event) => + this._onFrameStoppedLoading(event.frameId) + ); + this._client.on('Runtime.executionContextCreated', (event) => + this._onExecutionContextCreated(event.context) + ); + this._client.on('Runtime.executionContextDestroyed', (event) => + this._onExecutionContextDestroyed(event.executionContextId) + ); + this._client.on('Runtime.executionContextsCleared', () => + this._onExecutionContextsCleared() + ); + this._client.on('Page.lifecycleEvent', (event) => + this._onLifecycleEvent(event) + ); + this._client.on('Target.attachedToTarget', async (event) => + this._onFrameMoved(event) + ); + } + + async initialize(): Promise<void> { + const result = await Promise.all([ + this._client.send('Page.enable'), + this._client.send('Page.getFrameTree'), + ]); + + const { frameTree } = result[1]; + this._handleFrameTree(frameTree); + await Promise.all([ + this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }), + this._client + .send('Runtime.enable') + .then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)), + this._networkManager.initialize(), + ]); + } + + networkManager(): NetworkManager { + return this._networkManager; + } + + async navigateFrame( + frame: Frame, + url: string, + options: { + referer?: string; + timeout?: number; + waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; + } = {} + ): Promise<HTTPResponse | null> { + assertNoLegacyNavigationOptions(options); + const { + referer = this._networkManager.extraHTTPHeaders()['referer'], + waitUntil = ['load'], + timeout = this._timeoutSettings.navigationTimeout(), + } = options; + + const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout); + let ensureNewDocumentNavigation = false; + let error = await Promise.race([ + navigate(this._client, url, referer, frame._id), + watcher.timeoutOrTerminationPromise(), + ]); + if (!error) { + error = await Promise.race([ + watcher.timeoutOrTerminationPromise(), + ensureNewDocumentNavigation + ? watcher.newDocumentNavigationPromise() + : watcher.sameDocumentNavigationPromise(), + ]); + } + watcher.dispose(); + if (error) throw error; + return watcher.navigationResponse(); + + async function navigate( + client: CDPSession, + url: string, + referrer: string, + frameId: string + ): Promise<Error | null> { + try { + const response = await client.send('Page.navigate', { + url, + referrer, + frameId, + }); + ensureNewDocumentNavigation = !!response.loaderId; + return response.errorText + ? new Error(`${response.errorText} at ${url}`) + : null; + } catch (error) { + return error; + } + } + } + + async waitForFrameNavigation( + frame: Frame, + options: { + timeout?: number; + waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; + } = {} + ): Promise<HTTPResponse | null> { + assertNoLegacyNavigationOptions(options); + const { + waitUntil = ['load'], + timeout = this._timeoutSettings.navigationTimeout(), + } = options; + const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout); + const error = await Promise.race([ + watcher.timeoutOrTerminationPromise(), + watcher.sameDocumentNavigationPromise(), + watcher.newDocumentNavigationPromise(), + ]); + watcher.dispose(); + if (error) throw error; + return watcher.navigationResponse(); + } + + private async _onFrameMoved(event: Protocol.Target.AttachedToTargetEvent) { + if (event.targetInfo.type !== 'iframe') { + return; + } + + // TODO(sadym): Remove debug message once proper OOPIF support is + // implemented: https://github.com/puppeteer/puppeteer/issues/2548 + debug('puppeteer:frame')( + `The frame '${event.targetInfo.targetId}' moved to another session. ` + + `Out-of-process iframes (OOPIF) are not supported by Puppeteer yet. ` + + `https://github.com/puppeteer/puppeteer/issues/2548` + ); + } + + _onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void { + const frame = this._frames.get(event.frameId); + if (!frame) return; + frame._onLifecycleEvent(event.loaderId, event.name); + this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame); + } + + _onFrameStoppedLoading(frameId: string): void { + const frame = this._frames.get(frameId); + if (!frame) return; + frame._onLoadingStopped(); + this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame); + } + + _handleFrameTree(frameTree: Protocol.Page.FrameTree): void { + if (frameTree.frame.parentId) + this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId); + this._onFrameNavigated(frameTree.frame); + if (!frameTree.childFrames) return; + + for (const child of frameTree.childFrames) this._handleFrameTree(child); + } + + page(): Page { + return this._page; + } + + mainFrame(): Frame { + return this._mainFrame; + } + + frames(): Frame[] { + return Array.from(this._frames.values()); + } + + frame(frameId: string): Frame | null { + return this._frames.get(frameId) || null; + } + + _onFrameAttached(frameId: string, parentFrameId?: string): void { + if (this._frames.has(frameId)) return; + assert(parentFrameId); + const parentFrame = this._frames.get(parentFrameId); + const frame = new Frame(this, parentFrame, frameId); + this._frames.set(frame._id, frame); + this.emit(FrameManagerEmittedEvents.FrameAttached, frame); + } + + _onFrameNavigated(framePayload: Protocol.Page.Frame): void { + const isMainFrame = !framePayload.parentId; + let frame = isMainFrame + ? this._mainFrame + : this._frames.get(framePayload.id); + assert( + isMainFrame || frame, + 'We either navigate top level or have old version of the navigated frame' + ); + + // Detach all child frames first. + if (frame) { + for (const child of frame.childFrames()) + this._removeFramesRecursively(child); + } + + // Update or create main frame. + if (isMainFrame) { + if (frame) { + // Update frame id to retain frame identity on cross-process navigation. + this._frames.delete(frame._id); + frame._id = framePayload.id; + } else { + // Initial main frame navigation. + frame = new Frame(this, null, framePayload.id); + } + this._frames.set(framePayload.id, frame); + this._mainFrame = frame; + } + + // Update frame payload. + frame._navigated(framePayload); + + this.emit(FrameManagerEmittedEvents.FrameNavigated, frame); + } + + async _ensureIsolatedWorld(name: string): Promise<void> { + if (this._isolatedWorlds.has(name)) return; + this._isolatedWorlds.add(name); + await this._client.send('Page.addScriptToEvaluateOnNewDocument', { + source: `//# sourceURL=${EVALUATION_SCRIPT_URL}`, + worldName: name, + }), + await Promise.all( + this.frames().map((frame) => + this._client + .send('Page.createIsolatedWorld', { + frameId: frame._id, + grantUniveralAccess: true, + worldName: name, + }) + .catch(debugError) + ) + ); // frames might be removed before we send this + } + + _onFrameNavigatedWithinDocument(frameId: string, url: string): void { + const frame = this._frames.get(frameId); + if (!frame) return; + frame._navigatedWithinDocument(url); + this.emit(FrameManagerEmittedEvents.FrameNavigatedWithinDocument, frame); + this.emit(FrameManagerEmittedEvents.FrameNavigated, frame); + } + + _onFrameDetached(frameId: string): void { + const frame = this._frames.get(frameId); + if (frame) this._removeFramesRecursively(frame); + } + + _onExecutionContextCreated( + contextPayload: Protocol.Runtime.ExecutionContextDescription + ): void { + const auxData = contextPayload.auxData as { frameId?: string }; + const frameId = auxData ? auxData.frameId : null; + const frame = this._frames.get(frameId) || null; + let world = null; + if (frame) { + if (contextPayload.auxData && !!contextPayload.auxData['isDefault']) { + world = frame._mainWorld; + } else if ( + contextPayload.name === UTILITY_WORLD_NAME && + !frame._secondaryWorld._hasContext() + ) { + // In case of multiple sessions to the same target, there's a race between + // connections so we might end up creating multiple isolated worlds. + // We can use either. + world = frame._secondaryWorld; + } + } + if (contextPayload.auxData && contextPayload.auxData['type'] === 'isolated') + this._isolatedWorlds.add(contextPayload.name); + const context = new ExecutionContext(this._client, contextPayload, world); + if (world) world._setContext(context); + this._contextIdToContext.set(contextPayload.id, context); + } + + private _onExecutionContextDestroyed(executionContextId: number): void { + const context = this._contextIdToContext.get(executionContextId); + if (!context) return; + this._contextIdToContext.delete(executionContextId); + if (context._world) context._world._setContext(null); + } + + private _onExecutionContextsCleared(): void { + for (const context of this._contextIdToContext.values()) { + if (context._world) context._world._setContext(null); + } + this._contextIdToContext.clear(); + } + + executionContextById(contextId: number): ExecutionContext { + const context = this._contextIdToContext.get(contextId); + assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId); + return context; + } + + private _removeFramesRecursively(frame: Frame): void { + for (const child of frame.childFrames()) + this._removeFramesRecursively(child); + frame._detach(); + this._frames.delete(frame._id); + this.emit(FrameManagerEmittedEvents.FrameDetached, frame); + } +} + +/** + * @public + */ +export interface FrameWaitForFunctionOptions { + /** + * An interval at which the `pageFunction` is executed, defaults to `raf`. If + * `polling` is a number, then it is treated as an interval in milliseconds at + * which the function would be executed. If `polling` is a string, then it can + * be one of the following values: + * + * - `raf` - to constantly execute `pageFunction` in `requestAnimationFrame` + * callback. This is the tightest polling mode which is suitable to observe + * styling changes. + * + * - `mutation` - to execute `pageFunction` on every DOM mutation. + */ + polling?: string | number; + /** + * Maximum time to wait in milliseconds. Defaults to `30000` (30 seconds). + * Pass `0` to disable the timeout. Puppeteer's default timeout can be changed + * using {@link Page.setDefaultTimeout}. + */ + timeout?: number; +} + +/** + * @public + */ +export interface FrameAddScriptTagOptions { + /** + * the URL of the script to be added. + */ + url?: string; + /** + * The path to a JavaScript file to be injected into the frame. + * @remarks + * If `path` is a relative path, it is resolved relative to the current + * working directory (`process.cwd()` in Node.js). + */ + path?: string; + /** + * Raw JavaScript content to be injected into the frame. + */ + content?: string; + /** + * Set the script's `type`. Use `module` in order to load an ES2015 module. + */ + type?: string; +} + +/** + * @public + */ +export interface FrameAddStyleTagOptions { + /** + * the URL of the CSS file to be added. + */ + url?: string; + /** + * The path to a CSS file to be injected into the frame. + * @remarks + * If `path` is a relative path, it is resolved relative to the current + * working directory (`process.cwd()` in Node.js). + */ + path?: string; + /** + * Raw CSS content to be injected into the frame. + */ + content?: string; +} + +/** + * At every point of time, page exposes its current frame tree via the + * {@link Page.mainFrame | page.mainFrame} and + * {@link Frame.childFrames | frame.childFrames} methods. + * + * @remarks + * + * `Frame` object lifecycles are controlled by three events that are all + * dispatched on the page object: + * + * - {@link PageEmittedEvents.FrameAttached} + * + * - {@link PageEmittedEvents.FrameNavigated} + * + * - {@link PageEmittedEvents.FrameDetached} + * + * @Example + * An example of dumping frame tree: + * + * ```js + * const puppeteer = require('puppeteer'); + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.goto('https://www.google.com/chrome/browser/canary.html'); + * dumpFrameTree(page.mainFrame(), ''); + * await browser.close(); + * + * function dumpFrameTree(frame, indent) { + * console.log(indent + frame.url()); + * for (const child of frame.childFrames()) { + * dumpFrameTree(child, indent + ' '); + * } + * } + * })(); + * ``` + * + * @Example + * An example of getting text from an iframe element: + * + * ```js + * const frame = page.frames().find(frame => frame.name() === 'myframe'); + * const text = await frame.$eval('.selector', element => element.textContent); + * console.log(text); + * ``` + * + * @public + */ +export class Frame { + /** + * @internal + */ + _frameManager: FrameManager; + private _parentFrame?: Frame; + /** + * @internal + */ + _id: string; + + private _url = ''; + private _detached = false; + /** + * @internal + */ + _loaderId = ''; + /** + * @internal + */ + _name?: string; + + /** + * @internal + */ + _lifecycleEvents = new Set<string>(); + /** + * @internal + */ + _mainWorld: DOMWorld; + /** + * @internal + */ + _secondaryWorld: DOMWorld; + /** + * @internal + */ + _childFrames: Set<Frame>; + + /** + * @internal + */ + constructor( + frameManager: FrameManager, + parentFrame: Frame | null, + frameId: string + ) { + this._frameManager = frameManager; + this._parentFrame = parentFrame; + this._url = ''; + this._id = frameId; + this._detached = false; + + this._loaderId = ''; + this._mainWorld = new DOMWorld( + frameManager, + this, + frameManager._timeoutSettings + ); + this._secondaryWorld = new DOMWorld( + frameManager, + this, + frameManager._timeoutSettings + ); + + this._childFrames = new Set(); + if (this._parentFrame) this._parentFrame._childFrames.add(this); + } + + /** + * @remarks + * + * `frame.goto` will throw an error if: + * - there's an SSL error (e.g. in case of self-signed certificates). + * + * - target URL is invalid. + * + * - the `timeout` is exceeded during navigation. + * + * - the remote server does not respond or is unreachable. + * + * - the main resource failed to load. + * + * `frame.goto` will not throw an error when any valid HTTP status code is + * returned by the remote server, including 404 "Not Found" and 500 "Internal + * Server Error". The status code for such responses can be retrieved by + * calling {@link HTTPResponse.status}. + * + * NOTE: `frame.goto` either throws an error or returns a main resource + * response. The only exceptions are navigation to `about:blank` or + * navigation to the same URL with a different hash, which would succeed and + * return `null`. + * + * NOTE: Headless mode doesn't support navigation to a PDF document. See + * the {@link https://bugs.chromium.org/p/chromium/issues/detail?id=761295 | upstream + * issue}. + * + * @param url - the URL to navigate the frame to. This should include the + * scheme, e.g. `https://`. + * @param options - navigation options. `waitUntil` is useful to define when + * the navigation should be considered successful - see the docs for + * {@link PuppeteerLifeCycleEvent} for more details. + * + * @returns A promise which resolves to the main resource response. In case of + * multiple redirects, the navigation will resolve with the response of the + * last redirect. + */ + async goto( + url: string, + options: { + referer?: string; + timeout?: number; + waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; + } = {} + ): Promise<HTTPResponse | null> { + return await this._frameManager.navigateFrame(this, url, options); + } + + /** + * @remarks + * + * This resolves when the frame navigates to a new URL. It is useful for when + * you run code which will indirectly cause the frame to navigate. Consider + * this example: + * + * ```js + * const [response] = await Promise.all([ + * // The navigation promise resolves after navigation has finished + * frame.waitForNavigation(), + * // Clicking the link will indirectly cause a navigation + * frame.click('a.my-link'), + * ]); + * ``` + * + * Usage of the {@link https://developer.mozilla.org/en-US/docs/Web/API/History_API | History API} to change the URL is considered a navigation. + * + * @param options - options to configure when the navigation is consided finished. + * @returns a promise that resolves when the frame navigates to a new URL. + */ + async waitForNavigation( + options: { + timeout?: number; + waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; + } = {} + ): Promise<HTTPResponse | null> { + return await this._frameManager.waitForFrameNavigation(this, options); + } + + /** + * @returns a promise that resolves to the frame's default execution context. + */ + executionContext(): Promise<ExecutionContext> { + return this._mainWorld.executionContext(); + } + + /** + * @remarks + * + * The only difference between {@link Frame.evaluate} and + * `frame.evaluateHandle` is that `evaluateHandle` will return the value + * wrapped in an in-page object. + * + * This method behaves identically to {@link Page.evaluateHandle} except it's + * run within the context of the `frame`, rather than the entire page. + * + * @param pageFunction - a function that is run within the frame + * @param args - arguments to be passed to the pageFunction + */ + async evaluateHandle<HandlerType extends JSHandle = JSHandle>( + pageFunction: EvaluateHandleFn, + ...args: SerializableOrJSHandle[] + ): Promise<HandlerType> { + return this._mainWorld.evaluateHandle<HandlerType>(pageFunction, ...args); + } + + /** + * @remarks + * + * This method behaves identically to {@link Page.evaluate} except it's run + * within the context of the `frame`, rather than the entire page. + * + * @param pageFunction - a function that is run within the frame + * @param args - arguments to be passed to the pageFunction + */ + async evaluate<T extends EvaluateFn>( + pageFunction: T, + ...args: SerializableOrJSHandle[] + ): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> { + return this._mainWorld.evaluate<T>(pageFunction, ...args); + } + + /** + * This method queries the frame for the given selector. + * + * @param selector - a selector to query for. + * @returns A promise which resolves to an `ElementHandle` pointing at the + * element, or `null` if it was not found. + */ + async $(selector: string): Promise<ElementHandle | null> { + return this._mainWorld.$(selector); + } + + /** + * This method evaluates the given XPath expression and returns the results. + * + * @param expression - the XPath expression to evaluate. + */ + async $x(expression: string): Promise<ElementHandle[]> { + return this._mainWorld.$x(expression); + } + + /** + * @remarks + * + * This method runs `document.querySelector` within + * the frame and passes it as the first argument to `pageFunction`. + * + * If `pageFunction` returns a Promise, then `frame.$eval` would wait for + * the promise to resolve and return its value. + * + * @example + * + * ```js + * const searchValue = await frame.$eval('#search', el => el.value); + * ``` + * + * @param selector - the selector to query for + * @param pageFunction - the function to be evaluated in the frame's context + * @param args - additional arguments to pass to `pageFuncton` + */ + async $eval<ReturnType>( + selector: string, + pageFunction: ( + element: Element, + ...args: unknown[] + ) => ReturnType | Promise<ReturnType>, + ...args: SerializableOrJSHandle[] + ): Promise<WrapElementHandle<ReturnType>> { + return this._mainWorld.$eval<ReturnType>(selector, pageFunction, ...args); + } + + /** + * @remarks + * + * This method runs `Array.from(document.querySelectorAll(selector))` within + * the frame and passes it as the first argument to `pageFunction`. + * + * If `pageFunction` returns a Promise, then `frame.$$eval` would wait for + * the promise to resolve and return its value. + * + * @example + * + * ```js + * const divsCounts = await frame.$$eval('div', divs => divs.length); + * ``` + * + * @param selector - the selector to query for + * @param pageFunction - the function to be evaluated in the frame's context + * @param args - additional arguments to pass to `pageFuncton` + */ + async $$eval<ReturnType>( + selector: string, + pageFunction: ( + elements: Element[], + ...args: unknown[] + ) => ReturnType | Promise<ReturnType>, + ...args: SerializableOrJSHandle[] + ): Promise<WrapElementHandle<ReturnType>> { + return this._mainWorld.$$eval<ReturnType>(selector, pageFunction, ...args); + } + + /** + * This runs `document.querySelectorAll` in the frame and returns the result. + * + * @param selector - a selector to search for + * @returns An array of element handles pointing to the found frame elements. + */ + async $$(selector: string): Promise<ElementHandle[]> { + return this._mainWorld.$$(selector); + } + + /** + * @returns the full HTML contents of the frame, including the doctype. + */ + async content(): Promise<string> { + return this._secondaryWorld.content(); + } + + /** + * Set the content of the frame. + * + * @param html - HTML markup to assign to the page. + * @param options - options to configure how long before timing out and at + * what point to consider the content setting successful. + */ + async setContent( + html: string, + options: { + timeout?: number; + waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; + } = {} + ): Promise<void> { + return this._secondaryWorld.setContent(html, options); + } + + /** + * @remarks + * + * If the name is empty, it returns the `id` attribute instead. + * + * Note: This value is calculated once when the frame is created, and will not + * update if the attribute is changed later. + * + * @returns the frame's `name` attribute as specified in the tag. + */ + name(): string { + return this._name || ''; + } + + /** + * @returns the frame's URL. + */ + url(): string { + return this._url; + } + + /** + * @returns the parent `Frame`, if any. Detached and main frames return `null`. + */ + parentFrame(): Frame | null { + return this._parentFrame; + } + + /** + * @returns an array of child frames. + */ + childFrames(): Frame[] { + return Array.from(this._childFrames); + } + + /** + * @returns `true` if the frame has been detached, or `false` otherwise. + */ + isDetached(): boolean { + return this._detached; + } + + /** + * Adds a `<script>` tag into the page with the desired url or content. + * + * @param options - configure the script to add to the page. + * + * @returns a promise that resolves to the added tag when the script's + * `onload` event fires or when the script content was injected into the + * frame. + */ + async addScriptTag( + options: FrameAddScriptTagOptions + ): Promise<ElementHandle> { + return this._mainWorld.addScriptTag(options); + } + + /** + * Adds a `<link rel="stylesheet">` tag into the page with the desired url or + * a `<style type="text/css">` tag with the content. + * + * @param options - configure the CSS to add to the page. + * + * @returns a promise that resolves to the added tag when the stylesheets's + * `onload` event fires or when the CSS content was injected into the + * frame. + */ + async addStyleTag(options: FrameAddStyleTagOptions): Promise<ElementHandle> { + return this._mainWorld.addStyleTag(options); + } + + /** + * + * This method clicks the first element found that matches `selector`. + * + * @remarks + * + * This method scrolls the element into view if needed, and then uses + * {@link Page.mouse} to click in the center of the element. If there's no + * element matching `selector`, the method throws an error. + * + * Bear in mind that if `click()` triggers a navigation event and there's a + * separate `page.waitForNavigation()` promise to be resolved, you may end up + * with a race condition that yields unexpected results. The correct pattern + * for click and wait for navigation is the following: + * + * ```javascript + * const [response] = await Promise.all([ + * page.waitForNavigation(waitOptions), + * frame.click(selector, clickOptions), + * ]); + * ``` + * @param selector - the selector to search for to click. If there are + * multiple elements, the first will be clicked. + */ + async click( + selector: string, + options: { + delay?: number; + button?: MouseButton; + clickCount?: number; + } = {} + ): Promise<void> { + return this._secondaryWorld.click(selector, options); + } + + /** + * This method fetches an element with `selector` and focuses it. + * + * @remarks + * If there's no element matching `selector`, the method throws an error. + * + * @param selector - the selector for the element to focus. If there are + * multiple elements, the first will be focused. + */ + async focus(selector: string): Promise<void> { + return this._secondaryWorld.focus(selector); + } + + /** + * This method fetches an element with `selector`, scrolls it into view if + * needed, and then uses {@link Page.mouse} to hover over the center of the + * element. + * + * @remarks + * If there's no element matching `selector`, the method throws an + * + * @param selector - the selector for the element to hover. If there are + * multiple elements, the first will be hovered. + */ + async hover(selector: string): Promise<void> { + return this._secondaryWorld.hover(selector); + } + + /** + * Triggers a `change` and `input` event once all the provided options have + * been selected. + * + * @remarks + * + * If there's no `<select>` element matching `selector`, the + * method throws an error. + * + * @example + * ```js + * frame.select('select#colors', 'blue'); // single selection + * frame.select('select#colors', 'red', 'green', 'blue'); // multiple selections + * ``` + * + * @param selector - a selector to query the frame for + * @param values - an array of values to select. If the `<select>` has the + * `multiple` attribute, all values are considered, otherwise only the first + * one is taken into account. + * @returns the list of values that were successfully selected. + */ + select(selector: string, ...values: string[]): Promise<string[]> { + return this._secondaryWorld.select(selector, ...values); + } + + /** + * This method fetches an element with `selector`, scrolls it into view if + * needed, and then uses {@link Page.touchscreen} to tap in the center of the + * element. + * + * @remarks + * + * If there's no element matching `selector`, the method throws an error. + * + * @param selector - the selector to tap. + * @returns a promise that resolves when the element has been tapped. + */ + async tap(selector: string): Promise<void> { + return this._secondaryWorld.tap(selector); + } + + /** + * Sends a `keydown`, `keypress`/`input`, and `keyup` event for each character + * in the text. + * + * @remarks + * To press a special key, like `Control` or `ArrowDown`, use + * {@link Keyboard.press}. + * + * @example + * ```js + * await frame.type('#mytextarea', 'Hello'); // Types instantly + * await frame.type('#mytextarea', 'World', {delay: 100}); // Types slower, like a user + * ``` + * + * @param selector - the selector for the element to type into. If there are + * multiple the first will be used. + * @param text - text to type into the element + * @param options - takes one option, `delay`, which sets the time to wait + * between key presses in milliseconds. Defaults to `0`. + * + * @returns a promise that resolves when the typing is complete. + */ + async type( + selector: string, + text: string, + options?: { delay: number } + ): Promise<void> { + return this._mainWorld.type(selector, text, options); + } + + /** + * @remarks + * + * This method behaves differently depending on the first parameter. If it's a + * `string`, it will be treated as a `selector` or `xpath` (if the string + * starts with `//`). This method then is a shortcut for + * {@link Frame.waitForSelector} or {@link Frame.waitForXPath}. + * + * If the first argument is a function this method is a shortcut for + * {@link Frame.waitForFunction}. + * + * If the first argument is a `number`, it's treated as a timeout in + * milliseconds and the method returns a promise which resolves after the + * timeout. + * + * @param selectorOrFunctionOrTimeout - a selector, predicate or timeout to + * wait for. + * @param options - optional waiting parameters. + * @param args - arguments to pass to `pageFunction`. + * + * @deprecated Don't use this method directly. Instead use the more explicit + * methods available: {@link Frame.waitForSelector}, + * {@link Frame.waitForXPath}, {@link Frame.waitForFunction} or + * {@link Frame.waitForTimeout}. + */ + waitFor( + selectorOrFunctionOrTimeout: string | number | Function, + options: Record<string, unknown> = {}, + ...args: SerializableOrJSHandle[] + ): Promise<JSHandle | null> { + const xPathPattern = '//'; + + console.warn( + 'waitFor is deprecated and will be removed in a future release. See https://github.com/puppeteer/puppeteer/issues/6214 for details and how to migrate your code.' + ); + + if (helper.isString(selectorOrFunctionOrTimeout)) { + const string = selectorOrFunctionOrTimeout; + if (string.startsWith(xPathPattern)) + return this.waitForXPath(string, options); + return this.waitForSelector(string, options); + } + if (helper.isNumber(selectorOrFunctionOrTimeout)) + return new Promise((fulfill) => + setTimeout(fulfill, selectorOrFunctionOrTimeout) + ); + if (typeof selectorOrFunctionOrTimeout === 'function') + return this.waitForFunction( + selectorOrFunctionOrTimeout, + options, + ...args + ); + return Promise.reject( + new Error( + 'Unsupported target type: ' + typeof selectorOrFunctionOrTimeout + ) + ); + } + + /** + * Causes your script to wait for the given number of milliseconds. + * + * @remarks + * It's generally recommended to not wait for a number of seconds, but instead + * use {@link Frame.waitForSelector}, {@link Frame.waitForXPath} or + * {@link Frame.waitForFunction} to wait for exactly the conditions you want. + * + * @example + * + * Wait for 1 second: + * + * ``` + * await frame.waitForTimeout(1000); + * ``` + * + * @param milliseconds - the number of milliseconds to wait. + */ + waitForTimeout(milliseconds: number): Promise<void> { + return new Promise((resolve) => { + setTimeout(resolve, milliseconds); + }); + } + + /** + * @remarks + * + * + * Wait for the `selector` to appear in page. If at the moment of calling the + * method the `selector` already exists, the method will return immediately. + * If the selector doesn't appear after the `timeout` milliseconds of waiting, + * the function will throw. + * + * This method works across navigations. + * + * @example + * ```js + * const puppeteer = require('puppeteer'); + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * let currentURL; + * page.mainFrame() + * .waitForSelector('img') + * .then(() => console.log('First URL with image: ' + currentURL)); + * + * for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com']) { + * await page.goto(currentURL); + * } + * await browser.close(); + * })(); + * ``` + * @param selector - the selector to wait for. + * @param options - options to define if the element should be visible and how + * long to wait before timing out. + * @returns a promise which resolves when an element matching the selector + * string is added to the DOM. + */ + async waitForSelector( + selector: string, + options: WaitForSelectorOptions = {} + ): Promise<ElementHandle | null> { + const handle = await this._secondaryWorld.waitForSelector( + selector, + options + ); + if (!handle) return null; + const mainExecutionContext = await this._mainWorld.executionContext(); + const result = await mainExecutionContext._adoptElementHandle(handle); + await handle.dispose(); + return result; + } + + /** + * @remarks + * Wait for the `xpath` to appear in page. If at the moment of calling the + * method the `xpath` already exists, the method will return immediately. If + * the xpath doesn't appear after the `timeout` milliseconds of waiting, the + * function will throw. + * + * For a code example, see the example for {@link Frame.waitForSelector}. That + * function behaves identically other than taking a CSS selector rather than + * an XPath. + * + * @param xpath - the XPath expression to wait for. + * @param options - options to configure the visiblity of the element and how + * long to wait before timing out. + */ + async waitForXPath( + xpath: string, + options: WaitForSelectorOptions = {} + ): Promise<ElementHandle | null> { + const handle = await this._secondaryWorld.waitForXPath(xpath, options); + if (!handle) return null; + const mainExecutionContext = await this._mainWorld.executionContext(); + const result = await mainExecutionContext._adoptElementHandle(handle); + await handle.dispose(); + return result; + } + + /** + * @remarks + * + * @example + * + * The `waitForFunction` can be used to observe viewport size change: + * ```js + * const puppeteer = require('puppeteer'); + * + * (async () => { + * . const browser = await puppeteer.launch(); + * . const page = await browser.newPage(); + * . const watchDog = page.mainFrame().waitForFunction('window.innerWidth < 100'); + * . page.setViewport({width: 50, height: 50}); + * . await watchDog; + * . await browser.close(); + * })(); + * ``` + * + * To pass arguments from Node.js to the predicate of `page.waitForFunction` function: + * + * ```js + * const selector = '.foo'; + * await frame.waitForFunction( + * selector => !!document.querySelector(selector), + * {}, // empty options object + * selector + *); + * ``` + * + * @param pageFunction - the function to evaluate in the frame context. + * @param options - options to configure the polling method and timeout. + * @param args - arguments to pass to the `pageFunction`. + * @returns the promise which resolve when the `pageFunction` returns a truthy value. + */ + waitForFunction( + pageFunction: Function | string, + options: FrameWaitForFunctionOptions = {}, + ...args: SerializableOrJSHandle[] + ): Promise<JSHandle> { + return this._mainWorld.waitForFunction(pageFunction, options, ...args); + } + + /** + * @returns the frame's title. + */ + async title(): Promise<string> { + return this._secondaryWorld.title(); + } + + /** + * @internal + */ + _navigated(framePayload: Protocol.Page.Frame): void { + this._name = framePayload.name; + this._url = `${framePayload.url}${framePayload.urlFragment || ''}`; + } + + /** + * @internal + */ + _navigatedWithinDocument(url: string): void { + this._url = url; + } + + /** + * @internal + */ + _onLifecycleEvent(loaderId: string, name: string): void { + if (name === 'init') { + this._loaderId = loaderId; + this._lifecycleEvents.clear(); + } + this._lifecycleEvents.add(name); + } + + /** + * @internal + */ + _onLoadingStopped(): void { + this._lifecycleEvents.add('DOMContentLoaded'); + this._lifecycleEvents.add('load'); + } + + /** + * @internal + */ + _detach(): void { + this._detached = true; + this._mainWorld._detach(); + this._secondaryWorld._detach(); + if (this._parentFrame) this._parentFrame._childFrames.delete(this); + this._parentFrame = null; + } +} + +function assertNoLegacyNavigationOptions(options: { + [optionName: string]: unknown; +}): void { + assert( + options['networkIdleTimeout'] === undefined, + 'ERROR: networkIdleTimeout option is no longer supported.' + ); + assert( + options['networkIdleInflight'] === undefined, + 'ERROR: networkIdleInflight option is no longer supported.' + ); + assert( + options.waitUntil !== 'networkidle', + 'ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead' + ); +} diff --git a/remote/test/puppeteer/src/common/HTTPRequest.ts b/remote/test/puppeteer/src/common/HTTPRequest.ts new file mode 100644 index 0000000000..f068317616 --- /dev/null +++ b/remote/test/puppeteer/src/common/HTTPRequest.ts @@ -0,0 +1,537 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { CDPSession } from './Connection.js'; +import { Frame } from './FrameManager.js'; +import { HTTPResponse } from './HTTPResponse.js'; +import { assert } from './assert.js'; +import { helper, debugError } from './helper.js'; +import { Protocol } from 'devtools-protocol'; + +/** + * @public + */ +export interface ContinueRequestOverrides { + /** + * If set, the request URL will change. This is not a redirect. + */ + url?: string; + method?: string; + postData?: string; + headers?: Record<string, string>; +} + +/** + * Required response data to fulfill a request with. + * + * @public + */ +export interface ResponseForRequest { + status: number; + headers: Record<string, string>; + contentType: string; + body: string | Buffer; +} + +/** + * + * Represents an HTTP request sent by a page. + * @remarks + * + * Whenever the page sends a request, such as for a network resource, the + * following events are emitted by Puppeteer's `page`: + * + * - `request`: emitted when the request is issued by the page. + * - `requestfinished` - emitted when the response body is downloaded and the + * request is complete. + * + * If request fails at some point, then instead of `requestfinished` event the + * `requestfailed` event is emitted. + * + * All of these events provide an instance of `HTTPRequest` representing the + * request that occurred: + * + * ``` + * page.on('request', request => ...) + * ``` + * + * NOTE: HTTP Error responses, such as 404 or 503, are still successful + * responses from HTTP standpoint, so request will complete with + * `requestfinished` event. + * + * If request gets a 'redirect' response, the request is successfully finished + * with the `requestfinished` event, and a new request is issued to a + * redirected url. + * + * @public + */ +export class HTTPRequest { + /** + * @internal + */ + _requestId: string; + /** + * @internal + */ + _interceptionId: string; + /** + * @internal + */ + _failureText = null; + /** + * @internal + */ + _response: HTTPResponse | null = null; + /** + * @internal + */ + _fromMemoryCache = false; + /** + * @internal + */ + _redirectChain: HTTPRequest[]; + + private _client: CDPSession; + private _isNavigationRequest: boolean; + private _allowInterception: boolean; + private _interceptionHandled = false; + private _url: string; + private _resourceType: string; + + private _method: string; + private _postData?: string; + private _headers: Record<string, string> = {}; + private _frame: Frame; + + /** + * @internal + */ + constructor( + client: CDPSession, + frame: Frame, + interceptionId: string, + allowInterception: boolean, + event: Protocol.Network.RequestWillBeSentEvent, + redirectChain: HTTPRequest[] + ) { + this._client = client; + this._requestId = event.requestId; + this._isNavigationRequest = + event.requestId === event.loaderId && event.type === 'Document'; + this._interceptionId = interceptionId; + this._allowInterception = allowInterception; + this._url = event.request.url; + this._resourceType = event.type.toLowerCase(); + this._method = event.request.method; + this._postData = event.request.postData; + this._frame = frame; + this._redirectChain = redirectChain; + + for (const key of Object.keys(event.request.headers)) + this._headers[key.toLowerCase()] = event.request.headers[key]; + } + + /** + * @returns the URL of the request + */ + url(): string { + return this._url; + } + + /** + * Contains the request's resource type as it was perceived by the rendering + * engine. + * @remarks + * @returns one of the following: `document`, `stylesheet`, `image`, `media`, + * `font`, `script`, `texttrack`, `xhr`, `fetch`, `eventsource`, `websocket`, + * `manifest`, `other`. + */ + resourceType(): string { + // TODO (@jackfranklin): protocol.d.ts has a type for this, but all the + // string values are uppercase. The Puppeteer docs explicitly say the + // potential values are all lower case, and the constructor takes the event + // type and calls toLowerCase() on it, so we can't reuse the type from the + // protocol.d.ts. Why do we lower case? + return this._resourceType; + } + + /** + * @returns the method used (`GET`, `POST`, etc.) + */ + method(): string { + return this._method; + } + + /** + * @returns the request's post body, if any. + */ + postData(): string | undefined { + return this._postData; + } + + /** + * @returns an object with HTTP headers associated with the request. All + * header names are lower-case. + */ + headers(): Record<string, string> { + return this._headers; + } + + /** + * @returns the response for this request, if a response has been received. + */ + response(): HTTPResponse | null { + return this._response; + } + + /** + * @returns the frame that initiated the request. + */ + frame(): Frame | null { + return this._frame; + } + + /** + * @returns true if the request is the driver of the current frame's navigation. + */ + isNavigationRequest(): boolean { + return this._isNavigationRequest; + } + + /** + * @remarks + * + * `redirectChain` is shared between all the requests of the same chain. + * + * For example, if the website `http://example.com` has a single redirect to + * `https://example.com`, then the chain will contain one request: + * + * ```js + * const response = await page.goto('http://example.com'); + * const chain = response.request().redirectChain(); + * console.log(chain.length); // 1 + * console.log(chain[0].url()); // 'http://example.com' + * ``` + * + * If the website `https://google.com` has no redirects, then the chain will be empty: + * + * ```js + * const response = await page.goto('https://google.com'); + * const chain = response.request().redirectChain(); + * console.log(chain.length); // 0 + * ``` + * + * @returns the chain of requests - if a server responds with at least a + * single redirect, this chain will contain all requests that were redirected. + */ + redirectChain(): HTTPRequest[] { + return this._redirectChain.slice(); + } + + /** + * Access information about the request's failure. + * + * @remarks + * + * @example + * + * Example of logging all failed requests: + * + * ```js + * page.on('requestfailed', request => { + * console.log(request.url() + ' ' + request.failure().errorText); + * }); + * ``` + * + * @returns `null` unless the request failed. If the request fails this can + * return an object with `errorText` containing a human-readable error + * message, e.g. `net::ERR_FAILED`. It is not guaranteeded that there will be + * failure text if the request fails. + */ + failure(): { errorText: string } | null { + if (!this._failureText) return null; + return { + errorText: this._failureText, + }; + } + + /** + * Continues request with optional request overrides. + * + * @remarks + * + * To use this, request + * interception should be enabled with {@link Page.setRequestInterception}. + * + * Exception is immediately thrown if the request interception is not enabled. + * + * @example + * ```js + * await page.setRequestInterception(true); + * page.on('request', request => { + * // Override headers + * const headers = Object.assign({}, request.headers(), { + * foo: 'bar', // set "foo" header + * origin: undefined, // remove "origin" header + * }); + * request.continue({headers}); + * }); + * ``` + * + * @param overrides - optional overrides to apply to the request. + */ + async continue(overrides: ContinueRequestOverrides = {}): Promise<void> { + // Request interception is not supported for data: urls. + if (this._url.startsWith('data:')) return; + assert(this._allowInterception, 'Request Interception is not enabled!'); + assert(!this._interceptionHandled, 'Request is already handled!'); + const { url, method, postData, headers } = overrides; + this._interceptionHandled = true; + + const postDataBinaryBase64 = postData + ? Buffer.from(postData).toString('base64') + : undefined; + + await this._client + .send('Fetch.continueRequest', { + requestId: this._interceptionId, + url, + method, + postData: postDataBinaryBase64, + headers: headers ? headersArray(headers) : undefined, + }) + .catch((error) => { + // In certain cases, protocol will return error if the request was + // already canceled or the page was closed. We should tolerate these + // errors. + debugError(error); + }); + } + + /** + * Fulfills a request with the given response. + * + * @remarks + * + * To use this, request + * interception should be enabled with {@link Page.setRequestInterception}. + * + * Exception is immediately thrown if the request interception is not enabled. + * + * @example + * An example of fulfilling all requests with 404 responses: + * ```js + * await page.setRequestInterception(true); + * page.on('request', request => { + * request.respond({ + * status: 404, + * contentType: 'text/plain', + * body: 'Not Found!' + * }); + * }); + * ``` + * + * NOTE: Mocking responses for dataURL requests is not supported. + * Calling `request.respond` for a dataURL request is a noop. + * + * @param response - the response to fulfill the request with. + */ + async respond(response: ResponseForRequest): Promise<void> { + // Mocking responses for dataURL requests is not currently supported. + if (this._url.startsWith('data:')) return; + assert(this._allowInterception, 'Request Interception is not enabled!'); + assert(!this._interceptionHandled, 'Request is already handled!'); + this._interceptionHandled = true; + + const responseBody: Buffer | null = + response.body && helper.isString(response.body) + ? Buffer.from(response.body) + : (response.body as Buffer) || null; + + const responseHeaders: Record<string, string> = {}; + if (response.headers) { + for (const header of Object.keys(response.headers)) + responseHeaders[header.toLowerCase()] = response.headers[header]; + } + if (response.contentType) + responseHeaders['content-type'] = response.contentType; + if (responseBody && !('content-length' in responseHeaders)) + responseHeaders['content-length'] = String( + Buffer.byteLength(responseBody) + ); + + await this._client + .send('Fetch.fulfillRequest', { + requestId: this._interceptionId, + responseCode: response.status || 200, + responsePhrase: STATUS_TEXTS[response.status || 200], + responseHeaders: headersArray(responseHeaders), + body: responseBody ? responseBody.toString('base64') : undefined, + }) + .catch((error) => { + // In certain cases, protocol will return error if the request was + // already canceled or the page was closed. We should tolerate these + // errors. + debugError(error); + }); + } + + /** + * Aborts a request. + * + * @remarks + * To use this, request interception should be enabled with + * {@link Page.setRequestInterception}. If it is not enabled, this method will + * throw an exception immediately. + * + * @param errorCode - optional error code to provide. + */ + async abort(errorCode: ErrorCode = 'failed'): Promise<void> { + // Request interception is not supported for data: urls. + if (this._url.startsWith('data:')) return; + const errorReason = errorReasons[errorCode]; + assert(errorReason, 'Unknown error code: ' + errorCode); + assert(this._allowInterception, 'Request Interception is not enabled!'); + assert(!this._interceptionHandled, 'Request is already handled!'); + this._interceptionHandled = true; + await this._client + .send('Fetch.failRequest', { + requestId: this._interceptionId, + errorReason, + }) + .catch((error) => { + // In certain cases, protocol will return error if the request was + // already canceled or the page was closed. We should tolerate these + // errors. + debugError(error); + }); + } +} + +/** + * @public + */ +export type ErrorCode = + | 'aborted' + | 'accessdenied' + | 'addressunreachable' + | 'blockedbyclient' + | 'blockedbyresponse' + | 'connectionaborted' + | 'connectionclosed' + | 'connectionfailed' + | 'connectionrefused' + | 'connectionreset' + | 'internetdisconnected' + | 'namenotresolved' + | 'timedout' + | 'failed'; + +const errorReasons: Record<ErrorCode, Protocol.Network.ErrorReason> = { + aborted: 'Aborted', + accessdenied: 'AccessDenied', + addressunreachable: 'AddressUnreachable', + blockedbyclient: 'BlockedByClient', + blockedbyresponse: 'BlockedByResponse', + connectionaborted: 'ConnectionAborted', + connectionclosed: 'ConnectionClosed', + connectionfailed: 'ConnectionFailed', + connectionrefused: 'ConnectionRefused', + connectionreset: 'ConnectionReset', + internetdisconnected: 'InternetDisconnected', + namenotresolved: 'NameNotResolved', + timedout: 'TimedOut', + failed: 'Failed', +} as const; + +function headersArray( + headers: Record<string, string> +): Array<{ name: string; value: string }> { + const result = []; + for (const name in headers) { + if (!Object.is(headers[name], undefined)) + result.push({ name, value: headers[name] + '' }); + } + return result; +} + +// List taken from +// https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml +// with extra 306 and 418 codes. +const STATUS_TEXTS = { + '100': 'Continue', + '101': 'Switching Protocols', + '102': 'Processing', + '103': 'Early Hints', + '200': 'OK', + '201': 'Created', + '202': 'Accepted', + '203': 'Non-Authoritative Information', + '204': 'No Content', + '205': 'Reset Content', + '206': 'Partial Content', + '207': 'Multi-Status', + '208': 'Already Reported', + '226': 'IM Used', + '300': 'Multiple Choices', + '301': 'Moved Permanently', + '302': 'Found', + '303': 'See Other', + '304': 'Not Modified', + '305': 'Use Proxy', + '306': 'Switch Proxy', + '307': 'Temporary Redirect', + '308': 'Permanent Redirect', + '400': 'Bad Request', + '401': 'Unauthorized', + '402': 'Payment Required', + '403': 'Forbidden', + '404': 'Not Found', + '405': 'Method Not Allowed', + '406': 'Not Acceptable', + '407': 'Proxy Authentication Required', + '408': 'Request Timeout', + '409': 'Conflict', + '410': 'Gone', + '411': 'Length Required', + '412': 'Precondition Failed', + '413': 'Payload Too Large', + '414': 'URI Too Long', + '415': 'Unsupported Media Type', + '416': 'Range Not Satisfiable', + '417': 'Expectation Failed', + '418': "I'm a teapot", + '421': 'Misdirected Request', + '422': 'Unprocessable Entity', + '423': 'Locked', + '424': 'Failed Dependency', + '425': 'Too Early', + '426': 'Upgrade Required', + '428': 'Precondition Required', + '429': 'Too Many Requests', + '431': 'Request Header Fields Too Large', + '451': 'Unavailable For Legal Reasons', + '500': 'Internal Server Error', + '501': 'Not Implemented', + '502': 'Bad Gateway', + '503': 'Service Unavailable', + '504': 'Gateway Timeout', + '505': 'HTTP Version Not Supported', + '506': 'Variant Also Negotiates', + '507': 'Insufficient Storage', + '508': 'Loop Detected', + '510': 'Not Extended', + '511': 'Network Authentication Required', +} as const; diff --git a/remote/test/puppeteer/src/common/HTTPResponse.ts b/remote/test/puppeteer/src/common/HTTPResponse.ts new file mode 100644 index 0000000000..3df6c62761 --- /dev/null +++ b/remote/test/puppeteer/src/common/HTTPResponse.ts @@ -0,0 +1,213 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { CDPSession } from './Connection.js'; +import { Frame } from './FrameManager.js'; +import { HTTPRequest } from './HTTPRequest.js'; +import { SecurityDetails } from './SecurityDetails.js'; +import { Protocol } from 'devtools-protocol'; + +/** + * @public + */ +export interface RemoteAddress { + ip: string; + port: number; +} + +/** + * The HTTPResponse class represents responses which are received by the + * {@link Page} class. + * + * @public + */ +export class HTTPResponse { + private _client: CDPSession; + private _request: HTTPRequest; + private _contentPromise: Promise<Buffer> | null = null; + private _bodyLoadedPromise: Promise<Error | void>; + private _bodyLoadedPromiseFulfill: (err: Error | void) => void; + private _remoteAddress: RemoteAddress; + private _status: number; + private _statusText: string; + private _url: string; + private _fromDiskCache: boolean; + private _fromServiceWorker: boolean; + private _headers: Record<string, string> = {}; + private _securityDetails: SecurityDetails | null; + + /** + * @internal + */ + constructor( + client: CDPSession, + request: HTTPRequest, + responsePayload: Protocol.Network.Response + ) { + this._client = client; + this._request = request; + + this._bodyLoadedPromise = new Promise((fulfill) => { + this._bodyLoadedPromiseFulfill = fulfill; + }); + + this._remoteAddress = { + ip: responsePayload.remoteIPAddress, + port: responsePayload.remotePort, + }; + this._status = responsePayload.status; + this._statusText = responsePayload.statusText; + this._url = request.url(); + this._fromDiskCache = !!responsePayload.fromDiskCache; + this._fromServiceWorker = !!responsePayload.fromServiceWorker; + for (const key of Object.keys(responsePayload.headers)) + this._headers[key.toLowerCase()] = responsePayload.headers[key]; + this._securityDetails = responsePayload.securityDetails + ? new SecurityDetails(responsePayload.securityDetails) + : null; + } + + /** + * @internal + */ + _resolveBody(err: Error | null): void { + return this._bodyLoadedPromiseFulfill(err); + } + + /** + * @returns The IP address and port number used to connect to the remote + * server. + */ + remoteAddress(): RemoteAddress { + return this._remoteAddress; + } + + /** + * @returns The URL of the response. + */ + url(): string { + return this._url; + } + + /** + * @returns True if the response was successful (status in the range 200-299). + */ + ok(): boolean { + // TODO: document === 0 case? + return this._status === 0 || (this._status >= 200 && this._status <= 299); + } + + /** + * @returns The status code of the response (e.g., 200 for a success). + */ + status(): number { + return this._status; + } + + /** + * @returns The status text of the response (e.g. usually an "OK" for a + * success). + */ + statusText(): string { + return this._statusText; + } + + /** + * @returns An object with HTTP headers associated with the response. All + * header names are lower-case. + */ + headers(): Record<string, string> { + return this._headers; + } + + /** + * @returns {@link SecurityDetails} if the response was received over the + * secure connection, or `null` otherwise. + */ + securityDetails(): SecurityDetails | null { + return this._securityDetails; + } + + /** + * @returns Promise which resolves to a buffer with response body. + */ + buffer(): Promise<Buffer> { + if (!this._contentPromise) { + this._contentPromise = this._bodyLoadedPromise.then(async (error) => { + if (error) throw error; + const response = await this._client.send('Network.getResponseBody', { + requestId: this._request._requestId, + }); + return Buffer.from( + response.body, + response.base64Encoded ? 'base64' : 'utf8' + ); + }); + } + return this._contentPromise; + } + + /** + * @returns Promise which resolves to a text representation of response body. + */ + async text(): Promise<string> { + const content = await this.buffer(); + return content.toString('utf8'); + } + + /** + * + * @returns Promise which resolves to a JSON representation of response body. + * + * @remarks + * + * This method will throw if the response body is not parsable via + * `JSON.parse`. + */ + async json(): Promise<any> { + const content = await this.text(); + return JSON.parse(content); + } + + /** + * @returns A matching {@link HTTPRequest} object. + */ + request(): HTTPRequest { + return this._request; + } + + /** + * @returns True if the response was served from either the browser's disk + * cache or memory cache. + */ + fromCache(): boolean { + return this._fromDiskCache || this._request._fromMemoryCache; + } + + /** + * @returns True if the response was served by a service worker. + */ + fromServiceWorker(): boolean { + return this._fromServiceWorker; + } + + /** + * @returns A {@link Frame} that initiated this response, or `null` if + * navigating to error pages. + */ + frame(): Frame | null { + return this._request.frame(); + } +} diff --git a/remote/test/puppeteer/src/common/Input.ts b/remote/test/puppeteer/src/common/Input.ts new file mode 100644 index 0000000000..3123706256 --- /dev/null +++ b/remote/test/puppeteer/src/common/Input.ts @@ -0,0 +1,525 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assert } from './assert.js'; +import { CDPSession } from './Connection.js'; +import { keyDefinitions, KeyDefinition, KeyInput } from './USKeyboardLayout.js'; + +type KeyDescription = Required< + Pick<KeyDefinition, 'keyCode' | 'key' | 'text' | 'code' | 'location'> +>; + +/** + * Keyboard provides an api for managing a virtual keyboard. + * The high level api is {@link Keyboard."type"}, + * which takes raw characters and generates proper keydown, keypress/input, + * and keyup events on your page. + * + * @remarks + * For finer control, you can use {@link Keyboard.down}, + * {@link Keyboard.up}, and {@link Keyboard.sendCharacter} + * to manually fire events as if they were generated from a real keyboard. + * + * On MacOS, keyboard shortcuts like `⌘ A` -\> Select All do not work. + * See {@link https://github.com/puppeteer/puppeteer/issues/1313 | #1313}. + * + * @example + * An example of holding down `Shift` in order to select and delete some text: + * ```js + * await page.keyboard.type('Hello World!'); + * await page.keyboard.press('ArrowLeft'); + * + * await page.keyboard.down('Shift'); + * for (let i = 0; i < ' World'.length; i++) + * await page.keyboard.press('ArrowLeft'); + * await page.keyboard.up('Shift'); + * + * await page.keyboard.press('Backspace'); + * // Result text will end up saying 'Hello!' + * ``` + * + * @example + * An example of pressing `A` + * ```js + * await page.keyboard.down('Shift'); + * await page.keyboard.press('KeyA'); + * await page.keyboard.up('Shift'); + * ``` + * + * @public + */ +export class Keyboard { + private _client: CDPSession; + /** @internal */ + _modifiers = 0; + private _pressedKeys = new Set<string>(); + + /** @internal */ + constructor(client: CDPSession) { + this._client = client; + } + + /** + * Dispatches a `keydown` event. + * + * @remarks + * If `key` is a single character and no modifier keys besides `Shift` + * are being held down, a `keypress`/`input` event will also generated. + * The `text` option can be specified to force an input event to be generated. + * If `key` is a modifier key, `Shift`, `Meta`, `Control`, or `Alt`, + * subsequent key presses will be sent with that modifier active. + * To release the modifier key, use {@link Keyboard.up}. + * + * After the key is pressed once, subsequent calls to + * {@link Keyboard.down} will have + * {@link https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat | repeat} + * set to true. To release the key, use {@link Keyboard.up}. + * + * Modifier keys DO influence {@link Keyboard.down}. + * Holding down `Shift` will type the text in upper case. + * + * @param key - Name of key to press, such as `ArrowLeft`. + * See {@link KeyInput} for a list of all key names. + * + * @param options - An object of options. Accepts text which, if specified, + * generates an input event with this text. + */ + async down( + key: KeyInput, + options: { text?: string } = { text: undefined } + ): Promise<void> { + const description = this._keyDescriptionForString(key); + + const autoRepeat = this._pressedKeys.has(description.code); + this._pressedKeys.add(description.code); + this._modifiers |= this._modifierBit(description.key); + + const text = options.text === undefined ? description.text : options.text; + await this._client.send('Input.dispatchKeyEvent', { + type: text ? 'keyDown' : 'rawKeyDown', + modifiers: this._modifiers, + windowsVirtualKeyCode: description.keyCode, + code: description.code, + key: description.key, + text: text, + unmodifiedText: text, + autoRepeat, + location: description.location, + isKeypad: description.location === 3, + }); + } + + private _modifierBit(key: string): number { + if (key === 'Alt') return 1; + if (key === 'Control') return 2; + if (key === 'Meta') return 4; + if (key === 'Shift') return 8; + return 0; + } + + private _keyDescriptionForString(keyString: KeyInput): KeyDescription { + const shift = this._modifiers & 8; + const description = { + key: '', + keyCode: 0, + code: '', + text: '', + location: 0, + }; + + const definition = keyDefinitions[keyString]; + assert(definition, `Unknown key: "${keyString}"`); + + if (definition.key) description.key = definition.key; + if (shift && definition.shiftKey) description.key = definition.shiftKey; + + if (definition.keyCode) description.keyCode = definition.keyCode; + if (shift && definition.shiftKeyCode) + description.keyCode = definition.shiftKeyCode; + + if (definition.code) description.code = definition.code; + + if (definition.location) description.location = definition.location; + + if (description.key.length === 1) description.text = description.key; + + if (definition.text) description.text = definition.text; + if (shift && definition.shiftText) description.text = definition.shiftText; + + // if any modifiers besides shift are pressed, no text should be sent + if (this._modifiers & ~8) description.text = ''; + + return description; + } + + /** + * Dispatches a `keyup` event. + * + * @param key - Name of key to release, such as `ArrowLeft`. + * See {@link KeyInput | KeyInput} + * for a list of all key names. + */ + async up(key: KeyInput): Promise<void> { + const description = this._keyDescriptionForString(key); + + this._modifiers &= ~this._modifierBit(description.key); + this._pressedKeys.delete(description.code); + await this._client.send('Input.dispatchKeyEvent', { + type: 'keyUp', + modifiers: this._modifiers, + key: description.key, + windowsVirtualKeyCode: description.keyCode, + code: description.code, + location: description.location, + }); + } + + /** + * Dispatches a `keypress` and `input` event. + * This does not send a `keydown` or `keyup` event. + * + * @remarks + * Modifier keys DO NOT effect {@link Keyboard.sendCharacter | Keyboard.sendCharacter}. + * Holding down `Shift` will not type the text in upper case. + * + * @example + * ```js + * page.keyboard.sendCharacter('嗨'); + * ``` + * + * @param char - Character to send into the page. + */ + async sendCharacter(char: string): Promise<void> { + await this._client.send('Input.insertText', { text: char }); + } + + private charIsKey(char: string): char is KeyInput { + return !!keyDefinitions[char]; + } + + /** + * Sends a `keydown`, `keypress`/`input`, + * and `keyup` event for each character in the text. + * + * @remarks + * To press a special key, like `Control` or `ArrowDown`, + * use {@link Keyboard.press}. + * + * Modifier keys DO NOT effect `keyboard.type`. + * Holding down `Shift` will not type the text in upper case. + * + * @example + * ```js + * await page.keyboard.type('Hello'); // Types instantly + * await page.keyboard.type('World', {delay: 100}); // Types slower, like a user + * ``` + * + * @param text - A text to type into a focused element. + * @param options - An object of options. Accepts delay which, + * if specified, is the time to wait between `keydown` and `keyup` in milliseconds. + * Defaults to 0. + */ + async type(text: string, options: { delay?: number } = {}): Promise<void> { + const delay = options.delay || null; + for (const char of text) { + if (this.charIsKey(char)) { + await this.press(char, { delay }); + } else { + if (delay) await new Promise((f) => setTimeout(f, delay)); + await this.sendCharacter(char); + } + } + } + + /** + * Shortcut for {@link Keyboard.down} + * and {@link Keyboard.up}. + * + * @remarks + * If `key` is a single character and no modifier keys besides `Shift` + * are being held down, a `keypress`/`input` event will also generated. + * The `text` option can be specified to force an input event to be generated. + * + * Modifier keys DO effect {@link Keyboard.press}. + * Holding down `Shift` will type the text in upper case. + * + * @param key - Name of key to press, such as `ArrowLeft`. + * See {@link KeyInput} for a list of all key names. + * + * @param options - An object of options. Accepts text which, if specified, + * generates an input event with this text. Accepts delay which, + * if specified, is the time to wait between `keydown` and `keyup` in milliseconds. + * Defaults to 0. + */ + async press( + key: KeyInput, + options: { delay?: number; text?: string } = {} + ): Promise<void> { + const { delay = null } = options; + await this.down(key, options); + if (delay) await new Promise((f) => setTimeout(f, options.delay)); + await this.up(key); + } +} + +/** + * @public + */ +export type MouseButton = 'left' | 'right' | 'middle'; + +/** + * @public + */ +export interface MouseOptions { + button?: MouseButton; + clickCount?: number; +} + +/** + * @public + */ +export interface MouseWheelOptions { + deltaX?: number; + deltaY?: number; +} + +/** + * The Mouse class operates in main-frame CSS pixels + * relative to the top-left corner of the viewport. + * @remarks + * Every `page` object has its own Mouse, accessible with [`page.mouse`](#pagemouse). + * + * @example + * ```js + * // Using ‘page.mouse’ to trace a 100x100 square. + * await page.mouse.move(0, 0); + * await page.mouse.down(); + * await page.mouse.move(0, 100); + * await page.mouse.move(100, 100); + * await page.mouse.move(100, 0); + * await page.mouse.move(0, 0); + * await page.mouse.up(); + * ``` + * + * **Note**: The mouse events trigger synthetic `MouseEvent`s. + * This means that it does not fully replicate the functionality of what a normal user + * would be able to do with their mouse. + * + * For example, dragging and selecting text is not possible using `page.mouse`. + * Instead, you can use the {@link https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot/getSelection | `DocumentOrShadowRoot.getSelection()`} functionality implemented in the platform. + * + * @example + * For example, if you want to select all content between nodes: + * ```js + * await page.evaluate((from, to) => { + * const selection = from.getRootNode().getSelection(); + * const range = document.createRange(); + * range.setStartBefore(from); + * range.setEndAfter(to); + * selection.removeAllRanges(); + * selection.addRange(range); + * }, fromJSHandle, toJSHandle); + * ``` + * If you then would want to copy-paste your selection, you can use the clipboard api: + * ```js + * // The clipboard api does not allow you to copy, unless the tab is focused. + * await page.bringToFront(); + * await page.evaluate(() => { + * // Copy the selected content to the clipboard + * document.execCommand('copy'); + * // Obtain the content of the clipboard as a string + * return navigator.clipboard.readText(); + * }); + * ``` + * **Note**: If you want access to the clipboard API, + * you have to give it permission to do so: + * ```js + * await browser.defaultBrowserContext().overridePermissions( + * '<your origin>', ['clipboard-read', 'clipboard-write'] + * ); + * ``` + * @public + */ +export class Mouse { + private _client: CDPSession; + private _keyboard: Keyboard; + private _x = 0; + private _y = 0; + private _button: MouseButton | 'none' = 'none'; + + /** + * @internal + */ + constructor(client: CDPSession, keyboard: Keyboard) { + this._client = client; + this._keyboard = keyboard; + } + + /** + * Dispatches a `mousemove` event. + * @param x - Horizontal position of the mouse. + * @param y - Vertical position of the mouse. + * @param options - Optional object. If specified, the `steps` property + * sends intermediate `mousemove` events when set to `1` (default). + */ + async move( + x: number, + y: number, + options: { steps?: number } = {} + ): Promise<void> { + const { steps = 1 } = options; + const fromX = this._x, + fromY = this._y; + this._x = x; + this._y = y; + for (let i = 1; i <= steps; i++) { + await this._client.send('Input.dispatchMouseEvent', { + type: 'mouseMoved', + button: this._button, + x: fromX + (this._x - fromX) * (i / steps), + y: fromY + (this._y - fromY) * (i / steps), + modifiers: this._keyboard._modifiers, + }); + } + } + + /** + * Shortcut for `mouse.move`, `mouse.down` and `mouse.up`. + * @param x - Horizontal position of the mouse. + * @param y - Vertical position of the mouse. + * @param options - Optional `MouseOptions`. + */ + async click( + x: number, + y: number, + options: MouseOptions & { delay?: number } = {} + ): Promise<void> { + const { delay = null } = options; + if (delay !== null) { + await Promise.all([this.move(x, y), this.down(options)]); + await new Promise((f) => setTimeout(f, delay)); + await this.up(options); + } else { + await Promise.all([ + this.move(x, y), + this.down(options), + this.up(options), + ]); + } + } + + /** + * Dispatches a `mousedown` event. + * @param options - Optional `MouseOptions`. + */ + async down(options: MouseOptions = {}): Promise<void> { + const { button = 'left', clickCount = 1 } = options; + this._button = button; + await this._client.send('Input.dispatchMouseEvent', { + type: 'mousePressed', + button, + x: this._x, + y: this._y, + modifiers: this._keyboard._modifiers, + clickCount, + }); + } + + /** + * Dispatches a `mouseup` event. + * @param options - Optional `MouseOptions`. + */ + async up(options: MouseOptions = {}): Promise<void> { + const { button = 'left', clickCount = 1 } = options; + this._button = 'none'; + await this._client.send('Input.dispatchMouseEvent', { + type: 'mouseReleased', + button, + x: this._x, + y: this._y, + modifiers: this._keyboard._modifiers, + clickCount, + }); + } + + /** + * Dispatches a `mousewheel` event. + * @param options - Optional: `MouseWheelOptions`. + * + * @example + * An example of zooming into an element: + * ```js + * await page.goto('https://mdn.mozillademos.org/en-US/docs/Web/API/Element/wheel_event$samples/Scaling_an_element_via_the_wheel?revision=1587366'); + * + * const elem = await page.$('div'); + * const boundingBox = await elem.boundingBox(); + * await page.mouse.move( + * boundingBox.x + boundingBox.width / 2, + * boundingBox.y + boundingBox.height / 2 + * ); + * + * await page.mouse.wheel({ deltaY: -100 }) + * ``` + */ + async wheel(options: MouseWheelOptions = {}): Promise<void> { + const { deltaX = 0, deltaY = 0 } = options; + await this._client.send('Input.dispatchMouseEvent', { + type: 'mouseWheel', + x: this._x, + y: this._y, + deltaX, + deltaY, + modifiers: this._keyboard._modifiers, + pointerType: 'mouse', + }); + } +} + +/** + * The Touchscreen class exposes touchscreen events. + * @public + */ +export class Touchscreen { + private _client: CDPSession; + private _keyboard: Keyboard; + + /** + * @internal + */ + constructor(client: CDPSession, keyboard: Keyboard) { + this._client = client; + this._keyboard = keyboard; + } + + /** + * Dispatches a `touchstart` and `touchend` event. + * @param x - Horizontal position of the tap. + * @param y - Vertical position of the tap. + */ + async tap(x: number, y: number): Promise<void> { + const touchPoints = [{ x: Math.round(x), y: Math.round(y) }]; + await this._client.send('Input.dispatchTouchEvent', { + type: 'touchStart', + touchPoints, + modifiers: this._keyboard._modifiers, + }); + await this._client.send('Input.dispatchTouchEvent', { + type: 'touchEnd', + touchPoints: [], + modifiers: this._keyboard._modifiers, + }); + } +} diff --git a/remote/test/puppeteer/src/common/JSHandle.ts b/remote/test/puppeteer/src/common/JSHandle.ts new file mode 100644 index 0000000000..c0ee4070b9 --- /dev/null +++ b/remote/test/puppeteer/src/common/JSHandle.ts @@ -0,0 +1,982 @@ +/** + * Copyright 2019 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assert } from './assert.js'; +import { helper, debugError } from './helper.js'; +import { ExecutionContext } from './ExecutionContext.js'; +import { Page } from './Page.js'; +import { CDPSession } from './Connection.js'; +import { KeyInput } from './USKeyboardLayout.js'; +import { FrameManager, Frame } from './FrameManager.js'; +import { getQueryHandlerAndSelector } from './QueryHandler.js'; +import { Protocol } from 'devtools-protocol'; +import { + EvaluateFn, + SerializableOrJSHandle, + EvaluateFnReturnType, + EvaluateHandleFn, + WrapElementHandle, + UnwrapPromiseLike, +} from './EvalTypes.js'; +import { isNode } from '../environment.js'; + +export interface BoxModel { + content: Array<{ x: number; y: number }>; + padding: Array<{ x: number; y: number }>; + border: Array<{ x: number; y: number }>; + margin: Array<{ x: number; y: number }>; + width: number; + height: number; +} + +/** + * @public + */ +export interface BoundingBox { + /** + * the x coordinate of the element in pixels. + */ + x: number; + /** + * the y coordinate of the element in pixels. + */ + y: number; + /** + * the width of the element in pixels. + */ + width: number; + /** + * the height of the element in pixels. + */ + height: number; +} + +/** + * @internal + */ +export function createJSHandle( + context: ExecutionContext, + remoteObject: Protocol.Runtime.RemoteObject +): JSHandle { + const frame = context.frame(); + if (remoteObject.subtype === 'node' && frame) { + const frameManager = frame._frameManager; + return new ElementHandle( + context, + context._client, + remoteObject, + frameManager.page(), + frameManager + ); + } + return new JSHandle(context, context._client, remoteObject); +} + +/** + * Represents an in-page JavaScript object. JSHandles can be created with the + * {@link Page.evaluateHandle | page.evaluateHandle} method. + * + * @example + * ```js + * const windowHandle = await page.evaluateHandle(() => window); + * ``` + * + * JSHandle prevents the referenced JavaScript object from being garbage-collected + * unless the handle is {@link JSHandle.dispose | disposed}. JSHandles are auto- + * disposed when their origin frame gets navigated or the parent context gets destroyed. + * + * JSHandle instances can be used as arguments for {@link Page.$eval}, + * {@link Page.evaluate}, and {@link Page.evaluateHandle}. + * + * @public + */ +export class JSHandle { + /** + * @internal + */ + _context: ExecutionContext; + /** + * @internal + */ + _client: CDPSession; + /** + * @internal + */ + _remoteObject: Protocol.Runtime.RemoteObject; + /** + * @internal + */ + _disposed = false; + + /** + * @internal + */ + constructor( + context: ExecutionContext, + client: CDPSession, + remoteObject: Protocol.Runtime.RemoteObject + ) { + this._context = context; + this._client = client; + this._remoteObject = remoteObject; + } + + /** Returns the execution context the handle belongs to. + */ + executionContext(): ExecutionContext { + return this._context; + } + + /** + * This method passes this handle as the first argument to `pageFunction`. + * If `pageFunction` returns a Promise, then `handle.evaluate` would wait + * for the promise to resolve and return its value. + * + * @example + * ```js + * const tweetHandle = await page.$('.tweet .retweets'); + * expect(await tweetHandle.evaluate(node => node.innerText)).toBe('10'); + * ``` + */ + + async evaluate<T extends EvaluateFn>( + pageFunction: T | string, + ...args: SerializableOrJSHandle[] + ): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> { + return await this.executionContext().evaluate< + UnwrapPromiseLike<EvaluateFnReturnType<T>> + >(pageFunction, this, ...args); + } + + /** + * This method passes this handle as the first argument to `pageFunction`. + * + * @remarks + * + * The only difference between `jsHandle.evaluate` and + * `jsHandle.evaluateHandle` is that `jsHandle.evaluateHandle` + * returns an in-page object (JSHandle). + * + * If the function passed to `jsHandle.evaluateHandle` returns a Promise, + * then `evaluateHandle.evaluateHandle` waits for the promise to resolve and + * returns its value. + * + * See {@link Page.evaluateHandle} for more details. + */ + async evaluateHandle<HandleType extends JSHandle = JSHandle>( + pageFunction: EvaluateHandleFn, + ...args: SerializableOrJSHandle[] + ): Promise<HandleType> { + return await this.executionContext().evaluateHandle( + pageFunction, + this, + ...args + ); + } + + /** Fetches a single property from the referenced object. + */ + async getProperty(propertyName: string): Promise<JSHandle | undefined> { + const objectHandle = await this.evaluateHandle( + (object: HTMLElement, propertyName: string) => { + const result = { __proto__: null }; + result[propertyName] = object[propertyName]; + return result; + }, + propertyName + ); + const properties = await objectHandle.getProperties(); + const result = properties.get(propertyName) || null; + await objectHandle.dispose(); + return result; + } + + /** + * The method returns a map with property names as keys and JSHandle + * instances for the property values. + * + * @example + * ```js + * const listHandle = await page.evaluateHandle(() => document.body.children); + * const properties = await listHandle.getProperties(); + * const children = []; + * for (const property of properties.values()) { + * const element = property.asElement(); + * if (element) + * children.push(element); + * } + * children; // holds elementHandles to all children of document.body + * ``` + */ + async getProperties(): Promise<Map<string, JSHandle>> { + const response = await this._client.send('Runtime.getProperties', { + objectId: this._remoteObject.objectId, + ownProperties: true, + }); + const result = new Map<string, JSHandle>(); + for (const property of response.result) { + if (!property.enumerable) continue; + result.set(property.name, createJSHandle(this._context, property.value)); + } + return result; + } + + /** + * Returns a JSON representation of the object. + * + * @remarks + * + * The JSON is generated by running {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify | JSON.stringify} + * on the object in page and consequent {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse | JSON.parse} in puppeteer. + * **NOTE** The method throws if the referenced object is not stringifiable. + */ + async jsonValue(): Promise<Record<string, unknown>> { + if (this._remoteObject.objectId) { + const response = await this._client.send('Runtime.callFunctionOn', { + functionDeclaration: 'function() { return this; }', + objectId: this._remoteObject.objectId, + returnByValue: true, + awaitPromise: true, + }); + return helper.valueFromRemoteObject(response.result); + } + return helper.valueFromRemoteObject(this._remoteObject); + } + + /** + * Returns either `null` or the object handle itself, if the object handle is + * an instance of {@link ElementHandle}. + */ + asElement(): ElementHandle | null { + // This always returns null, but subclasses can override this and return an + // ElementHandle. + return null; + } + + /** + * Stops referencing the element handle, and resolves when the object handle is + * successfully disposed of. + */ + async dispose(): Promise<void> { + if (this._disposed) return; + this._disposed = true; + await helper.releaseObject(this._client, this._remoteObject); + } + + /** + * Returns a string representation of the JSHandle. + * + * @remarks Useful during debugging. + */ + toString(): string { + if (this._remoteObject.objectId) { + const type = this._remoteObject.subtype || this._remoteObject.type; + return 'JSHandle@' + type; + } + return 'JSHandle:' + helper.valueFromRemoteObject(this._remoteObject); + } +} + +/** + * ElementHandle represents an in-page DOM element. + * + * @remarks + * + * ElementHandles can be created with the {@link Page.$} method. + * + * ```js + * const puppeteer = require('puppeteer'); + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.goto('https://example.com'); + * const hrefElement = await page.$('a'); + * await hrefElement.click(); + * // ... + * })(); + * ``` + * + * ElementHandle prevents the DOM element from being garbage-collected unless the + * handle is {@link JSHandle.dispose | disposed}. ElementHandles are auto-disposed + * when their origin frame gets navigated. + * + * ElementHandle instances can be used as arguments in {@link Page.$eval} and + * {@link Page.evaluate} methods. + * + * If you're using TypeScript, ElementHandle takes a generic argument that + * denotes the type of element the handle is holding within. For example, if you + * have a handle to a `<select>` element, you can type it as + * `ElementHandle<HTMLSelectElement>` and you get some nicer type checks. + * + * @public + */ +export class ElementHandle< + ElementType extends Element = Element +> extends JSHandle { + private _page: Page; + private _frameManager: FrameManager; + + /** + * @internal + */ + constructor( + context: ExecutionContext, + client: CDPSession, + remoteObject: Protocol.Runtime.RemoteObject, + page: Page, + frameManager: FrameManager + ) { + super(context, client, remoteObject); + this._client = client; + this._remoteObject = remoteObject; + this._page = page; + this._frameManager = frameManager; + } + + asElement(): ElementHandle<ElementType> | null { + return this; + } + + /** + * Resolves to the content frame for element handles referencing + * iframe nodes, or null otherwise + */ + async contentFrame(): Promise<Frame | null> { + const nodeInfo = await this._client.send('DOM.describeNode', { + objectId: this._remoteObject.objectId, + }); + if (typeof nodeInfo.node.frameId !== 'string') return null; + return this._frameManager.frame(nodeInfo.node.frameId); + } + + private async _scrollIntoViewIfNeeded(): Promise<void> { + const error = await this.evaluate< + ( + element: Element, + pageJavascriptEnabled: boolean + ) => Promise<string | false> + >(async (element, pageJavascriptEnabled) => { + if (!element.isConnected) return 'Node is detached from document'; + if (element.nodeType !== Node.ELEMENT_NODE) + return 'Node is not of type HTMLElement'; + // force-scroll if page's javascript is disabled. + if (!pageJavascriptEnabled) { + element.scrollIntoView({ + block: 'center', + inline: 'center', + // @ts-expect-error Chrome still supports behavior: instant but + // it's not in the spec so TS shouts We don't want to make this + // breaking change in Puppeteer yet so we'll ignore the line. + behavior: 'instant', + }); + return false; + } + const visibleRatio = await new Promise((resolve) => { + const observer = new IntersectionObserver((entries) => { + resolve(entries[0].intersectionRatio); + observer.disconnect(); + }); + observer.observe(element); + }); + if (visibleRatio !== 1.0) { + element.scrollIntoView({ + block: 'center', + inline: 'center', + // @ts-expect-error Chrome still supports behavior: instant but + // it's not in the spec so TS shouts We don't want to make this + // breaking change in Puppeteer yet so we'll ignore the line. + behavior: 'instant', + }); + } + return false; + }, this._page.isJavaScriptEnabled()); + + if (error) throw new Error(error); + } + + private async _clickablePoint(): Promise<{ x: number; y: number }> { + const [result, layoutMetrics] = await Promise.all([ + this._client + .send('DOM.getContentQuads', { + objectId: this._remoteObject.objectId, + }) + .catch(debugError), + this._client.send('Page.getLayoutMetrics'), + ]); + if (!result || !result.quads.length) + throw new Error('Node is either not visible or not an HTMLElement'); + // Filter out quads that have too small area to click into. + const { clientWidth, clientHeight } = layoutMetrics.layoutViewport; + const quads = result.quads + .map((quad) => this._fromProtocolQuad(quad)) + .map((quad) => + this._intersectQuadWithViewport(quad, clientWidth, clientHeight) + ) + .filter((quad) => computeQuadArea(quad) > 1); + if (!quads.length) + throw new Error('Node is either not visible or not an HTMLElement'); + // Return the middle point of the first quad. + const quad = quads[0]; + let x = 0; + let y = 0; + for (const point of quad) { + x += point.x; + y += point.y; + } + return { + x: x / 4, + y: y / 4, + }; + } + + private _getBoxModel(): Promise<void | Protocol.DOM.GetBoxModelResponse> { + const params: Protocol.DOM.GetBoxModelRequest = { + objectId: this._remoteObject.objectId, + }; + return this._client + .send('DOM.getBoxModel', params) + .catch((error) => debugError(error)); + } + + private _fromProtocolQuad(quad: number[]): Array<{ x: number; y: number }> { + return [ + { x: quad[0], y: quad[1] }, + { x: quad[2], y: quad[3] }, + { x: quad[4], y: quad[5] }, + { x: quad[6], y: quad[7] }, + ]; + } + + private _intersectQuadWithViewport( + quad: Array<{ x: number; y: number }>, + width: number, + height: number + ): Array<{ x: number; y: number }> { + return quad.map((point) => ({ + x: Math.min(Math.max(point.x, 0), width), + y: Math.min(Math.max(point.y, 0), height), + })); + } + + /** + * This method scrolls element into view if needed, and then + * uses {@link Page.mouse} to hover over the center of the element. + * If the element is detached from DOM, the method throws an error. + */ + async hover(): Promise<void> { + await this._scrollIntoViewIfNeeded(); + const { x, y } = await this._clickablePoint(); + await this._page.mouse.move(x, y); + } + + /** + * This method scrolls element into view if needed, and then + * uses {@link Page.mouse} to click in the center of the element. + * If the element is detached from DOM, the method throws an error. + */ + async click(options: ClickOptions = {}): Promise<void> { + await this._scrollIntoViewIfNeeded(); + const { x, y } = await this._clickablePoint(); + await this._page.mouse.click(x, y, options); + } + + /** + * Triggers a `change` and `input` event once all the provided options have been + * selected. If there's no `<select>` element matching `selector`, the method + * throws an error. + * + * @example + * ```js + * handle.select('blue'); // single selection + * handle.select('red', 'green', 'blue'); // multiple selections + * ``` + * @param values - Values of options to select. If the `<select>` has the + * `multiple` attribute, all values are considered, otherwise only the first + * one is taken into account. + */ + async select(...values: string[]): Promise<string[]> { + for (const value of values) + assert( + helper.isString(value), + 'Values must be strings. Found value "' + + value + + '" of type "' + + typeof value + + '"' + ); + + return this.evaluate< + (element: HTMLSelectElement, values: string[]) => string[] + >((element, values) => { + if (element.nodeName.toLowerCase() !== 'select') + throw new Error('Element is not a <select> element.'); + + const options = Array.from(element.options); + element.value = undefined; + for (const option of options) { + option.selected = values.includes(option.value); + if (option.selected && !element.multiple) break; + } + element.dispatchEvent(new Event('input', { bubbles: true })); + element.dispatchEvent(new Event('change', { bubbles: true })); + return options + .filter((option) => option.selected) + .map((option) => option.value); + }, values); + } + + /** + * This method expects `elementHandle` to point to an + * {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input | input element}. + * @param filePaths - Sets the value of the file input to these paths. + * If some of the `filePaths` are relative paths, then they are resolved + * relative to the {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory} + */ + async uploadFile(...filePaths: string[]): Promise<void> { + const isMultiple = await this.evaluate< + (element: HTMLInputElement) => boolean + >((element) => element.multiple); + assert( + filePaths.length <= 1 || isMultiple, + 'Multiple file uploads only work with <input type=file multiple>' + ); + + if (!isNode) { + throw new Error( + `JSHandle#uploadFile can only be used in Node environments.` + ); + } + // This import is only needed for `uploadFile`, so keep it scoped here to avoid paying + // the cost unnecessarily. + const path = await import('path'); + const fs = await helper.importFSModule(); + // Locate all files and confirm that they exist. + const files = await Promise.all( + filePaths.map(async (filePath) => { + const resolvedPath: string = path.resolve(filePath); + try { + await fs.promises.access(resolvedPath, fs.constants.R_OK); + } catch (error) { + if (error.code === 'ENOENT') + throw new Error(`${filePath} does not exist or is not readable`); + } + + return resolvedPath; + }) + ); + const { objectId } = this._remoteObject; + const { node } = await this._client.send('DOM.describeNode', { objectId }); + const { backendNodeId } = node; + + // The zero-length array is a special case, it seems that DOM.setFileInputFiles does + // not actually update the files in that case, so the solution is to eval the element + // value to a new FileList directly. + if (files.length === 0) { + await this.evaluate<(element: HTMLInputElement) => void>((element) => { + element.files = new DataTransfer().files; + + // Dispatch events for this case because it should behave akin to a user action. + element.dispatchEvent(new Event('input', { bubbles: true })); + element.dispatchEvent(new Event('change', { bubbles: true })); + }); + } else { + await this._client.send('DOM.setFileInputFiles', { + objectId, + files, + backendNodeId, + }); + } + } + + /** + * This method scrolls element into view if needed, and then uses + * {@link Touchscreen.tap} to tap in the center of the element. + * If the element is detached from DOM, the method throws an error. + */ + async tap(): Promise<void> { + await this._scrollIntoViewIfNeeded(); + const { x, y } = await this._clickablePoint(); + await this._page.touchscreen.tap(x, y); + } + + /** + * Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element. + */ + async focus(): Promise<void> { + await this.evaluate<(element: HTMLElement) => void>((element) => + element.focus() + ); + } + + /** + * Focuses the element, and then sends a `keydown`, `keypress`/`input`, and + * `keyup` event for each character in the text. + * + * To press a special key, like `Control` or `ArrowDown`, + * use {@link ElementHandle.press}. + * + * @example + * ```js + * await elementHandle.type('Hello'); // Types instantly + * await elementHandle.type('World', {delay: 100}); // Types slower, like a user + * ``` + * + * @example + * An example of typing into a text field and then submitting the form: + * + * ```js + * const elementHandle = await page.$('input'); + * await elementHandle.type('some text'); + * await elementHandle.press('Enter'); + * ``` + */ + async type(text: string, options?: { delay: number }): Promise<void> { + await this.focus(); + await this._page.keyboard.type(text, options); + } + + /** + * Focuses the element, and then uses {@link Keyboard.down} and {@link Keyboard.up}. + * + * @remarks + * If `key` is a single character and no modifier keys besides `Shift` + * are being held down, a `keypress`/`input` event will also be generated. + * The `text` option can be specified to force an input event to be generated. + * + * **NOTE** Modifier keys DO affect `elementHandle.press`. Holding down `Shift` + * will type the text in upper case. + * + * @param key - Name of key to press, such as `ArrowLeft`. + * See {@link KeyInput} for a list of all key names. + */ + async press(key: KeyInput, options?: PressOptions): Promise<void> { + await this.focus(); + await this._page.keyboard.press(key, options); + } + + /** + * This method returns the bounding box of the element (relative to the main frame), + * or `null` if the element is not visible. + */ + async boundingBox(): Promise<BoundingBox | null> { + const result = await this._getBoxModel(); + + if (!result) return null; + + const quad = result.model.border; + const x = Math.min(quad[0], quad[2], quad[4], quad[6]); + const y = Math.min(quad[1], quad[3], quad[5], quad[7]); + const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x; + const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y; + + return { x, y, width, height }; + } + + /** + * This method returns boxes of the element, or `null` if the element is not visible. + * + * @remarks + * + * Boxes are represented as an array of points; + * Each Point is an object `{x, y}`. Box points are sorted clock-wise. + */ + async boxModel(): Promise<BoxModel | null> { + const result = await this._getBoxModel(); + + if (!result) return null; + + const { content, padding, border, margin, width, height } = result.model; + return { + content: this._fromProtocolQuad(content), + padding: this._fromProtocolQuad(padding), + border: this._fromProtocolQuad(border), + margin: this._fromProtocolQuad(margin), + width, + height, + }; + } + + /** + * This method scrolls element into view if needed, and then uses + * {@link Page.screenshot} to take a screenshot of the element. + * If the element is detached from DOM, the method throws an error. + */ + async screenshot(options = {}): Promise<string | Buffer | void> { + let needsViewportReset = false; + + let boundingBox = await this.boundingBox(); + assert(boundingBox, 'Node is either not visible or not an HTMLElement'); + + const viewport = this._page.viewport(); + + if ( + viewport && + (boundingBox.width > viewport.width || + boundingBox.height > viewport.height) + ) { + const newViewport = { + width: Math.max(viewport.width, Math.ceil(boundingBox.width)), + height: Math.max(viewport.height, Math.ceil(boundingBox.height)), + }; + await this._page.setViewport(Object.assign({}, viewport, newViewport)); + + needsViewportReset = true; + } + + await this._scrollIntoViewIfNeeded(); + + boundingBox = await this.boundingBox(); + assert(boundingBox, 'Node is either not visible or not an HTMLElement'); + assert(boundingBox.width !== 0, 'Node has 0 width.'); + assert(boundingBox.height !== 0, 'Node has 0 height.'); + + const { + layoutViewport: { pageX, pageY }, + } = await this._client.send('Page.getLayoutMetrics'); + + const clip = Object.assign({}, boundingBox); + clip.x += pageX; + clip.y += pageY; + + const imageData = await this._page.screenshot( + Object.assign( + {}, + { + clip, + }, + options + ) + ); + + if (needsViewportReset) await this._page.setViewport(viewport); + + return imageData; + } + + /** + * Runs `element.querySelector` within the page. If no element matches the selector, + * the return value resolves to `null`. + */ + async $(selector: string): Promise<ElementHandle | null> { + const { updatedSelector, queryHandler } = getQueryHandlerAndSelector( + selector + ); + return queryHandler.queryOne(this, updatedSelector); + } + + /** + * Runs `element.querySelectorAll` within the page. If no elements match the selector, + * the return value resolves to `[]`. + */ + async $$(selector: string): Promise<ElementHandle[]> { + const { updatedSelector, queryHandler } = getQueryHandlerAndSelector( + selector + ); + return queryHandler.queryAll(this, updatedSelector); + } + + /** + * This method runs `document.querySelector` within the element and passes it as + * the first argument to `pageFunction`. If there's no element matching `selector`, + * the method throws an error. + * + * If `pageFunction` returns a Promise, then `frame.$eval` would wait for the promise + * to resolve and return its value. + * + * @example + * ```js + * const tweetHandle = await page.$('.tweet'); + * expect(await tweetHandle.$eval('.like', node => node.innerText)).toBe('100'); + * expect(await tweetHandle.$eval('.retweets', node => node.innerText)).toBe('10'); + * ``` + */ + async $eval<ReturnType>( + selector: string, + pageFunction: ( + element: Element, + ...args: unknown[] + ) => ReturnType | Promise<ReturnType>, + ...args: SerializableOrJSHandle[] + ): Promise<WrapElementHandle<ReturnType>> { + const elementHandle = await this.$(selector); + if (!elementHandle) + throw new Error( + `Error: failed to find element matching selector "${selector}"` + ); + const result = await elementHandle.evaluate< + ( + element: Element, + ...args: SerializableOrJSHandle[] + ) => ReturnType | Promise<ReturnType> + >(pageFunction, ...args); + await elementHandle.dispose(); + + /** + * This `as` is a little unfortunate but helps TS understand the behavior of + * `elementHandle.evaluate`. If evaluate returns an element it will return an + * ElementHandle instance, rather than the plain object. All the + * WrapElementHandle type does is wrap ReturnType into + * ElementHandle<ReturnType> if it is an ElementHandle, or leave it alone as + * ReturnType if it isn't. + */ + return result as WrapElementHandle<ReturnType>; + } + + /** + * This method runs `document.querySelectorAll` within the element and passes it as + * the first argument to `pageFunction`. If there's no element matching `selector`, + * the method throws an error. + * + * If `pageFunction` returns a Promise, then `frame.$$eval` would wait for the + * promise to resolve and return its value. + * + * @example + * ```html + * <div class="feed"> + * <div class="tweet">Hello!</div> + * <div class="tweet">Hi!</div> + * </div> + * ``` + * + * @example + * ```js + * const feedHandle = await page.$('.feed'); + * expect(await feedHandle.$$eval('.tweet', nodes => nodes.map(n => n.innerText))) + * .toEqual(['Hello!', 'Hi!']); + * ``` + */ + async $$eval<ReturnType>( + selector: string, + pageFunction: ( + elements: Element[], + ...args: unknown[] + ) => ReturnType | Promise<ReturnType>, + ...args: SerializableOrJSHandle[] + ): Promise<WrapElementHandle<ReturnType>> { + const { updatedSelector, queryHandler } = getQueryHandlerAndSelector( + selector + ); + const arrayHandle = await queryHandler.queryAllArray(this, updatedSelector); + const result = await arrayHandle.evaluate< + ( + elements: Element[], + ...args: unknown[] + ) => ReturnType | Promise<ReturnType> + >(pageFunction, ...args); + await arrayHandle.dispose(); + /* This `as` exists for the same reason as the `as` in $eval above. + * See the comment there for a full explanation. + */ + return result as WrapElementHandle<ReturnType>; + } + + /** + * The method evaluates the XPath expression relative to the elementHandle. + * If there are no such elements, the method will resolve to an empty array. + * @param expression - Expression to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate | evaluate} + */ + async $x(expression: string): Promise<ElementHandle[]> { + const arrayHandle = await this.evaluateHandle( + (element: Document, expression: string) => { + const document = element.ownerDocument || element; + const iterator = document.evaluate( + expression, + element, + null, + XPathResult.ORDERED_NODE_ITERATOR_TYPE + ); + const array = []; + let item; + while ((item = iterator.iterateNext())) array.push(item); + return array; + }, + expression + ); + const properties = await arrayHandle.getProperties(); + await arrayHandle.dispose(); + const result = []; + for (const property of properties.values()) { + const elementHandle = property.asElement(); + if (elementHandle) result.push(elementHandle); + } + return result; + } + + /** + * Resolves to true if the element is visible in the current viewport. + */ + async isIntersectingViewport(): Promise<boolean> { + return await this.evaluate<(element: Element) => Promise<boolean>>( + async (element) => { + const visibleRatio = await new Promise((resolve) => { + const observer = new IntersectionObserver((entries) => { + resolve(entries[0].intersectionRatio); + observer.disconnect(); + }); + observer.observe(element); + }); + return visibleRatio > 0; + } + ); + } +} + +/** + * @public + */ +export interface ClickOptions { + /** + * Time to wait between `mousedown` and `mouseup` in milliseconds. + * + * @defaultValue 0 + */ + delay?: number; + /** + * @defaultValue 'left' + */ + button?: 'left' | 'right' | 'middle'; + /** + * @defaultValue 1 + */ + clickCount?: number; +} + +/** + * @public + */ +export interface PressOptions { + /** + * Time to wait between `keydown` and `keyup` in milliseconds. Defaults to 0. + */ + delay?: number; + /** + * If specified, generates an input event with this text. + */ + text?: string; +} + +function computeQuadArea(quad: Array<{ x: number; y: number }>): number { + // Compute sum of all directed areas of adjacent triangles + // https://en.wikipedia.org/wiki/Polygon#Simple_polygons + let area = 0; + for (let i = 0; i < quad.length; ++i) { + const p1 = quad[i]; + const p2 = quad[(i + 1) % quad.length]; + area += (p1.x * p2.y - p2.x * p1.y) / 2; + } + return Math.abs(area); +} diff --git a/remote/test/puppeteer/src/common/LifecycleWatcher.ts b/remote/test/puppeteer/src/common/LifecycleWatcher.ts new file mode 100644 index 0000000000..2b8ab60421 --- /dev/null +++ b/remote/test/puppeteer/src/common/LifecycleWatcher.ts @@ -0,0 +1,244 @@ +/** + * Copyright 2019 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assert } from './assert.js'; +import { helper, PuppeteerEventListener } from './helper.js'; +import { TimeoutError } from './Errors.js'; +import { + FrameManager, + Frame, + FrameManagerEmittedEvents, +} from './FrameManager.js'; +import { HTTPRequest } from './HTTPRequest.js'; +import { HTTPResponse } from './HTTPResponse.js'; +import { NetworkManagerEmittedEvents } from './NetworkManager.js'; +import { CDPSessionEmittedEvents } from './Connection.js'; + +export type PuppeteerLifeCycleEvent = + | 'load' + | 'domcontentloaded' + | 'networkidle0' + | 'networkidle2'; +type ProtocolLifeCycleEvent = + | 'load' + | 'DOMContentLoaded' + | 'networkIdle' + | 'networkAlmostIdle'; + +const puppeteerToProtocolLifecycle = new Map< + PuppeteerLifeCycleEvent, + ProtocolLifeCycleEvent +>([ + ['load', 'load'], + ['domcontentloaded', 'DOMContentLoaded'], + ['networkidle0', 'networkIdle'], + ['networkidle2', 'networkAlmostIdle'], +]); + +/** + * @internal + */ +export class LifecycleWatcher { + _expectedLifecycle: ProtocolLifeCycleEvent[]; + _frameManager: FrameManager; + _frame: Frame; + _timeout: number; + _navigationRequest?: HTTPRequest; + _eventListeners: PuppeteerEventListener[]; + _initialLoaderId: string; + + _sameDocumentNavigationPromise: Promise<Error | null>; + _sameDocumentNavigationCompleteCallback: (x?: Error) => void; + + _lifecyclePromise: Promise<void>; + _lifecycleCallback: () => void; + + _newDocumentNavigationPromise: Promise<Error | null>; + _newDocumentNavigationCompleteCallback: (x?: Error) => void; + + _terminationPromise: Promise<Error | null>; + _terminationCallback: (x?: Error) => void; + + _timeoutPromise: Promise<TimeoutError | null>; + + _maximumTimer?: NodeJS.Timeout; + _hasSameDocumentNavigation?: boolean; + + constructor( + frameManager: FrameManager, + frame: Frame, + waitUntil: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[], + timeout: number + ) { + if (Array.isArray(waitUntil)) waitUntil = waitUntil.slice(); + else if (typeof waitUntil === 'string') waitUntil = [waitUntil]; + this._expectedLifecycle = waitUntil.map((value) => { + const protocolEvent = puppeteerToProtocolLifecycle.get(value); + assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value); + return protocolEvent; + }); + + this._frameManager = frameManager; + this._frame = frame; + this._initialLoaderId = frame._loaderId; + this._timeout = timeout; + this._navigationRequest = null; + this._eventListeners = [ + helper.addEventListener( + frameManager._client, + CDPSessionEmittedEvents.Disconnected, + () => + this._terminate( + new Error('Navigation failed because browser has disconnected!') + ) + ), + helper.addEventListener( + this._frameManager, + FrameManagerEmittedEvents.LifecycleEvent, + this._checkLifecycleComplete.bind(this) + ), + helper.addEventListener( + this._frameManager, + FrameManagerEmittedEvents.FrameNavigatedWithinDocument, + this._navigatedWithinDocument.bind(this) + ), + helper.addEventListener( + this._frameManager, + FrameManagerEmittedEvents.FrameDetached, + this._onFrameDetached.bind(this) + ), + helper.addEventListener( + this._frameManager.networkManager(), + NetworkManagerEmittedEvents.Request, + this._onRequest.bind(this) + ), + ]; + + this._sameDocumentNavigationPromise = new Promise<Error | null>( + (fulfill) => { + this._sameDocumentNavigationCompleteCallback = fulfill; + } + ); + + this._lifecyclePromise = new Promise((fulfill) => { + this._lifecycleCallback = fulfill; + }); + + this._newDocumentNavigationPromise = new Promise((fulfill) => { + this._newDocumentNavigationCompleteCallback = fulfill; + }); + + this._timeoutPromise = this._createTimeoutPromise(); + this._terminationPromise = new Promise((fulfill) => { + this._terminationCallback = fulfill; + }); + this._checkLifecycleComplete(); + } + + _onRequest(request: HTTPRequest): void { + if (request.frame() !== this._frame || !request.isNavigationRequest()) + return; + this._navigationRequest = request; + } + + _onFrameDetached(frame: Frame): void { + if (this._frame === frame) { + this._terminationCallback.call( + null, + new Error('Navigating frame was detached') + ); + return; + } + this._checkLifecycleComplete(); + } + + navigationResponse(): HTTPResponse | null { + return this._navigationRequest ? this._navigationRequest.response() : null; + } + + _terminate(error: Error): void { + this._terminationCallback.call(null, error); + } + + sameDocumentNavigationPromise(): Promise<Error | null> { + return this._sameDocumentNavigationPromise; + } + + newDocumentNavigationPromise(): Promise<Error | null> { + return this._newDocumentNavigationPromise; + } + + lifecyclePromise(): Promise<void> { + return this._lifecyclePromise; + } + + timeoutOrTerminationPromise(): Promise<Error | TimeoutError | null> { + return Promise.race([this._timeoutPromise, this._terminationPromise]); + } + + _createTimeoutPromise(): Promise<TimeoutError | null> { + if (!this._timeout) return new Promise(() => {}); + const errorMessage = + 'Navigation timeout of ' + this._timeout + ' ms exceeded'; + return new Promise( + (fulfill) => (this._maximumTimer = setTimeout(fulfill, this._timeout)) + ).then(() => new TimeoutError(errorMessage)); + } + + _navigatedWithinDocument(frame: Frame): void { + if (frame !== this._frame) return; + this._hasSameDocumentNavigation = true; + this._checkLifecycleComplete(); + } + + _checkLifecycleComplete(): void { + // We expect navigation to commit. + if (!checkLifecycle(this._frame, this._expectedLifecycle)) return; + this._lifecycleCallback(); + if ( + this._frame._loaderId === this._initialLoaderId && + !this._hasSameDocumentNavigation + ) + return; + if (this._hasSameDocumentNavigation) + this._sameDocumentNavigationCompleteCallback(); + if (this._frame._loaderId !== this._initialLoaderId) + this._newDocumentNavigationCompleteCallback(); + + /** + * @param {!Frame} frame + * @param {!Array<string>} expectedLifecycle + * @returns {boolean} + */ + function checkLifecycle( + frame: Frame, + expectedLifecycle: ProtocolLifeCycleEvent[] + ): boolean { + for (const event of expectedLifecycle) { + if (!frame._lifecycleEvents.has(event)) return false; + } + for (const child of frame.childFrames()) { + if (!checkLifecycle(child, expectedLifecycle)) return false; + } + return true; + } + } + + dispose(): void { + helper.removeEventListeners(this._eventListeners); + clearTimeout(this._maximumTimer); + } +} diff --git a/remote/test/puppeteer/src/common/NetworkManager.ts b/remote/test/puppeteer/src/common/NetworkManager.ts new file mode 100644 index 0000000000..52b0aee0bf --- /dev/null +++ b/remote/test/puppeteer/src/common/NetworkManager.ts @@ -0,0 +1,340 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { EventEmitter } from './EventEmitter.js'; +import { assert } from './assert.js'; +import { helper, debugError } from './helper.js'; +import { Protocol } from 'devtools-protocol'; +import { CDPSession } from './Connection.js'; +import { FrameManager } from './FrameManager.js'; +import { HTTPRequest } from './HTTPRequest.js'; +import { HTTPResponse } from './HTTPResponse.js'; + +/** + * @public + */ +export interface Credentials { + username: string; + password: string; +} + +/** + * We use symbols to prevent any external parties listening to these events. + * They are internal to Puppeteer. + * + * @internal + */ +export const NetworkManagerEmittedEvents = { + Request: Symbol('NetworkManager.Request'), + Response: Symbol('NetworkManager.Response'), + RequestFailed: Symbol('NetworkManager.RequestFailed'), + RequestFinished: Symbol('NetworkManager.RequestFinished'), +} as const; + +/** + * @internal + */ +export class NetworkManager extends EventEmitter { + _client: CDPSession; + _ignoreHTTPSErrors: boolean; + _frameManager: FrameManager; + _requestIdToRequest = new Map<string, HTTPRequest>(); + _requestIdToRequestWillBeSentEvent = new Map< + string, + Protocol.Network.RequestWillBeSentEvent + >(); + _extraHTTPHeaders: Record<string, string> = {}; + _offline = false; + _credentials?: Credentials = null; + _attemptedAuthentications = new Set<string>(); + _userRequestInterceptionEnabled = false; + _protocolRequestInterceptionEnabled = false; + _userCacheDisabled = false; + _requestIdToInterceptionId = new Map<string, string>(); + + constructor( + client: CDPSession, + ignoreHTTPSErrors: boolean, + frameManager: FrameManager + ) { + super(); + this._client = client; + this._ignoreHTTPSErrors = ignoreHTTPSErrors; + this._frameManager = frameManager; + + this._client.on('Fetch.requestPaused', this._onRequestPaused.bind(this)); + this._client.on('Fetch.authRequired', this._onAuthRequired.bind(this)); + this._client.on( + 'Network.requestWillBeSent', + this._onRequestWillBeSent.bind(this) + ); + this._client.on( + 'Network.requestServedFromCache', + this._onRequestServedFromCache.bind(this) + ); + this._client.on( + 'Network.responseReceived', + this._onResponseReceived.bind(this) + ); + this._client.on( + 'Network.loadingFinished', + this._onLoadingFinished.bind(this) + ); + this._client.on('Network.loadingFailed', this._onLoadingFailed.bind(this)); + } + + async initialize(): Promise<void> { + await this._client.send('Network.enable'); + if (this._ignoreHTTPSErrors) + await this._client.send('Security.setIgnoreCertificateErrors', { + ignore: true, + }); + } + + async authenticate(credentials?: Credentials): Promise<void> { + this._credentials = credentials; + await this._updateProtocolRequestInterception(); + } + + async setExtraHTTPHeaders( + extraHTTPHeaders: Record<string, string> + ): Promise<void> { + this._extraHTTPHeaders = {}; + for (const key of Object.keys(extraHTTPHeaders)) { + const value = extraHTTPHeaders[key]; + assert( + helper.isString(value), + `Expected value of header "${key}" to be String, but "${typeof value}" is found.` + ); + this._extraHTTPHeaders[key.toLowerCase()] = value; + } + await this._client.send('Network.setExtraHTTPHeaders', { + headers: this._extraHTTPHeaders, + }); + } + + extraHTTPHeaders(): Record<string, string> { + return Object.assign({}, this._extraHTTPHeaders); + } + + async setOfflineMode(value: boolean): Promise<void> { + if (this._offline === value) return; + this._offline = value; + await this._client.send('Network.emulateNetworkConditions', { + offline: this._offline, + // values of 0 remove any active throttling. crbug.com/456324#c9 + latency: 0, + downloadThroughput: -1, + uploadThroughput: -1, + }); + } + + async setUserAgent(userAgent: string): Promise<void> { + await this._client.send('Network.setUserAgentOverride', { userAgent }); + } + + async setCacheEnabled(enabled: boolean): Promise<void> { + this._userCacheDisabled = !enabled; + await this._updateProtocolCacheDisabled(); + } + + async setRequestInterception(value: boolean): Promise<void> { + this._userRequestInterceptionEnabled = value; + await this._updateProtocolRequestInterception(); + } + + async _updateProtocolRequestInterception(): Promise<void> { + const enabled = this._userRequestInterceptionEnabled || !!this._credentials; + if (enabled === this._protocolRequestInterceptionEnabled) return; + this._protocolRequestInterceptionEnabled = enabled; + if (enabled) { + await Promise.all([ + this._updateProtocolCacheDisabled(), + this._client.send('Fetch.enable', { + handleAuthRequests: true, + patterns: [{ urlPattern: '*' }], + }), + ]); + } else { + await Promise.all([ + this._updateProtocolCacheDisabled(), + this._client.send('Fetch.disable'), + ]); + } + } + + async _updateProtocolCacheDisabled(): Promise<void> { + await this._client.send('Network.setCacheDisabled', { + cacheDisabled: + this._userCacheDisabled || this._protocolRequestInterceptionEnabled, + }); + } + + _onRequestWillBeSent(event: Protocol.Network.RequestWillBeSentEvent): void { + // Request interception doesn't happen for data URLs with Network Service. + if ( + this._protocolRequestInterceptionEnabled && + !event.request.url.startsWith('data:') + ) { + const requestId = event.requestId; + const interceptionId = this._requestIdToInterceptionId.get(requestId); + if (interceptionId) { + this._onRequest(event, interceptionId); + this._requestIdToInterceptionId.delete(requestId); + } else { + this._requestIdToRequestWillBeSentEvent.set(event.requestId, event); + } + return; + } + this._onRequest(event, null); + } + + _onAuthRequired(event: Protocol.Fetch.AuthRequiredEvent): void { + /* TODO(jacktfranklin): This is defined in protocol.d.ts but not + * in an easily referrable way - we should look at exposing it. + */ + type AuthResponse = 'Default' | 'CancelAuth' | 'ProvideCredentials'; + let response: AuthResponse = 'Default'; + if (this._attemptedAuthentications.has(event.requestId)) { + response = 'CancelAuth'; + } else if (this._credentials) { + response = 'ProvideCredentials'; + this._attemptedAuthentications.add(event.requestId); + } + const { username, password } = this._credentials || { + username: undefined, + password: undefined, + }; + this._client + .send('Fetch.continueWithAuth', { + requestId: event.requestId, + authChallengeResponse: { response, username, password }, + }) + .catch(debugError); + } + + _onRequestPaused(event: Protocol.Fetch.RequestPausedEvent): void { + if ( + !this._userRequestInterceptionEnabled && + this._protocolRequestInterceptionEnabled + ) { + this._client + .send('Fetch.continueRequest', { + requestId: event.requestId, + }) + .catch(debugError); + } + + const requestId = event.networkId; + const interceptionId = event.requestId; + if (requestId && this._requestIdToRequestWillBeSentEvent.has(requestId)) { + const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get( + requestId + ); + this._onRequest(requestWillBeSentEvent, interceptionId); + this._requestIdToRequestWillBeSentEvent.delete(requestId); + } else { + this._requestIdToInterceptionId.set(requestId, interceptionId); + } + } + + _onRequest( + event: Protocol.Network.RequestWillBeSentEvent, + interceptionId?: string + ): void { + let redirectChain = []; + if (event.redirectResponse) { + const request = this._requestIdToRequest.get(event.requestId); + // If we connect late to the target, we could have missed the + // requestWillBeSent event. + if (request) { + this._handleRequestRedirect(request, event.redirectResponse); + redirectChain = request._redirectChain; + } + } + const frame = event.frameId + ? this._frameManager.frame(event.frameId) + : null; + const request = new HTTPRequest( + this._client, + frame, + interceptionId, + this._userRequestInterceptionEnabled, + event, + redirectChain + ); + this._requestIdToRequest.set(event.requestId, request); + this.emit(NetworkManagerEmittedEvents.Request, request); + } + + _onRequestServedFromCache( + event: Protocol.Network.RequestServedFromCacheEvent + ): void { + const request = this._requestIdToRequest.get(event.requestId); + if (request) request._fromMemoryCache = true; + } + + _handleRequestRedirect( + request: HTTPRequest, + responsePayload: Protocol.Network.Response + ): void { + const response = new HTTPResponse(this._client, request, responsePayload); + request._response = response; + request._redirectChain.push(request); + response._resolveBody( + new Error('Response body is unavailable for redirect responses') + ); + this._requestIdToRequest.delete(request._requestId); + this._attemptedAuthentications.delete(request._interceptionId); + this.emit(NetworkManagerEmittedEvents.Response, response); + this.emit(NetworkManagerEmittedEvents.RequestFinished, request); + } + + _onResponseReceived(event: Protocol.Network.ResponseReceivedEvent): void { + const request = this._requestIdToRequest.get(event.requestId); + // FileUpload sends a response without a matching request. + if (!request) return; + const response = new HTTPResponse(this._client, request, event.response); + request._response = response; + this.emit(NetworkManagerEmittedEvents.Response, response); + } + + _onLoadingFinished(event: Protocol.Network.LoadingFinishedEvent): void { + const request = this._requestIdToRequest.get(event.requestId); + // For certain requestIds we never receive requestWillBeSent event. + // @see https://crbug.com/750469 + if (!request) return; + + // Under certain conditions we never get the Network.responseReceived + // event from protocol. @see https://crbug.com/883475 + if (request.response()) request.response()._resolveBody(null); + this._requestIdToRequest.delete(request._requestId); + this._attemptedAuthentications.delete(request._interceptionId); + this.emit(NetworkManagerEmittedEvents.RequestFinished, request); + } + + _onLoadingFailed(event: Protocol.Network.LoadingFailedEvent): void { + const request = this._requestIdToRequest.get(event.requestId); + // For certain requestIds we never receive requestWillBeSent event. + // @see https://crbug.com/750469 + if (!request) return; + request._failureText = event.errorText; + const response = request.response(); + if (response) response._resolveBody(null); + this._requestIdToRequest.delete(request._requestId); + this._attemptedAuthentications.delete(request._interceptionId); + this.emit(NetworkManagerEmittedEvents.RequestFailed, request); + } +} diff --git a/remote/test/puppeteer/src/common/PDFOptions.ts b/remote/test/puppeteer/src/common/PDFOptions.ts new file mode 100644 index 0000000000..743085904a --- /dev/null +++ b/remote/test/puppeteer/src/common/PDFOptions.ts @@ -0,0 +1,179 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @public + */ +export interface PDFMargin { + top?: string | number; + bottom?: string | number; + left?: string | number; + right?: string | number; +} + +/** + * All the valid paper format types when printing a PDF. + * + * @remarks + * + * The sizes of each format are as follows: + * - `Letter`: 8.5in x 11in + * + * - `Legal`: 8.5in x 14in + * + * - `Tabloid`: 11in x 17in + * + * - `Ledger`: 17in x 11in + * + * - `A0`: 33.1in x 46.8in + * + * - `A1`: 23.4in x 33.1in + * + * - `A2`: 16.54in x 23.4in + * + * - `A3`: 11.7in x 16.54in + * + * - `A4`: 8.27in x 11.7in + * + * - `A5`: 5.83in x 8.27in + * + * - `A6`: 4.13in x 5.83in + * + * @public + */ +export type PaperFormat = + | 'letter' + | 'legal' + | 'tabloid' + | 'ledger' + | 'a0' + | 'a1' + | 'a2' + | 'a3' + | 'a4' + | 'a5' + | 'a6'; + +/** + * Valid options to configure PDF generation via {@link Page.pdf}. + * @public + */ +export interface PDFOptions { + /** + * Scales the rendering of the web page. Amount must be between `0.1` and `2`. + * @defaultValue 1 + */ + scale?: number; + /** + * Whether to show the header and footer. + * @defaultValue false + */ + displayHeaderFooter?: boolean; + /** + * HTML template for the print header. Should be valid HTML with the following + * classes used to inject values into them: + * - `date` formatted print date + * + * - `title` document title + * + * - `url` document location + * + * - `pageNumber` current page number + * + * - `totalPages` total pages in the document + */ + headerTemplate?: string; + /** + * HTML template for the print footer. Has the same constraints and support + * for special classes as {@link PDFOptions.headerTemplate}. + */ + footerTemplate?: string; + /** + * Set to `true` to print background graphics. + * @defaultValue false + */ + printBackground?: boolean; + /** + * Whether to print in landscape orientation. + * @defaultValue = false + */ + landscape?: boolean; + /** + * Paper ranges to print, e.g. `1-5, 8, 11-13`. + * @defaultValue The empty string, which means all pages are printed. + */ + pageRanges?: string; + /** + * @remarks + * If set, this takes priority over the `width` and `height` options. + * @defaultValue `letter`. + */ + format?: PaperFormat; + /** + * Sets the width of paper. You can pass in a number or a string with a unit. + */ + width?: string | number; + /** + * Sets the height of paper. You can pass in a number or a string with a unit. + */ + height?: string | number; + /** + * Give any CSS `@page` size declared in the page priority over what is + * declared in the `width` or `height` or `format` option. + * @defaultValue `false`, which will scale the content to fit the paper size. + */ + preferCSSPageSize?: boolean; + /** + * Set the PDF margins. + * @defaultValue no margins are set. + */ + margin?: PDFMargin; + /** + * The path to save the file to. + * + * @remarks + * + * If the path is relative, it's resolved relative to the current working directory. + * + * @defaultValue the empty string, which means the PDF will not be written to disk. + */ + path?: string; +} + +/** + * @internal + */ +export interface PaperFormatDimensions { + width: number; + height: number; +} + +/** + * @internal + */ +export const paperFormats: Record<PaperFormat, PaperFormatDimensions> = { + letter: { width: 8.5, height: 11 }, + legal: { width: 8.5, height: 14 }, + tabloid: { width: 11, height: 17 }, + ledger: { width: 17, height: 11 }, + a0: { width: 33.1, height: 46.8 }, + a1: { width: 23.4, height: 33.1 }, + a2: { width: 16.54, height: 23.4 }, + a3: { width: 11.7, height: 16.54 }, + a4: { width: 8.27, height: 11.7 }, + a5: { width: 5.83, height: 8.27 }, + a6: { width: 4.13, height: 5.83 }, +} as const; diff --git a/remote/test/puppeteer/src/common/Page.ts b/remote/test/puppeteer/src/common/Page.ts new file mode 100644 index 0000000000..7194649bef --- /dev/null +++ b/remote/test/puppeteer/src/common/Page.ts @@ -0,0 +1,2013 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EventEmitter } from './EventEmitter.js'; +import { + Connection, + CDPSession, + CDPSessionEmittedEvents, +} from './Connection.js'; +import { Dialog } from './Dialog.js'; +import { EmulationManager } from './EmulationManager.js'; +import { + Frame, + FrameManager, + FrameManagerEmittedEvents, +} from './FrameManager.js'; +import { Keyboard, Mouse, Touchscreen, MouseButton } from './Input.js'; +import { Tracing } from './Tracing.js'; +import { assert } from './assert.js'; +import { helper, debugError } from './helper.js'; +import { Coverage } from './Coverage.js'; +import { WebWorker } from './WebWorker.js'; +import { Browser, BrowserContext } from './Browser.js'; +import { Target } from './Target.js'; +import { createJSHandle, JSHandle, ElementHandle } from './JSHandle.js'; +import { Viewport } from './PuppeteerViewport.js'; +import { Credentials, NetworkManagerEmittedEvents } from './NetworkManager.js'; +import { HTTPRequest } from './HTTPRequest.js'; +import { HTTPResponse } from './HTTPResponse.js'; +import { Accessibility } from './Accessibility.js'; +import { TimeoutSettings } from './TimeoutSettings.js'; +import { FileChooser } from './FileChooser.js'; +import { ConsoleMessage, ConsoleMessageType } from './ConsoleMessage.js'; +import { PuppeteerLifeCycleEvent } from './LifecycleWatcher.js'; +import { Protocol } from 'devtools-protocol'; +import { + SerializableOrJSHandle, + EvaluateHandleFn, + WrapElementHandle, + EvaluateFn, + EvaluateFnReturnType, + UnwrapPromiseLike, +} from './EvalTypes.js'; +import { PDFOptions, paperFormats } from './PDFOptions.js'; +import { isNode } from '../environment.js'; + +/** + * @public + */ +export interface Metrics { + Timestamp?: number; + Documents?: number; + Frames?: number; + JSEventListeners?: number; + Nodes?: number; + LayoutCount?: number; + RecalcStyleCount?: number; + LayoutDuration?: number; + RecalcStyleDuration?: number; + ScriptDuration?: number; + TaskDuration?: number; + JSHeapUsedSize?: number; + JSHeapTotalSize?: number; +} + +/** + * @public + */ +export interface WaitTimeoutOptions { + /** + * Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to + * disable the timeout. + * + * @remarks + * The default value can be changed by using the + * {@link Page.setDefaultTimeout} method. + */ + timeout?: number; +} + +/** + * @public + */ +export interface WaitForOptions { + /** + * Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to + * disable the timeout. + * + * @remarks + * The default value can be changed by using the + * {@link Page.setDefaultTimeout} or {@link Page.setDefaultNavigationTimeout} + * methods. + */ + timeout?: number; + waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; +} + +/** + * @public + */ +export interface GeolocationOptions { + /** + * Latitude between -90 and 90. + */ + longitude: number; + /** + * Longitude between -180 and 180. + */ + latitude: number; + /** + * Optional non-negative accuracy value. + */ + accuracy?: number; +} + +interface MediaFeature { + name: string; + value: string; +} + +interface ScreenshotClip { + x: number; + y: number; + width: number; + height: number; +} + +interface ScreenshotOptions { + type?: 'png' | 'jpeg'; + path?: string; + fullPage?: boolean; + clip?: ScreenshotClip; + quality?: number; + omitBackground?: boolean; + encoding?: string; +} + +/** + * All the events that a page instance may emit. + * + * @public + */ +export const enum PageEmittedEvents { + /** Emitted when the page closes. */ + Close = 'close', + /** + * Emitted when JavaScript within the page calls one of console API methods, + * e.g. `console.log` or `console.dir`. Also emitted if the page throws an + * error or a warning. + * + * @remarks + * + * A `console` event provides a {@link ConsoleMessage} representing the + * console message that was logged. + * + * @example + * An example of handling `console` event: + * ```js + * page.on('console', msg => { + * for (let i = 0; i < msg.args().length; ++i) + * console.log(`${i}: ${msg.args()[i]}`); + * }); + * page.evaluate(() => console.log('hello', 5, {foo: 'bar'})); + * ``` + */ + Console = 'console', + /** + * Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, + * `confirm` or `beforeunload`. Puppeteer can respond to the dialog via + * {@link Dialog.accept} or {@link Dialog.dismiss}. + */ + Dialog = 'dialog', + /** + * Emitted when the JavaScript + * {@link https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded | DOMContentLoaded } event is dispatched. + */ + DOMContentLoaded = 'domcontentloaded', + /** + * Emitted when the page crashes. Will contain an `Error`. + */ + Error = 'error', + /** Emitted when a frame is attached. Will contain a {@link Frame}. */ + FrameAttached = 'frameattached', + /** Emitted when a frame is detached. Will contain a {@link Frame}. */ + FrameDetached = 'framedetached', + /** Emitted when a frame is navigated to a new URL. Will contain a {@link Frame}. */ + FrameNavigated = 'framenavigated', + /** + * Emitted when the JavaScript + * {@link https://developer.mozilla.org/en-US/docs/Web/Events/load | load} + * event is dispatched. + */ + Load = 'load', + /** + * Emitted when the JavaScript code makes a call to `console.timeStamp`. For + * the list of metrics see {@link Page.metrics | page.metrics}. + * + * @remarks + * Contains an object with two properties: + * - `title`: the title passed to `console.timeStamp` + * - `metrics`: objec containing metrics as key/value pairs. The values will + * be `number`s. + */ + Metrics = 'metrics', + /** + * Emitted when an uncaught exception happens within the page. + * Contains an `Error`. + */ + PageError = 'pageerror', + /** + * Emitted when the page opens a new tab or window. + * + * Contains a {@link Page} corresponding to the popup window. + * + * @example + * + * ```js + * const [popup] = await Promise.all([ + * new Promise(resolve => page.once('popup', resolve)), + * page.click('a[target=_blank]'), + * ]); + * ``` + * + * ```js + * const [popup] = await Promise.all([ + * new Promise(resolve => page.once('popup', resolve)), + * page.evaluate(() => window.open('https://example.com')), + * ]); + * ``` + */ + Popup = 'popup', + /** + * Emitted when a page issues a request and contains a {@link HTTPRequest}. + * + * @remarks + * The object is readonly. See {@Page.setRequestInterception} for intercepting + * and mutating requests. + */ + Request = 'request', + /** + * Emitted when a request fails, for example by timing out. + * + * Contains a {@link HTTPRequest}. + * + * @remarks + * + * NOTE: HTTP Error responses, such as 404 or 503, are still successful + * responses from HTTP standpoint, so request will complete with + * `requestfinished` event and not with `requestfailed`. + */ + RequestFailed = 'requestfailed', + /** + * Emitted when a request finishes successfully. Contains a {@link HTTPRequest}. + */ + RequestFinished = 'requestfinished', + /** + * Emitted when a response is received. Contains a {@link HTTPResponse}. + */ + Response = 'response', + /** + * Emitted when a dedicated + * {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API | WebWorker} + * is spawned by the page. + */ + WorkerCreated = 'workercreated', + /** + * Emitted when a dedicated + * {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API | WebWorker} + * is destroyed by the page. + */ + WorkerDestroyed = 'workerdestroyed', +} + +class ScreenshotTaskQueue { + _chain: Promise<Buffer | string | void>; + + constructor() { + this._chain = Promise.resolve<Buffer | string | void>(undefined); + } + + public postTask( + task: () => Promise<Buffer | string> + ): Promise<Buffer | string | void> { + const result = this._chain.then(task); + this._chain = result.catch(() => {}); + return result; + } +} + +/** + * Page provides methods to interact with a single tab or + * {@link https://developer.chrome.com/extensions/background_pages | extension background page} in Chromium. + * + * @remarks + * + * One Browser instance might have multiple Page instances. + * + * @example + * This example creates a page, navigates it to a URL, and then * saves a screenshot: + * ```js + * const puppeteer = require('puppeteer'); + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.goto('https://example.com'); + * await page.screenshot({path: 'screenshot.png'}); + * await browser.close(); + * })(); + * ``` + * + * The Page class extends from Puppeteer's {@link EventEmitter} class and will + * emit various events which are documented in the {@link PageEmittedEvents} enum. + * + * @example + * This example logs a message for a single page `load` event: + * ```js + * page.once('load', () => console.log('Page loaded!')); + * ``` + * + * To unsubscribe from events use the `off` method: + * + * ```js + * function logRequest(interceptedRequest) { + * console.log('A request was made:', interceptedRequest.url()); + * } + * page.on('request', logRequest); + * // Sometime later... + * page.off('request', logRequest); + * ``` + * @public + */ +export class Page extends EventEmitter { + /** + * @internal + */ + static async create( + client: CDPSession, + target: Target, + ignoreHTTPSErrors: boolean, + defaultViewport: Viewport | null + ): Promise<Page> { + const page = new Page(client, target, ignoreHTTPSErrors); + await page._initialize(); + if (defaultViewport) await page.setViewport(defaultViewport); + return page; + } + + private _closed = false; + private _client: CDPSession; + private _target: Target; + private _keyboard: Keyboard; + private _mouse: Mouse; + private _timeoutSettings = new TimeoutSettings(); + private _touchscreen: Touchscreen; + private _accessibility: Accessibility; + private _frameManager: FrameManager; + private _emulationManager: EmulationManager; + private _tracing: Tracing; + private _pageBindings = new Map<string, Function>(); + private _coverage: Coverage; + private _javascriptEnabled = true; + private _viewport: Viewport | null; + private _screenshotTaskQueue: ScreenshotTaskQueue; + private _workers = new Map<string, WebWorker>(); + // TODO: improve this typedef - it's a function that takes a file chooser or + // something? + private _fileChooserInterceptors = new Set<Function>(); + + private _disconnectPromise?: Promise<Error>; + + /** + * @internal + */ + constructor(client: CDPSession, target: Target, ignoreHTTPSErrors: boolean) { + super(); + this._client = client; + this._target = target; + this._keyboard = new Keyboard(client); + this._mouse = new Mouse(client, this._keyboard); + this._touchscreen = new Touchscreen(client, this._keyboard); + this._accessibility = new Accessibility(client); + this._frameManager = new FrameManager( + client, + this, + ignoreHTTPSErrors, + this._timeoutSettings + ); + this._emulationManager = new EmulationManager(client); + this._tracing = new Tracing(client); + this._coverage = new Coverage(client); + this._screenshotTaskQueue = new ScreenshotTaskQueue(); + this._viewport = null; + + client.on('Target.attachedToTarget', (event) => { + if (event.targetInfo.type !== 'worker') { + // If we don't detach from service workers, they will never die. + client + .send('Target.detachFromTarget', { + sessionId: event.sessionId, + }) + .catch(debugError); + return; + } + const session = Connection.fromSession(client).session(event.sessionId); + const worker = new WebWorker( + session, + event.targetInfo.url, + this._addConsoleMessage.bind(this), + this._handleException.bind(this) + ); + this._workers.set(event.sessionId, worker); + this.emit(PageEmittedEvents.WorkerCreated, worker); + }); + client.on('Target.detachedFromTarget', (event) => { + const worker = this._workers.get(event.sessionId); + if (!worker) return; + this.emit(PageEmittedEvents.WorkerDestroyed, worker); + this._workers.delete(event.sessionId); + }); + + this._frameManager.on(FrameManagerEmittedEvents.FrameAttached, (event) => + this.emit(PageEmittedEvents.FrameAttached, event) + ); + this._frameManager.on(FrameManagerEmittedEvents.FrameDetached, (event) => + this.emit(PageEmittedEvents.FrameDetached, event) + ); + this._frameManager.on(FrameManagerEmittedEvents.FrameNavigated, (event) => + this.emit(PageEmittedEvents.FrameNavigated, event) + ); + + const networkManager = this._frameManager.networkManager(); + networkManager.on(NetworkManagerEmittedEvents.Request, (event) => + this.emit(PageEmittedEvents.Request, event) + ); + networkManager.on(NetworkManagerEmittedEvents.Response, (event) => + this.emit(PageEmittedEvents.Response, event) + ); + networkManager.on(NetworkManagerEmittedEvents.RequestFailed, (event) => + this.emit(PageEmittedEvents.RequestFailed, event) + ); + networkManager.on(NetworkManagerEmittedEvents.RequestFinished, (event) => + this.emit(PageEmittedEvents.RequestFinished, event) + ); + this._fileChooserInterceptors = new Set(); + + client.on('Page.domContentEventFired', () => + this.emit(PageEmittedEvents.DOMContentLoaded) + ); + client.on('Page.loadEventFired', () => this.emit(PageEmittedEvents.Load)); + client.on('Runtime.consoleAPICalled', (event) => this._onConsoleAPI(event)); + client.on('Runtime.bindingCalled', (event) => this._onBindingCalled(event)); + client.on('Page.javascriptDialogOpening', (event) => this._onDialog(event)); + client.on('Runtime.exceptionThrown', (exception) => + this._handleException(exception.exceptionDetails) + ); + client.on('Inspector.targetCrashed', () => this._onTargetCrashed()); + client.on('Performance.metrics', (event) => this._emitMetrics(event)); + client.on('Log.entryAdded', (event) => this._onLogEntryAdded(event)); + client.on('Page.fileChooserOpened', (event) => this._onFileChooser(event)); + this._target._isClosedPromise.then(() => { + this.emit(PageEmittedEvents.Close); + this._closed = true; + }); + } + + private async _initialize(): Promise<void> { + await Promise.all([ + this._frameManager.initialize(), + this._client.send('Target.setAutoAttach', { + autoAttach: true, + waitForDebuggerOnStart: false, + flatten: true, + }), + this._client.send('Performance.enable'), + this._client.send('Log.enable'), + ]); + } + + private async _onFileChooser( + event: Protocol.Page.FileChooserOpenedEvent + ): Promise<void> { + if (!this._fileChooserInterceptors.size) return; + const frame = this._frameManager.frame(event.frameId); + const context = await frame.executionContext(); + const element = await context._adoptBackendNodeId(event.backendNodeId); + const interceptors = Array.from(this._fileChooserInterceptors); + this._fileChooserInterceptors.clear(); + const fileChooser = new FileChooser(element, event); + for (const interceptor of interceptors) interceptor.call(null, fileChooser); + } + + /** + * @returns `true` if the page has JavaScript enabled, `false` otherwise. + */ + public isJavaScriptEnabled(): boolean { + return this._javascriptEnabled; + } + + /** + * @param options - Optional waiting parameters + * @returns Resolves after a page requests a file picker. + */ + async waitForFileChooser( + options: WaitTimeoutOptions = {} + ): Promise<FileChooser> { + if (!this._fileChooserInterceptors.size) + await this._client.send('Page.setInterceptFileChooserDialog', { + enabled: true, + }); + + const { timeout = this._timeoutSettings.timeout() } = options; + let callback; + const promise = new Promise<FileChooser>((x) => (callback = x)); + this._fileChooserInterceptors.add(callback); + return helper + .waitWithTimeout<FileChooser>( + promise, + 'waiting for file chooser', + timeout + ) + .catch((error) => { + this._fileChooserInterceptors.delete(callback); + throw error; + }); + } + + /** + * Sets the page's geolocation. + * + * @remarks + * Consider using {@link BrowserContext.overridePermissions} to grant + * permissions for the page to read its geolocation. + * + * @example + * ```js + * await page.setGeolocation({latitude: 59.95, longitude: 30.31667}); + * ``` + */ + async setGeolocation(options: GeolocationOptions): Promise<void> { + const { longitude, latitude, accuracy = 0 } = options; + if (longitude < -180 || longitude > 180) + throw new Error( + `Invalid longitude "${longitude}": precondition -180 <= LONGITUDE <= 180 failed.` + ); + if (latitude < -90 || latitude > 90) + throw new Error( + `Invalid latitude "${latitude}": precondition -90 <= LATITUDE <= 90 failed.` + ); + if (accuracy < 0) + throw new Error( + `Invalid accuracy "${accuracy}": precondition 0 <= ACCURACY failed.` + ); + await this._client.send('Emulation.setGeolocationOverride', { + longitude, + latitude, + accuracy, + }); + } + + /** + * @returns A target this page was created from. + */ + target(): Target { + return this._target; + } + + /** + * @returns The browser this page belongs to. + */ + browser(): Browser { + return this._target.browser(); + } + + /** + * @returns The browser context that the page belongs to + */ + browserContext(): BrowserContext { + return this._target.browserContext(); + } + + private _onTargetCrashed(): void { + this.emit('error', new Error('Page crashed!')); + } + + private _onLogEntryAdded(event: Protocol.Log.EntryAddedEvent): void { + const { level, text, args, source, url, lineNumber } = event.entry; + if (args) args.map((arg) => helper.releaseObject(this._client, arg)); + if (source !== 'worker') + this.emit( + PageEmittedEvents.Console, + new ConsoleMessage(level, text, [], [{ url, lineNumber }]) + ); + } + + /** + * @returns The page's main frame. + */ + mainFrame(): Frame { + return this._frameManager.mainFrame(); + } + + get keyboard(): Keyboard { + return this._keyboard; + } + + get touchscreen(): Touchscreen { + return this._touchscreen; + } + + get coverage(): Coverage { + return this._coverage; + } + + get tracing(): Tracing { + return this._tracing; + } + + get accessibility(): Accessibility { + return this._accessibility; + } + + /** + * @returns An array of all frames attached to the page. + */ + frames(): Frame[] { + return this._frameManager.frames(); + } + + /** + * @returns all of the dedicated + * {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API | WebWorkers} + * associated with the page. + */ + workers(): WebWorker[] { + return Array.from(this._workers.values()); + } + + /** + * @param value - Whether to enable request interception. + * + * @remarks + * Activating request interception enables {@link HTTPRequest.abort}, + * {@link HTTPRequest.continue} and {@link HTTPRequest.respond} methods. This + * provides the capability to modify network requests that are made by a page. + * + * Once request interception is enabled, every request will stall unless it's + * continued, responded or aborted. + * + * **NOTE** Enabling request interception disables page caching. + * + * @example + * An example of a naïve request interceptor that aborts all image requests: + * ```js + * const puppeteer = require('puppeteer'); + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.setRequestInterception(true); + * page.on('request', interceptedRequest => { + * if (interceptedRequest.url().endsWith('.png') || + * interceptedRequest.url().endsWith('.jpg')) + * interceptedRequest.abort(); + * else + * interceptedRequest.continue(); + * }); + * await page.goto('https://example.com'); + * await browser.close(); + * })(); + * ``` + */ + async setRequestInterception(value: boolean): Promise<void> { + return this._frameManager.networkManager().setRequestInterception(value); + } + + /** + * @param enabled - When `true`, enables offline mode for the page. + */ + setOfflineMode(enabled: boolean): Promise<void> { + return this._frameManager.networkManager().setOfflineMode(enabled); + } + + /** + * @param timeout - Maximum navigation time in milliseconds. + */ + setDefaultNavigationTimeout(timeout: number): void { + this._timeoutSettings.setDefaultNavigationTimeout(timeout); + } + + /** + * @param timeout - Maximum time in milliseconds. + */ + setDefaultTimeout(timeout: number): void { + this._timeoutSettings.setDefaultTimeout(timeout); + } + + /** + * Runs `document.querySelector` within the page. If no element matches the + * selector, the return value resolves to `null`. + * + * @remarks + * Shortcut for {@link Frame.$ | Page.mainFrame().$(selector) }. + * + * @param selector - A + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector} + * to query page for. + */ + async $(selector: string): Promise<ElementHandle | null> { + return this.mainFrame().$(selector); + } + + /** + * @remarks + * + * The only difference between {@link Page.evaluate | page.evaluate} and + * `page.evaluateHandle` is that `evaluateHandle` will return the value + * wrapped in an in-page object. + * + * If the function passed to `page.evaluteHandle` returns a Promise, the + * function will wait for the promise to resolve and return its value. + * + * You can pass a string instead of a function (although functions are + * recommended as they are easier to debug and use with TypeScript): + * + * @example + * ``` + * const aHandle = await page.evaluateHandle('document') + * ``` + * + * @example + * {@link JSHandle} instances can be passed as arguments to the `pageFunction`: + * ``` + * const aHandle = await page.evaluateHandle(() => document.body); + * const resultHandle = await page.evaluateHandle(body => body.innerHTML, aHandle); + * console.log(await resultHandle.jsonValue()); + * await resultHandle.dispose(); + * ``` + * + * Most of the time this function returns a {@link JSHandle}, + * but if `pageFunction` returns a reference to an element, + * you instead get an {@link ElementHandle} back: + * + * @example + * ``` + * const button = await page.evaluateHandle(() => document.querySelector('button')); + * // can call `click` because `button` is an `ElementHandle` + * await button.click(); + * ``` + * + * The TypeScript definitions assume that `evaluateHandle` returns + * a `JSHandle`, but if you know it's going to return an + * `ElementHandle`, pass it as the generic argument: + * + * ``` + * const button = await page.evaluateHandle<ElementHandle>(...); + * ``` + * + * @param pageFunction - a function that is run within the page + * @param args - arguments to be passed to the pageFunction + */ + async evaluateHandle<HandlerType extends JSHandle = JSHandle>( + pageFunction: EvaluateHandleFn, + ...args: SerializableOrJSHandle[] + ): Promise<HandlerType> { + const context = await this.mainFrame().executionContext(); + return context.evaluateHandle<HandlerType>(pageFunction, ...args); + } + + /** + * This method iterates the JavaScript heap and finds all objects with the + * given prototype. + * + * @remarks + * + * @example + * + * ```js + * // Create a Map object + * await page.evaluate(() => window.map = new Map()); + * // Get a handle to the Map object prototype + * const mapPrototype = await page.evaluateHandle(() => Map.prototype); + * // Query all map instances into an array + * const mapInstances = await page.queryObjects(mapPrototype); + * // Count amount of map objects in heap + * const count = await page.evaluate(maps => maps.length, mapInstances); + * await mapInstances.dispose(); + * await mapPrototype.dispose(); + * ``` + * @param prototypeHandle - a handle to the object prototype. + */ + async queryObjects(prototypeHandle: JSHandle): Promise<JSHandle> { + const context = await this.mainFrame().executionContext(); + return context.queryObjects(prototypeHandle); + } + + /** + * This method runs `document.querySelector` within the page and passes the + * result as the first argument to the `pageFunction`. + * + * @remarks + * + * If no element is found matching `selector`, the method will throw an error. + * + * If `pageFunction` returns a promise `$eval` will wait for the promise to + * resolve and then return its value. + * + * @example + * + * ``` + * const searchValue = await page.$eval('#search', el => el.value); + * const preloadHref = await page.$eval('link[rel=preload]', el => el.href); + * const html = await page.$eval('.main-container', el => el.outerHTML); + * ``` + * + * If you are using TypeScript, you may have to provide an explicit type to the + * first argument of the `pageFunction`. + * By default it is typed as `Element`, but you may need to provide a more + * specific sub-type: + * + * @example + * + * ``` + * // if you don't provide HTMLInputElement here, TS will error + * // as `value` is not on `Element` + * const searchValue = await page.$eval('#search', (el: HTMLInputElement) => el.value); + * ``` + * + * The compiler should be able to infer the return type + * from the `pageFunction` you provide. If it is unable to, you can use the generic + * type to tell the compiler what return type you expect from `$eval`: + * + * @example + * + * ``` + * // The compiler can infer the return type in this case, but if it can't + * // or if you want to be more explicit, provide it as the generic type. + * const searchValue = await page.$eval<string>( + * '#search', (el: HTMLInputElement) => el.value + * ); + * ``` + * + * @param selector - the + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector} + * to query for + * @param pageFunction - the function to be evaluated in the page context. + * Will be passed the result of `document.querySelector(selector)` as its + * first argument. + * @param args - any additional arguments to pass through to `pageFunction`. + * + * @returns The result of calling `pageFunction`. If it returns an element it + * is wrapped in an {@link ElementHandle}, else the raw value itself is + * returned. + */ + async $eval<ReturnType>( + selector: string, + pageFunction: ( + element: Element, + /* Unfortunately this has to be unknown[] because it's hard to get + * TypeScript to understand that the arguments will be left alone unless + * they are an ElementHandle, in which case they will be unwrapped. + * The nice thing about unknown vs any is that unknown will force the user + * to type the item before using it to avoid errors. + * + * TODO(@jackfranklin): We could fix this by using overloads like + * DefinitelyTyped does: + * https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/puppeteer/index.d.ts#L114 + */ + ...args: unknown[] + ) => ReturnType | Promise<ReturnType>, + ...args: SerializableOrJSHandle[] + ): Promise<WrapElementHandle<ReturnType>> { + return this.mainFrame().$eval<ReturnType>(selector, pageFunction, ...args); + } + + /** + * This method runs `Array.from(document.querySelectorAll(selector))` within + * the page and passes the result as the first argument to the `pageFunction`. + * + * @remarks + * + * If `pageFunction` returns a promise `$$eval` will wait for the promise to + * resolve and then return its value. + * + * @example + * + * ``` + * // get the amount of divs on the page + * const divCount = await page.$$eval('div', divs => divs.length); + * + * // get the text content of all the `.options` elements: + * const options = await page.$$eval('div > span.options', options => { + * return options.map(option => option.textContent) + * }); + * ``` + * + * If you are using TypeScript, you may have to provide an explicit type to the + * first argument of the `pageFunction`. + * By default it is typed as `Element[]`, but you may need to provide a more + * specific sub-type: + * + * @example + * + * ``` + * // if you don't provide HTMLInputElement here, TS will error + * // as `value` is not on `Element` + * await page.$$eval('input', (elements: HTMLInputElement[]) => { + * return elements.map(e => e.value); + * }); + * ``` + * + * The compiler should be able to infer the return type + * from the `pageFunction` you provide. If it is unable to, you can use the generic + * type to tell the compiler what return type you expect from `$$eval`: + * + * @example + * + * ``` + * // The compiler can infer the return type in this case, but if it can't + * // or if you want to be more explicit, provide it as the generic type. + * const allInputValues = await page.$$eval<string[]>( + * 'input', (elements: HTMLInputElement[]) => elements.map(e => e.textContent) + * ); + * ``` + * + * @param selector the + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector} + * to query for + * @param pageFunction the function to be evaluated in the page context. Will + * be passed the result of `Array.from(document.querySelectorAll(selector))` + * as its first argument. + * @param args any additional arguments to pass through to `pageFunction`. + * + * @returns The result of calling `pageFunction`. If it returns an element it + * is wrapped in an {@link ElementHandle}, else the raw value itself is + * returned. + */ + async $$eval<ReturnType>( + selector: string, + pageFunction: ( + elements: Element[], + /* These have to be typed as unknown[] for the same reason as the $eval + * definition above, please see that comment for more details and the TODO + * that will improve things. + */ + ...args: unknown[] + ) => ReturnType | Promise<ReturnType>, + ...args: SerializableOrJSHandle[] + ): Promise<WrapElementHandle<ReturnType>> { + return this.mainFrame().$$eval<ReturnType>(selector, pageFunction, ...args); + } + + async $$(selector: string): Promise<ElementHandle[]> { + return this.mainFrame().$$(selector); + } + + async $x(expression: string): Promise<ElementHandle[]> { + return this.mainFrame().$x(expression); + } + + /** + * If no URLs are specified, this method returns cookies for the current page + * URL. If URLs are specified, only cookies for those URLs are returned. + */ + async cookies(...urls: string[]): Promise<Protocol.Network.Cookie[]> { + const originalCookies = ( + await this._client.send('Network.getCookies', { + urls: urls.length ? urls : [this.url()], + }) + ).cookies; + + const unsupportedCookieAttributes = ['priority']; + const filterUnsupportedAttributes = ( + cookie: Protocol.Network.Cookie + ): Protocol.Network.Cookie => { + for (const attr of unsupportedCookieAttributes) delete cookie[attr]; + return cookie; + }; + return originalCookies.map(filterUnsupportedAttributes); + } + + async deleteCookie( + ...cookies: Protocol.Network.DeleteCookiesRequest[] + ): Promise<void> { + const pageURL = this.url(); + for (const cookie of cookies) { + const item = Object.assign({}, cookie); + if (!cookie.url && pageURL.startsWith('http')) item.url = pageURL; + await this._client.send('Network.deleteCookies', item); + } + } + + async setCookie(...cookies: Protocol.Network.CookieParam[]): Promise<void> { + const pageURL = this.url(); + const startsWithHTTP = pageURL.startsWith('http'); + const items = cookies.map((cookie) => { + const item = Object.assign({}, cookie); + if (!item.url && startsWithHTTP) item.url = pageURL; + assert( + item.url !== 'about:blank', + `Blank page can not have cookie "${item.name}"` + ); + assert( + !String.prototype.startsWith.call(item.url || '', 'data:'), + `Data URL page can not have cookie "${item.name}"` + ); + return item; + }); + await this.deleteCookie(...items); + if (items.length) + await this._client.send('Network.setCookies', { cookies: items }); + } + + async addScriptTag(options: { + url?: string; + path?: string; + content?: string; + type?: string; + }): Promise<ElementHandle> { + return this.mainFrame().addScriptTag(options); + } + + async addStyleTag(options: { + url?: string; + path?: string; + content?: string; + }): Promise<ElementHandle> { + return this.mainFrame().addStyleTag(options); + } + + async exposeFunction( + name: string, + puppeteerFunction: Function + ): Promise<void> { + if (this._pageBindings.has(name)) + throw new Error( + `Failed to add page binding with name ${name}: window['${name}'] already exists!` + ); + this._pageBindings.set(name, puppeteerFunction); + + const expression = helper.pageBindingInitString('exposedFun', name); + await this._client.send('Runtime.addBinding', { name: name }); + await this._client.send('Page.addScriptToEvaluateOnNewDocument', { + source: expression, + }); + await Promise.all( + this.frames().map((frame) => frame.evaluate(expression).catch(debugError)) + ); + } + + async authenticate(credentials: Credentials): Promise<void> { + return this._frameManager.networkManager().authenticate(credentials); + } + + async setExtraHTTPHeaders(headers: Record<string, string>): Promise<void> { + return this._frameManager.networkManager().setExtraHTTPHeaders(headers); + } + + async setUserAgent(userAgent: string): Promise<void> { + return this._frameManager.networkManager().setUserAgent(userAgent); + } + + async metrics(): Promise<Metrics> { + const response = await this._client.send('Performance.getMetrics'); + return this._buildMetricsObject(response.metrics); + } + + private _emitMetrics(event: Protocol.Performance.MetricsEvent): void { + this.emit(PageEmittedEvents.Metrics, { + title: event.title, + metrics: this._buildMetricsObject(event.metrics), + }); + } + + private _buildMetricsObject( + metrics?: Protocol.Performance.Metric[] + ): Metrics { + const result = {}; + for (const metric of metrics || []) { + if (supportedMetrics.has(metric.name)) result[metric.name] = metric.value; + } + return result; + } + + private _handleException( + exceptionDetails: Protocol.Runtime.ExceptionDetails + ): void { + const message = helper.getExceptionMessage(exceptionDetails); + const err = new Error(message); + err.stack = ''; // Don't report clientside error with a node stack attached + this.emit(PageEmittedEvents.PageError, err); + } + + private async _onConsoleAPI( + event: Protocol.Runtime.ConsoleAPICalledEvent + ): Promise<void> { + if (event.executionContextId === 0) { + // DevTools protocol stores the last 1000 console messages. These + // messages are always reported even for removed execution contexts. In + // this case, they are marked with executionContextId = 0 and are + // reported upon enabling Runtime agent. + // + // Ignore these messages since: + // - there's no execution context we can use to operate with message + // arguments + // - these messages are reported before Puppeteer clients can subscribe + // to the 'console' + // page event. + // + // @see https://github.com/puppeteer/puppeteer/issues/3865 + return; + } + const context = this._frameManager.executionContextById( + event.executionContextId + ); + const values = event.args.map((arg) => createJSHandle(context, arg)); + this._addConsoleMessage(event.type, values, event.stackTrace); + } + + private async _onBindingCalled( + event: Protocol.Runtime.BindingCalledEvent + ): Promise<void> { + let payload: { type: string; name: string; seq: number; args: unknown[] }; + try { + payload = JSON.parse(event.payload); + } catch { + // The binding was either called by something in the page or it was + // called before our wrapper was initialized. + return; + } + const { type, name, seq, args } = payload; + if (type !== 'exposedFun' || !this._pageBindings.has(name)) return; + let expression = null; + try { + const result = await this._pageBindings.get(name)(...args); + expression = helper.pageBindingDeliverResultString(name, seq, result); + } catch (error) { + if (error instanceof Error) + expression = helper.pageBindingDeliverErrorString( + name, + seq, + error.message, + error.stack + ); + else + expression = helper.pageBindingDeliverErrorValueString( + name, + seq, + error + ); + } + this._client + .send('Runtime.evaluate', { + expression, + contextId: event.executionContextId, + }) + .catch(debugError); + } + + private _addConsoleMessage( + type: ConsoleMessageType, + args: JSHandle[], + stackTrace?: Protocol.Runtime.StackTrace + ): void { + if (!this.listenerCount(PageEmittedEvents.Console)) { + args.forEach((arg) => arg.dispose()); + return; + } + const textTokens = []; + for (const arg of args) { + const remoteObject = arg._remoteObject; + if (remoteObject.objectId) textTokens.push(arg.toString()); + else textTokens.push(helper.valueFromRemoteObject(remoteObject)); + } + const stackTraceLocations = []; + if (stackTrace) { + for (const callFrame of stackTrace.callFrames) { + stackTraceLocations.push({ + url: callFrame.url, + lineNumber: callFrame.lineNumber, + columnNumber: callFrame.columnNumber, + }); + } + } + const message = new ConsoleMessage( + type, + textTokens.join(' '), + args, + stackTraceLocations + ); + this.emit(PageEmittedEvents.Console, message); + } + + private _onDialog(event: Protocol.Page.JavascriptDialogOpeningEvent): void { + let dialogType = null; + const validDialogTypes = new Set<Protocol.Page.DialogType>([ + 'alert', + 'confirm', + 'prompt', + 'beforeunload', + ]); + + if (validDialogTypes.has(event.type)) { + dialogType = event.type as Protocol.Page.DialogType; + } + assert(dialogType, 'Unknown javascript dialog type: ' + event.type); + + const dialog = new Dialog( + this._client, + dialogType, + event.message, + event.defaultPrompt + ); + this.emit(PageEmittedEvents.Dialog, dialog); + } + + url(): string { + return this.mainFrame().url(); + } + + async content(): Promise<string> { + return await this._frameManager.mainFrame().content(); + } + + async setContent(html: string, options: WaitForOptions = {}): Promise<void> { + await this._frameManager.mainFrame().setContent(html, options); + } + + async goto( + url: string, + options: WaitForOptions & { referer?: string } = {} + ): Promise<HTTPResponse> { + return await this._frameManager.mainFrame().goto(url, options); + } + + async reload(options?: WaitForOptions): Promise<HTTPResponse | null> { + const result = await Promise.all<HTTPResponse, void>([ + this.waitForNavigation(options), + this._client.send('Page.reload'), + ]); + + return result[0]; + } + + async waitForNavigation( + options: WaitForOptions = {} + ): Promise<HTTPResponse | null> { + return await this._frameManager.mainFrame().waitForNavigation(options); + } + + private _sessionClosePromise(): Promise<Error> { + if (!this._disconnectPromise) + this._disconnectPromise = new Promise((fulfill) => + this._client.once(CDPSessionEmittedEvents.Disconnected, () => + fulfill(new Error('Target closed')) + ) + ); + return this._disconnectPromise; + } + + async waitForRequest( + urlOrPredicate: string | Function, + options: { timeout?: number } = {} + ): Promise<HTTPRequest> { + const { timeout = this._timeoutSettings.timeout() } = options; + return helper.waitForEvent( + this._frameManager.networkManager(), + NetworkManagerEmittedEvents.Request, + (request) => { + if (helper.isString(urlOrPredicate)) + return urlOrPredicate === request.url(); + if (typeof urlOrPredicate === 'function') + return !!urlOrPredicate(request); + return false; + }, + timeout, + this._sessionClosePromise() + ); + } + + async waitForResponse( + urlOrPredicate: string | Function, + options: { timeout?: number } = {} + ): Promise<HTTPResponse> { + const { timeout = this._timeoutSettings.timeout() } = options; + return helper.waitForEvent( + this._frameManager.networkManager(), + NetworkManagerEmittedEvents.Response, + (response) => { + if (helper.isString(urlOrPredicate)) + return urlOrPredicate === response.url(); + if (typeof urlOrPredicate === 'function') + return !!urlOrPredicate(response); + return false; + }, + timeout, + this._sessionClosePromise() + ); + } + + async goBack(options: WaitForOptions = {}): Promise<HTTPResponse | null> { + return this._go(-1, options); + } + + async goForward(options: WaitForOptions = {}): Promise<HTTPResponse | null> { + return this._go(+1, options); + } + + private async _go( + delta: number, + options: WaitForOptions + ): Promise<HTTPResponse | null> { + const history = await this._client.send('Page.getNavigationHistory'); + const entry = history.entries[history.currentIndex + delta]; + if (!entry) return null; + const result = await Promise.all([ + this.waitForNavigation(options), + this._client.send('Page.navigateToHistoryEntry', { entryId: entry.id }), + ]); + return result[0]; + } + + async bringToFront(): Promise<void> { + await this._client.send('Page.bringToFront'); + } + + async emulate(options: { + viewport: Viewport; + userAgent: string; + }): Promise<void> { + await Promise.all([ + this.setViewport(options.viewport), + this.setUserAgent(options.userAgent), + ]); + } + + async setJavaScriptEnabled(enabled: boolean): Promise<void> { + if (this._javascriptEnabled === enabled) return; + this._javascriptEnabled = enabled; + await this._client.send('Emulation.setScriptExecutionDisabled', { + value: !enabled, + }); + } + + async setBypassCSP(enabled: boolean): Promise<void> { + await this._client.send('Page.setBypassCSP', { enabled }); + } + + async emulateMediaType(type?: string): Promise<void> { + assert( + type === 'screen' || type === 'print' || type === null, + 'Unsupported media type: ' + type + ); + await this._client.send('Emulation.setEmulatedMedia', { + media: type || '', + }); + } + + async emulateMediaFeatures(features?: MediaFeature[]): Promise<void> { + if (features === null) + await this._client.send('Emulation.setEmulatedMedia', { features: null }); + if (Array.isArray(features)) { + features.every((mediaFeature) => { + const name = mediaFeature.name; + assert( + /^prefers-(?:color-scheme|reduced-motion)$/.test(name), + 'Unsupported media feature: ' + name + ); + return true; + }); + await this._client.send('Emulation.setEmulatedMedia', { + features: features, + }); + } + } + + async emulateTimezone(timezoneId?: string): Promise<void> { + try { + await this._client.send('Emulation.setTimezoneOverride', { + timezoneId: timezoneId || '', + }); + } catch (error) { + if (error.message.includes('Invalid timezone')) + throw new Error(`Invalid timezone ID: ${timezoneId}`); + throw error; + } + } + + /** + * Emulates the idle state. + * If no arguments set, clears idle state emulation. + * + * @example + * ```js + * // set idle emulation + * await page.emulateIdleState({isUserActive: true, isScreenUnlocked: false}); + * + * // do some checks here + * ... + * + * // clear idle emulation + * await page.emulateIdleState(); + * ``` + * + * @param overrides Mock idle state. If not set, clears idle overrides + * @param isUserActive Mock isUserActive + * @param isScreenUnlocked Mock isScreenUnlocked + */ + async emulateIdleState(overrides?: { + isUserActive: boolean; + isScreenUnlocked: boolean; + }): Promise<void> { + if (overrides) { + await this._client.send('Emulation.setIdleOverride', { + isUserActive: overrides.isUserActive, + isScreenUnlocked: overrides.isScreenUnlocked, + }); + } else { + await this._client.send('Emulation.clearIdleOverride'); + } + } + + /** + * Simulates the given vision deficiency on the page. + * + * @example + * ```js + * const puppeteer = require('puppeteer'); + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.goto('https://v8.dev/blog/10-years'); + * + * await page.emulateVisionDeficiency('achromatopsia'); + * await page.screenshot({ path: 'achromatopsia.png' }); + * + * await page.emulateVisionDeficiency('deuteranopia'); + * await page.screenshot({ path: 'deuteranopia.png' }); + * + * await page.emulateVisionDeficiency('blurredVision'); + * await page.screenshot({ path: 'blurred-vision.png' }); + * + * await browser.close(); + * })(); + * ``` + * + * @param type - the type of deficiency to simulate, or `'none'` to reset. + */ + async emulateVisionDeficiency( + type?: Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type'] + ): Promise<void> { + const visionDeficiencies = new Set< + Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type'] + >([ + 'none', + 'achromatopsia', + 'blurredVision', + 'deuteranopia', + 'protanopia', + 'tritanopia', + ]); + try { + assert( + !type || visionDeficiencies.has(type), + `Unsupported vision deficiency: ${type}` + ); + await this._client.send('Emulation.setEmulatedVisionDeficiency', { + type: type || 'none', + }); + } catch (error) { + throw error; + } + } + + async setViewport(viewport: Viewport): Promise<void> { + const needsReload = await this._emulationManager.emulateViewport(viewport); + this._viewport = viewport; + if (needsReload) await this.reload(); + } + + viewport(): Viewport | null { + return this._viewport; + } + + /** + * @remarks + * + * Evaluates a function in the page's context and returns the result. + * + * If the function passed to `page.evaluteHandle` returns a Promise, the + * function will wait for the promise to resolve and return its value. + * + * @example + * + * ```js + * const result = await frame.evaluate(() => { + * return Promise.resolve(8 * 7); + * }); + * console.log(result); // prints "56" + * ``` + * + * You can pass a string instead of a function (although functions are + * recommended as they are easier to debug and use with TypeScript): + * + * @example + * ``` + * const aHandle = await page.evaluate('1 + 2'); + * ``` + * + * To get the best TypeScript experience, you should pass in as the + * generic the type of `pageFunction`: + * + * ``` + * const aHandle = await page.evaluate<() => number>(() => 2); + * ``` + * + * @example + * + * {@link ElementHandle} instances (including {@link JSHandle}s) can be passed + * as arguments to the `pageFunction`: + * + * ``` + * const bodyHandle = await page.$('body'); + * const html = await page.evaluate(body => body.innerHTML, bodyHandle); + * await bodyHandle.dispose(); + * ``` + * + * @param pageFunction - a function that is run within the page + * @param args - arguments to be passed to the pageFunction + * + * @returns the return value of `pageFunction`. + */ + async evaluate<T extends EvaluateFn>( + pageFunction: T, + ...args: SerializableOrJSHandle[] + ): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> { + return this._frameManager.mainFrame().evaluate<T>(pageFunction, ...args); + } + + async evaluateOnNewDocument( + pageFunction: Function | string, + ...args: unknown[] + ): Promise<void> { + const source = helper.evaluationString(pageFunction, ...args); + await this._client.send('Page.addScriptToEvaluateOnNewDocument', { + source, + }); + } + + async setCacheEnabled(enabled = true): Promise<void> { + await this._frameManager.networkManager().setCacheEnabled(enabled); + } + + async screenshot( + options: ScreenshotOptions = {} + ): Promise<Buffer | string | void> { + let screenshotType = null; + // options.type takes precedence over inferring the type from options.path + // because it may be a 0-length file with no extension created beforehand + // (i.e. as a temp file). + if (options.type) { + assert( + options.type === 'png' || options.type === 'jpeg', + 'Unknown options.type value: ' + options.type + ); + screenshotType = options.type; + } else if (options.path) { + const filePath = options.path; + const extension = filePath + .slice(filePath.lastIndexOf('.') + 1) + .toLowerCase(); + if (extension === 'png') screenshotType = 'png'; + else if (extension === 'jpg' || extension === 'jpeg') + screenshotType = 'jpeg'; + assert( + screenshotType, + `Unsupported screenshot type for extension \`.${extension}\`` + ); + } + + if (!screenshotType) screenshotType = 'png'; + + if (options.quality) { + assert( + screenshotType === 'jpeg', + 'options.quality is unsupported for the ' + + screenshotType + + ' screenshots' + ); + assert( + typeof options.quality === 'number', + 'Expected options.quality to be a number but found ' + + typeof options.quality + ); + assert( + Number.isInteger(options.quality), + 'Expected options.quality to be an integer' + ); + assert( + options.quality >= 0 && options.quality <= 100, + 'Expected options.quality to be between 0 and 100 (inclusive), got ' + + options.quality + ); + } + assert( + !options.clip || !options.fullPage, + 'options.clip and options.fullPage are exclusive' + ); + if (options.clip) { + assert( + typeof options.clip.x === 'number', + 'Expected options.clip.x to be a number but found ' + + typeof options.clip.x + ); + assert( + typeof options.clip.y === 'number', + 'Expected options.clip.y to be a number but found ' + + typeof options.clip.y + ); + assert( + typeof options.clip.width === 'number', + 'Expected options.clip.width to be a number but found ' + + typeof options.clip.width + ); + assert( + typeof options.clip.height === 'number', + 'Expected options.clip.height to be a number but found ' + + typeof options.clip.height + ); + assert( + options.clip.width !== 0, + 'Expected options.clip.width not to be 0.' + ); + assert( + options.clip.height !== 0, + 'Expected options.clip.height not to be 0.' + ); + } + return this._screenshotTaskQueue.postTask(() => + this._screenshotTask(screenshotType, options) + ); + } + + private async _screenshotTask( + format: 'png' | 'jpeg', + options?: ScreenshotOptions + ): Promise<Buffer | string> { + await this._client.send('Target.activateTarget', { + targetId: this._target._targetId, + }); + let clip = options.clip ? processClip(options.clip) : undefined; + + if (options.fullPage) { + const metrics = await this._client.send('Page.getLayoutMetrics'); + const width = Math.ceil(metrics.contentSize.width); + const height = Math.ceil(metrics.contentSize.height); + + // Overwrite clip for full page at all times. + clip = { x: 0, y: 0, width, height, scale: 1 }; + const { isMobile = false, deviceScaleFactor = 1, isLandscape = false } = + this._viewport || {}; + const screenOrientation: Protocol.Emulation.ScreenOrientation = isLandscape + ? { angle: 90, type: 'landscapePrimary' } + : { angle: 0, type: 'portraitPrimary' }; + await this._client.send('Emulation.setDeviceMetricsOverride', { + mobile: isMobile, + width, + height, + deviceScaleFactor, + screenOrientation, + }); + } + const shouldSetDefaultBackground = + options.omitBackground && format === 'png'; + if (shouldSetDefaultBackground) + await this._client.send('Emulation.setDefaultBackgroundColorOverride', { + color: { r: 0, g: 0, b: 0, a: 0 }, + }); + const result = await this._client.send('Page.captureScreenshot', { + format, + quality: options.quality, + clip, + }); + if (shouldSetDefaultBackground) + await this._client.send('Emulation.setDefaultBackgroundColorOverride'); + + if (options.fullPage && this._viewport) + await this.setViewport(this._viewport); + + const buffer = + options.encoding === 'base64' + ? result.data + : Buffer.from(result.data, 'base64'); + if (!isNode && options.path) { + throw new Error( + 'Screenshots can only be written to a file path in a Node environment.' + ); + } + const fs = await helper.importFSModule(); + if (options.path) await fs.promises.writeFile(options.path, buffer); + return buffer; + + function processClip( + clip: ScreenshotClip + ): ScreenshotClip & { scale: number } { + const x = Math.round(clip.x); + const y = Math.round(clip.y); + const width = Math.round(clip.width + clip.x - x); + const height = Math.round(clip.height + clip.y - y); + return { x, y, width, height, scale: 1 }; + } + } + + /** + * Generatees a PDF of the page with the `print` CSS media type. + * @remarks + * + * IMPORTANT: PDF generation is only supported in Chrome headless mode. + * + * To generate a PDF with the `screen` media type, call + * {@link Page.emulateMediaType | `page.emulateMediaType('screen')`} before + * calling `page.pdf()`. + * + * By default, `page.pdf()` generates a pdf with modified colors for printing. + * Use the + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-print-color-adjust | `-webkit-print-color-adjust`} + * property to force rendering of exact colors. + * + * + * @param options - options for generating the PDF. + */ + async pdf(options: PDFOptions = {}): Promise<Buffer> { + const { + scale = 1, + displayHeaderFooter = false, + headerTemplate = '', + footerTemplate = '', + printBackground = false, + landscape = false, + pageRanges = '', + preferCSSPageSize = false, + margin = {}, + path = null, + } = options; + + let paperWidth = 8.5; + let paperHeight = 11; + if (options.format) { + const format = paperFormats[options.format.toLowerCase()]; + assert(format, 'Unknown paper format: ' + options.format); + paperWidth = format.width; + paperHeight = format.height; + } else { + paperWidth = convertPrintParameterToInches(options.width) || paperWidth; + paperHeight = + convertPrintParameterToInches(options.height) || paperHeight; + } + + const marginTop = convertPrintParameterToInches(margin.top) || 0; + const marginLeft = convertPrintParameterToInches(margin.left) || 0; + const marginBottom = convertPrintParameterToInches(margin.bottom) || 0; + const marginRight = convertPrintParameterToInches(margin.right) || 0; + + const result = await this._client.send('Page.printToPDF', { + transferMode: 'ReturnAsStream', + landscape, + displayHeaderFooter, + headerTemplate, + footerTemplate, + printBackground, + scale, + paperWidth, + paperHeight, + marginTop, + marginBottom, + marginLeft, + marginRight, + pageRanges, + preferCSSPageSize, + }); + return await helper.readProtocolStream(this._client, result.stream, path); + } + + async title(): Promise<string> { + return this.mainFrame().title(); + } + + async close( + options: { runBeforeUnload?: boolean } = { runBeforeUnload: undefined } + ): Promise<void> { + assert( + !!this._client._connection, + 'Protocol error: Connection closed. Most likely the page has been closed.' + ); + const runBeforeUnload = !!options.runBeforeUnload; + if (runBeforeUnload) { + await this._client.send('Page.close'); + } else { + await this._client._connection.send('Target.closeTarget', { + targetId: this._target._targetId, + }); + await this._target._isClosedPromise; + } + } + + isClosed(): boolean { + return this._closed; + } + + get mouse(): Mouse { + return this._mouse; + } + + click( + selector: string, + options: { + delay?: number; + button?: MouseButton; + clickCount?: number; + } = {} + ): Promise<void> { + return this.mainFrame().click(selector, options); + } + + focus(selector: string): Promise<void> { + return this.mainFrame().focus(selector); + } + + hover(selector: string): Promise<void> { + return this.mainFrame().hover(selector); + } + + select(selector: string, ...values: string[]): Promise<string[]> { + return this.mainFrame().select(selector, ...values); + } + + tap(selector: string): Promise<void> { + return this.mainFrame().tap(selector); + } + + type( + selector: string, + text: string, + options?: { delay: number } + ): Promise<void> { + return this.mainFrame().type(selector, text, options); + } + + /** + * @remarks + * + * This method behaves differently depending on the first parameter. If it's a + * `string`, it will be treated as a `selector` or `xpath` (if the string + * starts with `//`). This method then is a shortcut for + * {@link Page.waitForSelector} or {@link Page.waitForXPath}. + * + * If the first argument is a function this method is a shortcut for + * {@link Page.waitForFunction}. + * + * If the first argument is a `number`, it's treated as a timeout in + * milliseconds and the method returns a promise which resolves after the + * timeout. + * + * @param selectorOrFunctionOrTimeout - a selector, predicate or timeout to + * wait for. + * @param options - optional waiting parameters. + * @param args - arguments to pass to `pageFunction`. + * + * @deprecated Don't use this method directly. Instead use the more explicit + * methods available: {@link Page.waitForSelector}, + * {@link Page.waitForXPath}, {@link Page.waitForFunction} or + * {@link Page.waitForTimeout}. + */ + waitFor( + selectorOrFunctionOrTimeout: string | number | Function, + options: { + visible?: boolean; + hidden?: boolean; + timeout?: number; + polling?: string | number; + } = {}, + ...args: SerializableOrJSHandle[] + ): Promise<JSHandle> { + return this.mainFrame().waitFor( + selectorOrFunctionOrTimeout, + options, + ...args + ); + } + + /** + * Causes your script to wait for the given number of milliseconds. + * + * @remarks + * + * It's generally recommended to not wait for a number of seconds, but instead + * use {@link Page.waitForSelector}, {@link Page.waitForXPath} or + * {@link Page.waitForFunction} to wait for exactly the conditions you want. + * + * @example + * + * Wait for 1 second: + * + * ``` + * await page.waitForTimeout(1000); + * ``` + * + * @param milliseconds - the number of milliseconds to wait. + */ + waitForTimeout(milliseconds: number): Promise<void> { + return this.mainFrame().waitForTimeout(milliseconds); + } + + waitForSelector( + selector: string, + options: { + visible?: boolean; + hidden?: boolean; + timeout?: number; + } = {} + ): Promise<ElementHandle | null> { + return this.mainFrame().waitForSelector(selector, options); + } + + waitForXPath( + xpath: string, + options: { + visible?: boolean; + hidden?: boolean; + timeout?: number; + } = {} + ): Promise<ElementHandle | null> { + return this.mainFrame().waitForXPath(xpath, options); + } + + waitForFunction( + pageFunction: Function | string, + options: { + timeout?: number; + polling?: string | number; + } = {}, + ...args: SerializableOrJSHandle[] + ): Promise<JSHandle> { + return this.mainFrame().waitForFunction(pageFunction, options, ...args); + } +} + +const supportedMetrics = new Set<string>([ + 'Timestamp', + 'Documents', + 'Frames', + 'JSEventListeners', + 'Nodes', + 'LayoutCount', + 'RecalcStyleCount', + 'LayoutDuration', + 'RecalcStyleDuration', + 'ScriptDuration', + 'TaskDuration', + 'JSHeapUsedSize', + 'JSHeapTotalSize', +]); + +const unitToPixels = { + px: 1, + in: 96, + cm: 37.8, + mm: 3.78, +}; + +function convertPrintParameterToInches( + parameter?: string | number +): number | undefined { + if (typeof parameter === 'undefined') return undefined; + let pixels; + if (helper.isNumber(parameter)) { + // Treat numbers as pixel values to be aligned with phantom's paperSize. + pixels = /** @type {number} */ parameter; + } else if (helper.isString(parameter)) { + const text = /** @type {string} */ parameter; + let unit = text.substring(text.length - 2).toLowerCase(); + let valueText = ''; + if (unitToPixels.hasOwnProperty(unit)) { + valueText = text.substring(0, text.length - 2); + } else { + // In case of unknown unit try to parse the whole parameter as number of pixels. + // This is consistent with phantom's paperSize behavior. + unit = 'px'; + valueText = text; + } + const value = Number(valueText); + assert(!isNaN(value), 'Failed to parse parameter value: ' + text); + pixels = value * unitToPixels[unit]; + } else { + throw new Error( + 'page.pdf() Cannot handle parameter type: ' + typeof parameter + ); + } + return pixels / 96; +} diff --git a/remote/test/puppeteer/src/common/Product.ts b/remote/test/puppeteer/src/common/Product.ts new file mode 100644 index 0000000000..58a62fad3e --- /dev/null +++ b/remote/test/puppeteer/src/common/Product.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Supported products. + * @public + */ +export type Product = 'chrome' | 'firefox'; diff --git a/remote/test/puppeteer/src/common/Puppeteer.ts b/remote/test/puppeteer/src/common/Puppeteer.ts new file mode 100644 index 0000000000..0dc40d33b5 --- /dev/null +++ b/remote/test/puppeteer/src/common/Puppeteer.ts @@ -0,0 +1,169 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { puppeteerErrors, PuppeteerErrors } from './Errors.js'; +import { ConnectionTransport } from './ConnectionTransport.js'; +import { devicesMap, DevicesMap } from './DeviceDescriptors.js'; +import { Browser } from './Browser.js'; +import { + registerCustomQueryHandler, + unregisterCustomQueryHandler, + customQueryHandlerNames, + clearCustomQueryHandlers, + CustomQueryHandler, +} from './QueryHandler.js'; +import { Product } from './Product.js'; +import { connectToBrowser, BrowserOptions } from './BrowserConnector.js'; + +/** + * Settings that are common to the Puppeteer class, regardless of enviroment. + * @internal + */ +export interface CommonPuppeteerSettings { + isPuppeteerCore: boolean; +} + +export interface ConnectOptions extends BrowserOptions { + browserWSEndpoint?: string; + browserURL?: string; + transport?: ConnectionTransport; + product?: Product; +} + +/** + * The main Puppeteer class. + * + * IMPORTANT: if you are using Puppeteer in a Node environment, you will get an + * instance of {@link PuppeteerNode} when you import or require `puppeteer`. + * That class extends `Puppeteer`, so has all the methods documented below as + * well as all that are defined on {@link PuppeteerNode}. + * @public + */ +export class Puppeteer { + protected _isPuppeteerCore: boolean; + protected _changedProduct = false; + + /** + * @internal + */ + constructor(settings: CommonPuppeteerSettings) { + this._isPuppeteerCore = settings.isPuppeteerCore; + } + + /** + * This method attaches Puppeteer to an existing browser instance. + * + * @remarks + * + * @param options - Set of configurable options to set on the browser. + * @returns Promise which resolves to browser instance. + */ + connect(options: ConnectOptions): Promise<Browser> { + return connectToBrowser(options); + } + + /** + * @remarks + * A list of devices to be used with `page.emulate(options)`. Actual list of devices can be found in {@link https://github.com/puppeteer/puppeteer/blob/main/src/common/DeviceDescriptors.ts | src/common/DeviceDescriptors.ts}. + * + * @example + * + * ```js + * const puppeteer = require('puppeteer'); + * const iPhone = puppeteer.devices['iPhone 6']; + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.emulate(iPhone); + * await page.goto('https://www.google.com'); + * // other actions... + * await browser.close(); + * })(); + * ``` + * + */ + get devices(): DevicesMap { + return devicesMap; + } + + /** + * @remarks + * + * Puppeteer methods might throw errors if they are unable to fulfill a request. + * For example, `page.waitForSelector(selector[, options])` might fail if + * the selector doesn't match any nodes during the given timeframe. + * + * For certain types of errors Puppeteer uses specific error classes. + * These classes are available via `puppeteer.errors`. + * + * @example + * An example of handling a timeout error: + * ```js + * try { + * await page.waitForSelector('.foo'); + * } catch (e) { + * if (e instanceof puppeteer.errors.TimeoutError) { + * // Do something if this is a timeout. + * } + * } + * ``` + */ + get errors(): PuppeteerErrors { + return puppeteerErrors; + } + + /** + * Registers a {@link CustomQueryHandler | custom query handler}. After + * registration, the handler can be used everywhere where a selector is + * expected by prepending the selection string with `<name>/`. The name is + * only allowed to consist of lower- and upper case latin letters. + * @example + * ``` + * puppeteer.registerCustomQueryHandler('text', { … }); + * const aHandle = await page.$('text/…'); + * ``` + * @param name - The name that the custom query handler will be registered under. + * @param queryHandler - The {@link CustomQueryHandler | custom query handler} to + * register. + */ + registerCustomQueryHandler( + name: string, + queryHandler: CustomQueryHandler + ): void { + registerCustomQueryHandler(name, queryHandler); + } + + /** + * @param name - The name of the query handler to unregistered. + */ + unregisterCustomQueryHandler(name: string): void { + unregisterCustomQueryHandler(name); + } + + /** + * @returns a list with the names of all registered custom query handlers. + */ + customQueryHandlerNames(): string[] { + return customQueryHandlerNames(); + } + + /** + * Clears all registered handlers. + */ + clearCustomQueryHandlers(): void { + clearCustomQueryHandlers(); + } +} diff --git a/remote/test/puppeteer/src/common/PuppeteerViewport.ts b/remote/test/puppeteer/src/common/PuppeteerViewport.ts new file mode 100644 index 0000000000..f626ce8d2b --- /dev/null +++ b/remote/test/puppeteer/src/common/PuppeteerViewport.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export interface Viewport { + width: number; + height: number; + deviceScaleFactor?: number; + isMobile?: boolean; + isLandscape?: boolean; + hasTouch?: boolean; +} diff --git a/remote/test/puppeteer/src/common/QueryHandler.ts b/remote/test/puppeteer/src/common/QueryHandler.ts new file mode 100644 index 0000000000..b7984067ee --- /dev/null +++ b/remote/test/puppeteer/src/common/QueryHandler.ts @@ -0,0 +1,238 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { WaitForSelectorOptions, DOMWorld } from './DOMWorld.js'; +import { ElementHandle, JSHandle } from './JSHandle.js'; +import { ariaHandler } from './AriaQueryHandler.js'; + +/** + * @internal + */ +export interface InternalQueryHandler { + queryOne?: ( + element: ElementHandle, + selector: string + ) => Promise<ElementHandle | null>; + waitFor?: ( + domWorld: DOMWorld, + selector: string, + options: WaitForSelectorOptions + ) => Promise<ElementHandle | null>; + queryAll?: ( + element: ElementHandle, + selector: string + ) => Promise<ElementHandle[]>; + queryAllArray?: ( + element: ElementHandle, + selector: string + ) => Promise<JSHandle>; +} + +/** + * Contains two functions `queryOne` and `queryAll` that can + * be {@link Puppeteer.registerCustomQueryHandler | registered} + * as alternative querying strategies. The functions `queryOne` and `queryAll` + * are executed in the page context. `queryOne` should take an `Element` and a + * selector string as argument and return a single `Element` or `null` if no + * element is found. `queryAll` takes the same arguments but should instead + * return a `NodeListOf<Element>` or `Array<Element>` with all the elements + * that match the given query selector. + * @public + */ +export interface CustomQueryHandler { + queryOne?: (element: Element | Document, selector: string) => Element | null; + queryAll?: ( + element: Element | Document, + selector: string + ) => Element[] | NodeListOf<Element>; +} + +function makeQueryHandler(handler: CustomQueryHandler): InternalQueryHandler { + const internalHandler: InternalQueryHandler = {}; + + if (handler.queryOne) { + internalHandler.queryOne = async (element, selector) => { + const jsHandle = await element.evaluateHandle(handler.queryOne, selector); + const elementHandle = jsHandle.asElement(); + if (elementHandle) return elementHandle; + await jsHandle.dispose(); + return null; + }; + internalHandler.waitFor = ( + domWorld: DOMWorld, + selector: string, + options: WaitForSelectorOptions + ) => domWorld.waitForSelectorInPage(handler.queryOne, selector, options); + } + + if (handler.queryAll) { + internalHandler.queryAll = async (element, selector) => { + const jsHandle = await element.evaluateHandle(handler.queryAll, selector); + const properties = await jsHandle.getProperties(); + await jsHandle.dispose(); + const result = []; + for (const property of properties.values()) { + const elementHandle = property.asElement(); + if (elementHandle) result.push(elementHandle); + } + return result; + }; + internalHandler.queryAllArray = async (element, selector) => { + const resultHandle = await element.evaluateHandle( + handler.queryAll, + selector + ); + const arrayHandle = await resultHandle.evaluateHandle( + (res: Element[] | NodeListOf<Element>) => Array.from(res) + ); + return arrayHandle; + }; + } + + return internalHandler; +} + +const _defaultHandler = makeQueryHandler({ + queryOne: (element: Element, selector: string) => + element.querySelector(selector), + queryAll: (element: Element, selector: string) => + element.querySelectorAll(selector), +}); + +const pierceHandler = makeQueryHandler({ + queryOne: (element, selector) => { + let found: Element | null = null; + const search = (root: Element | ShadowRoot) => { + const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); + do { + const currentNode = iter.currentNode as HTMLElement; + if (currentNode.shadowRoot) { + search(currentNode.shadowRoot); + } + if (currentNode instanceof ShadowRoot) { + continue; + } + if (!found && currentNode.matches(selector)) { + found = currentNode; + } + } while (!found && iter.nextNode()); + }; + if (element instanceof Document) { + element = element.documentElement; + } + search(element); + return found; + }, + + queryAll: (element, selector) => { + const result: Element[] = []; + const collect = (root: Element | ShadowRoot) => { + const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); + do { + const currentNode = iter.currentNode as HTMLElement; + if (currentNode.shadowRoot) { + collect(currentNode.shadowRoot); + } + if (currentNode instanceof ShadowRoot) { + continue; + } + if (currentNode.matches(selector)) { + result.push(currentNode); + } + } while (iter.nextNode()); + }; + if (element instanceof Document) { + element = element.documentElement; + } + collect(element); + return result; + }, +}); + +const _builtInHandlers = new Map([ + ['aria', ariaHandler], + ['pierce', pierceHandler], +]); +const _queryHandlers = new Map(_builtInHandlers); + +/** + * @internal + */ +export function registerCustomQueryHandler( + name: string, + handler: CustomQueryHandler +): void { + if (_queryHandlers.get(name)) + throw new Error(`A custom query handler named "${name}" already exists`); + + const isValidName = /^[a-zA-Z]+$/.test(name); + if (!isValidName) + throw new Error(`Custom query handler names may only contain [a-zA-Z]`); + + const internalHandler = makeQueryHandler(handler); + + _queryHandlers.set(name, internalHandler); +} + +/** + * @internal + */ +export function unregisterCustomQueryHandler(name: string): void { + if (_queryHandlers.has(name) && !_builtInHandlers.has(name)) { + _queryHandlers.delete(name); + } +} + +/** + * @internal + */ +export function customQueryHandlerNames(): string[] { + return [..._queryHandlers.keys()].filter( + (name) => !_builtInHandlers.has(name) + ); +} + +/** + * @internal + */ +export function clearCustomQueryHandlers(): void { + customQueryHandlerNames().forEach(unregisterCustomQueryHandler); +} + +/** + * @internal + */ +export function getQueryHandlerAndSelector( + selector: string +): { updatedSelector: string; queryHandler: InternalQueryHandler } { + const hasCustomQueryHandler = /^[a-zA-Z]+\//.test(selector); + if (!hasCustomQueryHandler) + return { updatedSelector: selector, queryHandler: _defaultHandler }; + + const index = selector.indexOf('/'); + const name = selector.slice(0, index); + const updatedSelector = selector.slice(index + 1); + const queryHandler = _queryHandlers.get(name); + if (!queryHandler) + throw new Error( + `Query set to use "${name}", but no query handler of that name was found` + ); + + return { + updatedSelector, + queryHandler, + }; +} diff --git a/remote/test/puppeteer/src/common/SecurityDetails.ts b/remote/test/puppeteer/src/common/SecurityDetails.ts new file mode 100644 index 0000000000..aceba1a3d0 --- /dev/null +++ b/remote/test/puppeteer/src/common/SecurityDetails.ts @@ -0,0 +1,88 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Protocol } from 'devtools-protocol'; + +/** + * The SecurityDetails class represents the security details of a + * response that was received over a secure connection. + * + * @public + */ +export class SecurityDetails { + private _subjectName: string; + private _issuer: string; + private _validFrom: number; + private _validTo: number; + private _protocol: string; + private _sanList: string[]; + + /** + * @internal + */ + constructor(securityPayload: Protocol.Network.SecurityDetails) { + this._subjectName = securityPayload.subjectName; + this._issuer = securityPayload.issuer; + this._validFrom = securityPayload.validFrom; + this._validTo = securityPayload.validTo; + this._protocol = securityPayload.protocol; + this._sanList = securityPayload.sanList; + } + + /** + * @returns The name of the issuer of the certificate. + */ + issuer(): string { + return this._issuer; + } + + /** + * @returns {@link https://en.wikipedia.org/wiki/Unix_time | Unix timestamp} + * marking the start of the certificate's validity. + */ + validFrom(): number { + return this._validFrom; + } + + /** + * @returns {@link https://en.wikipedia.org/wiki/Unix_time | Unix timestamp} + * marking the end of the certificate's validity. + */ + validTo(): number { + return this._validTo; + } + + /** + * @returns The security protocol being used, e.g. "TLS 1.2". + */ + protocol(): string { + return this._protocol; + } + + /** + * @returns The name of the subject to which the certificate was issued. + */ + subjectName(): string { + return this._subjectName; + } + + /** + * @returns The list of {@link https://en.wikipedia.org/wiki/Subject_Alternative_Name | subject alternative names (SANs)} of the certificate. + */ + subjectAlternativeNames(): string[] { + return this._sanList; + } +} diff --git a/remote/test/puppeteer/src/common/Target.ts b/remote/test/puppeteer/src/common/Target.ts new file mode 100644 index 0000000000..0e8296a633 --- /dev/null +++ b/remote/test/puppeteer/src/common/Target.ts @@ -0,0 +1,222 @@ +/** + * Copyright 2019 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Page, PageEmittedEvents } from './Page.js'; +import { WebWorker } from './WebWorker.js'; +import { CDPSession } from './Connection.js'; +import { Browser, BrowserContext } from './Browser.js'; +import { Viewport } from './PuppeteerViewport.js'; +import { Protocol } from 'devtools-protocol'; + +/** + * @public + */ +export class Target { + private _targetInfo: Protocol.Target.TargetInfo; + private _browserContext: BrowserContext; + + private _sessionFactory: () => Promise<CDPSession>; + private _ignoreHTTPSErrors: boolean; + private _defaultViewport?: Viewport; + private _pagePromise?: Promise<Page>; + private _workerPromise?: Promise<WebWorker>; + /** + * @internal + */ + _initializedPromise: Promise<boolean>; + /** + * @internal + */ + _initializedCallback: (x: boolean) => void; + /** + * @internal + */ + _isClosedPromise: Promise<void>; + /** + * @internal + */ + _closedCallback: () => void; + /** + * @internal + */ + _isInitialized: boolean; + /** + * @internal + */ + _targetId: string; + + /** + * @internal + */ + constructor( + targetInfo: Protocol.Target.TargetInfo, + browserContext: BrowserContext, + sessionFactory: () => Promise<CDPSession>, + ignoreHTTPSErrors: boolean, + defaultViewport: Viewport | null + ) { + this._targetInfo = targetInfo; + this._browserContext = browserContext; + this._targetId = targetInfo.targetId; + this._sessionFactory = sessionFactory; + this._ignoreHTTPSErrors = ignoreHTTPSErrors; + this._defaultViewport = defaultViewport; + /** @type {?Promise<!Puppeteer.Page>} */ + this._pagePromise = null; + /** @type {?Promise<!WebWorker>} */ + this._workerPromise = null; + this._initializedPromise = new Promise<boolean>( + (fulfill) => (this._initializedCallback = fulfill) + ).then(async (success) => { + if (!success) return false; + const opener = this.opener(); + if (!opener || !opener._pagePromise || this.type() !== 'page') + return true; + const openerPage = await opener._pagePromise; + if (!openerPage.listenerCount(PageEmittedEvents.Popup)) return true; + const popupPage = await this.page(); + openerPage.emit(PageEmittedEvents.Popup, popupPage); + return true; + }); + this._isClosedPromise = new Promise<void>( + (fulfill) => (this._closedCallback = fulfill) + ); + this._isInitialized = + this._targetInfo.type !== 'page' || this._targetInfo.url !== ''; + if (this._isInitialized) this._initializedCallback(true); + } + + /** + * Creates a Chrome Devtools Protocol session attached to the target. + */ + createCDPSession(): Promise<CDPSession> { + return this._sessionFactory(); + } + + /** + * If the target is not of type `"page"` or `"background_page"`, returns `null`. + */ + async page(): Promise<Page | null> { + if ( + (this._targetInfo.type === 'page' || + this._targetInfo.type === 'background_page' || + this._targetInfo.type === 'webview') && + !this._pagePromise + ) { + this._pagePromise = this._sessionFactory().then((client) => + Page.create( + client, + this, + this._ignoreHTTPSErrors, + this._defaultViewport + ) + ); + } + return this._pagePromise; + } + + /** + * If the target is not of type `"service_worker"` or `"shared_worker"`, returns `null`. + */ + async worker(): Promise<WebWorker | null> { + if ( + this._targetInfo.type !== 'service_worker' && + this._targetInfo.type !== 'shared_worker' + ) + return null; + if (!this._workerPromise) { + // TODO(einbinder): Make workers send their console logs. + this._workerPromise = this._sessionFactory().then( + (client) => + new WebWorker( + client, + this._targetInfo.url, + () => {} /* consoleAPICalled */, + () => {} /* exceptionThrown */ + ) + ); + } + return this._workerPromise; + } + + url(): string { + return this._targetInfo.url; + } + + /** + * Identifies what kind of target this is. + * + * @remarks + * + * See {@link https://developer.chrome.com/extensions/background_pages | docs} for more info about background pages. + */ + type(): + | 'page' + | 'background_page' + | 'service_worker' + | 'shared_worker' + | 'other' + | 'browser' + | 'webview' { + const type = this._targetInfo.type; + if ( + type === 'page' || + type === 'background_page' || + type === 'service_worker' || + type === 'shared_worker' || + type === 'browser' || + type === 'webview' + ) + return type; + return 'other'; + } + + /** + * Get the browser the target belongs to. + */ + browser(): Browser { + return this._browserContext.browser(); + } + + browserContext(): BrowserContext { + return this._browserContext; + } + + /** + * Get the target that opened this target. Top-level targets return `null`. + */ + opener(): Target | null { + const { openerId } = this._targetInfo; + if (!openerId) return null; + return this.browser()._targets.get(openerId); + } + + /** + * @internal + */ + _targetInfoChanged(targetInfo: Protocol.Target.TargetInfo): void { + this._targetInfo = targetInfo; + + if ( + !this._isInitialized && + (this._targetInfo.type !== 'page' || this._targetInfo.url !== '') + ) { + this._isInitialized = true; + this._initializedCallback(true); + return; + } + } +} diff --git a/remote/test/puppeteer/src/common/TimeoutSettings.ts b/remote/test/puppeteer/src/common/TimeoutSettings.ts new file mode 100644 index 0000000000..9c441498de --- /dev/null +++ b/remote/test/puppeteer/src/common/TimeoutSettings.ts @@ -0,0 +1,50 @@ +/** + * Copyright 2019 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const DEFAULT_TIMEOUT = 30000; + +/** + * @internal + */ +export class TimeoutSettings { + _defaultTimeout: number | null; + _defaultNavigationTimeout: number | null; + + constructor() { + this._defaultTimeout = null; + this._defaultNavigationTimeout = null; + } + + setDefaultTimeout(timeout: number): void { + this._defaultTimeout = timeout; + } + + setDefaultNavigationTimeout(timeout: number): void { + this._defaultNavigationTimeout = timeout; + } + + navigationTimeout(): number { + if (this._defaultNavigationTimeout !== null) + return this._defaultNavigationTimeout; + if (this._defaultTimeout !== null) return this._defaultTimeout; + return DEFAULT_TIMEOUT; + } + + timeout(): number { + if (this._defaultTimeout !== null) return this._defaultTimeout; + return DEFAULT_TIMEOUT; + } +} diff --git a/remote/test/puppeteer/src/common/Tracing.ts b/remote/test/puppeteer/src/common/Tracing.ts new file mode 100644 index 0000000000..ad075e9bac --- /dev/null +++ b/remote/test/puppeteer/src/common/Tracing.ts @@ -0,0 +1,118 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { assert } from './assert.js'; +import { helper } from './helper.js'; +import { CDPSession } from './Connection.js'; + +/** + * @public + */ +export interface TracingOptions { + path?: string; + screenshots?: boolean; + categories?: string[]; +} + +/** + * The Tracing class exposes the tracing audit interface. + * @remarks + * You can use `tracing.start` and `tracing.stop` to create a trace file + * which can be opened in Chrome DevTools or {@link https://chromedevtools.github.io/timeline-viewer/ | timeline viewer}. + * + * @example + * ```js + * await page.tracing.start({path: 'trace.json'}); + * await page.goto('https://www.google.com'); + * await page.tracing.stop(); + * ``` + * + * @public + */ +export class Tracing { + _client: CDPSession; + _recording = false; + _path = ''; + + /** + * @internal + */ + constructor(client: CDPSession) { + this._client = client; + } + + /** + * Starts a trace for the current page. + * @remarks + * Only one trace can be active at a time per browser. + * @param options - Optional `TracingOptions`. + */ + async start(options: TracingOptions = {}): Promise<void> { + assert( + !this._recording, + 'Cannot start recording trace while already recording trace.' + ); + + const defaultCategories = [ + '-*', + 'devtools.timeline', + 'v8.execute', + 'disabled-by-default-devtools.timeline', + 'disabled-by-default-devtools.timeline.frame', + 'toplevel', + 'blink.console', + 'blink.user_timing', + 'latencyInfo', + 'disabled-by-default-devtools.timeline.stack', + 'disabled-by-default-v8.cpu_profiler', + 'disabled-by-default-v8.cpu_profiler.hires', + ]; + const { + path = null, + screenshots = false, + categories = defaultCategories, + } = options; + + if (screenshots) categories.push('disabled-by-default-devtools.screenshot'); + + this._path = path; + this._recording = true; + await this._client.send('Tracing.start', { + transferMode: 'ReturnAsStream', + categories: categories.join(','), + }); + } + + /** + * Stops a trace started with the `start` method. + * @returns Promise which resolves to buffer with trace data. + */ + async stop(): Promise<Buffer> { + let fulfill: (value: Buffer) => void; + let reject: (err: Error) => void; + const contentPromise = new Promise<Buffer>((x, y) => { + fulfill = x; + reject = y; + }); + this._client.once('Tracing.tracingComplete', (event) => { + helper + .readProtocolStream(this._client, event.stream, this._path) + .then(fulfill, reject); + }); + await this._client.send('Tracing.end'); + this._recording = false; + return contentPromise; + } +} diff --git a/remote/test/puppeteer/src/common/USKeyboardLayout.ts b/remote/test/puppeteer/src/common/USKeyboardLayout.ts new file mode 100644 index 0000000000..feb3a1f7f1 --- /dev/null +++ b/remote/test/puppeteer/src/common/USKeyboardLayout.ts @@ -0,0 +1,681 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @internal + */ +export interface KeyDefinition { + keyCode?: number; + shiftKeyCode?: number; + key?: string; + shiftKey?: string; + code?: string; + text?: string; + shiftText?: string; + location?: number; +} + +/** + * All the valid keys that can be passed to functions that take user input, such + * as {@link Keyboard.press | keyboard.press } + * + * @public + */ +export type KeyInput = + | '0' + | '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + | '8' + | '9' + | 'Power' + | 'Eject' + | 'Abort' + | 'Help' + | 'Backspace' + | 'Tab' + | 'Numpad5' + | 'NumpadEnter' + | 'Enter' + | '\r' + | '\n' + | 'ShiftLeft' + | 'ShiftRight' + | 'ControlLeft' + | 'ControlRight' + | 'AltLeft' + | 'AltRight' + | 'Pause' + | 'CapsLock' + | 'Escape' + | 'Convert' + | 'NonConvert' + | 'Space' + | 'Numpad9' + | 'PageUp' + | 'Numpad3' + | 'PageDown' + | 'End' + | 'Numpad1' + | 'Home' + | 'Numpad7' + | 'ArrowLeft' + | 'Numpad4' + | 'Numpad8' + | 'ArrowUp' + | 'ArrowRight' + | 'Numpad6' + | 'Numpad2' + | 'ArrowDown' + | 'Select' + | 'Open' + | 'PrintScreen' + | 'Insert' + | 'Numpad0' + | 'Delete' + | 'NumpadDecimal' + | 'Digit0' + | 'Digit1' + | 'Digit2' + | 'Digit3' + | 'Digit4' + | 'Digit5' + | 'Digit6' + | 'Digit7' + | 'Digit8' + | 'Digit9' + | 'KeyA' + | 'KeyB' + | 'KeyC' + | 'KeyD' + | 'KeyE' + | 'KeyF' + | 'KeyG' + | 'KeyH' + | 'KeyI' + | 'KeyJ' + | 'KeyK' + | 'KeyL' + | 'KeyM' + | 'KeyN' + | 'KeyO' + | 'KeyP' + | 'KeyQ' + | 'KeyR' + | 'KeyS' + | 'KeyT' + | 'KeyU' + | 'KeyV' + | 'KeyW' + | 'KeyX' + | 'KeyY' + | 'KeyZ' + | 'MetaLeft' + | 'MetaRight' + | 'ContextMenu' + | 'NumpadMultiply' + | 'NumpadAdd' + | 'NumpadSubtract' + | 'NumpadDivide' + | 'F1' + | 'F2' + | 'F3' + | 'F4' + | 'F5' + | 'F6' + | 'F7' + | 'F8' + | 'F9' + | 'F10' + | 'F11' + | 'F12' + | 'F13' + | 'F14' + | 'F15' + | 'F16' + | 'F17' + | 'F18' + | 'F19' + | 'F20' + | 'F21' + | 'F22' + | 'F23' + | 'F24' + | 'NumLock' + | 'ScrollLock' + | 'AudioVolumeMute' + | 'AudioVolumeDown' + | 'AudioVolumeUp' + | 'MediaTrackNext' + | 'MediaTrackPrevious' + | 'MediaStop' + | 'MediaPlayPause' + | 'Semicolon' + | 'Equal' + | 'NumpadEqual' + | 'Comma' + | 'Minus' + | 'Period' + | 'Slash' + | 'Backquote' + | 'BracketLeft' + | 'Backslash' + | 'BracketRight' + | 'Quote' + | 'AltGraph' + | 'Props' + | 'Cancel' + | 'Clear' + | 'Shift' + | 'Control' + | 'Alt' + | 'Accept' + | 'ModeChange' + | ' ' + | 'Print' + | 'Execute' + | '\u0000' + | 'a' + | 'b' + | 'c' + | 'd' + | 'e' + | 'f' + | 'g' + | 'h' + | 'i' + | 'j' + | 'k' + | 'l' + | 'm' + | 'n' + | 'o' + | 'p' + | 'q' + | 'r' + | 's' + | 't' + | 'u' + | 'v' + | 'w' + | 'x' + | 'y' + | 'z' + | 'Meta' + | '*' + | '+' + | '-' + | '/' + | ';' + | '=' + | ',' + | '.' + | '`' + | '[' + | '\\' + | ']' + | "'" + | 'Attn' + | 'CrSel' + | 'ExSel' + | 'EraseEof' + | 'Play' + | 'ZoomOut' + | ')' + | '!' + | '@' + | '#' + | '$' + | '%' + | '^' + | '&' + | '(' + | 'A' + | 'B' + | 'C' + | 'D' + | 'E' + | 'F' + | 'G' + | 'H' + | 'I' + | 'J' + | 'K' + | 'L' + | 'M' + | 'N' + | 'O' + | 'P' + | 'Q' + | 'R' + | 'S' + | 'T' + | 'U' + | 'V' + | 'W' + | 'X' + | 'Y' + | 'Z' + | ':' + | '<' + | '_' + | '>' + | '?' + | '~' + | '{' + | '|' + | '}' + | '"' + | 'SoftLeft' + | 'SoftRight' + | 'Camera' + | 'Call' + | 'EndCall' + | 'VolumeDown' + | 'VolumeUp'; + +/** + * @internal + */ +export const keyDefinitions: Readonly<Record<KeyInput, KeyDefinition>> = { + '0': { keyCode: 48, key: '0', code: 'Digit0' }, + '1': { keyCode: 49, key: '1', code: 'Digit1' }, + '2': { keyCode: 50, key: '2', code: 'Digit2' }, + '3': { keyCode: 51, key: '3', code: 'Digit3' }, + '4': { keyCode: 52, key: '4', code: 'Digit4' }, + '5': { keyCode: 53, key: '5', code: 'Digit5' }, + '6': { keyCode: 54, key: '6', code: 'Digit6' }, + '7': { keyCode: 55, key: '7', code: 'Digit7' }, + '8': { keyCode: 56, key: '8', code: 'Digit8' }, + '9': { keyCode: 57, key: '9', code: 'Digit9' }, + Power: { key: 'Power', code: 'Power' }, + Eject: { key: 'Eject', code: 'Eject' }, + Abort: { keyCode: 3, code: 'Abort', key: 'Cancel' }, + Help: { keyCode: 6, code: 'Help', key: 'Help' }, + Backspace: { keyCode: 8, code: 'Backspace', key: 'Backspace' }, + Tab: { keyCode: 9, code: 'Tab', key: 'Tab' }, + Numpad5: { + keyCode: 12, + shiftKeyCode: 101, + key: 'Clear', + code: 'Numpad5', + shiftKey: '5', + location: 3, + }, + NumpadEnter: { + keyCode: 13, + code: 'NumpadEnter', + key: 'Enter', + text: '\r', + location: 3, + }, + Enter: { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' }, + '\r': { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' }, + '\n': { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' }, + ShiftLeft: { keyCode: 16, code: 'ShiftLeft', key: 'Shift', location: 1 }, + ShiftRight: { keyCode: 16, code: 'ShiftRight', key: 'Shift', location: 2 }, + ControlLeft: { + keyCode: 17, + code: 'ControlLeft', + key: 'Control', + location: 1, + }, + ControlRight: { + keyCode: 17, + code: 'ControlRight', + key: 'Control', + location: 2, + }, + AltLeft: { keyCode: 18, code: 'AltLeft', key: 'Alt', location: 1 }, + AltRight: { keyCode: 18, code: 'AltRight', key: 'Alt', location: 2 }, + Pause: { keyCode: 19, code: 'Pause', key: 'Pause' }, + CapsLock: { keyCode: 20, code: 'CapsLock', key: 'CapsLock' }, + Escape: { keyCode: 27, code: 'Escape', key: 'Escape' }, + Convert: { keyCode: 28, code: 'Convert', key: 'Convert' }, + NonConvert: { keyCode: 29, code: 'NonConvert', key: 'NonConvert' }, + Space: { keyCode: 32, code: 'Space', key: ' ' }, + Numpad9: { + keyCode: 33, + shiftKeyCode: 105, + key: 'PageUp', + code: 'Numpad9', + shiftKey: '9', + location: 3, + }, + PageUp: { keyCode: 33, code: 'PageUp', key: 'PageUp' }, + Numpad3: { + keyCode: 34, + shiftKeyCode: 99, + key: 'PageDown', + code: 'Numpad3', + shiftKey: '3', + location: 3, + }, + PageDown: { keyCode: 34, code: 'PageDown', key: 'PageDown' }, + End: { keyCode: 35, code: 'End', key: 'End' }, + Numpad1: { + keyCode: 35, + shiftKeyCode: 97, + key: 'End', + code: 'Numpad1', + shiftKey: '1', + location: 3, + }, + Home: { keyCode: 36, code: 'Home', key: 'Home' }, + Numpad7: { + keyCode: 36, + shiftKeyCode: 103, + key: 'Home', + code: 'Numpad7', + shiftKey: '7', + location: 3, + }, + ArrowLeft: { keyCode: 37, code: 'ArrowLeft', key: 'ArrowLeft' }, + Numpad4: { + keyCode: 37, + shiftKeyCode: 100, + key: 'ArrowLeft', + code: 'Numpad4', + shiftKey: '4', + location: 3, + }, + Numpad8: { + keyCode: 38, + shiftKeyCode: 104, + key: 'ArrowUp', + code: 'Numpad8', + shiftKey: '8', + location: 3, + }, + ArrowUp: { keyCode: 38, code: 'ArrowUp', key: 'ArrowUp' }, + ArrowRight: { keyCode: 39, code: 'ArrowRight', key: 'ArrowRight' }, + Numpad6: { + keyCode: 39, + shiftKeyCode: 102, + key: 'ArrowRight', + code: 'Numpad6', + shiftKey: '6', + location: 3, + }, + Numpad2: { + keyCode: 40, + shiftKeyCode: 98, + key: 'ArrowDown', + code: 'Numpad2', + shiftKey: '2', + location: 3, + }, + ArrowDown: { keyCode: 40, code: 'ArrowDown', key: 'ArrowDown' }, + Select: { keyCode: 41, code: 'Select', key: 'Select' }, + Open: { keyCode: 43, code: 'Open', key: 'Execute' }, + PrintScreen: { keyCode: 44, code: 'PrintScreen', key: 'PrintScreen' }, + Insert: { keyCode: 45, code: 'Insert', key: 'Insert' }, + Numpad0: { + keyCode: 45, + shiftKeyCode: 96, + key: 'Insert', + code: 'Numpad0', + shiftKey: '0', + location: 3, + }, + Delete: { keyCode: 46, code: 'Delete', key: 'Delete' }, + NumpadDecimal: { + keyCode: 46, + shiftKeyCode: 110, + code: 'NumpadDecimal', + key: '\u0000', + shiftKey: '.', + location: 3, + }, + Digit0: { keyCode: 48, code: 'Digit0', shiftKey: ')', key: '0' }, + Digit1: { keyCode: 49, code: 'Digit1', shiftKey: '!', key: '1' }, + Digit2: { keyCode: 50, code: 'Digit2', shiftKey: '@', key: '2' }, + Digit3: { keyCode: 51, code: 'Digit3', shiftKey: '#', key: '3' }, + Digit4: { keyCode: 52, code: 'Digit4', shiftKey: '$', key: '4' }, + Digit5: { keyCode: 53, code: 'Digit5', shiftKey: '%', key: '5' }, + Digit6: { keyCode: 54, code: 'Digit6', shiftKey: '^', key: '6' }, + Digit7: { keyCode: 55, code: 'Digit7', shiftKey: '&', key: '7' }, + Digit8: { keyCode: 56, code: 'Digit8', shiftKey: '*', key: '8' }, + Digit9: { keyCode: 57, code: 'Digit9', shiftKey: '(', key: '9' }, + KeyA: { keyCode: 65, code: 'KeyA', shiftKey: 'A', key: 'a' }, + KeyB: { keyCode: 66, code: 'KeyB', shiftKey: 'B', key: 'b' }, + KeyC: { keyCode: 67, code: 'KeyC', shiftKey: 'C', key: 'c' }, + KeyD: { keyCode: 68, code: 'KeyD', shiftKey: 'D', key: 'd' }, + KeyE: { keyCode: 69, code: 'KeyE', shiftKey: 'E', key: 'e' }, + KeyF: { keyCode: 70, code: 'KeyF', shiftKey: 'F', key: 'f' }, + KeyG: { keyCode: 71, code: 'KeyG', shiftKey: 'G', key: 'g' }, + KeyH: { keyCode: 72, code: 'KeyH', shiftKey: 'H', key: 'h' }, + KeyI: { keyCode: 73, code: 'KeyI', shiftKey: 'I', key: 'i' }, + KeyJ: { keyCode: 74, code: 'KeyJ', shiftKey: 'J', key: 'j' }, + KeyK: { keyCode: 75, code: 'KeyK', shiftKey: 'K', key: 'k' }, + KeyL: { keyCode: 76, code: 'KeyL', shiftKey: 'L', key: 'l' }, + KeyM: { keyCode: 77, code: 'KeyM', shiftKey: 'M', key: 'm' }, + KeyN: { keyCode: 78, code: 'KeyN', shiftKey: 'N', key: 'n' }, + KeyO: { keyCode: 79, code: 'KeyO', shiftKey: 'O', key: 'o' }, + KeyP: { keyCode: 80, code: 'KeyP', shiftKey: 'P', key: 'p' }, + KeyQ: { keyCode: 81, code: 'KeyQ', shiftKey: 'Q', key: 'q' }, + KeyR: { keyCode: 82, code: 'KeyR', shiftKey: 'R', key: 'r' }, + KeyS: { keyCode: 83, code: 'KeyS', shiftKey: 'S', key: 's' }, + KeyT: { keyCode: 84, code: 'KeyT', shiftKey: 'T', key: 't' }, + KeyU: { keyCode: 85, code: 'KeyU', shiftKey: 'U', key: 'u' }, + KeyV: { keyCode: 86, code: 'KeyV', shiftKey: 'V', key: 'v' }, + KeyW: { keyCode: 87, code: 'KeyW', shiftKey: 'W', key: 'w' }, + KeyX: { keyCode: 88, code: 'KeyX', shiftKey: 'X', key: 'x' }, + KeyY: { keyCode: 89, code: 'KeyY', shiftKey: 'Y', key: 'y' }, + KeyZ: { keyCode: 90, code: 'KeyZ', shiftKey: 'Z', key: 'z' }, + MetaLeft: { keyCode: 91, code: 'MetaLeft', key: 'Meta', location: 1 }, + MetaRight: { keyCode: 92, code: 'MetaRight', key: 'Meta', location: 2 }, + ContextMenu: { keyCode: 93, code: 'ContextMenu', key: 'ContextMenu' }, + NumpadMultiply: { + keyCode: 106, + code: 'NumpadMultiply', + key: '*', + location: 3, + }, + NumpadAdd: { keyCode: 107, code: 'NumpadAdd', key: '+', location: 3 }, + NumpadSubtract: { + keyCode: 109, + code: 'NumpadSubtract', + key: '-', + location: 3, + }, + NumpadDivide: { keyCode: 111, code: 'NumpadDivide', key: '/', location: 3 }, + F1: { keyCode: 112, code: 'F1', key: 'F1' }, + F2: { keyCode: 113, code: 'F2', key: 'F2' }, + F3: { keyCode: 114, code: 'F3', key: 'F3' }, + F4: { keyCode: 115, code: 'F4', key: 'F4' }, + F5: { keyCode: 116, code: 'F5', key: 'F5' }, + F6: { keyCode: 117, code: 'F6', key: 'F6' }, + F7: { keyCode: 118, code: 'F7', key: 'F7' }, + F8: { keyCode: 119, code: 'F8', key: 'F8' }, + F9: { keyCode: 120, code: 'F9', key: 'F9' }, + F10: { keyCode: 121, code: 'F10', key: 'F10' }, + F11: { keyCode: 122, code: 'F11', key: 'F11' }, + F12: { keyCode: 123, code: 'F12', key: 'F12' }, + F13: { keyCode: 124, code: 'F13', key: 'F13' }, + F14: { keyCode: 125, code: 'F14', key: 'F14' }, + F15: { keyCode: 126, code: 'F15', key: 'F15' }, + F16: { keyCode: 127, code: 'F16', key: 'F16' }, + F17: { keyCode: 128, code: 'F17', key: 'F17' }, + F18: { keyCode: 129, code: 'F18', key: 'F18' }, + F19: { keyCode: 130, code: 'F19', key: 'F19' }, + F20: { keyCode: 131, code: 'F20', key: 'F20' }, + F21: { keyCode: 132, code: 'F21', key: 'F21' }, + F22: { keyCode: 133, code: 'F22', key: 'F22' }, + F23: { keyCode: 134, code: 'F23', key: 'F23' }, + F24: { keyCode: 135, code: 'F24', key: 'F24' }, + NumLock: { keyCode: 144, code: 'NumLock', key: 'NumLock' }, + ScrollLock: { keyCode: 145, code: 'ScrollLock', key: 'ScrollLock' }, + AudioVolumeMute: { + keyCode: 173, + code: 'AudioVolumeMute', + key: 'AudioVolumeMute', + }, + AudioVolumeDown: { + keyCode: 174, + code: 'AudioVolumeDown', + key: 'AudioVolumeDown', + }, + AudioVolumeUp: { keyCode: 175, code: 'AudioVolumeUp', key: 'AudioVolumeUp' }, + MediaTrackNext: { + keyCode: 176, + code: 'MediaTrackNext', + key: 'MediaTrackNext', + }, + MediaTrackPrevious: { + keyCode: 177, + code: 'MediaTrackPrevious', + key: 'MediaTrackPrevious', + }, + MediaStop: { keyCode: 178, code: 'MediaStop', key: 'MediaStop' }, + MediaPlayPause: { + keyCode: 179, + code: 'MediaPlayPause', + key: 'MediaPlayPause', + }, + Semicolon: { keyCode: 186, code: 'Semicolon', shiftKey: ':', key: ';' }, + Equal: { keyCode: 187, code: 'Equal', shiftKey: '+', key: '=' }, + NumpadEqual: { keyCode: 187, code: 'NumpadEqual', key: '=', location: 3 }, + Comma: { keyCode: 188, code: 'Comma', shiftKey: '<', key: ',' }, + Minus: { keyCode: 189, code: 'Minus', shiftKey: '_', key: '-' }, + Period: { keyCode: 190, code: 'Period', shiftKey: '>', key: '.' }, + Slash: { keyCode: 191, code: 'Slash', shiftKey: '?', key: '/' }, + Backquote: { keyCode: 192, code: 'Backquote', shiftKey: '~', key: '`' }, + BracketLeft: { keyCode: 219, code: 'BracketLeft', shiftKey: '{', key: '[' }, + Backslash: { keyCode: 220, code: 'Backslash', shiftKey: '|', key: '\\' }, + BracketRight: { keyCode: 221, code: 'BracketRight', shiftKey: '}', key: ']' }, + Quote: { keyCode: 222, code: 'Quote', shiftKey: '"', key: "'" }, + AltGraph: { keyCode: 225, code: 'AltGraph', key: 'AltGraph' }, + Props: { keyCode: 247, code: 'Props', key: 'CrSel' }, + Cancel: { keyCode: 3, key: 'Cancel', code: 'Abort' }, + Clear: { keyCode: 12, key: 'Clear', code: 'Numpad5', location: 3 }, + Shift: { keyCode: 16, key: 'Shift', code: 'ShiftLeft', location: 1 }, + Control: { keyCode: 17, key: 'Control', code: 'ControlLeft', location: 1 }, + Alt: { keyCode: 18, key: 'Alt', code: 'AltLeft', location: 1 }, + Accept: { keyCode: 30, key: 'Accept' }, + ModeChange: { keyCode: 31, key: 'ModeChange' }, + ' ': { keyCode: 32, key: ' ', code: 'Space' }, + Print: { keyCode: 42, key: 'Print' }, + Execute: { keyCode: 43, key: 'Execute', code: 'Open' }, + '\u0000': { keyCode: 46, key: '\u0000', code: 'NumpadDecimal', location: 3 }, + a: { keyCode: 65, key: 'a', code: 'KeyA' }, + b: { keyCode: 66, key: 'b', code: 'KeyB' }, + c: { keyCode: 67, key: 'c', code: 'KeyC' }, + d: { keyCode: 68, key: 'd', code: 'KeyD' }, + e: { keyCode: 69, key: 'e', code: 'KeyE' }, + f: { keyCode: 70, key: 'f', code: 'KeyF' }, + g: { keyCode: 71, key: 'g', code: 'KeyG' }, + h: { keyCode: 72, key: 'h', code: 'KeyH' }, + i: { keyCode: 73, key: 'i', code: 'KeyI' }, + j: { keyCode: 74, key: 'j', code: 'KeyJ' }, + k: { keyCode: 75, key: 'k', code: 'KeyK' }, + l: { keyCode: 76, key: 'l', code: 'KeyL' }, + m: { keyCode: 77, key: 'm', code: 'KeyM' }, + n: { keyCode: 78, key: 'n', code: 'KeyN' }, + o: { keyCode: 79, key: 'o', code: 'KeyO' }, + p: { keyCode: 80, key: 'p', code: 'KeyP' }, + q: { keyCode: 81, key: 'q', code: 'KeyQ' }, + r: { keyCode: 82, key: 'r', code: 'KeyR' }, + s: { keyCode: 83, key: 's', code: 'KeyS' }, + t: { keyCode: 84, key: 't', code: 'KeyT' }, + u: { keyCode: 85, key: 'u', code: 'KeyU' }, + v: { keyCode: 86, key: 'v', code: 'KeyV' }, + w: { keyCode: 87, key: 'w', code: 'KeyW' }, + x: { keyCode: 88, key: 'x', code: 'KeyX' }, + y: { keyCode: 89, key: 'y', code: 'KeyY' }, + z: { keyCode: 90, key: 'z', code: 'KeyZ' }, + Meta: { keyCode: 91, key: 'Meta', code: 'MetaLeft', location: 1 }, + '*': { keyCode: 106, key: '*', code: 'NumpadMultiply', location: 3 }, + '+': { keyCode: 107, key: '+', code: 'NumpadAdd', location: 3 }, + '-': { keyCode: 109, key: '-', code: 'NumpadSubtract', location: 3 }, + '/': { keyCode: 111, key: '/', code: 'NumpadDivide', location: 3 }, + ';': { keyCode: 186, key: ';', code: 'Semicolon' }, + '=': { keyCode: 187, key: '=', code: 'Equal' }, + ',': { keyCode: 188, key: ',', code: 'Comma' }, + '.': { keyCode: 190, key: '.', code: 'Period' }, + '`': { keyCode: 192, key: '`', code: 'Backquote' }, + '[': { keyCode: 219, key: '[', code: 'BracketLeft' }, + '\\': { keyCode: 220, key: '\\', code: 'Backslash' }, + ']': { keyCode: 221, key: ']', code: 'BracketRight' }, + "'": { keyCode: 222, key: "'", code: 'Quote' }, + Attn: { keyCode: 246, key: 'Attn' }, + CrSel: { keyCode: 247, key: 'CrSel', code: 'Props' }, + ExSel: { keyCode: 248, key: 'ExSel' }, + EraseEof: { keyCode: 249, key: 'EraseEof' }, + Play: { keyCode: 250, key: 'Play' }, + ZoomOut: { keyCode: 251, key: 'ZoomOut' }, + ')': { keyCode: 48, key: ')', code: 'Digit0' }, + '!': { keyCode: 49, key: '!', code: 'Digit1' }, + '@': { keyCode: 50, key: '@', code: 'Digit2' }, + '#': { keyCode: 51, key: '#', code: 'Digit3' }, + $: { keyCode: 52, key: '$', code: 'Digit4' }, + '%': { keyCode: 53, key: '%', code: 'Digit5' }, + '^': { keyCode: 54, key: '^', code: 'Digit6' }, + '&': { keyCode: 55, key: '&', code: 'Digit7' }, + '(': { keyCode: 57, key: '(', code: 'Digit9' }, + A: { keyCode: 65, key: 'A', code: 'KeyA' }, + B: { keyCode: 66, key: 'B', code: 'KeyB' }, + C: { keyCode: 67, key: 'C', code: 'KeyC' }, + D: { keyCode: 68, key: 'D', code: 'KeyD' }, + E: { keyCode: 69, key: 'E', code: 'KeyE' }, + F: { keyCode: 70, key: 'F', code: 'KeyF' }, + G: { keyCode: 71, key: 'G', code: 'KeyG' }, + H: { keyCode: 72, key: 'H', code: 'KeyH' }, + I: { keyCode: 73, key: 'I', code: 'KeyI' }, + J: { keyCode: 74, key: 'J', code: 'KeyJ' }, + K: { keyCode: 75, key: 'K', code: 'KeyK' }, + L: { keyCode: 76, key: 'L', code: 'KeyL' }, + M: { keyCode: 77, key: 'M', code: 'KeyM' }, + N: { keyCode: 78, key: 'N', code: 'KeyN' }, + O: { keyCode: 79, key: 'O', code: 'KeyO' }, + P: { keyCode: 80, key: 'P', code: 'KeyP' }, + Q: { keyCode: 81, key: 'Q', code: 'KeyQ' }, + R: { keyCode: 82, key: 'R', code: 'KeyR' }, + S: { keyCode: 83, key: 'S', code: 'KeyS' }, + T: { keyCode: 84, key: 'T', code: 'KeyT' }, + U: { keyCode: 85, key: 'U', code: 'KeyU' }, + V: { keyCode: 86, key: 'V', code: 'KeyV' }, + W: { keyCode: 87, key: 'W', code: 'KeyW' }, + X: { keyCode: 88, key: 'X', code: 'KeyX' }, + Y: { keyCode: 89, key: 'Y', code: 'KeyY' }, + Z: { keyCode: 90, key: 'Z', code: 'KeyZ' }, + ':': { keyCode: 186, key: ':', code: 'Semicolon' }, + '<': { keyCode: 188, key: '<', code: 'Comma' }, + _: { keyCode: 189, key: '_', code: 'Minus' }, + '>': { keyCode: 190, key: '>', code: 'Period' }, + '?': { keyCode: 191, key: '?', code: 'Slash' }, + '~': { keyCode: 192, key: '~', code: 'Backquote' }, + '{': { keyCode: 219, key: '{', code: 'BracketLeft' }, + '|': { keyCode: 220, key: '|', code: 'Backslash' }, + '}': { keyCode: 221, key: '}', code: 'BracketRight' }, + '"': { keyCode: 222, key: '"', code: 'Quote' }, + SoftLeft: { key: 'SoftLeft', code: 'SoftLeft', location: 4 }, + SoftRight: { key: 'SoftRight', code: 'SoftRight', location: 4 }, + Camera: { keyCode: 44, key: 'Camera', code: 'Camera', location: 4 }, + Call: { key: 'Call', code: 'Call', location: 4 }, + EndCall: { keyCode: 95, key: 'EndCall', code: 'EndCall', location: 4 }, + VolumeDown: { + keyCode: 182, + key: 'VolumeDown', + code: 'VolumeDown', + location: 4, + }, + VolumeUp: { keyCode: 183, key: 'VolumeUp', code: 'VolumeUp', location: 4 }, +}; diff --git a/remote/test/puppeteer/src/common/WebWorker.ts b/remote/test/puppeteer/src/common/WebWorker.ts new file mode 100644 index 0000000000..a4d415315e --- /dev/null +++ b/remote/test/puppeteer/src/common/WebWorker.ts @@ -0,0 +1,172 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { EventEmitter } from './EventEmitter.js'; +import { debugError } from './helper.js'; +import { ExecutionContext } from './ExecutionContext.js'; +import { JSHandle } from './JSHandle.js'; +import { CDPSession } from './Connection.js'; +import { Protocol } from 'devtools-protocol'; +import { EvaluateHandleFn, SerializableOrJSHandle } from './EvalTypes.js'; + +/** + * @internal + */ +type ConsoleAPICalledCallback = ( + eventType: string, + handles: JSHandle[], + trace: Protocol.Runtime.StackTrace +) => void; + +/** + * @internal + */ +type ExceptionThrownCallback = ( + details: Protocol.Runtime.ExceptionDetails +) => void; +type JSHandleFactory = (obj: Protocol.Runtime.RemoteObject) => JSHandle; + +/** + * The WebWorker class represents a + * {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API | WebWorker}. + * + * @remarks + * The events `workercreated` and `workerdestroyed` are emitted on the page + * object to signal the worker lifecycle. + * + * @example + * ```js + * page.on('workercreated', worker => console.log('Worker created: ' + worker.url())); + * page.on('workerdestroyed', worker => console.log('Worker destroyed: ' + worker.url())); + * + * console.log('Current workers:'); + * for (const worker of page.workers()) { + * console.log(' ' + worker.url()); + * } + * ``` + * + * @public + */ +export class WebWorker extends EventEmitter { + _client: CDPSession; + _url: string; + _executionContextPromise: Promise<ExecutionContext>; + _executionContextCallback: (value: ExecutionContext) => void; + + /** + * + * @internal + */ + constructor( + client: CDPSession, + url: string, + consoleAPICalled: ConsoleAPICalledCallback, + exceptionThrown: ExceptionThrownCallback + ) { + super(); + this._client = client; + this._url = url; + this._executionContextPromise = new Promise<ExecutionContext>( + (x) => (this._executionContextCallback = x) + ); + + let jsHandleFactory: JSHandleFactory; + this._client.once('Runtime.executionContextCreated', async (event) => { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + jsHandleFactory = (remoteObject) => + new JSHandle(executionContext, client, remoteObject); + const executionContext = new ExecutionContext( + client, + event.context, + null + ); + this._executionContextCallback(executionContext); + }); + + // This might fail if the target is closed before we recieve all execution contexts. + this._client.send('Runtime.enable').catch(debugError); + this._client.on('Runtime.consoleAPICalled', (event) => + consoleAPICalled( + event.type, + event.args.map(jsHandleFactory), + event.stackTrace + ) + ); + this._client.on('Runtime.exceptionThrown', (exception) => + exceptionThrown(exception.exceptionDetails) + ); + } + + /** + * @returns The URL of this web worker. + */ + url(): string { + return this._url; + } + + /** + * Returns the ExecutionContext the WebWorker runs in + * @returns The ExecutionContext the web worker runs in. + */ + async executionContext(): Promise<ExecutionContext> { + return this._executionContextPromise; + } + + /** + * If the function passed to the `worker.evaluate` returns a Promise, then + * `worker.evaluate` would wait for the promise to resolve and return its + * value. If the function passed to the `worker.evaluate` returns a + * non-serializable value, then `worker.evaluate` resolves to `undefined`. + * DevTools Protocol also supports transferring some additional values that + * are not serializable by `JSON`: `-0`, `NaN`, `Infinity`, `-Infinity`, and + * bigint literals. + * Shortcut for `await worker.executionContext()).evaluate(pageFunction, ...args)`. + * + * @param pageFunction - Function to be evaluated in the worker context. + * @param args - Arguments to pass to `pageFunction`. + * @returns Promise which resolves to the return value of `pageFunction`. + */ + async evaluate<ReturnType extends any>( + pageFunction: Function | string, + ...args: any[] + ): Promise<ReturnType> { + return (await this._executionContextPromise).evaluate<ReturnType>( + pageFunction, + ...args + ); + } + + /** + * The only difference between `worker.evaluate` and `worker.evaluateHandle` + * is that `worker.evaluateHandle` returns in-page object (JSHandle). If the + * function passed to the `worker.evaluateHandle` returns a [Promise], then + * `worker.evaluateHandle` would wait for the promise to resolve and return + * its value. Shortcut for + * `await worker.executionContext()).evaluateHandle(pageFunction, ...args)` + * + * @param pageFunction - Function to be evaluated in the page context. + * @param args - Arguments to pass to `pageFunction`. + * @returns Promise which resolves to the return value of `pageFunction`. + */ + async evaluateHandle<HandlerType extends JSHandle = JSHandle>( + pageFunction: EvaluateHandleFn, + ...args: SerializableOrJSHandle[] + ): Promise<JSHandle> { + return (await this._executionContextPromise).evaluateHandle<HandlerType>( + pageFunction, + ...args + ); + } +} diff --git a/remote/test/puppeteer/src/common/assert.ts b/remote/test/puppeteer/src/common/assert.ts new file mode 100644 index 0000000000..6ba090ce26 --- /dev/null +++ b/remote/test/puppeteer/src/common/assert.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Asserts that the given value is truthy. + * @param value + * @param message - the error message to throw if the value is not truthy. + */ +export const assert = (value: unknown, message?: string): void => { + if (!value) throw new Error(message); +}; diff --git a/remote/test/puppeteer/src/common/fetch.ts b/remote/test/puppeteer/src/common/fetch.ts new file mode 100644 index 0000000000..ae4b65c45f --- /dev/null +++ b/remote/test/puppeteer/src/common/fetch.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isNode } from '../environment.js'; + +/* Use the global version if we're in the browser, else load the node-fetch module. */ +export const getFetch = async (): Promise<typeof fetch> => { + return isNode ? await import('node-fetch') : globalThis.fetch; +}; diff --git a/remote/test/puppeteer/src/common/helper.ts b/remote/test/puppeteer/src/common/helper.ts new file mode 100644 index 0000000000..d8ca9b4ef4 --- /dev/null +++ b/remote/test/puppeteer/src/common/helper.ts @@ -0,0 +1,389 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TimeoutError } from './Errors.js'; +import { debug } from './Debug.js'; +import { CDPSession } from './Connection.js'; +import { Protocol } from 'devtools-protocol'; +import { CommonEventEmitter } from './EventEmitter.js'; +import { assert } from './assert.js'; +import { isNode } from '../environment.js'; + +export const debugError = debug('puppeteer:error'); + +function getExceptionMessage( + exceptionDetails: Protocol.Runtime.ExceptionDetails +): string { + if (exceptionDetails.exception) + return ( + exceptionDetails.exception.description || exceptionDetails.exception.value + ); + let message = exceptionDetails.text; + if (exceptionDetails.stackTrace) { + for (const callframe of exceptionDetails.stackTrace.callFrames) { + const location = + callframe.url + + ':' + + callframe.lineNumber + + ':' + + callframe.columnNumber; + const functionName = callframe.functionName || '<anonymous>'; + message += `\n at ${functionName} (${location})`; + } + } + return message; +} + +function valueFromRemoteObject( + remoteObject: Protocol.Runtime.RemoteObject +): any { + assert(!remoteObject.objectId, 'Cannot extract value when objectId is given'); + if (remoteObject.unserializableValue) { + if (remoteObject.type === 'bigint' && typeof BigInt !== 'undefined') + return BigInt(remoteObject.unserializableValue.replace('n', '')); + switch (remoteObject.unserializableValue) { + case '-0': + return -0; + case 'NaN': + return NaN; + case 'Infinity': + return Infinity; + case '-Infinity': + return -Infinity; + default: + throw new Error( + 'Unsupported unserializable value: ' + + remoteObject.unserializableValue + ); + } + } + return remoteObject.value; +} + +async function releaseObject( + client: CDPSession, + remoteObject: Protocol.Runtime.RemoteObject +): Promise<void> { + if (!remoteObject.objectId) return; + await client + .send('Runtime.releaseObject', { objectId: remoteObject.objectId }) + .catch((error) => { + // Exceptions might happen in case of a page been navigated or closed. + // Swallow these since they are harmless and we don't leak anything in this case. + debugError(error); + }); +} + +export interface PuppeteerEventListener { + emitter: CommonEventEmitter; + eventName: string | symbol; + handler: (...args: any[]) => void; +} + +function addEventListener( + emitter: CommonEventEmitter, + eventName: string | symbol, + handler: (...args: any[]) => void +): PuppeteerEventListener { + emitter.on(eventName, handler); + return { emitter, eventName, handler }; +} + +function removeEventListeners( + listeners: Array<{ + emitter: CommonEventEmitter; + eventName: string | symbol; + handler: (...args: any[]) => void; + }> +): void { + for (const listener of listeners) + listener.emitter.removeListener(listener.eventName, listener.handler); + listeners.length = 0; +} + +function isString(obj: unknown): obj is string { + return typeof obj === 'string' || obj instanceof String; +} + +function isNumber(obj: unknown): obj is number { + return typeof obj === 'number' || obj instanceof Number; +} + +async function waitForEvent<T extends any>( + emitter: CommonEventEmitter, + eventName: string | symbol, + predicate: (event: T) => boolean, + timeout: number, + abortPromise: Promise<Error> +): Promise<T> { + let eventTimeout, resolveCallback, rejectCallback; + const promise = new Promise<T>((resolve, reject) => { + resolveCallback = resolve; + rejectCallback = reject; + }); + const listener = addEventListener(emitter, eventName, (event) => { + if (!predicate(event)) return; + resolveCallback(event); + }); + if (timeout) { + eventTimeout = setTimeout(() => { + rejectCallback( + new TimeoutError('Timeout exceeded while waiting for event') + ); + }, timeout); + } + function cleanup(): void { + removeEventListeners([listener]); + clearTimeout(eventTimeout); + } + const result = await Promise.race([promise, abortPromise]).then( + (r) => { + cleanup(); + return r; + }, + (error) => { + cleanup(); + throw error; + } + ); + if (result instanceof Error) throw result; + + return result; +} + +function evaluationString(fun: Function | string, ...args: unknown[]): string { + if (isString(fun)) { + assert(args.length === 0, 'Cannot evaluate a string with arguments'); + return fun; + } + + function serializeArgument(arg: unknown): string { + if (Object.is(arg, undefined)) return 'undefined'; + return JSON.stringify(arg); + } + + return `(${fun})(${args.map(serializeArgument).join(',')})`; +} + +function pageBindingInitString(type: string, name: string): string { + function addPageBinding(type: string, bindingName: string): void { + /* Cast window to any here as we're about to add properties to it + * via win[bindingName] which TypeScript doesn't like. + */ + const win = window as any; + const binding = win[bindingName]; + + win[bindingName] = (...args: unknown[]): Promise<unknown> => { + const me = window[bindingName]; + let callbacks = me.callbacks; + if (!callbacks) { + callbacks = new Map(); + me.callbacks = callbacks; + } + const seq = (me.lastSeq || 0) + 1; + me.lastSeq = seq; + const promise = new Promise((resolve, reject) => + callbacks.set(seq, { resolve, reject }) + ); + binding(JSON.stringify({ type, name: bindingName, seq, args })); + return promise; + }; + } + return evaluationString(addPageBinding, type, name); +} + +function pageBindingDeliverResultString( + name: string, + seq: number, + result: unknown +): string { + function deliverResult(name: string, seq: number, result: unknown): void { + window[name].callbacks.get(seq).resolve(result); + window[name].callbacks.delete(seq); + } + return evaluationString(deliverResult, name, seq, result); +} + +function pageBindingDeliverErrorString( + name: string, + seq: number, + message: string, + stack: string +): string { + function deliverError( + name: string, + seq: number, + message: string, + stack: string + ): void { + const error = new Error(message); + error.stack = stack; + window[name].callbacks.get(seq).reject(error); + window[name].callbacks.delete(seq); + } + return evaluationString(deliverError, name, seq, message, stack); +} + +function pageBindingDeliverErrorValueString( + name: string, + seq: number, + value: unknown +): string { + function deliverErrorValue(name: string, seq: number, value: unknown): void { + window[name].callbacks.get(seq).reject(value); + window[name].callbacks.delete(seq); + } + return evaluationString(deliverErrorValue, name, seq, value); +} + +function makePredicateString( + predicate: Function, + predicateQueryHandler?: Function +): string { + function checkWaitForOptions( + node: Node, + waitForVisible: boolean, + waitForHidden: boolean + ): Node | null | boolean { + if (!node) return waitForHidden; + if (!waitForVisible && !waitForHidden) return node; + const element = + node.nodeType === Node.TEXT_NODE ? node.parentElement : (node as Element); + + const style = window.getComputedStyle(element); + const isVisible = + style && style.visibility !== 'hidden' && hasVisibleBoundingBox(); + const success = + waitForVisible === isVisible || waitForHidden === !isVisible; + return success ? node : null; + + function hasVisibleBoundingBox(): boolean { + const rect = element.getBoundingClientRect(); + return !!(rect.top || rect.bottom || rect.width || rect.height); + } + } + const predicateQueryHandlerDef = predicateQueryHandler + ? `const predicateQueryHandler = ${predicateQueryHandler};` + : ''; + return ` + (() => { + ${predicateQueryHandlerDef} + const checkWaitForOptions = ${checkWaitForOptions}; + return (${predicate})(...args) + })() `; +} + +async function waitWithTimeout<T extends any>( + promise: Promise<T>, + taskName: string, + timeout: number +): Promise<T> { + let reject; + const timeoutError = new TimeoutError( + `waiting for ${taskName} failed: timeout ${timeout}ms exceeded` + ); + const timeoutPromise = new Promise<T>((resolve, x) => (reject = x)); + let timeoutTimer = null; + if (timeout) timeoutTimer = setTimeout(() => reject(timeoutError), timeout); + try { + return await Promise.race([promise, timeoutPromise]); + } finally { + if (timeoutTimer) clearTimeout(timeoutTimer); + } +} + +async function readProtocolStream( + client: CDPSession, + handle: string, + path?: string +): Promise<Buffer> { + if (!isNode && path) { + throw new Error('Cannot write to a path outside of Node.js environment.'); + } + + const fs = isNode ? await importFSModule() : null; + + let eof = false; + let fileHandle: import('fs').promises.FileHandle; + + if (path && fs) { + fileHandle = await fs.promises.open(path, 'w'); + } + const bufs = []; + while (!eof) { + const response = await client.send('IO.read', { handle }); + eof = response.eof; + const buf = Buffer.from( + response.data, + response.base64Encoded ? 'base64' : undefined + ); + bufs.push(buf); + if (path && fs) { + await fs.promises.writeFile(fileHandle, buf); + } + } + if (path) await fileHandle.close(); + await client.send('IO.close', { handle }); + let resultBuffer = null; + try { + resultBuffer = Buffer.concat(bufs); + } finally { + return resultBuffer; + } +} + +/** + * Loads the Node fs promises API. Needed because on Node 10.17 and below, + * fs.promises is experimental, and therefore not marked as enumerable. That + * means when TypeScript compiles an `import('fs')`, its helper doesn't spot the + * promises declaration and therefore on Node <10.17 you get an error as + * fs.promises is undefined in compiled TypeScript land. + * + * See https://github.com/puppeteer/puppeteer/issues/6548 for more details. + * + * Once Node 10 is no longer supported (April 2021) we can remove this and use + * `(await import('fs')).promises`. + */ +async function importFSModule(): Promise<typeof import('fs')> { + if (!isNode) { + throw new Error('Cannot load the fs module API outside of Node.'); + } + + const fs = await import('fs'); + if (fs.promises) { + return fs; + } + return fs.default; +} + +export const helper = { + evaluationString, + pageBindingInitString, + pageBindingDeliverResultString, + pageBindingDeliverErrorString, + pageBindingDeliverErrorValueString, + makePredicateString, + readProtocolStream, + waitWithTimeout, + waitForEvent, + isString, + isNumber, + importFSModule, + addEventListener, + removeEventListeners, + valueFromRemoteObject, + getExceptionMessage, + releaseObject, +}; diff --git a/remote/test/puppeteer/src/environment.ts b/remote/test/puppeteer/src/environment.ts new file mode 100644 index 0000000000..f7d869775b --- /dev/null +++ b/remote/test/puppeteer/src/environment.ts @@ -0,0 +1,17 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const isNode = !!(typeof process !== 'undefined' && process.version); diff --git a/remote/test/puppeteer/src/initialize-node.ts b/remote/test/puppeteer/src/initialize-node.ts new file mode 100644 index 0000000000..e533665403 --- /dev/null +++ b/remote/test/puppeteer/src/initialize-node.ts @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PuppeteerNode } from './node/Puppeteer.js'; +import { PUPPETEER_REVISIONS } from './revisions.js'; +import pkgDir from 'pkg-dir'; +import { Product } from './common/Product.js'; + +export const initializePuppeteerNode = (packageName: string): PuppeteerNode => { + const puppeteerRootDirectory = pkgDir.sync(__dirname); + + let preferredRevision = PUPPETEER_REVISIONS.chromium; + const isPuppeteerCore = packageName === 'puppeteer-core'; + // puppeteer-core ignores environment variables + const productName = isPuppeteerCore + ? undefined + : process.env.PUPPETEER_PRODUCT || + process.env.npm_config_puppeteer_product || + process.env.npm_package_config_puppeteer_product; + + if (!isPuppeteerCore && productName === 'firefox') + preferredRevision = PUPPETEER_REVISIONS.firefox; + + return new PuppeteerNode({ + projectRoot: puppeteerRootDirectory, + preferredRevision, + isPuppeteerCore, + productName: productName as Product, + }); +}; diff --git a/remote/test/puppeteer/src/initialize-web.ts b/remote/test/puppeteer/src/initialize-web.ts new file mode 100644 index 0000000000..4ec42ce3fe --- /dev/null +++ b/remote/test/puppeteer/src/initialize-web.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Puppeteer } from './common/Puppeteer.js'; + +export const initializePuppeteerWeb = (packageName: string): Puppeteer => { + const isPuppeteerCore = packageName === 'puppeteer-core'; + return new Puppeteer({ + isPuppeteerCore, + }); +}; diff --git a/remote/test/puppeteer/src/node-puppeteer-core.ts b/remote/test/puppeteer/src/node-puppeteer-core.ts new file mode 100644 index 0000000000..976dfd5715 --- /dev/null +++ b/remote/test/puppeteer/src/node-puppeteer-core.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { initializePuppeteerNode } from './initialize-node.js'; +import { isNode } from './environment.js'; + +if (!isNode) { + throw new Error('Cannot run puppeteer-core outside of Node.js'); +} + +const puppeteer = initializePuppeteerNode('puppeteer-core'); +export default puppeteer; diff --git a/remote/test/puppeteer/src/node.ts b/remote/test/puppeteer/src/node.ts new file mode 100644 index 0000000000..714f8c2b94 --- /dev/null +++ b/remote/test/puppeteer/src/node.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { initializePuppeteerNode } from './initialize-node.js'; +import { isNode } from './environment.js'; + +if (!isNode) { + throw new Error('Trying to run Puppeteer-Node in a web environment.'); +} +export default initializePuppeteerNode('puppeteer'); diff --git a/remote/test/puppeteer/src/node/BrowserFetcher.ts b/remote/test/puppeteer/src/node/BrowserFetcher.ts new file mode 100644 index 0000000000..4653cd230c --- /dev/null +++ b/remote/test/puppeteer/src/node/BrowserFetcher.ts @@ -0,0 +1,602 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as os from 'os'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as util from 'util'; +import * as childProcess from 'child_process'; +import * as https from 'https'; +import * as http from 'http'; + +import { Product } from '../common/Product.js'; +import extractZip from 'extract-zip'; +import { debug } from '../common/Debug.js'; +import { promisify } from 'util'; +import removeRecursive from 'rimraf'; +import * as URL from 'url'; +import ProxyAgent from 'https-proxy-agent'; +import { getProxyForUrl } from 'proxy-from-env'; +import { assert } from '../common/assert.js'; + +const debugFetcher = debug(`puppeteer:fetcher`); + +const downloadURLs = { + chrome: { + linux: '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip', + mac: '%s/chromium-browser-snapshots/Mac/%d/%s.zip', + win32: '%s/chromium-browser-snapshots/Win/%d/%s.zip', + win64: '%s/chromium-browser-snapshots/Win_x64/%d/%s.zip', + }, + firefox: { + linux: '%s/firefox-%s.en-US.%s-x86_64.tar.bz2', + mac: '%s/firefox-%s.en-US.%s.dmg', + win32: '%s/firefox-%s.en-US.%s.zip', + win64: '%s/firefox-%s.en-US.%s.zip', + }, +} as const; + +const browserConfig = { + chrome: { + host: 'https://storage.googleapis.com', + destination: '.local-chromium', + }, + firefox: { + host: + 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central', + destination: '.local-firefox', + }, +} as const; + +/** + * Supported platforms. + * @public + */ +export type Platform = 'linux' | 'mac' | 'win32' | 'win64'; + +function archiveName( + product: Product, + platform: Platform, + revision: string +): string { + if (product === 'chrome') { + if (platform === 'linux') return 'chrome-linux'; + if (platform === 'mac') return 'chrome-mac'; + if (platform === 'win32' || platform === 'win64') { + // Windows archive name changed at r591479. + return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32'; + } + } else if (product === 'firefox') { + return platform; + } +} + +/** + * @internal + */ +function downloadURL( + product: Product, + platform: Platform, + host: string, + revision: string +): string { + const url = util.format( + downloadURLs[product][platform], + host, + revision, + archiveName(product, platform, revision) + ); + return url; +} + +/** + * @internal + */ +function handleArm64(): void { + fs.stat('/usr/bin/chromium-browser', function (err, stats) { + if (stats === undefined) { + console.error(`The chromium binary is not available for arm64: `); + console.error(`If you are on Ubuntu, you can install with: `); + console.error(`\n apt-get install chromium-browser\n`); + throw new Error(); + } + }); +} +const readdirAsync = promisify(fs.readdir.bind(fs)); +const mkdirAsync = promisify(fs.mkdir.bind(fs)); +const unlinkAsync = promisify(fs.unlink.bind(fs)); +const chmodAsync = promisify(fs.chmod.bind(fs)); + +function existsAsync(filePath: string): Promise<boolean> { + return new Promise((resolve) => { + fs.access(filePath, (err) => resolve(!err)); + }); +} + +/** + * @public + */ +export interface BrowserFetcherOptions { + platform?: Platform; + product?: string; + path?: string; + host?: string; +} + +/** + * @public + */ +export interface BrowserFetcherRevisionInfo { + folderPath: string; + executablePath: string; + url: string; + local: boolean; + revision: string; + product: string; +} +/** + * BrowserFetcher can download and manage different versions of Chromium and Firefox. + * + * @remarks + * BrowserFetcher operates on revision strings that specify a precise version of Chromium, e.g. `"533271"`. Revision strings can be obtained from {@link http://omahaproxy.appspot.com/ | omahaproxy.appspot.com}. + * In the Firefox case, BrowserFetcher downloads Firefox Nightly and + * operates on version numbers such as `"75"`. + * + * @example + * An example of using BrowserFetcher to download a specific version of Chromium + * and running Puppeteer against it: + * + * ```js + * const browserFetcher = puppeteer.createBrowserFetcher(); + * const revisionInfo = await browserFetcher.download('533271'); + * const browser = await puppeteer.launch({executablePath: revisionInfo.executablePath}) + * ``` + * + * **NOTE** BrowserFetcher is not designed to work concurrently with other + * instances of BrowserFetcher that share the same downloads directory. + * + * @public + */ + +export class BrowserFetcher { + private _product: Product; + private _downloadsFolder: string; + private _downloadHost: string; + private _platform: Platform; + + /** + * @internal + */ + constructor(projectRoot: string, options: BrowserFetcherOptions = {}) { + this._product = (options.product || 'chrome').toLowerCase() as Product; + assert( + this._product === 'chrome' || this._product === 'firefox', + `Unknown product: "${options.product}"` + ); + + this._downloadsFolder = + options.path || + path.join(projectRoot, browserConfig[this._product].destination); + this._downloadHost = options.host || browserConfig[this._product].host; + this.setPlatform(options.platform); + assert( + downloadURLs[this._product][this._platform], + 'Unsupported platform: ' + this._platform + ); + } + + private setPlatform(platformFromOptions?: Platform): void { + if (platformFromOptions) { + this._platform = platformFromOptions; + return; + } + + const platform = os.platform(); + if (platform === 'darwin') this._platform = 'mac'; + else if (platform === 'linux') this._platform = 'linux'; + else if (platform === 'win32') + this._platform = os.arch() === 'x64' ? 'win64' : 'win32'; + else assert(this._platform, 'Unsupported platform: ' + os.platform()); + } + + /** + * @returns Returns the current `Platform`. + */ + platform(): Platform { + return this._platform; + } + + /** + * @returns Returns the current `Product`. + */ + product(): Product { + return this._product; + } + + /** + * @returns The download host being used. + */ + host(): string { + return this._downloadHost; + } + + /** + * Initiates a HEAD request to check if the revision is available. + * @remarks + * This method is affected by the current `product`. + * @param revision - The revision to check availability for. + * @returns A promise that resolves to `true` if the revision could be downloaded + * from the host. + */ + canDownload(revision: string): Promise<boolean> { + const url = downloadURL( + this._product, + this._platform, + this._downloadHost, + revision + ); + return new Promise((resolve) => { + const request = httpRequest(url, 'HEAD', (response) => { + resolve(response.statusCode === 200); + }); + request.on('error', (error) => { + console.error(error); + resolve(false); + }); + }); + } + + /** + * Initiates a GET request to download the revision from the host. + * @remarks + * This method is affected by the current `product`. + * @param revision - The revision to download. + * @param progressCallback - A function that will be called with two arguments: + * How many bytes have been downloaded and the total number of bytes of the download. + * @returns A promise with revision information when the revision is downloaded + * and extracted. + */ + async download( + revision: string, + progressCallback: (x: number, y: number) => void = (): void => {} + ): Promise<BrowserFetcherRevisionInfo> { + const url = downloadURL( + this._product, + this._platform, + this._downloadHost, + revision + ); + const fileName = url.split('/').pop(); + const archivePath = path.join(this._downloadsFolder, fileName); + const outputPath = this._getFolderPath(revision); + if (await existsAsync(outputPath)) return this.revisionInfo(revision); + if (!(await existsAsync(this._downloadsFolder))) + await mkdirAsync(this._downloadsFolder); + if (os.arch() === 'arm64') { + handleArm64(); + return; + } + try { + await downloadFile(url, archivePath, progressCallback); + await install(archivePath, outputPath); + } finally { + if (await existsAsync(archivePath)) await unlinkAsync(archivePath); + } + const revisionInfo = this.revisionInfo(revision); + if (revisionInfo) await chmodAsync(revisionInfo.executablePath, 0o755); + return revisionInfo; + } + + /** + * @remarks + * This method is affected by the current `product`. + * @returns A promise with a list of all revision strings (for the current `product`) + * available locally on disk. + */ + async localRevisions(): Promise<string[]> { + if (!(await existsAsync(this._downloadsFolder))) return []; + const fileNames = await readdirAsync(this._downloadsFolder); + return fileNames + .map((fileName) => parseFolderPath(this._product, fileName)) + .filter((entry) => entry && entry.platform === this._platform) + .map((entry) => entry.revision); + } + + /** + * @remarks + * This method is affected by the current `product`. + * @param revision - A revision to remove for the current `product`. + * @returns A promise that resolves when the revision has been removes or + * throws if the revision has not been downloaded. + */ + async remove(revision: string): Promise<void> { + const folderPath = this._getFolderPath(revision); + assert( + await existsAsync(folderPath), + `Failed to remove: revision ${revision} is not downloaded` + ); + await new Promise((fulfill) => removeRecursive(folderPath, fulfill)); + } + + /** + * @param revision - The revision to get info for. + * @returns The revision info for the given revision. + */ + revisionInfo(revision: string): BrowserFetcherRevisionInfo { + const folderPath = this._getFolderPath(revision); + let executablePath = ''; + if (this._product === 'chrome') { + if (this._platform === 'mac') + executablePath = path.join( + folderPath, + archiveName(this._product, this._platform, revision), + 'Chromium.app', + 'Contents', + 'MacOS', + 'Chromium' + ); + else if (this._platform === 'linux') + executablePath = path.join( + folderPath, + archiveName(this._product, this._platform, revision), + 'chrome' + ); + else if (this._platform === 'win32' || this._platform === 'win64') + executablePath = path.join( + folderPath, + archiveName(this._product, this._platform, revision), + 'chrome.exe' + ); + else throw new Error('Unsupported platform: ' + this._platform); + } else if (this._product === 'firefox') { + if (this._platform === 'mac') + executablePath = path.join( + folderPath, + 'Firefox Nightly.app', + 'Contents', + 'MacOS', + 'firefox' + ); + else if (this._platform === 'linux') + executablePath = path.join(folderPath, 'firefox', 'firefox'); + else if (this._platform === 'win32' || this._platform === 'win64') + executablePath = path.join(folderPath, 'firefox', 'firefox.exe'); + else throw new Error('Unsupported platform: ' + this._platform); + } else { + throw new Error('Unsupported product: ' + this._product); + } + const url = downloadURL( + this._product, + this._platform, + this._downloadHost, + revision + ); + const local = fs.existsSync(folderPath); + debugFetcher({ + revision, + executablePath, + folderPath, + local, + url, + product: this._product, + }); + return { + revision, + executablePath, + folderPath, + local, + url, + product: this._product, + }; + } + + /** + * @internal + */ + _getFolderPath(revision: string): string { + return path.join(this._downloadsFolder, this._platform + '-' + revision); + } +} + +function parseFolderPath( + product: Product, + folderPath: string +): { product: string; platform: string; revision: string } | null { + const name = path.basename(folderPath); + const splits = name.split('-'); + if (splits.length !== 2) return null; + const [platform, revision] = splits; + if (!downloadURLs[product][platform]) return null; + return { product, platform, revision }; +} + +/** + * @internal + */ +function downloadFile( + url: string, + destinationPath: string, + progressCallback: (x: number, y: number) => void +): Promise<void> { + debugFetcher(`Downloading binary from ${url}`); + let fulfill, reject; + let downloadedBytes = 0; + let totalBytes = 0; + + const promise = new Promise<void>((x, y) => { + fulfill = x; + reject = y; + }); + + const request = httpRequest(url, 'GET', (response) => { + if (response.statusCode !== 200) { + const error = new Error( + `Download failed: server returned code ${response.statusCode}. URL: ${url}` + ); + // consume response data to free up memory + response.resume(); + reject(error); + return; + } + const file = fs.createWriteStream(destinationPath); + file.on('finish', () => fulfill()); + file.on('error', (error) => reject(error)); + response.pipe(file); + totalBytes = parseInt( + /** @type {string} */ response.headers['content-length'], + 10 + ); + if (progressCallback) response.on('data', onData); + }); + request.on('error', (error) => reject(error)); + return promise; + + function onData(chunk: string): void { + downloadedBytes += chunk.length; + progressCallback(downloadedBytes, totalBytes); + } +} + +function install(archivePath: string, folderPath: string): Promise<unknown> { + debugFetcher(`Installing ${archivePath} to ${folderPath}`); + if (archivePath.endsWith('.zip')) + return extractZip(archivePath, { dir: folderPath }); + else if (archivePath.endsWith('.tar.bz2')) + return extractTar(archivePath, folderPath); + else if (archivePath.endsWith('.dmg')) + return mkdirAsync(folderPath).then(() => + installDMG(archivePath, folderPath) + ); + else throw new Error(`Unsupported archive format: ${archivePath}`); +} + +/** + * @internal + */ +function extractTar(tarPath: string, folderPath: string): Promise<unknown> { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const tar = require('tar-fs'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const bzip = require('unbzip2-stream'); + return new Promise((fulfill, reject) => { + const tarStream = tar.extract(folderPath); + tarStream.on('error', reject); + tarStream.on('finish', fulfill); + const readStream = fs.createReadStream(tarPath); + readStream.pipe(bzip()).pipe(tarStream); + }); +} + +/** + * @internal + */ +function installDMG(dmgPath: string, folderPath: string): Promise<void> { + let mountPath; + + function mountAndCopy(fulfill: () => void, reject: (Error) => void): void { + const mountCommand = `hdiutil attach -nobrowse -noautoopen "${dmgPath}"`; + childProcess.exec(mountCommand, (err, stdout) => { + if (err) return reject(err); + const volumes = stdout.match(/\/Volumes\/(.*)/m); + if (!volumes) + return reject(new Error(`Could not find volume path in ${stdout}`)); + mountPath = volumes[0]; + readdirAsync(mountPath) + .then((fileNames) => { + const appName = fileNames.filter( + (item) => typeof item === 'string' && item.endsWith('.app') + )[0]; + if (!appName) + return reject(new Error(`Cannot find app in ${mountPath}`)); + const copyPath = path.join(mountPath, appName); + debugFetcher(`Copying ${copyPath} to ${folderPath}`); + childProcess.exec(`cp -R "${copyPath}" "${folderPath}"`, (err) => { + if (err) reject(err); + else fulfill(); + }); + }) + .catch(reject); + }); + } + + function unmount(): void { + if (!mountPath) return; + const unmountCommand = `hdiutil detach "${mountPath}" -quiet`; + debugFetcher(`Unmounting ${mountPath}`); + childProcess.exec(unmountCommand, (err) => { + if (err) console.error(`Error unmounting dmg: ${err}`); + }); + } + + return new Promise<void>(mountAndCopy) + .catch((error) => { + console.error(error); + }) + .finally(unmount); +} + +function httpRequest( + url: string, + method: string, + response: (x: http.IncomingMessage) => void +): http.ClientRequest { + const urlParsed = URL.parse(url); + + type Options = Partial<URL.UrlWithStringQuery> & { + method?: string; + agent?: ProxyAgent; + rejectUnauthorized?: boolean; + }; + + let options: Options = { + ...urlParsed, + method, + }; + + const proxyURL = getProxyForUrl(url); + if (proxyURL) { + if (url.startsWith('http:')) { + const proxy = URL.parse(proxyURL); + options = { + path: options.href, + host: proxy.hostname, + port: proxy.port, + }; + } else { + const parsedProxyURL = URL.parse(proxyURL); + + const proxyOptions = { + ...parsedProxyURL, + secureProxy: parsedProxyURL.protocol === 'https:', + } as ProxyAgent.HttpsProxyAgentOptions; + + options.agent = new ProxyAgent(proxyOptions); + options.rejectUnauthorized = false; + } + } + + const requestCallback = (res: http.IncomingMessage): void => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) + httpRequest(res.headers.location, method, response); + else response(res); + }; + const request = + options.protocol === 'https:' + ? https.request(options, requestCallback) + : http.request(options, requestCallback); + request.end(); + return request; +} diff --git a/remote/test/puppeteer/src/node/BrowserRunner.ts b/remote/test/puppeteer/src/node/BrowserRunner.ts new file mode 100644 index 0000000000..e7e11bb143 --- /dev/null +++ b/remote/test/puppeteer/src/node/BrowserRunner.ts @@ -0,0 +1,257 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { debug } from '../common/Debug.js'; + +import removeFolder from 'rimraf'; +import * as childProcess from 'child_process'; +import { assert } from '../common/assert.js'; +import { helper, debugError } from '../common/helper.js'; +import { LaunchOptions } from './LaunchOptions.js'; +import { Connection } from '../common/Connection.js'; +import { NodeWebSocketTransport as WebSocketTransport } from '../node/NodeWebSocketTransport.js'; +import { PipeTransport } from './PipeTransport.js'; +import * as readline from 'readline'; +import { TimeoutError } from '../common/Errors.js'; +import { promisify } from 'util'; + +const removeFolderAsync = promisify(removeFolder); +const debugLauncher = debug('puppeteer:launcher'); +const PROCESS_ERROR_EXPLANATION = `Puppeteer was unable to kill the process which ran the browser binary. +This means that, on future Puppeteer launches, Puppeteer might not be able to launch the browser. +Please check your open processes and ensure that the browser processes that Puppeteer launched have been killed. +If you think this is a bug, please report it on the Puppeteer issue tracker.`; + +export class BrowserRunner { + private _executablePath: string; + private _processArguments: string[]; + private _tempDirectory?: string; + + proc = null; + connection = null; + + private _closed = true; + private _listeners = []; + private _processClosing: Promise<void>; + + constructor( + executablePath: string, + processArguments: string[], + tempDirectory?: string + ) { + this._executablePath = executablePath; + this._processArguments = processArguments; + this._tempDirectory = tempDirectory; + } + + start(options: LaunchOptions): void { + const { + handleSIGINT, + handleSIGTERM, + handleSIGHUP, + dumpio, + env, + pipe, + } = options; + let stdio: Array<'ignore' | 'pipe'> = ['pipe', 'pipe', 'pipe']; + if (pipe) { + if (dumpio) stdio = ['ignore', 'pipe', 'pipe', 'pipe', 'pipe']; + else stdio = ['ignore', 'ignore', 'ignore', 'pipe', 'pipe']; + } + assert(!this.proc, 'This process has previously been started.'); + debugLauncher( + `Calling ${this._executablePath} ${this._processArguments.join(' ')}` + ); + this.proc = childProcess.spawn( + this._executablePath, + this._processArguments, + { + // On non-windows platforms, `detached: true` makes child process a + // leader of a new process group, making it possible to kill child + // process tree with `.kill(-pid)` command. @see + // https://nodejs.org/api/child_process.html#child_process_options_detached + detached: process.platform !== 'win32', + env, + stdio, + } + ); + if (dumpio) { + this.proc.stderr.pipe(process.stderr); + this.proc.stdout.pipe(process.stdout); + } + this._closed = false; + this._processClosing = new Promise((fulfill) => { + this.proc.once('exit', () => { + this._closed = true; + // Cleanup as processes exit. + if (this._tempDirectory) { + removeFolderAsync(this._tempDirectory) + .then(() => fulfill()) + .catch((error) => console.error(error)); + } else { + fulfill(); + } + }); + }); + this._listeners = [ + helper.addEventListener(process, 'exit', this.kill.bind(this)), + ]; + if (handleSIGINT) + this._listeners.push( + helper.addEventListener(process, 'SIGINT', () => { + this.kill(); + process.exit(130); + }) + ); + if (handleSIGTERM) + this._listeners.push( + helper.addEventListener(process, 'SIGTERM', this.close.bind(this)) + ); + if (handleSIGHUP) + this._listeners.push( + helper.addEventListener(process, 'SIGHUP', this.close.bind(this)) + ); + } + + close(): Promise<void> { + if (this._closed) return Promise.resolve(); + if (this._tempDirectory) { + this.kill(); + } else if (this.connection) { + // Attempt to close the browser gracefully + this.connection.send('Browser.close').catch((error) => { + debugError(error); + this.kill(); + }); + } + // Cleanup this listener last, as that makes sure the full callback runs. If we + // perform this earlier, then the previous function calls would not happen. + helper.removeEventListeners(this._listeners); + return this._processClosing; + } + + kill(): void { + // Attempt to remove temporary profile directory to avoid littering. + try { + removeFolder.sync(this._tempDirectory); + } catch (error) {} + + // If the process failed to launch (for example if the browser executable path + // is invalid), then the process does not get a pid assigned. A call to + // `proc.kill` would error, as the `pid` to-be-killed can not be found. + if (this.proc && this.proc.pid && !this.proc.killed) { + try { + this.proc.kill('SIGKILL'); + } catch (error) { + throw new Error( + `${PROCESS_ERROR_EXPLANATION}\nError cause: ${error.stack}` + ); + } + } + // Cleanup this listener last, as that makes sure the full callback runs. If we + // perform this earlier, then the previous function calls would not happen. + helper.removeEventListeners(this._listeners); + } + + async setupConnection(options: { + usePipe?: boolean; + timeout: number; + slowMo: number; + preferredRevision: string; + }): Promise<Connection> { + const { usePipe, timeout, slowMo, preferredRevision } = options; + if (!usePipe) { + const browserWSEndpoint = await waitForWSEndpoint( + this.proc, + timeout, + preferredRevision + ); + const transport = await WebSocketTransport.create(browserWSEndpoint); + this.connection = new Connection(browserWSEndpoint, transport, slowMo); + } else { + // stdio was assigned during start(), and the 'pipe' option there adds the + // 4th and 5th items to stdio array + const { 3: pipeWrite, 4: pipeRead } = this.proc.stdio; + const transport = new PipeTransport( + pipeWrite as NodeJS.WritableStream, + pipeRead as NodeJS.ReadableStream + ); + this.connection = new Connection('', transport, slowMo); + } + return this.connection; + } +} + +function waitForWSEndpoint( + browserProcess: childProcess.ChildProcess, + timeout: number, + preferredRevision: string +): Promise<string> { + return new Promise((resolve, reject) => { + const rl = readline.createInterface({ input: browserProcess.stderr }); + let stderr = ''; + const listeners = [ + helper.addEventListener(rl, 'line', onLine), + helper.addEventListener(rl, 'close', () => onClose()), + helper.addEventListener(browserProcess, 'exit', () => onClose()), + helper.addEventListener(browserProcess, 'error', (error) => + onClose(error) + ), + ]; + const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0; + + /** + * @param {!Error=} error + */ + function onClose(error?: Error): void { + cleanup(); + reject( + new Error( + [ + 'Failed to launch the browser process!' + + (error ? ' ' + error.message : ''), + stderr, + '', + 'TROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md', + '', + ].join('\n') + ) + ); + } + + function onTimeout(): void { + cleanup(); + reject( + new TimeoutError( + `Timed out after ${timeout} ms while trying to connect to the browser! Only Chrome at revision r${preferredRevision} is guaranteed to work.` + ) + ); + } + + function onLine(line: string): void { + stderr += line + '\n'; + const match = line.match(/^DevTools listening on (ws:\/\/.*)$/); + if (!match) return; + cleanup(); + resolve(match[1]); + } + + function cleanup(): void { + if (timeoutId) clearTimeout(timeoutId); + helper.removeEventListeners(listeners); + } + }); +} diff --git a/remote/test/puppeteer/src/node/LaunchOptions.ts b/remote/test/puppeteer/src/node/LaunchOptions.ts new file mode 100644 index 0000000000..0eb99b6980 --- /dev/null +++ b/remote/test/puppeteer/src/node/LaunchOptions.ts @@ -0,0 +1,42 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Launcher options that only apply to Chrome. + * + * @public + */ +export interface ChromeArgOptions { + headless?: boolean; + args?: string[]; + userDataDir?: string; + devtools?: boolean; +} + +/** + * Generic launch options that can be passed when launching any browser. + * @public + */ +export interface LaunchOptions { + executablePath?: string; + ignoreDefaultArgs?: boolean | string[]; + handleSIGINT?: boolean; + handleSIGTERM?: boolean; + handleSIGHUP?: boolean; + timeout?: number; + dumpio?: boolean; + env?: Record<string, string | undefined>; + pipe?: boolean; +} diff --git a/remote/test/puppeteer/src/node/Launcher.ts b/remote/test/puppeteer/src/node/Launcher.ts new file mode 100644 index 0000000000..8ca62db389 --- /dev/null +++ b/remote/test/puppeteer/src/node/Launcher.ts @@ -0,0 +1,673 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as os from 'os'; +import * as path from 'path'; +import * as fs from 'fs'; + +import { BrowserFetcher } from './BrowserFetcher.js'; +import { Browser } from '../common/Browser.js'; +import { BrowserRunner } from './BrowserRunner.js'; +import { promisify } from 'util'; + +const mkdtempAsync = promisify(fs.mkdtemp); +const writeFileAsync = promisify(fs.writeFile); + +import { ChromeArgOptions, LaunchOptions } from './LaunchOptions.js'; +import { BrowserOptions } from '../common/BrowserConnector.js'; +import { Product } from '../common/Product.js'; + +/** + * Describes a launcher - a class that is able to create and launch a browser instance. + * @public + */ +export interface ProductLauncher { + launch(object); + executablePath: () => string; + defaultArgs(object); + product: Product; +} + +/** + * @internal + */ +class ChromeLauncher implements ProductLauncher { + _projectRoot: string; + _preferredRevision: string; + _isPuppeteerCore: boolean; + + constructor( + projectRoot: string, + preferredRevision: string, + isPuppeteerCore: boolean + ) { + this._projectRoot = projectRoot; + this._preferredRevision = preferredRevision; + this._isPuppeteerCore = isPuppeteerCore; + } + + async launch( + options: LaunchOptions & ChromeArgOptions & BrowserOptions = {} + ): Promise<Browser> { + const { + ignoreDefaultArgs = false, + args = [], + dumpio = false, + executablePath = null, + pipe = false, + env = process.env, + handleSIGINT = true, + handleSIGTERM = true, + handleSIGHUP = true, + ignoreHTTPSErrors = false, + defaultViewport = { width: 800, height: 600 }, + slowMo = 0, + timeout = 30000, + } = options; + + const profilePath = path.join(os.tmpdir(), 'puppeteer_dev_chrome_profile-'); + const chromeArguments = []; + if (!ignoreDefaultArgs) chromeArguments.push(...this.defaultArgs(options)); + else if (Array.isArray(ignoreDefaultArgs)) + chromeArguments.push( + ...this.defaultArgs(options).filter( + (arg) => !ignoreDefaultArgs.includes(arg) + ) + ); + else chromeArguments.push(...args); + + let temporaryUserDataDir = null; + + if ( + !chromeArguments.some((argument) => + argument.startsWith('--remote-debugging-') + ) + ) + chromeArguments.push( + pipe ? '--remote-debugging-pipe' : '--remote-debugging-port=0' + ); + if (!chromeArguments.some((arg) => arg.startsWith('--user-data-dir'))) { + temporaryUserDataDir = await mkdtempAsync(profilePath); + chromeArguments.push(`--user-data-dir=${temporaryUserDataDir}`); + } + + let chromeExecutable = executablePath; + if (os.arch() === 'arm64') { + chromeExecutable = '/usr/bin/chromium-browser'; + } else if (!executablePath) { + const { missingText, executablePath } = resolveExecutablePath(this); + if (missingText) throw new Error(missingText); + chromeExecutable = executablePath; + } + + const usePipe = chromeArguments.includes('--remote-debugging-pipe'); + const runner = new BrowserRunner( + chromeExecutable, + chromeArguments, + temporaryUserDataDir + ); + runner.start({ + handleSIGHUP, + handleSIGTERM, + handleSIGINT, + dumpio, + env, + pipe: usePipe, + }); + + try { + const connection = await runner.setupConnection({ + usePipe, + timeout, + slowMo, + preferredRevision: this._preferredRevision, + }); + const browser = await Browser.create( + connection, + [], + ignoreHTTPSErrors, + defaultViewport, + runner.proc, + runner.close.bind(runner) + ); + await browser.waitForTarget((t) => t.type() === 'page'); + return browser; + } catch (error) { + runner.kill(); + throw error; + } + } + + /** + * @param {!Launcher.ChromeArgOptions=} options + * @returns {!Array<string>} + */ + defaultArgs(options: ChromeArgOptions = {}): string[] { + const chromeArguments = [ + '--disable-background-networking', + '--enable-features=NetworkService,NetworkServiceInProcess', + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-breakpad', + '--disable-client-side-phishing-detection', + '--disable-component-extensions-with-background-pages', + '--disable-default-apps', + '--disable-dev-shm-usage', + '--disable-extensions', + '--disable-features=TranslateUI', + '--disable-hang-monitor', + '--disable-ipc-flooding-protection', + '--disable-popup-blocking', + '--disable-prompt-on-repost', + '--disable-renderer-backgrounding', + '--disable-sync', + '--force-color-profile=srgb', + '--metrics-recording-only', + '--no-first-run', + '--enable-automation', + '--password-store=basic', + '--use-mock-keychain', + // TODO(sadym): remove '--enable-blink-features=IdleDetection' + // once IdleDetection is turned on by default. + '--enable-blink-features=IdleDetection', + ]; + const { + devtools = false, + headless = !devtools, + args = [], + userDataDir = null, + } = options; + if (userDataDir) + chromeArguments.push(`--user-data-dir=${path.resolve(userDataDir)}`); + if (devtools) chromeArguments.push('--auto-open-devtools-for-tabs'); + if (headless) { + chromeArguments.push('--headless', '--hide-scrollbars', '--mute-audio'); + } + if (args.every((arg) => arg.startsWith('-'))) + chromeArguments.push('about:blank'); + chromeArguments.push(...args); + return chromeArguments; + } + + executablePath(): string { + return resolveExecutablePath(this).executablePath; + } + + get product(): Product { + return 'chrome'; + } +} + +/** + * @internal + */ +class FirefoxLauncher implements ProductLauncher { + _projectRoot: string; + _preferredRevision: string; + _isPuppeteerCore: boolean; + + constructor( + projectRoot: string, + preferredRevision: string, + isPuppeteerCore: boolean + ) { + this._projectRoot = projectRoot; + this._preferredRevision = preferredRevision; + this._isPuppeteerCore = isPuppeteerCore; + } + + async launch( + options: LaunchOptions & + ChromeArgOptions & + BrowserOptions & { + extraPrefsFirefox?: { [x: string]: unknown }; + } = {} + ): Promise<Browser> { + const { + ignoreDefaultArgs = false, + args = [], + dumpio = false, + executablePath = null, + pipe = false, + env = process.env, + handleSIGINT = true, + handleSIGTERM = true, + handleSIGHUP = true, + ignoreHTTPSErrors = false, + defaultViewport = { width: 800, height: 600 }, + slowMo = 0, + timeout = 30000, + extraPrefsFirefox = {}, + } = options; + + const firefoxArguments = []; + if (!ignoreDefaultArgs) firefoxArguments.push(...this.defaultArgs(options)); + else if (Array.isArray(ignoreDefaultArgs)) + firefoxArguments.push( + ...this.defaultArgs(options).filter( + (arg) => !ignoreDefaultArgs.includes(arg) + ) + ); + else firefoxArguments.push(...args); + + if ( + !firefoxArguments.some((argument) => + argument.startsWith('--remote-debugging-') + ) + ) + firefoxArguments.push('--remote-debugging-port=0'); + + let temporaryUserDataDir = null; + + if ( + !firefoxArguments.includes('-profile') && + !firefoxArguments.includes('--profile') + ) { + temporaryUserDataDir = await this._createProfile(extraPrefsFirefox); + firefoxArguments.push('--profile'); + firefoxArguments.push(temporaryUserDataDir); + } + + await this._updateRevision(); + let firefoxExecutable = executablePath; + if (!executablePath) { + const { missingText, executablePath } = resolveExecutablePath(this); + if (missingText) throw new Error(missingText); + firefoxExecutable = executablePath; + } + + const runner = new BrowserRunner( + firefoxExecutable, + firefoxArguments, + temporaryUserDataDir + ); + runner.start({ + handleSIGHUP, + handleSIGTERM, + handleSIGINT, + dumpio, + env, + pipe, + }); + + try { + const connection = await runner.setupConnection({ + usePipe: pipe, + timeout, + slowMo, + preferredRevision: this._preferredRevision, + }); + const browser = await Browser.create( + connection, + [], + ignoreHTTPSErrors, + defaultViewport, + runner.proc, + runner.close.bind(runner) + ); + await browser.waitForTarget((t) => t.type() === 'page'); + return browser; + } catch (error) { + runner.kill(); + throw error; + } + } + + executablePath(): string { + return resolveExecutablePath(this).executablePath; + } + + async _updateRevision(): Promise<void> { + // replace 'latest' placeholder with actual downloaded revision + if (this._preferredRevision === 'latest') { + const browserFetcher = new BrowserFetcher(this._projectRoot, { + product: this.product, + }); + const localRevisions = await browserFetcher.localRevisions(); + if (localRevisions[0]) this._preferredRevision = localRevisions[0]; + } + } + + get product(): Product { + return 'firefox'; + } + + defaultArgs(options: ChromeArgOptions = {}): string[] { + const firefoxArguments = ['--no-remote', '--foreground']; + if (os.platform().startsWith('win')) { + firefoxArguments.push('--wait-for-browser'); + } + const { + devtools = false, + headless = !devtools, + args = [], + userDataDir = null, + } = options; + if (userDataDir) { + firefoxArguments.push('--profile'); + firefoxArguments.push(userDataDir); + } + if (headless) firefoxArguments.push('--headless'); + if (devtools) firefoxArguments.push('--devtools'); + if (args.every((arg) => arg.startsWith('-'))) + firefoxArguments.push('about:blank'); + firefoxArguments.push(...args); + return firefoxArguments; + } + + async _createProfile(extraPrefs: { [x: string]: unknown }): Promise<string> { + const profilePath = await mkdtempAsync( + path.join(os.tmpdir(), 'puppeteer_dev_firefox_profile-') + ); + const prefsJS = []; + const userJS = []; + const server = 'dummy.test'; + const defaultPreferences = { + // Make sure Shield doesn't hit the network. + 'app.normandy.api_url': '', + // Disable Firefox old build background check + 'app.update.checkInstallTime': false, + // Disable automatically upgrading Firefox + 'app.update.disabledForTesting': true, + + // Increase the APZ content response timeout to 1 minute + 'apz.content_response_timeout': 60000, + + // Prevent various error message on the console + // jest-puppeteer asserts that no error message is emitted by the console + 'browser.contentblocking.features.standard': + '-tp,tpPrivate,cookieBehavior0,-cm,-fp', + + // Enable the dump function: which sends messages to the system + // console + // https://bugzilla.mozilla.org/show_bug.cgi?id=1543115 + 'browser.dom.window.dump.enabled': true, + // Disable topstories + 'browser.newtabpage.activity-stream.feeds.system.topstories': false, + // Always display a blank page + 'browser.newtabpage.enabled': false, + // Background thumbnails in particular cause grief: and disabling + // thumbnails in general cannot hurt + 'browser.pagethumbnails.capturing_disabled': true, + + // Disable safebrowsing components. + 'browser.safebrowsing.blockedURIs.enabled': false, + 'browser.safebrowsing.downloads.enabled': false, + 'browser.safebrowsing.malware.enabled': false, + 'browser.safebrowsing.passwords.enabled': false, + 'browser.safebrowsing.phishing.enabled': false, + + // Disable updates to search engines. + 'browser.search.update': false, + // Do not restore the last open set of tabs if the browser has crashed + 'browser.sessionstore.resume_from_crash': false, + // Skip check for default browser on startup + 'browser.shell.checkDefaultBrowser': false, + + // Disable newtabpage + 'browser.startup.homepage': 'about:blank', + // Do not redirect user when a milstone upgrade of Firefox is detected + 'browser.startup.homepage_override.mstone': 'ignore', + // Start with a blank page about:blank + 'browser.startup.page': 0, + + // Do not allow background tabs to be zombified on Android: otherwise for + // tests that open additional tabs: the test harness tab itself might get + // unloaded + 'browser.tabs.disableBackgroundZombification': false, + // Do not warn when closing all other open tabs + 'browser.tabs.warnOnCloseOtherTabs': false, + // Do not warn when multiple tabs will be opened + 'browser.tabs.warnOnOpen': false, + + // Disable the UI tour. + 'browser.uitour.enabled': false, + // Turn off search suggestions in the location bar so as not to trigger + // network connections. + 'browser.urlbar.suggest.searches': false, + // Disable first run splash page on Windows 10 + 'browser.usedOnWindows10.introURL': '', + // Do not warn on quitting Firefox + 'browser.warnOnQuit': false, + + // Defensively disable data reporting systems + 'datareporting.healthreport.documentServerURI': `http://${server}/dummy/healthreport/`, + 'datareporting.healthreport.logging.consoleEnabled': false, + 'datareporting.healthreport.service.enabled': false, + 'datareporting.healthreport.service.firstRun': false, + 'datareporting.healthreport.uploadEnabled': false, + + // Do not show datareporting policy notifications which can interfere with tests + 'datareporting.policy.dataSubmissionEnabled': false, + 'datareporting.policy.dataSubmissionPolicyBypassNotification': true, + + // DevTools JSONViewer sometimes fails to load dependencies with its require.js. + // This doesn't affect Puppeteer but spams console (Bug 1424372) + 'devtools.jsonview.enabled': false, + + // Disable popup-blocker + 'dom.disable_open_during_load': false, + + // Enable the support for File object creation in the content process + // Required for |Page.setFileInputFiles| protocol method. + 'dom.file.createInChild': true, + + // Disable the ProcessHangMonitor + 'dom.ipc.reportProcessHangs': false, + + // Disable slow script dialogues + 'dom.max_chrome_script_run_time': 0, + 'dom.max_script_run_time': 0, + + // Only load extensions from the application and user profile + // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION + 'extensions.autoDisableScopes': 0, + 'extensions.enabledScopes': 5, + + // Disable metadata caching for installed add-ons by default + 'extensions.getAddons.cache.enabled': false, + + // Disable installing any distribution extensions or add-ons. + 'extensions.installDistroAddons': false, + + // Disabled screenshots extension + 'extensions.screenshots.disabled': true, + + // Turn off extension updates so they do not bother tests + 'extensions.update.enabled': false, + + // Turn off extension updates so they do not bother tests + 'extensions.update.notifyUser': false, + + // Make sure opening about:addons will not hit the network + 'extensions.webservice.discoverURL': `http://${server}/dummy/discoveryURL`, + + // Force disable Fission until the Remote Agent is compatible + 'fission.autostart': false, + + // Allow the application to have focus even it runs in the background + 'focusmanager.testmode': true, + // Disable useragent updates + 'general.useragent.updates.enabled': false, + // Always use network provider for geolocation tests so we bypass the + // macOS dialog raised by the corelocation provider + 'geo.provider.testing': true, + // Do not scan Wifi + 'geo.wifi.scan': false, + // No hang monitor + 'hangmonitor.timeout': 0, + // Show chrome errors and warnings in the error console + 'javascript.options.showInConsole': true, + + // Disable download and usage of OpenH264: and Widevine plugins + 'media.gmp-manager.updateEnabled': false, + // Prevent various error message on the console + // jest-puppeteer asserts that no error message is emitted by the console + 'network.cookie.cookieBehavior': 0, + + // Disable experimental feature that is only available in Nightly + 'network.cookie.sameSite.laxByDefault': false, + + // Do not prompt for temporary redirects + 'network.http.prompt-temp-redirect': false, + + // Disable speculative connections so they are not reported as leaking + // when they are hanging around + 'network.http.speculative-parallel-limit': 0, + + // Do not automatically switch between offline and online + 'network.manage-offline-status': false, + + // Make sure SNTP requests do not hit the network + 'network.sntp.pools': server, + + // Disable Flash. + 'plugin.state.flash': 0, + + 'privacy.trackingprotection.enabled': false, + + // Enable Remote Agent + // https://bugzilla.mozilla.org/show_bug.cgi?id=1544393 + 'remote.enabled': true, + + // Don't do network connections for mitm priming + 'security.certerrors.mitm.priming.enabled': false, + // Local documents have access to all other local documents, + // including directory listings + 'security.fileuri.strict_origin_policy': false, + // Do not wait for the notification button security delay + 'security.notification_enable_delay': 0, + + // Ensure blocklist updates do not hit the network + 'services.settings.server': `http://${server}/dummy/blocklist/`, + + // Do not automatically fill sign-in forms with known usernames and + // passwords + 'signon.autofillForms': false, + // Disable password capture, so that tests that include forms are not + // influenced by the presence of the persistent doorhanger notification + 'signon.rememberSignons': false, + + // Disable first-run welcome page + 'startup.homepage_welcome_url': 'about:blank', + + // Disable first-run welcome page + 'startup.homepage_welcome_url.additional': '', + + // Disable browser animations (tabs, fullscreen, sliding alerts) + 'toolkit.cosmeticAnimations.enabled': false, + + // Prevent starting into safe mode after application crashes + 'toolkit.startup.max_resumed_crashes': -1, + }; + + Object.assign(defaultPreferences, extraPrefs); + for (const [key, value] of Object.entries(defaultPreferences)) + userJS.push( + `user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});` + ); + await writeFileAsync(path.join(profilePath, 'user.js'), userJS.join('\n')); + await writeFileAsync( + path.join(profilePath, 'prefs.js'), + prefsJS.join('\n') + ); + return profilePath; + } +} + +function resolveExecutablePath( + launcher: ChromeLauncher | FirefoxLauncher +): { executablePath: string; missingText?: string } { + let downloadPath: string; + // puppeteer-core doesn't take into account PUPPETEER_* env variables. + if (!launcher._isPuppeteerCore) { + const executablePath = + process.env.PUPPETEER_EXECUTABLE_PATH || + process.env.npm_config_puppeteer_executable_path || + process.env.npm_package_config_puppeteer_executable_path; + if (executablePath) { + const missingText = !fs.existsSync(executablePath) + ? 'Tried to use PUPPETEER_EXECUTABLE_PATH env variable to launch browser but did not find any executable at: ' + + executablePath + : null; + return { executablePath, missingText }; + } + downloadPath = + process.env.PUPPETEER_DOWNLOAD_PATH || + process.env.npm_config_puppeteer_download_path || + process.env.npm_package_config_puppeteer_download_path; + } + const browserFetcher = new BrowserFetcher(launcher._projectRoot, { + product: launcher.product, + path: downloadPath, + }); + if (!launcher._isPuppeteerCore && launcher.product === 'chrome') { + const revision = process.env['PUPPETEER_CHROMIUM_REVISION']; + if (revision) { + const revisionInfo = browserFetcher.revisionInfo(revision); + const missingText = !revisionInfo.local + ? 'Tried to use PUPPETEER_CHROMIUM_REVISION env variable to launch browser but did not find executable at: ' + + revisionInfo.executablePath + : null; + return { executablePath: revisionInfo.executablePath, missingText }; + } + } + const revisionInfo = browserFetcher.revisionInfo(launcher._preferredRevision); + const missingText = !revisionInfo.local + ? `Could not find browser revision ${launcher._preferredRevision}. Run "PUPPETEER_PRODUCT=firefox npm install" or "PUPPETEER_PRODUCT=firefox yarn install" to download a supported Firefox browser binary.` + : null; + return { executablePath: revisionInfo.executablePath, missingText }; +} + +/** + * @internal + */ +export default function Launcher( + projectRoot: string, + preferredRevision: string, + isPuppeteerCore: boolean, + product?: string +): ProductLauncher { + // puppeteer-core doesn't take into account PUPPETEER_* env variables. + if (!product && !isPuppeteerCore) + product = + process.env.PUPPETEER_PRODUCT || + process.env.npm_config_puppeteer_product || + process.env.npm_package_config_puppeteer_product; + switch (product) { + case 'firefox': + return new FirefoxLauncher( + projectRoot, + preferredRevision, + isPuppeteerCore + ); + case 'chrome': + default: + if (typeof product !== 'undefined' && product !== 'chrome') { + /* The user gave us an incorrect product name + * we'll default to launching Chrome, but log to the console + * to let the user know (they've probably typoed). + */ + console.warn( + `Warning: unknown product name ${product}. Falling back to chrome.` + ); + } + return new ChromeLauncher( + projectRoot, + preferredRevision, + isPuppeteerCore + ); + } +} diff --git a/remote/test/puppeteer/src/node/NodeWebSocketTransport.ts b/remote/test/puppeteer/src/node/NodeWebSocketTransport.ts new file mode 100644 index 0000000000..d1077ae7c2 --- /dev/null +++ b/remote/test/puppeteer/src/node/NodeWebSocketTransport.ts @@ -0,0 +1,59 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConnectionTransport } from '../common/ConnectionTransport.js'; +import NodeWebSocket from 'ws'; + +export class NodeWebSocketTransport implements ConnectionTransport { + static create(url: string): Promise<NodeWebSocketTransport> { + return new Promise((resolve, reject) => { + const ws = new NodeWebSocket(url, [], { + perMessageDeflate: false, + maxPayload: 256 * 1024 * 1024, // 256Mb + }); + + ws.addEventListener('open', () => + resolve(new NodeWebSocketTransport(ws)) + ); + ws.addEventListener('error', reject); + }); + } + + private _ws: NodeWebSocket; + onmessage?: (message: string) => void; + onclose?: () => void; + + constructor(ws: NodeWebSocket) { + this._ws = ws; + this._ws.addEventListener('message', (event) => { + if (this.onmessage) this.onmessage.call(null, event.data); + }); + this._ws.addEventListener('close', () => { + if (this.onclose) this.onclose.call(null); + }); + // Silently ignore all errors - we don't know what to do with them. + this._ws.addEventListener('error', () => {}); + this.onmessage = null; + this.onclose = null; + } + + send(message: string): void { + this._ws.send(message); + } + + close(): void { + this._ws.close(); + } +} diff --git a/remote/test/puppeteer/src/node/PipeTransport.ts b/remote/test/puppeteer/src/node/PipeTransport.ts new file mode 100644 index 0000000000..e66c2c0b47 --- /dev/null +++ b/remote/test/puppeteer/src/node/PipeTransport.ts @@ -0,0 +1,80 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + helper, + debugError, + PuppeteerEventListener, +} from '../common/helper.js'; +import { ConnectionTransport } from '../common/ConnectionTransport.js'; + +export class PipeTransport implements ConnectionTransport { + _pipeWrite: NodeJS.WritableStream; + _pendingMessage: string; + _eventListeners: PuppeteerEventListener[]; + + onclose?: () => void; + onmessage?: () => void; + + constructor( + pipeWrite: NodeJS.WritableStream, + pipeRead: NodeJS.ReadableStream + ) { + this._pipeWrite = pipeWrite; + this._pendingMessage = ''; + this._eventListeners = [ + helper.addEventListener(pipeRead, 'data', (buffer) => + this._dispatch(buffer) + ), + helper.addEventListener(pipeRead, 'close', () => { + if (this.onclose) this.onclose.call(null); + }), + helper.addEventListener(pipeRead, 'error', debugError), + helper.addEventListener(pipeWrite, 'error', debugError), + ]; + this.onmessage = null; + this.onclose = null; + } + + send(message: string): void { + this._pipeWrite.write(message); + this._pipeWrite.write('\0'); + } + + _dispatch(buffer: Buffer): void { + let end = buffer.indexOf('\0'); + if (end === -1) { + this._pendingMessage += buffer.toString(); + return; + } + const message = this._pendingMessage + buffer.toString(undefined, 0, end); + if (this.onmessage) this.onmessage.call(null, message); + + let start = end + 1; + end = buffer.indexOf('\0', start); + while (end !== -1) { + if (this.onmessage) + this.onmessage.call(null, buffer.toString(undefined, start, end)); + start = end + 1; + end = buffer.indexOf('\0', start); + } + this._pendingMessage = buffer.toString(undefined, start); + } + + close(): void { + this._pipeWrite = null; + helper.removeEventListeners(this._eventListeners); + } +} diff --git a/remote/test/puppeteer/src/node/Puppeteer.ts b/remote/test/puppeteer/src/node/Puppeteer.ts new file mode 100644 index 0000000000..924d2fa96e --- /dev/null +++ b/remote/test/puppeteer/src/node/Puppeteer.ts @@ -0,0 +1,230 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Puppeteer, + CommonPuppeteerSettings, + ConnectOptions, +} from '../common/Puppeteer.js'; +import { BrowserFetcher, BrowserFetcherOptions } from './BrowserFetcher.js'; +import { LaunchOptions, ChromeArgOptions } from './LaunchOptions.js'; +import { BrowserOptions } from '../common/BrowserConnector.js'; +import { Browser } from '../common/Browser.js'; +import Launcher, { ProductLauncher } from './Launcher.js'; +import { PUPPETEER_REVISIONS } from '../revisions.js'; +import { Product } from '../common/Product.js'; + +/** + * Extends the main {@link Puppeteer} class with Node specific behaviour for fetching and + * downloading browsers. + * + * If you're using Puppeteer in a Node environment, this is the class you'll get + * when you run `require('puppeteer')` (or the equivalent ES `import`). + * + * @remarks + * + * The most common method to use is {@link PuppeteerNode.launch | launch}, which + * is used to launch and connect to a new browser instance. + * + * See {@link Puppeteer | the main Puppeteer class} for methods common to all + * environments, such as {@link Puppeteer.connect}. + * + * @example + * The following is a typical example of using Puppeteer to drive automation: + * ```js + * const puppeteer = require('puppeteer'); + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.goto('https://www.google.com'); + * // other actions... + * await browser.close(); + * })(); + * ``` + * + * Once you have created a `page` you have access to a large API to interact + * with the page, navigate, or find certain elements in that page. + * The {@link Page | `page` documentation} lists all the available methods. + * + * @public + */ +export class PuppeteerNode extends Puppeteer { + private _lazyLauncher: ProductLauncher; + private _projectRoot: string; + private __productName?: Product; + /** + * @internal + */ + _preferredRevision: string; + + /** + * @internal + */ + constructor( + settings: { + projectRoot: string; + preferredRevision: string; + productName?: Product; + } & CommonPuppeteerSettings + ) { + const { + projectRoot, + preferredRevision, + productName, + ...commonSettings + } = settings; + super(commonSettings); + this._projectRoot = projectRoot; + this.__productName = productName; + this._preferredRevision = preferredRevision; + } + + /** + * This method attaches Puppeteer to an existing browser instance. + * + * @remarks + * + * @param options - Set of configurable options to set on the browser. + * @returns Promise which resolves to browser instance. + */ + connect(options: ConnectOptions): Promise<Browser> { + if (options.product) this._productName = options.product; + return super.connect(options); + } + + /** + * @internal + */ + get _productName(): Product { + return this.__productName; + } + + // don't need any TSDoc here - because the getter is internal the setter is too. + set _productName(name: Product) { + if (this.__productName !== name) this._changedProduct = true; + this.__productName = name; + } + + /** + * Launches puppeteer and launches a browser instance with given arguments + * and options when specified. + * + * @remarks + * + * @example + * You can use `ignoreDefaultArgs` to filter out `--mute-audio` from default arguments: + * ```js + * const browser = await puppeteer.launch({ + * ignoreDefaultArgs: ['--mute-audio'] + * }); + * ``` + * + * **NOTE** Puppeteer can also be used to control the Chrome browser, + * but it works best with the version of Chromium it is bundled with. + * There is no guarantee it will work with any other version. + * Use `executablePath` option with extreme caution. + * If Google Chrome (rather than Chromium) is preferred, a {@link https://www.google.com/chrome/browser/canary.html | Chrome Canary} or {@link https://www.chromium.org/getting-involved/dev-channel | Dev Channel} build is suggested. + * In `puppeteer.launch([options])`, any mention of Chromium also applies to Chrome. + * See {@link https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/ | this article} for a description of the differences between Chromium and Chrome. {@link https://chromium.googlesource.com/chromium/src/+/lkgr/docs/chromium_browser_vs_google_chrome.md | This article} describes some differences for Linux users. + * + * @param options - Set of configurable options to set on the browser. + * @returns Promise which resolves to browser instance. + */ + launch( + options: LaunchOptions & + ChromeArgOptions & + BrowserOptions & { + product?: Product; + extraPrefsFirefox?: Record<string, unknown>; + } = {} + ): Promise<Browser> { + if (options.product) this._productName = options.product; + return this._launcher.launch(options); + } + + /** + * @remarks + * + * **NOTE** `puppeteer.executablePath()` is affected by the `PUPPETEER_EXECUTABLE_PATH` + * and `PUPPETEER_CHROMIUM_REVISION` environment variables. + * + * @returns A path where Puppeteer expects to find the bundled browser. + * The browser binary might not be there if the download was skipped with + * the `PUPPETEER_SKIP_DOWNLOAD` environment variable. + */ + executablePath(): string { + return this._launcher.executablePath(); + } + + /** + * @internal + */ + get _launcher(): ProductLauncher { + if ( + !this._lazyLauncher || + this._lazyLauncher.product !== this._productName || + this._changedProduct + ) { + switch (this._productName) { + case 'firefox': + this._preferredRevision = PUPPETEER_REVISIONS.firefox; + break; + case 'chrome': + default: + this._preferredRevision = PUPPETEER_REVISIONS.chromium; + } + this._changedProduct = false; + this._lazyLauncher = Launcher( + this._projectRoot, + this._preferredRevision, + this._isPuppeteerCore, + this._productName + ); + } + return this._lazyLauncher; + } + + /** + * The name of the browser that is under automation (`"chrome"` or `"firefox"`) + * + * @remarks + * The product is set by the `PUPPETEER_PRODUCT` environment variable or the `product` + * option in `puppeteer.launch([options])` and defaults to `chrome`. + * Firefox support is experimental. + */ + get product(): string { + return this._launcher.product; + } + + /** + * + * @param options - Set of configurable options to set on the browser. + * @returns The default flags that Chromium will be launched with. + */ + defaultArgs(options: ChromeArgOptions = {}): string[] { + return this._launcher.defaultArgs(options); + } + + /** + * @param options - Set of configurable options to specify the settings + * of the BrowserFetcher. + * @returns A new BrowserFetcher instance. + */ + createBrowserFetcher(options: BrowserFetcherOptions): BrowserFetcher { + return new BrowserFetcher(this._projectRoot, options); + } +} diff --git a/remote/test/puppeteer/src/node/install.ts b/remote/test/puppeteer/src/node/install.ts new file mode 100644 index 0000000000..41f2834d80 --- /dev/null +++ b/remote/test/puppeteer/src/node/install.ts @@ -0,0 +1,185 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import os from 'os'; +import https from 'https'; +import ProgressBar from 'progress'; +import puppeteer from '../node.js'; +import { PUPPETEER_REVISIONS } from '../revisions.js'; +import { PuppeteerNode } from './Puppeteer.js'; + +const supportedProducts = { + chrome: 'Chromium', + firefox: 'Firefox Nightly', +} as const; + +export async function downloadBrowser() { + const downloadHost = + process.env.PUPPETEER_DOWNLOAD_HOST || + process.env.npm_config_puppeteer_download_host || + process.env.npm_package_config_puppeteer_download_host; + const product = + process.env.PUPPETEER_PRODUCT || + process.env.npm_config_puppeteer_product || + process.env.npm_package_config_puppeteer_product || + 'chrome'; + const downloadPath = + process.env.PUPPETEER_DOWNLOAD_PATH || + process.env.npm_config_puppeteer_download_path || + process.env.npm_package_config_puppeteer_download_path; + const browserFetcher = (puppeteer as PuppeteerNode).createBrowserFetcher({ + product, + host: downloadHost, + path: downloadPath, + }); + const revision = await getRevision(); + await fetchBinary(revision); + + function getRevision() { + if (product === 'chrome') { + return ( + process.env.PUPPETEER_CHROMIUM_REVISION || + process.env.npm_config_puppeteer_chromium_revision || + PUPPETEER_REVISIONS.chromium + ); + } else if (product === 'firefox') { + (puppeteer as PuppeteerNode)._preferredRevision = + PUPPETEER_REVISIONS.firefox; + return getFirefoxNightlyVersion().catch((error) => { + console.error(error); + process.exit(1); + }); + } else { + throw new Error(`Unsupported product ${product}`); + } + } + + function fetchBinary(revision) { + const revisionInfo = browserFetcher.revisionInfo(revision); + + // Do nothing if the revision is already downloaded. + if (revisionInfo.local) { + logPolitely( + `${supportedProducts[product]} is already in ${revisionInfo.folderPath}; skipping download.` + ); + return; + } + + // Override current environment proxy settings with npm configuration, if any. + const NPM_HTTPS_PROXY = + process.env.npm_config_https_proxy || process.env.npm_config_proxy; + const NPM_HTTP_PROXY = + process.env.npm_config_http_proxy || process.env.npm_config_proxy; + const NPM_NO_PROXY = process.env.npm_config_no_proxy; + + if (NPM_HTTPS_PROXY) process.env.HTTPS_PROXY = NPM_HTTPS_PROXY; + if (NPM_HTTP_PROXY) process.env.HTTP_PROXY = NPM_HTTP_PROXY; + if (NPM_NO_PROXY) process.env.NO_PROXY = NPM_NO_PROXY; + + function onSuccess(localRevisions: string[]): void { + if (os.arch() !== 'arm64') { + logPolitely( + `${supportedProducts[product]} (${revisionInfo.revision}) downloaded to ${revisionInfo.folderPath}` + ); + } + localRevisions = localRevisions.filter( + (revision) => revision !== revisionInfo.revision + ); + const cleanupOldVersions = localRevisions.map((revision) => + browserFetcher.remove(revision) + ); + Promise.all([...cleanupOldVersions]); + } + + function onError(error: Error) { + console.error( + `ERROR: Failed to set up ${supportedProducts[product]} r${revision}! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.` + ); + console.error(error); + process.exit(1); + } + + let progressBar = null; + let lastDownloadedBytes = 0; + function onProgress(downloadedBytes, totalBytes) { + if (!progressBar) { + progressBar = new ProgressBar( + `Downloading ${ + supportedProducts[product] + } r${revision} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `, + { + complete: '=', + incomplete: ' ', + width: 20, + total: totalBytes, + } + ); + } + const delta = downloadedBytes - lastDownloadedBytes; + lastDownloadedBytes = downloadedBytes; + progressBar.tick(delta); + } + + return browserFetcher + .download(revisionInfo.revision, onProgress) + .then(() => browserFetcher.localRevisions()) + .then(onSuccess) + .catch(onError); + } + + function toMegabytes(bytes) { + const mb = bytes / 1024 / 1024; + return `${Math.round(mb * 10) / 10} Mb`; + } + + function getFirefoxNightlyVersion() { + const firefoxVersions = + 'https://product-details.mozilla.org/1.0/firefox_versions.json'; + + const promise = new Promise((resolve, reject) => { + let data = ''; + logPolitely( + `Requesting latest Firefox Nightly version from ${firefoxVersions}` + ); + https + .get(firefoxVersions, (r) => { + if (r.statusCode >= 400) + return reject(new Error(`Got status code ${r.statusCode}`)); + r.on('data', (chunk) => { + data += chunk; + }); + r.on('end', () => { + try { + const versions = JSON.parse(data); + return resolve(versions.FIREFOX_NIGHTLY); + } catch { + return reject(new Error('Firefox version not found')); + } + }); + }) + .on('error', reject); + }); + return promise; + } +} + +export function logPolitely(toBeLogged) { + const logLevel = process.env.npm_config_loglevel; + const logLevelDisplay = ['silent', 'error', 'warn'].indexOf(logLevel) > -1; + + // eslint-disable-next-line no-console + if (!logLevelDisplay) console.log(toBeLogged); +} diff --git a/remote/test/puppeteer/src/revisions.ts b/remote/test/puppeteer/src/revisions.ts new file mode 100644 index 0000000000..5e9169adb0 --- /dev/null +++ b/remote/test/puppeteer/src/revisions.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +type Revisions = Readonly<{ + readonly chromium: string; + readonly firefox: string; +}>; + +export const PUPPETEER_REVISIONS: Revisions = { + chromium: '818858', + firefox: 'latest', +}; diff --git a/remote/test/puppeteer/src/tsconfig.cjs.json b/remote/test/puppeteer/src/tsconfig.cjs.json new file mode 100644 index 0000000000..c144b956bf --- /dev/null +++ b/remote/test/puppeteer/src/tsconfig.cjs.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "../lib/cjs/puppeteer", + "module": "CommonJS" + }, + "references": [ + { "path": "../vendor/tsconfig.cjs.json"} + ] +} diff --git a/remote/test/puppeteer/src/tsconfig.esm.json b/remote/test/puppeteer/src/tsconfig.esm.json new file mode 100644 index 0000000000..487533061f --- /dev/null +++ b/remote/test/puppeteer/src/tsconfig.esm.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "../lib/esm/puppeteer", + "module": "esnext" + }, + "references": [ + { "path": "../vendor/tsconfig.esm.json"} + ] +} diff --git a/remote/test/puppeteer/src/web.ts b/remote/test/puppeteer/src/web.ts new file mode 100644 index 0000000000..a48a5cf14d --- /dev/null +++ b/remote/test/puppeteer/src/web.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { initializePuppeteerWeb } from './initialize-web.js'; +import { isNode } from './environment.js'; + +if (isNode) { + throw new Error('Trying to run Puppeteer-Web in a Node environment'); +} + +export default initializePuppeteerWeb('puppeteer'); diff --git a/remote/test/puppeteer/test-browser/connection.spec.js b/remote/test/puppeteer/test-browser/connection.spec.js new file mode 100644 index 0000000000..eef8fe3c21 --- /dev/null +++ b/remote/test/puppeteer/test-browser/connection.spec.js @@ -0,0 +1,55 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Connection } from '../lib/esm/puppeteer/common/Connection.js'; +import { BrowserWebSocketTransport } from '../lib/esm/puppeteer/common/BrowserWebSocketTransport.js'; +import puppeteer from '../lib/esm/puppeteer/web.js'; +import expect from '../node_modules/expect/build-es5/index.js'; +import { getWebSocketEndpoint } from './helper.js'; + +describe('creating a Connection', () => { + it('can create a real connection to the backend and send messages', async () => { + const wsUrl = getWebSocketEndpoint(); + const transport = await BrowserWebSocketTransport.create(wsUrl); + + const connection = new Connection(wsUrl, transport); + const result = await connection.send('Browser.getVersion'); + /* We can't expect exact results as the version of Chrome/CDP might change + * and we don't want flakey tests, so let's assert the structure, which is + * enough to confirm the result was recieved successfully. + */ + expect(result).toEqual({ + protocolVersion: expect.any(String), + jsVersion: expect.any(String), + revision: expect.any(String), + userAgent: expect.any(String), + product: expect.any(String), + }); + }); +}); + +describe('puppeteer.connect', () => { + it('can connect over websocket and make requests to the backend', async () => { + const wsUrl = getWebSocketEndpoint(); + const browser = await puppeteer.connect({ + browserWSEndpoint: wsUrl, + }); + + const version = await browser.version(); + const versionLooksCorrect = /.+Chrome\/\d{2}/.test(version); + expect(version).toEqual(expect.any(String)); + expect(versionLooksCorrect).toEqual(true); + }); +}); diff --git a/remote/test/puppeteer/test-browser/debug.spec.js b/remote/test/puppeteer/test-browser/debug.spec.js new file mode 100644 index 0000000000..971d3a5346 --- /dev/null +++ b/remote/test/puppeteer/test-browser/debug.spec.js @@ -0,0 +1,65 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { debug } from '../lib/esm/puppeteer/common/Debug.js'; +import expect from '../node_modules/expect/build-es5/index.js'; + +describe('debug', () => { + let originalLog; + let logs; + beforeEach(() => { + originalLog = console.log; + logs = []; + console.log = (...args) => { + logs.push(args); + }; + }); + + afterEach(() => { + console.log = originalLog; + }); + + it('should return a function', async () => { + expect(debug('foo')).toBeInstanceOf(Function); + }); + + it('does not log to the console if __PUPPETEER_DEBUG global is not set', async () => { + const debugFn = debug('foo'); + debugFn('lorem', 'ipsum'); + + expect(logs.length).toEqual(0); + }); + + it('logs to the console if __PUPPETEER_DEBUG global is set to *', async () => { + globalThis.__PUPPETEER_DEBUG = '*'; + const debugFn = debug('foo'); + debugFn('lorem', 'ipsum'); + + expect(logs.length).toEqual(1); + expect(logs).toEqual([['foo:', 'lorem', 'ipsum']]); + }); + + it('logs only messages matching the __PUPPETEER_DEBUG prefix', async () => { + globalThis.__PUPPETEER_DEBUG = 'foo'; + const debugFoo = debug('foo'); + const debugBar = debug('bar'); + debugFoo('a'); + debugBar('b'); + + expect(logs.length).toEqual(1); + expect(logs).toEqual([['foo:', 'a']]); + }); +}); diff --git a/remote/test/puppeteer/test-browser/helper.js b/remote/test/puppeteer/test-browser/helper.js new file mode 100644 index 0000000000..6cfbe934d5 --- /dev/null +++ b/remote/test/puppeteer/test-browser/helper.js @@ -0,0 +1,27 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Returns the web socket endpoint for the backend of the browser the tests run + * in. Used to create connections to that browser in Puppeteer for unit tests. + * + * It's available on window.__ENV__ because setup code in + * web-test-runner.config.js puts it there. If you're changing this code (or + * that code), make sure the other is updated accordingly. + */ +export function getWebSocketEndpoint() { + return window.__ENV__.wsEndpoint; +} diff --git a/remote/test/puppeteer/test/.eslintrc.js b/remote/test/puppeteer/test/.eslintrc.js new file mode 100644 index 0000000000..9d86da20e1 --- /dev/null +++ b/remote/test/puppeteer/test/.eslintrc.js @@ -0,0 +1,13 @@ +module.exports = { + rules: { + 'no-restricted-imports': [ + 'error', + { + /** The mocha tests run on the compiled output in the /lib directory + * so we should avoid importing from src. + */ + patterns: ['*src*'], + }, + ], + }, +}; diff --git a/remote/test/puppeteer/test/CDPSession.spec.ts b/remote/test/puppeteer/test/CDPSession.spec.ts new file mode 100644 index 0000000000..2ebf10fd96 --- /dev/null +++ b/remote/test/puppeteer/test/CDPSession.spec.ts @@ -0,0 +1,106 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { waitEvent } from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeChromeOnly, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describeChromeOnly('Target.createCDPSession', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should work', async () => { + const { page } = getTestState(); + + const client = await page.target().createCDPSession(); + + await Promise.all([ + client.send('Runtime.enable'), + client.send('Runtime.evaluate', { expression: 'window.foo = "bar"' }), + ]); + const foo = await page.evaluate(() => globalThis.foo); + expect(foo).toBe('bar'); + }); + it('should send events', async () => { + const { page, server } = getTestState(); + + const client = await page.target().createCDPSession(); + await client.send('Network.enable'); + const events = []; + client.on('Network.requestWillBeSent', (event) => events.push(event)); + await page.goto(server.EMPTY_PAGE); + expect(events.length).toBe(1); + }); + it('should enable and disable domains independently', async () => { + const { page } = getTestState(); + + const client = await page.target().createCDPSession(); + await client.send('Runtime.enable'); + await client.send('Debugger.enable'); + // JS coverage enables and then disables Debugger domain. + await page.coverage.startJSCoverage(); + await page.coverage.stopJSCoverage(); + // generate a script in page and wait for the event. + const [event] = await Promise.all([ + waitEvent(client, 'Debugger.scriptParsed'), + page.evaluate('//# sourceURL=foo.js'), + ]); + // expect events to be dispatched. + expect(event.url).toBe('foo.js'); + }); + it('should be able to detach session', async () => { + const { page } = getTestState(); + + const client = await page.target().createCDPSession(); + await client.send('Runtime.enable'); + const evalResponse = await client.send('Runtime.evaluate', { + expression: '1 + 2', + returnByValue: true, + }); + expect(evalResponse.result.value).toBe(3); + await client.detach(); + let error = null; + try { + await client.send('Runtime.evaluate', { + expression: '3 + 1', + returnByValue: true, + }); + } catch (error_) { + error = error_; + } + expect(error.message).toContain('Session closed.'); + }); + it('should throw nice errors', async () => { + const { page } = getTestState(); + + const client = await page.target().createCDPSession(); + const error = await theSourceOfTheProblems().catch((error) => error); + expect(error.stack).toContain('theSourceOfTheProblems'); + expect(error.message).toContain('ThisCommand.DoesNotExist'); + + async function theSourceOfTheProblems() { + // @ts-expect-error This fails in TS as it knows that command does not + // exist but we want to have this tests for our users who consume in JS + // not TS. + await client.send('ThisCommand.DoesNotExist'); + } + }); +}); diff --git a/remote/test/puppeteer/test/EventEmitter.spec.ts b/remote/test/puppeteer/test/EventEmitter.spec.ts new file mode 100644 index 0000000000..bf20e7fe8f --- /dev/null +++ b/remote/test/puppeteer/test/EventEmitter.spec.ts @@ -0,0 +1,170 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EventEmitter } from '../lib/cjs/puppeteer/common/EventEmitter.js'; +import sinon from 'sinon'; +import expect from 'expect'; + +describe('EventEmitter', () => { + let emitter; + + beforeEach(() => { + emitter = new EventEmitter(); + }); + + describe('on', () => { + const onTests = (methodName: 'on' | 'addListener'): void => { + it(`${methodName}: adds an event listener that is fired when the event is emitted`, () => { + const listener = sinon.spy(); + emitter[methodName]('foo', listener); + emitter.emit('foo'); + expect(listener.callCount).toEqual(1); + }); + + it(`${methodName} sends the event data to the handler`, () => { + const listener = sinon.spy(); + const data = {}; + emitter[methodName]('foo', listener); + emitter.emit('foo', data); + expect(listener.callCount).toEqual(1); + expect(listener.firstCall.args[0]).toBe(data); + }); + + it(`${methodName}: supports chaining`, () => { + const listener = sinon.spy(); + const returnValue = emitter[methodName]('foo', listener); + expect(returnValue).toBe(emitter); + }); + }; + onTests('on'); + // we support addListener for legacy reasons + onTests('addListener'); + }); + + describe('off', () => { + const offTests = (methodName: 'off' | 'removeListener'): void => { + it(`${methodName}: removes the listener so it is no longer called`, () => { + const listener = sinon.spy(); + emitter.on('foo', listener); + emitter.emit('foo'); + expect(listener.callCount).toEqual(1); + emitter.off('foo', listener); + emitter.emit('foo'); + expect(listener.callCount).toEqual(1); + }); + + it(`${methodName}: supports chaining`, () => { + const listener = sinon.spy(); + emitter.on('foo', listener); + const returnValue = emitter.off('foo', listener); + expect(returnValue).toBe(emitter); + }); + }; + offTests('off'); + // we support removeListener for legacy reasons + offTests('removeListener'); + }); + + describe('once', () => { + it('only calls the listener once and then removes it', () => { + const listener = sinon.spy(); + emitter.once('foo', listener); + emitter.emit('foo'); + expect(listener.callCount).toEqual(1); + emitter.emit('foo'); + expect(listener.callCount).toEqual(1); + }); + + it('supports chaining', () => { + const listener = sinon.spy(); + const returnValue = emitter.once('foo', listener); + expect(returnValue).toBe(emitter); + }); + }); + + describe('emit', () => { + it('calls all the listeners for an event', () => { + const listener1 = sinon.spy(); + const listener2 = sinon.spy(); + const listener3 = sinon.spy(); + emitter.on('foo', listener1).on('foo', listener2).on('bar', listener3); + + emitter.emit('foo'); + + expect(listener1.callCount).toEqual(1); + expect(listener2.callCount).toEqual(1); + expect(listener3.callCount).toEqual(0); + }); + + it('passes data through to the listener', () => { + const listener = sinon.spy(); + emitter.on('foo', listener); + const data = {}; + + emitter.emit('foo', data); + expect(listener.callCount).toEqual(1); + expect(listener.firstCall.args[0]).toBe(data); + }); + + it('returns true if the event has listeners', () => { + const listener = sinon.spy(); + emitter.on('foo', listener); + expect(emitter.emit('foo')).toBe(true); + }); + + it('returns false if the event has listeners', () => { + const listener = sinon.spy(); + emitter.on('foo', listener); + expect(emitter.emit('notFoo')).toBe(false); + }); + }); + + describe('listenerCount', () => { + it('returns the number of listeners for the given event', () => { + emitter.on('foo', () => {}); + emitter.on('foo', () => {}); + emitter.on('bar', () => {}); + expect(emitter.listenerCount('foo')).toEqual(2); + expect(emitter.listenerCount('bar')).toEqual(1); + expect(emitter.listenerCount('noListeners')).toEqual(0); + }); + }); + + describe('removeAllListeners', () => { + it('removes every listener from all events by default', () => { + emitter.on('foo', () => {}).on('bar', () => {}); + + emitter.removeAllListeners(); + expect(emitter.emit('foo')).toBe(false); + expect(emitter.emit('bar')).toBe(false); + }); + + it('returns the emitter for chaining', () => { + expect(emitter.removeAllListeners()).toBe(emitter); + }); + + it('can filter to remove only listeners for a given event name', () => { + emitter + .on('foo', () => {}) + .on('bar', () => {}) + .on('bar', () => {}); + + emitter.removeAllListeners('bar'); + expect(emitter.emit('foo')).toBe(true); + expect(emitter.emit('bar')).toBe(false); + }); + }); +}); diff --git a/remote/test/puppeteer/test/README.md b/remote/test/puppeteer/test/README.md new file mode 100644 index 0000000000..304e3e0b0a --- /dev/null +++ b/remote/test/puppeteer/test/README.md @@ -0,0 +1,88 @@ +# Puppeteer unit tests + +Unit tests in Puppeteer are written using [Mocha] as the test runner and [Expect] as the assertions library. + +## Test state + + +We have some common setup that runs before each test and is defined in `mocha-utils.js`. + +You can use the `getTestState` function to read state. It exposes the following that you can use in your tests. These will be reset/tidied between tests automatically for you: + +* `puppeteer`: an instance of the Puppeteer library. This is exactly what you'd get if you ran `require('puppeteer')`. +* `puppeteerPath`: the path to the root source file for Puppeteer. +* `defaultBrowserOptions`: the default options the Puppeteer browser is launched from in test mode, so tests can use them and override if required. +* `server`: a dummy test server instance (see `utils/testserver` for more). +* `httpsServer`: a dummy test server HTTPS instance (see `utils/testserver` for more). +* `isFirefox`: true if running in Firefox. +* `isChrome`: true if running Chromium. +* `isHeadless`: true if the test is in headless mode. + +If your test needs a browser instance, you can use the `setupTestBrowserHooks()` function which will automatically configure a browser that will be cleaned between each test suite run. You access this via `getTestState()`. + +If your test needs a Puppeteer page and context, you can use the `setupTestPageAndContextHooks()` function which will configure these. You can access `page` and `context` from `getTestState()` once you have done this. + +The best place to look is an existing test to see how they use the helpers. + +## Skipping tests in specific conditions + +Tests that are not expected to pass in Firefox can be skipped. You can skip an individual test by using `itFailsFirefox` rather than `it`. Similarly you can skip a describe block with `describeFailsFirefox`. + +There is also `describeChromeOnly` and `itChromeOnly` which will only execute the test if running in Chromium. Note that this is different from `describeFailsFirefox`: the goal is to get any `FailsFirefox` calls passing in Firefox, whereas `describeChromeOnly` should be used to test behaviour that will only ever apply in Chromium. + +There are also tests that assume a normal install flow, with browser binaries ending up in `.local-<browser>`, for example. Such tests are skipped with +`itOnlyRegularInstall` which checks `BINARY` and `PUPPETEER_ALT_INSTALL` environment variables. + +[Mocha]: https://mochajs.org/ +[Expect]: https://www.npmjs.com/package/expect + +## Running tests + +Despite being named 'unit', these are integration tests, making sure public API methods and events work as expected. + +- To run all tests: + +```bash +npm run unit +``` + +- __Important__: don't forget to first run TypeScript if you're testing local changes: + +```bash +npm run tsc && npm run unit +``` + +- To run a specific test, substitute the `it` with `it.only`: + +```js + ... + it.only('should work', async function() { + const {server, page} = getTestState(); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok).toBe(true); + }); +``` + +- To disable a specific test, substitute the `it` with `xit` (mnemonic rule: '*cross it*'): + +```js + ... + // Using "xit" to skip specific test + xit('should work', async function({server, page}) { + const {server, page} = getTestState(); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok).toBe(true); + }); +``` + +- To run tests in non-headless mode: + +```bash +HEADLESS=false npm run unit +``` + +- To run tests with custom browser executable: + +```bash +BINARY=<path-to-executable> npm run unit +``` diff --git a/remote/test/puppeteer/test/accessibility.spec.ts b/remote/test/puppeteer/test/accessibility.spec.ts new file mode 100644 index 0000000000..e941c683a4 --- /dev/null +++ b/remote/test/puppeteer/test/accessibility.spec.ts @@ -0,0 +1,520 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describeFailsFirefox('Accessibility', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should work', async () => { + const { page, isFirefox } = getTestState(); + + await page.setContent(` + <head> + <title>Accessibility Test</title> + </head> + <body> + <div>Hello World</div> + <h1>Inputs</h1> + <input placeholder="Empty input" autofocus /> + <input placeholder="readonly input" readonly /> + <input placeholder="disabled input" disabled /> + <input aria-label="Input with whitespace" value=" " /> + <input value="value only" /> + <input aria-placeholder="placeholder" value="and a value" /> + <div aria-hidden="true" id="desc">This is a description!</div> + <input aria-placeholder="placeholder" value="and a value" aria-describedby="desc" /> + <select> + <option>First Option</option> + <option>Second Option</option> + </select> + </body>`); + + await page.focus('[placeholder="Empty input"]'); + const golden = isFirefox + ? { + role: 'document', + name: 'Accessibility Test', + children: [ + { role: 'text leaf', name: 'Hello World' }, + { role: 'heading', name: 'Inputs', level: 1 }, + { role: 'entry', name: 'Empty input', focused: true }, + { role: 'entry', name: 'readonly input', readonly: true }, + { role: 'entry', name: 'disabled input', disabled: true }, + { role: 'entry', name: 'Input with whitespace', value: ' ' }, + { role: 'entry', name: '', value: 'value only' }, + { role: 'entry', name: '', value: 'and a value' }, // firefox doesn't use aria-placeholder for the name + { + role: 'entry', + name: '', + value: 'and a value', + description: 'This is a description!', + }, // and here + { + role: 'combobox', + name: '', + value: 'First Option', + haspopup: true, + children: [ + { + role: 'combobox option', + name: 'First Option', + selected: true, + }, + { role: 'combobox option', name: 'Second Option' }, + ], + }, + ], + } + : { + role: 'WebArea', + name: 'Accessibility Test', + children: [ + { role: 'text', name: 'Hello World' }, + { role: 'heading', name: 'Inputs', level: 1 }, + { role: 'textbox', name: 'Empty input', focused: true }, + { role: 'textbox', name: 'readonly input', readonly: true }, + { role: 'textbox', name: 'disabled input', disabled: true }, + { role: 'textbox', name: 'Input with whitespace', value: ' ' }, + { role: 'textbox', name: '', value: 'value only' }, + { role: 'textbox', name: 'placeholder', value: 'and a value' }, + { + role: 'textbox', + name: 'placeholder', + value: 'and a value', + description: 'This is a description!', + }, + { + role: 'combobox', + name: '', + value: 'First Option', + children: [ + { role: 'menuitem', name: 'First Option', selected: true }, + { role: 'menuitem', name: 'Second Option' }, + ], + }, + ], + }; + expect(await page.accessibility.snapshot()).toEqual(golden); + }); + it('should report uninteresting nodes', async () => { + const { page, isFirefox } = getTestState(); + + await page.setContent(`<textarea>hi</textarea>`); + await page.focus('textarea'); + const golden = isFirefox + ? { + role: 'entry', + name: '', + value: 'hi', + focused: true, + multiline: true, + children: [ + { + role: 'text leaf', + name: 'hi', + }, + ], + } + : { + role: 'textbox', + name: '', + value: 'hi', + focused: true, + multiline: true, + children: [ + { + role: 'generic', + name: '', + children: [ + { + role: 'text', + name: 'hi', + }, + ], + }, + ], + }; + expect( + findFocusedNode( + await page.accessibility.snapshot({ interestingOnly: false }) + ) + ).toEqual(golden); + }); + it('roledescription', async () => { + const { page } = getTestState(); + + await page.setContent( + '<div tabIndex=-1 aria-roledescription="foo">Hi</div>' + ); + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0].roledescription).toEqual('foo'); + }); + it('orientation', async () => { + const { page } = getTestState(); + + await page.setContent( + '<a href="" role="slider" aria-orientation="vertical">11</a>' + ); + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0].orientation).toEqual('vertical'); + }); + it('autocomplete', async () => { + const { page } = getTestState(); + + await page.setContent('<input type="number" aria-autocomplete="list" />'); + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0].autocomplete).toEqual('list'); + }); + it('multiselectable', async () => { + const { page } = getTestState(); + + await page.setContent( + '<div role="grid" tabIndex=-1 aria-multiselectable=true>hey</div>' + ); + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0].multiselectable).toEqual(true); + }); + it('keyshortcuts', async () => { + const { page } = getTestState(); + + await page.setContent( + '<div role="grid" tabIndex=-1 aria-keyshortcuts="foo">hey</div>' + ); + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0].keyshortcuts).toEqual('foo'); + }); + describe('filtering children of leaf nodes', function () { + it('should not report text nodes inside controls', async () => { + const { page, isFirefox } = getTestState(); + + await page.setContent(` + <div role="tablist"> + <div role="tab" aria-selected="true"><b>Tab1</b></div> + <div role="tab">Tab2</div> + </div>`); + const golden = isFirefox + ? { + role: 'document', + name: '', + children: [ + { + role: 'pagetab', + name: 'Tab1', + selected: true, + }, + { + role: 'pagetab', + name: 'Tab2', + }, + ], + } + : { + role: 'WebArea', + name: '', + children: [ + { + role: 'tab', + name: 'Tab1', + selected: true, + }, + { + role: 'tab', + name: 'Tab2', + }, + ], + }; + expect(await page.accessibility.snapshot()).toEqual(golden); + }); + it('rich text editable fields should have children', async () => { + const { page, isFirefox } = getTestState(); + + await page.setContent(` + <div contenteditable="true"> + Edit this image: <img src="fakeimage.png" alt="my fake image"> + </div>`); + const golden = isFirefox + ? { + role: 'section', + name: '', + children: [ + { + role: 'text leaf', + name: 'Edit this image: ', + }, + { + role: 'text', + name: 'my fake image', + }, + ], + } + : { + role: 'generic', + name: '', + value: 'Edit this image: ', + children: [ + { + role: 'text', + name: 'Edit this image:', + }, + { + role: 'img', + name: 'my fake image', + }, + ], + }; + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0]).toEqual(golden); + }); + it('rich text editable fields with role should have children', async () => { + const { page, isFirefox } = getTestState(); + + await page.setContent(` + <div contenteditable="true" role='textbox'> + Edit this image: <img src="fakeimage.png" alt="my fake image"> + </div>`); + const golden = isFirefox + ? { + role: 'entry', + name: '', + value: 'Edit this image: my fake image', + children: [ + { + role: 'text', + name: 'my fake image', + }, + ], + } + : { + role: 'textbox', + name: '', + value: 'Edit this image: ', + children: [ + { + role: 'text', + name: 'Edit this image:', + }, + { + role: 'img', + name: 'my fake image', + }, + ], + }; + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0]).toEqual(golden); + }); + + // Firefox does not support contenteditable="plaintext-only". + describeFailsFirefox('plaintext contenteditable', function () { + it('plain text field with role should not have children', async () => { + const { page } = getTestState(); + + await page.setContent(` + <div contenteditable="plaintext-only" role='textbox'>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>`); + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0]).toEqual({ + role: 'textbox', + name: '', + value: 'Edit this image:', + }); + }); + it('plain text field without role should not have content', async () => { + const { page } = getTestState(); + + await page.setContent(` + <div contenteditable="plaintext-only">Edit this image:<img src="fakeimage.png" alt="my fake image"></div>`); + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0]).toEqual({ + role: 'generic', + name: '', + }); + }); + it('plain text field with tabindex and without role should not have content', async () => { + const { page } = getTestState(); + + await page.setContent(` + <div contenteditable="plaintext-only" tabIndex=0>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>`); + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0]).toEqual({ + role: 'generic', + name: '', + }); + }); + }); + it('non editable textbox with role and tabIndex and label should not have children', async () => { + const { page, isFirefox } = getTestState(); + + await page.setContent(` + <div role="textbox" tabIndex=0 aria-checked="true" aria-label="my favorite textbox"> + this is the inner content + <img alt="yo" src="fakeimg.png"> + </div>`); + const golden = isFirefox + ? { + role: 'entry', + name: 'my favorite textbox', + value: 'this is the inner content yo', + } + : { + role: 'textbox', + name: 'my favorite textbox', + value: 'this is the inner content ', + }; + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0]).toEqual(golden); + }); + it('checkbox with and tabIndex and label should not have children', async () => { + const { page, isFirefox } = getTestState(); + + await page.setContent(` + <div role="checkbox" tabIndex=0 aria-checked="true" aria-label="my favorite checkbox"> + this is the inner content + <img alt="yo" src="fakeimg.png"> + </div>`); + const golden = isFirefox + ? { + role: 'checkbutton', + name: 'my favorite checkbox', + checked: true, + } + : { + role: 'checkbox', + name: 'my favorite checkbox', + checked: true, + }; + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0]).toEqual(golden); + }); + it('checkbox without label should not have children', async () => { + const { page, isFirefox } = getTestState(); + + await page.setContent(` + <div role="checkbox" aria-checked="true"> + this is the inner content + <img alt="yo" src="fakeimg.png"> + </div>`); + const golden = isFirefox + ? { + role: 'checkbutton', + name: 'this is the inner content yo', + checked: true, + } + : { + role: 'checkbox', + name: 'this is the inner content yo', + checked: true, + }; + const snapshot = await page.accessibility.snapshot(); + expect(snapshot.children[0]).toEqual(golden); + }); + + describe('root option', function () { + it('should work a button', async () => { + const { page } = getTestState(); + + await page.setContent(`<button>My Button</button>`); + + const button = await page.$('button'); + expect(await page.accessibility.snapshot({ root: button })).toEqual({ + role: 'button', + name: 'My Button', + }); + }); + it('should work an input', async () => { + const { page } = getTestState(); + + await page.setContent(`<input title="My Input" value="My Value">`); + + const input = await page.$('input'); + expect(await page.accessibility.snapshot({ root: input })).toEqual({ + role: 'textbox', + name: 'My Input', + value: 'My Value', + }); + }); + it('should work a menu', async () => { + const { page } = getTestState(); + + await page.setContent(` + <div role="menu" title="My Menu"> + <div role="menuitem">First Item</div> + <div role="menuitem">Second Item</div> + <div role="menuitem">Third Item</div> + </div> + `); + + const menu = await page.$('div[role="menu"]'); + expect(await page.accessibility.snapshot({ root: menu })).toEqual({ + role: 'menu', + name: 'My Menu', + children: [ + { role: 'menuitem', name: 'First Item' }, + { role: 'menuitem', name: 'Second Item' }, + { role: 'menuitem', name: 'Third Item' }, + ], + }); + }); + it('should return null when the element is no longer in DOM', async () => { + const { page } = getTestState(); + + await page.setContent(`<button>My Button</button>`); + const button = await page.$('button'); + await page.$eval('button', (button) => button.remove()); + expect(await page.accessibility.snapshot({ root: button })).toEqual( + null + ); + }); + it('should support the interestingOnly option', async () => { + const { page } = getTestState(); + + await page.setContent(`<div><button>My Button</button></div>`); + const div = await page.$('div'); + expect(await page.accessibility.snapshot({ root: div })).toEqual(null); + expect( + await page.accessibility.snapshot({ + root: div, + interestingOnly: false, + }) + ).toEqual({ + role: 'generic', + name: '', + children: [ + { + role: 'button', + name: 'My Button', + children: [{ role: 'text', name: 'My Button' }], + }, + ], + }); + }); + }); + }); + function findFocusedNode(node) { + if (node.focused) return node; + for (const child of node.children || []) { + const focusedChild = findFocusedNode(child); + if (focusedChild) return focusedChild; + } + return null; + } +}); diff --git a/remote/test/puppeteer/test/ariaqueryhandler.spec.ts b/remote/test/puppeteer/test/ariaqueryhandler.spec.ts new file mode 100644 index 0000000000..83f78fd611 --- /dev/null +++ b/remote/test/puppeteer/test/ariaqueryhandler.spec.ts @@ -0,0 +1,565 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeChromeOnly, +} from './mocha-utils'; // eslint-disable-line import/extensions + +import { ElementHandle } from '../lib/cjs/puppeteer/common/JSHandle.js'; +import utils from './utils.js'; + +describeChromeOnly('AriaQueryHandler', () => { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('parseAriaSelector', () => { + beforeEach(async () => { + const { page } = getTestState(); + await page.setContent( + '<button id="btn" role="button"> Submit button and some spaces </button>' + ); + }); + it('should find button', async () => { + const { page } = getTestState(); + const expectFound = async (button: ElementHandle) => { + const id = await button.evaluate((button: Element) => button.id); + expect(id).toBe('btn'); + }; + let button = await page.$( + 'aria/Submit button and some spaces[role="button"]' + ); + await expectFound(button); + button = await page.$( + 'aria/ Submit button and some spaces[role="button"]' + ); + await expectFound(button); + button = await page.$( + 'aria/Submit button and some spaces [role="button"]' + ); + await expectFound(button); + button = await page.$( + 'aria/Submit button and some spaces [ role = "button" ] ' + ); + await expectFound(button); + button = await page.$( + 'aria/[role="button"]Submit button and some spaces' + ); + await expectFound(button); + button = await page.$( + 'aria/Submit button [role="button"]and some spaces' + ); + await expectFound(button); + button = await page.$( + 'aria/[name=" Submit button and some spaces"][role="button"]' + ); + await expectFound(button); + button = await page.$( + 'aria/ignored[name="Submit button and some spaces"][role="button"]' + ); + await expectFound(button); + }); + }); + + describe('queryOne', () => { + it('should find button by role', async () => { + const { page } = getTestState(); + await page.setContent( + '<div id="div"><button id="btn" role="button">Submit</button></div>' + ); + const button = await page.$('aria/[role="button"]'); + const id = await button.evaluate((button: Element) => button.id); + expect(id).toBe('btn'); + }); + + it('should find button by name and role', async () => { + const { page } = getTestState(); + await page.setContent( + '<div id="div"><button id="btn" role="button">Submit</button></div>' + ); + const button = await page.$('aria/Submit[role="button"]'); + const id = await button.evaluate((button: Element) => button.id); + expect(id).toBe('btn'); + }); + + it('should find first matching element', async () => { + const { page } = getTestState(); + await page.setContent( + ` + <div role="menu" id="mnu1" aria-label="menu div"></div> + <div role="menu" id="mnu2" aria-label="menu div"></div> + ` + ); + const div = await page.$('aria/menu div'); + const id = await div.evaluate((div: Element) => div.id); + expect(id).toBe('mnu1'); + }); + + it('should find by name', async () => { + const { page } = getTestState(); + await page.setContent( + ` + <div role="menu" id="mnu1" aria-label="menu-label1">menu div</div> + <div role="menu" id="mnu2" aria-label="menu-label2">menu div</div> + ` + ); + const menu = await page.$('aria/menu-label1'); + const id = await menu.evaluate((div: Element) => div.id); + expect(id).toBe('mnu1'); + }); + + it('should find by name', async () => { + const { page } = getTestState(); + await page.setContent( + ` + <div role="menu" id="mnu1" aria-label="menu-label1">menu div</div> + <div role="menu" id="mnu2" aria-label="menu-label2">menu div</div> + ` + ); + const menu = await page.$('aria/menu-label2'); + const id = await menu.evaluate((div: Element) => div.id); + expect(id).toBe('mnu2'); + }); + }); + + describe('queryAll', () => { + it('should find menu by name', async () => { + const { page } = getTestState(); + await page.setContent( + ` + <div role="menu" id="mnu1" aria-label="menu div"></div> + <div role="menu" id="mnu2" aria-label="menu div"></div> + ` + ); + const divs = await page.$$('aria/menu div'); + const ids = await Promise.all( + divs.map((n) => n.evaluate((div: Element) => div.id)) + ); + expect(ids.join(', ')).toBe('mnu1, mnu2'); + }); + }); + describe('queryAllArray', () => { + it('$$eval should handle many elements', async () => { + const { page } = getTestState(); + await page.setContent(''); + await page.evaluate( + ` + for (var i = 0; i <= 10000; i++) { + const button = document.createElement('button'); + button.textContent = i; + document.body.appendChild(button); + } + ` + ); + const sum = await page.$$eval('aria/[role="button"]', (buttons) => + buttons.reduce((acc, button) => acc + Number(button.textContent), 0) + ); + expect(sum).toBe(50005000); + }); + }); + + describe('waitForSelector (aria)', function () { + const addElement = (tag) => + document.body.appendChild(document.createElement(tag)); + + it('should immediately resolve promise if node exists', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(addElement, 'button'); + await page.waitForSelector('aria/[role="button"]'); + }); + + it('should work independently of `exposeFunction`', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.exposeFunction('ariaQuerySelector', (a, b) => a + b); + await page.evaluate(addElement, 'button'); + await page.waitForSelector('aria/[role="button"]'); + const result = await page.evaluate('globalThis.ariaQuerySelector(2,8)'); + expect(result).toBe(10); + }); + + it('should work with removed MutationObserver', async () => { + const { page } = getTestState(); + + await page.evaluate(() => delete window.MutationObserver); + const [handle] = await Promise.all([ + page.waitForSelector('aria/anything'), + page.setContent(`<h1>anything</h1>`), + ]); + expect( + await page.evaluate((x: HTMLElement) => x.textContent, handle) + ).toBe('anything'); + }); + + it('should resolve promise when node is added', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const frame = page.mainFrame(); + const watchdog = frame.waitForSelector('aria/[role="heading"]'); + await frame.evaluate(addElement, 'br'); + await frame.evaluate(addElement, 'h1'); + const elementHandle = await watchdog; + const tagName = await elementHandle + .getProperty('tagName') + .then((element) => element.jsonValue()); + expect(tagName).toBe('H1'); + }); + + it('should work when node is added through innerHTML', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const watchdog = page.waitForSelector('aria/name'); + await page.evaluate(addElement, 'span'); + await page.evaluate( + () => + (document.querySelector('span').innerHTML = + '<h3><div aria-label="name"></div></h3>') + ); + await watchdog; + }); + + it('Page.waitForSelector is shortcut for main frame', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const otherFrame = page.frames()[1]; + const watchdog = page.waitForSelector('aria/[role="button"]'); + await otherFrame.evaluate(addElement, 'button'); + await page.evaluate(addElement, 'button'); + const elementHandle = await watchdog; + expect(elementHandle.executionContext().frame()).toBe(page.mainFrame()); + }); + + it('should run in specified frame', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE); + const frame1 = page.frames()[1]; + const frame2 = page.frames()[2]; + const waitForSelectorPromise = frame2.waitForSelector( + 'aria/[role="button"]' + ); + await frame1.evaluate(addElement, 'button'); + await frame2.evaluate(addElement, 'button'); + const elementHandle = await waitForSelectorPromise; + expect(elementHandle.executionContext().frame()).toBe(frame2); + }); + + it('should throw when frame is detached', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const frame = page.frames()[1]; + let waitError = null; + const waitPromise = frame + .waitForSelector('aria/does-not-exist') + .catch((error) => (waitError = error)); + await utils.detachFrame(page, 'frame1'); + await waitPromise; + expect(waitError).toBeTruthy(); + expect(waitError.message).toContain( + 'waitForFunction failed: frame got detached.' + ); + }); + + it('should survive cross-process navigation', async () => { + const { page, server } = getTestState(); + + let imgFound = false; + const waitForSelector = page + .waitForSelector('aria/[role="img"]') + .then(() => (imgFound = true)); + await page.goto(server.EMPTY_PAGE); + expect(imgFound).toBe(false); + await page.reload(); + expect(imgFound).toBe(false); + await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html'); + await waitForSelector; + expect(imgFound).toBe(true); + }); + + it('should wait for visible', async () => { + const { page } = getTestState(); + + let divFound = false; + const waitForSelector = page + .waitForSelector('aria/name', { visible: true }) + .then(() => (divFound = true)); + await page.setContent( + `<div aria-label='name' style='display: none; visibility: hidden;'>1</div>` + ); + expect(divFound).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.removeProperty('display') + ); + expect(divFound).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.removeProperty('visibility') + ); + expect(await waitForSelector).toBe(true); + expect(divFound).toBe(true); + }); + + it('should wait for visible recursively', async () => { + const { page } = getTestState(); + + let divVisible = false; + const waitForSelector = page + .waitForSelector('aria/inner', { visible: true }) + .then(() => (divVisible = true)); + await page.setContent( + `<div style='display: none; visibility: hidden;'><div aria-label="inner">hi</div></div>` + ); + expect(divVisible).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.removeProperty('display') + ); + expect(divVisible).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.removeProperty('visibility') + ); + expect(await waitForSelector).toBe(true); + expect(divVisible).toBe(true); + }); + + it('hidden should wait for visibility: hidden', async () => { + const { page } = getTestState(); + + let divHidden = false; + await page.setContent( + `<div role='button' style='display: block;'></div>` + ); + const waitForSelector = page + .waitForSelector('aria/[role="button"]', { hidden: true }) + .then(() => (divHidden = true)); + await page.waitForSelector('aria/[role="button"]'); // do a round trip + expect(divHidden).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.setProperty('visibility', 'hidden') + ); + expect(await waitForSelector).toBe(true); + expect(divHidden).toBe(true); + }); + + it('hidden should wait for display: none', async () => { + const { page } = getTestState(); + + let divHidden = false; + await page.setContent(`<div role='main' style='display: block;'></div>`); + const waitForSelector = page + .waitForSelector('aria/[role="main"]', { hidden: true }) + .then(() => (divHidden = true)); + await page.waitForSelector('aria/[role="main"]'); // do a round trip + expect(divHidden).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.setProperty('display', 'none') + ); + expect(await waitForSelector).toBe(true); + expect(divHidden).toBe(true); + }); + + it('hidden should wait for removal', async () => { + const { page } = getTestState(); + + await page.setContent(`<div role='main'></div>`); + let divRemoved = false; + const waitForSelector = page + .waitForSelector('aria/[role="main"]', { hidden: true }) + .then(() => (divRemoved = true)); + await page.waitForSelector('aria/[role="main"]'); // do a round trip + expect(divRemoved).toBe(false); + await page.evaluate(() => document.querySelector('div').remove()); + expect(await waitForSelector).toBe(true); + expect(divRemoved).toBe(true); + }); + + it('should return null if waiting to hide non-existing element', async () => { + const { page } = getTestState(); + + const handle = await page.waitForSelector('aria/non-existing', { + hidden: true, + }); + expect(handle).toBe(null); + }); + + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + await page + .waitForSelector('aria/[role="button"]', { timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'waiting for selector `[role="button"]` failed: timeout' + ); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + + it('should have an error message specifically for awaiting an element to be hidden', async () => { + const { page } = getTestState(); + + await page.setContent(`<div role='main'></div>`); + let error = null; + await page + .waitForSelector('aria/[role="main"]', { hidden: true, timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'waiting for selector `[role="main"]` to be hidden failed: timeout' + ); + }); + + it('should respond to node attribute mutation', async () => { + const { page } = getTestState(); + + let divFound = false; + const waitForSelector = page + .waitForSelector('aria/zombo') + .then(() => (divFound = true)); + await page.setContent(`<div aria-label='notZombo'></div>`); + expect(divFound).toBe(false); + await page.evaluate(() => + document.querySelector('div').setAttribute('aria-label', 'zombo') + ); + expect(await waitForSelector).toBe(true); + }); + + it('should return the element handle', async () => { + const { page } = getTestState(); + + const waitForSelector = page.waitForSelector('aria/zombo'); + await page.setContent(`<div aria-label='zombo'>anything</div>`); + expect( + await page.evaluate( + (x: HTMLElement) => x.textContent, + await waitForSelector + ) + ).toBe('anything'); + }); + + it('should have correct stack trace for timeout', async () => { + const { page } = getTestState(); + + let error; + await page + .waitForSelector('aria/zombo', { timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error.stack).toContain('waiting for selector `zombo` failed'); + }); + }); + + describe('queryOne (Chromium web test)', async () => { + beforeEach(async () => { + const { page } = getTestState(); + await page.setContent( + ` + <h2 id="shown">title</h2> + <h2 id="hidden" aria-hidden="true">title</h2> + <div id="node1" aria-labeledby="node2"></div> + <div id="node2" aria-label="bar"></div> + <div id="node3" aria-label="foo"></div> + <div id="node4" class="container"> + <div id="node5" role="button" aria-label="foo"></div> + <div id="node6" role="button" aria-label="foo"></div> + <!-- Accessible name not available when element is hidden --> + <div id="node7" hidden role="button" aria-label="foo"></div> + <div id="node8" role="button" aria-label="bar"></div> + </div> + <button id="node10">text content</button> + <h1 id="node11">text content</h1> + <!-- Accessible name not available when role is "presentation" --> + <h1 id="node12" role="presentation">text content</h1> + <!-- Elements inside shadow dom should be found --> + <script> + const div = document.createElement('div'); + const shadowRoot = div.attachShadow({mode: 'open'}); + const h1 = document.createElement('h1'); + h1.textContent = 'text content'; + h1.id = 'node13'; + shadowRoot.appendChild(h1); + document.documentElement.appendChild(div); + </script> + <img id="node20" src="" alt="Accessible Name"> + <input id="node21" type="submit" value="Accessible Name"> + <label id="node22" for="node23">Accessible Name</label> + <!-- Accessible name for the <input> is "Accessible Name" --> + <input id="node23"> + <div id="node24" title="Accessible Name"></div> + <div role="treeitem" id="node30"> + <div role="treeitem" id="node31"> + <div role="treeitem" id="node32">item1</div> + <div role="treeitem" id="node33">item2</div> + </div> + <div role="treeitem" id="node34">item3</div> + </div> + <!-- Accessible name for the <div> is "item1 item2 item3" --> + <div aria-describedby="node30"></div> + ` + ); + }); + const getIds = async (elements: ElementHandle[]) => + Promise.all( + elements.map((element) => + element.evaluate((element: Element) => element.id) + ) + ); + it('should find by name "foo"', async () => { + const { page } = getTestState(); + const found = await page.$$('aria/foo'); + const ids = await getIds(found); + expect(ids).toEqual(['node3', 'node5', 'node6']); + }); + it('should find by name "bar"', async () => { + const { page } = getTestState(); + const found = await page.$$('aria/bar'); + const ids = await getIds(found); + expect(ids).toEqual(['node1', 'node2', 'node8']); + }); + it('should find treeitem by name', async () => { + const { page } = getTestState(); + const found = await page.$$('aria/item1 item2 item3'); + const ids = await getIds(found); + expect(ids).toEqual(['node30']); + }); + it('should find by role "button"', async () => { + const { page } = getTestState(); + const found = await page.$$('aria/[role="button"]'); + const ids = await getIds(found); + expect(ids).toEqual(['node5', 'node6', 'node8', 'node10', 'node21']); + }); + it('should find by role "heading"', async () => { + const { page } = getTestState(); + const found = await page.$$('aria/[role="heading"]'); + const ids = await getIds(found); + expect(ids).toEqual(['shown', 'hidden', 'node11', 'node13']); + }); + it('should find both ignored and unignored', async () => { + const { page } = getTestState(); + const found = await page.$$('aria/title'); + const ids = await getIds(found); + expect(ids).toEqual(['shown', 'hidden']); + }); + }); +}); diff --git a/remote/test/puppeteer/test/assert-coverage-test.js b/remote/test/puppeteer/test/assert-coverage-test.js new file mode 100644 index 0000000000..6e26a1121a --- /dev/null +++ b/remote/test/puppeteer/test/assert-coverage-test.js @@ -0,0 +1,25 @@ +const { describe, it } = require('mocha'); +const { getCoverageResults } = require('./coverage-utils'); +const expect = require('expect'); + +describe('API coverage test', () => { + it('calls every method', () => { + if (!process.env.COVERAGE) return; + + const coverageMap = getCoverageResults(); + const missingMethods = []; + for (const method of coverageMap.keys()) { + if (!coverageMap.get(method)) missingMethods.push(method); + } + if (missingMethods.length) { + console.error( + '\nCoverage check failed: not all API methods called. See above output for list of missing methods.' + ); + console.error(missingMethods.join('\n')); + } + + // We know this will fail because we checked above + // but we need the actual test to fail. + expect(missingMethods.length).toEqual(0); + }); +}); diff --git a/remote/test/puppeteer/test/assets/beforeunload.html b/remote/test/puppeteer/test/assets/beforeunload.html new file mode 100644 index 0000000000..3cef6763f3 --- /dev/null +++ b/remote/test/puppeteer/test/assets/beforeunload.html @@ -0,0 +1,10 @@ +<div>beforeunload demo.</div> +<script> +window.addEventListener('beforeunload', event => { + // Chrome way. + event.returnValue = 'Leave?'; + // Firefox way. + event.preventDefault(); +}); +</script> + diff --git a/remote/test/puppeteer/test/assets/cached/one-style.css b/remote/test/puppeteer/test/assets/cached/one-style.css new file mode 100644 index 0000000000..04e7110b41 --- /dev/null +++ b/remote/test/puppeteer/test/assets/cached/one-style.css @@ -0,0 +1,3 @@ +body { + background-color: pink; +} diff --git a/remote/test/puppeteer/test/assets/cached/one-style.html b/remote/test/puppeteer/test/assets/cached/one-style.html new file mode 100644 index 0000000000..4760f2b9f7 --- /dev/null +++ b/remote/test/puppeteer/test/assets/cached/one-style.html @@ -0,0 +1,2 @@ +<link rel='stylesheet' href='./one-style.css'> +<div>hello, world!</div> diff --git a/remote/test/puppeteer/test/assets/chromium-linux.zip b/remote/test/puppeteer/test/assets/chromium-linux.zip Binary files differnew file mode 100644 index 0000000000..9c00ec080d --- /dev/null +++ b/remote/test/puppeteer/test/assets/chromium-linux.zip diff --git a/remote/test/puppeteer/test/assets/consolelog.html b/remote/test/puppeteer/test/assets/consolelog.html new file mode 100644 index 0000000000..4a27803aa9 --- /dev/null +++ b/remote/test/puppeteer/test/assets/consolelog.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> + <head> + <title>console.log test</title> + </head> + <body> + <script> + function foo() { + console.log('yellow') + } + function bar() { + foo(); + } + bar(); + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/csp.html b/remote/test/puppeteer/test/assets/csp.html new file mode 100644 index 0000000000..34fc1fc1a5 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csp.html @@ -0,0 +1 @@ +<meta http-equiv="Content-Security-Policy" content="default-src 'self'"> diff --git a/remote/test/puppeteer/test/assets/csscoverage/Dosis-Regular.ttf b/remote/test/puppeteer/test/assets/csscoverage/Dosis-Regular.ttf Binary files differnew file mode 100644 index 0000000000..4b208624e8 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/Dosis-Regular.ttf diff --git a/remote/test/puppeteer/test/assets/csscoverage/OFL.txt b/remote/test/puppeteer/test/assets/csscoverage/OFL.txt new file mode 100644 index 0000000000..a9b3c8b34e --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/OFL.txt @@ -0,0 +1,95 @@ +Copyright (c) 2011, Edgar Tolentino and Pablo Impallari (www.impallari.com|impallari@gmail.com), +Copyright (c) 2011, Igino Marini. (www.ikern.com|mail@iginomarini.com), +with Reserved Font Names "Dosis". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/remote/test/puppeteer/test/assets/csscoverage/involved.html b/remote/test/puppeteer/test/assets/csscoverage/involved.html new file mode 100644 index 0000000000..bcd9845b93 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/involved.html @@ -0,0 +1,26 @@ +<style> +@charset "utf-8"; +@namespace svg url(http://www.w3.org/2000/svg); +@font-face { + font-family: "Example Font"; + src: url("./Dosis-Regular.ttf"); +} + +#fluffy { + border: 1px solid black; + z-index: 1; + /* -webkit-disabled-property: rgb(1, 2, 3) */ + -lol-cats: "dogs" /* non-existing property */ +} + +@media (min-width: 1px) { + span { + -webkit-border-radius: 10px; + font-family: "Example Font"; + animation: 1s identifier; + } +} +</style> +<div id="fluffy">woof!</div> +<span>fancy text</span> + diff --git a/remote/test/puppeteer/test/assets/csscoverage/media.html b/remote/test/puppeteer/test/assets/csscoverage/media.html new file mode 100644 index 0000000000..bfb89f8f75 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/media.html @@ -0,0 +1,4 @@ +<style> +@media screen { div { color: green; } } </style> +<div>hello, world</div> + diff --git a/remote/test/puppeteer/test/assets/csscoverage/multiple.html b/remote/test/puppeteer/test/assets/csscoverage/multiple.html new file mode 100644 index 0000000000..0fd97e962a --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/multiple.html @@ -0,0 +1,8 @@ +<link rel="stylesheet" href="stylesheet1.css"> +<link rel="stylesheet" href="stylesheet2.css"> +<script> +window.addEventListener('DOMContentLoaded', () => { + // Force stylesheets to load. + console.log(window.getComputedStyle(document.body).color); +}, false); +</script> diff --git a/remote/test/puppeteer/test/assets/csscoverage/simple.html b/remote/test/puppeteer/test/assets/csscoverage/simple.html new file mode 100644 index 0000000000..3beae21829 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/simple.html @@ -0,0 +1,6 @@ +<style> +div { color: green; } +a { color: blue; } +</style> +<div>hello, world</div> + diff --git a/remote/test/puppeteer/test/assets/csscoverage/sourceurl.html b/remote/test/puppeteer/test/assets/csscoverage/sourceurl.html new file mode 100644 index 0000000000..df4e9c276c --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/sourceurl.html @@ -0,0 +1,7 @@ +<style> +body { + padding: 10px; +} +/*# sourceURL=nicename.css */ +</style> + diff --git a/remote/test/puppeteer/test/assets/csscoverage/stylesheet1.css b/remote/test/puppeteer/test/assets/csscoverage/stylesheet1.css new file mode 100644 index 0000000000..60f1eab971 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/stylesheet1.css @@ -0,0 +1,3 @@ +body { + color: red; +} diff --git a/remote/test/puppeteer/test/assets/csscoverage/stylesheet2.css b/remote/test/puppeteer/test/assets/csscoverage/stylesheet2.css new file mode 100644 index 0000000000..a87defb098 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/stylesheet2.css @@ -0,0 +1,4 @@ +html { + margin: 0; + padding: 0; +} diff --git a/remote/test/puppeteer/test/assets/csscoverage/unused.html b/remote/test/puppeteer/test/assets/csscoverage/unused.html new file mode 100644 index 0000000000..5b8186a3bf --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/unused.html @@ -0,0 +1,7 @@ +<style> +@media screen { + a { color: green; } +} +/*# sourceURL=unused.css */ +</style> + diff --git a/remote/test/puppeteer/test/assets/detect-touch.html b/remote/test/puppeteer/test/assets/detect-touch.html new file mode 100644 index 0000000000..80a4123fbd --- /dev/null +++ b/remote/test/puppeteer/test/assets/detect-touch.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> + <head> + <title>Detect Touch Test</title> + <script src='modernizr.js'></script> + </head> + <body style="font-size:30vmin"> + <script> + document.body.textContent = Modernizr.touchevents ? 'YES' : 'NO'; + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/digits/0.png b/remote/test/puppeteer/test/assets/digits/0.png Binary files differnew file mode 100644 index 0000000000..ac3c4768ed --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/0.png diff --git a/remote/test/puppeteer/test/assets/digits/1.png b/remote/test/puppeteer/test/assets/digits/1.png Binary files differnew file mode 100644 index 0000000000..6768222729 --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/1.png diff --git a/remote/test/puppeteer/test/assets/digits/2.png b/remote/test/puppeteer/test/assets/digits/2.png Binary files differnew file mode 100644 index 0000000000..b1daa4735d --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/2.png diff --git a/remote/test/puppeteer/test/assets/digits/3.png b/remote/test/puppeteer/test/assets/digits/3.png Binary files differnew file mode 100644 index 0000000000..6eca99b21b --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/3.png diff --git a/remote/test/puppeteer/test/assets/digits/4.png b/remote/test/puppeteer/test/assets/digits/4.png Binary files differnew file mode 100644 index 0000000000..a721071e2c --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/4.png diff --git a/remote/test/puppeteer/test/assets/digits/5.png b/remote/test/puppeteer/test/assets/digits/5.png Binary files differnew file mode 100644 index 0000000000..15cb19932a --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/5.png diff --git a/remote/test/puppeteer/test/assets/digits/6.png b/remote/test/puppeteer/test/assets/digits/6.png Binary files differnew file mode 100644 index 0000000000..639f38439d --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/6.png diff --git a/remote/test/puppeteer/test/assets/digits/7.png b/remote/test/puppeteer/test/assets/digits/7.png Binary files differnew file mode 100644 index 0000000000..5c1150b005 --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/7.png diff --git a/remote/test/puppeteer/test/assets/digits/8.png b/remote/test/puppeteer/test/assets/digits/8.png Binary files differnew file mode 100644 index 0000000000..abb8b48b0b --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/8.png diff --git a/remote/test/puppeteer/test/assets/digits/9.png b/remote/test/puppeteer/test/assets/digits/9.png Binary files differnew file mode 100644 index 0000000000..6a40a21c6f --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/9.png diff --git a/remote/test/puppeteer/test/assets/dynamic-oopif.html b/remote/test/puppeteer/test/assets/dynamic-oopif.html new file mode 100644 index 0000000000..f00c741dfb --- /dev/null +++ b/remote/test/puppeteer/test/assets/dynamic-oopif.html @@ -0,0 +1,10 @@ +<script> +window.addEventListener('DOMContentLoaded', () => { + const iframe = document.createElement('iframe'); + const url = new URL(location.href); + url.hostname = url.hostname === 'localhost' ? '127.0.0.1' : 'localhost'; + url.pathname = '/grid.html'; + iframe.src = url.toString(); + document.body.appendChild(iframe); +}, false); +</script> diff --git a/remote/test/puppeteer/test/assets/empty.html b/remote/test/puppeteer/test/assets/empty.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/remote/test/puppeteer/test/assets/empty.html diff --git a/remote/test/puppeteer/test/assets/error.html b/remote/test/puppeteer/test/assets/error.html new file mode 100644 index 0000000000..130400c006 --- /dev/null +++ b/remote/test/puppeteer/test/assets/error.html @@ -0,0 +1,15 @@ +<script> +a(); + +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + throw new Error('Fancy error!'); +} +</script> diff --git a/remote/test/puppeteer/test/assets/es6/.eslintrc b/remote/test/puppeteer/test/assets/es6/.eslintrc new file mode 100644 index 0000000000..1903e176f5 --- /dev/null +++ b/remote/test/puppeteer/test/assets/es6/.eslintrc @@ -0,0 +1,5 @@ +{ + "parserOptions": { + "sourceType": "module" + } +}
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/es6/es6import.js b/remote/test/puppeteer/test/assets/es6/es6import.js new file mode 100644 index 0000000000..9aac2d4d64 --- /dev/null +++ b/remote/test/puppeteer/test/assets/es6/es6import.js @@ -0,0 +1,2 @@ +import num from './es6module.js'; +window.__es6injected = num; diff --git a/remote/test/puppeteer/test/assets/es6/es6module.js b/remote/test/puppeteer/test/assets/es6/es6module.js new file mode 100644 index 0000000000..7a4e8a723a --- /dev/null +++ b/remote/test/puppeteer/test/assets/es6/es6module.js @@ -0,0 +1 @@ +export default 42; diff --git a/remote/test/puppeteer/test/assets/es6/es6pathimport.js b/remote/test/puppeteer/test/assets/es6/es6pathimport.js new file mode 100644 index 0000000000..eb17a9a3d1 --- /dev/null +++ b/remote/test/puppeteer/test/assets/es6/es6pathimport.js @@ -0,0 +1,2 @@ +import num from './es6/es6module.js'; +window.__es6injected = num; diff --git a/remote/test/puppeteer/test/assets/file-to-upload.txt b/remote/test/puppeteer/test/assets/file-to-upload.txt new file mode 100644 index 0000000000..b4ad118489 --- /dev/null +++ b/remote/test/puppeteer/test/assets/file-to-upload.txt @@ -0,0 +1 @@ +contents of the file
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/firefox-75.0a1.en-US.linux-x86_64.tar.bz2 b/remote/test/puppeteer/test/assets/firefox-75.0a1.en-US.linux-x86_64.tar.bz2 Binary files differnew file mode 100644 index 0000000000..be6d188027 --- /dev/null +++ b/remote/test/puppeteer/test/assets/firefox-75.0a1.en-US.linux-x86_64.tar.bz2 diff --git a/remote/test/puppeteer/test/assets/frames/frame.html b/remote/test/puppeteer/test/assets/frames/frame.html new file mode 100644 index 0000000000..8f20d2da9f --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/frame.html @@ -0,0 +1,8 @@ +<link rel='stylesheet' href='./style.css'> +<script src='./script.js' type='text/javascript'></script> +<style> +div { + line-height: 18px; +} +</style> +<div>Hi, I'm frame</div> diff --git a/remote/test/puppeteer/test/assets/frames/frameset.html b/remote/test/puppeteer/test/assets/frames/frameset.html new file mode 100644 index 0000000000..4d56f88839 --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/frameset.html @@ -0,0 +1,8 @@ +<frameset> + <frameset> + <frame src='./frame.html'></frame> + <frame src='about:blank'></frame> + </frameset> + <frame src='/empty.html'></frame> + <frame></frame> +</frameset> diff --git a/remote/test/puppeteer/test/assets/frames/nested-frames.html b/remote/test/puppeteer/test/assets/frames/nested-frames.html new file mode 100644 index 0000000000..de1987586f --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/nested-frames.html @@ -0,0 +1,25 @@ +<style> +body { + display: flex; +} + +body iframe { + flex-grow: 1; + flex-shrink: 1; +} +::-webkit-scrollbar{ + display: none; +} +</style> +<script> +async function attachFrame(frameId, url) { + var frame = document.createElement('iframe'); + frame.src = url; + frame.id = frameId; + document.body.appendChild(frame); + await new Promise(x => frame.onload = x); + return 'kazakh'; +} +</script> +<iframe src='./two-frames.html' name='2frames'></iframe> +<iframe src='./frame.html' name='aframe'></iframe> diff --git a/remote/test/puppeteer/test/assets/frames/one-frame-url-fragment.html b/remote/test/puppeteer/test/assets/frames/one-frame-url-fragment.html new file mode 100644 index 0000000000..d1462641ff --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/one-frame-url-fragment.html @@ -0,0 +1 @@ +<iframe src='./frame.html?param=value#fragment'></iframe> diff --git a/remote/test/puppeteer/test/assets/frames/one-frame.html b/remote/test/puppeteer/test/assets/frames/one-frame.html new file mode 100644 index 0000000000..e941d795a2 --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/one-frame.html @@ -0,0 +1 @@ +<iframe src='./frame.html'></iframe> diff --git a/remote/test/puppeteer/test/assets/frames/script.js b/remote/test/puppeteer/test/assets/frames/script.js new file mode 100644 index 0000000000..be22256d16 --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/script.js @@ -0,0 +1 @@ +console.log('Cheers!'); diff --git a/remote/test/puppeteer/test/assets/frames/style.css b/remote/test/puppeteer/test/assets/frames/style.css new file mode 100644 index 0000000000..5b5436e874 --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/style.css @@ -0,0 +1,3 @@ +div { + color: blue; +} diff --git a/remote/test/puppeteer/test/assets/frames/two-frames.html b/remote/test/puppeteer/test/assets/frames/two-frames.html new file mode 100644 index 0000000000..b2ee853eda --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/two-frames.html @@ -0,0 +1,13 @@ +<style> +body { + display: flex; + flex-direction: column; +} + +body iframe { + flex-grow: 1; + flex-shrink: 1; +} +</style> +<iframe src='./frame.html' name='uno'></iframe> +<iframe src='./frame.html' name='dos'></iframe> diff --git a/remote/test/puppeteer/test/assets/global-var.html b/remote/test/puppeteer/test/assets/global-var.html new file mode 100644 index 0000000000..b6be975038 --- /dev/null +++ b/remote/test/puppeteer/test/assets/global-var.html @@ -0,0 +1,3 @@ +<script> +var globalVar = 123; +</script>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/grid.html b/remote/test/puppeteer/test/assets/grid.html new file mode 100644 index 0000000000..0bdbb1220e --- /dev/null +++ b/remote/test/puppeteer/test/assets/grid.html @@ -0,0 +1,52 @@ +<script> +document.addEventListener('DOMContentLoaded', function() { + function generatePalette(amount) { + var result = []; + var hueStep = 360 / amount; + for (var i = 0; i < amount; ++i) + result.push('hsl(' + (hueStep * i) + ', 100%, 90%)'); + return result; + } + + var palette = generatePalette(100); + for (var i = 0; i < 200; ++i) { + var box = document.createElement('div'); + box.classList.add('box'); + box.style.setProperty('background-color', palette[i % palette.length]); + var x = i; + do { + var digit = x % 10; + x = (x / 10)|0; + var img = document.createElement('img'); + img.src = `./digits/${digit}.png`; + box.insertBefore(img, box.firstChild); + } while (x); + document.body.appendChild(box); + } +}); +</script> + +<style> + +body { + margin: 0; + padding: 0; +} + +.box { + font-family: arial; + display: inline-flex; + align-items: center; + justify-content: center; + margin: 0; + padding: 0; + width: 50px; + height: 50px; + box-sizing: border-box; + border: 1px solid darkgray; +} + +::-webkit-scrollbar { + display: none; +} +</style> diff --git a/remote/test/puppeteer/test/assets/historyapi.html b/remote/test/puppeteer/test/assets/historyapi.html new file mode 100644 index 0000000000..bacaf9e9a0 --- /dev/null +++ b/remote/test/puppeteer/test/assets/historyapi.html @@ -0,0 +1,5 @@ +<script> +window.addEventListener('DOMContentLoaded', () => { + history.pushState({}, '', '#1'); +}); +</script> diff --git a/remote/test/puppeteer/test/assets/idle-detector.html b/remote/test/puppeteer/test/assets/idle-detector.html new file mode 100644 index 0000000000..83b496c03d --- /dev/null +++ b/remote/test/puppeteer/test/assets/idle-detector.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<div id="state"></div> +<script> + const elState = document.querySelector('#state'); + function setState(msg) { + elState.textContent = msg; + } + async function main() { + const controller = new AbortController(); + const signal = controller.signal; + const idleDetector = new IdleDetector({ + threshold: 60000, + signal, + }); + idleDetector.addEventListener('change', () => { + const userState = idleDetector.userState; + const screenState = idleDetector.screenState; + setState(`Idle state: ${userState}, ${screenState}.`); + }); + idleDetector.start(); + } + main(); +</script> diff --git a/remote/test/puppeteer/test/assets/injectedfile.js b/remote/test/puppeteer/test/assets/injectedfile.js new file mode 100644 index 0000000000..c211b62c16 --- /dev/null +++ b/remote/test/puppeteer/test/assets/injectedfile.js @@ -0,0 +1,2 @@ +window.__injected = 42; +window.__injectedError = new Error('hi'); diff --git a/remote/test/puppeteer/test/assets/injectedstyle.css b/remote/test/puppeteer/test/assets/injectedstyle.css new file mode 100644 index 0000000000..aa1634c255 --- /dev/null +++ b/remote/test/puppeteer/test/assets/injectedstyle.css @@ -0,0 +1,3 @@ +body { + background-color: red; +} diff --git a/remote/test/puppeteer/test/assets/input/button.html b/remote/test/puppeteer/test/assets/input/button.html new file mode 100644 index 0000000000..d4c6e13fd2 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/button.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html> + <head> + <title>Button test</title> + </head> + <body> + <script src="mouse-helper.js"></script> + <button onclick="clicked();">Click target</button> + <script> + window.result = 'Was not clicked'; + function clicked() { + result = 'Clicked'; + } + </script> + </body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/input/checkbox.html b/remote/test/puppeteer/test/assets/input/checkbox.html new file mode 100644 index 0000000000..ca56762e2b --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/checkbox.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html> + <head> + <title>Selection Test</title> + </head> + <body> + <label for="agree">Remember Me</label> + <input id="agree" type="checkbox"> + <script> + window.result = { + check: null, + events: [], + }; + + let checkbox = document.querySelector('input'); + + const events = [ + 'change', + 'click', + 'dblclick', + 'input', + 'mousedown', + 'mouseenter', + 'mouseleave', + 'mousemove', + 'mouseout', + 'mouseover', + 'mouseup', + ]; + + for (let event of events) { + checkbox.addEventListener(event, () => { + if (['change', 'click', 'dblclick', 'input'].includes(event) === true) { + result.check = checkbox.checked; + } + + result.events.push(event); + }, false); + } + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/input/fileupload.html b/remote/test/puppeteer/test/assets/input/fileupload.html new file mode 100644 index 0000000000..55fd7c5006 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/fileupload.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <title>File upload test</title> + </head> + <body> + <input type="file"> + </body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/input/keyboard.html b/remote/test/puppeteer/test/assets/input/keyboard.html new file mode 100644 index 0000000000..fd962c7518 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/keyboard.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html> + <head> + <title>Keyboard test</title> + </head> + <body> + <textarea></textarea> + <script> + window.result = ""; + let textarea = document.querySelector('textarea'); + textarea.focus(); + textarea.addEventListener('keydown', event => { + log('Keydown:', event.key, event.code, event.which, modifiers(event)); + }); + textarea.addEventListener('keypress', event => { + log('Keypress:', event.key, event.code, event.which, event.charCode, modifiers(event)); + }); + textarea.addEventListener('keyup', event => { + log('Keyup:', event.key, event.code, event.which, modifiers(event)); + }); + function modifiers(event) { + let m = []; + if (event.altKey) + m.push('Alt') + if (event.ctrlKey) + m.push('Control'); + if (event.shiftKey) + m.push('Shift') + return '[' + m.join(' ') + ']'; + } + function log(...args) { + console.log.apply(console, args); + result += args.join(' ') + '\n'; + } + function getResult() { + let temp = result.trim(); + result = ""; + return temp; + } + </script> + </body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/input/mouse-helper.js b/remote/test/puppeteer/test/assets/input/mouse-helper.js new file mode 100644 index 0000000000..4f2824dceb --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/mouse-helper.js @@ -0,0 +1,74 @@ +// This injects a box into the page that moves with the mouse; +// Useful for debugging +(function () { + const box = document.createElement('div'); + box.classList.add('mouse-helper'); + const styleElement = document.createElement('style'); + styleElement.innerHTML = ` + .mouse-helper { + pointer-events: none; + position: absolute; + top: 0; + left: 0; + width: 20px; + height: 20px; + background: rgba(0,0,0,.4); + border: 1px solid white; + border-radius: 10px; + margin-left: -10px; + margin-top: -10px; + transition: background .2s, border-radius .2s, border-color .2s; + } + .mouse-helper.button-1 { + transition: none; + background: rgba(0,0,0,0.9); + } + .mouse-helper.button-2 { + transition: none; + border-color: rgba(0,0,255,0.9); + } + .mouse-helper.button-3 { + transition: none; + border-radius: 4px; + } + .mouse-helper.button-4 { + transition: none; + border-color: rgba(255,0,0,0.9); + } + .mouse-helper.button-5 { + transition: none; + border-color: rgba(0,255,0,0.9); + } + `; + document.head.appendChild(styleElement); + document.body.appendChild(box); + document.addEventListener( + 'mousemove', + (event) => { + box.style.left = event.pageX + 'px'; + box.style.top = event.pageY + 'px'; + updateButtons(event.buttons); + }, + true + ); + document.addEventListener( + 'mousedown', + (event) => { + updateButtons(event.buttons); + box.classList.add('button-' + event.which); + }, + true + ); + document.addEventListener( + 'mouseup', + (event) => { + updateButtons(event.buttons); + box.classList.remove('button-' + event.which); + }, + true + ); + function updateButtons(buttons) { + for (let i = 0; i < 5; i++) + box.classList.toggle('button-' + i, buttons & (1 << i)); + } +})(); diff --git a/remote/test/puppeteer/test/assets/input/rotatedButton.html b/remote/test/puppeteer/test/assets/input/rotatedButton.html new file mode 100644 index 0000000000..1bce66cf5e --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/rotatedButton.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> + <head> + <title>Rotated button test</title> + </head> + <body> + <script src="mouse-helper.js"></script> + <button onclick="clicked();">Click target</button> + <style> + button { + transform: rotateY(180deg); + } + </style> + <script> + window.result = 'Was not clicked'; + function clicked() { + result = 'Clicked'; + } + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/input/scrollable.html b/remote/test/puppeteer/test/assets/input/scrollable.html new file mode 100644 index 0000000000..885d3739d5 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/scrollable.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html> + <head> + <title>Scrollable test</title> + </head> + <body> + <script src='mouse-helper.js'></script> + <script> + for (let i = 0; i < 100; i++) { + let button = document.createElement('button'); + button.textContent = i + ': not clicked'; + button.id = 'button-' + i; + button.onclick = () => button.textContent = 'clicked'; + button.oncontextmenu = event => { + event.preventDefault(); + button.textContent = 'context menu'; + } + document.body.appendChild(button); + document.body.appendChild(document.createElement('br')); + } + </script> + </body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/input/select.html b/remote/test/puppeteer/test/assets/input/select.html new file mode 100644 index 0000000000..879a537a76 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/select.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<html> + <head> + <title>Selection Test</title> + </head> + <body> + <select> + <option value="black">Black</option> + <option value="blue">Blue</option> + <option value="brown">Brown</option> + <option value="cyan">Cyan</option> + <option value="gray">Gray</option> + <option value="green">Green</option> + <option value="indigo">Indigo</option> + <option value="magenta">Magenta</option> + <option value="orange">Orange</option> + <option value="pink">Pink</option> + <option value="purple">Purple</option> + <option value="red">Red</option> + <option value="violet">Violet</option> + <option value="white">White</option> + <option value="yellow">Yellow</option> + </select> + <script> + window.result = { + onInput: null, + onChange: null, + onBubblingChange: null, + onBubblingInput: null, + }; + + let select = document.querySelector('select'); + + function makeEmpty() { + for (let i = select.options.length - 1; i >= 0; --i) { + select.remove(i); + } + } + + function makeMultiple() { + select.setAttribute('multiple', true); + } + + select.addEventListener('input', () => { + result.onInput = Array.from(select.querySelectorAll('option:checked')).map((option) => { + return option.value; + }); + }, false); + + select.addEventListener('change', () => { + result.onChange = Array.from(select.querySelectorAll('option:checked')).map((option) => { + return option.value; + }); + }, false); + + document.body.addEventListener('input', () => { + result.onBubblingInput = Array.from(select.querySelectorAll('option:checked')).map((option) => { + return option.value; + }); + }, false); + + document.body.addEventListener('change', () => { + result.onBubblingChange = Array.from(select.querySelectorAll('option:checked')).map((option) => { + return option.value; + }); + }, false); + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/input/textarea.html b/remote/test/puppeteer/test/assets/input/textarea.html new file mode 100644 index 0000000000..6d77f3106d --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/textarea.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> + <head> + <title>Textarea test</title> + </head> + <body> + <textarea></textarea> + <script src='mouse-helper.js'></script> + <script> + globalThis.result = ''; + globalThis.textarea = document.querySelector('textarea'); + textarea.addEventListener('input', () => result = textarea.value, false); + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/input/touches.html b/remote/test/puppeteer/test/assets/input/touches.html new file mode 100644 index 0000000000..4392cfacbd --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/touches.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html> + <head> + <title>Touch test</title> + </head> + <body> + <script src="mouse-helper.js"></script> + <button onclick="clicked();">Click target</button> + <script> + window.result = []; + const button = document.querySelector('button'); + button.style.height = '200px'; + button.style.width = '200px'; + button.focus(); + button.addEventListener('touchstart', event => { + log('Touchstart:', ...Array.from(event.changedTouches).map(touch => touch.identifier)); + }); + button.addEventListener('touchend', event => { + log('Touchend:', ...Array.from(event.changedTouches).map(touch => touch.identifier)); + }); + button.addEventListener('touchmove', event => { + log('Touchmove:', ...Array.from(event.changedTouches).map(touch => touch.identifier)); + }); + function log(...args) { + console.log.apply(console, args); + result.push(args.join(' ')); + } + function getResult() { + let temp = result; + result = []; + return temp; + } + </script> + </body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/input/wheel.html b/remote/test/puppeteer/test/assets/input/wheel.html new file mode 100644 index 0000000000..3d093a993e --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/wheel.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <style> + body { + min-height: 100vh; + margin: 0; + display: flex; + align-items: center; + justify-content: center; + } + + div { + width: 105px; + height: 105px; + background: #cdf; + padding: 5px; + } + </style> + <title>Element: wheel event - Scaling_an_element_via_the_wheel - code sample</title> + </head> + <body> + <div>Scale me with your mouse wheel.</div> + <script> + function zoom(event) { + event.preventDefault(); + + scale += event.deltaY * -0.01; + + // Restrict scale + scale = Math.min(Math.max(.125, scale), 4); + + // Apply scale transform + el.style.transform = `scale(${scale})`; + } + + let scale = 1; + const el = document.querySelector('div'); + el.onwheel = zoom; + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/jscoverage/eval.html b/remote/test/puppeteer/test/assets/jscoverage/eval.html new file mode 100644 index 0000000000..838ae28763 --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/eval.html @@ -0,0 +1 @@ +<script>eval('console.log("foo")')</script> diff --git a/remote/test/puppeteer/test/assets/jscoverage/involved.html b/remote/test/puppeteer/test/assets/jscoverage/involved.html new file mode 100644 index 0000000000..889c86bed5 --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/involved.html @@ -0,0 +1,15 @@ +<script> +function foo() { + if (1 > 2) + console.log(1); + if (1 < 2) + console.log(2); + let x = 1 > 2 ? 'foo' : 'bar'; + let y = 1 < 2 ? 'foo' : 'bar'; + let z = () => {}; + let q = () => {}; + q(); +} + +foo(); +</script> diff --git a/remote/test/puppeteer/test/assets/jscoverage/multiple.html b/remote/test/puppeteer/test/assets/jscoverage/multiple.html new file mode 100644 index 0000000000..bdef59885b --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/multiple.html @@ -0,0 +1,2 @@ +<script src='script1.js'></script> +<script src='script2.js'></script> diff --git a/remote/test/puppeteer/test/assets/jscoverage/ranges.html b/remote/test/puppeteer/test/assets/jscoverage/ranges.html new file mode 100644 index 0000000000..a537a7da6a --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/ranges.html @@ -0,0 +1,2 @@ +<script> +function unused(){}console.log('used!');</script> diff --git a/remote/test/puppeteer/test/assets/jscoverage/script1.js b/remote/test/puppeteer/test/assets/jscoverage/script1.js new file mode 100644 index 0000000000..3bd241b50e --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/script1.js @@ -0,0 +1 @@ +console.log(3); diff --git a/remote/test/puppeteer/test/assets/jscoverage/script2.js b/remote/test/puppeteer/test/assets/jscoverage/script2.js new file mode 100644 index 0000000000..3bd241b50e --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/script2.js @@ -0,0 +1 @@ +console.log(3); diff --git a/remote/test/puppeteer/test/assets/jscoverage/simple.html b/remote/test/puppeteer/test/assets/jscoverage/simple.html new file mode 100644 index 0000000000..49eeeea6ae --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/simple.html @@ -0,0 +1,2 @@ +<script> +function foo() {function bar() { } console.log(1); } foo(); </script> diff --git a/remote/test/puppeteer/test/assets/jscoverage/sourceurl.html b/remote/test/puppeteer/test/assets/jscoverage/sourceurl.html new file mode 100644 index 0000000000..e477750320 --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/sourceurl.html @@ -0,0 +1,4 @@ +<script> +console.log(1); +//# sourceURL=nicename.js +</script> diff --git a/remote/test/puppeteer/test/assets/jscoverage/unused.html b/remote/test/puppeteer/test/assets/jscoverage/unused.html new file mode 100644 index 0000000000..59c4a5a70b --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/unused.html @@ -0,0 +1 @@ +<script>function foo() { }</script> diff --git a/remote/test/puppeteer/test/assets/mobile.html b/remote/test/puppeteer/test/assets/mobile.html new file mode 100644 index 0000000000..8e94b2fe29 --- /dev/null +++ b/remote/test/puppeteer/test/assets/mobile.html @@ -0,0 +1 @@ +<meta name = "viewport" content = "initial-scale = 1, user-scalable = no"> diff --git a/remote/test/puppeteer/test/assets/modernizr.js b/remote/test/puppeteer/test/assets/modernizr.js new file mode 100644 index 0000000000..7991a4ec40 --- /dev/null +++ b/remote/test/puppeteer/test/assets/modernizr.js @@ -0,0 +1,3 @@ +/*! modernizr 3.5.0 (Custom Build) | MIT * +* https://modernizr.com/download/?-touchevents-setclasses !*/ +!function(e,n,t){function o(e,n){return typeof e===n}function s(){var e,n,t,s,a,i,r;for(var l in c)if(c.hasOwnProperty(l)){if(e=[],n=c[l],n.name&&(e.push(n.name.toLowerCase()),n.options&&n.options.aliases&&n.options.aliases.length))for(t=0;t<n.options.aliases.length;t++)e.push(n.options.aliases[t].toLowerCase());for(s=o(n.fn,"function")?n.fn():n.fn,a=0;a<e.length;a++)i=e[a],r=i.split("."),1===r.length?Modernizr[r[0]]=s:(!Modernizr[r[0]]||Modernizr[r[0]]instanceof Boolean||(Modernizr[r[0]]=new Boolean(Modernizr[r[0]])),Modernizr[r[0]][r[1]]=s),f.push((s?"":"no-")+r.join("-"))}}function a(e){var n=u.className,t=Modernizr._config.classPrefix||"";if(p&&(n=n.baseVal),Modernizr._config.enableJSClass){var o=new RegExp("(^|\\s)"+t+"no-js(\\s|$)");n=n.replace(o,"$1"+t+"js$2")}Modernizr._config.enableClasses&&(n+=" "+t+e.join(" "+t),p?u.className.baseVal=n:u.className=n)}function i(){return"function"!=typeof n.createElement?n.createElement(arguments[0]):p?n.createElementNS.call(n,"http://www.w3.org/2000/svg",arguments[0]):n.createElement.apply(n,arguments)}function r(){var e=n.body;return e||(e=i(p?"svg":"body"),e.fake=!0),e}function l(e,t,o,s){var a,l,f,c,d="modernizr",p=i("div"),h=r();if(parseInt(o,10))for(;o--;)f=i("div"),f.id=s?s[o]:d+(o+1),p.appendChild(f);return a=i("style"),a.type="text/css",a.id="s"+d,(h.fake?h:p).appendChild(a),h.appendChild(p),a.styleSheet?a.styleSheet.cssText=e:a.appendChild(n.createTextNode(e)),p.id=d,h.fake&&(h.style.background="",h.style.overflow="hidden",c=u.style.overflow,u.style.overflow="hidden",u.appendChild(h)),l=t(p,e),h.fake?(h.parentNode.removeChild(h),u.style.overflow=c,u.offsetHeight):p.parentNode.removeChild(p),!!l}var f=[],c=[],d={_version:"3.5.0",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,n){var t=this;setTimeout(function(){n(t[e])},0)},addTest:function(e,n,t){c.push({name:e,fn:n,options:t})},addAsyncTest:function(e){c.push({name:null,fn:e})}},Modernizr=function(){};Modernizr.prototype=d,Modernizr=new Modernizr;var u=n.documentElement,p="svg"===u.nodeName.toLowerCase(),h=d._config.usePrefixes?" -webkit- -moz- -o- -ms- ".split(" "):["",""];d._prefixes=h;var m=d.testStyles=l;Modernizr.addTest("touchevents",function(){var t;if("ontouchstart"in e||e.DocumentTouch&&n instanceof DocumentTouch)t=!0;else{var o=["@media (",h.join("touch-enabled),("),"heartz",")","{#modernizr{top:9px;position:absolute}}"].join("");m(o,function(e){t=9===e.offsetTop})}return t}),s(),a(f),delete d.addTest,delete d.addAsyncTest;for(var v=0;v<Modernizr._q.length;v++)Modernizr._q[v]();e.Modernizr=Modernizr}(window,document); diff --git a/remote/test/puppeteer/test/assets/networkidle.html b/remote/test/puppeteer/test/assets/networkidle.html new file mode 100644 index 0000000000..910ae1736d --- /dev/null +++ b/remote/test/puppeteer/test/assets/networkidle.html @@ -0,0 +1,19 @@ +<script> + async function sleep(delay) { + return new Promise(resolve => setTimeout(resolve, delay)); + } + + async function main() { + const roundOne = Promise.all([ + fetch('fetch-request-a.js'), + fetch('fetch-request-b.js'), + fetch('fetch-request-c.js'), + ]); + + await roundOne; + await sleep(50); + await fetch('fetch-request-d.js'); + } + + main(); +</script> diff --git a/remote/test/puppeteer/test/assets/offscreenbuttons.html b/remote/test/puppeteer/test/assets/offscreenbuttons.html new file mode 100644 index 0000000000..d45e2a4129 --- /dev/null +++ b/remote/test/puppeteer/test/assets/offscreenbuttons.html @@ -0,0 +1,36 @@ +<style> + button { + position: absolute; + width: 100px; + height: 20px; + } + + #btn0 { right: 0px; top: 0; } + #btn1 { right: -10px; top: 25px; } + #btn2 { right: -20px; top: 50px; } + #btn3 { right: -30px; top: 75px; } + #btn4 { right: -40px; top: 100px; } + #btn5 { right: -50px; top: 125px; } + #btn6 { right: -60px; top: 150px; } + #btn7 { right: -70px; top: 175px; } + #btn8 { right: -80px; top: 200px; } + #btn9 { right: -90px; top: 225px; } + #btn10 { right: -100px; top: 250px; } +</style> +<button id=btn0>0</button> +<button id=btn1>1</button> +<button id=btn2>2</button> +<button id=btn3>3</button> +<button id=btn4>4</button> +<button id=btn5>5</button> +<button id=btn6>6</button> +<button id=btn7>7</button> +<button id=btn8>8</button> +<button id=btn9>9</button> +<button id=btn10>10</button> +<script> +window.addEventListener('DOMContentLoaded', () => { + for (const button of Array.from(document.querySelectorAll('button'))) + button.addEventListener('click', () => console.log('button #' + button.textContent + ' clicked'), false); +}, false); +</script> diff --git a/remote/test/puppeteer/test/assets/one-style.css b/remote/test/puppeteer/test/assets/one-style.css new file mode 100644 index 0000000000..7b26410d8a --- /dev/null +++ b/remote/test/puppeteer/test/assets/one-style.css @@ -0,0 +1,3 @@ +body { + background-color: pink; +} diff --git a/remote/test/puppeteer/test/assets/one-style.html b/remote/test/puppeteer/test/assets/one-style.html new file mode 100644 index 0000000000..4760f2b9f7 --- /dev/null +++ b/remote/test/puppeteer/test/assets/one-style.html @@ -0,0 +1,2 @@ +<link rel='stylesheet' href='./one-style.css'> +<div>hello, world!</div> diff --git a/remote/test/puppeteer/test/assets/playground.html b/remote/test/puppeteer/test/assets/playground.html new file mode 100644 index 0000000000..828cfb1c70 --- /dev/null +++ b/remote/test/puppeteer/test/assets/playground.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> + <head> + <title>Playground</title> + </head> + <body> + <button>A button</button> + <textarea>A text area</textarea> + <div id="first">First div</div> + <div id="second"> + Second div + <span class="inner">Inner span</span> + </div> + </body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/popup/popup.html b/remote/test/puppeteer/test/assets/popup/popup.html new file mode 100644 index 0000000000..b855162c25 --- /dev/null +++ b/remote/test/puppeteer/test/assets/popup/popup.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <title>Popup</title> + </head> + <body> + I am a popup + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/popup/window-open.html b/remote/test/puppeteer/test/assets/popup/window-open.html new file mode 100644 index 0000000000..d138be1d22 --- /dev/null +++ b/remote/test/puppeteer/test/assets/popup/window-open.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> + <head> + <title>Popup test</title> + </head> + <body> + <script> + window.open('./popup.html'); + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/pptr.png b/remote/test/puppeteer/test/assets/pptr.png Binary files differnew file mode 100644 index 0000000000..65d87c68e6 --- /dev/null +++ b/remote/test/puppeteer/test/assets/pptr.png diff --git a/remote/test/puppeteer/test/assets/resetcss.html b/remote/test/puppeteer/test/assets/resetcss.html new file mode 100644 index 0000000000..e4e04b1f8a --- /dev/null +++ b/remote/test/puppeteer/test/assets/resetcss.html @@ -0,0 +1,50 @@ +<style> +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} +</style> diff --git a/remote/test/puppeteer/test/assets/self-request.html b/remote/test/puppeteer/test/assets/self-request.html new file mode 100644 index 0000000000..88aff620ff --- /dev/null +++ b/remote/test/puppeteer/test/assets/self-request.html @@ -0,0 +1,5 @@ +<script> +var req = new XMLHttpRequest(); +req.open('GET', '/self-request.html'); +req.send(null); +</script> diff --git a/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.html b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.html new file mode 100644 index 0000000000..bef85d985b --- /dev/null +++ b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.html @@ -0,0 +1,3 @@ +<script> + window.registrationPromise = navigator.serviceWorker.register('sw.js'); +</script> diff --git a/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.js b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.js diff --git a/remote/test/puppeteer/test/assets/serviceworkers/fetch/style.css b/remote/test/puppeteer/test/assets/serviceworkers/fetch/style.css new file mode 100644 index 0000000000..7b26410d8a --- /dev/null +++ b/remote/test/puppeteer/test/assets/serviceworkers/fetch/style.css @@ -0,0 +1,3 @@ +body { + background-color: pink; +} diff --git a/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.html b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.html new file mode 100644 index 0000000000..a9d28acb09 --- /dev/null +++ b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.html @@ -0,0 +1,5 @@ +<link rel="stylesheet" href="./style.css"> +<script> + window.registrationPromise = navigator.serviceWorker.register('sw.js'); + window.activationPromise = new Promise(resolve => navigator.serviceWorker.oncontrollerchange = resolve); +</script> diff --git a/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.js b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.js new file mode 100644 index 0000000000..21381484b6 --- /dev/null +++ b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.js @@ -0,0 +1,7 @@ +self.addEventListener('fetch', (event) => { + event.respondWith(fetch(event.request)); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil(clients.claim()); +}); diff --git a/remote/test/puppeteer/test/assets/shadow.html b/remote/test/puppeteer/test/assets/shadow.html new file mode 100644 index 0000000000..3796ca768c --- /dev/null +++ b/remote/test/puppeteer/test/assets/shadow.html @@ -0,0 +1,17 @@ +<script> + +let h1 = null; +window.button = null; +window.clicked = false; + +window.addEventListener('DOMContentLoaded', () => { + const shadowRoot = document.body.attachShadow({mode: 'open'}); + h1 = document.createElement('h1'); + h1.textContent = 'Hellow Shadow DOM v1'; + button = document.createElement('button'); + button.textContent = 'Click'; + button.addEventListener('click', () => clicked = true); + shadowRoot.appendChild(h1); + shadowRoot.appendChild(button); +}); +</script> diff --git a/remote/test/puppeteer/test/assets/simple-extension/content-script.js b/remote/test/puppeteer/test/assets/simple-extension/content-script.js new file mode 100644 index 0000000000..0fd83b90f1 --- /dev/null +++ b/remote/test/puppeteer/test/assets/simple-extension/content-script.js @@ -0,0 +1,2 @@ +console.log('hey from the content-script'); +self.thisIsTheContentScript = true; diff --git a/remote/test/puppeteer/test/assets/simple-extension/index.js b/remote/test/puppeteer/test/assets/simple-extension/index.js new file mode 100644 index 0000000000..a0bb3f4eae --- /dev/null +++ b/remote/test/puppeteer/test/assets/simple-extension/index.js @@ -0,0 +1,2 @@ +// Mock script for background extension +window.MAGIC = 42; diff --git a/remote/test/puppeteer/test/assets/simple-extension/manifest.json b/remote/test/puppeteer/test/assets/simple-extension/manifest.json new file mode 100644 index 0000000000..da2cd082ed --- /dev/null +++ b/remote/test/puppeteer/test/assets/simple-extension/manifest.json @@ -0,0 +1,14 @@ +{ + "name": "Simple extension", + "version": "0.1", + "background": { + "scripts": ["index.js"] + }, + "content_scripts": [{ + "matches": ["<all_urls>"], + "css": [], + "js": ["content-script.js"] + }], + "permissions": ["background", "activeTab"], + "manifest_version": 2 +} diff --git a/remote/test/puppeteer/test/assets/simple.json b/remote/test/puppeteer/test/assets/simple.json new file mode 100644 index 0000000000..6d95903051 --- /dev/null +++ b/remote/test/puppeteer/test/assets/simple.json @@ -0,0 +1 @@ +{"foo": "bar"} diff --git a/remote/test/puppeteer/test/assets/tamperable.html b/remote/test/puppeteer/test/assets/tamperable.html new file mode 100644 index 0000000000..d027e97038 --- /dev/null +++ b/remote/test/puppeteer/test/assets/tamperable.html @@ -0,0 +1,3 @@ +<script> + window.result = window.injected; +</script>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/title.html b/remote/test/puppeteer/test/assets/title.html new file mode 100644 index 0000000000..88a86ce412 --- /dev/null +++ b/remote/test/puppeteer/test/assets/title.html @@ -0,0 +1 @@ +<title>Woof-Woof</title> diff --git a/remote/test/puppeteer/test/assets/worker/worker.html b/remote/test/puppeteer/test/assets/worker/worker.html new file mode 100644 index 0000000000..7de2d9fd9e --- /dev/null +++ b/remote/test/puppeteer/test/assets/worker/worker.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> + <head> + <title>Worker test</title> + </head> + <body> + <script> + var worker = new Worker('worker.js'); + worker.onmessage = function(message) { + console.log(message.data); + }; + </script> + </body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/worker/worker.js b/remote/test/puppeteer/test/assets/worker/worker.js new file mode 100644 index 0000000000..0626f13e58 --- /dev/null +++ b/remote/test/puppeteer/test/assets/worker/worker.js @@ -0,0 +1,16 @@ +console.log('hello from the worker'); + +function workerFunction() { + return 'worker function result'; +} + +self.addEventListener('message', (event) => { + console.log('got this data: ' + event.data); +}); + +(async function () { + while (true) { + self.postMessage(workerFunction.toString()); + await new Promise((x) => setTimeout(x, 100)); + } +})(); diff --git a/remote/test/puppeteer/test/assets/wrappedlink.html b/remote/test/puppeteer/test/assets/wrappedlink.html new file mode 100644 index 0000000000..429b6e9156 --- /dev/null +++ b/remote/test/puppeteer/test/assets/wrappedlink.html @@ -0,0 +1,32 @@ +<style> +:root { + font-family: monospace; +} + +body { + display: flex; + align-items: center; + justify-content: center; +} + +div { + width: 10ch; + word-wrap: break-word; + border: 1px solid blue; + transform: rotate(33deg); + line-height: 8ch; + padding: 2ch; +} + +a { + margin-left: 7ch; +} +</style> +<div> + <a href='#clicked'>123321</a> +</div> +<script> + document.querySelector('a').addEventListener('click', () => { + window.__clicked = true; + }); +</script> diff --git a/remote/test/puppeteer/test/browser.spec.ts b/remote/test/puppeteer/test/browser.spec.ts new file mode 100644 index 0000000000..0d06e7f60e --- /dev/null +++ b/remote/test/puppeteer/test/browser.spec.ts @@ -0,0 +1,81 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import { getTestState, setupTestBrowserHooks } from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Browser specs', function () { + setupTestBrowserHooks(); + + describe('Browser.version', function () { + it('should return whether we are in headless', async () => { + const { browser, isHeadless } = getTestState(); + + const version = await browser.version(); + expect(version.length).toBeGreaterThan(0); + expect(version.startsWith('Headless')).toBe(isHeadless); + }); + }); + + describe('Browser.userAgent', function () { + it('should include WebKit', async () => { + const { browser, isChrome } = getTestState(); + + const userAgent = await browser.userAgent(); + expect(userAgent.length).toBeGreaterThan(0); + if (isChrome) expect(userAgent).toContain('WebKit'); + else expect(userAgent).toContain('Gecko'); + }); + }); + + describe('Browser.target', function () { + it('should return browser target', async () => { + const { browser } = getTestState(); + + const target = browser.target(); + expect(target.type()).toBe('browser'); + }); + }); + + describe('Browser.process', function () { + it('should return child_process instance', async () => { + const { browser } = getTestState(); + + const process = await browser.process(); + expect(process.pid).toBeGreaterThan(0); + }); + it('should not return child_process for remote browser', async () => { + const { browser, puppeteer } = getTestState(); + + const browserWSEndpoint = browser.wsEndpoint(); + const remoteBrowser = await puppeteer.connect({ browserWSEndpoint }); + expect(remoteBrowser.process()).toBe(null); + remoteBrowser.disconnect(); + }); + }); + + describe('Browser.isConnected', () => { + it('should set the browser connected state', async () => { + const { browser, puppeteer } = getTestState(); + + const browserWSEndpoint = browser.wsEndpoint(); + const newBrowser = await puppeteer.connect({ browserWSEndpoint }); + expect(newBrowser.isConnected()).toBe(true); + newBrowser.disconnect(); + expect(newBrowser.isConnected()).toBe(false); + }); + }); +}); diff --git a/remote/test/puppeteer/test/browsercontext.spec.ts b/remote/test/puppeteer/test/browsercontext.spec.ts new file mode 100644 index 0000000000..dd2be4b673 --- /dev/null +++ b/remote/test/puppeteer/test/browsercontext.spec.ts @@ -0,0 +1,206 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions +import utils from './utils.js'; + +describe('BrowserContext', function () { + setupTestBrowserHooks(); + it('should have default context', async () => { + const { browser } = getTestState(); + expect(browser.browserContexts().length).toEqual(1); + const defaultContext = browser.browserContexts()[0]; + expect(defaultContext.isIncognito()).toBe(false); + let error = null; + await defaultContext.close().catch((error_) => (error = error_)); + expect(browser.defaultBrowserContext()).toBe(defaultContext); + expect(error.message).toContain('cannot be closed'); + }); + it('should create new incognito context', async () => { + const { browser } = getTestState(); + + expect(browser.browserContexts().length).toBe(1); + const context = await browser.createIncognitoBrowserContext(); + expect(context.isIncognito()).toBe(true); + expect(browser.browserContexts().length).toBe(2); + expect(browser.browserContexts().indexOf(context) !== -1).toBe(true); + await context.close(); + expect(browser.browserContexts().length).toBe(1); + }); + it('should close all belonging targets once closing context', async () => { + const { browser } = getTestState(); + + expect((await browser.pages()).length).toBe(1); + + const context = await browser.createIncognitoBrowserContext(); + await context.newPage(); + expect((await browser.pages()).length).toBe(2); + expect((await context.pages()).length).toBe(1); + + await context.close(); + expect((await browser.pages()).length).toBe(1); + }); + it('window.open should use parent tab context', async () => { + const { browser, server } = getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + const [popupTarget] = await Promise.all([ + utils.waitEvent(browser, 'targetcreated'), + page.evaluate<(url: string) => void>( + (url) => window.open(url), + server.EMPTY_PAGE + ), + ]); + expect(popupTarget.browserContext()).toBe(context); + await context.close(); + }); + it('should fire target events', async () => { + const { browser, server } = getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + const events = []; + context.on('targetcreated', (target) => + events.push('CREATED: ' + target.url()) + ); + context.on('targetchanged', (target) => + events.push('CHANGED: ' + target.url()) + ); + context.on('targetdestroyed', (target) => + events.push('DESTROYED: ' + target.url()) + ); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.close(); + expect(events).toEqual([ + 'CREATED: about:blank', + `CHANGED: ${server.EMPTY_PAGE}`, + `DESTROYED: ${server.EMPTY_PAGE}`, + ]); + await context.close(); + }); + it('should wait for a target', async () => { + const { browser, puppeteer, server } = getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + let resolved = false; + + const targetPromise = context.waitForTarget( + (target) => target.url() === server.EMPTY_PAGE + ); + targetPromise + .then(() => (resolved = true)) + .catch((error) => { + resolved = true; + if (error instanceof puppeteer.errors.TimeoutError) { + console.error(error); + } else throw error; + }); + const page = await context.newPage(); + expect(resolved).toBe(false); + await page.goto(server.EMPTY_PAGE); + try { + const target = await targetPromise; + expect(await target.page()).toBe(page); + } catch (error) { + if (error instanceof puppeteer.errors.TimeoutError) { + console.error(error); + } else throw error; + } + await context.close(); + }); + + it('should timeout waiting for a non-existent target', async () => { + const { browser, puppeteer, server } = getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + const error = await context + .waitForTarget((target) => target.url() === server.EMPTY_PAGE, { + timeout: 1, + }) + .catch((error_) => error_); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + await context.close(); + }); + + it('should isolate localStorage and cookies', async () => { + const { browser, server } = getTestState(); + + // Create two incognito contexts. + const context1 = await browser.createIncognitoBrowserContext(); + const context2 = await browser.createIncognitoBrowserContext(); + expect(context1.targets().length).toBe(0); + expect(context2.targets().length).toBe(0); + + // Create a page in first incognito context. + const page1 = await context1.newPage(); + await page1.goto(server.EMPTY_PAGE); + await page1.evaluate(() => { + localStorage.setItem('name', 'page1'); + document.cookie = 'name=page1'; + }); + + expect(context1.targets().length).toBe(1); + expect(context2.targets().length).toBe(0); + + // Create a page in second incognito context. + const page2 = await context2.newPage(); + await page2.goto(server.EMPTY_PAGE); + await page2.evaluate(() => { + localStorage.setItem('name', 'page2'); + document.cookie = 'name=page2'; + }); + + expect(context1.targets().length).toBe(1); + expect(context1.targets()[0]).toBe(page1.target()); + expect(context2.targets().length).toBe(1); + expect(context2.targets()[0]).toBe(page2.target()); + + // Make sure pages don't share localstorage or cookies. + expect(await page1.evaluate(() => localStorage.getItem('name'))).toBe( + 'page1' + ); + expect(await page1.evaluate(() => document.cookie)).toBe('name=page1'); + expect(await page2.evaluate(() => localStorage.getItem('name'))).toBe( + 'page2' + ); + expect(await page2.evaluate(() => document.cookie)).toBe('name=page2'); + + // Cleanup contexts. + await Promise.all([context1.close(), context2.close()]); + expect(browser.browserContexts().length).toBe(1); + }); + + it('should work across sessions', async () => { + const { browser, puppeteer } = getTestState(); + + expect(browser.browserContexts().length).toBe(1); + const context = await browser.createIncognitoBrowserContext(); + expect(browser.browserContexts().length).toBe(2); + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + }); + const contexts = remoteBrowser.browserContexts(); + expect(contexts.length).toBe(2); + remoteBrowser.disconnect(); + await context.close(); + }); +}); diff --git a/remote/test/puppeteer/test/chromiumonly.spec.ts b/remote/test/puppeteer/test/chromiumonly.spec.ts new file mode 100644 index 0000000000..7b64a70fd3 --- /dev/null +++ b/remote/test/puppeteer/test/chromiumonly.spec.ts @@ -0,0 +1,159 @@ +/** + * Copyright 2019 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeChromeOnly, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describeChromeOnly('Chromium-Specific Launcher tests', function () { + describe('Puppeteer.launch |browserURL| option', function () { + it('should be able to connect using browserUrl, with and without trailing slash', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + + const originalBrowser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + args: ['--remote-debugging-port=21222'], + }) + ); + const browserURL = 'http://127.0.0.1:21222'; + + const browser1 = await puppeteer.connect({ browserURL }); + const page1 = await browser1.newPage(); + expect(await page1.evaluate(() => 7 * 8)).toBe(56); + browser1.disconnect(); + + const browser2 = await puppeteer.connect({ + browserURL: browserURL + '/', + }); + const page2 = await browser2.newPage(); + expect(await page2.evaluate(() => 8 * 7)).toBe(56); + browser2.disconnect(); + originalBrowser.close(); + }); + it('should throw when using both browserWSEndpoint and browserURL', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + + const originalBrowser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + args: ['--remote-debugging-port=21222'], + }) + ); + const browserURL = 'http://127.0.0.1:21222'; + + let error = null; + await puppeteer + .connect({ + browserURL, + browserWSEndpoint: originalBrowser.wsEndpoint(), + }) + .catch((error_) => (error = error_)); + expect(error.message).toContain( + 'Exactly one of browserWSEndpoint, browserURL or transport' + ); + + originalBrowser.close(); + }); + it('should throw when trying to connect to non-existing browser', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + + const originalBrowser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + args: ['--remote-debugging-port=21222'], + }) + ); + const browserURL = 'http://127.0.0.1:32333'; + + let error = null; + await puppeteer + .connect({ browserURL }) + .catch((error_) => (error = error_)); + expect(error.message).toContain( + 'Failed to fetch browser webSocket URL from' + ); + originalBrowser.close(); + }); + }); + + describe('Puppeteer.launch |pipe| option', function () { + it('should support the pipe option', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + const options = Object.assign({ pipe: true }, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + expect((await browser.pages()).length).toBe(1); + expect(browser.wsEndpoint()).toBe(''); + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + await browser.close(); + }); + it('should support the pipe argument', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + const options = Object.assign({}, defaultBrowserOptions); + options.args = ['--remote-debugging-pipe'].concat(options.args || []); + const browser = await puppeteer.launch(options); + expect(browser.wsEndpoint()).toBe(''); + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + await browser.close(); + }); + it('should fire "disconnected" when closing with pipe', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + const options = Object.assign({ pipe: true }, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + const disconnectedEventPromise = new Promise((resolve) => + browser.once('disconnected', resolve) + ); + // Emulate user exiting browser. + browser.process().kill(); + await disconnectedEventPromise; + }); + }); +}); + +describeChromeOnly('Chromium-Specific Page Tests', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('Page.setRequestInterception should work with intervention headers', async () => { + const { server, page } = getTestState(); + + server.setRoute('/intervention', (req, res) => + res.end(` + <script> + document.write('<script src="${server.CROSS_PROCESS_PREFIX}/intervention.js">' + '</scr' + 'ipt>'); + </script> + `) + ); + server.setRedirect('/intervention.js', '/redirect.js'); + let serverRequest = null; + server.setRoute('/redirect.js', (req, res) => { + serverRequest = req; + res.end('console.log(1);'); + }); + + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + await page.goto(server.PREFIX + '/intervention'); + // Check for feature URL substring rather than https://www.chromestatus.com to + // make it work with Edgium. + expect(serverRequest.headers.intervention).toContain( + 'feature/5718547946799104' + ); + }); +}); diff --git a/remote/test/puppeteer/test/click.spec.ts b/remote/test/puppeteer/test/click.spec.ts new file mode 100644 index 0000000000..48d4b64409 --- /dev/null +++ b/remote/test/puppeteer/test/click.spec.ts @@ -0,0 +1,352 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import { + getTestState, + setupTestPageAndContextHooks, + setupTestBrowserHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import utils from './utils.js'; + +describe('Page.click', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('should click the button', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + await page.click('button'); + expect(await page.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + it('should click svg', async () => { + const { page } = getTestState(); + + await page.setContent(` + <svg height="100" width="100"> + <circle onclick="javascript:window.__CLICKED=42" cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" /> + </svg> + `); + await page.click('circle'); + expect(await page.evaluate(() => globalThis.__CLICKED)).toBe(42); + }); + it( + 'should click the button if window.Node is removed', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + await page.evaluate(() => delete window.Node); + await page.click('button'); + expect(await page.evaluate(() => globalThis.result)).toBe('Clicked'); + } + ); + // @see https://github.com/puppeteer/puppeteer/issues/4281 + it('should click on a span with an inline element inside', async () => { + const { page } = getTestState(); + + await page.setContent(` + <style> + span::before { + content: 'q'; + } + </style> + <span onclick='javascript:window.CLICKED=42'></span> + `); + await page.click('span'); + expect(await page.evaluate(() => globalThis.CLICKED)).toBe(42); + }); + it('should not throw UnhandledPromiseRejection when page closes', async () => { + const { page } = getTestState(); + + const newPage = await page.browser().newPage(); + await Promise.all([ + newPage.close(), + newPage.mouse.click(1, 2), + ]).catch(() => {}); + }); + it('should click the button after navigation ', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + await page.click('button'); + await page.goto(server.PREFIX + '/input/button.html'); + await page.click('button'); + expect(await page.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + it('should click with disabled javascript', async () => { + const { page, server } = getTestState(); + + await page.setJavaScriptEnabled(false); + await page.goto(server.PREFIX + '/wrappedlink.html'); + await Promise.all([page.click('a'), page.waitForNavigation()]); + expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked'); + }); + it('should click when one of inline box children is outside of viewport', async () => { + const { page } = getTestState(); + + await page.setContent(` + <style> + i { + position: absolute; + top: -1000px; + } + </style> + <span onclick='javascript:window.CLICKED = 42;'><i>woof</i><b>doggo</b></span> + `); + await page.click('span'); + expect(await page.evaluate(() => globalThis.CLICKED)).toBe(42); + }); + it('should select the text by triple clicking', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + const text = + "This is the text that we are going to try to select. Let's see how it goes."; + await page.keyboard.type(text); + await page.click('textarea'); + await page.click('textarea', { clickCount: 2 }); + await page.click('textarea', { clickCount: 3 }); + expect( + await page.evaluate(() => { + const textarea = document.querySelector('textarea'); + return textarea.value.substring( + textarea.selectionStart, + textarea.selectionEnd + ); + }) + ).toBe(text); + }); + it('should click offscreen buttons', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + const messages = []; + page.on('console', (msg) => messages.push(msg.text())); + for (let i = 0; i < 11; ++i) { + // We might've scrolled to click a button - reset to (0, 0). + await page.evaluate(() => window.scrollTo(0, 0)); + await page.click(`#btn${i}`); + } + expect(messages).toEqual([ + 'button #0 clicked', + 'button #1 clicked', + 'button #2 clicked', + 'button #3 clicked', + 'button #4 clicked', + 'button #5 clicked', + 'button #6 clicked', + 'button #7 clicked', + 'button #8 clicked', + 'button #9 clicked', + 'button #10 clicked', + ]); + }); + + it('should click wrapped links', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/wrappedlink.html'); + await page.click('a'); + expect(await page.evaluate(() => globalThis.__clicked)).toBe(true); + }); + + it('should click on checkbox input and toggle', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/checkbox.html'); + expect(await page.evaluate(() => globalThis.result.check)).toBe(null); + await page.click('input#agree'); + expect(await page.evaluate(() => globalThis.result.check)).toBe(true); + expect(await page.evaluate(() => globalThis.result.events)).toEqual([ + 'mouseover', + 'mouseenter', + 'mousemove', + 'mousedown', + 'mouseup', + 'click', + 'input', + 'change', + ]); + await page.click('input#agree'); + expect(await page.evaluate(() => globalThis.result.check)).toBe(false); + }); + + it('should click on checkbox label and toggle', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/checkbox.html'); + expect(await page.evaluate(() => globalThis.result.check)).toBe(null); + await page.click('label[for="agree"]'); + expect(await page.evaluate(() => globalThis.result.check)).toBe(true); + expect(await page.evaluate(() => globalThis.result.events)).toEqual([ + 'click', + 'input', + 'change', + ]); + await page.click('label[for="agree"]'); + expect(await page.evaluate(() => globalThis.result.check)).toBe(false); + }); + + it('should fail to click a missing button', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + let error = null; + await page + .click('button.does-not-exist') + .catch((error_) => (error = error_)); + expect(error.message).toBe( + 'No node found for selector: button.does-not-exist' + ); + }); + // @see https://github.com/puppeteer/puppeteer/issues/161 + it('should not hang with touch-enabled viewports', async () => { + const { page, puppeteer } = getTestState(); + + await page.setViewport(puppeteer.devices['iPhone 6'].viewport); + await page.mouse.down(); + await page.mouse.move(100, 10); + await page.mouse.up(); + }); + it('should scroll and click the button', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.click('#button-5'); + expect( + await page.evaluate(() => document.querySelector('#button-5').textContent) + ).toBe('clicked'); + await page.click('#button-80'); + expect( + await page.evaluate( + () => document.querySelector('#button-80').textContent + ) + ).toBe('clicked'); + }); + it('should double click the button', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + await page.evaluate(() => { + globalThis.double = false; + const button = document.querySelector('button'); + button.addEventListener('dblclick', () => { + globalThis.double = true; + }); + }); + const button = await page.$('button'); + await button.click({ clickCount: 2 }); + expect(await page.evaluate('double')).toBe(true); + expect(await page.evaluate('result')).toBe('Clicked'); + }); + it('should click a partially obscured button', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + await page.evaluate(() => { + const button = document.querySelector('button'); + button.textContent = 'Some really long text that will go offscreen'; + button.style.position = 'absolute'; + button.style.left = '368px'; + }); + await page.click('button'); + expect(await page.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + it('should click a rotated button', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/rotatedButton.html'); + await page.click('button'); + expect(await page.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + it('should fire contextmenu event on right click', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.click('#button-8', { button: 'right' }); + expect( + await page.evaluate(() => document.querySelector('#button-8').textContent) + ).toBe('context menu'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/206 + it('should click links which cause navigation', async () => { + const { page, server } = getTestState(); + + await page.setContent(`<a href="${server.EMPTY_PAGE}">empty.html</a>`); + // This await should not hang. + await page.click('a'); + }); + it('should click the button inside an iframe', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent('<div style="width:100px;height:100px">spacer</div>'); + await utils.attachFrame( + page, + 'button-test', + server.PREFIX + '/input/button.html' + ); + const frame = page.frames()[1]; + const button = await frame.$('button'); + await button.click(); + expect(await frame.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4110 + xit('should click the button with fixed position inside an iframe', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setViewport({ width: 500, height: 500 }); + await page.setContent( + '<div style="width:100px;height:2000px">spacer</div>' + ); + await utils.attachFrame( + page, + 'button-test', + server.CROSS_PROCESS_PREFIX + '/input/button.html' + ); + const frame = page.frames()[1]; + await frame.$eval('button', (button: HTMLElement) => + button.style.setProperty('position', 'fixed') + ); + await frame.click('button'); + expect(await frame.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + it( + 'should click the button with deviceScaleFactor set', + async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 400, height: 400, deviceScaleFactor: 5 }); + expect(await page.evaluate(() => window.devicePixelRatio)).toBe(5); + await page.setContent( + '<div style="width:100px;height:100px">spacer</div>' + ); + await utils.attachFrame( + page, + 'button-test', + server.PREFIX + '/input/button.html' + ); + const frame = page.frames()[1]; + const button = await frame.$('button'); + await button.click(); + expect(await frame.evaluate(() => globalThis.result)).toBe('Clicked'); + } + ); +}); diff --git a/remote/test/puppeteer/test/cookies.spec.ts b/remote/test/puppeteer/test/cookies.spec.ts new file mode 100644 index 0000000000..6d3d9a8cab --- /dev/null +++ b/remote/test/puppeteer/test/cookies.spec.ts @@ -0,0 +1,526 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Cookie specs', () => { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.cookies', function () { + it('should return no cookies in pristine browser context', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + expect(await page.cookies()).toEqual([]); + }); + it('should get a cookie', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + document.cookie = 'username=John Doe'; + }); + expect(await page.cookies()).toEqual([ + { + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + }, + ]); + }); + it('should properly report httpOnly cookie', async () => { + const { page, server } = getTestState(); + server.setRoute('/empty.html', (req, res) => { + res.setHeader('Set-Cookie', 'a=b; HttpOnly; Path=/'); + res.end(); + }); + await page.goto(server.EMPTY_PAGE); + const cookies = await page.cookies(); + expect(cookies.length).toBe(1); + expect(cookies[0].httpOnly).toBe(true); + }); + it('should properly report "Strict" sameSite cookie', async () => { + const { page, server } = getTestState(); + server.setRoute('/empty.html', (req, res) => { + res.setHeader('Set-Cookie', 'a=b; SameSite=Strict'); + res.end(); + }); + await page.goto(server.EMPTY_PAGE); + const cookies = await page.cookies(); + expect(cookies.length).toBe(1); + expect(cookies[0].sameSite).toBe('Strict'); + }); + it('should properly report "Lax" sameSite cookie', async () => { + const { page, server } = getTestState(); + server.setRoute('/empty.html', (req, res) => { + res.setHeader('Set-Cookie', 'a=b; SameSite=Lax'); + res.end(); + }); + await page.goto(server.EMPTY_PAGE); + const cookies = await page.cookies(); + expect(cookies.length).toBe(1); + expect(cookies[0].sameSite).toBe('Lax'); + }); + it('should get multiple cookies', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + document.cookie = 'username=John Doe'; + document.cookie = 'password=1234'; + }); + const cookies = await page.cookies(); + cookies.sort((a, b) => a.name.localeCompare(b.name)); + expect(cookies).toEqual([ + { + name: 'password', + value: '1234', + domain: 'localhost', + path: '/', + expires: -1, + size: 12, + httpOnly: false, + secure: false, + session: true, + }, + { + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + }, + ]); + }); + it('should get cookies from multiple urls', async () => { + const { page } = getTestState(); + await page.setCookie( + { + url: 'https://foo.com', + name: 'doggo', + value: 'woofs', + }, + { + url: 'https://bar.com', + name: 'catto', + value: 'purrs', + }, + { + url: 'https://baz.com', + name: 'birdo', + value: 'tweets', + } + ); + const cookies = await page.cookies('https://foo.com', 'https://baz.com'); + cookies.sort((a, b) => a.name.localeCompare(b.name)); + expect(cookies).toEqual([ + { + name: 'birdo', + value: 'tweets', + domain: 'baz.com', + path: '/', + expires: -1, + size: 11, + httpOnly: false, + secure: true, + session: true, + }, + { + name: 'doggo', + value: 'woofs', + domain: 'foo.com', + path: '/', + expires: -1, + size: 10, + httpOnly: false, + secure: true, + session: true, + }, + ]); + }); + }); + describe('Page.setCookie', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456', + }); + expect(await page.evaluate(() => document.cookie)).toEqual( + 'password=123456' + ); + }); + it('should isolate cookies in browser contexts', async () => { + const { page, server, browser } = getTestState(); + + const anotherContext = await browser.createIncognitoBrowserContext(); + const anotherPage = await anotherContext.newPage(); + + await page.goto(server.EMPTY_PAGE); + await anotherPage.goto(server.EMPTY_PAGE); + + await page.setCookie({ name: 'page1cookie', value: 'page1value' }); + await anotherPage.setCookie({ name: 'page2cookie', value: 'page2value' }); + + const cookies1 = await page.cookies(); + const cookies2 = await anotherPage.cookies(); + expect(cookies1.length).toBe(1); + expect(cookies2.length).toBe(1); + expect(cookies1[0].name).toBe('page1cookie'); + expect(cookies1[0].value).toBe('page1value'); + expect(cookies2[0].name).toBe('page2cookie'); + expect(cookies2[0].value).toBe('page2value'); + await anotherContext.close(); + }); + it('should set multiple cookies', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie( + { + name: 'password', + value: '123456', + }, + { + name: 'foo', + value: 'bar', + } + ); + expect( + await page.evaluate(() => { + const cookies = document.cookie.split(';'); + return cookies.map((cookie) => cookie.trim()).sort(); + }) + ).toEqual(['foo=bar', 'password=123456']); + }); + it('should have |expires| set to |-1| for session cookies', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456', + }); + const cookies = await page.cookies(); + expect(cookies[0].session).toBe(true); + expect(cookies[0].expires).toBe(-1); + }); + it('should set cookie with reasonable defaults', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456', + }); + const cookies = await page.cookies(); + expect(cookies.sort((a, b) => a.name.localeCompare(b.name))).toEqual([ + { + name: 'password', + value: '123456', + domain: 'localhost', + path: '/', + expires: -1, + size: 14, + httpOnly: false, + secure: false, + session: true, + }, + ]); + }); + it('should set a cookie with a path', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/grid.html'); + await page.setCookie({ + name: 'gridcookie', + value: 'GRID', + path: '/grid.html', + }); + expect(await page.cookies()).toEqual([ + { + name: 'gridcookie', + value: 'GRID', + domain: 'localhost', + path: '/grid.html', + expires: -1, + size: 14, + httpOnly: false, + secure: false, + session: true, + }, + ]); + expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID'); + await page.goto(server.EMPTY_PAGE); + expect(await page.cookies()).toEqual([]); + expect(await page.evaluate('document.cookie')).toBe(''); + await page.goto(server.PREFIX + '/grid.html'); + expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID'); + }); + it('should not set a cookie on a blank page', async () => { + const { page } = getTestState(); + + await page.goto('about:blank'); + let error = null; + try { + await page.setCookie({ name: 'example-cookie', value: 'best' }); + } catch (error_) { + error = error_; + } + expect(error.message).toContain( + 'At least one of the url and domain needs to be specified' + ); + }); + it('should not set a cookie with blank page URL', async () => { + const { page, server } = getTestState(); + + let error = null; + await page.goto(server.EMPTY_PAGE); + try { + await page.setCookie( + { name: 'example-cookie', value: 'best' }, + { url: 'about:blank', name: 'example-cookie-blank', value: 'best' } + ); + } catch (error_) { + error = error_; + } + expect(error.message).toEqual( + `Blank page can not have cookie "example-cookie-blank"` + ); + }); + it('should not set a cookie on a data URL page', async () => { + const { page } = getTestState(); + + let error = null; + await page.goto('data:,Hello%2C%20World!'); + try { + await page.setCookie({ name: 'example-cookie', value: 'best' }); + } catch (error_) { + error = error_; + } + expect(error.message).toContain( + 'At least one of the url and domain needs to be specified' + ); + }); + it( + 'should default to setting secure cookie for HTTPS websites', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const SECURE_URL = 'https://example.com'; + await page.setCookie({ + url: SECURE_URL, + name: 'foo', + value: 'bar', + }); + const [cookie] = await page.cookies(SECURE_URL); + expect(cookie.secure).toBe(true); + } + ); + it('should be able to set unsecure cookie for HTTP website', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const HTTP_URL = 'http://example.com'; + await page.setCookie({ + url: HTTP_URL, + name: 'foo', + value: 'bar', + }); + const [cookie] = await page.cookies(HTTP_URL); + expect(cookie.secure).toBe(false); + }); + it('should set a cookie on a different domain', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + url: 'https://www.example.com', + name: 'example-cookie', + value: 'best', + }); + expect(await page.evaluate('document.cookie')).toBe(''); + expect(await page.cookies()).toEqual([]); + expect(await page.cookies('https://www.example.com')).toEqual([ + { + name: 'example-cookie', + value: 'best', + domain: 'www.example.com', + path: '/', + expires: -1, + size: 18, + httpOnly: false, + secure: true, + session: true, + }, + ]); + }); + it('should set cookies from a frame', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/grid.html'); + await page.setCookie({ name: 'localhost-cookie', value: 'best' }); + await page.evaluate<(src: string) => Promise<void>>((src) => { + let fulfill; + const promise = new Promise<void>((x) => (fulfill = x)); + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.onload = fulfill; + iframe.src = src; + return promise; + }, server.CROSS_PROCESS_PREFIX); + await page.setCookie({ + name: '127-cookie', + value: 'worst', + url: server.CROSS_PROCESS_PREFIX, + }); + expect(await page.evaluate('document.cookie')).toBe( + 'localhost-cookie=best' + ); + expect(await page.frames()[1].evaluate('document.cookie')).toBe(''); + + expect(await page.cookies()).toEqual([ + { + name: 'localhost-cookie', + value: 'best', + domain: 'localhost', + path: '/', + expires: -1, + size: 20, + httpOnly: false, + secure: false, + session: true, + }, + ]); + + expect(await page.cookies(server.CROSS_PROCESS_PREFIX)).toEqual([ + { + name: '127-cookie', + value: 'worst', + domain: '127.0.0.1', + path: '/', + expires: -1, + size: 15, + httpOnly: false, + secure: false, + session: true, + }, + ]); + }); + it( + 'should set secure same-site cookies from a frame', + async () => { + const { + httpsServer, + puppeteer, + defaultBrowserOptions, + } = getTestState(); + + const browser = await puppeteer.launch({ + ...defaultBrowserOptions, + ignoreHTTPSErrors: true, + }); + + const page = await browser.newPage(); + + try { + await page.goto(httpsServer.PREFIX + '/grid.html'); + await page.evaluate<(src: string) => Promise<void>>((src) => { + let fulfill; + const promise = new Promise<void>((x) => (fulfill = x)); + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.onload = fulfill; + iframe.src = src; + return promise; + }, httpsServer.CROSS_PROCESS_PREFIX); + await page.setCookie({ + name: '127-same-site-cookie', + value: 'best', + url: httpsServer.CROSS_PROCESS_PREFIX, + sameSite: 'None', + }); + + expect(await page.frames()[1].evaluate('document.cookie')).toBe( + '127-same-site-cookie=best' + ); + expect(await page.cookies(httpsServer.CROSS_PROCESS_PREFIX)).toEqual([ + { + name: '127-same-site-cookie', + value: 'best', + domain: '127.0.0.1', + path: '/', + expires: -1, + size: 24, + httpOnly: false, + sameSite: 'None', + secure: true, + session: true, + }, + ]); + } finally { + await page.close(); + await browser.close(); + } + } + ); + }); + + describe('Page.deleteCookie', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie( + { + name: 'cookie1', + value: '1', + }, + { + name: 'cookie2', + value: '2', + }, + { + name: 'cookie3', + value: '3', + } + ); + expect(await page.evaluate('document.cookie')).toBe( + 'cookie1=1; cookie2=2; cookie3=3' + ); + await page.deleteCookie({ name: 'cookie2' }); + expect(await page.evaluate('document.cookie')).toBe( + 'cookie1=1; cookie3=3' + ); + }); + }); +}); diff --git a/remote/test/puppeteer/test/coverage-utils.js b/remote/test/puppeteer/test/coverage-utils.js new file mode 100644 index 0000000000..c23e507f9d --- /dev/null +++ b/remote/test/puppeteer/test/coverage-utils.js @@ -0,0 +1,164 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO (@jackfranklin): convert this to TypeScript and enable type-checking +// @ts-nocheck + +/* We want to ensure that all of Puppeteer's public API is tested via our unit + * tests but we can't use a tool like Istanbul because the way it instruments + * code unfortunately breaks in Puppeteer where some of that code is then being + * executed in a browser context. + * + * So instead we maintain this coverage code which does the following: + * * takes every public method that we expect to be tested + * * replaces it with a method that calls the original but also updates a Map of calls + * * in an after() test callback it asserts that every public method was called. + * + * We run this when COVERAGE=1. + */ + +const path = require('path'); +const fs = require('fs'); + +/** + * This object is also used by DocLint to know which classes to check are + * documented. It's a pretty hacky solution but DocLint is going away soon as + * part of the TSDoc migration. + */ +const MODULES_TO_CHECK_FOR_COVERAGE = { + Accessibility: '../lib/cjs/puppeteer/common/Accessibility', + Browser: '../lib/cjs/puppeteer/common/Browser', + BrowserContext: '../lib/cjs/puppeteer/common/Browser', + BrowserFetcher: '../lib/cjs/puppeteer/node/BrowserFetcher', + CDPSession: '../lib/cjs/puppeteer/common/Connection', + ConsoleMessage: '../lib/cjs/puppeteer/common/ConsoleMessage', + Coverage: '../lib/cjs/puppeteer/common/Coverage', + Dialog: '../lib/cjs/puppeteer/common/Dialog', + ElementHandle: '../lib/cjs/puppeteer/common/JSHandle', + ExecutionContext: '../lib/cjs/puppeteer/common/ExecutionContext', + EventEmitter: '../lib/cjs/puppeteer/common/EventEmitter', + FileChooser: '../lib/cjs/puppeteer/common/FileChooser', + Frame: '../lib/cjs/puppeteer/common/FrameManager', + JSHandle: '../lib/cjs/puppeteer/common/JSHandle', + Keyboard: '../lib/cjs/puppeteer/common/Input', + Mouse: '../lib/cjs/puppeteer/common/Input', + Page: '../lib/cjs/puppeteer/common/Page', + Puppeteer: '../lib/cjs/puppeteer/common/Puppeteer', + PuppeteerNode: '../lib/cjs/puppeteer/node/Puppeteer', + HTTPRequest: '../lib/cjs/puppeteer/common/HTTPRequest', + HTTPResponse: '../lib/cjs/puppeteer/common/HTTPResponse', + SecurityDetails: '../lib/cjs/puppeteer/common/SecurityDetails', + Target: '../lib/cjs/puppeteer/common/Target', + TimeoutError: '../lib/cjs/puppeteer/common/Errors', + Touchscreen: '../lib/cjs/puppeteer/common/Input', + Tracing: '../lib/cjs/puppeteer/common/Tracing', + WebWorker: '../lib/cjs/puppeteer/common/WebWorker', +}; + +function traceAPICoverage(apiCoverage, className, modulePath) { + const loadedModule = require(modulePath); + const classType = loadedModule[className]; + + if (!classType || !classType.prototype) { + console.error( + `Coverage error: could not find class for ${className}. Is src/api.ts up to date?` + ); + process.exit(1); + } + for (const methodName of Reflect.ownKeys(classType.prototype)) { + const method = Reflect.get(classType.prototype, methodName); + if ( + methodName === 'constructor' || + typeof methodName !== 'string' || + methodName.startsWith('_') || + typeof method !== 'function' + ) + continue; + apiCoverage.set(`${className}.${methodName}`, false); + Reflect.set(classType.prototype, methodName, function (...args) { + apiCoverage.set(`${className}.${methodName}`, true); + return method.call(this, ...args); + }); + } + + /** + * If classes emit events, those events are exposed via an object in the same + * module named XEmittedEvents, where X is the name of the class. For example, + * the Page module exposes PageEmittedEvents. + */ + const eventsName = `${className}EmittedEvents`; + if (loadedModule[eventsName]) { + for (const event of Object.values(loadedModule[eventsName])) { + if (typeof event !== 'symbol') + apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false); + } + const method = Reflect.get(classType.prototype, 'emit'); + Reflect.set(classType.prototype, 'emit', function (event, ...args) { + if (typeof event !== 'symbol' && this.listenerCount(event)) + apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true); + return method.call(this, event, ...args); + }); + } +} + +const coverageLocation = path.join(__dirname, 'coverage.json'); + +const clearOldCoverage = () => { + try { + fs.unlinkSync(coverageLocation); + } catch (error) { + // do nothing, the file didn't exist + } +}; +const writeCoverage = (coverage) => { + fs.writeFileSync(coverageLocation, JSON.stringify([...coverage.entries()])); +}; + +const getCoverageResults = () => { + let contents; + try { + contents = fs.readFileSync(coverageLocation, { encoding: 'utf8' }); + } catch (error) { + console.error('Warning: coverage file does not exist or is not readable.'); + } + + const coverageMap = new Map(JSON.parse(contents)); + return coverageMap; +}; + +const trackCoverage = () => { + clearOldCoverage(); + const coverageMap = new Map(); + + return { + beforeAll: () => { + for (const [className, moduleFilePath] of Object.entries( + MODULES_TO_CHECK_FOR_COVERAGE + )) { + traceAPICoverage(coverageMap, className, moduleFilePath); + } + }, + afterAll: () => { + writeCoverage(coverageMap); + }, + }; +}; + +module.exports = { + trackCoverage, + getCoverageResults, + MODULES_TO_CHECK_FOR_COVERAGE, +}; diff --git a/remote/test/puppeteer/test/coverage.spec.ts b/remote/test/puppeteer/test/coverage.spec.ts new file mode 100644 index 0000000000..d3aa3bf225 --- /dev/null +++ b/remote/test/puppeteer/test/coverage.spec.ts @@ -0,0 +1,280 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import { + getTestState, + setupTestPageAndContextHooks, + setupTestBrowserHooks, + describeChromeOnly, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Coverage specs', function () { + describeChromeOnly('JSCoverage', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should work', async () => { + const { page, server } = getTestState(); + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/simple.html', { + waitUntil: 'networkidle0', + }); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].url).toContain('/jscoverage/simple.html'); + expect(coverage[0].ranges).toEqual([ + { start: 0, end: 17 }, + { start: 35, end: 61 }, + ]); + }); + it('should report sourceURLs', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/sourceurl.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].url).toBe('nicename.js'); + }); + it('should ignore eval() scripts by default', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/eval.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + }); + it("shouldn't ignore eval() scripts if reportAnonymousScripts is true", async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage({ reportAnonymousScripts: true }); + await page.goto(server.PREFIX + '/jscoverage/eval.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect( + coverage.find((entry) => entry.url.startsWith('debugger://')) + ).not.toBe(null); + expect(coverage.length).toBe(2); + }); + it('should ignore pptr internal scripts if reportAnonymousScripts is true', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage({ reportAnonymousScripts: true }); + await page.goto(server.EMPTY_PAGE); + await page.evaluate('console.log("foo")'); + await page.evaluate(() => console.log('bar')); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(0); + }); + it('should report multiple scripts', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/multiple.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(2); + coverage.sort((a, b) => a.url.localeCompare(b.url)); + expect(coverage[0].url).toContain('/jscoverage/script1.js'); + expect(coverage[1].url).toContain('/jscoverage/script2.js'); + }); + it('should report right ranges', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/ranges.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + const entry = coverage[0]; + expect(entry.ranges.length).toBe(1); + const range = entry.ranges[0]; + expect(entry.text.substring(range.start, range.end)).toBe( + `console.log('used!');` + ); + }); + it('should report scripts that have no coverage', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/unused.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + const entry = coverage[0]; + expect(entry.url).toContain('unused.html'); + expect(entry.ranges.length).toBe(0); + }); + it('should work with conditionals', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/involved.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect( + JSON.stringify(coverage, null, 2).replace(/:\d{4}\//g, ':<PORT>/') + ).toBeGolden('jscoverage-involved.txt'); + }); + describe('resetOnNavigation', function () { + it('should report scripts across navigations when disabled', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage({ resetOnNavigation: false }); + await page.goto(server.PREFIX + '/jscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(2); + }); + + it('should NOT report scripts across navigations when enabled', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage(); // Enabled by default. + await page.goto(server.PREFIX + '/jscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(0); + }); + }); + // @see https://crbug.com/990945 + xit('should not hang when there is a debugger statement', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + debugger; // eslint-disable-line no-debugger + }); + await page.coverage.stopJSCoverage(); + }); + }); + + describeChromeOnly('CSSCoverage', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should work', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/simple.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].url).toContain('/csscoverage/simple.html'); + expect(coverage[0].ranges).toEqual([{ start: 1, end: 22 }]); + const range = coverage[0].ranges[0]; + expect(coverage[0].text.substring(range.start, range.end)).toBe( + 'div { color: green; }' + ); + }); + it('should report sourceURLs', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/sourceurl.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].url).toBe('nicename.css'); + }); + it('should report multiple stylesheets', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/multiple.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(2); + coverage.sort((a, b) => a.url.localeCompare(b.url)); + expect(coverage[0].url).toContain('/csscoverage/stylesheet1.css'); + expect(coverage[1].url).toContain('/csscoverage/stylesheet2.css'); + }); + it('should report stylesheets that have no coverage', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/unused.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].url).toBe('unused.css'); + expect(coverage[0].ranges.length).toBe(0); + }); + it('should work with media queries', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/media.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].url).toContain('/csscoverage/media.html'); + expect(coverage[0].ranges).toEqual([{ start: 17, end: 38 }]); + }); + it('should work with complicated usecases', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/involved.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect( + JSON.stringify(coverage, null, 2).replace(/:\d{4}\//g, ':<PORT>/') + ).toBeGolden('csscoverage-involved.txt'); + }); + it('should ignore injected stylesheets', async () => { + const { page } = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.addStyleTag({ content: 'body { margin: 10px;}' }); + // trigger style recalc + const margin = await page.evaluate( + () => window.getComputedStyle(document.body).margin + ); + expect(margin).toBe('10px'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(0); + }); + describe('resetOnNavigation', function () { + it('should report stylesheets across navigations', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage({ resetOnNavigation: false }); + await page.goto(server.PREFIX + '/csscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(2); + }); + it('should NOT report scripts across navigations', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage(); // Enabled by default. + await page.goto(server.PREFIX + '/csscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(0); + }); + }); + it('should work with a recently loaded stylesheet', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.evaluate<(url: string) => Promise<void>>(async (url) => { + document.body.textContent = 'hello, world'; + + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = url; + document.head.appendChild(link); + await new Promise((x) => (link.onload = x)); + }, server.PREFIX + '/csscoverage/stylesheet1.css'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + }); + }); +}); diff --git a/remote/test/puppeteer/test/defaultbrowsercontext.spec.ts b/remote/test/puppeteer/test/defaultbrowsercontext.spec.ts new file mode 100644 index 0000000000..23521ae762 --- /dev/null +++ b/remote/test/puppeteer/test/defaultbrowsercontext.spec.ts @@ -0,0 +1,104 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('DefaultBrowserContext', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('page.cookies() should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + document.cookie = 'username=John Doe'; + }); + expect(await page.cookies()).toEqual([ + { + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + }, + ]); + }); + it('page.setCookie() should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'username', + value: 'John Doe', + }); + expect(await page.evaluate(() => document.cookie)).toBe( + 'username=John Doe' + ); + expect(await page.cookies()).toEqual([ + { + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + }, + ]); + }); + it('page.deleteCookie() should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie( + { + name: 'cookie1', + value: '1', + }, + { + name: 'cookie2', + value: '2', + } + ); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie2=2'); + await page.deleteCookie({ name: 'cookie2' }); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1'); + expect(await page.cookies()).toEqual([ + { + name: 'cookie1', + value: '1', + domain: 'localhost', + path: '/', + expires: -1, + size: 8, + httpOnly: false, + secure: false, + session: true, + }, + ]); + }); +}); diff --git a/remote/test/puppeteer/test/dialog.spec.ts b/remote/test/puppeteer/test/dialog.spec.ts new file mode 100644 index 0000000000..0064020fff --- /dev/null +++ b/remote/test/puppeteer/test/dialog.spec.ts @@ -0,0 +1,73 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import expect from 'expect'; +import sinon from 'sinon'; + +import { + getTestState, + setupTestPageAndContextHooks, + setupTestBrowserHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Page.Events.Dialog', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should fire', async () => { + const { page } = getTestState(); + + const onDialog = sinon.stub().callsFake((dialog) => { + dialog.accept(); + }); + page.on('dialog', onDialog); + + await page.evaluate(() => alert('yo')); + + expect(onDialog.callCount).toEqual(1); + const dialog = onDialog.firstCall.args[0]; + expect(dialog.type()).toBe('alert'); + expect(dialog.defaultValue()).toBe(''); + expect(dialog.message()).toBe('yo'); + }); + + it('should allow accepting prompts', async () => { + const { page } = getTestState(); + + const onDialog = sinon.stub().callsFake((dialog) => { + dialog.accept('answer!'); + }); + page.on('dialog', onDialog); + + const result = await page.evaluate(() => prompt('question?', 'yes.')); + + expect(onDialog.callCount).toEqual(1); + const dialog = onDialog.firstCall.args[0]; + expect(dialog.type()).toBe('prompt'); + expect(dialog.defaultValue()).toBe('yes.'); + expect(dialog.message()).toBe('question?'); + + expect(result).toBe('answer!'); + }); + it('should dismiss the prompt', async () => { + const { page } = getTestState(); + + page.on('dialog', (dialog) => { + dialog.dismiss(); + }); + const result = await page.evaluate(() => prompt('question?')); + expect(result).toBe(null); + }); +}); diff --git a/remote/test/puppeteer/test/diffstyle.css b/remote/test/puppeteer/test/diffstyle.css new file mode 100644 index 0000000000..c58f0e90a6 --- /dev/null +++ b/remote/test/puppeteer/test/diffstyle.css @@ -0,0 +1,13 @@ +body { + font-family: monospace; + white-space: pre; +} + +ins { + background-color: #9cffa0; + text-decoration: none; +} + +del { + background-color: #ff9e9e; +} diff --git a/remote/test/puppeteer/test/elementhandle.spec.ts b/remote/test/puppeteer/test/elementhandle.spec.ts new file mode 100644 index 0000000000..617ff8e0ec --- /dev/null +++ b/remote/test/puppeteer/test/elementhandle.spec.ts @@ -0,0 +1,449 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import sinon from 'sinon'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions + +import utils from './utils.js'; +import { ElementHandle } from '../lib/cjs/puppeteer/common/JSHandle.js'; + +describe('ElementHandle specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('ElementHandle.boundingBox', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/grid.html'); + const elementHandle = await page.$('.box:nth-of-type(13)'); + const box = await elementHandle.boundingBox(); + expect(box).toEqual({ x: 100, y: 50, width: 50, height: 50 }); + }); + it('should handle nested frames', async () => { + const { page, server, isChrome } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + const nestedFrame = page.frames()[1].childFrames()[1]; + const elementHandle = await nestedFrame.$('div'); + const box = await elementHandle.boundingBox(); + if (isChrome) + expect(box).toEqual({ x: 28, y: 182, width: 264, height: 18 }); + else expect(box).toEqual({ x: 28, y: 182, width: 254, height: 18 }); + }); + it('should return null for invisible elements', async () => { + const { page } = getTestState(); + + await page.setContent('<div style="display:none">hi</div>'); + const element = await page.$('div'); + expect(await element.boundingBox()).toBe(null); + }); + it('should force a layout', async () => { + const { page } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.setContent( + '<div style="width: 100px; height: 100px">hello</div>' + ); + const elementHandle = await page.$('div'); + await page.evaluate<(element: HTMLElement) => void>( + (element) => (element.style.height = '200px'), + elementHandle + ); + const box = await elementHandle.boundingBox(); + expect(box).toEqual({ x: 8, y: 8, width: 100, height: 200 }); + }); + it('should work with SVG nodes', async () => { + const { page } = getTestState(); + + await page.setContent(` + <svg xmlns="http://www.w3.org/2000/svg" width="500" height="500"> + <rect id="theRect" x="30" y="50" width="200" height="300"></rect> + </svg> + `); + const element = await page.$('#therect'); + const pptrBoundingBox = await element.boundingBox(); + const webBoundingBox = await page.evaluate((e: HTMLElement) => { + const rect = e.getBoundingClientRect(); + return { x: rect.x, y: rect.y, width: rect.width, height: rect.height }; + }, element); + expect(pptrBoundingBox).toEqual(webBoundingBox); + }); + }); + + describe('ElementHandle.boxModel', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/resetcss.html'); + + // Step 1: Add Frame and position it absolutely. + await utils.attachFrame(page, 'frame1', server.PREFIX + '/resetcss.html'); + await page.evaluate(() => { + const frame = document.querySelector<HTMLElement>('#frame1'); + frame.style.position = 'absolute'; + frame.style.left = '1px'; + frame.style.top = '2px'; + }); + + // Step 2: Add div and position it absolutely inside frame. + const frame = page.frames()[1]; + const divHandle = ( + await frame.evaluateHandle(() => { + const div = document.createElement('div'); + document.body.appendChild(div); + div.style.boxSizing = 'border-box'; + div.style.position = 'absolute'; + div.style.borderLeft = '1px solid black'; + div.style.paddingLeft = '2px'; + div.style.marginLeft = '3px'; + div.style.left = '4px'; + div.style.top = '5px'; + div.style.width = '6px'; + div.style.height = '7px'; + return div; + }) + ).asElement(); + + // Step 3: query div's boxModel and assert box values. + const box = await divHandle.boxModel(); + expect(box.width).toBe(6); + expect(box.height).toBe(7); + expect(box.margin[0]).toEqual({ + x: 1 + 4, // frame.left + div.left + y: 2 + 5, + }); + expect(box.border[0]).toEqual({ + x: 1 + 4 + 3, // frame.left + div.left + div.margin-left + y: 2 + 5, + }); + expect(box.padding[0]).toEqual({ + x: 1 + 4 + 3 + 1, // frame.left + div.left + div.marginLeft + div.borderLeft + y: 2 + 5, + }); + expect(box.content[0]).toEqual({ + x: 1 + 4 + 3 + 1 + 2, // frame.left + div.left + div.marginLeft + div.borderLeft + dif.paddingLeft + y: 2 + 5, + }); + }); + + it('should return null for invisible elements', async () => { + const { page } = getTestState(); + + await page.setContent('<div style="display:none">hi</div>'); + const element = await page.$('div'); + expect(await element.boxModel()).toBe(null); + }); + }); + + describe('ElementHandle.contentFrame', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const elementHandle = await page.$('#frame1'); + const frame = await elementHandle.contentFrame(); + expect(frame).toBe(page.frames()[1]); + }); + }); + + describe('ElementHandle.click', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await button.click(); + expect(await page.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + it('should work for Shadow DOM v1', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/shadow.html'); + const buttonHandle = await page.evaluateHandle<ElementHandle>( + // @ts-expect-error button is expected to be in the page's scope. + () => button + ); + await buttonHandle.click(); + expect( + await page.evaluate( + // @ts-expect-error clicked is expected to be in the page's scope. + () => clicked + ) + ).toBe(true); + }); + it('should work for TextNodes', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + const buttonTextNode = await page.evaluateHandle<ElementHandle>( + () => document.querySelector('button').firstChild + ); + let error = null; + await buttonTextNode.click().catch((error_) => (error = error_)); + expect(error.message).toBe('Node is not of type HTMLElement'); + }); + it('should throw for detached nodes', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await page.evaluate((button: HTMLElement) => button.remove(), button); + let error = null; + await button.click().catch((error_) => (error = error_)); + expect(error.message).toBe('Node is detached from document'); + }); + it('should throw for hidden nodes', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await page.evaluate( + (button: HTMLElement) => (button.style.display = 'none'), + button + ); + const error = await button.click().catch((error_) => error_); + expect(error.message).toBe( + 'Node is either not visible or not an HTMLElement' + ); + }); + it('should throw for recursively hidden nodes', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await page.evaluate( + (button: HTMLElement) => (button.parentElement.style.display = 'none'), + button + ); + const error = await button.click().catch((error_) => error_); + expect(error.message).toBe( + 'Node is either not visible or not an HTMLElement' + ); + }); + it('should throw for <br> elements', async () => { + const { page } = getTestState(); + + await page.setContent('hello<br>goodbye'); + const br = await page.$('br'); + const error = await br.click().catch((error_) => error_); + expect(error.message).toBe( + 'Node is either not visible or not an HTMLElement' + ); + }); + }); + + describe('ElementHandle.hover', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + const button = await page.$('#button-6'); + await button.hover(); + expect( + await page.evaluate(() => document.querySelector('button:hover').id) + ).toBe('button-6'); + }); + }); + + describe('ElementHandle.isIntersectingViewport', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + for (let i = 0; i < 11; ++i) { + const button = await page.$('#btn' + i); + // All but last button are visible. + const visible = i < 10; + expect(await button.isIntersectingViewport()).toBe(visible); + } + }); + }); + + describe('Custom queries', function () { + this.afterEach(() => { + const { puppeteer } = getTestState(); + puppeteer.clearCustomQueryHandlers(); + }); + it('should register and unregister', async () => { + const { page, puppeteer } = getTestState(); + await page.setContent('<div id="not-foo"></div><div id="foo"></div>'); + + // Register. + puppeteer.registerCustomQueryHandler('getById', { + queryOne: (element, selector) => + document.querySelector(`[id="${selector}"]`), + }); + const element = await page.$('getById/foo'); + expect( + await page.evaluate<(element: HTMLElement) => string>( + (element) => element.id, + element + ) + ).toBe('foo'); + const handlerNamesAfterRegistering = puppeteer.customQueryHandlerNames(); + expect(handlerNamesAfterRegistering.includes('getById')).toBeTruthy(); + + // Unregister. + puppeteer.unregisterCustomQueryHandler('getById'); + try { + await page.$('getById/foo'); + throw new Error('Custom query handler name not set - throw expected'); + } catch (error) { + expect(error).toStrictEqual( + new Error( + 'Query set to use "getById", but no query handler of that name was found' + ) + ); + } + const handlerNamesAfterUnregistering = puppeteer.customQueryHandlerNames(); + expect(handlerNamesAfterUnregistering.includes('getById')).toBeFalsy(); + }); + it('should throw with invalid query names', () => { + try { + const { puppeteer } = getTestState(); + puppeteer.registerCustomQueryHandler('1/2/3', { + queryOne: () => document.querySelector('foo'), + }); + throw new Error( + 'Custom query handler name was invalid - throw expected' + ); + } catch (error) { + expect(error).toStrictEqual( + new Error('Custom query handler names may only contain [a-zA-Z]') + ); + } + }); + it('should work for multiple elements', async () => { + const { page, puppeteer } = getTestState(); + await page.setContent( + '<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>' + ); + puppeteer.registerCustomQueryHandler('getByClass', { + queryAll: (element, selector) => + document.querySelectorAll(`.${selector}`), + }); + const elements = await page.$$('getByClass/foo'); + const classNames = await Promise.all( + elements.map( + async (element) => + await page.evaluate<(element: HTMLElement) => string>( + (element) => element.className, + element + ) + ) + ); + + expect(classNames).toStrictEqual(['foo', 'foo baz']); + }); + it('should eval correctly', async () => { + const { page, puppeteer } = getTestState(); + await page.setContent( + '<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>' + ); + puppeteer.registerCustomQueryHandler('getByClass', { + queryAll: (element, selector) => + document.querySelectorAll(`.${selector}`), + }); + const elements = await page.$$eval( + 'getByClass/foo', + (divs) => divs.length + ); + + expect(elements).toBe(2); + }); + it('should wait correctly with waitForSelector', async () => { + const { page, puppeteer } = getTestState(); + puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => element.querySelector(`.${selector}`), + }); + const waitFor = page.waitForSelector('getByClass/foo'); + + // Set the page content after the waitFor has been started. + await page.setContent( + '<div id="not-foo"></div><div class="foo">Foo1</div>' + ); + const element = await waitFor; + + expect(element).toBeDefined(); + }); + + it('should wait correctly with waitFor', async () => { + /* page.waitFor is deprecated so we silence the warning to avoid test noise */ + sinon.stub(console, 'warn').callsFake(() => {}); + const { page, puppeteer } = getTestState(); + puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => element.querySelector(`.${selector}`), + }); + const waitFor = page.waitFor('getByClass/foo'); + + // Set the page content after the waitFor has been started. + await page.setContent( + '<div id="not-foo"></div><div class="foo">Foo1</div>' + ); + const element = await waitFor; + + expect(element).toBeDefined(); + }); + it('should work when both queryOne and queryAll are registered', async () => { + const { page, puppeteer } = getTestState(); + await page.setContent( + '<div id="not-foo"></div><div class="foo"><div id="nested-foo" class="foo"/></div><div class="foo baz">Foo2</div>' + ); + puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => element.querySelector(`.${selector}`), + queryAll: (element, selector) => + element.querySelectorAll(`.${selector}`), + }); + + const element = await page.$('getByClass/foo'); + expect(element).toBeDefined(); + + const elements = await page.$$('getByClass/foo'); + expect(elements.length).toBe(3); + }); + it('should eval when both queryOne and queryAll are registered', async () => { + const { page, puppeteer } = getTestState(); + await page.setContent( + '<div id="not-foo"></div><div class="foo">text</div><div class="foo baz">content</div>' + ); + puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => element.querySelector(`.${selector}`), + queryAll: (element, selector) => + element.querySelectorAll(`.${selector}`), + }); + + const txtContent = await page.$eval( + 'getByClass/foo', + (div) => div.textContent + ); + expect(txtContent).toBe('text'); + + const txtContents = await page.$$eval('getByClass/foo', (divs) => + divs.map((d) => d.textContent).join('') + ); + expect(txtContents).toBe('textcontent'); + }); + }); +}); diff --git a/remote/test/puppeteer/test/emulation.spec.ts b/remote/test/puppeteer/test/emulation.spec.ts new file mode 100644 index 0000000000..f594127a63 --- /dev/null +++ b/remote/test/puppeteer/test/emulation.spec.ts @@ -0,0 +1,355 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Emulation', () => { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + let iPhone; + let iPhoneLandscape; + + before(() => { + const { puppeteer } = getTestState(); + iPhone = puppeteer.devices['iPhone 6']; + iPhoneLandscape = puppeteer.devices['iPhone 6 landscape']; + }); + + describe('Page.viewport', function () { + it('should get the proper viewport size', async () => { + const { page } = getTestState(); + + expect(page.viewport()).toEqual({ width: 800, height: 600 }); + await page.setViewport({ width: 123, height: 456 }); + expect(page.viewport()).toEqual({ width: 123, height: 456 }); + }); + it('should support mobile emulation', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + expect(await page.evaluate(() => window.innerWidth)).toBe(800); + await page.setViewport(iPhone.viewport); + expect(await page.evaluate(() => window.innerWidth)).toBe(375); + await page.setViewport({ width: 400, height: 300 }); + expect(await page.evaluate(() => window.innerWidth)).toBe(400); + }); + it('should support touch emulation', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false); + await page.setViewport(iPhone.viewport); + expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(true); + expect(await page.evaluate(dispatchTouch)).toBe('Received touch'); + await page.setViewport({ width: 100, height: 100 }); + expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false); + + function dispatchTouch() { + let fulfill; + const promise = new Promise((x) => (fulfill = x)); + window.ontouchstart = () => { + fulfill('Received touch'); + }; + window.dispatchEvent(new Event('touchstart')); + + fulfill('Did not receive touch'); + + return promise; + } + }); + it('should be detectable by Modernizr', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/detect-touch.html'); + expect(await page.evaluate(() => document.body.textContent.trim())).toBe( + 'NO' + ); + await page.setViewport(iPhone.viewport); + await page.goto(server.PREFIX + '/detect-touch.html'); + expect(await page.evaluate(() => document.body.textContent.trim())).toBe( + 'YES' + ); + }); + it('should detect touch when applying viewport with touches', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 800, height: 600, hasTouch: true }); + await page.addScriptTag({ url: server.PREFIX + '/modernizr.js' }); + expect(await page.evaluate(() => globalThis.Modernizr.touchevents)).toBe( + true + ); + }); + it('should support landscape emulation', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + expect(await page.evaluate(() => screen.orientation.type)).toBe( + 'portrait-primary' + ); + await page.setViewport(iPhoneLandscape.viewport); + expect(await page.evaluate(() => screen.orientation.type)).toBe( + 'landscape-primary' + ); + await page.setViewport({ width: 100, height: 100 }); + expect(await page.evaluate(() => screen.orientation.type)).toBe( + 'portrait-primary' + ); + }); + }); + + describe('Page.emulate', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + await page.emulate(iPhone); + expect(await page.evaluate(() => window.innerWidth)).toBe(375); + expect(await page.evaluate(() => navigator.userAgent)).toContain( + 'iPhone' + ); + }); + it('should support clicking', async () => { + const { page, server } = getTestState(); + + await page.emulate(iPhone); + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await page.evaluate( + (button: HTMLElement) => (button.style.marginTop = '200px'), + button + ); + await button.click(); + expect(await page.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + }); + + describe('Page.emulateMediaType', function () { + it('should work', async () => { + const { page } = getTestState(); + + expect(await page.evaluate(() => matchMedia('screen').matches)).toBe( + true + ); + expect(await page.evaluate(() => matchMedia('print').matches)).toBe( + false + ); + await page.emulateMediaType('print'); + expect(await page.evaluate(() => matchMedia('screen').matches)).toBe( + false + ); + expect(await page.evaluate(() => matchMedia('print').matches)).toBe(true); + await page.emulateMediaType(null); + expect(await page.evaluate(() => matchMedia('screen').matches)).toBe( + true + ); + expect(await page.evaluate(() => matchMedia('print').matches)).toBe( + false + ); + }); + it('should throw in case of bad argument', async () => { + const { page } = getTestState(); + + let error = null; + await page.emulateMediaType('bad').catch((error_) => (error = error_)); + expect(error.message).toBe('Unsupported media type: bad'); + }); + }); + + describe('Page.emulateMediaFeatures', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.emulateMediaFeatures([ + { 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.emulateMediaFeatures([ + { name: 'prefers-color-scheme', value: '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); + await page.emulateMediaFeatures([ + { name: 'prefers-color-scheme', value: '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); + await page.emulateMediaFeatures([ + { 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); + expect( + await page.evaluate( + () => matchMedia('(prefers-color-scheme: dark)').matches + ) + ).toBe(false); + }); + it('should throw in case of bad argument', async () => { + const { page } = getTestState(); + + let error = null; + await page + .emulateMediaFeatures([{ name: 'bad', value: '' }]) + .catch((error_) => (error = error_)); + expect(error.message).toBe('Unsupported media feature: bad'); + }); + }); + + describe('Page.emulateTimezone', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.evaluate(() => { + globalThis.date = new Date(1479579154987); + }); + await page.emulateTimezone('America/Jamaica'); + expect(await page.evaluate(() => globalThis.date.toString())).toBe( + 'Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)' + ); + + await page.emulateTimezone('Pacific/Honolulu'); + expect(await page.evaluate(() => globalThis.date.toString())).toBe( + 'Sat Nov 19 2016 08:12:34 GMT-1000 (Hawaii-Aleutian Standard Time)' + ); + + await page.emulateTimezone('America/Buenos_Aires'); + expect(await page.evaluate(() => globalThis.date.toString())).toBe( + 'Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)' + ); + + await page.emulateTimezone('Europe/Berlin'); + expect(await page.evaluate(() => globalThis.date.toString())).toBe( + 'Sat Nov 19 2016 19:12:34 GMT+0100 (Central European Standard Time)' + ); + }); + + it('should throw for invalid timezone IDs', async () => { + const { page } = getTestState(); + + let error = null; + await page.emulateTimezone('Foo/Bar').catch((error_) => (error = error_)); + expect(error.message).toBe('Invalid timezone ID: Foo/Bar'); + await page.emulateTimezone('Baz/Qux').catch((error_) => (error = error_)); + expect(error.message).toBe('Invalid timezone ID: Baz/Qux'); + }); + }); + + describe('Page.emulateVisionDeficiency', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/grid.html'); + + { + await page.emulateVisionDeficiency('none'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('screenshot-sanity.png'); + } + + { + await page.emulateVisionDeficiency('achromatopsia'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('vision-deficiency-achromatopsia.png'); + } + + { + await page.emulateVisionDeficiency('blurredVision'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('vision-deficiency-blurredVision.png'); + } + + { + await page.emulateVisionDeficiency('deuteranopia'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('vision-deficiency-deuteranopia.png'); + } + + { + await page.emulateVisionDeficiency('protanopia'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('vision-deficiency-protanopia.png'); + } + + { + await page.emulateVisionDeficiency('tritanopia'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('vision-deficiency-tritanopia.png'); + } + + { + await page.emulateVisionDeficiency('none'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('screenshot-sanity.png'); + } + }); + + it('should throw for invalid vision deficiencies', async () => { + const { page } = getTestState(); + + let error = null; + await page + // @ts-expect-error deliberately passign invalid deficiency + .emulateVisionDeficiency('invalid') + .catch((error_) => (error = error_)); + expect(error.message).toBe('Unsupported vision deficiency: invalid'); + }); + }); +}); diff --git a/remote/test/puppeteer/test/evaluation.spec.ts b/remote/test/puppeteer/test/evaluation.spec.ts new file mode 100644 index 0000000000..3e9423728a --- /dev/null +++ b/remote/test/puppeteer/test/evaluation.spec.ts @@ -0,0 +1,474 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions + +const bigint = typeof BigInt !== 'undefined'; + +describe('Evaluation specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.evaluate', function () { + it('should work', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => 7 * 3); + expect(result).toBe(21); + }); + (bigint ? it : xit)('should transfer BigInt', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a: BigInt) => a, BigInt(42)); + expect(result).toBe(BigInt(42)); + }); + it('should transfer NaN', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => a, NaN); + expect(Object.is(result, NaN)).toBe(true); + }); + it('should transfer -0', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => a, -0); + expect(Object.is(result, -0)).toBe(true); + }); + it('should transfer Infinity', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => a, Infinity); + expect(Object.is(result, Infinity)).toBe(true); + }); + it('should transfer -Infinity', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => a, -Infinity); + expect(Object.is(result, -Infinity)).toBe(true); + }); + it('should transfer arrays', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => a, [1, 2, 3]); + expect(result).toEqual([1, 2, 3]); + }); + it('should transfer arrays as arrays, not objects', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => Array.isArray(a), [1, 2, 3]); + expect(result).toBe(true); + }); + it('should modify global environment', async () => { + const { page } = getTestState(); + + await page.evaluate(() => (globalThis.globalVar = 123)); + expect(await page.evaluate('globalVar')).toBe(123); + }); + it('should evaluate in the page context', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/global-var.html'); + expect(await page.evaluate('globalVar')).toBe(123); + }); + it( + 'should return undefined for objects with symbols', + async () => { + const { page } = getTestState(); + + expect(await page.evaluate(() => [Symbol('foo4')])).toBe(undefined); + } + ); + it('should work with function shorthands', async () => { + const { page } = getTestState(); + + const a = { + sum(a, b) { + return a + b; + }, + + async mult(a, b) { + return a * b; + }, + }; + expect(await page.evaluate(a.sum, 1, 2)).toBe(3); + expect(await page.evaluate(a.mult, 2, 4)).toBe(8); + }); + it('should work with unicode chars', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => a['中文字符'], { + 中文字符: 42, + }); + expect(result).toBe(42); + }); + it('should throw when evaluation triggers reload', async () => { + const { page } = getTestState(); + + let error = null; + await page + .evaluate(() => { + location.reload(); + return new Promise(() => {}); + }) + .catch((error_) => (error = error_)); + expect(error.message).toContain('Protocol error'); + }); + it('should await promise', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => Promise.resolve(8 * 7)); + expect(result).toBe(56); + }); + it('should work right after framenavigated', async () => { + const { page, server } = getTestState(); + + let frameEvaluation = null; + page.on('framenavigated', async (frame) => { + frameEvaluation = frame.evaluate(() => 6 * 7); + }); + await page.goto(server.EMPTY_PAGE); + expect(await frameEvaluation).toBe(42); + }); + it('should work from-inside an exposed function', async () => { + const { page } = getTestState(); + + // Setup inpage callback, which calls Page.evaluate + await page.exposeFunction('callController', async function (a, b) { + return await page.evaluate<(a: number, b: number) => number>( + (a, b) => a * b, + a, + b + ); + }); + const result = await page.evaluate(async function () { + return await globalThis.callController(9, 3); + }); + expect(result).toBe(27); + }); + it('should reject promise with exception', async () => { + const { page } = getTestState(); + + let error = null; + await page + // @ts-expect-error we know the object doesn't exist + .evaluate(() => notExistingObject.property) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain('notExistingObject'); + }); + it('should support thrown strings as error messages', async () => { + const { page } = getTestState(); + + let error = null; + await page + .evaluate(() => { + throw 'qwerty'; + }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain('qwerty'); + }); + it('should support thrown numbers as error messages', async () => { + const { page } = getTestState(); + + let error = null; + await page + .evaluate(() => { + throw 100500; + }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain('100500'); + }); + it('should return complex objects', async () => { + const { page } = getTestState(); + + const object = { foo: 'bar!' }; + const result = await page.evaluate((a) => a, object); + expect(result).not.toBe(object); + expect(result).toEqual(object); + }); + (bigint ? it : xit)('should return BigInt', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => BigInt(42)); + expect(result).toBe(BigInt(42)); + }); + it('should return NaN', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => NaN); + expect(Object.is(result, NaN)).toBe(true); + }); + it('should return -0', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => -0); + expect(Object.is(result, -0)).toBe(true); + }); + it('should return Infinity', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => Infinity); + expect(Object.is(result, Infinity)).toBe(true); + }); + it('should return -Infinity', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => -Infinity); + expect(Object.is(result, -Infinity)).toBe(true); + }); + it('should accept "undefined" as one of multiple parameters', async () => { + const { page } = getTestState(); + + const result = await page.evaluate( + (a, b) => Object.is(a, undefined) && Object.is(b, 'foo'), + undefined, + 'foo' + ); + expect(result).toBe(true); + }); + it('should properly serialize null fields', async () => { + const { page } = getTestState(); + + expect(await page.evaluate(() => ({ a: undefined }))).toEqual({}); + }); + it( + 'should return undefined for non-serializable objects', + async () => { + const { page } = getTestState(); + + expect(await page.evaluate(() => window)).toBe(undefined); + } + ); + it('should fail for circular object', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => { + const a: { [x: string]: any } = {}; + const b = { a }; + a.b = b; + return a; + }); + expect(result).toBe(undefined); + }); + it('should be able to throw a tricky error', async () => { + const { page } = getTestState(); + + const windowHandle = await page.evaluateHandle(() => window); + const errorText = await windowHandle + .jsonValue() + .catch((error_) => error_.message); + const error = await page + .evaluate<(errorText: string) => Error>((errorText) => { + throw new Error(errorText); + }, errorText) + .catch((error_) => error_); + expect(error.message).toContain(errorText); + }); + it('should accept a string', async () => { + const { page } = getTestState(); + + const result = await page.evaluate('1 + 2'); + expect(result).toBe(3); + }); + it('should accept a string with semi colons', async () => { + const { page } = getTestState(); + + const result = await page.evaluate('1 + 5;'); + expect(result).toBe(6); + }); + it('should accept a string with comments', async () => { + const { page } = getTestState(); + + const result = await page.evaluate('2 + 5;\n// do some math!'); + expect(result).toBe(7); + }); + it('should accept element handle as an argument', async () => { + const { page } = getTestState(); + + await page.setContent('<section>42</section>'); + const element = await page.$('section'); + const text = await page.evaluate<(e: HTMLElement) => string>( + (e) => e.textContent, + element + ); + expect(text).toBe('42'); + }); + it('should throw if underlying element was disposed', async () => { + const { page } = getTestState(); + + await page.setContent('<section>39</section>'); + const element = await page.$('section'); + expect(element).toBeTruthy(); + await element.dispose(); + let error = null; + await page + .evaluate((e: HTMLElement) => e.textContent, element) + .catch((error_) => (error = error_)); + expect(error.message).toContain('JSHandle is disposed'); + }); + it( + 'should throw if elementHandles are from other frames', + async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const bodyHandle = await page.frames()[1].$('body'); + let error = null; + await page + .evaluate((body: HTMLElement) => body.innerHTML, bodyHandle) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'JSHandles can be evaluated only in the context they were created' + ); + } + ); + it('should simulate a user gesture', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => { + document.body.appendChild(document.createTextNode('test')); + document.execCommand('selectAll'); + return document.execCommand('copy'); + }); + expect(result).toBe(true); + }); + it('should throw a nice error after a navigation', async () => { + const { page } = getTestState(); + + const executionContext = await page.mainFrame().executionContext(); + + await Promise.all([ + page.waitForNavigation(), + executionContext.evaluate(() => window.location.reload()), + ]); + const error = await executionContext + .evaluate(() => null) + .catch((error_) => error_); + expect((error as Error).message).toContain('navigation'); + }); + it( + 'should not throw an error when evaluation does a navigation', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/one-style.html'); + const result = await page.evaluate(() => { + (window as any).location = '/empty.html'; + return [42]; + }); + expect(result).toEqual([42]); + } + ); + it('should transfer 100Mb of data from page to node.js', async function () { + const { page } = getTestState(); + + const a = await page.evaluate<() => string>(() => + Array(100 * 1024 * 1024 + 1).join('a') + ); + expect(a.length).toBe(100 * 1024 * 1024); + }); + it('should throw error with detailed information on exception inside promise ', async () => { + const { page } = getTestState(); + + let error = null; + await page + .evaluate( + () => + new Promise(() => { + throw new Error('Error in promise'); + }) + ) + .catch((error_) => (error = error_)); + expect(error.message).toContain('Error in promise'); + }); + }); + + describe('Page.evaluateOnNewDocument', function () { + it('should evaluate before anything else on the page', async () => { + const { page, server } = getTestState(); + + await page.evaluateOnNewDocument(function () { + globalThis.injected = 123; + }); + await page.goto(server.PREFIX + '/tamperable.html'); + expect(await page.evaluate(() => globalThis.result)).toBe(123); + }); + it('should work with CSP', async () => { + const { page, server } = getTestState(); + + server.setCSP('/empty.html', 'script-src ' + server.PREFIX); + await page.evaluateOnNewDocument(function () { + globalThis.injected = 123; + }); + await page.goto(server.PREFIX + '/empty.html'); + expect(await page.evaluate(() => globalThis.injected)).toBe(123); + + // Make sure CSP works. + await page + .addScriptTag({ content: 'window.e = 10;' }) + .catch((error) => void error); + expect(await page.evaluate(() => (window as any).e)).toBe(undefined); + }); + }); + + describe('Frame.evaluate', function () { + it('should have different execution contexts', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(page.frames().length).toBe(2); + await page.frames()[0].evaluate(() => (globalThis.FOO = 'foo')); + await page.frames()[1].evaluate(() => (globalThis.FOO = 'bar')); + expect(await page.frames()[0].evaluate(() => globalThis.FOO)).toBe('foo'); + expect(await page.frames()[1].evaluate(() => globalThis.FOO)).toBe('bar'); + }); + it('should have correct execution contexts', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame.html'); + expect(page.frames().length).toBe(2); + expect( + await page.frames()[0].evaluate(() => document.body.textContent.trim()) + ).toBe(''); + expect( + await page.frames()[1].evaluate(() => document.body.textContent.trim()) + ).toBe(`Hi, I'm frame`); + }); + it('should execute after cross-site navigation', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + expect(await mainFrame.evaluate(() => window.location.href)).toContain( + 'localhost' + ); + await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(await mainFrame.evaluate(() => window.location.href)).toContain( + '127' + ); + }); + }); +}); diff --git a/remote/test/puppeteer/test/fixtures.spec.ts b/remote/test/puppeteer/test/fixtures.spec.ts new file mode 100644 index 0000000000..8eca362071 --- /dev/null +++ b/remote/test/puppeteer/test/fixtures.spec.ts @@ -0,0 +1,93 @@ +/** + * Copyright 2019 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable @typescript-eslint/no-var-requires */ + +import expect from 'expect'; +import { getTestState, itChromeOnly } from './mocha-utils'; // eslint-disable-line import/extensions + +import path from 'path'; + +describe('Fixtures', function () { + itChromeOnly('dumpio option should work with pipe option ', async () => { + const { defaultBrowserOptions, puppeteerPath } = getTestState(); + + let dumpioData = ''; + const { spawn } = require('child_process'); + const options = Object.assign({}, defaultBrowserOptions, { + pipe: true, + dumpio: true, + }); + const res = spawn('node', [ + path.join(__dirname, 'fixtures', 'dumpio.js'), + puppeteerPath, + JSON.stringify(options), + ]); + res.stderr.on('data', (data) => (dumpioData += data.toString('utf8'))); + await new Promise((resolve) => res.on('close', resolve)); + expect(dumpioData).toContain('message from dumpio'); + }); + it('should dump browser process stderr', async () => { + const { defaultBrowserOptions, puppeteerPath } = getTestState(); + + let dumpioData = ''; + const { spawn } = require('child_process'); + const options = Object.assign({}, defaultBrowserOptions, { dumpio: true }); + const res = spawn('node', [ + path.join(__dirname, 'fixtures', 'dumpio.js'), + puppeteerPath, + JSON.stringify(options), + ]); + res.stderr.on('data', (data) => (dumpioData += data.toString('utf8'))); + await new Promise((resolve) => res.on('close', resolve)); + expect(dumpioData).toContain('DevTools listening on ws://'); + }); + it('should close the browser when the node process closes', async () => { + const { defaultBrowserOptions, puppeteerPath, puppeteer } = getTestState(); + + const { spawn, execSync } = require('child_process'); + const options = Object.assign({}, defaultBrowserOptions, { + // Disable DUMPIO to cleanly read stdout. + dumpio: false, + }); + const res = spawn('node', [ + path.join(__dirname, 'fixtures', 'closeme.js'), + puppeteerPath, + JSON.stringify(options), + ]); + let wsEndPointCallback; + const wsEndPointPromise = new Promise<string>( + (x) => (wsEndPointCallback = x) + ); + let output = ''; + res.stdout.on('data', (data) => { + output += data; + if (output.indexOf('\n')) + wsEndPointCallback(output.substring(0, output.indexOf('\n'))); + }); + const browser = await puppeteer.connect({ + browserWSEndpoint: await wsEndPointPromise, + }); + const promises = [ + new Promise((resolve) => browser.once('disconnected', resolve)), + new Promise((resolve) => res.on('close', resolve)), + ]; + if (process.platform === 'win32') + execSync(`taskkill /pid ${res.pid} /T /F`); + else process.kill(res.pid); + await Promise.all(promises); + }); +}); diff --git a/remote/test/puppeteer/test/fixtures/closeme.js b/remote/test/puppeteer/test/fixtures/closeme.js new file mode 100644 index 0000000000..dbe798f70d --- /dev/null +++ b/remote/test/puppeteer/test/fixtures/closeme.js @@ -0,0 +1,5 @@ +(async () => { + const [, , puppeteerRoot, options] = process.argv; + const browser = await require(puppeteerRoot).launch(JSON.parse(options)); + console.log(browser.wsEndpoint()); +})(); diff --git a/remote/test/puppeteer/test/fixtures/dumpio.js b/remote/test/puppeteer/test/fixtures/dumpio.js new file mode 100644 index 0000000000..40b9714f6c --- /dev/null +++ b/remote/test/puppeteer/test/fixtures/dumpio.js @@ -0,0 +1,8 @@ +(async () => { + const [, , puppeteerRoot, options] = process.argv; + const browser = await require(puppeteerRoot).launch(JSON.parse(options)); + const page = await browser.newPage(); + await page.evaluate(() => console.error('message from dumpio')); + await page.close(); + await browser.close(); +})(); diff --git a/remote/test/puppeteer/test/frame.spec.ts b/remote/test/puppeteer/test/frame.spec.ts new file mode 100644 index 0000000000..6f226c12ea --- /dev/null +++ b/remote/test/puppeteer/test/frame.spec.ts @@ -0,0 +1,269 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Frame specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Frame.executionContext', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(page.frames().length).toBe(2); + const [frame1, frame2] = page.frames(); + const context1 = await frame1.executionContext(); + const context2 = await frame2.executionContext(); + expect(context1).toBeTruthy(); + expect(context2).toBeTruthy(); + expect(context1 !== context2).toBeTruthy(); + expect(context1.frame()).toBe(frame1); + expect(context2.frame()).toBe(frame2); + + await Promise.all([ + context1.evaluate(() => (globalThis.a = 1)), + context2.evaluate(() => (globalThis.a = 2)), + ]); + const [a1, a2] = await Promise.all([ + context1.evaluate(() => globalThis.a), + context2.evaluate(() => globalThis.a), + ]); + expect(a1).toBe(1); + expect(a2).toBe(2); + }); + }); + + describe('Frame.evaluateHandle', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + const windowHandle = await mainFrame.evaluateHandle(() => window); + expect(windowHandle).toBeTruthy(); + }); + }); + + describe('Frame.evaluate', function () { + it('should throw for detached frames', async () => { + const { page, server } = getTestState(); + + const frame1 = await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.detachFrame(page, 'frame1'); + let error = null; + await frame1.evaluate(() => 7 * 8).catch((error_) => (error = error_)); + expect(error.message).toContain( + 'Execution context is not available in detached frame' + ); + }); + }); + + describe('Frame Management', function () { + it('should handle nested frames', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + expect(utils.dumpFrames(page.mainFrame())).toEqual([ + 'http://localhost:<PORT>/frames/nested-frames.html', + ' http://localhost:<PORT>/frames/two-frames.html (2frames)', + ' http://localhost:<PORT>/frames/frame.html (uno)', + ' http://localhost:<PORT>/frames/frame.html (dos)', + ' http://localhost:<PORT>/frames/frame.html (aframe)', + ]); + }); + it( + 'should send events when frames are manipulated dynamically', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + // validate frameattached events + const attachedFrames = []; + page.on('frameattached', (frame) => attachedFrames.push(frame)); + await utils.attachFrame(page, 'frame1', './assets/frame.html'); + expect(attachedFrames.length).toBe(1); + expect(attachedFrames[0].url()).toContain('/assets/frame.html'); + + // validate framenavigated events + const navigatedFrames = []; + page.on('framenavigated', (frame) => navigatedFrames.push(frame)); + await utils.navigateFrame(page, 'frame1', './empty.html'); + expect(navigatedFrames.length).toBe(1); + expect(navigatedFrames[0].url()).toBe(server.EMPTY_PAGE); + + // validate framedetached events + const detachedFrames = []; + page.on('framedetached', (frame) => detachedFrames.push(frame)); + await utils.detachFrame(page, 'frame1'); + expect(detachedFrames.length).toBe(1); + expect(detachedFrames[0].isDetached()).toBe(true); + } + ); + it( + 'should send "framenavigated" when navigating on anchor URLs', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await Promise.all([ + page.goto(server.EMPTY_PAGE + '#foo'), + utils.waitEvent(page, 'framenavigated'), + ]); + expect(page.url()).toBe(server.EMPTY_PAGE + '#foo'); + } + ); + it('should persist mainFrame on cross-process navigation', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(page.mainFrame() === mainFrame).toBeTruthy(); + }); + it('should not send attach/detach events for main frame', async () => { + const { page, server } = getTestState(); + + let hasEvents = false; + page.on('frameattached', () => (hasEvents = true)); + page.on('framedetached', () => (hasEvents = true)); + await page.goto(server.EMPTY_PAGE); + expect(hasEvents).toBe(false); + }); + it('should detach child frames on navigation', async () => { + const { page, server } = getTestState(); + + let attachedFrames = []; + let detachedFrames = []; + let navigatedFrames = []; + page.on('frameattached', (frame) => attachedFrames.push(frame)); + page.on('framedetached', (frame) => detachedFrames.push(frame)); + page.on('framenavigated', (frame) => navigatedFrames.push(frame)); + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + expect(attachedFrames.length).toBe(4); + expect(detachedFrames.length).toBe(0); + expect(navigatedFrames.length).toBe(5); + + attachedFrames = []; + detachedFrames = []; + navigatedFrames = []; + await page.goto(server.EMPTY_PAGE); + expect(attachedFrames.length).toBe(0); + expect(detachedFrames.length).toBe(4); + expect(navigatedFrames.length).toBe(1); + }); + it('should support framesets', async () => { + const { page, server } = getTestState(); + + let attachedFrames = []; + let detachedFrames = []; + let navigatedFrames = []; + page.on('frameattached', (frame) => attachedFrames.push(frame)); + page.on('framedetached', (frame) => detachedFrames.push(frame)); + page.on('framenavigated', (frame) => navigatedFrames.push(frame)); + await page.goto(server.PREFIX + '/frames/frameset.html'); + expect(attachedFrames.length).toBe(4); + expect(detachedFrames.length).toBe(0); + expect(navigatedFrames.length).toBe(5); + + attachedFrames = []; + detachedFrames = []; + navigatedFrames = []; + await page.goto(server.EMPTY_PAGE); + expect(attachedFrames.length).toBe(0); + expect(detachedFrames.length).toBe(4); + expect(navigatedFrames.length).toBe(1); + }); + it('should report frame from-inside shadow DOM', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/shadow.html'); + await page.evaluate(async (url: string) => { + const frame = document.createElement('iframe'); + frame.src = url; + document.body.shadowRoot.appendChild(frame); + await new Promise((x) => (frame.onload = x)); + }, server.EMPTY_PAGE); + expect(page.frames().length).toBe(2); + expect(page.frames()[1].url()).toBe(server.EMPTY_PAGE); + }); + it('should report frame.name()', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'theFrameId', server.EMPTY_PAGE); + await page.evaluate((url: string) => { + const frame = document.createElement('iframe'); + frame.name = 'theFrameName'; + frame.src = url; + document.body.appendChild(frame); + return new Promise((x) => (frame.onload = x)); + }, server.EMPTY_PAGE); + expect(page.frames()[0].name()).toBe(''); + expect(page.frames()[1].name()).toBe('theFrameId'); + expect(page.frames()[2].name()).toBe('theFrameName'); + }); + it('should report frame.parent()', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE); + expect(page.frames()[0].parentFrame()).toBe(null); + expect(page.frames()[1].parentFrame()).toBe(page.mainFrame()); + expect(page.frames()[2].parentFrame()).toBe(page.mainFrame()); + }); + it( + 'should report different frame instance when frame re-attaches', + async () => { + const { page, server } = getTestState(); + + const frame1 = await utils.attachFrame( + page, + 'frame1', + server.EMPTY_PAGE + ); + await page.evaluate(() => { + globalThis.frame = document.querySelector('#frame1'); + globalThis.frame.remove(); + }); + expect(frame1.isDetached()).toBe(true); + const [frame2] = await Promise.all([ + utils.waitEvent(page, 'frameattached'), + page.evaluate(() => document.body.appendChild(globalThis.frame)), + ]); + expect(frame2.isDetached()).toBe(false); + expect(frame1).not.toBe(frame2); + } + ); + it('should support url fragment', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame-url-fragment.html'); + + expect(page.frames().length).toBe(2); + expect(page.frames()[1].url()).toBe( + server.PREFIX + '/frames/frame.html?param=value#fragment' + ); + }); + }); +}); diff --git a/remote/test/puppeteer/test/golden-chromium/csscoverage-involved.txt b/remote/test/puppeteer/test/golden-chromium/csscoverage-involved.txt new file mode 100644 index 0000000000..9b851d0bd3 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/csscoverage-involved.txt @@ -0,0 +1,16 @@ +[ + { + "url": "http://localhost:<PORT>/csscoverage/involved.html", + "ranges": [ + { + "start": 149, + "end": 297 + }, + { + "start": 327, + "end": 433 + } + ], + "text": "\n@charset \"utf-8\";\n@namespace svg url(http://www.w3.org/2000/svg);\n@font-face {\n font-family: \"Example Font\";\n src: url(\"./Dosis-Regular.ttf\");\n}\n\n#fluffy {\n border: 1px solid black;\n z-index: 1;\n /* -webkit-disabled-property: rgb(1, 2, 3) */\n -lol-cats: \"dogs\" /* non-existing property */\n}\n\n@media (min-width: 1px) {\n span {\n -webkit-border-radius: 10px;\n font-family: \"Example Font\";\n animation: 1s identifier;\n }\n}\n" + } +]
\ No newline at end of file diff --git a/remote/test/puppeteer/test/golden-chromium/grid-cell-0.png b/remote/test/puppeteer/test/golden-chromium/grid-cell-0.png Binary files differnew file mode 100644 index 0000000000..ff282e989b --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/grid-cell-0.png diff --git a/remote/test/puppeteer/test/golden-chromium/grid-cell-1.png b/remote/test/puppeteer/test/golden-chromium/grid-cell-1.png Binary files differnew file mode 100644 index 0000000000..91a1cb8510 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/grid-cell-1.png diff --git a/remote/test/puppeteer/test/golden-chromium/grid-cell-2.png b/remote/test/puppeteer/test/golden-chromium/grid-cell-2.png Binary files differnew file mode 100644 index 0000000000..7b01753b6a --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/grid-cell-2.png diff --git a/remote/test/puppeteer/test/golden-chromium/grid-cell-3.png b/remote/test/puppeteer/test/golden-chromium/grid-cell-3.png Binary files differnew file mode 100644 index 0000000000..b9b8b2922b --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/grid-cell-3.png diff --git a/remote/test/puppeteer/test/golden-chromium/jscoverage-involved.txt b/remote/test/puppeteer/test/golden-chromium/jscoverage-involved.txt new file mode 100644 index 0000000000..6f28e1580e --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/jscoverage-involved.txt @@ -0,0 +1,28 @@ +[ + { + "url": "http://localhost:<PORT>/jscoverage/involved.html", + "ranges": [ + { + "start": 0, + "end": 35 + }, + { + "start": 50, + "end": 100 + }, + { + "start": 107, + "end": 141 + }, + { + "start": 148, + "end": 160 + }, + { + "start": 168, + "end": 207 + } + ], + "text": "\nfunction foo() {\n if (1 > 2)\n console.log(1);\n if (1 < 2)\n console.log(2);\n let x = 1 > 2 ? 'foo' : 'bar';\n let y = 1 < 2 ? 'foo' : 'bar';\n let z = () => {};\n let q = () => {};\n q();\n}\n\nfoo();\n" + } +]
\ No newline at end of file diff --git a/remote/test/puppeteer/test/golden-chromium/mock-binary-response.png b/remote/test/puppeteer/test/golden-chromium/mock-binary-response.png Binary files differnew file mode 100644 index 0000000000..8595e0598e --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/mock-binary-response.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-clip-odd-size.png b/remote/test/puppeteer/test/golden-chromium/screenshot-clip-odd-size.png Binary files differnew file mode 100644 index 0000000000..b010d1f87f --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-clip-odd-size.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-clip-rect.png b/remote/test/puppeteer/test/golden-chromium/screenshot-clip-rect.png Binary files differnew file mode 100644 index 0000000000..ac23b7de50 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-clip-rect.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-bounding-box.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-bounding-box.png Binary files differnew file mode 100644 index 0000000000..32e05bf05b --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-bounding-box.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional-offset.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional-offset.png Binary files differnew file mode 100644 index 0000000000..cc8669d598 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional-offset.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional.png Binary files differnew file mode 100644 index 0000000000..35c53377f9 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-larger-than-viewport.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-larger-than-viewport.png Binary files differnew file mode 100644 index 0000000000..5fcdb92355 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-larger-than-viewport.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-padding-border.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-padding-border.png Binary files differnew file mode 100644 index 0000000000..917dd48188 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-padding-border.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-rotate.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-rotate.png Binary files differnew file mode 100644 index 0000000000..52e2a0f6d3 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-rotate.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-scrolled-into-view.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-scrolled-into-view.png Binary files differnew file mode 100644 index 0000000000..917dd48188 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-scrolled-into-view.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-grid-fullpage.png b/remote/test/puppeteer/test/golden-chromium/screenshot-grid-fullpage.png Binary files differnew file mode 100644 index 0000000000..d6d38217f7 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-grid-fullpage.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-offscreen-clip.png b/remote/test/puppeteer/test/golden-chromium/screenshot-offscreen-clip.png Binary files differnew file mode 100644 index 0000000000..31a0935cda --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-offscreen-clip.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-sanity.png b/remote/test/puppeteer/test/golden-chromium/screenshot-sanity.png Binary files differnew file mode 100644 index 0000000000..ecab61fe17 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-sanity.png diff --git a/remote/test/puppeteer/test/golden-chromium/transparent.png b/remote/test/puppeteer/test/golden-chromium/transparent.png Binary files differnew file mode 100644 index 0000000000..1cf45d8688 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/transparent.png diff --git a/remote/test/puppeteer/test/golden-chromium/vision-deficiency-achromatopsia.png b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-achromatopsia.png Binary files differnew file mode 100644 index 0000000000..4d74aac44c --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-achromatopsia.png diff --git a/remote/test/puppeteer/test/golden-chromium/vision-deficiency-blurredVision.png b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-blurredVision.png Binary files differnew file mode 100644 index 0000000000..d89858ef53 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-blurredVision.png diff --git a/remote/test/puppeteer/test/golden-chromium/vision-deficiency-deuteranopia.png b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-deuteranopia.png Binary files differnew file mode 100644 index 0000000000..79b4b0fa1b --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-deuteranopia.png diff --git a/remote/test/puppeteer/test/golden-chromium/vision-deficiency-protanopia.png b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-protanopia.png Binary files differnew file mode 100644 index 0000000000..bede7c1ed0 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-protanopia.png diff --git a/remote/test/puppeteer/test/golden-chromium/vision-deficiency-tritanopia.png b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-tritanopia.png Binary files differnew file mode 100644 index 0000000000..d5f6bbec2e --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-tritanopia.png diff --git a/remote/test/puppeteer/test/golden-chromium/white.jpg b/remote/test/puppeteer/test/golden-chromium/white.jpg Binary files differnew file mode 100644 index 0000000000..fb9070def3 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/white.jpg diff --git a/remote/test/puppeteer/test/golden-firefox/grid-cell-0.png b/remote/test/puppeteer/test/golden-firefox/grid-cell-0.png Binary files differnew file mode 100644 index 0000000000..4677bdbc4f --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/grid-cell-0.png diff --git a/remote/test/puppeteer/test/golden-firefox/grid-cell-1.png b/remote/test/puppeteer/test/golden-firefox/grid-cell-1.png Binary files differnew file mode 100644 index 0000000000..532dc8db65 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/grid-cell-1.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.png b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.png Binary files differnew file mode 100644 index 0000000000..8e86dc9017 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.png b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.png Binary files differnew file mode 100644 index 0000000000..7a74457869 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.png Binary files differnew file mode 100644 index 0000000000..f4e059c300 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.png Binary files differnew file mode 100644 index 0000000000..f554b1d62c --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.png Binary files differnew file mode 100644 index 0000000000..d1431bd91d --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.png Binary files differnew file mode 100644 index 0000000000..6d28cddcea --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.png Binary files differnew file mode 100644 index 0000000000..2b72c7528b --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.png Binary files differnew file mode 100644 index 0000000000..0a78fb1ae7 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.png Binary files differnew file mode 100644 index 0000000000..2b72c7528b --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.png b/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.png Binary files differnew file mode 100644 index 0000000000..ac47ec83b1 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.png b/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.png Binary files differnew file mode 100644 index 0000000000..31a0935cda --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-sanity.png b/remote/test/puppeteer/test/golden-firefox/screenshot-sanity.png Binary files differnew file mode 100644 index 0000000000..07890a04b3 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-sanity.png diff --git a/remote/test/puppeteer/test/golden-utils.js b/remote/test/puppeteer/test/golden-utils.js new file mode 100644 index 0000000000..f820afe6bf --- /dev/null +++ b/remote/test/puppeteer/test/golden-utils.js @@ -0,0 +1,160 @@ +// @ts-nocheck +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const path = require('path'); +const fs = require('fs'); +const Diff = require('text-diff'); +const mime = require('mime'); +const PNG = require('pngjs').PNG; +const jpeg = require('jpeg-js'); +const pixelmatch = require('pixelmatch'); + +module.exports = { compare }; + +const GoldenComparators = { + 'image/png': compareImages, + 'image/jpeg': compareImages, + 'text/plain': compareText, +}; + +/** + * @param {?Object} actualBuffer + * @param {!Buffer} expectedBuffer + * @param {!string} mimeType + * @returns {?{diff: (!Object:undefined), errorMessage: (string|undefined)}} + */ +function compareImages(actualBuffer, expectedBuffer, mimeType) { + if (!actualBuffer || !(actualBuffer instanceof Buffer)) + return { errorMessage: 'Actual result should be Buffer.' }; + + const actual = + mimeType === 'image/png' + ? PNG.sync.read(actualBuffer) + : jpeg.decode(actualBuffer); + const expected = + mimeType === 'image/png' + ? PNG.sync.read(expectedBuffer) + : jpeg.decode(expectedBuffer); + if (expected.width !== actual.width || expected.height !== actual.height) { + return { + errorMessage: `Sizes differ: expected image ${expected.width}px X ${expected.height}px, but got ${actual.width}px X ${actual.height}px. `, + }; + } + const diff = new PNG({ width: expected.width, height: expected.height }); + const count = pixelmatch( + expected.data, + actual.data, + diff.data, + expected.width, + expected.height, + { threshold: 0.1 } + ); + return count > 0 ? { diff: PNG.sync.write(diff) } : null; +} + +/** + * @param {?Object} actual + * @param {!Buffer} expectedBuffer + * @returns {?{diff: (!Object:undefined), errorMessage: (string|undefined)}} + */ +function compareText(actual, expectedBuffer) { + if (typeof actual !== 'string') + return { errorMessage: 'Actual result should be string' }; + const expected = expectedBuffer.toString('utf-8'); + if (expected === actual) return null; + const diff = new Diff(); + const result = diff.main(expected, actual); + diff.cleanupSemantic(result); + let html = diff.prettyHtml(result); + const diffStylePath = path.join(__dirname, 'diffstyle.css'); + html = `<link rel="stylesheet" href="file://${diffStylePath}">` + html; + return { + diff: html, + diffExtension: '.html', + }; +} + +/** + * @param {?Object} actual + * @param {string} goldenName + * @returns {!{pass: boolean, message: (undefined|string)}} + */ +function compare(goldenPath, outputPath, actual, goldenName) { + goldenPath = path.normalize(goldenPath); + outputPath = path.normalize(outputPath); + const expectedPath = path.join(goldenPath, goldenName); + const actualPath = path.join(outputPath, goldenName); + + const messageSuffix = + 'Output is saved in "' + path.basename(outputPath + '" directory'); + + if (!fs.existsSync(expectedPath)) { + ensureOutputDir(); + fs.writeFileSync(actualPath, actual); + return { + pass: false, + message: goldenName + ' is missing in golden results. ' + messageSuffix, + }; + } + const expected = fs.readFileSync(expectedPath); + const mimeType = mime.getType(goldenName); + const comparator = GoldenComparators[mimeType]; + if (!comparator) { + return { + pass: false, + message: + 'Failed to find comparator with type ' + mimeType + ': ' + goldenName, + }; + } + const result = comparator(actual, expected, mimeType); + if (!result) return { pass: true }; + ensureOutputDir(); + if (goldenPath === outputPath) { + fs.writeFileSync(addSuffix(actualPath, '-actual'), actual); + } else { + fs.writeFileSync(actualPath, actual); + // Copy expected to the output/ folder for convenience. + fs.writeFileSync(addSuffix(actualPath, '-expected'), expected); + } + if (result.diff) { + const diffPath = addSuffix(actualPath, '-diff', result.diffExtension); + fs.writeFileSync(diffPath, result.diff); + } + + let message = goldenName + ' mismatch!'; + if (result.errorMessage) message += ' ' + result.errorMessage; + return { + pass: false, + message: message + ' ' + messageSuffix, + }; + + function ensureOutputDir() { + if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath); + } +} + +/** + * @param {string} filePath + * @param {string} suffix + * @param {string=} customExtension + * @returns {string} + */ +function addSuffix(filePath, suffix, customExtension) { + const dirname = path.dirname(filePath); + const ext = path.extname(filePath); + const name = path.basename(filePath, ext); + return path.join(dirname, name + suffix + (customExtension || ext)); +} diff --git a/remote/test/puppeteer/test/headful.spec.ts b/remote/test/puppeteer/test/headful.spec.ts new file mode 100644 index 0000000000..119823eb50 --- /dev/null +++ b/remote/test/puppeteer/test/headful.spec.ts @@ -0,0 +1,204 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import path from 'path'; +import os from 'os'; +import fs from 'fs'; +import { promisify } from 'util'; +import expect from 'expect'; +import { + getTestState, + describeChromeOnly, + itFailsWindows, +} from './mocha-utils'; // eslint-disable-line import/extensions +import rimraf from 'rimraf'; + +const rmAsync = promisify(rimraf); +const mkdtempAsync = promisify(fs.mkdtemp); + +const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); + +const extensionPath = path.join(__dirname, 'assets', 'simple-extension'); + +describeChromeOnly('headful tests', function () { + /* These tests fire up an actual browser so let's + * allow a higher timeout + */ + this.timeout(20 * 1000); + + let headfulOptions; + let headlessOptions; + let extensionOptions; + + beforeEach(() => { + const { defaultBrowserOptions } = getTestState(); + headfulOptions = Object.assign({}, defaultBrowserOptions, { + headless: false, + }); + headlessOptions = Object.assign({}, defaultBrowserOptions, { + headless: true, + }); + + extensionOptions = Object.assign({}, defaultBrowserOptions, { + headless: false, + args: [ + `--disable-extensions-except=${extensionPath}`, + `--load-extension=${extensionPath}`, + ], + }); + }); + + describe('HEADFUL', function () { + it('background_page target type should be available', async () => { + const { puppeteer } = getTestState(); + const browserWithExtension = await puppeteer.launch(extensionOptions); + const page = await browserWithExtension.newPage(); + const backgroundPageTarget = await browserWithExtension.waitForTarget( + (target) => target.type() === 'background_page' + ); + await page.close(); + await browserWithExtension.close(); + expect(backgroundPageTarget).toBeTruthy(); + }); + it('target.page() should return a background_page', async function () { + const { puppeteer } = getTestState(); + const browserWithExtension = await puppeteer.launch(extensionOptions); + const backgroundPageTarget = await browserWithExtension.waitForTarget( + (target) => target.type() === 'background_page' + ); + const page = await backgroundPageTarget.page(); + expect(await page.evaluate(() => 2 * 3)).toBe(6); + expect(await page.evaluate(() => globalThis.MAGIC)).toBe(42); + await browserWithExtension.close(); + }); + it('should have default url when launching browser', async function () { + const { puppeteer } = getTestState(); + const browser = await puppeteer.launch(extensionOptions); + const pages = (await browser.pages()).map((page) => page.url()); + expect(pages).toEqual(['about:blank']); + await browser.close(); + }); + itFailsWindows( + 'headless should be able to read cookies written by headful', + async () => { + /* Needs investigation into why but this fails consistently on Windows CI. */ + const { server, puppeteer } = getTestState(); + + const userDataDir = await mkdtempAsync(TMP_FOLDER); + // Write a cookie in headful chrome + const headfulBrowser = await puppeteer.launch( + Object.assign({ userDataDir }, headfulOptions) + ); + const headfulPage = await headfulBrowser.newPage(); + await headfulPage.goto(server.EMPTY_PAGE); + await headfulPage.evaluate( + () => + (document.cookie = + 'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT') + ); + await headfulBrowser.close(); + // Read the cookie from headless chrome + const headlessBrowser = await puppeteer.launch( + Object.assign({ userDataDir }, headlessOptions) + ); + const headlessPage = await headlessBrowser.newPage(); + await headlessPage.goto(server.EMPTY_PAGE); + const cookie = await headlessPage.evaluate(() => document.cookie); + await headlessBrowser.close(); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(() => {}); + expect(cookie).toBe('foo=true'); + } + ); + // TODO: Support OOOPIF. @see https://github.com/puppeteer/puppeteer/issues/2548 + xit('OOPIF: should report google.com frame', async () => { + const { server, puppeteer } = getTestState(); + + // https://google.com is isolated by default in Chromium embedder. + const browser = await puppeteer.launch(headfulOptions); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + page.on('request', (r) => r.respond({ body: 'YO, GOOGLE.COM' })); + await page.evaluate(() => { + const frame = document.createElement('iframe'); + frame.setAttribute('src', 'https://google.com/'); + document.body.appendChild(frame); + return new Promise((x) => (frame.onload = x)); + }); + await page.waitForSelector('iframe[src="https://google.com/"]'); + const urls = page + .frames() + .map((frame) => frame.url()) + .sort(); + expect(urls).toEqual([server.EMPTY_PAGE, 'https://google.com/']); + await browser.close(); + }); + it('should close browser with beforeunload page', async () => { + const { server, puppeteer } = getTestState(); + + const browser = await puppeteer.launch(headfulOptions); + const page = await browser.newPage(); + await page.goto(server.PREFIX + '/beforeunload.html'); + // We have to interact with a page so that 'beforeunload' handlers + // fire. + await page.click('body'); + await browser.close(); + }); + it('should open devtools when "devtools: true" option is given', async () => { + const { puppeteer } = getTestState(); + + const browser = await puppeteer.launch( + Object.assign({ devtools: true }, headfulOptions) + ); + const context = await browser.createIncognitoBrowserContext(); + await Promise.all([ + context.newPage(), + context.waitForTarget((target) => target.url().includes('devtools://')), + ]); + await browser.close(); + }); + }); + + describe('Page.bringToFront', function () { + it('should work', async () => { + const { puppeteer } = getTestState(); + const browser = await puppeteer.launch(headfulOptions); + const page1 = await browser.newPage(); + const page2 = await browser.newPage(); + + await page1.bringToFront(); + expect(await page1.evaluate(() => document.visibilityState)).toBe( + 'visible' + ); + expect(await page2.evaluate(() => document.visibilityState)).toBe( + 'hidden' + ); + + await page2.bringToFront(); + expect(await page1.evaluate(() => document.visibilityState)).toBe( + 'hidden' + ); + expect(await page2.evaluate(() => document.visibilityState)).toBe( + 'visible' + ); + + await page1.close(); + await page2.close(); + await browser.close(); + }); + }); +}); diff --git a/remote/test/puppeteer/test/idle_override.spec.ts b/remote/test/puppeteer/test/idle_override.spec.ts new file mode 100644 index 0000000000..b6a927e48a --- /dev/null +++ b/remote/test/puppeteer/test/idle_override.spec.ts @@ -0,0 +1,93 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Emulate idle state', () => { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + async function getIdleState() { + const { page } = getTestState(); + + const stateElement = await page.$('#state'); + return await page.evaluate((element: HTMLElement) => { + return element.innerText; + }, stateElement); + } + + async function verifyState(expectedState: string) { + const actualState = await getIdleState(); + expect(actualState).toEqual(expectedState); + } + + it('changing idle state emulation causes change of the IdleDetector state', async () => { + const { page, server, context } = getTestState(); + await context.overridePermissions(server.PREFIX + '/idle-detector.html', [ + 'idle-detection', + ]); + + await page.goto(server.PREFIX + '/idle-detector.html'); + + // Store initial state, as soon as it is not guaranteed to be `active, unlocked`. + const initialState = await getIdleState(); + + // Emulate Idle states and verify IdleDetector updates state accordingly. + await page.emulateIdleState({ + isUserActive: false, + isScreenUnlocked: false, + }); + await verifyState('Idle state: idle, locked.'); + + await page.emulateIdleState({ + isUserActive: true, + isScreenUnlocked: false, + }); + await verifyState('Idle state: active, locked.'); + + await page.emulateIdleState({ + isUserActive: true, + isScreenUnlocked: true, + }); + await verifyState('Idle state: active, unlocked.'); + + await page.emulateIdleState({ + isUserActive: false, + isScreenUnlocked: true, + }); + await verifyState('Idle state: idle, unlocked.'); + + // Remove Idle emulation and verify IdleDetector is in initial state. + await page.emulateIdleState(); + await verifyState(initialState); + + // Emulate idle state again after removing emulation. + await page.emulateIdleState({ + isUserActive: false, + isScreenUnlocked: false, + }); + await verifyState('Idle state: idle, locked.'); + + // Remove emulation second time. + await page.emulateIdleState(); + await verifyState(initialState); + }); +}); diff --git a/remote/test/puppeteer/test/ignorehttpserrors.spec.ts b/remote/test/puppeteer/test/ignorehttpserrors.spec.ts new file mode 100644 index 0000000000..81252af298 --- /dev/null +++ b/remote/test/puppeteer/test/ignorehttpserrors.spec.ts @@ -0,0 +1,133 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import { + getTestState, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('ignoreHTTPSErrors', function () { + /* Note that this test creates its own browser rather than use + * the one provided by the test set-up as we need one + * with ignoreHTTPSErrors set to true + */ + let browser; + let context; + let page; + + before(async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + const options = Object.assign( + { ignoreHTTPSErrors: true }, + defaultBrowserOptions + ); + browser = await puppeteer.launch(options); + }); + + after(async () => { + await browser.close(); + browser = null; + }); + + beforeEach(async () => { + context = await browser.createIncognitoBrowserContext(); + page = await context.newPage(); + }); + + afterEach(async () => { + await context.close(); + context = null; + page = null; + }); + + describe('Response.securityDetails', function () { + it('should work', async () => { + const { httpsServer } = getTestState(); + + const [serverRequest, response] = await Promise.all([ + httpsServer.waitForRequest('/empty.html'), + page.goto(httpsServer.EMPTY_PAGE), + ]); + const securityDetails = response.securityDetails(); + expect(securityDetails.issuer()).toBe('puppeteer-tests'); + const protocol = serverRequest.socket.getProtocol().replace('v', ' '); + expect(securityDetails.protocol()).toBe(protocol); + expect(securityDetails.subjectName()).toBe('puppeteer-tests'); + expect(securityDetails.validFrom()).toBe(1589357069); + expect(securityDetails.validTo()).toBe(1904717069); + expect(securityDetails.subjectAlternativeNames()).toEqual([ + 'www.puppeteer-tests.test', + 'www.puppeteer-tests-1.test', + ]); + }); + it('should be |null| for non-secure requests', async () => { + const { server } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE); + expect(response.securityDetails()).toBe(null); + }); + it('Network redirects should report SecurityDetails', async () => { + const { httpsServer } = getTestState(); + + httpsServer.setRedirect('/plzredirect', '/empty.html'); + const responses = []; + page.on('response', (response) => responses.push(response)); + const [serverRequest] = await Promise.all([ + httpsServer.waitForRequest('/plzredirect'), + page.goto(httpsServer.PREFIX + '/plzredirect'), + ]); + expect(responses.length).toBe(2); + expect(responses[0].status()).toBe(302); + const securityDetails = responses[0].securityDetails(); + const protocol = serverRequest.socket.getProtocol().replace('v', ' '); + expect(securityDetails.protocol()).toBe(protocol); + }); + }); + + it('should work', async () => { + const { httpsServer } = getTestState(); + + let error = null; + const response = await page + .goto(httpsServer.EMPTY_PAGE) + .catch((error_) => (error = error_)); + expect(error).toBe(null); + expect(response.ok()).toBe(true); + }); + it('should work with request interception', async () => { + const { httpsServer } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + const response = await page.goto(httpsServer.EMPTY_PAGE); + expect(response.status()).toBe(200); + }); + it('should work with mixed content', async () => { + const { server, httpsServer } = getTestState(); + + httpsServer.setRoute('/mixedcontent.html', (req, res) => { + res.end(`<iframe src=${server.EMPTY_PAGE}></iframe>`); + }); + await page.goto(httpsServer.PREFIX + '/mixedcontent.html', { + waitUntil: 'load', + }); + expect(page.frames().length).toBe(2); + // Make sure blocked iframe has functional execution context + // @see https://github.com/puppeteer/puppeteer/issues/2709 + expect(await page.frames()[0].evaluate('1 + 2')).toBe(3); + expect(await page.frames()[1].evaluate('2 + 3')).toBe(5); + }); +}); diff --git a/remote/test/puppeteer/test/input.spec.ts b/remote/test/puppeteer/test/input.spec.ts new file mode 100644 index 0000000000..d87aa1375f --- /dev/null +++ b/remote/test/puppeteer/test/input.spec.ts @@ -0,0 +1,337 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import path from 'path'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +const FILE_TO_UPLOAD = path.join(__dirname, '/assets/file-to-upload.txt'); + +describe('input tests', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describeFailsFirefox('input', function () { + it('should upload the file', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/fileupload.html'); + const filePath = path.relative(process.cwd(), FILE_TO_UPLOAD); + const input = await page.$('input'); + await page.evaluate((e: HTMLElement) => { + globalThis._inputEvents = []; + e.addEventListener('change', (ev) => + globalThis._inputEvents.push(ev.type) + ); + e.addEventListener('input', (ev) => + globalThis._inputEvents.push(ev.type) + ); + }, input); + await input.uploadFile(filePath); + expect( + await page.evaluate((e: HTMLInputElement) => e.files[0].name, input) + ).toBe('file-to-upload.txt'); + expect( + await page.evaluate((e: HTMLInputElement) => e.files[0].type, input) + ).toBe('text/plain'); + expect(await page.evaluate(() => globalThis._inputEvents)).toEqual([ + 'input', + 'change', + ]); + expect( + await page.evaluate((e: HTMLInputElement) => { + const reader = new FileReader(); + const promise = new Promise((fulfill) => (reader.onload = fulfill)); + reader.readAsText(e.files[0]); + return promise.then(() => reader.result); + }, input) + ).toBe('contents of the file'); + }); + }); + + describeFailsFirefox('Page.waitForFileChooser', function () { + it('should work when file input is attached to DOM', async () => { + const { page } = getTestState(); + + await page.setContent(`<input type=file>`); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser).toBeTruthy(); + }); + it('should work when file input is not attached to DOM', async () => { + const { page } = getTestState(); + + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.evaluate(() => { + const el = document.createElement('input'); + el.type = 'file'; + el.click(); + }), + ]); + expect(chooser).toBeTruthy(); + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + await page + .waitForFileChooser({ timeout: 1 }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default timeout when there is no custom timeout', async () => { + const { page, puppeteer } = getTestState(); + + page.setDefaultTimeout(1); + let error = null; + await page.waitForFileChooser().catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should prioritize exact timeout over default timeout', async () => { + const { page, puppeteer } = getTestState(); + + page.setDefaultTimeout(0); + let error = null; + await page + .waitForFileChooser({ timeout: 1 }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should work with no timeout', async () => { + const { page } = getTestState(); + + const [chooser] = await Promise.all([ + page.waitForFileChooser({ timeout: 0 }), + page.evaluate(() => + setTimeout(() => { + const el = document.createElement('input'); + el.type = 'file'; + el.click(); + }, 50) + ), + ]); + expect(chooser).toBeTruthy(); + }); + it('should return the same file chooser when there are many watchdogs simultaneously', async () => { + const { page } = getTestState(); + + await page.setContent(`<input type=file>`); + const [fileChooser1, fileChooser2] = await Promise.all([ + page.waitForFileChooser(), + page.waitForFileChooser(), + page.$eval('input', (input: HTMLInputElement) => input.click()), + ]); + expect(fileChooser1 === fileChooser2).toBe(true); + }); + }); + + describeFailsFirefox('FileChooser.accept', function () { + it('should accept single file', async () => { + const { page } = getTestState(); + + await page.setContent( + `<input type=file oninput='javascript:console.timeStamp()'>` + ); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + await Promise.all([ + chooser.accept([FILE_TO_UPLOAD]), + new Promise((x) => page.once('metrics', x)), + ]); + expect( + await page.$eval( + 'input', + (input: HTMLInputElement) => input.files.length + ) + ).toBe(1); + expect( + await page.$eval( + 'input', + (input: HTMLInputElement) => input.files[0].name + ) + ).toBe('file-to-upload.txt'); + }); + it('should be able to read selected file', async () => { + const { page } = getTestState(); + + await page.setContent(`<input type=file>`); + page + .waitForFileChooser() + .then((chooser) => chooser.accept([FILE_TO_UPLOAD])); + expect( + await page.$eval('input', async (picker: HTMLInputElement) => { + picker.click(); + await new Promise((x) => (picker.oninput = x)); + const reader = new FileReader(); + const promise = new Promise((fulfill) => (reader.onload = fulfill)); + reader.readAsText(picker.files[0]); + return promise.then(() => reader.result); + }) + ).toBe('contents of the file'); + }); + it('should be able to reset selected files with empty file list', async () => { + const { page } = getTestState(); + + await page.setContent(`<input type=file>`); + page + .waitForFileChooser() + .then((chooser) => chooser.accept([FILE_TO_UPLOAD])); + expect( + await page.$eval('input', async (picker: HTMLInputElement) => { + picker.click(); + await new Promise((x) => (picker.oninput = x)); + return picker.files.length; + }) + ).toBe(1); + page.waitForFileChooser().then((chooser) => chooser.accept([])); + expect( + await page.$eval('input', async (picker: HTMLInputElement) => { + picker.click(); + await new Promise((x) => (picker.oninput = x)); + return picker.files.length; + }) + ).toBe(0); + }); + it('should not accept multiple files for single-file input', async () => { + const { page } = getTestState(); + + await page.setContent(`<input type=file>`); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + let error = null; + await chooser + .accept([ + path.relative( + process.cwd(), + __dirname + '/assets/file-to-upload.txt' + ), + path.relative(process.cwd(), __dirname + '/assets/pptr.png'), + ]) + .catch((error_) => (error = error_)); + expect(error).not.toBe(null); + }); + it('should fail for non-existent files', async () => { + const { page } = getTestState(); + + await page.setContent(`<input type=file>`); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + let error = null; + await chooser + .accept(['file-does-not-exist.txt']) + .catch((error_) => (error = error_)); + expect(error).not.toBe(null); + }); + it('should fail when accepting file chooser twice', async () => { + const { page } = getTestState(); + + await page.setContent(`<input type=file>`); + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', (input: HTMLInputElement) => input.click()), + ]); + await fileChooser.accept([]); + let error = null; + await fileChooser.accept([]).catch((error_) => (error = error_)); + expect(error.message).toBe( + 'Cannot accept FileChooser which is already handled!' + ); + }); + }); + + describeFailsFirefox('FileChooser.cancel', function () { + it('should cancel dialog', async () => { + const { page } = getTestState(); + + // Consider file chooser canceled if we can summon another one. + // There's no reliable way in WebPlatform to see that FileChooser was + // canceled. + await page.setContent(`<input type=file>`); + const [fileChooser1] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', (input: HTMLInputElement) => input.click()), + ]); + await fileChooser1.cancel(); + // If this resolves, than we successfully canceled file chooser. + await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', (input: HTMLInputElement) => input.click()), + ]); + }); + it('should fail when canceling file chooser twice', async () => { + const { page } = getTestState(); + + await page.setContent(`<input type=file>`); + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', (input: HTMLInputElement) => input.click()), + ]); + await fileChooser.cancel(); + let error = null; + await fileChooser.cancel().catch((error_) => (error = error_)); + expect(error.message).toBe( + 'Cannot cancel FileChooser which is already handled!' + ); + }); + }); + + describeFailsFirefox('FileChooser.isMultiple', () => { + it('should work for single file pick', async () => { + const { page } = getTestState(); + + await page.setContent(`<input type=file>`); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(false); + }); + it('should work for "multiple"', async () => { + const { page } = getTestState(); + + await page.setContent(`<input multiple type=file>`); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(true); + }); + it('should work for "webkitdirectory"', async () => { + const { page } = getTestState(); + + await page.setContent(`<input multiple webkitdirectory type=file>`); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(true); + }); + }); +}); diff --git a/remote/test/puppeteer/test/jshandle.spec.ts b/remote/test/puppeteer/test/jshandle.spec.ts new file mode 100644 index 0000000000..35b0f8edbe --- /dev/null +++ b/remote/test/puppeteer/test/jshandle.spec.ts @@ -0,0 +1,282 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('JSHandle', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.evaluateHandle', function () { + it('should work', async () => { + const { page } = getTestState(); + + const windowHandle = await page.evaluateHandle(() => window); + expect(windowHandle).toBeTruthy(); + }); + it('should accept object handle as an argument', async () => { + const { page } = getTestState(); + + const navigatorHandle = await page.evaluateHandle(() => navigator); + const text = await page.evaluate( + (e: Navigator) => e.userAgent, + navigatorHandle + ); + expect(text).toContain('Mozilla'); + }); + it('should accept object handle to primitive types', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => 5); + const isFive = await page.evaluate((e) => Object.is(e, 5), aHandle); + expect(isFive).toBeTruthy(); + }); + it('should warn on nested object handles', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => document.body); + let error = null; + await page + // @ts-expect-error we are deliberately passing a bad type here (nested object) + .evaluateHandle((opts) => opts.elem.querySelector('p'), { + elem: aHandle, + }) + .catch((error_) => (error = error_)); + expect(error.message).toContain('Are you passing a nested JSHandle?'); + }); + it('should accept object handle to unserializable value', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => Infinity); + expect(await page.evaluate((e) => Object.is(e, Infinity), aHandle)).toBe( + true + ); + }); + it('should use the same JS wrappers', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + globalThis.FOO = 123; + return window; + }); + expect(await page.evaluate((e: { FOO: number }) => e.FOO, aHandle)).toBe( + 123 + ); + }); + it('should work with primitives', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + globalThis.FOO = 123; + return window; + }); + expect(await page.evaluate((e: { FOO: number }) => e.FOO, aHandle)).toBe( + 123 + ); + }); + }); + + describe('JSHandle.getProperty', function () { + it('should work', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => ({ + one: 1, + two: 2, + three: 3, + })); + const twoHandle = await aHandle.getProperty('two'); + expect(await twoHandle.jsonValue()).toEqual(2); + }); + }); + + describe('JSHandle.jsonValue', function () { + it('should work', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => ({ foo: 'bar' })); + const json = await aHandle.jsonValue(); + expect(json).toEqual({ foo: 'bar' }); + }); + it('should not work with dates', async () => { + const { page } = getTestState(); + + const dateHandle = await page.evaluateHandle( + () => new Date('2017-09-26T00:00:00.000Z') + ); + const json = await dateHandle.jsonValue(); + expect(json).toEqual({}); + }); + it('should throw for circular objects', async () => { + const { page, isChrome } = getTestState(); + + const windowHandle = await page.evaluateHandle('window'); + let error = null; + await windowHandle.jsonValue().catch((error_) => (error = error_)); + if (isChrome) + expect(error.message).toContain('Object reference chain is too long'); + else expect(error.message).toContain('Object is not serializable'); + }); + }); + + describe('JSHandle.getProperties', function () { + it('should work', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => ({ + foo: 'bar', + })); + const properties = await aHandle.getProperties(); + const foo = properties.get('foo'); + expect(foo).toBeTruthy(); + expect(await foo.jsonValue()).toBe('bar'); + }); + it('should return even non-own properties', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + class A { + a: string; + constructor() { + this.a = '1'; + } + } + class B extends A { + b: string; + constructor() { + super(); + this.b = '2'; + } + } + return new B(); + }); + const properties = await aHandle.getProperties(); + expect(await properties.get('a').jsonValue()).toBe('1'); + expect(await properties.get('b').jsonValue()).toBe('2'); + }); + }); + + describe('JSHandle.asElement', function () { + it('should work', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => document.body); + const element = aHandle.asElement(); + expect(element).toBeTruthy(); + }); + it('should return null for non-elements', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => 2); + const element = aHandle.asElement(); + expect(element).toBeFalsy(); + }); + it('should return ElementHandle for TextNodes', async () => { + const { page } = getTestState(); + + await page.setContent('<div>ee!</div>'); + const aHandle = await page.evaluateHandle( + () => document.querySelector('div').firstChild + ); + const element = aHandle.asElement(); + expect(element).toBeTruthy(); + expect( + await page.evaluate( + (e: HTMLElement) => e.nodeType === Node.TEXT_NODE, + element + ) + ); + }); + }); + + describe('JSHandle.toString', function () { + it('should work for primitives', async () => { + const { page } = getTestState(); + + const numberHandle = await page.evaluateHandle(() => 2); + expect(numberHandle.toString()).toBe('JSHandle:2'); + const stringHandle = await page.evaluateHandle(() => 'a'); + expect(stringHandle.toString()).toBe('JSHandle:a'); + }); + it('should work for complicated objects', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => window); + expect(aHandle.toString()).toBe('JSHandle@object'); + }); + it('should work with different subtypes', async () => { + const { page } = getTestState(); + + expect((await page.evaluateHandle('(function(){})')).toString()).toBe( + 'JSHandle@function' + ); + expect((await page.evaluateHandle('12')).toString()).toBe('JSHandle:12'); + expect((await page.evaluateHandle('true')).toString()).toBe( + 'JSHandle:true' + ); + expect((await page.evaluateHandle('undefined')).toString()).toBe( + 'JSHandle:undefined' + ); + expect((await page.evaluateHandle('"foo"')).toString()).toBe( + 'JSHandle:foo' + ); + expect((await page.evaluateHandle('Symbol()')).toString()).toBe( + 'JSHandle@symbol' + ); + expect((await page.evaluateHandle('new Map()')).toString()).toBe( + 'JSHandle@map' + ); + expect((await page.evaluateHandle('new Set()')).toString()).toBe( + 'JSHandle@set' + ); + expect((await page.evaluateHandle('[]')).toString()).toBe( + 'JSHandle@array' + ); + expect((await page.evaluateHandle('null')).toString()).toBe( + 'JSHandle:null' + ); + expect((await page.evaluateHandle('/foo/')).toString()).toBe( + 'JSHandle@regexp' + ); + expect((await page.evaluateHandle('document.body')).toString()).toBe( + 'JSHandle@node' + ); + expect((await page.evaluateHandle('new Date()')).toString()).toBe( + 'JSHandle@date' + ); + expect((await page.evaluateHandle('new WeakMap()')).toString()).toBe( + 'JSHandle@weakmap' + ); + expect((await page.evaluateHandle('new WeakSet()')).toString()).toBe( + 'JSHandle@weakset' + ); + expect((await page.evaluateHandle('new Error()')).toString()).toBe( + 'JSHandle@error' + ); + expect((await page.evaluateHandle('new Int32Array()')).toString()).toBe( + 'JSHandle@typedarray' + ); + expect((await page.evaluateHandle('new Proxy({}, {})')).toString()).toBe( + 'JSHandle@proxy' + ); + }); + }); +}); diff --git a/remote/test/puppeteer/test/keyboard.spec.ts b/remote/test/puppeteer/test/keyboard.spec.ts new file mode 100644 index 0000000000..c6d28dac8e --- /dev/null +++ b/remote/test/puppeteer/test/keyboard.spec.ts @@ -0,0 +1,407 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import utils from './utils.js'; +import os from 'os'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions +import { KeyInput } from '../lib/cjs/puppeteer/common/USKeyboardLayout.js'; + +describe('Keyboard', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should type into a textarea', async () => { + const { page } = getTestState(); + + await page.evaluate(() => { + const textarea = document.createElement('textarea'); + document.body.appendChild(textarea); + textarea.focus(); + }); + const text = 'Hello world. I am the text that was typed!'; + await page.keyboard.type(text); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe(text); + }); + it('should press the metaKey', async () => { + const { page, isFirefox } = getTestState(); + + await page.evaluate(() => { + (window as any).keyPromise = new Promise((resolve) => + document.addEventListener('keydown', (event) => resolve(event.key)) + ); + }); + await page.keyboard.press('Meta'); + expect(await page.evaluate('keyPromise')).toBe( + isFirefox && os.platform() !== 'darwin' ? 'OS' : 'Meta' + ); + }); + it('should move with the arrow keys', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.type('textarea', 'Hello World!'); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe('Hello World!'); + for (let i = 0; i < 'World!'.length; i++) page.keyboard.press('ArrowLeft'); + await page.keyboard.type('inserted '); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe('Hello inserted World!'); + page.keyboard.down('Shift'); + for (let i = 0; i < 'inserted '.length; i++) + page.keyboard.press('ArrowLeft'); + page.keyboard.up('Shift'); + await page.keyboard.press('Backspace'); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe('Hello World!'); + }); + it('should send a character with ElementHandle.press', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + const textarea = await page.$('textarea'); + await textarea.press('a'); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe('a'); + + await page.evaluate(() => + window.addEventListener('keydown', (e) => e.preventDefault(), true) + ); + + await textarea.press('b'); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe('a'); + }); + it( + 'ElementHandle.press should support |text| option', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + const textarea = await page.$('textarea'); + await textarea.press('a', { text: 'ё' }); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe('ё'); + } + ); + it('should send a character with sendCharacter', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + await page.keyboard.sendCharacter('嗨'); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe('嗨'); + await page.evaluate(() => + window.addEventListener('keydown', (e) => e.preventDefault(), true) + ); + await page.keyboard.sendCharacter('a'); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe('嗨a'); + }); + it('should report shiftKey', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/keyboard.html'); + const keyboard = page.keyboard; + const codeForKey = new Map<KeyInput, number>([ + ['Shift', 16], + ['Alt', 18], + ['Control', 17], + ]); + for (const [modifierKey, modifierCode] of codeForKey) { + await keyboard.down(modifierKey); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keydown: ' + + modifierKey + + ' ' + + modifierKey + + 'Left ' + + modifierCode + + ' [' + + modifierKey + + ']' + ); + await keyboard.down('!'); + // Shift+! will generate a keypress + if (modifierKey === 'Shift') + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keydown: ! Digit1 49 [' + + modifierKey + + ']\nKeypress: ! Digit1 33 33 [' + + modifierKey + + ']' + ); + else + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keydown: ! Digit1 49 [' + modifierKey + ']' + ); + + await keyboard.up('!'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keyup: ! Digit1 49 [' + modifierKey + ']' + ); + await keyboard.up(modifierKey); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keyup: ' + + modifierKey + + ' ' + + modifierKey + + 'Left ' + + modifierCode + + ' []' + ); + } + }); + it('should report multiple modifiers', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/keyboard.html'); + const keyboard = page.keyboard; + await keyboard.down('Control'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keydown: Control ControlLeft 17 [Control]' + ); + await keyboard.down('Alt'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keydown: Alt AltLeft 18 [Alt Control]' + ); + await keyboard.down(';'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keydown: ; Semicolon 186 [Alt Control]' + ); + await keyboard.up(';'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keyup: ; Semicolon 186 [Alt Control]' + ); + await keyboard.up('Control'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keyup: Control ControlLeft 17 [Alt]' + ); + await keyboard.up('Alt'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keyup: Alt AltLeft 18 []' + ); + }); + it('should send proper codes while typing', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/keyboard.html'); + await page.keyboard.type('!'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + [ + 'Keydown: ! Digit1 49 []', + 'Keypress: ! Digit1 33 33 []', + 'Keyup: ! Digit1 49 []', + ].join('\n') + ); + await page.keyboard.type('^'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + [ + 'Keydown: ^ Digit6 54 []', + 'Keypress: ^ Digit6 94 94 []', + 'Keyup: ^ Digit6 54 []', + ].join('\n') + ); + }); + it('should send proper codes while typing with shift', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/keyboard.html'); + const keyboard = page.keyboard; + await keyboard.down('Shift'); + await page.keyboard.type('~'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + [ + 'Keydown: Shift ShiftLeft 16 [Shift]', + 'Keydown: ~ Backquote 192 [Shift]', // 192 is ` keyCode + 'Keypress: ~ Backquote 126 126 [Shift]', // 126 is ~ charCode + 'Keyup: ~ Backquote 192 [Shift]', + ].join('\n') + ); + await keyboard.up('Shift'); + }); + it('should not type canceled events', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + await page.evaluate(() => { + window.addEventListener( + 'keydown', + (event) => { + event.stopPropagation(); + event.stopImmediatePropagation(); + if (event.key === 'l') event.preventDefault(); + if (event.key === 'o') event.preventDefault(); + }, + false + ); + }); + await page.keyboard.type('Hello World!'); + expect(await page.evaluate(() => globalThis.textarea.value)).toBe( + 'He Wrd!' + ); + }); + it('should specify repeat property', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + await page.evaluate(() => + document + .querySelector('textarea') + .addEventListener('keydown', (e) => (globalThis.lastEvent = e), true) + ); + await page.keyboard.down('a'); + expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(false); + await page.keyboard.press('a'); + expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(true); + + await page.keyboard.down('b'); + expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(false); + await page.keyboard.down('b'); + expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(true); + + await page.keyboard.up('a'); + await page.keyboard.down('a'); + expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(false); + }); + it('should type all kinds of characters', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + const text = 'This text goes onto two lines.\nThis character is 嗨.'; + await page.keyboard.type(text); + expect(await page.evaluate('result')).toBe(text); + }); + it('should specify location', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.evaluate(() => { + window.addEventListener( + 'keydown', + (event) => (globalThis.keyLocation = event.location), + true + ); + }); + const textarea = await page.$('textarea'); + + await textarea.press('Digit5'); + expect(await page.evaluate('keyLocation')).toBe(0); + + await textarea.press('ControlLeft'); + expect(await page.evaluate('keyLocation')).toBe(1); + + await textarea.press('ControlRight'); + expect(await page.evaluate('keyLocation')).toBe(2); + + await textarea.press('NumpadSubtract'); + expect(await page.evaluate('keyLocation')).toBe(3); + }); + it('should throw on unknown keys', async () => { + const { page } = getTestState(); + + let error = await page.keyboard + // @ts-expect-error bad input + .press('NotARealKey') + .catch((error_) => error_); + expect(error.message).toBe('Unknown key: "NotARealKey"'); + + // @ts-expect-error bad input + error = await page.keyboard.press('ё').catch((error_) => error_); + expect(error && error.message).toBe('Unknown key: "ё"'); + + // @ts-expect-error bad input + error = await page.keyboard.press('😊').catch((error_) => error_); + expect(error && error.message).toBe('Unknown key: "😊"'); + }); + it('should type emoji', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.type('textarea', '👹 Tokyo street Japan 🇯🇵'); + expect( + await page.$eval( + 'textarea', + (textarea: HTMLInputElement) => textarea.value + ) + ).toBe('👹 Tokyo street Japan 🇯🇵'); + }); + it('should type emoji into an iframe', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame( + page, + 'emoji-test', + server.PREFIX + '/input/textarea.html' + ); + const frame = page.frames()[1]; + const textarea = await frame.$('textarea'); + await textarea.type('👹 Tokyo street Japan 🇯🇵'); + expect( + await frame.$eval( + 'textarea', + (textarea: HTMLInputElement) => textarea.value + ) + ).toBe('👹 Tokyo street Japan 🇯🇵'); + }); + it('should press the meta key', async () => { + const { page, isFirefox } = getTestState(); + + await page.evaluate(() => { + globalThis.result = null; + document.addEventListener('keydown', (event) => { + globalThis.result = [event.key, event.code, event.metaKey]; + }); + }); + await page.keyboard.press('Meta'); + // Have to do this because we lose a lot of type info when evaluating a + // string not a function. This is why functions are recommended rather than + // using strings (although we'll leave this test so we have coverage of both + // approaches.) + const [key, code, metaKey] = (await page.evaluate('result')) as [ + string, + string, + boolean + ]; + if (isFirefox && os.platform() !== 'darwin') expect(key).toBe('OS'); + else expect(key).toBe('Meta'); + + if (isFirefox) expect(code).toBe('OSLeft'); + else expect(code).toBe('MetaLeft'); + + if (isFirefox && os.platform() !== 'darwin') expect(metaKey).toBe(false); + else expect(metaKey).toBe(true); + }); +}); diff --git a/remote/test/puppeteer/test/launcher.spec.ts b/remote/test/puppeteer/test/launcher.spec.ts new file mode 100644 index 0000000000..4c9c2b6e61 --- /dev/null +++ b/remote/test/puppeteer/test/launcher.spec.ts @@ -0,0 +1,654 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import sinon from 'sinon'; +import { promisify } from 'util'; +import { + getTestState, + itOnlyRegularInstall, +} from './mocha-utils'; // eslint-disable-line import/extensions +import utils from './utils.js'; +import expect from 'expect'; +import rimraf from 'rimraf'; +import { Page } from '../lib/cjs/puppeteer/common/Page.js'; + +const rmAsync = promisify(rimraf); +const mkdtempAsync = promisify(fs.mkdtemp); +const readFileAsync = promisify(fs.readFile); +const statAsync = promisify(fs.stat); +const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); +const FIREFOX_TIMEOUT = 30 * 1000; + +describe('Launcher specs', function () { + if (getTestState().isFirefox) this.timeout(FIREFOX_TIMEOUT); + + describe('Puppeteer', function () { + describe('BrowserFetcher', function () { + it('should download and extract chrome linux binary', async () => { + const { server, puppeteer } = getTestState(); + + const downloadsFolder = await mkdtempAsync(TMP_FOLDER); + const browserFetcher = puppeteer.createBrowserFetcher({ + platform: 'linux', + path: downloadsFolder, + host: server.PREFIX, + }); + const expectedRevision = '123456'; + let revisionInfo = browserFetcher.revisionInfo(expectedRevision); + server.setRoute( + revisionInfo.url.substring(server.PREFIX.length), + (req, res) => { + server.serveFile(req, res, '/chromium-linux.zip'); + } + ); + + expect(revisionInfo.local).toBe(false); + expect(browserFetcher.platform()).toBe('linux'); + expect(browserFetcher.product()).toBe('chrome'); + expect(!!browserFetcher.host()).toBe(true); + expect(await browserFetcher.canDownload('100000')).toBe(false); + expect(await browserFetcher.canDownload(expectedRevision)).toBe(true); + + revisionInfo = await browserFetcher.download(expectedRevision); + expect(revisionInfo.local).toBe(true); + expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe( + 'LINUX BINARY\n' + ); + const expectedPermissions = os.platform() === 'win32' ? 0o666 : 0o755; + expect( + (await statAsync(revisionInfo.executablePath)).mode & 0o777 + ).toBe(expectedPermissions); + expect(await browserFetcher.localRevisions()).toEqual([ + expectedRevision, + ]); + await browserFetcher.remove(expectedRevision); + expect(await browserFetcher.localRevisions()).toEqual([]); + await rmAsync(downloadsFolder); + }); + it('should download and extract firefox linux binary', async () => { + const { server, puppeteer } = getTestState(); + + const downloadsFolder = await mkdtempAsync(TMP_FOLDER); + const browserFetcher = puppeteer.createBrowserFetcher({ + platform: 'linux', + path: downloadsFolder, + host: server.PREFIX, + product: 'firefox', + }); + const expectedVersion = '75.0a1'; + let revisionInfo = browserFetcher.revisionInfo(expectedVersion); + server.setRoute( + revisionInfo.url.substring(server.PREFIX.length), + (req, res) => { + server.serveFile( + req, + res, + `/firefox-${expectedVersion}.en-US.linux-x86_64.tar.bz2` + ); + } + ); + + expect(revisionInfo.local).toBe(false); + expect(browserFetcher.platform()).toBe('linux'); + expect(browserFetcher.product()).toBe('firefox'); + expect(await browserFetcher.canDownload('100000')).toBe(false); + expect(await browserFetcher.canDownload(expectedVersion)).toBe(true); + + revisionInfo = await browserFetcher.download(expectedVersion); + expect(revisionInfo.local).toBe(true); + expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe( + 'FIREFOX LINUX BINARY\n' + ); + const expectedPermissions = os.platform() === 'win32' ? 0o666 : 0o755; + expect( + (await statAsync(revisionInfo.executablePath)).mode & 0o777 + ).toBe(expectedPermissions); + expect(await browserFetcher.localRevisions()).toEqual([ + expectedVersion, + ]); + await browserFetcher.remove(expectedVersion); + expect(await browserFetcher.localRevisions()).toEqual([]); + await rmAsync(downloadsFolder); + }); + }); + + describe('Browser.disconnect', function () { + it('should reject navigation when browser closes', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + server.setRoute('/one-style.css', () => {}); + const browser = await puppeteer.launch(defaultBrowserOptions); + const remote = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + }); + const page = await remote.newPage(); + const navigationPromise = page + .goto(server.PREFIX + '/one-style.html', { timeout: 60000 }) + .catch((error_) => error_); + await server.waitForRequest('/one-style.css'); + remote.disconnect(); + const error = await navigationPromise; + expect(error.message).toBe( + 'Navigation failed because browser has disconnected!' + ); + await browser.close(); + }); + it('should reject waitForSelector when browser closes', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + server.setRoute('/empty.html', () => {}); + const browser = await puppeteer.launch(defaultBrowserOptions); + const remote = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + }); + const page = await remote.newPage(); + const watchdog = page + .waitForSelector('div', { timeout: 60000 }) + .catch((error_) => error_); + remote.disconnect(); + const error = await watchdog; + expect(error.message).toContain('Protocol error'); + await browser.close(); + }); + }); + describe('Browser.close', function () { + it('should terminate network waiters', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const browser = await puppeteer.launch(defaultBrowserOptions); + const remote = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + }); + const newPage = await remote.newPage(); + const results = await Promise.all([ + newPage.waitForRequest(server.EMPTY_PAGE).catch((error) => error), + newPage.waitForResponse(server.EMPTY_PAGE).catch((error) => error), + browser.close(), + ]); + for (let i = 0; i < 2; i++) { + const message = results[i].message; + expect(message).toContain('Target closed'); + expect(message).not.toContain('Timeout'); + } + await browser.close(); + }); + }); + describe('Puppeteer.launch', function () { + it('should reject all promises when browser is closed', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + const browser = await puppeteer.launch(defaultBrowserOptions); + const page = await browser.newPage(); + let error = null; + const neverResolves = page + .evaluate(() => new Promise(() => {})) + .catch((error_) => (error = error_)); + await browser.close(); + await neverResolves; + expect(error.message).toContain('Protocol error'); + }); + it('should reject if executable path is invalid', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + + let waitError = null; + const options = Object.assign({}, defaultBrowserOptions, { + executablePath: 'random-invalid-path', + }); + await puppeteer.launch(options).catch((error) => (waitError = error)); + expect(waitError.message).toContain('Failed to launch'); + }); + it('userDataDir option', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({ userDataDir }, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + // Open a page to make sure its functional. + await browser.newPage(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await browser.close(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(() => {}); + }); + it('userDataDir argument', async () => { + const { isChrome, puppeteer, defaultBrowserOptions } = getTestState(); + + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({}, defaultBrowserOptions); + if (isChrome) { + options.args = [ + ...(defaultBrowserOptions.args || []), + `--user-data-dir=${userDataDir}`, + ]; + } else { + options.args = [ + ...(defaultBrowserOptions.args || []), + `-profile`, + userDataDir, + ]; + } + const browser = await puppeteer.launch(options); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await browser.close(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(() => {}); + }); + it('userDataDir option should restore state', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({ userDataDir }, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => (localStorage.hey = 'hello')); + await browser.close(); + + const browser2 = await puppeteer.launch(options); + const page2 = await browser2.newPage(); + await page2.goto(server.EMPTY_PAGE); + expect(await page2.evaluate(() => localStorage.hey)).toBe('hello'); + await browser2.close(); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(() => {}); + }); + // This mysteriously fails on Windows on AppVeyor. See + // https://github.com/puppeteer/puppeteer/issues/4111 + xit('userDataDir option should restore cookies', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({ userDataDir }, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate( + () => + (document.cookie = + 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT') + ); + await browser.close(); + + const browser2 = await puppeteer.launch(options); + const page2 = await browser2.newPage(); + await page2.goto(server.EMPTY_PAGE); + expect(await page2.evaluate(() => document.cookie)).toBe( + 'doSomethingOnlyOnce=true' + ); + await browser2.close(); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(() => {}); + }); + it('should return the default arguments', async () => { + const { isChrome, isFirefox, puppeteer } = getTestState(); + + if (isChrome) { + expect(puppeteer.defaultArgs()).toContain('--no-first-run'); + expect(puppeteer.defaultArgs()).toContain('--headless'); + expect(puppeteer.defaultArgs({ headless: false })).not.toContain( + '--headless' + ); + expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain( + `--user-data-dir=${path.resolve('foo')}` + ); + } else if (isFirefox) { + expect(puppeteer.defaultArgs()).toContain('--headless'); + expect(puppeteer.defaultArgs()).toContain('--no-remote'); + expect(puppeteer.defaultArgs()).toContain('--foreground'); + expect(puppeteer.defaultArgs({ headless: false })).not.toContain( + '--headless' + ); + expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain( + '--profile' + ); + expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain( + 'foo' + ); + } else { + expect(puppeteer.defaultArgs()).toContain('-headless'); + expect(puppeteer.defaultArgs({ headless: false })).not.toContain( + '-headless' + ); + expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain( + '-profile' + ); + expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain( + path.resolve('foo') + ); + } + }); + it('should report the correct product', async () => { + const { isChrome, isFirefox, puppeteer } = getTestState(); + if (isChrome) expect(puppeteer.product).toBe('chrome'); + else if (isFirefox) expect(puppeteer.product).toBe('firefox'); + }); + it('should work with no default arguments', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + const options = Object.assign({}, defaultBrowserOptions); + options.ignoreDefaultArgs = true; + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + await browser.close(); + }); + it('should filter out ignored default arguments', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + // Make sure we launch with `--enable-automation` by default. + const defaultArgs = puppeteer.defaultArgs(); + const browser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + // Ignore first and third default argument. + ignoreDefaultArgs: [defaultArgs[0], defaultArgs[2]], + }) + ); + const spawnargs = browser.process().spawnargs; + if (!spawnargs) { + throw new Error('spawnargs not present'); + } + expect(spawnargs.indexOf(defaultArgs[0])).toBe(-1); + expect(spawnargs.indexOf(defaultArgs[1])).not.toBe(-1); + expect(spawnargs.indexOf(defaultArgs[2])).toBe(-1); + await browser.close(); + }); + it('should have default URL when launching browser', async function () { + const { defaultBrowserOptions, puppeteer } = getTestState(); + const browser = await puppeteer.launch(defaultBrowserOptions); + const pages = (await browser.pages()).map((page) => page.url()); + expect(pages).toEqual(['about:blank']); + await browser.close(); + }); + it( + 'should have custom URL when launching browser', + async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const options = Object.assign({}, defaultBrowserOptions); + options.args = [server.EMPTY_PAGE].concat(options.args || []); + const browser = await puppeteer.launch(options); + const pages = await browser.pages(); + expect(pages.length).toBe(1); + const page = pages[0]; + if (page.url() !== server.EMPTY_PAGE) await page.waitForNavigation(); + expect(page.url()).toBe(server.EMPTY_PAGE); + await browser.close(); + } + ); + it('should set the default viewport', async () => { + const { puppeteer, defaultBrowserOptions } = getTestState(); + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: { + width: 456, + height: 789, + }, + }); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + expect(await page.evaluate('window.innerWidth')).toBe(456); + expect(await page.evaluate('window.innerHeight')).toBe(789); + await browser.close(); + }); + it('should disable the default viewport', async () => { + const { puppeteer, defaultBrowserOptions } = getTestState(); + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: null, + }); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + expect(page.viewport()).toBe(null); + await browser.close(); + }); + it('should take fullPage screenshots when defaultViewport is null', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: null, + }); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + fullPage: true, + }); + expect(screenshot).toBeInstanceOf(Buffer); + await browser.close(); + }); + }); + + describe('Puppeteer.launch', function () { + let productName; + + before(async () => { + const { puppeteer } = getTestState(); + productName = puppeteer._productName; + }); + + after(async () => { + const { puppeteer } = getTestState(); + // @ts-expect-error launcher is a private property that users can't + // touch, but for testing purposes we need to reset it. + puppeteer._lazyLauncher = undefined; + puppeteer._productName = productName; + }); + + itOnlyRegularInstall('should be able to launch Chrome', async () => { + const { puppeteer } = getTestState(); + const browser = await puppeteer.launch({ product: 'chrome' }); + const userAgent = await browser.userAgent(); + await browser.close(); + expect(userAgent).toContain('Chrome'); + }); + + it('falls back to launching chrome if there is an unknown product but logs a warning', async () => { + const { puppeteer } = getTestState(); + const consoleStub = sinon.stub(console, 'warn'); + // @ts-expect-error purposeful bad input + const browser = await puppeteer.launch({ product: 'SO_NOT_A_PRODUCT' }); + const userAgent = await browser.userAgent(); + await browser.close(); + expect(userAgent).toContain('Chrome'); + expect(consoleStub.callCount).toEqual(1); + expect(consoleStub.firstCall.args).toEqual([ + 'Warning: unknown product name SO_NOT_A_PRODUCT. Falling back to chrome.', + ]); + }); + + /* We think there's a bug in the FF Windows launcher, or some + * combo of that plus it running on CI, but it's hard to track down. + * See comment here: https://github.com/puppeteer/puppeteer/issues/5673#issuecomment-670141377. + */ + itOnlyRegularInstall('should be able to launch Firefox', async function () { + this.timeout(FIREFOX_TIMEOUT); + const { puppeteer } = getTestState(); + const browser = await puppeteer.launch({ product: 'firefox' }); + const userAgent = await browser.userAgent(); + await browser.close(); + expect(userAgent).toContain('Firefox'); + }); + }); + + describe('Puppeteer.connect', function () { + it('should be able to connect multiple times to the same browser', async () => { + const { puppeteer, defaultBrowserOptions } = getTestState(); + + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const otherBrowser = await puppeteer.connect({ + browserWSEndpoint: originalBrowser.wsEndpoint(), + }); + const page = await otherBrowser.newPage(); + expect(await page.evaluate(() => 7 * 8)).toBe(56); + otherBrowser.disconnect(); + + const secondPage = await originalBrowser.newPage(); + expect(await secondPage.evaluate(() => 7 * 6)).toBe(42); + await originalBrowser.close(); + }); + it('should be able to close remote browser', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint: originalBrowser.wsEndpoint(), + }); + await Promise.all([ + utils.waitEvent(originalBrowser, 'disconnected'), + remoteBrowser.close(), + ]); + }); + it('should support ignoreHTTPSErrors option', async () => { + const { + httpsServer, + puppeteer, + defaultBrowserOptions, + } = getTestState(); + + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + + const browser = await puppeteer.connect({ + browserWSEndpoint, + ignoreHTTPSErrors: true, + }); + const page = await browser.newPage(); + let error = null; + const [serverRequest, response] = await Promise.all([ + httpsServer.waitForRequest('/empty.html'), + page.goto(httpsServer.EMPTY_PAGE).catch((error_) => (error = error_)), + ]); + expect(error).toBe(null); + expect(response.ok()).toBe(true); + expect(response.securityDetails()).toBeTruthy(); + const protocol = serverRequest.socket.getProtocol().replace('v', ' '); + expect(response.securityDetails().protocol()).toBe(protocol); + await page.close(); + await browser.close(); + }); + it( + 'should be able to reconnect to a disconnected browser', + async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + const page = await originalBrowser.newPage(); + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + originalBrowser.disconnect(); + + const browser = await puppeteer.connect({ browserWSEndpoint }); + const pages = await browser.pages(); + const restoredPage = pages.find( + (page) => + page.url() === server.PREFIX + '/frames/nested-frames.html' + ); + expect(utils.dumpFrames(restoredPage.mainFrame())).toEqual([ + 'http://localhost:<PORT>/frames/nested-frames.html', + ' http://localhost:<PORT>/frames/two-frames.html (2frames)', + ' http://localhost:<PORT>/frames/frame.html (uno)', + ' http://localhost:<PORT>/frames/frame.html (dos)', + ' http://localhost:<PORT>/frames/frame.html (aframe)', + ]); + expect(await restoredPage.evaluate(() => 7 * 8)).toBe(56); + await browser.close(); + } + ); + // @see https://github.com/puppeteer/puppeteer/issues/4197#issuecomment-481793410 + it( + 'should be able to connect to the same page simultaneously', + async () => { + const { puppeteer } = getTestState(); + + const browserOne = await puppeteer.launch(); + const browserTwo = await puppeteer.connect({ + browserWSEndpoint: browserOne.wsEndpoint(), + }); + const [page1, page2] = await Promise.all([ + new Promise<Page>((x) => + browserOne.once('targetcreated', (target) => x(target.page())) + ), + browserTwo.newPage(), + ]); + expect(await page1.evaluate(() => 7 * 8)).toBe(56); + expect(await page2.evaluate(() => 7 * 6)).toBe(42); + await browserOne.close(); + } + ); + }); + describe('Puppeteer.executablePath', function () { + itOnlyRegularInstall('should work', async () => { + const { puppeteer } = getTestState(); + + const executablePath = puppeteer.executablePath(); + expect(fs.existsSync(executablePath)).toBe(true); + expect(fs.realpathSync(executablePath)).toBe(executablePath); + }); + }); + }); + + describe('Browser target events', function () { + it('should work', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const browser = await puppeteer.launch(defaultBrowserOptions); + const events = []; + browser.on('targetcreated', () => events.push('CREATED')); + browser.on('targetchanged', () => events.push('CHANGED')); + browser.on('targetdestroyed', () => events.push('DESTROYED')); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.close(); + expect(events).toEqual(['CREATED', 'CHANGED', 'DESTROYED']); + await browser.close(); + }); + }); + + describe('Browser.Events.disconnected', function () { + it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async () => { + const { puppeteer, defaultBrowserOptions } = getTestState(); + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + const remoteBrowser1 = await puppeteer.connect({ browserWSEndpoint }); + const remoteBrowser2 = await puppeteer.connect({ browserWSEndpoint }); + + let disconnectedOriginal = 0; + let disconnectedRemote1 = 0; + let disconnectedRemote2 = 0; + originalBrowser.on('disconnected', () => ++disconnectedOriginal); + remoteBrowser1.on('disconnected', () => ++disconnectedRemote1); + remoteBrowser2.on('disconnected', () => ++disconnectedRemote2); + + await Promise.all([ + utils.waitEvent(remoteBrowser2, 'disconnected'), + remoteBrowser2.disconnect(), + ]); + + expect(disconnectedOriginal).toBe(0); + expect(disconnectedRemote1).toBe(0); + expect(disconnectedRemote2).toBe(1); + + await Promise.all([ + utils.waitEvent(remoteBrowser1, 'disconnected'), + utils.waitEvent(originalBrowser, 'disconnected'), + originalBrowser.close(), + ]); + + expect(disconnectedOriginal).toBe(1); + expect(disconnectedRemote1).toBe(1); + expect(disconnectedRemote2).toBe(1); + }); + }); +}); diff --git a/remote/test/puppeteer/test/mocha-ts-require.js b/remote/test/puppeteer/test/mocha-ts-require.js new file mode 100644 index 0000000000..a0ac64fa62 --- /dev/null +++ b/remote/test/puppeteer/test/mocha-ts-require.js @@ -0,0 +1,11 @@ +const path = require('path'); + +require('ts-node').register({ + /** + * We ignore the lib/ directory because that's already been TypeScript + * compiled and checked. So we don't want to check it again as part of running + * the unit tests. + */ + ignore: ['lib/*', 'node_modules'], + project: path.join(__dirname, 'tsconfig.test.json'), +}); diff --git a/remote/test/puppeteer/test/mocha-utils.ts b/remote/test/puppeteer/test/mocha-utils.ts new file mode 100644 index 0000000000..d13c82701c --- /dev/null +++ b/remote/test/puppeteer/test/mocha-utils.ts @@ -0,0 +1,279 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestServer } from '../utils/testserver/index.js'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as os from 'os'; +import sinon from 'sinon'; +import puppeteer from '../lib/cjs/puppeteer/node.js'; +import { + Browser, + BrowserContext, +} from '../lib/cjs/puppeteer/common/Browser.js'; +import { Page } from '../lib/cjs/puppeteer/common/Page.js'; +import { PuppeteerNode } from '../lib/cjs/puppeteer/node/Puppeteer.js'; +import utils from './utils.js'; +import rimraf from 'rimraf'; + +import { trackCoverage } from './coverage-utils.js'; + +const setupServer = async () => { + const assetsPath = path.join(__dirname, 'assets'); + const cachedPath = path.join(__dirname, 'assets', 'cached'); + + const port = 8907; + const server = await TestServer.create(assetsPath, port); + server.enableHTTPCache(cachedPath); + server.PORT = port; + server.PREFIX = `http://localhost:${port}`; + server.CROSS_PROCESS_PREFIX = `http://127.0.0.1:${port}`; + server.EMPTY_PAGE = `http://localhost:${port}/empty.html`; + + const httpsPort = port + 1; + const httpsServer = await TestServer.createHTTPS(assetsPath, httpsPort); + httpsServer.enableHTTPCache(cachedPath); + httpsServer.PORT = httpsPort; + httpsServer.PREFIX = `https://localhost:${httpsPort}`; + httpsServer.CROSS_PROCESS_PREFIX = `https://127.0.0.1:${httpsPort}`; + httpsServer.EMPTY_PAGE = `https://localhost:${httpsPort}/empty.html`; + + return { server, httpsServer }; +}; + +export const getTestState = (): PuppeteerTestState => + state as PuppeteerTestState; + +const product = + process.env.PRODUCT || process.env.PUPPETEER_PRODUCT || 'Chromium'; + +const alternativeInstall = process.env.PUPPETEER_ALT_INSTALL || false; + +const isHeadless = + (process.env.HEADLESS || 'true').trim().toLowerCase() === 'true'; +const isFirefox = product === 'firefox'; +const isChrome = product === 'Chromium'; + +let extraLaunchOptions = {}; +try { + extraLaunchOptions = JSON.parse(process.env.EXTRA_LAUNCH_OPTIONS || '{}'); +} catch (error) { + console.warn( + `Error parsing EXTRA_LAUNCH_OPTIONS: ${error.message}. Skipping.` + ); +} + +const defaultBrowserOptions = Object.assign( + { + handleSIGINT: true, + executablePath: process.env.BINARY, + headless: isHeadless, + dumpio: !!process.env.DUMPIO, + }, + extraLaunchOptions +); + +(async (): Promise<void> => { + if (defaultBrowserOptions.executablePath) { + console.warn( + `WARN: running ${product} tests with ${defaultBrowserOptions.executablePath}` + ); + } else { + // TODO(jackfranklin): declare updateRevision in some form for the Firefox + // launcher. + // @ts-expect-error _updateRevision is defined on the FF launcher + // but not the Chrome one. The types need tidying so that TS can infer that + // properly and not error here. + if (product === 'firefox') await puppeteer._launcher._updateRevision(); + const executablePath = puppeteer.executablePath(); + if (!fs.existsSync(executablePath)) + throw new Error( + `Browser is not downloaded at ${executablePath}. Run 'npm install' and try to re-run tests` + ); + } +})(); + +declare module 'expect/build/types' { + interface Matchers<R> { + toBeGolden(x: string): R; + } +} + +const setupGoldenAssertions = (): void => { + const suffix = product.toLowerCase(); + const GOLDEN_DIR = path.join(__dirname, 'golden-' + suffix); + const OUTPUT_DIR = path.join(__dirname, 'output-' + suffix); + if (fs.existsSync(OUTPUT_DIR)) rimraf.sync(OUTPUT_DIR); + utils.extendExpectWithToBeGolden(GOLDEN_DIR, OUTPUT_DIR); +}; + +setupGoldenAssertions(); + +interface PuppeteerTestState { + browser: Browser; + context: BrowserContext; + page: Page; + puppeteer: PuppeteerNode; + defaultBrowserOptions: { + [x: string]: any; + }; + server: any; + httpsServer: any; + isFirefox: boolean; + isChrome: boolean; + isHeadless: boolean; + puppeteerPath: string; +} +const state: Partial<PuppeteerTestState> = {}; + +export const itFailsFirefox = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (isFirefox) return xit(description, body); + else return it(description, body); +}; + +export const itChromeOnly = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (isChrome) return it(description, body); + else return xit(description, body); +}; + +export const itOnlyRegularInstall = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (alternativeInstall || process.env.BINARY) return xit(description, body); + else return it(description, body); +}; + +export const itFailsWindowsUntilDate = ( + date: Date, + description: string, + body: Mocha.Func +): Mocha.Test => { + if (os.platform() === 'win32' && Date.now() < date.getTime()) { + // we are within the deferred time so skip the test + return xit(description, body); + } + + return it(description, body); +}; + +export const itFailsWindows = (description: string, body: Mocha.Func) => { + if (os.platform() === 'win32') { + return xit(description, body); + } + return it(description, body); +}; + +export const describeFailsFirefox = ( + description: string, + body: (this: Mocha.Suite) => void +): void | Mocha.Suite => { + if (isFirefox) return xdescribe(description, body); + else return describe(description, body); +}; + +export const describeChromeOnly = ( + description: string, + body: (this: Mocha.Suite) => void +): Mocha.Suite => { + if (isChrome) return describe(description, body); +}; + +let coverageHooks = { + beforeAll: (): void => {}, + afterAll: (): void => {}, +}; + +if (process.env.COVERAGE) { + coverageHooks = trackCoverage(); +} + +console.log( + `Running unit tests with: + -> product: ${product} + -> binary: ${ + defaultBrowserOptions.executablePath || + path.relative(process.cwd(), puppeteer.executablePath()) + }` +); + +export const setupTestBrowserHooks = () => { + before(async () => { + const browser = await puppeteer.launch(defaultBrowserOptions); + state.browser = browser; + }); + + after(async () => { + await state.browser.close(); + state.browser = null; + }); +}; + +export const setupTestPageAndContextHooks = () => { + beforeEach(async () => { + state.context = await state.browser.createIncognitoBrowserContext(); + state.page = await state.context.newPage(); + }); + + afterEach(async () => { + await state.context.close(); + state.context = null; + state.page = null; + }); +}; + +export const mochaHooks = { + beforeAll: [ + async () => { + const { server, httpsServer } = await setupServer(); + + state.puppeteer = puppeteer; + state.defaultBrowserOptions = defaultBrowserOptions; + state.server = server; + state.httpsServer = httpsServer; + state.isFirefox = isFirefox; + state.isChrome = isChrome; + state.isHeadless = isHeadless; + state.puppeteerPath = path.resolve(path.join(__dirname, '..')); + }, + coverageHooks.beforeAll, + ], + + beforeEach: async () => { + state.server.reset(); + state.httpsServer.reset(); + }, + + afterAll: [ + async () => { + await state.server.stop(); + state.server = null; + await state.httpsServer.stop(); + state.httpsServer = null; + }, + coverageHooks.afterAll, + ], + + afterEach: () => { + sinon.restore(); + }, +}; diff --git a/remote/test/puppeteer/test/mouse.spec.ts b/remote/test/puppeteer/test/mouse.spec.ts new file mode 100644 index 0000000000..fadd9b9b95 --- /dev/null +++ b/remote/test/puppeteer/test/mouse.spec.ts @@ -0,0 +1,241 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import os from 'os'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions +import { KeyInput } from '../lib/cjs/puppeteer/common/USKeyboardLayout.js'; + +interface Dimensions { + x: number; + y: number; + width: number; + height: number; +} + +function dimensions(): Dimensions { + const rect = document.querySelector('textarea').getBoundingClientRect(); + return { + x: rect.left, + y: rect.top, + width: rect.width, + height: rect.height, + }; +} + +describe('Mouse', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('should click the document', async () => { + const { page } = getTestState(); + + await page.evaluate(() => { + globalThis.clickPromise = new Promise((resolve) => { + document.addEventListener('click', (event) => { + resolve({ + type: event.type, + detail: event.detail, + clientX: event.clientX, + clientY: event.clientY, + isTrusted: event.isTrusted, + button: event.button, + }); + }); + }); + }); + await page.mouse.click(50, 60); + const event = await page.evaluate<() => MouseEvent>( + () => globalThis.clickPromise + ); + expect(event.type).toBe('click'); + expect(event.detail).toBe(1); + expect(event.clientX).toBe(50); + expect(event.clientY).toBe(60); + expect(event.isTrusted).toBe(true); + expect(event.button).toBe(0); + }); + it('should resize the textarea', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + const { x, y, width, height } = await page.evaluate<() => Dimensions>( + dimensions + ); + const mouse = page.mouse; + await mouse.move(x + width - 4, y + height - 4); + await mouse.down(); + await mouse.move(x + width + 100, y + height + 100); + await mouse.up(); + const newDimensions = await page.evaluate<() => Dimensions>(dimensions); + expect(newDimensions.width).toBe(Math.round(width + 104)); + expect(newDimensions.height).toBe(Math.round(height + 104)); + }); + it('should select the text with mouse', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + const text = + "This is the text that we are going to try to select. Let's see how it goes."; + await page.keyboard.type(text); + // Firefox needs an extra frame here after typing or it will fail to set the scrollTop + await page.evaluate(() => new Promise(requestAnimationFrame)); + await page.evaluate( + () => (document.querySelector('textarea').scrollTop = 0) + ); + const { x, y } = await page.evaluate(dimensions); + await page.mouse.move(x + 2, y + 2); + await page.mouse.down(); + await page.mouse.move(100, 100); + await page.mouse.up(); + expect( + await page.evaluate(() => { + const textarea = document.querySelector('textarea'); + return textarea.value.substring( + textarea.selectionStart, + textarea.selectionEnd + ); + }) + ).toBe(text); + }); + it('should trigger hover state', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.hover('#button-6'); + expect( + await page.evaluate(() => document.querySelector('button:hover').id) + ).toBe('button-6'); + await page.hover('#button-2'); + expect( + await page.evaluate(() => document.querySelector('button:hover').id) + ).toBe('button-2'); + await page.hover('#button-91'); + expect( + await page.evaluate(() => document.querySelector('button:hover').id) + ).toBe('button-91'); + }); + it( + 'should trigger hover state with removed window.Node', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.evaluate(() => delete window.Node); + await page.hover('#button-6'); + expect( + await page.evaluate(() => document.querySelector('button:hover').id) + ).toBe('button-6'); + } + ); + it('should set modifier keys on click', async () => { + const { page, server, isFirefox } = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.evaluate(() => + document + .querySelector('#button-3') + .addEventListener('mousedown', (e) => (globalThis.lastEvent = e), true) + ); + const modifiers = new Map<KeyInput, string>([ + ['Shift', 'shiftKey'], + ['Control', 'ctrlKey'], + ['Alt', 'altKey'], + ['Meta', 'metaKey'], + ]); + // In Firefox, the Meta modifier only exists on Mac + if (isFirefox && os.platform() !== 'darwin') delete modifiers['Meta']; + for (const [modifier, key] of modifiers) { + await page.keyboard.down(modifier); + await page.click('#button-3'); + if ( + !(await page.evaluate((mod: string) => globalThis.lastEvent[mod], key)) + ) + throw new Error(key + ' should be true'); + await page.keyboard.up(modifier); + } + await page.click('#button-3'); + for (const [modifier, key] of modifiers) { + if (await page.evaluate((mod: string) => globalThis.lastEvent[mod], key)) + throw new Error(modifiers[modifier] + ' should be false'); + } + }); + it('should send mouse wheel events', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/wheel.html'); + const elem = await page.$('div'); + const boundingBoxBefore = await elem.boundingBox(); + expect(boundingBoxBefore).toMatchObject({ + width: 115, + height: 115, + }); + + await page.mouse.move( + boundingBoxBefore.x + boundingBoxBefore.width / 2, + boundingBoxBefore.y + boundingBoxBefore.height / 2 + ); + + await page.mouse.wheel({ deltaY: -100 }); + const boundingBoxAfter = await elem.boundingBox(); + expect(boundingBoxAfter).toMatchObject({ + width: 230, + height: 230, + }); + }); + it('should tween mouse movement', async () => { + const { page } = getTestState(); + + await page.mouse.move(100, 100); + await page.evaluate(() => { + globalThis.result = []; + document.addEventListener('mousemove', (event) => { + globalThis.result.push([event.clientX, event.clientY]); + }); + }); + await page.mouse.move(200, 300, { steps: 5 }); + expect(await page.evaluate('result')).toEqual([ + [120, 140], + [140, 180], + [160, 220], + [180, 260], + [200, 300], + ]); + }); + // @see https://crbug.com/929806 + it( + 'should work with mobile viewports and cross process navigations', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setViewport({ width: 360, height: 640, isMobile: true }); + await page.goto(server.CROSS_PROCESS_PREFIX + '/mobile.html'); + await page.evaluate(() => { + document.addEventListener('click', (event) => { + globalThis.result = { x: event.clientX, y: event.clientY }; + }); + }); + + await page.mouse.click(30, 40); + + expect(await page.evaluate('result')).toEqual({ x: 30, y: 40 }); + } + ); +}); diff --git a/remote/test/puppeteer/test/navigation.spec.ts b/remote/test/puppeteer/test/navigation.spec.ts new file mode 100644 index 0000000000..205d98a0b0 --- /dev/null +++ b/remote/test/puppeteer/test/navigation.spec.ts @@ -0,0 +1,774 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions +import os from 'os'; + +describe('navigation', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + describe('Page.goto', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + }); + it('should work with anchor navigation', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + await page.goto(server.EMPTY_PAGE + '#foo'); + expect(page.url()).toBe(server.EMPTY_PAGE + '#foo'); + await page.goto(server.EMPTY_PAGE + '#bar'); + expect(page.url()).toBe(server.EMPTY_PAGE + '#bar'); + }); + it('should work with redirects', async () => { + const { page, server } = getTestState(); + + server.setRedirect('/redirect/1.html', '/redirect/2.html'); + server.setRedirect('/redirect/2.html', '/empty.html'); + await page.goto(server.PREFIX + '/redirect/1.html'); + expect(page.url()).toBe(server.EMPTY_PAGE); + }); + it('should navigate to about:blank', async () => { + const { page } = getTestState(); + + const response = await page.goto('about:blank'); + expect(response).toBe(null); + }); + it('should return response when page changes its URL after load', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.PREFIX + '/historyapi.html'); + expect(response.status()).toBe(200); + }); + it('should work with subframes return 204', async () => { + const { page, server } = getTestState(); + + server.setRoute('/frames/frame.html', (req, res) => { + res.statusCode = 204; + res.end(); + }); + let error = null; + await page + .goto(server.PREFIX + '/frames/one-frame.html') + .catch((error_) => (error = error_)); + expect(error).toBe(null); + }); + it('should fail when server returns 204', async () => { + const { page, server, isChrome } = getTestState(); + + server.setRoute('/empty.html', (req, res) => { + res.statusCode = 204; + res.end(); + }); + let error = null; + await page.goto(server.EMPTY_PAGE).catch((error_) => (error = error_)); + expect(error).not.toBe(null); + if (isChrome) expect(error.message).toContain('net::ERR_ABORTED'); + else expect(error.message).toContain('NS_BINDING_ABORTED'); + }); + it('should navigate to empty page with domcontentloaded', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE, { + waitUntil: 'domcontentloaded', + }); + expect(response.status()).toBe(200); + }); + it('should work when page calls history API in beforeunload', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + window.addEventListener( + 'beforeunload', + () => history.replaceState(null, 'initial', window.location.href), + false + ); + }); + const response = await page.goto(server.PREFIX + '/grid.html'); + expect(response.status()).toBe(200); + }); + it( + 'should navigate to empty page with networkidle0', + async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE, { + waitUntil: 'networkidle0', + }); + expect(response.status()).toBe(200); + } + ); + it( + 'should navigate to empty page with networkidle2', + async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE, { + waitUntil: 'networkidle2', + }); + expect(response.status()).toBe(200); + } + ); + it('should fail when navigating to bad url', async () => { + const { page, isChrome } = getTestState(); + + let error = null; + await page.goto('asdfasdf').catch((error_) => (error = error_)); + if (isChrome) + expect(error.message).toContain('Cannot navigate to invalid URL'); + else expect(error.message).toContain('Invalid url'); + }); + + /* If you are running this on pre-Catalina versions of macOS this will fail locally. + /* Mac OSX Catalina outputs a different message than other platforms. + * See https://support.google.com/chrome/thread/18125056?hl=en for details. + * If you're running pre-Catalina Mac OSX this test will fail locally. + */ + const EXPECTED_SSL_CERT_MESSAGE = + os.platform() === 'darwin' + ? 'net::ERR_CERT_INVALID' + : 'net::ERR_CERT_AUTHORITY_INVALID'; + + it('should fail when navigating to bad SSL', async () => { + const { page, httpsServer, isChrome } = getTestState(); + + // Make sure that network events do not emit 'undefined'. + // @see https://crbug.com/750469 + const requests = []; + page.on('request', () => requests.push('request')); + page.on('requestfinished', () => requests.push('requestfinished')); + page.on('requestfailed', () => requests.push('requestfailed')); + + let error = null; + await page + .goto(httpsServer.EMPTY_PAGE) + .catch((error_) => (error = error_)); + if (isChrome) expect(error.message).toContain(EXPECTED_SSL_CERT_MESSAGE); + else expect(error.message).toContain('SSL_ERROR_UNKNOWN'); + + expect(requests.length).toBe(2); + expect(requests[0]).toBe('request'); + expect(requests[1]).toBe('requestfailed'); + }); + it('should fail when navigating to bad SSL after redirects', async () => { + const { page, server, httpsServer, isChrome } = getTestState(); + + server.setRedirect('/redirect/1.html', '/redirect/2.html'); + server.setRedirect('/redirect/2.html', '/empty.html'); + let error = null; + await page + .goto(httpsServer.PREFIX + '/redirect/1.html') + .catch((error_) => (error = error_)); + if (isChrome) expect(error.message).toContain(EXPECTED_SSL_CERT_MESSAGE); + else expect(error.message).toContain('SSL_ERROR_UNKNOWN'); + }); + it('should throw if networkidle is passed as an option', async () => { + const { page, server } = getTestState(); + + let error = null; + await page + // @ts-expect-error purposefully passing an old option + .goto(server.EMPTY_PAGE, { waitUntil: 'networkidle' }) + .catch((error_) => (error = error_)); + expect(error.message).toContain( + '"networkidle" option is no longer supported' + ); + }); + it('should fail when main resources failed to load', async () => { + const { page, isChrome } = getTestState(); + + let error = null; + await page + .goto('http://localhost:44123/non-existing-url') + .catch((error_) => (error = error_)); + if (isChrome) + expect(error.message).toContain('net::ERR_CONNECTION_REFUSED'); + else expect(error.message).toContain('NS_ERROR_CONNECTION_REFUSED'); + }); + it('should fail when exceeding maximum navigation timeout', async () => { + const { page, server, puppeteer } = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error = null; + await page + .goto(server.PREFIX + '/empty.html', { timeout: 1 }) + .catch((error_) => (error = error_)); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should fail when exceeding default maximum navigation timeout', async () => { + const { page, server, puppeteer } = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error = null; + page.setDefaultNavigationTimeout(1); + await page + .goto(server.PREFIX + '/empty.html') + .catch((error_) => (error = error_)); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should fail when exceeding default maximum timeout', async () => { + const { page, server, puppeteer } = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error = null; + page.setDefaultTimeout(1); + await page + .goto(server.PREFIX + '/empty.html') + .catch((error_) => (error = error_)); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should prioritize default navigation timeout over default timeout', async () => { + const { page, server, puppeteer } = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error = null; + page.setDefaultTimeout(0); + page.setDefaultNavigationTimeout(1); + await page + .goto(server.PREFIX + '/empty.html') + .catch((error_) => (error = error_)); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should disable timeout when its set to 0', async () => { + const { page, server } = getTestState(); + + let error = null; + let loaded = false; + page.once('load', () => (loaded = true)); + await page + .goto(server.PREFIX + '/grid.html', { timeout: 0, waitUntil: ['load'] }) + .catch((error_) => (error = error_)); + expect(error).toBe(null); + expect(loaded).toBe(true); + }); + it('should work when navigating to valid url', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok()).toBe(true); + }); + it('should work when navigating to data url', async () => { + const { page } = getTestState(); + + const response = await page.goto('data:text/html,hello'); + expect(response.ok()).toBe(true); + }); + it('should work when navigating to 404', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.PREFIX + '/not-found'); + expect(response.ok()).toBe(false); + expect(response.status()).toBe(404); + }); + it('should return last response in redirect chain', async () => { + const { page, server } = getTestState(); + + server.setRedirect('/redirect/1.html', '/redirect/2.html'); + server.setRedirect('/redirect/2.html', '/redirect/3.html'); + server.setRedirect('/redirect/3.html', server.EMPTY_PAGE); + const response = await page.goto(server.PREFIX + '/redirect/1.html'); + expect(response.ok()).toBe(true); + expect(response.url()).toBe(server.EMPTY_PAGE); + }); + it( + 'should wait for network idle to succeed navigation', + async () => { + const { page, server } = getTestState(); + + let responses = []; + // Hold on to a bunch of requests without answering. + server.setRoute('/fetch-request-a.js', (req, res) => + responses.push(res) + ); + server.setRoute('/fetch-request-b.js', (req, res) => + responses.push(res) + ); + server.setRoute('/fetch-request-c.js', (req, res) => + responses.push(res) + ); + server.setRoute('/fetch-request-d.js', (req, res) => + responses.push(res) + ); + const initialFetchResourcesRequested = Promise.all([ + server.waitForRequest('/fetch-request-a.js'), + server.waitForRequest('/fetch-request-b.js'), + server.waitForRequest('/fetch-request-c.js'), + ]); + const secondFetchResourceRequested = server.waitForRequest( + '/fetch-request-d.js' + ); + + // Navigate to a page which loads immediately and then does a bunch of + // requests via javascript's fetch method. + const navigationPromise = page.goto( + server.PREFIX + '/networkidle.html', + { + waitUntil: 'networkidle0', + } + ); + // Track when the navigation gets completed. + let navigationFinished = false; + navigationPromise.then(() => (navigationFinished = true)); + + // Wait for the page's 'load' event. + await new Promise((fulfill) => page.once('load', fulfill)); + expect(navigationFinished).toBe(false); + + // Wait for the initial three resources to be requested. + await initialFetchResourcesRequested; + + // Expect navigation still to be not finished. + expect(navigationFinished).toBe(false); + + // Respond to initial requests. + for (const response of responses) { + response.statusCode = 404; + response.end(`File not found`); + } + + // Reset responses array + responses = []; + + // Wait for the second round to be requested. + await secondFetchResourceRequested; + // Expect navigation still to be not finished. + expect(navigationFinished).toBe(false); + + // Respond to requests. + for (const response of responses) { + response.statusCode = 404; + response.end(`File not found`); + } + + const response = await navigationPromise; + // Expect navigation to succeed. + expect(response.ok()).toBe(true); + } + ); + it('should not leak listeners during navigation', async () => { + const { page, server } = getTestState(); + + let warning = null; + const warningHandler = (w) => (warning = w); + process.on('warning', warningHandler); + for (let i = 0; i < 20; ++i) await page.goto(server.EMPTY_PAGE); + process.removeListener('warning', warningHandler); + expect(warning).toBe(null); + }); + it('should not leak listeners during bad navigation', async () => { + const { page } = getTestState(); + + let warning = null; + const warningHandler = (w) => (warning = w); + process.on('warning', warningHandler); + for (let i = 0; i < 20; ++i) + await page.goto('asdf').catch(() => { + /* swallow navigation error */ + }); + process.removeListener('warning', warningHandler); + expect(warning).toBe(null); + }); + it('should not leak listeners during navigation of 11 pages', async () => { + const { context, server } = getTestState(); + + let warning = null; + const warningHandler = (w) => (warning = w); + process.on('warning', warningHandler); + await Promise.all( + [...Array(20)].map(async () => { + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.close(); + }) + ); + process.removeListener('warning', warningHandler); + expect(warning).toBe(null); + }); + it( + 'should navigate to dataURL and fire dataURL requests', + async () => { + const { page } = getTestState(); + + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + const dataURL = 'data:text/html,<div>yo</div>'; + const response = await page.goto(dataURL); + expect(response.status()).toBe(200); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(dataURL); + } + ); + it( + 'should navigate to URL with hash and fire requests without hash', + async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + const response = await page.goto(server.EMPTY_PAGE + '#hash'); + expect(response.status()).toBe(200); + expect(response.url()).toBe(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(server.EMPTY_PAGE); + } + ); + it('should work with self requesting page', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.PREFIX + '/self-request.html'); + expect(response.status()).toBe(200); + expect(response.url()).toContain('self-request.html'); + }); + it('should fail when navigating and show the url at the error message', async () => { + const { page, httpsServer } = getTestState(); + + const url = httpsServer.PREFIX + '/redirect/1.html'; + let error = null; + try { + await page.goto(url); + } catch (error_) { + error = error_; + } + expect(error.message).toContain(url); + }); + it('should send referer', async () => { + const { page, server } = getTestState(); + + const [request1, request2] = await Promise.all([ + server.waitForRequest('/grid.html'), + server.waitForRequest('/digits/1.png'), + page.goto(server.PREFIX + '/grid.html', { + referer: 'http://google.com/', + }), + ]); + expect(request1.headers['referer']).toBe('http://google.com/'); + // Make sure subresources do not inherit referer. + expect(request2.headers['referer']).toBe(server.PREFIX + '/grid.html'); + }); + }); + + describe('Page.waitForNavigation', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.evaluate( + (url: string) => (window.location.href = url), + server.PREFIX + '/grid.html' + ), + ]); + expect(response.ok()).toBe(true); + expect(response.url()).toContain('grid.html'); + }); + it('should work with both domcontentloaded and load', async () => { + const { page, server } = getTestState(); + + let response = null; + server.setRoute('/one-style.css', (req, res) => (response = res)); + const navigationPromise = page.goto(server.PREFIX + '/one-style.html'); + const domContentLoadedPromise = page.waitForNavigation({ + waitUntil: 'domcontentloaded', + }); + + let bothFired = false; + const bothFiredPromise = page + .waitForNavigation({ + waitUntil: ['load', 'domcontentloaded'], + }) + .then(() => (bothFired = true)); + + await server.waitForRequest('/one-style.css'); + await domContentLoadedPromise; + expect(bothFired).toBe(false); + response.end(); + await bothFiredPromise; + await navigationPromise; + }); + it('should work with clicking on anchor links', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(`<a href='#foobar'>foobar</a>`); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.click('a'), + ]); + expect(response).toBe(null); + expect(page.url()).toBe(server.EMPTY_PAGE + '#foobar'); + }); + it('should work with history.pushState()', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + <a onclick='javascript:pushState()'>SPA</a> + <script> + function pushState() { history.pushState({}, '', 'wow.html') } + </script> + `); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.click('a'), + ]); + expect(response).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/wow.html'); + }); + it('should work with history.replaceState()', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + <a onclick='javascript:replaceState()'>SPA</a> + <script> + function replaceState() { history.replaceState({}, '', '/replaced.html') } + </script> + `); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.click('a'), + ]); + expect(response).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/replaced.html'); + }); + it( + 'should work with DOM history.back()/history.forward()', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + <a id=back onclick='javascript:goBack()'>back</a> + <a id=forward onclick='javascript:goForward()'>forward</a> + <script> + function goBack() { history.back(); } + function goForward() { history.forward(); } + history.pushState({}, '', '/first.html'); + history.pushState({}, '', '/second.html'); + </script> + `); + expect(page.url()).toBe(server.PREFIX + '/second.html'); + const [backResponse] = await Promise.all([ + page.waitForNavigation(), + page.click('a#back'), + ]); + expect(backResponse).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/first.html'); + const [forwardResponse] = await Promise.all([ + page.waitForNavigation(), + page.click('a#forward'), + ]); + expect(forwardResponse).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/second.html'); + } + ); + it( + 'should work when subframe issues window.stop()', + async () => { + const { page, server } = getTestState(); + + server.setRoute('/frames/style.css', () => {}); + const navigationPromise = page.goto( + server.PREFIX + '/frames/one-frame.html' + ); + const frame = await utils.waitEvent(page, 'frameattached'); + await new Promise((fulfill) => { + page.on('framenavigated', (f) => { + if (f === frame) fulfill(); + }); + }); + await Promise.all([ + frame.evaluate(() => window.stop()), + navigationPromise, + ]); + } + ); + }); + + describe('Page.goBack', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.goto(server.PREFIX + '/grid.html'); + + let response = await page.goBack(); + expect(response.ok()).toBe(true); + expect(response.url()).toContain(server.EMPTY_PAGE); + + response = await page.goForward(); + expect(response.ok()).toBe(true); + expect(response.url()).toContain('/grid.html'); + + response = await page.goForward(); + expect(response).toBe(null); + }); + it('should work with HistoryAPI', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + history.pushState({}, '', '/first.html'); + history.pushState({}, '', '/second.html'); + }); + expect(page.url()).toBe(server.PREFIX + '/second.html'); + + await page.goBack(); + expect(page.url()).toBe(server.PREFIX + '/first.html'); + await page.goBack(); + expect(page.url()).toBe(server.EMPTY_PAGE); + await page.goForward(); + expect(page.url()).toBe(server.PREFIX + '/first.html'); + }); + }); + + describe('Frame.goto', function () { + it('should navigate subframes', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame.html'); + expect(page.frames()[0].url()).toContain('/frames/one-frame.html'); + expect(page.frames()[1].url()).toContain('/frames/frame.html'); + + const response = await page.frames()[1].goto(server.EMPTY_PAGE); + expect(response.ok()).toBe(true); + expect(response.frame()).toBe(page.frames()[1]); + }); + it('should reject when frame detaches', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame.html'); + + server.setRoute('/empty.html', () => {}); + const navigationPromise = page + .frames()[1] + .goto(server.EMPTY_PAGE) + .catch((error_) => error_); + await server.waitForRequest('/empty.html'); + + await page.$eval('iframe', (frame) => frame.remove()); + const error = await navigationPromise; + expect(error.message).toBe('Navigating frame was detached'); + }); + it('should return matching responses', async () => { + const { page, server } = getTestState(); + + // Disable cache: otherwise, chromium will cache similar requests. + await page.setCacheEnabled(false); + await page.goto(server.EMPTY_PAGE); + // Attach three frames. + const frames = await Promise.all([ + utils.attachFrame(page, 'frame1', server.EMPTY_PAGE), + utils.attachFrame(page, 'frame2', server.EMPTY_PAGE), + utils.attachFrame(page, 'frame3', server.EMPTY_PAGE), + ]); + // Navigate all frames to the same URL. + const serverResponses = []; + server.setRoute('/one-style.html', (req, res) => + serverResponses.push(res) + ); + const navigations = []; + for (let i = 0; i < 3; ++i) { + navigations.push(frames[i].goto(server.PREFIX + '/one-style.html')); + await server.waitForRequest('/one-style.html'); + } + // Respond from server out-of-order. + const serverResponseTexts = ['AAA', 'BBB', 'CCC']; + for (const i of [1, 2, 0]) { + serverResponses[i].end(serverResponseTexts[i]); + const response = await navigations[i]; + expect(response.frame()).toBe(frames[i]); + expect(await response.text()).toBe(serverResponseTexts[i]); + } + }); + }); + + describe('Frame.waitForNavigation', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame.html'); + const frame = page.frames()[1]; + const [response] = await Promise.all([ + frame.waitForNavigation(), + frame.evaluate( + (url: string) => (window.location.href = url), + server.PREFIX + '/grid.html' + ), + ]); + expect(response.ok()).toBe(true); + expect(response.url()).toContain('grid.html'); + expect(response.frame()).toBe(frame); + expect(page.url()).toContain('/frames/one-frame.html'); + }); + it('should fail when frame detaches', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame.html'); + const frame = page.frames()[1]; + + server.setRoute('/empty.html', () => {}); + let error = null; + const navigationPromise = frame + .waitForNavigation() + .catch((error_) => (error = error_)); + await Promise.all([ + server.waitForRequest('/empty.html'), + frame.evaluate(() => ((window as any).location = '/empty.html')), + ]); + await page.$eval('iframe', (frame) => frame.remove()); + await navigationPromise; + expect(error.message).toBe('Navigating frame was detached'); + }); + }); + + describe('Page.reload', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => (globalThis._foo = 10)); + await page.reload(); + expect(await page.evaluate(() => globalThis._foo)).toBe(undefined); + }); + }); +}); diff --git a/remote/test/puppeteer/test/network.spec.ts b/remote/test/puppeteer/test/network.spec.ts new file mode 100644 index 0000000000..63b2dfb948 --- /dev/null +++ b/remote/test/puppeteer/test/network.spec.ts @@ -0,0 +1,571 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; +import utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('network', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.Events.Request', function () { + it('should fire for navigation requests', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + await page.goto(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + }); + it('should fire for iframes', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(requests.length).toBe(2); + }); + it('should fire for fetches', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => fetch('/empty.html')); + expect(requests.length).toBe(2); + }); + }); + + describe('Request.frame', function () { + it('should work for main frame navigation request', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + await page.goto(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].frame()).toBe(page.mainFrame()); + }); + it('should work for subframe navigation request', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].frame()).toBe(page.frames()[1]); + }); + it('should work for fetch requests', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + await page.evaluate(() => fetch('/digits/1.png')); + requests = requests.filter( + (request) => !request.url().includes('favicon') + ); + expect(requests.length).toBe(1); + expect(requests[0].frame()).toBe(page.mainFrame()); + }); + }); + + describe('Request.headers', function () { + it('should work', async () => { + const { page, server, isChrome } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE); + if (isChrome) + expect(response.request().headers()['user-agent']).toContain('Chrome'); + else + expect(response.request().headers()['user-agent']).toContain('Firefox'); + }); + }); + + describe('Response.headers', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + server.setRoute('/empty.html', (req, res) => { + res.setHeader('foo', 'bar'); + res.end(); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.headers()['foo']).toBe('bar'); + }); + }); + + describe('Response.fromCache', function () { + it('should return |false| for non-cached content', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE); + expect(response.fromCache()).toBe(false); + }); + + it('should work', async () => { + const { page, server } = getTestState(); + + const responses = new Map(); + page.on( + 'response', + (r) => + !utils.isFavicon(r.request()) && + responses.set(r.url().split('/').pop(), r) + ); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + await page.reload(); + + expect(responses.size).toBe(2); + expect(responses.get('one-style.css').status()).toBe(200); + expect(responses.get('one-style.css').fromCache()).toBe(true); + expect(responses.get('one-style.html').status()).toBe(304); + expect(responses.get('one-style.html').fromCache()).toBe(false); + }); + }); + + describe('Response.fromServiceWorker', function () { + it('should return |false| for non-service-worker content', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE); + expect(response.fromServiceWorker()).toBe(false); + }); + + it('Response.fromServiceWorker', async () => { + const { page, server } = getTestState(); + + const responses = new Map(); + page.on('response', (r) => responses.set(r.url().split('/').pop(), r)); + + // Load and re-load to make sure serviceworker is installed and running. + await page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html', { + waitUntil: 'networkidle2', + }); + await page.evaluate(async () => await globalThis.activationPromise); + await page.reload(); + + expect(responses.size).toBe(2); + expect(responses.get('sw.html').status()).toBe(200); + expect(responses.get('sw.html').fromServiceWorker()).toBe(true); + expect(responses.get('style.css').status()).toBe(200); + expect(responses.get('style.css').fromServiceWorker()).toBe(true); + }); + }); + + describe('Request.postData', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + server.setRoute('/post', (req, res) => res.end()); + let request = null; + page.on('request', (r) => (request = r)); + await page.evaluate(() => + fetch('./post', { + method: 'POST', + body: JSON.stringify({ foo: 'bar' }), + }) + ); + expect(request).toBeTruthy(); + expect(request.postData()).toBe('{"foo":"bar"}'); + }); + it('should be |undefined| when there is no post data', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE); + expect(response.request().postData()).toBe(undefined); + }); + }); + + describe('Response.text', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.PREFIX + '/simple.json'); + const responseText = (await response.text()).trimEnd(); + expect(responseText).toBe('{"foo": "bar"}'); + }); + it('should return uncompressed text', async () => { + const { page, server } = getTestState(); + + server.enableGzip('/simple.json'); + const response = await page.goto(server.PREFIX + '/simple.json'); + expect(response.headers()['content-encoding']).toBe('gzip'); + const responseText = (await response.text()).trimEnd(); + expect(responseText).toBe('{"foo": "bar"}'); + }); + it('should throw when requesting body of redirected response', async () => { + const { page, server } = getTestState(); + + server.setRedirect('/foo.html', '/empty.html'); + const response = await page.goto(server.PREFIX + '/foo.html'); + const redirectChain = response.request().redirectChain(); + expect(redirectChain.length).toBe(1); + const redirected = redirectChain[0].response(); + expect(redirected.status()).toBe(302); + let error = null; + await redirected.text().catch((error_) => (error = error_)); + expect(error.message).toContain( + 'Response body is unavailable for redirect responses' + ); + }); + it('should wait until response completes', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + // Setup server to trap request. + let serverResponse = null; + server.setRoute('/get', (req, res) => { + serverResponse = res; + // In Firefox, |fetch| will be hanging until it receives |Content-Type| header + // from server. + res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + res.write('hello '); + }); + // Setup page to trap response. + let requestFinished = false; + page.on( + 'requestfinished', + (r) => (requestFinished = requestFinished || r.url().includes('/get')) + ); + // send request and wait for server response + const [pageResponse] = await Promise.all([ + page.waitForResponse((r) => !utils.isFavicon(r.request())), + page.evaluate(() => fetch('./get', { method: 'GET' })), + server.waitForRequest('/get'), + ]); + + expect(serverResponse).toBeTruthy(); + expect(pageResponse).toBeTruthy(); + expect(pageResponse.status()).toBe(200); + expect(requestFinished).toBe(false); + + const responseText = pageResponse.text(); + // Write part of the response and wait for it to be flushed. + await new Promise((x) => serverResponse.write('wor', x)); + // Finish response. + await new Promise((x) => serverResponse.end('ld!', x)); + expect(await responseText).toBe('hello world!'); + }); + }); + + describe('Response.json', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.PREFIX + '/simple.json'); + expect(await response.json()).toEqual({ foo: 'bar' }); + }); + }); + + describe('Response.buffer', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.PREFIX + '/pptr.png'); + const imageBuffer = fs.readFileSync( + path.join(__dirname, 'assets', 'pptr.png') + ); + const responseBuffer = await response.buffer(); + expect(responseBuffer.equals(imageBuffer)).toBe(true); + }); + it('should work with compression', async () => { + const { page, server } = getTestState(); + + server.enableGzip('/pptr.png'); + const response = await page.goto(server.PREFIX + '/pptr.png'); + const imageBuffer = fs.readFileSync( + path.join(__dirname, 'assets', 'pptr.png') + ); + const responseBuffer = await response.buffer(); + expect(responseBuffer.equals(imageBuffer)).toBe(true); + }); + }); + + describe('Response.statusText', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + server.setRoute('/cool', (req, res) => { + res.writeHead(200, 'cool!'); + res.end(); + }); + const response = await page.goto(server.PREFIX + '/cool'); + expect(response.statusText()).toBe('cool!'); + }); + }); + + describe('Network Events', function () { + it('Page.Events.Request', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on('request', (request) => requests.push(request)); + await page.goto(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(server.EMPTY_PAGE); + expect(requests[0].resourceType()).toBe('document'); + expect(requests[0].method()).toBe('GET'); + expect(requests[0].response()).toBeTruthy(); + expect(requests[0].frame() === page.mainFrame()).toBe(true); + expect(requests[0].frame().url()).toBe(server.EMPTY_PAGE); + }); + it('Page.Events.Response', async () => { + const { page, server } = getTestState(); + + const responses = []; + page.on('response', (response) => responses.push(response)); + await page.goto(server.EMPTY_PAGE); + expect(responses.length).toBe(1); + expect(responses[0].url()).toBe(server.EMPTY_PAGE); + expect(responses[0].status()).toBe(200); + expect(responses[0].ok()).toBe(true); + expect(responses[0].request()).toBeTruthy(); + const remoteAddress = responses[0].remoteAddress(); + // Either IPv6 or IPv4, depending on environment. + expect( + remoteAddress.ip.includes('::1') || remoteAddress.ip === '127.0.0.1' + ).toBe(true); + expect(remoteAddress.port).toBe(server.PORT); + }); + + it('Page.Events.RequestFailed', async () => { + const { page, server, isChrome } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + if (request.url().endsWith('css')) request.abort(); + else request.continue(); + }); + const failedRequests = []; + page.on('requestfailed', (request) => failedRequests.push(request)); + await page.goto(server.PREFIX + '/one-style.html'); + expect(failedRequests.length).toBe(1); + expect(failedRequests[0].url()).toContain('one-style.css'); + expect(failedRequests[0].response()).toBe(null); + expect(failedRequests[0].resourceType()).toBe('stylesheet'); + if (isChrome) + expect(failedRequests[0].failure().errorText).toBe('net::ERR_FAILED'); + else + expect(failedRequests[0].failure().errorText).toBe('NS_ERROR_FAILURE'); + expect(failedRequests[0].frame()).toBeTruthy(); + }); + it('Page.Events.RequestFinished', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on('requestfinished', (request) => requests.push(request)); + await page.goto(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(server.EMPTY_PAGE); + expect(requests[0].response()).toBeTruthy(); + expect(requests[0].frame() === page.mainFrame()).toBe(true); + expect(requests[0].frame().url()).toBe(server.EMPTY_PAGE); + }); + it('should fire events in proper order', async () => { + const { page, server } = getTestState(); + + const events = []; + page.on('request', () => events.push('request')); + page.on('response', () => events.push('response')); + page.on('requestfinished', () => events.push('requestfinished')); + await page.goto(server.EMPTY_PAGE); + expect(events).toEqual(['request', 'response', 'requestfinished']); + }); + it('should support redirects', async () => { + const { page, server } = getTestState(); + + const events = []; + page.on('request', (request) => + events.push(`${request.method()} ${request.url()}`) + ); + page.on('response', (response) => + events.push(`${response.status()} ${response.url()}`) + ); + page.on('requestfinished', (request) => + events.push(`DONE ${request.url()}`) + ); + page.on('requestfailed', (request) => + events.push(`FAIL ${request.url()}`) + ); + server.setRedirect('/foo.html', '/empty.html'); + const FOO_URL = server.PREFIX + '/foo.html'; + const response = await page.goto(FOO_URL); + expect(events).toEqual([ + `GET ${FOO_URL}`, + `302 ${FOO_URL}`, + `DONE ${FOO_URL}`, + `GET ${server.EMPTY_PAGE}`, + `200 ${server.EMPTY_PAGE}`, + `DONE ${server.EMPTY_PAGE}`, + ]); + + // Check redirect chain + const redirectChain = response.request().redirectChain(); + expect(redirectChain.length).toBe(1); + expect(redirectChain[0].url()).toContain('/foo.html'); + expect(redirectChain[0].response().remoteAddress().port).toBe( + server.PORT + ); + }); + }); + + describe('Request.isNavigationRequest', () => { + it('should work', async () => { + const { page, server } = getTestState(); + + const requests = new Map(); + page.on('request', (request) => + requests.set(request.url().split('/').pop(), request) + ); + server.setRedirect('/rrredirect', '/frames/one-frame.html'); + await page.goto(server.PREFIX + '/rrredirect'); + expect(requests.get('rrredirect').isNavigationRequest()).toBe(true); + expect(requests.get('one-frame.html').isNavigationRequest()).toBe(true); + expect(requests.get('frame.html').isNavigationRequest()).toBe(true); + expect(requests.get('script.js').isNavigationRequest()).toBe(false); + expect(requests.get('style.css').isNavigationRequest()).toBe(false); + }); + it('should work with request interception', async () => { + const { page, server } = getTestState(); + + const requests = new Map(); + page.on('request', (request) => { + requests.set(request.url().split('/').pop(), request); + request.continue(); + }); + await page.setRequestInterception(true); + server.setRedirect('/rrredirect', '/frames/one-frame.html'); + await page.goto(server.PREFIX + '/rrredirect'); + expect(requests.get('rrredirect').isNavigationRequest()).toBe(true); + expect(requests.get('one-frame.html').isNavigationRequest()).toBe(true); + expect(requests.get('frame.html').isNavigationRequest()).toBe(true); + expect(requests.get('script.js').isNavigationRequest()).toBe(false); + expect(requests.get('style.css').isNavigationRequest()).toBe(false); + }); + it('should work when navigating to image', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on('request', (request) => requests.push(request)); + await page.goto(server.PREFIX + '/pptr.png'); + expect(requests[0].isNavigationRequest()).toBe(true); + }); + }); + + describe('Page.setExtraHTTPHeaders', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setExtraHTTPHeaders({ + foo: 'bar', + }); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE), + ]); + expect(request.headers['foo']).toBe('bar'); + }); + it('should throw for non-string header values', async () => { + const { page } = getTestState(); + + let error = null; + try { + // @ts-expect-error purposeful bad input + await page.setExtraHTTPHeaders({ foo: 1 }); + } catch (error_) { + error = error_; + } + expect(error.message).toBe( + 'Expected value of header "foo" to be String, but "number" is found.' + ); + }); + }); + + describe('Page.authenticate', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + server.setAuth('/empty.html', 'user', 'pass'); + let response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(401); + await page.authenticate({ + username: 'user', + password: 'pass', + }); + response = await page.reload(); + expect(response.status()).toBe(200); + }); + it('should fail if wrong credentials', async () => { + const { page, server } = getTestState(); + + // Use unique user/password since Chrome caches credentials per origin. + server.setAuth('/empty.html', 'user2', 'pass2'); + await page.authenticate({ + username: 'foo', + password: 'bar', + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(401); + }); + it('should allow disable authentication', async () => { + const { page, server } = getTestState(); + + // Use unique user/password since Chrome caches credentials per origin. + server.setAuth('/empty.html', 'user3', 'pass3'); + await page.authenticate({ + username: 'user3', + password: 'pass3', + }); + let response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(200); + await page.authenticate(null); + // Navigate to a different origin to bust Chrome's credential caching. + response = await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(response.status()).toBe(401); + }); + }); +}); diff --git a/remote/test/puppeteer/test/oopif.spec.ts b/remote/test/puppeteer/test/oopif.spec.ts new file mode 100644 index 0000000000..845429a69f --- /dev/null +++ b/remote/test/puppeteer/test/oopif.spec.ts @@ -0,0 +1,74 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import { getTestState, describeChromeOnly } from './mocha-utils'; // eslint-disable-line import/extensions + +describeChromeOnly('OOPIF', function () { + /* We use a special browser for this test as we need the --site-per-process flag */ + let browser; + let context; + let page; + + before(async () => { + const { puppeteer, defaultBrowserOptions } = getTestState(); + browser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + args: (defaultBrowserOptions.args || []).concat(['--site-per-process']), + }) + ); + }); + + beforeEach(async () => { + context = await browser.createIncognitoBrowserContext(); + page = await context.newPage(); + }); + + afterEach(async () => { + await context.close(); + page = null; + context = null; + }); + + after(async () => { + await browser.close(); + browser = null; + }); + xit('should report oopif frames', async () => { + const { server } = getTestState(); + + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + expect(oopifs(context).length).toBe(1); + expect(page.frames().length).toBe(2); + }); + it('should load oopif iframes with subresources and request interception', async () => { + const { server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + expect(oopifs(context).length).toBe(1); + }); +}); + +/** + * @param {!BrowserContext} context + */ +function oopifs(context) { + return context + .targets() + .filter((target) => target._targetInfo.type === 'iframe'); +} diff --git a/remote/test/puppeteer/test/page.spec.ts b/remote/test/puppeteer/test/page.spec.ts new file mode 100644 index 0000000000..512c26921e --- /dev/null +++ b/remote/test/puppeteer/test/page.spec.ts @@ -0,0 +1,1720 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import fs from 'fs'; +import path from 'path'; +import utils from './utils.js'; +const { waitEvent } = utils; +import expect from 'expect'; +import sinon from 'sinon'; +import { + getTestState, + itFailsFirefox, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions +import { Page, Metrics } from '../lib/cjs/puppeteer/common/Page.js'; +import { JSHandle } from '../lib/cjs/puppeteer/common/JSHandle.js'; + +describe('Page', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + describe('Page.close', function () { + it('should reject all promises when page is closed', async () => { + const { context } = getTestState(); + + const newPage = await context.newPage(); + let error = null; + await Promise.all([ + newPage + .evaluate(() => new Promise(() => {})) + .catch((error_) => (error = error_)), + newPage.close(), + ]); + expect(error.message).toContain('Protocol error'); + }); + it('should not be visible in browser.pages', async () => { + const { browser } = getTestState(); + + const newPage = await browser.newPage(); + expect(await browser.pages()).toContain(newPage); + await newPage.close(); + expect(await browser.pages()).not.toContain(newPage); + }); + it('should run beforeunload if asked for', async () => { + const { context, server, isChrome } = getTestState(); + + const newPage = await context.newPage(); + await newPage.goto(server.PREFIX + '/beforeunload.html'); + // We have to interact with a page so that 'beforeunload' handlers + // fire. + await newPage.click('body'); + const pageClosingPromise = newPage.close({ runBeforeUnload: true }); + const dialog = await waitEvent(newPage, 'dialog'); + expect(dialog.type()).toBe('beforeunload'); + expect(dialog.defaultValue()).toBe(''); + if (isChrome) expect(dialog.message()).toBe(''); + else + expect(dialog.message()).toBe( + 'This page is asking you to confirm that you want to leave - data you have entered may not be saved.' + ); + await dialog.accept(); + await pageClosingPromise; + }); + it('should *not* run beforeunload by default', async () => { + const { context, server } = getTestState(); + + const newPage = await context.newPage(); + await newPage.goto(server.PREFIX + '/beforeunload.html'); + // We have to interact with a page so that 'beforeunload' handlers + // fire. + await newPage.click('body'); + await newPage.close(); + }); + it('should set the page close state', async () => { + const { context } = getTestState(); + + const newPage = await context.newPage(); + expect(newPage.isClosed()).toBe(false); + await newPage.close(); + expect(newPage.isClosed()).toBe(true); + }); + it('should terminate network waiters', async () => { + const { context, server } = getTestState(); + + const newPage = await context.newPage(); + const results = await Promise.all([ + newPage.waitForRequest(server.EMPTY_PAGE).catch((error) => error), + newPage.waitForResponse(server.EMPTY_PAGE).catch((error) => error), + newPage.close(), + ]); + for (let i = 0; i < 2; i++) { + const message = results[i].message; + expect(message).toContain('Target closed'); + expect(message).not.toContain('Timeout'); + } + }); + }); + + describe('Page.Events.Load', function () { + it('should fire when expected', async () => { + const { page } = getTestState(); + + await Promise.all([ + page.goto('about:blank'), + utils.waitEvent(page, 'load'), + ]); + }); + }); + + // This test fails on Firefox on CI consistently but cannot be replicated + // locally. Skipping for now to unblock the Mitt release and given FF support + // isn't fully done yet but raising an issue to ask the FF folks to have a + // look at this. + describe('removing and adding event handlers', () => { + it('should correctly fire event handlers as they are added and then removed', async () => { + const { page, server } = getTestState(); + + const handler = sinon.spy(); + page.on('response', handler); + await page.goto(server.EMPTY_PAGE); + expect(handler.callCount).toBe(1); + page.off('response', handler); + await page.goto(server.EMPTY_PAGE); + // Still one because we removed the handler. + expect(handler.callCount).toBe(1); + page.on('response', handler); + await page.goto(server.EMPTY_PAGE); + // Two now because we added the handler back. + expect(handler.callCount).toBe(2); + }); + }); + + describe('Page.Events.error', function () { + it('should throw when page crashes', async () => { + const { page } = getTestState(); + + let error = null; + page.on('error', (err) => (error = err)); + page.goto('chrome://crash').catch(() => {}); + await waitEvent(page, 'error'); + expect(error.message).toBe('Page crashed!'); + }); + }); + + describe('Page.Events.Popup', function () { + it('should work', async () => { + const { page } = getTestState(); + + const [popup] = await Promise.all([ + new Promise<Page>((x) => page.once('popup', x)), + page.evaluate(() => window.open('about:blank')), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(true); + }); + it('should work with noopener', async () => { + const { page } = getTestState(); + + const [popup] = await Promise.all([ + new Promise<Page>((x) => page.once('popup', x)), + page.evaluate(() => window.open('about:blank', null, 'noopener')), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(false); + }); + it('should work with clicking target=_blank', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent('<a target=_blank href="/one-style.html">yo</a>'); + const [popup] = await Promise.all([ + new Promise<Page>((x) => page.once('popup', x)), + page.click('a'), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(true); + }); + it('should work with fake-clicking target=_blank and rel=noopener', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent( + '<a target=_blank rel=noopener href="/one-style.html">yo</a>' + ); + const [popup] = await Promise.all([ + new Promise<Page>((x) => page.once('popup', x)), + page.$eval('a', (a: HTMLAnchorElement) => a.click()), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(false); + }); + it('should work with clicking target=_blank and rel=noopener', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent( + '<a target=_blank rel=noopener href="/one-style.html">yo</a>' + ); + const [popup] = await Promise.all([ + new Promise<Page>((x) => page.once('popup', x)), + page.click('a'), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(false); + }); + }); + + describe('BrowserContext.overridePermissions', function () { + function getPermission(page, name) { + return page.evaluate( + (name) => + navigator.permissions.query({ name }).then((result) => result.state), + name + ); + } + + it('should be prompt by default', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + expect(await getPermission(page, 'geolocation')).toBe('prompt'); + }); + it('should deny permission when not listed', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await context.overridePermissions(server.EMPTY_PAGE, []); + expect(await getPermission(page, 'geolocation')).toBe('denied'); + }); + it('should fail when bad permission is given', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let error = null; + await context + .overridePermissions(server.EMPTY_PAGE, ['foo']) + .catch((error_) => (error = error_)); + expect(error.message).toBe('Unknown permission: foo'); + }); + it('should grant permission when listed', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']); + expect(await getPermission(page, 'geolocation')).toBe('granted'); + }); + it('should reset permissions', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']); + expect(await getPermission(page, 'geolocation')).toBe('granted'); + await context.clearPermissionOverrides(); + expect(await getPermission(page, 'geolocation')).toBe('prompt'); + }); + it('should trigger permission onchange', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + globalThis.events = []; + return navigator.permissions + .query({ name: 'geolocation' }) + .then(function (result) { + globalThis.events.push(result.state); + result.onchange = function () { + globalThis.events.push(result.state); + }; + }); + }); + expect(await page.evaluate(() => globalThis.events)).toEqual(['prompt']); + await context.overridePermissions(server.EMPTY_PAGE, []); + expect(await page.evaluate(() => globalThis.events)).toEqual([ + 'prompt', + 'denied', + ]); + await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']); + expect(await page.evaluate(() => globalThis.events)).toEqual([ + 'prompt', + 'denied', + 'granted', + ]); + await context.clearPermissionOverrides(); + expect(await page.evaluate(() => globalThis.events)).toEqual([ + 'prompt', + 'denied', + 'granted', + 'prompt', + ]); + }); + it( + 'should isolate permissions between browser contexs', + async () => { + const { page, server, context, browser } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const otherContext = await browser.createIncognitoBrowserContext(); + const otherPage = await otherContext.newPage(); + await otherPage.goto(server.EMPTY_PAGE); + expect(await getPermission(page, 'geolocation')).toBe('prompt'); + expect(await getPermission(otherPage, 'geolocation')).toBe('prompt'); + + await context.overridePermissions(server.EMPTY_PAGE, []); + await otherContext.overridePermissions(server.EMPTY_PAGE, [ + 'geolocation', + ]); + expect(await getPermission(page, 'geolocation')).toBe('denied'); + expect(await getPermission(otherPage, 'geolocation')).toBe('granted'); + + await context.clearPermissionOverrides(); + expect(await getPermission(page, 'geolocation')).toBe('prompt'); + expect(await getPermission(otherPage, 'geolocation')).toBe('granted'); + + await otherContext.close(); + } + ); + }); + + describe('Page.setGeolocation', function () { + it('should work', async () => { + const { page, server, context } = getTestState(); + + await context.overridePermissions(server.PREFIX, ['geolocation']); + await page.goto(server.EMPTY_PAGE); + await page.setGeolocation({ longitude: 10, latitude: 10 }); + const geolocation = await page.evaluate( + () => + new Promise((resolve) => + navigator.geolocation.getCurrentPosition((position) => { + resolve({ + latitude: position.coords.latitude, + longitude: position.coords.longitude, + }); + }) + ) + ); + expect(geolocation).toEqual({ + latitude: 10, + longitude: 10, + }); + }); + it('should throw when invalid longitude', async () => { + const { page } = getTestState(); + + let error = null; + try { + await page.setGeolocation({ longitude: 200, latitude: 10 }); + } catch (error_) { + error = error_; + } + expect(error.message).toContain('Invalid longitude "200"'); + }); + }); + + describe('Page.setOfflineMode', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setOfflineMode(true); + let error = null; + await page.goto(server.EMPTY_PAGE).catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + await page.setOfflineMode(false); + const response = await page.reload(); + expect(response.status()).toBe(200); + }); + it('should emulate navigator.onLine', async () => { + const { page } = getTestState(); + + expect(await page.evaluate(() => window.navigator.onLine)).toBe(true); + await page.setOfflineMode(true); + expect(await page.evaluate(() => window.navigator.onLine)).toBe(false); + await page.setOfflineMode(false); + expect(await page.evaluate(() => window.navigator.onLine)).toBe(true); + }); + }); + + describe('ExecutionContext.queryObjects', function () { + it('should work', async () => { + const { page } = getTestState(); + + // Instantiate an object + await page.evaluate(() => (globalThis.set = new Set(['hello', 'world']))); + const prototypeHandle = await page.evaluateHandle(() => Set.prototype); + const objectsHandle = await page.queryObjects(prototypeHandle); + const count = await page.evaluate( + (objects: JSHandle[]) => objects.length, + objectsHandle + ); + expect(count).toBe(1); + const values = await page.evaluate( + (objects) => Array.from(objects[0].values()), + objectsHandle + ); + expect(values).toEqual(['hello', 'world']); + }); + it('should work for non-blank page', async () => { + const { page, server } = getTestState(); + + // Instantiate an object + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => (globalThis.set = new Set(['hello', 'world']))); + const prototypeHandle = await page.evaluateHandle(() => Set.prototype); + const objectsHandle = await page.queryObjects(prototypeHandle); + const count = await page.evaluate( + (objects: JSHandle[]) => objects.length, + objectsHandle + ); + expect(count).toBe(1); + }); + it('should fail for disposed handles', async () => { + const { page } = getTestState(); + + const prototypeHandle = await page.evaluateHandle( + () => HTMLBodyElement.prototype + ); + await prototypeHandle.dispose(); + let error = null; + await page + .queryObjects(prototypeHandle) + .catch((error_) => (error = error_)); + expect(error.message).toBe('Prototype JSHandle is disposed!'); + }); + it('should fail primitive values as prototypes', async () => { + const { page } = getTestState(); + + const prototypeHandle = await page.evaluateHandle(() => 42); + let error = null; + await page + .queryObjects(prototypeHandle) + .catch((error_) => (error = error_)); + expect(error.message).toBe( + 'Prototype JSHandle must not be referencing primitive value' + ); + }); + }); + + describe('Page.Events.Console', function () { + it('should work', async () => { + const { page } = getTestState(); + + let message = null; + page.once('console', (m) => (message = m)); + await Promise.all([ + page.evaluate(() => console.log('hello', 5, { foo: 'bar' })), + waitEvent(page, 'console'), + ]); + expect(message.text()).toEqual('hello 5 JSHandle@object'); + expect(message.type()).toEqual('log'); + expect(message.args()).toHaveLength(3); + expect(message.location()).toEqual({ + url: expect.any(String), + lineNumber: expect.any(Number), + columnNumber: expect.any(Number), + }); + + expect(await message.args()[0].jsonValue()).toEqual('hello'); + expect(await message.args()[1].jsonValue()).toEqual(5); + expect(await message.args()[2].jsonValue()).toEqual({ foo: 'bar' }); + }); + it('should work for different console API calls', async () => { + const { page } = getTestState(); + + const messages = []; + page.on('console', (msg) => messages.push(msg)); + // All console events will be reported before `page.evaluate` is finished. + await page.evaluate(() => { + // A pair of time/timeEnd generates only one Console API call. + console.time('calling console.time'); + console.timeEnd('calling console.time'); + console.trace('calling console.trace'); + console.dir('calling console.dir'); + console.warn('calling console.warn'); + console.error('calling console.error'); + console.log(Promise.resolve('should not wait until resolved!')); + }); + expect(messages.map((msg) => msg.type())).toEqual([ + 'timeEnd', + 'trace', + 'dir', + 'warning', + 'error', + 'log', + ]); + expect(messages[0].text()).toContain('calling console.time'); + expect(messages.slice(1).map((msg) => msg.text())).toEqual([ + 'calling console.trace', + 'calling console.dir', + 'calling console.warn', + 'calling console.error', + 'JSHandle@promise', + ]); + }); + it('should not fail for window object', async () => { + const { page } = getTestState(); + + let message = null; + page.once('console', (msg) => (message = msg)); + await Promise.all([ + page.evaluate(() => console.error(window)), + waitEvent(page, 'console'), + ]); + expect(message.text()).toBe('JSHandle@object'); + }); + it('should trigger correct Log', async () => { + const { page, server, isChrome } = getTestState(); + + await page.goto('about:blank'); + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.evaluate( + async (url: string) => fetch(url).catch(() => {}), + server.EMPTY_PAGE + ), + ]); + expect(message.text()).toContain('Access-Control-Allow-Origin'); + if (isChrome) expect(message.type()).toEqual('error'); + else expect(message.type()).toEqual('warn'); + }); + it('should have location when fetch fails', async () => { + const { page, server } = getTestState(); + + // The point of this test is to make sure that we report console messages from + // Log domain: https://vanilla.aslushnikov.com/?Log.entryAdded + await page.goto(server.EMPTY_PAGE); + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.setContent(`<script>fetch('http://wat');</script>`), + ]); + expect(message.text()).toContain(`ERR_NAME_NOT_RESOLVED`); + expect(message.type()).toEqual('error'); + expect(message.location()).toEqual({ + url: 'http://wat/', + lineNumber: undefined, + }); + }); + it('should have location and stack trace for console API calls', async () => { + const { page, server, isChrome } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.goto(server.PREFIX + '/consolelog.html'), + ]); + expect(message.text()).toBe('yellow'); + expect(message.type()).toBe('log'); + expect(message.location()).toEqual({ + url: server.PREFIX + '/consolelog.html', + lineNumber: 8, + columnNumber: isChrome ? 16 : 8, // console.|log vs |console.log + }); + expect(message.stackTrace()).toEqual([ + { + url: server.PREFIX + '/consolelog.html', + lineNumber: 8, + columnNumber: isChrome ? 16 : 8, // console.|log vs |console.log + }, + { + url: server.PREFIX + '/consolelog.html', + lineNumber: 11, + columnNumber: 8, + }, + { + url: server.PREFIX + '/consolelog.html', + lineNumber: 13, + columnNumber: 6, + }, + ]); + }); + // @see https://github.com/puppeteer/puppeteer/issues/3865 + it('should not throw when there are console messages in detached iframes', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(async () => { + // 1. Create a popup that Puppeteer is not connected to. + const win = window.open( + window.location.href, + 'Title', + 'toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=780,height=200,top=0,left=0' + ); + await new Promise((x) => (win.onload = x)); + // 2. In this popup, create an iframe that console.logs a message. + win.document.body.innerHTML = `<iframe src='/consolelog.html'></iframe>`; + const frame = win.document.querySelector('iframe'); + await new Promise((x) => (frame.onload = x)); + // 3. After that, remove the iframe. + frame.remove(); + }); + const popupTarget = page + .browserContext() + .targets() + .find((target) => target !== page.target()); + // 4. Connect to the popup and make sure it doesn't throw. + await popupTarget.page(); + }); + }); + + describe('Page.Events.DOMContentLoaded', function () { + it('should fire when expected', async () => { + const { page } = getTestState(); + + page.goto('about:blank'); + await waitEvent(page, 'domcontentloaded'); + }); + }); + + describe('Page.metrics', function () { + it('should get metrics from a page', async () => { + const { page } = getTestState(); + + await page.goto('about:blank'); + const metrics = await page.metrics(); + checkMetrics(metrics); + }); + it('metrics event fired on console.timeStamp', async () => { + const { page } = getTestState(); + + const metricsPromise = new Promise<{ metrics: Metrics; title: string }>( + (fulfill) => page.once('metrics', fulfill) + ); + await page.evaluate(() => console.timeStamp('test42')); + const metrics = await metricsPromise; + expect(metrics.title).toBe('test42'); + checkMetrics(metrics.metrics); + }); + function checkMetrics(metrics) { + const metricsToCheck = new Set([ + 'Timestamp', + 'Documents', + 'Frames', + 'JSEventListeners', + 'Nodes', + 'LayoutCount', + 'RecalcStyleCount', + 'LayoutDuration', + 'RecalcStyleDuration', + 'ScriptDuration', + 'TaskDuration', + 'JSHeapUsedSize', + 'JSHeapTotalSize', + ]); + for (const name in metrics) { + expect(metricsToCheck.has(name)).toBeTruthy(); + expect(metrics[name]).toBeGreaterThanOrEqual(0); + metricsToCheck.delete(name); + } + expect(metricsToCheck.size).toBe(0); + } + }); + + describe('Page.waitForRequest', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + page.waitForRequest(server.PREFIX + '/digits/2.png'), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }), + ]); + expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should work with predicate', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + page.waitForRequest( + (request) => request.url() === server.PREFIX + '/digits/2.png' + ), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }), + ]); + expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + await page + .waitForRequest(() => false, { timeout: 1 }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + page.setDefaultTimeout(1); + await page + .waitForRequest(() => false) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should work with no timeout', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + page.waitForRequest(server.PREFIX + '/digits/2.png', { timeout: 0 }), + page.evaluate(() => + setTimeout(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }, 50) + ), + ]); + expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + }); + + describe('Page.waitForResponse', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForResponse(server.PREFIX + '/digits/2.png'), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }), + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + await page + .waitForResponse(() => false, { timeout: 1 }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + page.setDefaultTimeout(1); + await page + .waitForResponse(() => false) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should work with predicate', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForResponse( + (response) => response.url() === server.PREFIX + '/digits/2.png' + ), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }), + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should work with no timeout', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForResponse(server.PREFIX + '/digits/2.png', { timeout: 0 }), + page.evaluate(() => + setTimeout(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }, 50) + ), + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + }); + + describe('Page.exposeFunction', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.exposeFunction('compute', function (a, b) { + return a * b; + }); + const result = await page.evaluate(async function () { + return await globalThis.compute(9, 4); + }); + expect(result).toBe(36); + }); + it('should throw exception in page context', async () => { + const { page } = getTestState(); + + await page.exposeFunction('woof', function () { + throw new Error('WOOF WOOF'); + }); + const { message, stack } = await page.evaluate(async () => { + try { + await globalThis.woof(); + } catch (error) { + return { message: error.message, stack: error.stack }; + } + }); + expect(message).toBe('WOOF WOOF'); + expect(stack).toContain(__filename); + }); + it('should support throwing "null"', async () => { + const { page } = getTestState(); + + await page.exposeFunction('woof', function () { + throw null; + }); + const thrown = await page.evaluate(async () => { + try { + await globalThis.woof(); + } catch (error) { + return error; + } + }); + expect(thrown).toBe(null); + }); + it('should be callable from-inside evaluateOnNewDocument', async () => { + const { page } = getTestState(); + + let called = false; + await page.exposeFunction('woof', function () { + called = true; + }); + await page.evaluateOnNewDocument(() => globalThis.woof()); + await page.reload(); + expect(called).toBe(true); + }); + it('should survive navigation', async () => { + const { page, server } = getTestState(); + + await page.exposeFunction('compute', function (a, b) { + return a * b; + }); + + await page.goto(server.EMPTY_PAGE); + const result = await page.evaluate(async function () { + return await globalThis.compute(9, 4); + }); + expect(result).toBe(36); + }); + it('should await returned promise', async () => { + const { page } = getTestState(); + + await page.exposeFunction('compute', function (a, b) { + return Promise.resolve(a * b); + }); + + const result = await page.evaluate(async function () { + return await globalThis.compute(3, 5); + }); + expect(result).toBe(15); + }); + it('should work on frames', async () => { + const { page, server } = getTestState(); + + await page.exposeFunction('compute', function (a, b) { + return Promise.resolve(a * b); + }); + + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + const frame = page.frames()[1]; + const result = await frame.evaluate(async function () { + return await globalThis.compute(3, 5); + }); + expect(result).toBe(15); + }); + it('should work on frames before navigation', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + await page.exposeFunction('compute', function (a, b) { + return Promise.resolve(a * b); + }); + + const frame = page.frames()[1]; + const result = await frame.evaluate(async function () { + return await globalThis.compute(3, 5); + }); + expect(result).toBe(15); + }); + it('should work with complex objects', async () => { + const { page } = getTestState(); + + await page.exposeFunction('complexObject', function (a, b) { + return { x: a.x + b.x }; + }); + const result = await page.evaluate<() => Promise<{ x: number }>>( + async () => globalThis.complexObject({ x: 5 }, { x: 2 }) + ); + expect(result.x).toBe(7); + }); + }); + + describe('Page.Events.PageError', function () { + it('should fire', async () => { + const { page, server } = getTestState(); + + let error = null; + page.once('pageerror', (e) => (error = e)); + await Promise.all([ + page.goto(server.PREFIX + '/error.html'), + waitEvent(page, 'pageerror'), + ]); + expect(error.message).toContain('Fancy'); + }); + }); + + describe('Page.setUserAgent', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + expect(await page.evaluate(() => navigator.userAgent)).toContain( + 'Mozilla' + ); + await page.setUserAgent('foobar'); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE), + ]); + expect(request.headers['user-agent']).toBe('foobar'); + }); + it('should work for subframes', async () => { + const { page, server } = getTestState(); + + expect(await page.evaluate(() => navigator.userAgent)).toContain( + 'Mozilla' + ); + await page.setUserAgent('foobar'); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + utils.attachFrame(page, 'frame1', server.EMPTY_PAGE), + ]); + expect(request.headers['user-agent']).toBe('foobar'); + }); + it('should emulate device user-agent', async () => { + const { page, server, puppeteer } = getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + expect(await page.evaluate(() => navigator.userAgent)).not.toContain( + 'iPhone' + ); + await page.setUserAgent(puppeteer.devices['iPhone 6'].userAgent); + expect(await page.evaluate(() => navigator.userAgent)).toContain( + 'iPhone' + ); + }); + }); + + describe('Page.setContent', function () { + const expectedOutput = + '<html><head></head><body><div>hello</div></body></html>'; + it('should work', async () => { + const { page } = getTestState(); + + await page.setContent('<div>hello</div>'); + const result = await page.content(); + expect(result).toBe(expectedOutput); + }); + it('should work with doctype', async () => { + const { page } = getTestState(); + + const doctype = '<!DOCTYPE html>'; + await page.setContent(`${doctype}<div>hello</div>`); + const result = await page.content(); + expect(result).toBe(`${doctype}${expectedOutput}`); + }); + it('should work with HTML 4 doctype', async () => { + const { page } = getTestState(); + + const doctype = + '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" ' + + '"http://www.w3.org/TR/html4/strict.dtd">'; + await page.setContent(`${doctype}<div>hello</div>`); + const result = await page.content(); + expect(result).toBe(`${doctype}${expectedOutput}`); + }); + it('should respect timeout', async () => { + const { page, server, puppeteer } = getTestState(); + + const imgPath = '/img.png'; + // stall for image + server.setRoute(imgPath, () => {}); + let error = null; + await page + .setContent(`<img src="${server.PREFIX + imgPath}"></img>`, { + timeout: 1, + }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default navigation timeout', async () => { + const { page, server, puppeteer } = getTestState(); + + page.setDefaultNavigationTimeout(1); + const imgPath = '/img.png'; + // stall for image + server.setRoute(imgPath, () => {}); + let error = null; + await page + .setContent(`<img src="${server.PREFIX + imgPath}"></img>`) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should await resources to load', async () => { + const { page, server } = getTestState(); + + const imgPath = '/img.png'; + let imgResponse = null; + server.setRoute(imgPath, (req, res) => (imgResponse = res)); + let loaded = false; + const contentPromise = page + .setContent(`<img src="${server.PREFIX + imgPath}"></img>`) + .then(() => (loaded = true)); + await server.waitForRequest(imgPath); + expect(loaded).toBe(false); + imgResponse.end(); + await contentPromise; + }); + it('should work fast enough', async () => { + const { page } = getTestState(); + + for (let i = 0; i < 20; ++i) await page.setContent('<div>yo</div>'); + }); + it('should work with tricky content', async () => { + const { page } = getTestState(); + + await page.setContent('<div>hello world</div>' + '\x7F'); + expect(await page.$eval('div', (div) => div.textContent)).toBe( + 'hello world' + ); + }); + it('should work with accents', async () => { + const { page } = getTestState(); + + await page.setContent('<div>aberración</div>'); + expect(await page.$eval('div', (div) => div.textContent)).toBe( + 'aberración' + ); + }); + it('should work with emojis', async () => { + const { page } = getTestState(); + + await page.setContent('<div>🐥</div>'); + expect(await page.$eval('div', (div) => div.textContent)).toBe('🐥'); + }); + it('should work with newline', async () => { + const { page } = getTestState(); + + await page.setContent('<div>\n</div>'); + expect(await page.$eval('div', (div) => div.textContent)).toBe('\n'); + }); + }); + + describe('Page.setBypassCSP', function () { + it('should bypass CSP meta tag', async () => { + const { page, server } = getTestState(); + + // Make sure CSP prohibits addScriptTag. + await page.goto(server.PREFIX + '/csp.html'); + await page + .addScriptTag({ content: 'window.__injected = 42;' }) + .catch((error) => void error); + expect(await page.evaluate(() => globalThis.__injected)).toBe(undefined); + + // By-pass CSP and try one more time. + await page.setBypassCSP(true); + await page.reload(); + await page.addScriptTag({ content: 'window.__injected = 42;' }); + expect(await page.evaluate(() => globalThis.__injected)).toBe(42); + }); + + it('should bypass CSP header', async () => { + const { page, server } = getTestState(); + + // Make sure CSP prohibits addScriptTag. + server.setCSP('/empty.html', 'default-src "self"'); + await page.goto(server.EMPTY_PAGE); + await page + .addScriptTag({ content: 'window.__injected = 42;' }) + .catch((error) => void error); + expect(await page.evaluate(() => globalThis.__injected)).toBe(undefined); + + // By-pass CSP and try one more time. + await page.setBypassCSP(true); + await page.reload(); + await page.addScriptTag({ content: 'window.__injected = 42;' }); + expect(await page.evaluate(() => globalThis.__injected)).toBe(42); + }); + + it('should bypass after cross-process navigation', async () => { + const { page, server } = getTestState(); + + await page.setBypassCSP(true); + await page.goto(server.PREFIX + '/csp.html'); + await page.addScriptTag({ content: 'window.__injected = 42;' }); + expect(await page.evaluate(() => globalThis.__injected)).toBe(42); + + await page.goto(server.CROSS_PROCESS_PREFIX + '/csp.html'); + await page.addScriptTag({ content: 'window.__injected = 42;' }); + expect(await page.evaluate(() => globalThis.__injected)).toBe(42); + }); + it('should bypass CSP in iframes as well', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + { + // Make sure CSP prohibits addScriptTag in an iframe. + const frame = await utils.attachFrame( + page, + 'frame1', + server.PREFIX + '/csp.html' + ); + await frame + .addScriptTag({ content: 'window.__injected = 42;' }) + .catch((error) => void error); + expect(await frame.evaluate(() => globalThis.__injected)).toBe( + undefined + ); + } + + // By-pass CSP and try one more time. + await page.setBypassCSP(true); + await page.reload(); + + { + const frame = await utils.attachFrame( + page, + 'frame1', + server.PREFIX + '/csp.html' + ); + await frame + .addScriptTag({ content: 'window.__injected = 42;' }) + .catch((error) => void error); + expect(await frame.evaluate(() => globalThis.__injected)).toBe(42); + } + }); + }); + + describe('Page.addScriptTag', function () { + it('should throw an error if no options are provided', async () => { + const { page } = getTestState(); + + let error = null; + try { + // @ts-expect-error purposefully passing bad options + await page.addScriptTag('/injectedfile.js'); + } catch (error_) { + error = error_; + } + expect(error.message).toBe( + 'Provide an object with a `url`, `path` or `content` property' + ); + }); + + it('should work with a url', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const scriptHandle = await page.addScriptTag({ url: '/injectedfile.js' }); + expect(scriptHandle.asElement()).not.toBeNull(); + expect(await page.evaluate(() => globalThis.__injected)).toBe(42); + }); + + it('should work with a url and type=module', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ url: '/es6/es6import.js', type: 'module' }); + expect(await page.evaluate(() => globalThis.__es6injected)).toBe(42); + }); + + it('should work with a path and type=module', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ + path: path.join(__dirname, 'assets/es6/es6pathimport.js'), + type: 'module', + }); + await page.waitForFunction('window.__es6injected'); + expect(await page.evaluate(() => globalThis.__es6injected)).toBe(42); + }); + + it('should work with a content and type=module', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ + content: `import num from '/es6/es6module.js';window.__es6injected = num;`, + type: 'module', + }); + await page.waitForFunction('window.__es6injected'); + expect(await page.evaluate(() => globalThis.__es6injected)).toBe(42); + }); + + it('should throw an error if loading from url fail', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let error = null; + try { + await page.addScriptTag({ url: '/nonexistfile.js' }); + } catch (error_) { + error = error_; + } + expect(error.message).toBe('Loading script from /nonexistfile.js failed'); + }); + + it('should work with a path', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const scriptHandle = await page.addScriptTag({ + path: path.join(__dirname, 'assets/injectedfile.js'), + }); + expect(scriptHandle.asElement()).not.toBeNull(); + expect(await page.evaluate(() => globalThis.__injected)).toBe(42); + }); + + it('should include sourcemap when path is provided', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ + path: path.join(__dirname, 'assets/injectedfile.js'), + }); + const result = await page.evaluate( + () => globalThis.__injectedError.stack + ); + expect(result).toContain(path.join('assets', 'injectedfile.js')); + }); + + it('should work with content', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const scriptHandle = await page.addScriptTag({ + content: 'window.__injected = 35;', + }); + expect(scriptHandle.asElement()).not.toBeNull(); + expect(await page.evaluate(() => globalThis.__injected)).toBe(35); + }); + + // @see https://github.com/puppeteer/puppeteer/issues/4840 + xit('should throw when added with content to the CSP page', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/csp.html'); + let error = null; + await page + .addScriptTag({ content: 'window.__injected = 35;' }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + }); + + it('should throw when added with URL to the CSP page', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/csp.html'); + let error = null; + await page + .addScriptTag({ url: server.CROSS_PROCESS_PREFIX + '/injectedfile.js' }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + }); + }); + + describe('Page.addStyleTag', function () { + it('should throw an error if no options are provided', async () => { + const { page } = getTestState(); + + let error = null; + try { + // @ts-expect-error purposefully passing bad input + await page.addStyleTag('/injectedstyle.css'); + } catch (error_) { + error = error_; + } + expect(error.message).toBe( + 'Provide an object with a `url`, `path` or `content` property' + ); + }); + + it('should work with a url', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const styleHandle = await page.addStyleTag({ url: '/injectedstyle.css' }); + expect(styleHandle.asElement()).not.toBeNull(); + expect( + await page.evaluate( + `window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')` + ) + ).toBe('rgb(255, 0, 0)'); + }); + + it('should throw an error if loading from url fail', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let error = null; + try { + await page.addStyleTag({ url: '/nonexistfile.js' }); + } catch (error_) { + error = error_; + } + expect(error.message).toBe('Loading style from /nonexistfile.js failed'); + }); + + it('should work with a path', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const styleHandle = await page.addStyleTag({ + path: path.join(__dirname, 'assets/injectedstyle.css'), + }); + expect(styleHandle.asElement()).not.toBeNull(); + expect( + await page.evaluate( + `window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')` + ) + ).toBe('rgb(255, 0, 0)'); + }); + + it('should include sourcemap when path is provided', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.addStyleTag({ + path: path.join(__dirname, 'assets/injectedstyle.css'), + }); + const styleHandle = await page.$('style'); + const styleContent = await page.evaluate( + (style: HTMLStyleElement) => style.innerHTML, + styleHandle + ); + expect(styleContent).toContain(path.join('assets', 'injectedstyle.css')); + }); + + it('should work with content', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const styleHandle = await page.addStyleTag({ + content: 'body { background-color: green; }', + }); + expect(styleHandle.asElement()).not.toBeNull(); + expect( + await page.evaluate( + `window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')` + ) + ).toBe('rgb(0, 128, 0)'); + }); + + it( + 'should throw when added with content to the CSP page', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/csp.html'); + let error = null; + await page + .addStyleTag({ content: 'body { background-color: green; }' }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + } + ); + + it('should throw when added with URL to the CSP page', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/csp.html'); + let error = null; + await page + .addStyleTag({ + url: server.CROSS_PROCESS_PREFIX + '/injectedstyle.css', + }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + }); + }); + + describe('Page.url', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + expect(page.url()).toBe('about:blank'); + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + }); + }); + + describe('Page.setJavaScriptEnabled', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.setJavaScriptEnabled(false); + await page.goto( + 'data:text/html, <script>var something = "forbidden"</script>' + ); + let error = null; + await page.evaluate('something').catch((error_) => (error = error_)); + expect(error.message).toContain('something is not defined'); + + await page.setJavaScriptEnabled(true); + await page.goto( + 'data:text/html, <script>var something = "forbidden"</script>' + ); + expect(await page.evaluate('something')).toBe('forbidden'); + }); + }); + + describe('Page.setCacheEnabled', function () { + it('should enable or disable the cache based on the state passed', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/cached/one-style.html'); + const [cachedRequest] = await Promise.all([ + server.waitForRequest('/cached/one-style.html'), + page.reload(), + ]); + // Rely on "if-modified-since" caching in our test server. + expect(cachedRequest.headers['if-modified-since']).not.toBe(undefined); + + await page.setCacheEnabled(false); + const [nonCachedRequest] = await Promise.all([ + server.waitForRequest('/cached/one-style.html'), + page.reload(), + ]); + expect(nonCachedRequest.headers['if-modified-since']).toBe(undefined); + }); + it( + 'should stay disabled when toggling request interception on/off', + async () => { + const { page, server } = getTestState(); + + await page.setCacheEnabled(false); + await page.setRequestInterception(true); + await page.setRequestInterception(false); + + await page.goto(server.PREFIX + '/cached/one-style.html'); + const [nonCachedRequest] = await Promise.all([ + server.waitForRequest('/cached/one-style.html'), + page.reload(), + ]); + expect(nonCachedRequest.headers['if-modified-since']).toBe(undefined); + } + ); + }); + + describe('printing to PDF', function () { + it('can print to PDF and save to file', async () => { + // Printing to pdf is currently only supported in headless + const { isHeadless, page } = getTestState(); + + if (!isHeadless) return; + + const outputFile = __dirname + '/assets/output.pdf'; + await page.pdf({ path: outputFile }); + expect(fs.readFileSync(outputFile).byteLength).toBeGreaterThan(0); + fs.unlinkSync(outputFile); + }); + }); + + describe('Page.title', function () { + it('should return the page title', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/title.html'); + expect(await page.title()).toBe('Woof-Woof'); + }); + }); + + describe('Page.select', function () { + it('should select single option', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue'); + expect(await page.evaluate(() => globalThis.result.onInput)).toEqual([ + 'blue', + ]); + expect(await page.evaluate(() => globalThis.result.onChange)).toEqual([ + 'blue', + ]); + }); + it('should select only first option', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue', 'green', 'red'); + expect(await page.evaluate(() => globalThis.result.onInput)).toEqual([ + 'blue', + ]); + expect(await page.evaluate(() => globalThis.result.onChange)).toEqual([ + 'blue', + ]); + }); + it('should not throw when select causes navigation', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.$eval('select', (select) => + select.addEventListener( + 'input', + () => ((window as any).location = '/empty.html') + ) + ); + await Promise.all([ + page.select('select', 'blue'), + page.waitForNavigation(), + ]); + expect(page.url()).toContain('empty.html'); + }); + it('should select multiple options', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => globalThis.makeMultiple()); + await page.select('select', 'blue', 'green', 'red'); + expect(await page.evaluate(() => globalThis.result.onInput)).toEqual([ + 'blue', + 'green', + 'red', + ]); + expect(await page.evaluate(() => globalThis.result.onChange)).toEqual([ + 'blue', + 'green', + 'red', + ]); + }); + it('should respect event bubbling', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue'); + expect( + await page.evaluate(() => globalThis.result.onBubblingInput) + ).toEqual(['blue']); + expect( + await page.evaluate(() => globalThis.result.onBubblingChange) + ).toEqual(['blue']); + }); + it('should throw when element is not a <select>', async () => { + const { page, server } = getTestState(); + + let error = null; + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('body', '').catch((error_) => (error = error_)); + expect(error.message).toContain('Element is not a <select> element.'); + }); + it('should return [] on no matched values', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + const result = await page.select('select', '42', 'abc'); + expect(result).toEqual([]); + }); + it('should return an array of matched values', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => globalThis.makeMultiple()); + const result = await page.select('select', 'blue', 'black', 'magenta'); + expect( + result.reduce( + (accumulator, current) => + ['blue', 'black', 'magenta'].includes(current) && accumulator, + true + ) + ).toEqual(true); + }); + it('should return an array of one element when multiple is not set', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + const result = await page.select( + 'select', + '42', + 'blue', + 'black', + 'magenta' + ); + expect(result.length).toEqual(1); + }); + it('should return [] on no values', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + const result = await page.select('select'); + expect(result).toEqual([]); + }); + it('should deselect all options when passed no values for a multiple select', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => globalThis.makeMultiple()); + await page.select('select', 'blue', 'black', 'magenta'); + await page.select('select'); + expect( + await page.$eval('select', (select: HTMLSelectElement) => + Array.from(select.options).every( + (option: HTMLOptionElement) => !option.selected + ) + ) + ).toEqual(true); + }); + it('should deselect all options when passed no values for a select without multiple', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue', 'black', 'magenta'); + await page.select('select'); + expect( + await page.$eval('select', (select: HTMLSelectElement) => + Array.from(select.options).every( + (option: HTMLOptionElement) => !option.selected + ) + ) + ).toEqual(true); + }); + it('should throw if passed in non-strings', async () => { + const { page } = getTestState(); + + await page.setContent('<select><option value="12"/></select>'); + let error = null; + try { + // @ts-expect-error purposefully passing bad input + await page.select('select', 12); + } catch (error_) { + error = error_; + } + expect(error.message).toContain('Values must be strings'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/3327 + it( + 'should work when re-defining top-level Event class', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => (window.Event = null)); + await page.select('select', 'blue'); + expect(await page.evaluate(() => globalThis.result.onInput)).toEqual([ + 'blue', + ]); + expect(await page.evaluate(() => globalThis.result.onChange)).toEqual([ + 'blue', + ]); + } + ); + }); + + describe('Page.Events.Close', function () { + itFailsFirefox('should work with window.close', async () => { + const { page, context } = getTestState(); + + const newPagePromise = new Promise<Page>((fulfill) => + context.once('targetcreated', (target) => fulfill(target.page())) + ); + await page.evaluate( + () => (window['newPage'] = window.open('about:blank')) + ); + const newPage = await newPagePromise; + const closedPromise = new Promise((x) => newPage.on('close', x)); + await page.evaluate(() => window['newPage'].close()); + await closedPromise; + }); + it('should work with page.close', async () => { + const { context } = getTestState(); + + const newPage = await context.newPage(); + const closedPromise = new Promise((x) => newPage.on('close', x)); + await newPage.close(); + await closedPromise; + }); + }); + + describe('Page.browser', function () { + it('should return the correct browser instance', async () => { + const { page, browser } = getTestState(); + + expect(page.browser()).toBe(browser); + }); + }); + + describe('Page.browserContext', function () { + it('should return the correct browser instance', async () => { + const { page, context } = getTestState(); + + expect(page.browserContext()).toBe(context); + }); + }); +}); diff --git a/remote/test/puppeteer/test/queryselector.spec.ts b/remote/test/puppeteer/test/queryselector.spec.ts new file mode 100644 index 0000000000..7a147ddd01 --- /dev/null +++ b/remote/test/puppeteer/test/queryselector.spec.ts @@ -0,0 +1,507 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions +import { CustomQueryHandler } from '../lib/cjs/puppeteer/common/QueryHandler.js'; + +describe('querySelector', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + describe('Page.$eval', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.setContent('<section id="testAttribute">43543</section>'); + const idAttribute = await page.$eval('section', (e) => e.id); + expect(idAttribute).toBe('testAttribute'); + }); + it('should accept arguments', async () => { + const { page } = getTestState(); + + await page.setContent('<section>hello</section>'); + const text = await page.$eval( + 'section', + (e, suffix) => e.textContent + suffix, + ' world!' + ); + expect(text).toBe('hello world!'); + }); + it('should accept ElementHandles as arguments', async () => { + const { page } = getTestState(); + + await page.setContent('<section>hello</section><div> world</div>'); + const divHandle = await page.$('div'); + const text = await page.$eval( + 'section', + (e, div: HTMLElement) => e.textContent + div.textContent, + divHandle + ); + expect(text).toBe('hello world'); + }); + it('should throw error if no element is found', async () => { + const { page } = getTestState(); + + let error = null; + await page + .$eval('section', (e) => e.id) + .catch((error_) => (error = error_)); + expect(error.message).toContain( + 'failed to find element matching selector "section"' + ); + }); + }); + + describe('pierceHandler', function () { + beforeEach(async () => { + const { page } = getTestState(); + await page.setContent( + `<script> + const div = document.createElement('div'); + const shadowRoot = div.attachShadow({mode: 'open'}); + const div1 = document.createElement('div'); + div1.textContent = 'Hello'; + div1.className = 'foo'; + const div2 = document.createElement('div'); + div2.textContent = 'World'; + div2.className = 'foo'; + shadowRoot.appendChild(div1); + shadowRoot.appendChild(div2); + document.documentElement.appendChild(div); + </script>` + ); + }); + it('should find first element in shadow', async () => { + const { page } = getTestState(); + const div = await page.$('pierce/.foo'); + const text = await div.evaluate( + (element: Element) => element.textContent + ); + expect(text).toBe('Hello'); + }); + it('should find all elements in shadow', async () => { + const { page } = getTestState(); + const divs = await page.$$('pierce/.foo'); + const text = await Promise.all( + divs.map((div) => + div.evaluate((element: Element) => element.textContent) + ) + ); + expect(text.join(' ')).toBe('Hello World'); + }); + }); + + // The tests for $$eval are repeated later in this file in the test group 'QueryAll'. + // This is done to also test a query handler where QueryAll returns an Element[] + // as opposed to NodeListOf<Element>. + describe('Page.$$eval', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.setContent( + '<div>hello</div><div>beautiful</div><div>world!</div>' + ); + const divsCount = await page.$$eval('div', (divs) => divs.length); + expect(divsCount).toBe(3); + }); + it('should accept extra arguments', async () => { + const { page } = getTestState(); + await page.setContent( + '<div>hello</div><div>beautiful</div><div>world!</div>' + ); + const divsCountPlus5 = await page.$$eval( + 'div', + (divs, two: number, three: number) => divs.length + two + three, + 2, + 3 + ); + expect(divsCountPlus5).toBe(8); + }); + it('should accept ElementHandles as arguments', async () => { + const { page } = getTestState(); + await page.setContent( + '<section>2</section><section>2</section><section>1</section><div>3</div>' + ); + const divHandle = await page.$('div'); + const sum = await page.$$eval( + 'section', + (sections, div: HTMLElement) => + sections.reduce( + (acc, section) => acc + Number(section.textContent), + 0 + ) + Number(div.textContent), + divHandle + ); + expect(sum).toBe(8); + }); + it('should handle many elements', async () => { + const { page } = getTestState(); + await page.evaluate( + ` + for (var i = 0; i <= 1000; i++) { + const section = document.createElement('section'); + section.textContent = i; + document.body.appendChild(section); + } + ` + ); + const sum = await page.$$eval('section', (sections) => + sections.reduce((acc, section) => acc + Number(section.textContent), 0) + ); + expect(sum).toBe(500500); + }); + }); + + describe('Page.$', function () { + it('should query existing element', async () => { + const { page } = getTestState(); + + await page.setContent('<section>test</section>'); + const element = await page.$('section'); + expect(element).toBeTruthy(); + }); + it('should return null for non-existing element', async () => { + const { page } = getTestState(); + + const element = await page.$('non-existing-element'); + expect(element).toBe(null); + }); + }); + + describe('Page.$$', function () { + it('should query existing elements', async () => { + const { page } = getTestState(); + + await page.setContent('<div>A</div><br/><div>B</div>'); + const elements = await page.$$('div'); + expect(elements.length).toBe(2); + const promises = elements.map((element) => + page.evaluate((e: HTMLElement) => e.textContent, element) + ); + expect(await Promise.all(promises)).toEqual(['A', 'B']); + }); + it('should return empty array if nothing is found', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const elements = await page.$$('div'); + expect(elements.length).toBe(0); + }); + }); + + describe('Path.$x', function () { + it('should query existing element', async () => { + const { page } = getTestState(); + + await page.setContent('<section>test</section>'); + const elements = await page.$x('/html/body/section'); + expect(elements[0]).toBeTruthy(); + expect(elements.length).toBe(1); + }); + it('should return empty array for non-existing element', async () => { + const { page } = getTestState(); + + const element = await page.$x('/html/body/non-existing-element'); + expect(element).toEqual([]); + }); + it('should return multiple elements', async () => { + const { page } = getTestState(); + + await page.setContent('<div></div><div></div>'); + const elements = await page.$x('/html/body/div'); + expect(elements.length).toBe(2); + }); + }); + + describe('ElementHandle.$', function () { + it('should query existing element', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/playground.html'); + await page.setContent( + '<html><body><div class="second"><div class="inner">A</div></div></body></html>' + ); + const html = await page.$('html'); + const second = await html.$('.second'); + const inner = await second.$('.inner'); + const content = await page.evaluate( + (e: HTMLElement) => e.textContent, + inner + ); + expect(content).toBe('A'); + }); + + it('should return null for non-existing element', async () => { + const { page } = getTestState(); + + await page.setContent( + '<html><body><div class="second"><div class="inner">B</div></div></body></html>' + ); + const html = await page.$('html'); + const second = await html.$('.third'); + expect(second).toBe(null); + }); + }); + describe('ElementHandle.$eval', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.setContent( + '<html><body><div class="tweet"><div class="like">100</div><div class="retweets">10</div></div></body></html>' + ); + const tweet = await page.$('.tweet'); + const content = await tweet.$eval( + '.like', + (node: HTMLElement) => node.innerText + ); + expect(content).toBe('100'); + }); + + it('should retrieve content from subtree', async () => { + const { page } = getTestState(); + + const htmlContent = + '<div class="a">not-a-child-div</div><div id="myId"><div class="a">a-child-div</div></div>'; + await page.setContent(htmlContent); + const elementHandle = await page.$('#myId'); + const content = await elementHandle.$eval( + '.a', + (node: HTMLElement) => node.innerText + ); + expect(content).toBe('a-child-div'); + }); + + it('should throw in case of missing selector', async () => { + const { page } = getTestState(); + + const htmlContent = + '<div class="a">not-a-child-div</div><div id="myId"></div>'; + await page.setContent(htmlContent); + const elementHandle = await page.$('#myId'); + const errorMessage = await elementHandle + .$eval('.a', (node: HTMLElement) => node.innerText) + .catch((error) => error.message); + expect(errorMessage).toBe( + `Error: failed to find element matching selector ".a"` + ); + }); + }); + describe('ElementHandle.$$eval', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.setContent( + '<html><body><div class="tweet"><div class="like">100</div><div class="like">10</div></div></body></html>' + ); + const tweet = await page.$('.tweet'); + const content = await tweet.$$eval('.like', (nodes: HTMLElement[]) => + nodes.map((n) => n.innerText) + ); + expect(content).toEqual(['100', '10']); + }); + + it('should retrieve content from subtree', async () => { + const { page } = getTestState(); + + const htmlContent = + '<div class="a">not-a-child-div</div><div id="myId"><div class="a">a1-child-div</div><div class="a">a2-child-div</div></div>'; + await page.setContent(htmlContent); + const elementHandle = await page.$('#myId'); + const content = await elementHandle.$$eval('.a', (nodes: HTMLElement[]) => + nodes.map((n) => n.innerText) + ); + expect(content).toEqual(['a1-child-div', 'a2-child-div']); + }); + + it('should not throw in case of missing selector', async () => { + const { page } = getTestState(); + + const htmlContent = + '<div class="a">not-a-child-div</div><div id="myId"></div>'; + await page.setContent(htmlContent); + const elementHandle = await page.$('#myId'); + const nodesLength = await elementHandle.$$eval( + '.a', + (nodes) => nodes.length + ); + expect(nodesLength).toBe(0); + }); + }); + + describe('ElementHandle.$$', function () { + it('should query existing elements', async () => { + const { page } = getTestState(); + + await page.setContent( + '<html><body><div>A</div><br/><div>B</div></body></html>' + ); + const html = await page.$('html'); + const elements = await html.$$('div'); + expect(elements.length).toBe(2); + const promises = elements.map((element) => + page.evaluate((e: HTMLElement) => e.textContent, element) + ); + expect(await Promise.all(promises)).toEqual(['A', 'B']); + }); + + it('should return empty array for non-existing elements', async () => { + const { page } = getTestState(); + + await page.setContent( + '<html><body><span>A</span><br/><span>B</span></body></html>' + ); + const html = await page.$('html'); + const elements = await html.$$('div'); + expect(elements.length).toBe(0); + }); + }); + + describe('ElementHandle.$x', function () { + it('should query existing element', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/playground.html'); + await page.setContent( + '<html><body><div class="second"><div class="inner">A</div></div></body></html>' + ); + const html = await page.$('html'); + const second = await html.$x(`./body/div[contains(@class, 'second')]`); + const inner = await second[0].$x(`./div[contains(@class, 'inner')]`); + const content = await page.evaluate( + (e: HTMLElement) => e.textContent, + inner[0] + ); + expect(content).toBe('A'); + }); + + it('should return null for non-existing element', async () => { + const { page } = getTestState(); + + await page.setContent( + '<html><body><div class="second"><div class="inner">B</div></div></body></html>' + ); + const html = await page.$('html'); + const second = await html.$x(`/div[contains(@class, 'third')]`); + expect(second).toEqual([]); + }); + }); + + // This is the same tests for `$$eval` and `$$` as above, but with a queryAll + // handler that returns an array instead of a list of nodes. + describe('QueryAll', function () { + const handler: CustomQueryHandler = { + queryAll: (element: Element, selector: string) => + Array.from(element.querySelectorAll(selector)), + }; + before(() => { + const { puppeteer } = getTestState(); + puppeteer.registerCustomQueryHandler('allArray', handler); + }); + + it('should have registered handler', async () => { + const { puppeteer } = getTestState(); + expect( + puppeteer.customQueryHandlerNames().includes('allArray') + ).toBeTruthy(); + }); + it('$$ should query existing elements', async () => { + const { page } = getTestState(); + + await page.setContent( + '<html><body><div>A</div><br/><div>B</div></body></html>' + ); + const html = await page.$('html'); + const elements = await html.$$('allArray/div'); + expect(elements.length).toBe(2); + const promises = elements.map((element) => + page.evaluate((e: HTMLElement) => e.textContent, element) + ); + expect(await Promise.all(promises)).toEqual(['A', 'B']); + }); + + it('$$ should return empty array for non-existing elements', async () => { + const { page } = getTestState(); + + await page.setContent( + '<html><body><span>A</span><br/><span>B</span></body></html>' + ); + const html = await page.$('html'); + const elements = await html.$$('allArray/div'); + expect(elements.length).toBe(0); + }); + it('$$eval should work', async () => { + const { page } = getTestState(); + + await page.setContent( + '<div>hello</div><div>beautiful</div><div>world!</div>' + ); + const divsCount = await page.$$eval( + 'allArray/div', + (divs) => divs.length + ); + expect(divsCount).toBe(3); + }); + it('$$eval should accept extra arguments', async () => { + const { page } = getTestState(); + await page.setContent( + '<div>hello</div><div>beautiful</div><div>world!</div>' + ); + const divsCountPlus5 = await page.$$eval( + 'allArray/div', + (divs, two: number, three: number) => divs.length + two + three, + 2, + 3 + ); + expect(divsCountPlus5).toBe(8); + }); + it('$$eval should accept ElementHandles as arguments', async () => { + const { page } = getTestState(); + await page.setContent( + '<section>2</section><section>2</section><section>1</section><div>3</div>' + ); + const divHandle = await page.$('div'); + const sum = await page.$$eval( + 'allArray/section', + (sections, div: HTMLElement) => + sections.reduce( + (acc, section) => acc + Number(section.textContent), + 0 + ) + Number(div.textContent), + divHandle + ); + expect(sum).toBe(8); + }); + it('$$eval should handle many elements', async () => { + const { page } = getTestState(); + await page.evaluate( + ` + for (var i = 0; i <= 1000; i++) { + const section = document.createElement('section'); + section.textContent = i; + document.body.appendChild(section); + } + ` + ); + const sum = await page.$$eval('allArray/section', (sections) => + sections.reduce((acc, section) => acc + Number(section.textContent), 0) + ); + expect(sum).toBe(500500); + }); + }); +}); diff --git a/remote/test/puppeteer/test/requestinterception.spec.ts b/remote/test/puppeteer/test/requestinterception.spec.ts new file mode 100644 index 0000000000..462eb714c7 --- /dev/null +++ b/remote/test/puppeteer/test/requestinterception.spec.ts @@ -0,0 +1,703 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; +import utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('request interception', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + describe('Page.setRequestInterception', function () { + it('should intercept', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + if (utils.isFavicon(request)) { + request.continue(); + return; + } + expect(request.url()).toContain('empty.html'); + expect(request.headers()['user-agent']).toBeTruthy(); + expect(request.method()).toBe('GET'); + expect(request.postData()).toBe(undefined); + expect(request.isNavigationRequest()).toBe(true); + expect(request.resourceType()).toBe('document'); + expect(request.frame() === page.mainFrame()).toBe(true); + expect(request.frame().url()).toBe('about:blank'); + request.continue(); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok()).toBe(true); + expect(response.remoteAddress().port).toBe(server.PORT); + }); + it('should work when POST is redirected with 302', async () => { + const { page, server } = getTestState(); + + server.setRedirect('/rredirect', '/empty.html'); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + await page.setContent(` + <form action='/rredirect' method='post'> + <input type="hidden" id="foo" name="foo" value="FOOBAR"> + </form> + `); + await Promise.all([ + page.$eval('form', (form: HTMLFormElement) => form.submit()), + page.waitForNavigation(), + ]); + }); + // @see https://github.com/puppeteer/puppeteer/issues/3973 + it('should work when header manipulation headers with redirect', async () => { + const { page, server } = getTestState(); + + server.setRedirect('/rrredirect', '/empty.html'); + await page.setRequestInterception(true); + page.on('request', (request) => { + const headers = Object.assign({}, request.headers(), { + foo: 'bar', + }); + request.continue({ headers }); + }); + await page.goto(server.PREFIX + '/rrredirect'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4743 + it('should be able to remove headers', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + const headers = Object.assign({}, request.headers(), { + foo: 'bar', + origin: undefined, // remove "origin" header + }); + request.continue({ headers }); + }); + + const [serverRequest] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.PREFIX + '/empty.html'), + ]); + + expect(serverRequest.headers.origin).toBe(undefined); + }); + it('should contain referer header', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + if (!utils.isFavicon(request)) requests.push(request); + request.continue(); + }); + await page.goto(server.PREFIX + '/one-style.html'); + expect(requests[1].url()).toContain('/one-style.css'); + expect(requests[1].headers().referer).toContain('/one-style.html'); + }); + it('should properly return navigation response when URL has cookies', async () => { + const { page, server } = getTestState(); + + // Setup cookie. + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ name: 'foo', value: 'bar' }); + + // Setup request interception. + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + const response = await page.reload(); + expect(response.status()).toBe(200); + }); + it('should stop intercepting', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.once('request', (request) => request.continue()); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(false); + await page.goto(server.EMPTY_PAGE); + }); + it('should show custom HTTP headers', async () => { + const { page, server } = getTestState(); + + await page.setExtraHTTPHeaders({ + foo: 'bar', + }); + await page.setRequestInterception(true); + page.on('request', (request) => { + expect(request.headers()['foo']).toBe('bar'); + request.continue(); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok()).toBe(true); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4337 + it('should work with redirect inside sync XHR', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + server.setRedirect('/logo.png', '/pptr.png'); + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + const status = await page.evaluate(async () => { + const request = new XMLHttpRequest(); + request.open('GET', '/logo.png', false); // `false` makes the request synchronous + request.send(null); + return request.status; + }); + expect(status).toBe(200); + }); + it('should work with custom referer headers', async () => { + const { page, server } = getTestState(); + + await page.setExtraHTTPHeaders({ referer: server.EMPTY_PAGE }); + await page.setRequestInterception(true); + page.on('request', (request) => { + expect(request.headers()['referer']).toBe(server.EMPTY_PAGE); + request.continue(); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok()).toBe(true); + }); + it('should be abortable', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + if (request.url().endsWith('.css')) request.abort(); + else request.continue(); + }); + let failedRequests = 0; + page.on('requestfailed', () => ++failedRequests); + const response = await page.goto(server.PREFIX + '/one-style.html'); + expect(response.ok()).toBe(true); + expect(response.request().failure()).toBe(null); + expect(failedRequests).toBe(1); + }); + it('should be abortable with custom error codes', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.abort('internetdisconnected'); + }); + let failedRequest = null; + page.on('requestfailed', (request) => (failedRequest = request)); + await page.goto(server.EMPTY_PAGE).catch(() => {}); + expect(failedRequest).toBeTruthy(); + expect(failedRequest.failure().errorText).toBe( + 'net::ERR_INTERNET_DISCONNECTED' + ); + }); + it('should send referer', async () => { + const { page, server } = getTestState(); + + await page.setExtraHTTPHeaders({ + referer: 'http://google.com/', + }); + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + const [request] = await Promise.all([ + server.waitForRequest('/grid.html'), + page.goto(server.PREFIX + '/grid.html'), + ]); + expect(request.headers['referer']).toBe('http://google.com/'); + }); + it('should fail navigation when aborting main resource', async () => { + const { page, server, isChrome } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => request.abort()); + let error = null; + await page.goto(server.EMPTY_PAGE).catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + if (isChrome) expect(error.message).toContain('net::ERR_FAILED'); + else expect(error.message).toContain('NS_ERROR_FAILURE'); + }); + it('should work with redirects', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + request.continue(); + requests.push(request); + }); + server.setRedirect( + '/non-existing-page.html', + '/non-existing-page-2.html' + ); + server.setRedirect( + '/non-existing-page-2.html', + '/non-existing-page-3.html' + ); + server.setRedirect( + '/non-existing-page-3.html', + '/non-existing-page-4.html' + ); + server.setRedirect('/non-existing-page-4.html', '/empty.html'); + const response = await page.goto( + server.PREFIX + '/non-existing-page.html' + ); + expect(response.status()).toBe(200); + expect(response.url()).toContain('empty.html'); + expect(requests.length).toBe(5); + expect(requests[2].resourceType()).toBe('document'); + // Check redirect chain + const redirectChain = response.request().redirectChain(); + expect(redirectChain.length).toBe(4); + expect(redirectChain[0].url()).toContain('/non-existing-page.html'); + expect(redirectChain[2].url()).toContain('/non-existing-page-3.html'); + for (let i = 0; i < redirectChain.length; ++i) { + const request = redirectChain[i]; + expect(request.isNavigationRequest()).toBe(true); + expect(request.redirectChain().indexOf(request)).toBe(i); + } + }); + it('should work with redirects for subresources', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + request.continue(); + if (!utils.isFavicon(request)) requests.push(request); + }); + server.setRedirect('/one-style.css', '/two-style.css'); + server.setRedirect('/two-style.css', '/three-style.css'); + server.setRedirect('/three-style.css', '/four-style.css'); + server.setRoute('/four-style.css', (req, res) => + res.end('body {box-sizing: border-box; }') + ); + + const response = await page.goto(server.PREFIX + '/one-style.html'); + expect(response.status()).toBe(200); + expect(response.url()).toContain('one-style.html'); + expect(requests.length).toBe(5); + expect(requests[0].resourceType()).toBe('document'); + expect(requests[1].resourceType()).toBe('stylesheet'); + // Check redirect chain + const redirectChain = requests[1].redirectChain(); + expect(redirectChain.length).toBe(3); + expect(redirectChain[0].url()).toContain('/one-style.css'); + expect(redirectChain[2].url()).toContain('/three-style.css'); + }); + it('should be able to abort redirects', async () => { + const { page, server, isChrome } = getTestState(); + + await page.setRequestInterception(true); + server.setRedirect('/non-existing.json', '/non-existing-2.json'); + server.setRedirect('/non-existing-2.json', '/simple.html'); + page.on('request', (request) => { + if (request.url().includes('non-existing-2')) request.abort(); + else request.continue(); + }); + await page.goto(server.EMPTY_PAGE); + const result = await page.evaluate(async () => { + try { + await fetch('/non-existing.json'); + } catch (error) { + return error.message; + } + }); + if (isChrome) expect(result).toContain('Failed to fetch'); + else expect(result).toContain('NetworkError'); + }); + it('should work with equal requests', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let responseCount = 1; + server.setRoute('/zzz', (req, res) => res.end(responseCount++ * 11 + '')); + await page.setRequestInterception(true); + + let spinner = false; + // Cancel 2nd request. + page.on('request', (request) => { + if (utils.isFavicon(request)) { + request.continue(); + return; + } + spinner ? request.abort() : request.continue(); + spinner = !spinner; + }); + const results = await page.evaluate(() => + Promise.all([ + fetch('/zzz') + .then((response) => response.text()) + .catch(() => 'FAILED'), + fetch('/zzz') + .then((response) => response.text()) + .catch(() => 'FAILED'), + fetch('/zzz') + .then((response) => response.text()) + .catch(() => 'FAILED'), + ]) + ); + expect(results).toEqual(['11', 'FAILED', '22']); + }); + it('should navigate to dataURL and fire dataURL requests', async () => { + const { page } = getTestState(); + + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + requests.push(request); + request.continue(); + }); + const dataURL = 'data:text/html,<div>yo</div>'; + const response = await page.goto(dataURL); + expect(response.status()).toBe(200); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(dataURL); + }); + it('should be able to fetch dataURL and fire dataURL requests', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + requests.push(request); + request.continue(); + }); + const dataURL = 'data:text/html,<div>yo</div>'; + const text = await page.evaluate( + (url: string) => fetch(url).then((r) => r.text()), + dataURL + ); + expect(text).toBe('<div>yo</div>'); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(dataURL); + }); + it('should navigate to URL with hash and fire requests without hash', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + requests.push(request); + request.continue(); + }); + const response = await page.goto(server.EMPTY_PAGE + '#hash'); + expect(response.status()).toBe(200); + expect(response.url()).toBe(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(server.EMPTY_PAGE); + }); + it('should work with encoded server', async () => { + const { page, server } = getTestState(); + + // The requestWillBeSent will report encoded URL, whereas interception will + // report URL as-is. @see crbug.com/759388 + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + const response = await page.goto( + server.PREFIX + '/some nonexisting page' + ); + expect(response.status()).toBe(404); + }); + it('should work with badly encoded server', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + server.setRoute('/malformed?rnd=%911', (req, res) => res.end()); + page.on('request', (request) => request.continue()); + const response = await page.goto(server.PREFIX + '/malformed?rnd=%911'); + expect(response.status()).toBe(200); + }); + it('should work with encoded server - 2', async () => { + const { page, server } = getTestState(); + + // The requestWillBeSent will report URL as-is, whereas interception will + // report encoded URL for stylesheet. @see crbug.com/759388 + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + request.continue(); + requests.push(request); + }); + const response = await page.goto( + `data:text/html,<link rel="stylesheet" href="${server.PREFIX}/fonts?helvetica|arial"/>` + ); + expect(response.status()).toBe(200); + expect(requests.length).toBe(2); + expect(requests[1].response().status()).toBe(404); + }); + it('should not throw "Invalid Interception Id" if the request was cancelled', async () => { + const { page, server } = getTestState(); + + await page.setContent('<iframe></iframe>'); + await page.setRequestInterception(true); + let request = null; + page.on('request', async (r) => (request = r)); + page.$eval( + 'iframe', + (frame: HTMLIFrameElement, url: string) => (frame.src = url), + server.EMPTY_PAGE + ), + // Wait for request interception. + await utils.waitEvent(page, 'request'); + // Delete frame to cause request to be canceled. + await page.$eval('iframe', (frame) => frame.remove()); + let error = null; + await request.continue().catch((error_) => (error = error_)); + expect(error).toBe(null); + }); + it('should throw if interception is not enabled', async () => { + const { page, server } = getTestState(); + + let error = null; + page.on('request', async (request) => { + try { + await request.continue(); + } catch (error_) { + error = error_; + } + }); + await page.goto(server.EMPTY_PAGE); + expect(error.message).toContain('Request Interception is not enabled'); + }); + it('should work with file URLs', async () => { + const { page } = getTestState(); + + await page.setRequestInterception(true); + const urls = new Set(); + page.on('request', (request) => { + urls.add(request.url().split('/').pop()); + request.continue(); + }); + await page.goto( + pathToFileURL(path.join(__dirname, 'assets', 'one-style.html')) + ); + expect(urls.size).toBe(2); + expect(urls.has('one-style.html')).toBe(true); + expect(urls.has('one-style.css')).toBe(true); + }); + }); + + describe('Request.continue', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + await page.goto(server.EMPTY_PAGE); + }); + it('should amend HTTP headers', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + const headers = Object.assign({}, request.headers()); + headers['FOO'] = 'bar'; + request.continue({ headers }); + }); + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + server.waitForRequest('/sleep.zzz'), + page.evaluate(() => fetch('/sleep.zzz')), + ]); + expect(request.headers['foo']).toBe('bar'); + }); + it('should redirect in a way non-observable to page', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + const redirectURL = request.url().includes('/empty.html') + ? server.PREFIX + '/consolelog.html' + : undefined; + request.continue({ url: redirectURL }); + }); + let consoleMessage = null; + page.on('console', (msg) => (consoleMessage = msg)); + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + expect(consoleMessage.text()).toBe('yellow'); + }); + it('should amend method', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.continue({ method: 'POST' }); + }); + const [request] = await Promise.all([ + server.waitForRequest('/sleep.zzz'), + page.evaluate(() => fetch('/sleep.zzz')), + ]); + expect(request.method).toBe('POST'); + }); + it('should amend post data', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.continue({ postData: 'doggo' }); + }); + const [serverRequest] = await Promise.all([ + server.waitForRequest('/sleep.zzz'), + page.evaluate(() => + fetch('/sleep.zzz', { method: 'POST', body: 'birdy' }) + ), + ]); + expect(await serverRequest.postBody).toBe('doggo'); + }); + it('should amend both post data and method on navigation', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.continue({ method: 'POST', postData: 'doggo' }); + }); + const [serverRequest] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE), + ]); + expect(serverRequest.method).toBe('POST'); + expect(await serverRequest.postBody).toBe('doggo'); + }); + }); + + describe('Request.respond', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.respond({ + status: 201, + headers: { + foo: 'bar', + }, + body: 'Yo, page!', + }); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(201); + expect(response.headers().foo).toBe('bar'); + expect(await page.evaluate(() => document.body.textContent)).toBe( + 'Yo, page!' + ); + }); + it('should work with status code 422', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.respond({ + status: 422, + body: 'Yo, page!', + }); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(422); + expect(response.statusText()).toBe('Unprocessable Entity'); + expect(await page.evaluate(() => document.body.textContent)).toBe( + 'Yo, page!' + ); + }); + it('should redirect', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + if (!request.url().includes('rrredirect')) { + request.continue(); + return; + } + request.respond({ + status: 302, + headers: { + location: server.EMPTY_PAGE, + }, + }); + }); + const response = await page.goto(server.PREFIX + '/rrredirect'); + expect(response.request().redirectChain().length).toBe(1); + expect(response.request().redirectChain()[0].url()).toBe( + server.PREFIX + '/rrredirect' + ); + expect(response.url()).toBe(server.EMPTY_PAGE); + }); + it('should allow mocking binary responses', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + const imageBuffer = fs.readFileSync( + path.join(__dirname, 'assets', 'pptr.png') + ); + request.respond({ + contentType: 'image/png', + body: imageBuffer, + }); + }); + await page.evaluate((PREFIX) => { + const img = document.createElement('img'); + img.src = PREFIX + '/does-not-exist.png'; + document.body.appendChild(img); + return new Promise((fulfill) => (img.onload = fulfill)); + }, server.PREFIX); + const img = await page.$('img'); + expect(await img.screenshot()).toBeGolden('mock-binary-response.png'); + }); + it('should stringify intercepted request response headers', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.respond({ + status: 200, + headers: { + foo: true, + }, + body: 'Yo, page!', + }); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(200); + const headers = response.headers(); + expect(headers.foo).toBe('true'); + expect(await page.evaluate(() => document.body.textContent)).toBe( + 'Yo, page!' + ); + }); + }); +}); + +/** + * @param {string} path + * @returns {string} + */ +function pathToFileURL(path) { + let pathName = path.replace(/\\/g, '/'); + // Windows drive letter must be prefixed with a slash. + if (!pathName.startsWith('/')) pathName = '/' + pathName; + return 'file://' + pathName; +} diff --git a/remote/test/puppeteer/test/run_static_server.js b/remote/test/puppeteer/test/run_static_server.js new file mode 100755 index 0000000000..6779e8816a --- /dev/null +++ b/remote/test/puppeteer/test/run_static_server.js @@ -0,0 +1,33 @@ +#!/usr/bin/env node +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const path = require('path'); +const { TestServer } = require('../utils/testserver/'); + +const port = 8907; +const httpsPort = 8908; +const assetsPath = path.join(__dirname, 'assets'); +const cachedPath = path.join(__dirname, 'assets', 'cached'); + +Promise.all([ + TestServer.create(assetsPath, port), + TestServer.createHTTPS(assetsPath, httpsPort), +]).then(([server, httpsServer]) => { + server.enableHTTPCache(cachedPath); + httpsServer.enableHTTPCache(cachedPath); + console.log(`HTTP: server is running on http://localhost:${port}`); + console.log(`HTTPS: server is running on https://localhost:${httpsPort}`); +}); diff --git a/remote/test/puppeteer/test/screenshot.spec.ts b/remote/test/puppeteer/test/screenshot.spec.ts new file mode 100644 index 0000000000..de33b9c94f --- /dev/null +++ b/remote/test/puppeteer/test/screenshot.spec.ts @@ -0,0 +1,323 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Screenshots', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.screenshot', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('screenshot-sanity.png'); + }); + it('should clip rect', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + clip: { + x: 50, + y: 100, + width: 150, + height: 100, + }, + }); + expect(screenshot).toBeGolden('screenshot-clip-rect.png'); + }); + it('should clip elements to the viewport', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + clip: { + x: 50, + y: 600, + width: 100, + height: 100, + }, + }); + expect(screenshot).toBeGolden('screenshot-offscreen-clip.png'); + }); + it('should run in parallel', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/grid.html'); + const promises = []; + for (let i = 0; i < 3; ++i) { + promises.push( + page.screenshot({ + clip: { + x: 50 * i, + y: 0, + width: 50, + height: 50, + }, + }) + ); + } + const screenshots = await Promise.all(promises); + expect(screenshots[1]).toBeGolden('grid-cell-1.png'); + }); + it('should take fullPage screenshots', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + fullPage: true, + }); + expect(screenshot).toBeGolden('screenshot-grid-fullpage.png'); + }); + it('should run in parallel in multiple pages', async () => { + const { server, context } = getTestState(); + + const N = 2; + const pages = await Promise.all( + Array(N) + .fill(0) + .map(async () => { + const page = await context.newPage(); + await page.goto(server.PREFIX + '/grid.html'); + return page; + }) + ); + const promises = []; + for (let i = 0; i < N; ++i) + promises.push( + pages[i].screenshot({ + clip: { x: 50 * i, y: 0, width: 50, height: 50 }, + }) + ); + const screenshots = await Promise.all(promises); + for (let i = 0; i < N; ++i) + expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`); + await Promise.all(pages.map((page) => page.close())); + }); + it('should allow transparency', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 100, height: 100 }); + await page.goto(server.EMPTY_PAGE); + const screenshot = await page.screenshot({ omitBackground: true }); + expect(screenshot).toBeGolden('transparent.png'); + }); + it('should render white background on jpeg file', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 100, height: 100 }); + await page.goto(server.EMPTY_PAGE); + const screenshot = await page.screenshot({ + omitBackground: true, + type: 'jpeg', + }); + expect(screenshot).toBeGolden('white.jpg'); + }); + it('should work with odd clip size on Retina displays', async () => { + const { page } = getTestState(); + + const screenshot = await page.screenshot({ + clip: { + x: 0, + y: 0, + width: 11, + height: 11, + }, + }); + expect(screenshot).toBeGolden('screenshot-clip-odd-size.png'); + }); + it('should return base64', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + encoding: 'base64', + }); + // TODO (@jackfranklin): improve the screenshot types. + // - if we pass encoding: 'base64', it returns a string + // - else it returns a buffer. + // If we can fix that we can avoid this "as string" here. + expect(Buffer.from(screenshot as string, 'base64')).toBeGolden( + 'screenshot-sanity.png' + ); + }); + }); + + describe('ElementHandle.screenshot', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/grid.html'); + await page.evaluate(() => window.scrollBy(50, 100)); + const elementHandle = await page.$('.box:nth-of-type(3)'); + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-bounding-box.png'); + }); + it('should take into account padding and border', async () => { + const { page } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.setContent(` + something above + <style>div { + border: 2px solid blue; + background: green; + width: 50px; + height: 50px; + } + </style> + <div></div> + `); + const elementHandle = await page.$('div'); + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-padding-border.png'); + }); + it('should capture full element when larger than viewport', async () => { + const { page } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + + await page.setContent(` + something above + <style> + div.to-screenshot { + border: 1px solid blue; + width: 600px; + height: 600px; + margin-left: 50px; + } + ::-webkit-scrollbar{ + display: none; + } + </style> + <div class="to-screenshot"></div> + `); + const elementHandle = await page.$('div.to-screenshot'); + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden( + 'screenshot-element-larger-than-viewport.png' + ); + + expect( + await page.evaluate(() => ({ + w: window.innerWidth, + h: window.innerHeight, + })) + ).toEqual({ w: 500, h: 500 }); + }); + it('should scroll element into view', async () => { + const { page } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.setContent(` + something above + <style>div.above { + border: 2px solid blue; + background: red; + height: 1500px; + } + div.to-screenshot { + border: 2px solid blue; + background: green; + width: 50px; + height: 50px; + } + </style> + <div class="above"></div> + <div class="to-screenshot"></div> + `); + const elementHandle = await page.$('div.to-screenshot'); + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden( + 'screenshot-element-scrolled-into-view.png' + ); + }); + it('should work with a rotated element', async () => { + const { page } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.setContent(`<div style="position:absolute; + top: 100px; + left: 100px; + width: 100px; + height: 100px; + background: green; + transform: rotateZ(200deg);"> </div>`); + const elementHandle = await page.$('div'); + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-rotate.png'); + }); + it('should fail to screenshot a detached element', async () => { + const { page } = getTestState(); + + await page.setContent('<h1>remove this</h1>'); + const elementHandle = await page.$('h1'); + await page.evaluate( + (element: HTMLElement) => element.remove(), + elementHandle + ); + const screenshotError = await elementHandle + .screenshot() + .catch((error) => error); + expect(screenshotError.message).toBe( + 'Node is either not visible or not an HTMLElement' + ); + }); + it('should not hang with zero width/height element', async () => { + const { page } = getTestState(); + + await page.setContent('<div style="width: 50px; height: 0"></div>'); + const div = await page.$('div'); + const error = await div.screenshot().catch((error_) => error_); + expect(error.message).toBe('Node has 0 height.'); + }); + it('should work for an element with fractional dimensions', async () => { + const { page } = getTestState(); + + await page.setContent( + '<div style="width:48.51px;height:19.8px;border:1px solid black;"></div>' + ); + const elementHandle = await page.$('div'); + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-fractional.png'); + }); + it('should work for an element with an offset', async () => { + const { page } = getTestState(); + + await page.setContent( + '<div style="position:absolute; top: 10.3px; left: 20.4px;width:50.3px;height:20.2px;border:1px solid black;"></div>' + ); + const elementHandle = await page.$('div'); + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-fractional-offset.png'); + }); + }); +}); diff --git a/remote/test/puppeteer/test/target.spec.ts b/remote/test/puppeteer/test/target.spec.ts new file mode 100644 index 0000000000..72cfe1d835 --- /dev/null +++ b/remote/test/puppeteer/test/target.spec.ts @@ -0,0 +1,294 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import utils from './utils.js'; +const { waitEvent } = utils; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions +import { Target } from '../lib/cjs/puppeteer/common/Target.js'; + +describe('Target', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('Browser.targets should return all of the targets', async () => { + const { browser } = getTestState(); + + // The pages will be the testing page and the original newtab page + const targets = browser.targets(); + expect( + targets.some( + (target) => target.type() === 'page' && target.url() === 'about:blank' + ) + ).toBeTruthy(); + expect(targets.some((target) => target.type() === 'browser')).toBeTruthy(); + }); + it('Browser.pages should return all of the pages', async () => { + const { page, context } = getTestState(); + + // The pages will be the testing page + const allPages = await context.pages(); + expect(allPages.length).toBe(1); + expect(allPages).toContain(page); + expect(allPages[0]).not.toBe(allPages[1]); + }); + it('should contain browser target', async () => { + const { browser } = getTestState(); + + const targets = browser.targets(); + const browserTarget = targets.find((target) => target.type() === 'browser'); + expect(browserTarget).toBeTruthy(); + }); + it('should be able to use the default page in the browser', async () => { + const { page, browser } = getTestState(); + + // The pages will be the testing page and the original newtab page + const allPages = await browser.pages(); + const originalPage = allPages.find((p) => p !== page); + expect( + await originalPage.evaluate(() => ['Hello', 'world'].join(' ')) + ).toBe('Hello world'); + expect(await originalPage.$('body')).toBeTruthy(); + }); + it( + 'should report when a new page is created and closed', + async () => { + const { page, server, context } = getTestState(); + + const [otherPage] = await Promise.all([ + context + .waitForTarget( + (target) => + target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html' + ) + .then((target) => target.page()), + page.evaluate( + (url: string) => window.open(url), + server.CROSS_PROCESS_PREFIX + '/empty.html' + ), + ]); + expect(otherPage.url()).toContain(server.CROSS_PROCESS_PREFIX); + expect(await otherPage.evaluate(() => ['Hello', 'world'].join(' '))).toBe( + 'Hello world' + ); + expect(await otherPage.$('body')).toBeTruthy(); + + let allPages = await context.pages(); + expect(allPages).toContain(page); + expect(allPages).toContain(otherPage); + + const closePagePromise = new Promise((fulfill) => + context.once('targetdestroyed', (target) => fulfill(target.page())) + ); + await otherPage.close(); + expect(await closePagePromise).toBe(otherPage); + + allPages = await Promise.all( + context.targets().map((target) => target.page()) + ); + expect(allPages).toContain(page); + expect(allPages).not.toContain(otherPage); + } + ); + it( + 'should report when a service worker is created and destroyed', + async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const createdTarget = new Promise<Target>((fulfill) => + context.once('targetcreated', (target) => fulfill(target)) + ); + + await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'); + + expect((await createdTarget).type()).toBe('service_worker'); + expect((await createdTarget).url()).toBe( + server.PREFIX + '/serviceworkers/empty/sw.js' + ); + + const destroyedTarget = new Promise((fulfill) => + context.once('targetdestroyed', (target) => fulfill(target)) + ); + await page.evaluate(() => + globalThis.registrationPromise.then((registration) => + registration.unregister() + ) + ); + expect(await destroyedTarget).toBe(await createdTarget); + } + ); + it('should create a worker from a service worker', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'); + + const target = await context.waitForTarget( + (target) => target.type() === 'service_worker' + ); + const worker = await target.worker(); + expect(await worker.evaluate(() => self.toString())).toBe( + '[object ServiceWorkerGlobalScope]' + ); + }); + it('should create a worker from a shared worker', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + new SharedWorker('data:text/javascript,console.log("hi")'); + }); + const target = await context.waitForTarget( + (target) => target.type() === 'shared_worker' + ); + const worker = await target.worker(); + expect(await worker.evaluate(() => self.toString())).toBe( + '[object SharedWorkerGlobalScope]' + ); + }); + it('should report when a target url changes', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let changedTarget = new Promise<Target>((fulfill) => + context.once('targetchanged', (target) => fulfill(target)) + ); + await page.goto(server.CROSS_PROCESS_PREFIX + '/'); + expect((await changedTarget).url()).toBe(server.CROSS_PROCESS_PREFIX + '/'); + + changedTarget = new Promise((fulfill) => + context.once('targetchanged', (target) => fulfill(target)) + ); + await page.goto(server.EMPTY_PAGE); + expect((await changedTarget).url()).toBe(server.EMPTY_PAGE); + }); + it('should not report uninitialized pages', async () => { + const { context } = getTestState(); + + let targetChanged = false; + const listener = () => (targetChanged = true); + context.on('targetchanged', listener); + const targetPromise = new Promise<Target>((fulfill) => + context.once('targetcreated', (target) => fulfill(target)) + ); + const newPagePromise = context.newPage(); + const target = await targetPromise; + expect(target.url()).toBe('about:blank'); + + const newPage = await newPagePromise; + const targetPromise2 = new Promise<Target>((fulfill) => + context.once('targetcreated', (target) => fulfill(target)) + ); + const evaluatePromise = newPage.evaluate(() => window.open('about:blank')); + const target2 = await targetPromise2; + expect(target2.url()).toBe('about:blank'); + await evaluatePromise; + await newPage.close(); + expect(targetChanged).toBe(false); + context.removeListener('targetchanged', listener); + }); + it( + 'should not crash while redirecting if original request was missed', + async () => { + const { page, server, context } = getTestState(); + + let serverResponse = null; + server.setRoute('/one-style.css', (req, res) => (serverResponse = res)); + // Open a new page. Use window.open to connect to the page later. + await Promise.all([ + page.evaluate( + (url: string) => window.open(url), + server.PREFIX + '/one-style.html' + ), + server.waitForRequest('/one-style.css'), + ]); + // Connect to the opened page. + const target = await context.waitForTarget((target) => + target.url().includes('one-style.html') + ); + const newPage = await target.page(); + // Issue a redirect. + serverResponse.writeHead(302, { location: '/injectedstyle.css' }); + serverResponse.end(); + // Wait for the new page to load. + await waitEvent(newPage, 'load'); + // Cleanup. + await newPage.close(); + } + ); + it('should have an opener', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [createdTarget] = await Promise.all([ + new Promise<Target>((fulfill) => + context.once('targetcreated', (target) => fulfill(target)) + ), + page.goto(server.PREFIX + '/popup/window-open.html'), + ]); + expect((await createdTarget.page()).url()).toBe( + server.PREFIX + '/popup/popup.html' + ); + expect(createdTarget.opener()).toBe(page.target()); + expect(page.target().opener()).toBe(null); + }); + + describe('Browser.waitForTarget', () => { + it('should wait for a target', async () => { + const { browser, puppeteer, server } = getTestState(); + + let resolved = false; + const targetPromise = browser.waitForTarget( + (target) => target.url() === server.EMPTY_PAGE + ); + targetPromise + .then(() => (resolved = true)) + .catch((error) => { + resolved = true; + if (error instanceof puppeteer.errors.TimeoutError) { + console.error(error); + } else throw error; + }); + const page = await browser.newPage(); + expect(resolved).toBe(false); + await page.goto(server.EMPTY_PAGE); + try { + const target = await targetPromise; + expect(await target.page()).toBe(page); + } catch (error) { + if (error instanceof puppeteer.errors.TimeoutError) { + console.error(error); + } else throw error; + } + await page.close(); + }); + it('should timeout waiting for a non-existent target', async () => { + const { browser, server, puppeteer } = getTestState(); + + let error = null; + await browser + .waitForTarget((target) => target.url() === server.EMPTY_PAGE, { + timeout: 1, + }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + }); +}); diff --git a/remote/test/puppeteer/test/touchscreen.spec.ts b/remote/test/puppeteer/test/touchscreen.spec.ts new file mode 100644 index 0000000000..b7fc67bfa9 --- /dev/null +++ b/remote/test/puppeteer/test/touchscreen.spec.ts @@ -0,0 +1,48 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Touchscreen', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should tap the button', async () => { + const { puppeteer, page, server } = getTestState(); + const iPhone = puppeteer.devices['iPhone 6']; + await page.emulate(iPhone); + await page.goto(server.PREFIX + '/input/button.html'); + await page.tap('button'); + expect(await page.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + it('should report touches', async () => { + const { puppeteer, page, server } = getTestState(); + const iPhone = puppeteer.devices['iPhone 6']; + await page.emulate(iPhone); + await page.goto(server.PREFIX + '/input/touches.html'); + const button = await page.$('button'); + await button.tap(); + expect(await page.evaluate(() => globalThis.getResult())).toEqual([ + 'Touchstart: 0', + 'Touchend: 0', + ]); + }); +}); diff --git a/remote/test/puppeteer/test/tracing.spec.ts b/remote/test/puppeteer/test/tracing.spec.ts new file mode 100644 index 0000000000..5e06f12b4c --- /dev/null +++ b/remote/test/puppeteer/test/tracing.spec.ts @@ -0,0 +1,133 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; +import expect from 'expect'; +import { getTestState, describeChromeOnly } from './mocha-utils'; // eslint-disable-line import/extensions + +describeChromeOnly('Tracing', function () { + let outputFile; + let browser; + let page; + + /* we manually manage the browser here as we want a new browser for each + * individual test, which isn't the default behaviour of getTestState() + */ + + beforeEach(async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + browser = await puppeteer.launch(defaultBrowserOptions); + page = await browser.newPage(); + outputFile = path.join(__dirname, 'assets', 'trace.json'); + }); + + afterEach(async () => { + await browser.close(); + browser = null; + page = null; + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + outputFile = null; + } + }); + it('should output a trace', async () => { + const { server } = getTestState(); + + await page.tracing.start({ screenshots: true, path: outputFile }); + await page.goto(server.PREFIX + '/grid.html'); + await page.tracing.stop(); + expect(fs.existsSync(outputFile)).toBe(true); + }); + + it('should run with custom categories if provided', async () => { + await page.tracing.start({ + path: outputFile, + categories: ['disabled-by-default-v8.cpu_profiler.hires'], + }); + await page.tracing.stop(); + + const traceJson = JSON.parse( + fs.readFileSync(outputFile, { encoding: 'utf8' }) + ); + expect(traceJson.metadata['trace-config']).toContain( + 'disabled-by-default-v8.cpu_profiler.hires' + ); + }); + it('should throw if tracing on two pages', async () => { + await page.tracing.start({ path: outputFile }); + const newPage = await browser.newPage(); + let error = null; + await newPage.tracing + .start({ path: outputFile }) + .catch((error_) => (error = error_)); + await newPage.close(); + expect(error).toBeTruthy(); + await page.tracing.stop(); + }); + it('should return a buffer', async () => { + const { server } = getTestState(); + + await page.tracing.start({ screenshots: true, path: outputFile }); + await page.goto(server.PREFIX + '/grid.html'); + const trace = await page.tracing.stop(); + const buf = fs.readFileSync(outputFile); + expect(trace.toString()).toEqual(buf.toString()); + }); + it('should work without options', async () => { + const { server } = getTestState(); + + await page.tracing.start(); + await page.goto(server.PREFIX + '/grid.html'); + const trace = await page.tracing.stop(); + expect(trace).toBeTruthy(); + }); + + it('should return null in case of Buffer error', async () => { + const { server } = getTestState(); + + await page.tracing.start({ screenshots: true }); + await page.goto(server.PREFIX + '/grid.html'); + const oldBufferConcat = Buffer.concat; + Buffer.concat = () => { + throw 'error'; + }; + const trace = await page.tracing.stop(); + expect(trace).toEqual(null); + Buffer.concat = oldBufferConcat; + }); + + it('should support a buffer without a path', async () => { + const { server } = getTestState(); + + await page.tracing.start({ screenshots: true }); + await page.goto(server.PREFIX + '/grid.html'); + const trace = await page.tracing.stop(); + expect(trace.toString()).toContain('screenshot'); + }); + + it('should properly fail if readProtocolStream errors out', async () => { + await page.tracing.start({ path: __dirname }); + + let error: Error = null; + try { + await page.tracing.stop(); + } catch (error_) { + error = error_; + } + expect(error).toBeDefined(); + }); +}); diff --git a/remote/test/puppeteer/test/tsconfig.json b/remote/test/puppeteer/test/tsconfig.json new file mode 100644 index 0000000000..8b1f1e866c --- /dev/null +++ b/remote/test/puppeteer/test/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["*.ts", "*.js"] +} diff --git a/remote/test/puppeteer/test/tsconfig.test.json b/remote/test/puppeteer/test/tsconfig.test.json new file mode 100644 index 0000000000..3432441200 --- /dev/null +++ b/remote/test/puppeteer/test/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS" + } +} diff --git a/remote/test/puppeteer/test/utils.js b/remote/test/puppeteer/test/utils.js new file mode 100644 index 0000000000..935b44d98e --- /dev/null +++ b/remote/test/puppeteer/test/utils.js @@ -0,0 +1,135 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO (@jackfranklin): convert to TS and enable type checking. + +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); +const expect = require('expect'); +const GoldenUtils = require('./golden-utils'); +const PROJECT_ROOT = fs.existsSync(path.join(__dirname, '..', 'package.json')) + ? path.join(__dirname, '..') + : path.join(__dirname, '..', '..'); + +const utils = (module.exports = { + extendExpectWithToBeGolden: function (goldenDir, outputDir) { + expect.extend({ + toBeGolden: (testScreenshot, goldenFilePath) => { + const result = GoldenUtils.compare( + goldenDir, + outputDir, + testScreenshot, + goldenFilePath + ); + + return { + message: () => result.message, + pass: result.pass, + }; + }, + }); + }, + + /** + * @returns {string} + */ + projectRoot: function () { + return PROJECT_ROOT; + }, + + /** + * @param {!Page} page + * @param {string} frameId + * @param {string} url + * @returns {!Frame} + */ + attachFrame: async function (page, frameId, url) { + const handle = await page.evaluateHandle(attachFrame, frameId, url); + return await handle.asElement().contentFrame(); + + async function attachFrame(frameId, url) { + const frame = document.createElement('iframe'); + frame.src = url; + frame.id = frameId; + document.body.appendChild(frame); + await new Promise((x) => (frame.onload = x)); + return frame; + } + }, + + isFavicon: function (request) { + return request.url().includes('favicon.ico'); + }, + + /** + * @param {!Page} page + * @param {string} frameId + */ + detachFrame: async function (page, frameId) { + await page.evaluate(detachFrame, frameId); + + function detachFrame(frameId) { + const frame = document.getElementById(frameId); + frame.remove(); + } + }, + + /** + * @param {!Page} page + * @param {string} frameId + * @param {string} url + */ + navigateFrame: async function (page, frameId, url) { + await page.evaluate(navigateFrame, frameId, url); + + function navigateFrame(frameId, url) { + const frame = document.getElementById(frameId); + frame.src = url; + return new Promise((x) => (frame.onload = x)); + } + }, + + /** + * @param {!Frame} frame + * @param {string=} indentation + * @returns {Array<string>} + */ + dumpFrames: function (frame, indentation) { + indentation = indentation || ''; + let description = frame.url().replace(/:\d{4}\//, ':<PORT>/'); + if (frame.name()) description += ' (' + frame.name() + ')'; + const result = [indentation + description]; + for (const child of frame.childFrames()) + result.push(...utils.dumpFrames(child, ' ' + indentation)); + return result; + }, + + /** + * @param {!EventEmitter} emitter + * @param {string} eventName + * @returns {!Promise<!Object>} + */ + waitEvent: function (emitter, eventName, predicate = () => true) { + return new Promise((fulfill) => { + emitter.on(eventName, function listener(event) { + if (!predicate(event)) return; + emitter.removeListener(eventName, listener); + fulfill(event); + }); + }); + }, +}); diff --git a/remote/test/puppeteer/test/waittask.spec.ts b/remote/test/puppeteer/test/waittask.spec.ts new file mode 100644 index 0000000000..c65b682674 --- /dev/null +++ b/remote/test/puppeteer/test/waittask.spec.ts @@ -0,0 +1,774 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import utils from './utils.js'; +import sinon from 'sinon'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('waittask specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.waitFor', function () { + /* This method is deprecated but we don't want the warnings showing up in + * tests. Until we remove this method we still want to ensure we don't break + * it. + */ + beforeEach(() => sinon.stub(console, 'warn').callsFake(() => {})); + + it('should wait for selector', async () => { + const { page, server } = getTestState(); + + let found = false; + const waitFor = page.waitFor('div').then(() => (found = true)); + await page.goto(server.EMPTY_PAGE); + expect(found).toBe(false); + await page.goto(server.PREFIX + '/grid.html'); + await waitFor; + expect(found).toBe(true); + }); + + it('should wait for an xpath', async () => { + const { page, server } = getTestState(); + + let found = false; + const waitFor = page.waitFor('//div').then(() => (found = true)); + await page.goto(server.EMPTY_PAGE); + expect(found).toBe(false); + await page.goto(server.PREFIX + '/grid.html'); + await waitFor; + expect(found).toBe(true); + }); + it('should not allow you to select an element with single slash xpath', async () => { + const { page } = getTestState(); + + await page.setContent(`<div>some text</div>`); + let error = null; + await page.waitFor('/html/body/div').catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + }); + it('should timeout', async () => { + const { page } = getTestState(); + + const startTime = Date.now(); + const timeout = 42; + await page.waitFor(timeout); + expect(Date.now() - startTime).not.toBeLessThan(timeout / 2); + }); + it('should work with multiline body', async () => { + const { page } = getTestState(); + + const result = await page.waitForFunction(` + (() => true)() + `); + expect(await result.jsonValue()).toBe(true); + }); + it('should wait for predicate', async () => { + const { page } = getTestState(); + + await Promise.all([ + page.waitFor(() => window.innerWidth < 100), + page.setViewport({ width: 10, height: 10 }), + ]); + }); + it('should throw when unknown type', async () => { + const { page } = getTestState(); + + let error = null; + // @ts-expect-error purposefully passing bad type for test + await page.waitFor({ foo: 'bar' }).catch((error_) => (error = error_)); + expect(error.message).toContain('Unsupported target type'); + }); + it('should wait for predicate with arguments', async () => { + const { page } = getTestState(); + + await page.waitFor((arg1, arg2) => arg1 !== arg2, {}, 1, 2); + }); + + it('should log a deprecation warning', async () => { + const { page } = getTestState(); + + await page.waitFor(() => true); + + const consoleWarnStub = console.warn as sinon.SinonSpy; + + expect(consoleWarnStub.calledOnce).toBe(true); + expect( + consoleWarnStub.firstCall.calledWith( + 'waitFor is deprecated and will be removed in a future release. See https://github.com/puppeteer/puppeteer/issues/6214 for details and how to migrate your code.' + ) + ).toBe(true); + expect((console.warn as sinon.SinonSpy).calledOnce).toBe(true); + }); + }); + + describe('Frame.waitForFunction', function () { + it('should accept a string', async () => { + const { page } = getTestState(); + + const watchdog = page.waitForFunction('window.__FOO === 1'); + await page.evaluate(() => (globalThis.__FOO = 1)); + await watchdog; + }); + it('should work when resolved right before execution context disposal', async () => { + const { page } = getTestState(); + + await page.evaluateOnNewDocument(() => (globalThis.__RELOADED = true)); + await page.waitForFunction(() => { + if (!globalThis.__RELOADED) window.location.reload(); + return true; + }); + }); + it('should poll on interval', async () => { + const { page } = getTestState(); + + let success = false; + const startTime = Date.now(); + const polling = 100; + const watchdog = page + .waitForFunction(() => globalThis.__FOO === 'hit', { polling }) + .then(() => (success = true)); + await page.evaluate(() => (globalThis.__FOO = 'hit')); + expect(success).toBe(false); + await page.evaluate(() => + document.body.appendChild(document.createElement('div')) + ); + await watchdog; + expect(Date.now() - startTime).not.toBeLessThan(polling / 2); + }); + it('should poll on interval async', async () => { + const { page } = getTestState(); + let success = false; + const startTime = Date.now(); + const polling = 100; + const watchdog = page + .waitForFunction(async () => globalThis.__FOO === 'hit', { polling }) + .then(() => (success = true)); + await page.evaluate(async () => (globalThis.__FOO = 'hit')); + expect(success).toBe(false); + await page.evaluate(async () => + document.body.appendChild(document.createElement('div')) + ); + await watchdog; + expect(Date.now() - startTime).not.toBeLessThan(polling / 2); + }); + it('should poll on mutation', async () => { + const { page } = getTestState(); + + let success = false; + const watchdog = page + .waitForFunction(() => globalThis.__FOO === 'hit', { + polling: 'mutation', + }) + .then(() => (success = true)); + await page.evaluate(() => (globalThis.__FOO = 'hit')); + expect(success).toBe(false); + await page.evaluate(() => + document.body.appendChild(document.createElement('div')) + ); + await watchdog; + }); + it('should poll on mutation async', async () => { + const { page } = getTestState(); + + let success = false; + const watchdog = page + .waitForFunction(async () => globalThis.__FOO === 'hit', { + polling: 'mutation', + }) + .then(() => (success = true)); + await page.evaluate(async () => (globalThis.__FOO = 'hit')); + expect(success).toBe(false); + await page.evaluate(async () => + document.body.appendChild(document.createElement('div')) + ); + await watchdog; + }); + it('should poll on raf', async () => { + const { page } = getTestState(); + + const watchdog = page.waitForFunction(() => globalThis.__FOO === 'hit', { + polling: 'raf', + }); + await page.evaluate(() => (globalThis.__FOO = 'hit')); + await watchdog; + }); + it('should poll on raf async', async () => { + const { page } = getTestState(); + + const watchdog = page.waitForFunction( + async () => globalThis.__FOO === 'hit', + { + polling: 'raf', + } + ); + await page.evaluate(async () => (globalThis.__FOO = 'hit')); + await watchdog; + }); + it('should work with strict CSP policy', async () => { + const { page, server } = getTestState(); + + server.setCSP('/empty.html', 'script-src ' + server.PREFIX); + await page.goto(server.EMPTY_PAGE); + let error = null; + await Promise.all([ + page + .waitForFunction(() => globalThis.__FOO === 'hit', { polling: 'raf' }) + .catch((error_) => (error = error_)), + page.evaluate(() => (globalThis.__FOO = 'hit')), + ]); + expect(error).toBe(null); + }); + it('should throw on bad polling value', async () => { + const { page } = getTestState(); + + let error = null; + try { + await page.waitForFunction(() => !!document.body, { + polling: 'unknown', + }); + } catch (error_) { + error = error_; + } + expect(error).toBeTruthy(); + expect(error.message).toContain('polling'); + }); + it('should throw negative polling interval', async () => { + const { page } = getTestState(); + + let error = null; + try { + await page.waitForFunction(() => !!document.body, { polling: -10 }); + } catch (error_) { + error = error_; + } + expect(error).toBeTruthy(); + expect(error.message).toContain('Cannot poll with non-positive interval'); + }); + it('should return the success value as a JSHandle', async () => { + const { page } = getTestState(); + + expect(await (await page.waitForFunction(() => 5)).jsonValue()).toBe(5); + }); + it('should return the window as a success value', async () => { + const { page } = getTestState(); + + expect(await page.waitForFunction(() => window)).toBeTruthy(); + }); + it('should accept ElementHandle arguments', async () => { + const { page } = getTestState(); + + await page.setContent('<div></div>'); + const div = await page.$('div'); + let resolved = false; + const waitForFunction = page + .waitForFunction((element) => !element.parentElement, {}, div) + .then(() => (resolved = true)); + expect(resolved).toBe(false); + await page.evaluate((element: HTMLElement) => element.remove(), div); + await waitForFunction; + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + await page + .waitForFunction('false', { timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain('waiting for function failed: timeout'); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default timeout', async () => { + const { page, puppeteer } = getTestState(); + + page.setDefaultTimeout(1); + let error = null; + await page.waitForFunction('false').catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + expect(error.message).toContain('waiting for function failed: timeout'); + }); + it('should disable timeout when its set to 0', async () => { + const { page } = getTestState(); + + const watchdog = page.waitForFunction( + () => { + globalThis.__counter = (globalThis.__counter || 0) + 1; + return globalThis.__injected; + }, + { timeout: 0, polling: 10 } + ); + await page.waitForFunction(() => globalThis.__counter > 10); + await page.evaluate(() => (globalThis.__injected = true)); + await watchdog; + }); + it('should survive cross-process navigation', async () => { + const { page, server } = getTestState(); + + let fooFound = false; + const waitForFunction = page + .waitForFunction('globalThis.__FOO === 1') + .then(() => (fooFound = true)); + await page.goto(server.EMPTY_PAGE); + expect(fooFound).toBe(false); + await page.reload(); + expect(fooFound).toBe(false); + await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html'); + expect(fooFound).toBe(false); + await page.evaluate(() => (globalThis.__FOO = 1)); + await waitForFunction; + expect(fooFound).toBe(true); + }); + it('should survive navigations', async () => { + const { page, server } = getTestState(); + + const watchdog = page.waitForFunction(() => globalThis.__done); + await page.goto(server.EMPTY_PAGE); + await page.goto(server.PREFIX + '/consolelog.html'); + await page.evaluate(() => (globalThis.__done = true)); + await watchdog; + }); + }); + + describe('Page.waitForTimeout', () => { + it('waits for the given timeout before resolving', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + const startTime = Date.now(); + await page.waitForTimeout(1000); + const endTime = Date.now(); + /* In a perfect world endTime - startTime would be exactly 1000 but we + * expect some fluctuations and for it to be off by a little bit. So to + * avoid a flaky test we'll make sure it waited for roughly 1 second by + * ensuring 900 < endTime - startTime < 1100 + */ + expect(endTime - startTime).toBeGreaterThan(900); + expect(endTime - startTime).toBeLessThan(1100); + }); + }); + + describe('Frame.waitForTimeout', () => { + it('waits for the given timeout before resolving', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + const frame = page.mainFrame(); + const startTime = Date.now(); + await frame.waitForTimeout(1000); + const endTime = Date.now(); + /* In a perfect world endTime - startTime would be exactly 1000 but we + * expect some fluctuations and for it to be off by a little bit. So to + * avoid a flaky test we'll make sure it waited for roughly 1 second by + * ensuring 900 < endTime - startTime < 1100 + */ + expect(endTime - startTime).toBeGreaterThan(900); + expect(endTime - startTime).toBeLessThan(1100); + }); + }); + + describe('Frame.waitForSelector', function () { + const addElement = (tag) => + document.body.appendChild(document.createElement(tag)); + + it('should immediately resolve promise if node exists', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const frame = page.mainFrame(); + await frame.waitForSelector('*'); + await frame.evaluate(addElement, 'div'); + await frame.waitForSelector('div'); + }); + + it('should work with removed MutationObserver', async () => { + const { page } = getTestState(); + + await page.evaluate(() => delete window.MutationObserver); + const [handle] = await Promise.all([ + page.waitForSelector('.zombo'), + page.setContent(`<div class='zombo'>anything</div>`), + ]); + expect( + await page.evaluate((x: HTMLElement) => x.textContent, handle) + ).toBe('anything'); + }); + + it('should resolve promise when node is added', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const frame = page.mainFrame(); + const watchdog = frame.waitForSelector('div'); + await frame.evaluate(addElement, 'br'); + await frame.evaluate(addElement, 'div'); + const eHandle = await watchdog; + const tagName = await eHandle + .getProperty('tagName') + .then((e) => e.jsonValue()); + expect(tagName).toBe('DIV'); + }); + + it('should work when node is added through innerHTML', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const watchdog = page.waitForSelector('h3 div'); + await page.evaluate(addElement, 'span'); + await page.evaluate( + () => + (document.querySelector('span').innerHTML = '<h3><div></div></h3>') + ); + await watchdog; + }); + + it( + 'Page.waitForSelector is shortcut for main frame', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const otherFrame = page.frames()[1]; + const watchdog = page.waitForSelector('div'); + await otherFrame.evaluate(addElement, 'div'); + await page.evaluate(addElement, 'div'); + const eHandle = await watchdog; + expect(eHandle.executionContext().frame()).toBe(page.mainFrame()); + } + ); + + it('should run in specified frame', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE); + const frame1 = page.frames()[1]; + const frame2 = page.frames()[2]; + const waitForSelectorPromise = frame2.waitForSelector('div'); + await frame1.evaluate(addElement, 'div'); + await frame2.evaluate(addElement, 'div'); + const eHandle = await waitForSelectorPromise; + expect(eHandle.executionContext().frame()).toBe(frame2); + }); + + it('should throw when frame is detached', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const frame = page.frames()[1]; + let waitError = null; + const waitPromise = frame + .waitForSelector('.box') + .catch((error) => (waitError = error)); + await utils.detachFrame(page, 'frame1'); + await waitPromise; + expect(waitError).toBeTruthy(); + expect(waitError.message).toContain( + 'waitForFunction failed: frame got detached.' + ); + }); + it('should survive cross-process navigation', async () => { + const { page, server } = getTestState(); + + let boxFound = false; + const waitForSelector = page + .waitForSelector('.box') + .then(() => (boxFound = true)); + await page.goto(server.EMPTY_PAGE); + expect(boxFound).toBe(false); + await page.reload(); + expect(boxFound).toBe(false); + await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html'); + await waitForSelector; + expect(boxFound).toBe(true); + }); + it('should wait for visible', async () => { + const { page } = getTestState(); + + let divFound = false; + const waitForSelector = page + .waitForSelector('div', { visible: true }) + .then(() => (divFound = true)); + await page.setContent( + `<div style='display: none; visibility: hidden;'>1</div>` + ); + expect(divFound).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.removeProperty('display') + ); + expect(divFound).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.removeProperty('visibility') + ); + expect(await waitForSelector).toBe(true); + expect(divFound).toBe(true); + }); + it('should wait for visible recursively', async () => { + const { page } = getTestState(); + + let divVisible = false; + const waitForSelector = page + .waitForSelector('div#inner', { visible: true }) + .then(() => (divVisible = true)); + await page.setContent( + `<div style='display: none; visibility: hidden;'><div id="inner">hi</div></div>` + ); + expect(divVisible).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.removeProperty('display') + ); + expect(divVisible).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.removeProperty('visibility') + ); + expect(await waitForSelector).toBe(true); + expect(divVisible).toBe(true); + }); + it('hidden should wait for visibility: hidden', async () => { + const { page } = getTestState(); + + let divHidden = false; + await page.setContent(`<div style='display: block;'></div>`); + const waitForSelector = page + .waitForSelector('div', { hidden: true }) + .then(() => (divHidden = true)); + await page.waitForSelector('div'); // do a round trip + expect(divHidden).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.setProperty('visibility', 'hidden') + ); + expect(await waitForSelector).toBe(true); + expect(divHidden).toBe(true); + }); + it('hidden should wait for display: none', async () => { + const { page } = getTestState(); + + let divHidden = false; + await page.setContent(`<div style='display: block;'></div>`); + const waitForSelector = page + .waitForSelector('div', { hidden: true }) + .then(() => (divHidden = true)); + await page.waitForSelector('div'); // do a round trip + expect(divHidden).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.setProperty('display', 'none') + ); + expect(await waitForSelector).toBe(true); + expect(divHidden).toBe(true); + }); + it('hidden should wait for removal', async () => { + const { page } = getTestState(); + + await page.setContent(`<div></div>`); + let divRemoved = false; + const waitForSelector = page + .waitForSelector('div', { hidden: true }) + .then(() => (divRemoved = true)); + await page.waitForSelector('div'); // do a round trip + expect(divRemoved).toBe(false); + await page.evaluate(() => document.querySelector('div').remove()); + expect(await waitForSelector).toBe(true); + expect(divRemoved).toBe(true); + }); + it('should return null if waiting to hide non-existing element', async () => { + const { page } = getTestState(); + + const handle = await page.waitForSelector('non-existing', { + hidden: true, + }); + expect(handle).toBe(null); + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + await page + .waitForSelector('div', { timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'waiting for selector `div` failed: timeout' + ); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should have an error message specifically for awaiting an element to be hidden', async () => { + const { page } = getTestState(); + + await page.setContent(`<div></div>`); + let error = null; + await page + .waitForSelector('div', { hidden: true, timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'waiting for selector `div` to be hidden failed: timeout' + ); + }); + + it('should respond to node attribute mutation', async () => { + const { page } = getTestState(); + + let divFound = false; + const waitForSelector = page + .waitForSelector('.zombo') + .then(() => (divFound = true)); + await page.setContent(`<div class='notZombo'></div>`); + expect(divFound).toBe(false); + await page.evaluate( + () => (document.querySelector('div').className = 'zombo') + ); + expect(await waitForSelector).toBe(true); + }); + it('should return the element handle', async () => { + const { page } = getTestState(); + + const waitForSelector = page.waitForSelector('.zombo'); + await page.setContent(`<div class='zombo'>anything</div>`); + expect( + await page.evaluate( + (x: HTMLElement) => x.textContent, + await waitForSelector + ) + ).toBe('anything'); + }); + it('should have correct stack trace for timeout', async () => { + const { page } = getTestState(); + + let error; + await page + .waitForSelector('.zombo', { timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error.stack).toContain('waiting for selector `.zombo` failed'); + // The extension is ts here as Mocha maps back via sourcemaps. + expect(error.stack).toContain('waittask.spec.ts'); + }); + }); + + describe('Frame.waitForXPath', function () { + const addElement = (tag) => + document.body.appendChild(document.createElement(tag)); + + it('should support some fancy xpath', async () => { + const { page } = getTestState(); + + await page.setContent(`<p>red herring</p><p>hello world </p>`); + const waitForXPath = page.waitForXPath( + '//p[normalize-space(.)="hello world"]' + ); + expect( + await page.evaluate( + (x: HTMLElement) => x.textContent, + await waitForXPath + ) + ).toBe('hello world '); + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + await page + .waitForXPath('//div', { timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'waiting for XPath `//div` failed: timeout' + ); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should run in specified frame', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE); + const frame1 = page.frames()[1]; + const frame2 = page.frames()[2]; + const waitForXPathPromise = frame2.waitForXPath('//div'); + await frame1.evaluate(addElement, 'div'); + await frame2.evaluate(addElement, 'div'); + const eHandle = await waitForXPathPromise; + expect(eHandle.executionContext().frame()).toBe(frame2); + }); + it('should throw when frame is detached', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const frame = page.frames()[1]; + let waitError = null; + const waitPromise = frame + .waitForXPath('//*[@class="box"]') + .catch((error) => (waitError = error)); + await utils.detachFrame(page, 'frame1'); + await waitPromise; + expect(waitError).toBeTruthy(); + expect(waitError.message).toContain( + 'waitForFunction failed: frame got detached.' + ); + }); + it('hidden should wait for display: none', async () => { + const { page } = getTestState(); + + let divHidden = false; + await page.setContent(`<div style='display: block;'></div>`); + const waitForXPath = page + .waitForXPath('//div', { hidden: true }) + .then(() => (divHidden = true)); + await page.waitForXPath('//div'); // do a round trip + expect(divHidden).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.setProperty('display', 'none') + ); + expect(await waitForXPath).toBe(true); + expect(divHidden).toBe(true); + }); + it('should return the element handle', async () => { + const { page } = getTestState(); + + const waitForXPath = page.waitForXPath('//*[@class="zombo"]'); + await page.setContent(`<div class='zombo'>anything</div>`); + expect( + await page.evaluate( + (x: HTMLElement) => x.textContent, + await waitForXPath + ) + ).toBe('anything'); + }); + it('should allow you to select a text node', async () => { + const { page } = getTestState(); + + await page.setContent(`<div>some text</div>`); + const text = await page.waitForXPath('//div/text()'); + expect(await (await text.getProperty('nodeType')).jsonValue()).toBe( + 3 /* Node.TEXT_NODE */ + ); + }); + it('should allow you to select an element with single slash', async () => { + const { page } = getTestState(); + + await page.setContent(`<div>some text</div>`); + const waitForXPath = page.waitForXPath('/html/body/div'); + expect( + await page.evaluate( + (x: HTMLElement) => x.textContent, + await waitForXPath + ) + ).toBe('some text'); + }); + }); +}); diff --git a/remote/test/puppeteer/test/worker.spec.ts b/remote/test/puppeteer/test/worker.spec.ts new file mode 100644 index 0000000000..2c5827361b --- /dev/null +++ b/remote/test/puppeteer/test/worker.spec.ts @@ -0,0 +1,126 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import utils from './utils.js'; +import { WebWorker } from '../lib/cjs/puppeteer/common/WebWorker.js'; +import { ConsoleMessage } from '../lib/cjs/puppeteer/common/ConsoleMessage.js'; +const { waitEvent } = utils; + +describeFailsFirefox('Workers', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('Page.workers', async () => { + const { page, server } = getTestState(); + + await Promise.all([ + new Promise((x) => page.once('workercreated', x)), + page.goto(server.PREFIX + '/worker/worker.html'), + ]); + const worker = page.workers()[0]; + expect(worker.url()).toContain('worker.js'); + + expect(await worker.evaluate(() => globalThis.workerFunction())).toBe( + 'worker function result' + ); + + await page.goto(server.EMPTY_PAGE); + expect(page.workers().length).toBe(0); + }); + it('should emit created and destroyed events', async () => { + const { page } = getTestState(); + + const workerCreatedPromise = new Promise<WebWorker>((x) => + page.once('workercreated', x) + ); + const workerObj = await page.evaluateHandle( + () => new Worker('data:text/javascript,1') + ); + const worker = await workerCreatedPromise; + const workerThisObj = await worker.evaluateHandle(() => this); + const workerDestroyedPromise = new Promise((x) => + page.once('workerdestroyed', x) + ); + await page.evaluate( + (workerObj: Worker) => workerObj.terminate(), + workerObj + ); + expect(await workerDestroyedPromise).toBe(worker); + const error = await workerThisObj + .getProperty('self') + .catch((error) => error); + expect(error.message).toContain('Most likely the worker has been closed.'); + }); + it('should report console logs', async () => { + const { page } = getTestState(); + + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.evaluate(() => new Worker(`data:text/javascript,console.log(1)`)), + ]); + expect(message.text()).toBe('1'); + expect(message.location()).toEqual({ + url: 'data:text/javascript,console.log(1)', + lineNumber: 0, + columnNumber: 8, + }); + }); + it('should have JSHandles for console logs', async () => { + const { page } = getTestState(); + + const logPromise = new Promise<ConsoleMessage>((x) => + page.on('console', x) + ); + await page.evaluate( + () => new Worker(`data:text/javascript,console.log(1,2,3,this)`) + ); + const log = await logPromise; + expect(log.text()).toBe('1 2 3 JSHandle@object'); + expect(log.args().length).toBe(4); + expect(await (await log.args()[3].getProperty('origin')).jsonValue()).toBe( + 'null' + ); + }); + it('should have an execution context', async () => { + const { page } = getTestState(); + + const workerCreatedPromise = new Promise<WebWorker>((x) => + page.once('workercreated', x) + ); + await page.evaluate( + () => new Worker(`data:text/javascript,console.log(1)`) + ); + const worker = await workerCreatedPromise; + expect(await (await worker.executionContext()).evaluate('1+1')).toBe(2); + }); + it('should report errors', async () => { + const { page } = getTestState(); + + const errorPromise = new Promise<Error>((x) => page.on('pageerror', x)); + await page.evaluate( + () => + new Worker(`data:text/javascript, throw new Error('this is my error');`) + ); + const errorLog = await errorPromise; + expect(errorLog.message).toContain('this is my error'); + }); +}); diff --git a/remote/test/puppeteer/tsconfig-esm.json b/remote/test/puppeteer/tsconfig-esm.json new file mode 100644 index 0000000000..06d8e25d98 --- /dev/null +++ b/remote/test/puppeteer/tsconfig-esm.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./lib/esm", + "module": "ES2015", + }, +} diff --git a/remote/test/puppeteer/tsconfig.base.json b/remote/test/puppeteer/tsconfig.base.json new file mode 100644 index 0000000000..97d1cd068a --- /dev/null +++ b/remote/test/puppeteer/tsconfig.base.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "allowJs": true, + "checkJs": true, + "target": "ESNext", + "moduleResolution": "node", + "module": "ESNext", + "declaration": true, + "declarationMap": true, + "resolveJsonModule": true + } +} diff --git a/remote/test/puppeteer/tsconfig.json b/remote/test/puppeteer/tsconfig.json new file mode 100644 index 0000000000..69717ed6d9 --- /dev/null +++ b/remote/test/puppeteer/tsconfig.json @@ -0,0 +1,11 @@ +/** + * This configuration only exists for the API Extractor tool. See the details in + * CONTRIBUTING.md that describes our TypeScript setup. +*/ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src"] +} diff --git a/remote/test/puppeteer/typescript-if-required.js b/remote/test/puppeteer/typescript-if-required.js new file mode 100644 index 0000000000..96e6b541a7 --- /dev/null +++ b/remote/test/puppeteer/typescript-if-required.js @@ -0,0 +1,61 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const child_process = require('child_process'); +const path = require('path'); +const fs = require('fs'); +const { promisify } = require('util'); + +const exec = promisify(child_process.exec); +const fsAccess = promisify(fs.access); + +const fileExists = async (filePath) => + fsAccess(filePath) + .then(() => true) + .catch(() => false); +/* + + * Now Puppeteer is built with TypeScript, we need to ensure that + * locally we have the generated output before trying to install. + * + * For users installing puppeteer this is fine, they will have the + * generated lib/ directory as we ship it when we publish to npm. + * + * However, if you're cloning the repo to contribute, you won't have the + * generated lib/ directory so this script checks if we need to run + * TypeScript first to ensure the output exists and is in the right + * place. + */ +async function compileTypeScript() { + return exec('npm run tsc').catch((error) => { + console.error('Error running TypeScript', error); + process.exit(1); + }); +} + +async function compileTypeScriptIfRequired() { + const libPath = path.join(__dirname, 'lib'); + const libExists = await fileExists(libPath); + if (libExists) return; + + console.log('Puppeteer:', 'Compiling TypeScript...'); + await compileTypeScript(); +} + +// It's being run as node typescript-if-required.js, not require('..') +if (require.main === module) compileTypeScriptIfRequired(); + +module.exports = compileTypeScriptIfRequired; diff --git a/remote/test/puppeteer/utils/ESTreeWalker.js b/remote/test/puppeteer/utils/ESTreeWalker.js new file mode 100644 index 0000000000..1c6c6d4782 --- /dev/null +++ b/remote/test/puppeteer/utils/ESTreeWalker.js @@ -0,0 +1,135 @@ +// Copyright (c) 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @unrestricted + */ +class ESTreeWalker { + /** + * @param {function(!ESTree.Node):(!Object|undefined)} beforeVisit + * @param {function(!ESTree.Node)=} afterVisit + */ + constructor(beforeVisit, afterVisit) { + this._beforeVisit = beforeVisit; + this._afterVisit = afterVisit || new Function(); + } + + /** + * @param {!ESTree.Node} ast + */ + walk(ast) { + this._innerWalk(ast, null); + } + + /** + * @param {!ESTree.Node} node + * @param {?ESTree.Node} parent + */ + _innerWalk(node, parent) { + if (!node) return; + node.parent = parent; + + if (this._beforeVisit.call(null, node) === ESTreeWalker.SkipSubtree) { + this._afterVisit.call(null, node); + return; + } + + const walkOrder = ESTreeWalker._walkOrder[node.type]; + if (!walkOrder) return; + + if (node.type === 'TemplateLiteral') { + const templateLiteral = /** @type {!ESTree.TemplateLiteralNode} */ (node); + const expressionsLength = templateLiteral.expressions.length; + for (let i = 0; i < expressionsLength; ++i) { + this._innerWalk(templateLiteral.quasis[i], templateLiteral); + this._innerWalk(templateLiteral.expressions[i], templateLiteral); + } + this._innerWalk( + templateLiteral.quasis[expressionsLength], + templateLiteral + ); + } else { + for (let i = 0; i < walkOrder.length; ++i) { + const entity = node[walkOrder[i]]; + if (Array.isArray(entity)) this._walkArray(entity, node); + else this._innerWalk(entity, node); + } + } + + this._afterVisit.call(null, node); + } + + /** + * @param {!Array.<!ESTree.Node>} nodeArray + * @param {?ESTree.Node} parentNode + */ + _walkArray(nodeArray, parentNode) { + for (let i = 0; i < nodeArray.length; ++i) + this._innerWalk(nodeArray[i], parentNode); + } +} + +/** @typedef {!Object} ESTreeWalker.SkipSubtree */ +ESTreeWalker.SkipSubtree = {}; + +/** @enum {!Array.<string>} */ +ESTreeWalker._walkOrder = { + AwaitExpression: ['argument'], + ArrayExpression: ['elements'], + ArrowFunctionExpression: ['params', 'body'], + AssignmentExpression: ['left', 'right'], + AssignmentPattern: ['left', 'right'], + BinaryExpression: ['left', 'right'], + BlockStatement: ['body'], + BreakStatement: ['label'], + CallExpression: ['callee', 'arguments'], + CatchClause: ['param', 'body'], + ClassBody: ['body'], + ClassDeclaration: ['id', 'superClass', 'body'], + ClassExpression: ['id', 'superClass', 'body'], + ConditionalExpression: ['test', 'consequent', 'alternate'], + ContinueStatement: ['label'], + DebuggerStatement: [], + DoWhileStatement: ['body', 'test'], + EmptyStatement: [], + ExpressionStatement: ['expression'], + ForInStatement: ['left', 'right', 'body'], + ForOfStatement: ['left', 'right', 'body'], + ForStatement: ['init', 'test', 'update', 'body'], + FunctionDeclaration: ['id', 'params', 'body'], + FunctionExpression: ['id', 'params', 'body'], + Identifier: [], + IfStatement: ['test', 'consequent', 'alternate'], + LabeledStatement: ['label', 'body'], + Literal: [], + LogicalExpression: ['left', 'right'], + MemberExpression: ['object', 'property'], + MethodDefinition: ['key', 'value'], + NewExpression: ['callee', 'arguments'], + ObjectExpression: ['properties'], + ObjectPattern: ['properties'], + ParenthesizedExpression: ['expression'], + Program: ['body'], + Property: ['key', 'value'], + ReturnStatement: ['argument'], + SequenceExpression: ['expressions'], + Super: [], + SwitchCase: ['test', 'consequent'], + SwitchStatement: ['discriminant', 'cases'], + TaggedTemplateExpression: ['tag', 'quasi'], + TemplateElement: [], + TemplateLiteral: ['quasis', 'expressions'], + ThisExpression: [], + ThrowStatement: ['argument'], + TryStatement: ['block', 'handler', 'finalizer'], + UnaryExpression: ['argument'], + UpdateExpression: ['argument'], + VariableDeclaration: ['declarations'], + VariableDeclarator: ['id', 'init'], + WhileStatement: ['test', 'body'], + WithStatement: ['object', 'body'], + YieldExpression: ['argument'], +}; + +module.exports = ESTreeWalker; diff --git a/remote/test/puppeteer/utils/apply_next_version.js b/remote/test/puppeteer/utils/apply_next_version.js new file mode 100644 index 0000000000..cd62839d2a --- /dev/null +++ b/remote/test/puppeteer/utils/apply_next_version.js @@ -0,0 +1,32 @@ +const path = require('path'); +const fs = require('fs'); +const execSync = require('child_process').execSync; + +// Compare current HEAD to upstream main SHA. +// If they are not equal - refuse to publish since +// we're not tip-of-tree. +const upstream_sha = execSync( + `git ls-remote https://github.com/puppeteer/puppeteer --tags main | cut -f1` +).toString('utf8'); +const current_sha = execSync(`git rev-parse HEAD`).toString('utf8'); +if (upstream_sha.trim() !== current_sha.trim()) { + console.log('REFUSING TO PUBLISH: this is not tip-of-tree!'); + process.exit(1); + return; +} + +const package = require('../package.json'); +let version = package.version; +const dashIndex = version.indexOf('-'); +if (dashIndex !== -1) version = version.substring(0, dashIndex); +version += '-next.' + Date.now(); +console.log('Setting version to ' + version); +package.version = version; +fs.writeFileSync( + path.join(__dirname, '..', 'package.json'), + JSON.stringify(package, undefined, 2) + '\n' +); + +console.log( + 'IMPORTANT: you should update the pinned version of devtools-protocol to match the new revision.' +); diff --git a/remote/test/puppeteer/utils/bisect.js b/remote/test/puppeteer/utils/bisect.js new file mode 100755 index 0000000000..d81fdec8f5 --- /dev/null +++ b/remote/test/puppeteer/utils/bisect.js @@ -0,0 +1,229 @@ +#!/usr/bin/env node +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const URL = require('url'); +const debug = require('debug'); +const pptr = require('..'); +const browserFetcher = pptr.createBrowserFetcher(); +const path = require('path'); +const fs = require('fs'); +const { fork } = require('child_process'); + +const COLOR_RESET = '\x1b[0m'; +const COLOR_RED = '\x1b[31m'; +const COLOR_GREEN = '\x1b[32m'; +const COLOR_YELLOW = '\x1b[33m'; + +const argv = require('minimist')(process.argv.slice(2), {}); + +const help = ` +Usage: + node bisect.js --good <revision> --bad <revision> <script> + +Parameters: + --good revision that is known to be GOOD + --bad revision that is known to be BAD + <script> path to the script that returns non-zero code for BAD revisions and 0 for good + +Example: + node utils/bisect.js --good 577361 --bad 599821 simple.js +`; + +if (argv.h || argv.help) { + console.log(help); + process.exit(0); +} + +if (typeof argv.good !== 'number') { + console.log( + COLOR_RED + 'ERROR: expected --good argument to be a number' + COLOR_RESET + ); + console.log(help); + process.exit(1); +} + +if (typeof argv.bad !== 'number') { + console.log( + COLOR_RED + 'ERROR: expected --bad argument to be a number' + COLOR_RESET + ); + console.log(help); + process.exit(1); +} + +const scriptPath = path.resolve(argv._[0]); +if (!fs.existsSync(scriptPath)) { + console.log( + COLOR_RED + + 'ERROR: Expected to be given a path to a script to run' + + COLOR_RESET + ); + console.log(help); + process.exit(1); +} + +(async (scriptPath, good, bad) => { + const span = Math.abs(good - bad); + console.log( + `Bisecting ${COLOR_YELLOW}${span}${COLOR_RESET} revisions in ${COLOR_YELLOW}~${ + span.toString(2).length + }${COLOR_RESET} iterations` + ); + + while (true) { + const middle = Math.round((good + bad) / 2); + const revision = await findDownloadableRevision(middle, good, bad); + if (!revision || revision === good || revision === bad) break; + let info = browserFetcher.revisionInfo(revision); + const shouldRemove = !info.local; + info = await downloadRevision(revision); + const exitCode = await runScript(scriptPath, info); + if (shouldRemove) await browserFetcher.remove(revision); + let outcome; + if (exitCode) { + bad = revision; + outcome = COLOR_RED + 'BAD' + COLOR_RESET; + } else { + good = revision; + outcome = COLOR_GREEN + 'GOOD' + COLOR_RESET; + } + const span = Math.abs(good - bad); + let fromText = ''; + let toText = ''; + if (good < bad) { + fromText = COLOR_GREEN + good + COLOR_RESET; + toText = COLOR_RED + bad + COLOR_RESET; + } else { + fromText = COLOR_RED + bad + COLOR_RESET; + toText = COLOR_GREEN + good + COLOR_RESET; + } + console.log( + `- ${COLOR_YELLOW}r${revision}${COLOR_RESET} was ${outcome}. Bisecting [${fromText}, ${toText}] - ${COLOR_YELLOW}${span}${COLOR_RESET} revisions and ${COLOR_YELLOW}~${ + span.toString(2).length + }${COLOR_RESET} iterations` + ); + } + + const [fromSha, toSha] = await Promise.all([ + revisionToSha(Math.min(good, bad)), + revisionToSha(Math.max(good, bad)), + ]); + console.log( + `RANGE: https://chromium.googlesource.com/chromium/src/+log/${fromSha}..${toSha}` + ); +})(scriptPath, argv.good, argv.bad); + +function runScript(scriptPath, revisionInfo) { + const log = debug('bisect:runscript'); + log('Running script'); + const child = fork(scriptPath, [], { + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + env: { + ...process.env, + PUPPETEER_EXECUTABLE_PATH: revisionInfo.executablePath, + }, + }); + return new Promise((resolve, reject) => { + child.on('error', (err) => reject(err)); + child.on('exit', (code) => resolve(code)); + }); +} + +async function downloadRevision(revision) { + const log = debug('bisect:download'); + log(`Downloading ${revision}`); + let progressBar = null; + let lastDownloadedBytes = 0; + return await browserFetcher.download( + revision, + (downloadedBytes, totalBytes) => { + if (!progressBar) { + const ProgressBar = require('progress'); + progressBar = new ProgressBar( + `- downloading Chromium r${revision} - ${toMegabytes( + totalBytes + )} [:bar] :percent :etas `, + { + complete: '=', + incomplete: ' ', + width: 20, + total: totalBytes, + } + ); + } + const delta = downloadedBytes - lastDownloadedBytes; + lastDownloadedBytes = downloadedBytes; + progressBar.tick(delta); + } + ); + function toMegabytes(bytes) { + const mb = bytes / 1024 / 1024; + return `${Math.round(mb * 10) / 10} Mb`; + } +} + +async function findDownloadableRevision(rev, from, to) { + const log = debug('bisect:findrev'); + const min = Math.min(from, to); + const max = Math.max(from, to); + log(`Looking around ${rev} from [${min}, ${max}]`); + if (await browserFetcher.canDownload(rev)) return rev; + let down = rev; + let up = rev; + while (min <= down || up <= max) { + const [downOk, upOk] = await Promise.all([ + down > min ? probe(--down) : Promise.resolve(false), + up < max ? probe(++up) : Promise.resolve(false), + ]); + if (downOk) return down; + if (upOk) return up; + } + return null; + + async function probe(rev) { + const result = await browserFetcher.canDownload(rev); + log(` ${rev} - ${result ? 'OK' : 'missing'}`); + return result; + } +} + +async function revisionToSha(revision) { + const json = await fetchJSON( + 'https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/' + revision + ); + return json.git_sha; +} + +function fetchJSON(url) { + return new Promise((resolve, reject) => { + const agent = url.startsWith('https://') + ? require('https') + : require('http'); + const options = URL.parse(url); + options.method = 'GET'; + options.headers = { + 'Content-Type': 'application/json', + }; + const req = agent.request(options, function (res) { + let result = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => (result += chunk)); + res.on('end', () => resolve(JSON.parse(result))); + }); + req.on('error', (err) => reject(err)); + req.end(); + }); +} diff --git a/remote/test/puppeteer/utils/check_availability.js b/remote/test/puppeteer/utils/check_availability.js new file mode 100755 index 0000000000..e24e2b9dc3 --- /dev/null +++ b/remote/test/puppeteer/utils/check_availability.js @@ -0,0 +1,298 @@ +#!/usr/bin/env node +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const assert = require('assert'); +const https = require('https'); +// run `npm run dev-install` if lib dir is missing +const BrowserFetcher = require('../lib/cjs/puppeteer/node/BrowserFetcher.js') + .BrowserFetcher; + +const SUPPORTER_PLATFORMS = ['linux', 'mac', 'win32', 'win64']; +const fetchers = SUPPORTER_PLATFORMS.map( + (platform) => new BrowserFetcher('', { platform }) +); + +const colors = { + reset: '\x1b[0m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', +}; + +class Table { + /** + * @param {!Array<number>} columnWidths + */ + constructor(columnWidths) { + this.widths = columnWidths; + } + + /** + * @param {!Array<string>} values + */ + drawRow(values) { + assert(values.length === this.widths.length); + let row = ''; + for (let i = 0; i < values.length; ++i) + row += padCenter(values[i], this.widths[i]); + console.log(row); + } +} + +const helpMessage = ` +This script checks availability of prebuilt Chromium snapshots. + +Usage: node check_availability.js [<options>] [<browser version(s)>] + +options + -f full mode checks availability of all the platforms, default mode + -r roll mode checks for the most recent stable Chromium roll candidate + -rb roll mode checks for the most recent beta Chromium roll candidate + -rd roll mode checks for the most recent dev Chromium roll candidate + -h show this help + +browser version(s) + <revision> single revision number means checking for this specific revision + <from> <to> checks all the revisions within a given range, inclusively + +Examples + To check Chromium availability of a certain revision + node check_availability.js [revision] + + To find a Chromium roll candidate for current stable Linux version + node check_availability.js -r + use -rb for beta and -rd for dev versions. + + To check Chromium availability from the latest revision in a descending order + node check_availability.js +`; + +function main() { + const args = process.argv.slice(2); + + if (args.length > 3) { + console.log(helpMessage); + return; + } + + if (args.length === 0) { + checkOmahaProxyAvailability(); + return; + } + + if (args[0].startsWith('-')) { + const option = args[0].substring(1); + switch (option) { + case 'f': + break; + case 'r': + checkRollCandidate('stable'); + return; + case 'rb': + checkRollCandidate('beta'); + return; + case 'rd': + checkRollCandidate('dev'); + return; + default: + console.log(helpMessage); + return; + } + args.splice(0, 1); // remove options arg since we are done with options + } + + if (args.length === 1) { + const revision = parseInt(args[0], 10); + checkRangeAvailability({ + fromRevision: revision, + toRevision: revision, + stopWhenAllAvailable: false, + }); + } else { + const fromRevision = parseInt(args[0], 10); + const toRevision = parseInt(args[1], 10); + checkRangeAvailability({ + fromRevision, + toRevision, + stopWhenAllAvailable: false, + }); + } +} + +async function checkOmahaProxyAvailability() { + const latestRevisions = ( + await Promise.all([ + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Mac/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Win/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/LAST_CHANGE' + ), + ]) + ).map((s) => parseInt(s, 10)); + const from = Math.max(...latestRevisions); + checkRangeAvailability({ + fromRevision: from, + toRevision: 0, + stopWhenAllAvailable: false, + }); +} +async function checkRollCandidate(channel) { + const omahaResponse = await fetch( + `https://omahaproxy.appspot.com/all.json?channel=${channel}&os=linux` + ); + const linuxInfo = JSON.parse(omahaResponse)[0]; + if (!linuxInfo) { + console.error(`no ${channel} linux information available from omahaproxy`); + return; + } + + const linuxRevision = parseInt( + linuxInfo.versions[0].branch_base_position, + 10 + ); + const currentRevision = parseInt( + require('../lib/cjs/puppeteer/revisions').PUPPETEER_REVISIONS.chromium, + 10 + ); + + checkRangeAvailability({ + fromRevision: linuxRevision, + toRevision: currentRevision, + stopWhenAllAvailable: true, + }); +} + +/** + * @param {*} options + */ +async function checkRangeAvailability({ + fromRevision, + toRevision, + stopWhenAllAvailable, +}) { + const table = new Table([10, 7, 7, 7, 7]); + table.drawRow([''].concat(SUPPORTER_PLATFORMS)); + + const inc = fromRevision < toRevision ? 1 : -1; + const revisionToStop = toRevision + inc; // +inc so the range is fully inclusive + for ( + let revision = fromRevision; + revision !== revisionToStop; + revision += inc + ) { + const allAvailable = await checkAndDrawRevisionAvailability( + table, + '', + revision + ); + if (allAvailable && stopWhenAllAvailable) break; + } +} + +/** + * @param {!Table} table + * @param {string} name + * @param {number} revision + * @returns {boolean} + */ +async function checkAndDrawRevisionAvailability(table, name, revision) { + const promises = fetchers.map((fetcher) => fetcher.canDownload(revision)); + const availability = await Promise.all(promises); + const allAvailable = availability.every((e) => !!e); + const values = [ + name + + ' ' + + (allAvailable ? colors.green + revision + colors.reset : revision), + ]; + for (let i = 0; i < availability.length; ++i) { + const decoration = availability[i] ? '+' : '-'; + const color = availability[i] ? colors.green : colors.red; + values.push(color + decoration + colors.reset); + } + table.drawRow(values); + return allAvailable; +} + +/** + * @param {string} url + * @returns {!Promise<?string>} + */ +function fetch(url) { + let resolve; + const promise = new Promise((x) => (resolve = x)); + https + .get(url, (response) => { + if (response.statusCode !== 200) { + resolve(null); + return; + } + let body = ''; + response.on('data', function (chunk) { + body += chunk; + }); + response.on('end', function () { + resolve(body); + }); + }) + .on('error', function (e) { + console.error('Error fetching json: ' + e); + resolve(null); + }); + return promise; +} + +/** + * @param {number} size + * @returns {string} + */ +function spaceString(size) { + return new Array(size).fill(' ').join(''); +} + +/** + * @param {string} text + * @returns {string} + */ +function filterOutColors(text) { + for (const colorName in colors) { + const color = colors[colorName]; + text = text.replace(color, ''); + } + return text; +} + +/** + * @param {string} text + * @param {number} length + * @returns {string} + */ +function padCenter(text, length) { + const printableCharacters = filterOutColors(text); + if (printableCharacters.length >= length) return text; + const left = Math.floor((length - printableCharacters.length) / 2); + const right = Math.ceil((length - printableCharacters.length) / 2); + return spaceString(left) + text + spaceString(right); +} + +main(); diff --git a/remote/test/puppeteer/utils/doclint/.gitignore b/remote/test/puppeteer/utils/doclint/.gitignore new file mode 100644 index 0000000000..ea1472ec1f --- /dev/null +++ b/remote/test/puppeteer/utils/doclint/.gitignore @@ -0,0 +1 @@ +output/ diff --git a/remote/test/puppeteer/utils/doclint/Message.js b/remote/test/puppeteer/utils/doclint/Message.js new file mode 100644 index 0000000000..374b60d44d --- /dev/null +++ b/remote/test/puppeteer/utils/doclint/Message.js @@ -0,0 +1,44 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +class Message { + /** + * @param {string} type + * @param {string} text + */ + constructor(type, text) { + this.type = type; + this.text = text; + } + + /** + * @param {string} text + * @returns {!Message} + */ + static error(text) { + return new Message('error', text); + } + + /** + * @param {string} text + * @returns {!Message} + */ + static warning(text) { + return new Message('warning', text); + } +} + +module.exports = Message; diff --git a/remote/test/puppeteer/utils/doclint/README.md b/remote/test/puppeteer/utils/doclint/README.md new file mode 100644 index 0000000000..b3f8c366de --- /dev/null +++ b/remote/test/puppeteer/utils/doclint/README.md @@ -0,0 +1,31 @@ +# DocLint + +**Doclint** is a small program that lints Puppeteer's documentation against +Puppeteer's source code. + +Doclint works in a few steps: + +1. Read sources in `lib/` folder, parse AST trees and extract public API + - note that we run DocLint on the outputted JavaScript in `lib/` rather than the source code in `src/`. We will do this until we have migrated `src/` to be exclusively TypeScript and then we can update DocLint to support TypeScript. +2. Read sources in `docs/` folder, render markdown to HTML, use puppeteer to traverse the HTML + and extract described API +3. Compare one API to another + +Doclint is also responsible for general markdown checks, most notably for the table of contents +relevancy. + +## Running + +```bash +npm run doc +``` + +## Tests + +Doclint has its own set of jasmine tests, located at `utils/doclint/test` folder. + +To execute tests, run: + +```bash +npm run test-doclint +``` diff --git a/remote/test/puppeteer/utils/doclint/Source.js b/remote/test/puppeteer/utils/doclint/Source.js new file mode 100644 index 0000000000..0f7c13234f --- /dev/null +++ b/remote/test/puppeteer/utils/doclint/Source.js @@ -0,0 +1,117 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const path = require('path'); +const util = require('util'); +const fs = require('fs'); + +const readFileAsync = util.promisify(fs.readFile); +const readdirAsync = util.promisify(fs.readdir); +const writeFileAsync = util.promisify(fs.writeFile); + +const PROJECT_DIR = path.join(__dirname, '..', '..'); + +class Source { + /** + * @param {string} filePath + * @param {string} text + */ + constructor(filePath, text) { + this._filePath = filePath; + this._projectPath = path.relative(PROJECT_DIR, filePath); + this._name = path.basename(filePath); + this._text = text; + this._hasUpdatedText = false; + } + + /** + * @returns {string} + */ + filePath() { + return this._filePath; + } + + /** + * @returns {string} + */ + projectPath() { + return this._projectPath; + } + + /** + * @returns {string} + */ + name() { + return this._name; + } + + /** + * @param {string} text + * @returns {boolean} + */ + setText(text) { + if (text === this._text) return false; + this._hasUpdatedText = true; + this._text = text; + return true; + } + + /** + * @returns {string} + */ + text() { + return this._text; + } + + /** + * @returns {boolean} + */ + hasUpdatedText() { + return this._hasUpdatedText; + } + + async save() { + await writeFileAsync(this.filePath(), this.text()); + } + + /** + * @param {string} filePath + * @returns {!Promise<Source>} + */ + static async readFile(filePath) { + filePath = path.resolve(filePath); + const text = await readFileAsync(filePath, { encoding: 'utf8' }); + return new Source(filePath, text); + } + + /** + * @param {string} dirPath + * @param {string=} extension + * @returns {!Promise<!Array<!Source>>} + */ + static async readdir(dirPath, extension = '') { + const fileNames = await readdirAsync(dirPath); + const filePaths = fileNames + .filter((fileName) => fileName.endsWith(extension)) + .map((fileName) => path.join(dirPath, fileName)) + .filter((filePath) => { + const stats = fs.lstatSync(filePath); + return stats.isDirectory() === false; + }); + return Promise.all(filePaths.map((filePath) => Source.readFile(filePath))); + } +} +module.exports = Source; diff --git a/remote/test/puppeteer/utils/doclint/check_public_api/Documentation.js b/remote/test/puppeteer/utils/doclint/check_public_api/Documentation.js new file mode 100644 index 0000000000..9bbaabfc70 --- /dev/null +++ b/remote/test/puppeteer/utils/doclint/check_public_api/Documentation.js @@ -0,0 +1,157 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +class Documentation { + /** + * @param {!Array<!Documentation.Class>} classesArray + */ + constructor(classesArray) { + this.classesArray = classesArray; + /** @type {!Map<string, !Documentation.Class>} */ + this.classes = new Map(); + for (const cls of classesArray) this.classes.set(cls.name, cls); + } +} + +Documentation.Class = class { + /** + * @param {string} name + * @param {!Array<!Documentation.Member>} membersArray + * @param {?string=} extendsName + * @param {string=} comment + */ + constructor(name, membersArray, extendsName = null, comment = '') { + this.name = name; + this.membersArray = membersArray; + /** @type {!Map<string, !Documentation.Member>} */ + this.members = new Map(); + /** @type {!Map<string, !Documentation.Member>} */ + this.properties = new Map(); + /** @type {!Array<!Documentation.Member>} */ + this.propertiesArray = []; + /** @type {!Map<string, !Documentation.Member>} */ + this.methods = new Map(); + /** @type {!Array<!Documentation.Member>} */ + this.methodsArray = []; + /** @type {!Map<string, !Documentation.Member>} */ + this.events = new Map(); + /** @type {!Array<!Documentation.Member>} */ + this.eventsArray = []; + this.comment = comment; + this.extends = extendsName; + for (const member of membersArray) { + this.members.set(member.name, member); + if (member.kind === 'method') { + this.methods.set(member.name, member); + this.methodsArray.push(member); + } else if (member.kind === 'property') { + this.properties.set(member.name, member); + this.propertiesArray.push(member); + } else if (member.kind === 'event') { + this.events.set(member.name, member); + this.eventsArray.push(member); + } + } + } +}; + +Documentation.Member = class { + /** + * @param {string} kind + * @param {string} name + * @param {?Documentation.Type} type + * @param {!Array<!Documentation.Member>} argsArray + */ + constructor( + kind, + name, + type, + argsArray, + comment = '', + returnComment = '', + required = true + ) { + this.kind = kind; + this.name = name; + this.type = type; + this.comment = comment; + this.returnComment = returnComment; + this.argsArray = argsArray; + this.required = required; + /** @type {!Map<string, !Documentation.Member>} */ + this.args = new Map(); + for (const arg of argsArray) this.args.set(arg.name, arg); + } + + /** + * @param {string} name + * @param {!Array<!Documentation.Member>} argsArray + * @param {?Documentation.Type} returnType + * @returns {!Documentation.Member} + */ + static createMethod(name, argsArray, returnType, returnComment, comment) { + return new Documentation.Member( + 'method', + name, + returnType, + argsArray, + comment, + returnComment + ); + } + + /** + * @param {string} name + * @param {!Documentation.Type} type + * @param {string=} comment + * @param {boolean=} required + * @returns {!Documentation.Member} + */ + static createProperty(name, type, comment, required) { + return new Documentation.Member( + 'property', + name, + type, + [], + comment, + undefined, + required + ); + } + + /** + * @param {string} name + * @param {?Documentation.Type=} type + * @param {string=} comment + * @returns {!Documentation.Member} + */ + static createEvent(name, type = null, comment) { + return new Documentation.Member('event', name, type, [], comment); + } +}; + +Documentation.Type = class { + /** + * @param {string} name + * @param {!Array<!Documentation.Member>=} properties + */ + constructor(name, properties = []) { + this.name = name; + this.properties = properties; + } +}; + +module.exports = Documentation; diff --git a/remote/test/puppeteer/utils/doclint/check_public_api/JSBuilder.js b/remote/test/puppeteer/utils/doclint/check_public_api/JSBuilder.js new file mode 100644 index 0000000000..0994dffcff --- /dev/null +++ b/remote/test/puppeteer/utils/doclint/check_public_api/JSBuilder.js @@ -0,0 +1,279 @@ +const ts = require('typescript'); +const path = require('path'); +const Documentation = require('./Documentation'); +module.exports = checkSources; + +/** + * @param {!Array<!import('../Source')>} sources + */ +function checkSources(sources) { + // special treatment for Events.js + const classEvents = new Map(); + const eventsSource = sources.find((source) => source.name() === 'Events.js'); + if (eventsSource) { + const { Events } = require(eventsSource.filePath()); + for (const [className, events] of Object.entries(Events)) + classEvents.set( + className, + Array.from(Object.values(events)) + .filter((e) => typeof e === 'string') + .map((e) => Documentation.Member.createEvent(e)) + ); + } + + const excludeClasses = new Set([]); + const program = ts.createProgram({ + options: { + allowJs: true, + target: ts.ScriptTarget.ES2017, + }, + rootNames: sources.map((source) => source.filePath()), + }); + const checker = program.getTypeChecker(); + const sourceFiles = program.getSourceFiles(); + /** @type {!Array<!Documentation.Class>} */ + const classes = []; + /** @type {!Map<string, string>} */ + const inheritance = new Map(); + + const sourceFilesNoNodeModules = sourceFiles.filter( + (x) => !x.fileName.includes('node_modules') + ); + const sourceFileNamesSet = new Set( + sourceFilesNoNodeModules.map((x) => x.fileName) + ); + sourceFilesNoNodeModules.map((x) => { + if (x.fileName.includes('/lib/')) { + const potentialTSSource = x.fileName + .replace('lib', 'src') + .replace('.js', '.ts'); + if (sourceFileNamesSet.has(potentialTSSource)) { + /* Not going to visit this file because we have the TypeScript src code + * which we'll use instead. + */ + return; + } + } + + visit(x); + }); + + const errors = []; + const documentation = new Documentation( + recreateClassesWithInheritance(classes, inheritance) + ); + + return { errors, documentation }; + + /** + * @param {!Array<!Documentation.Class>} classes + * @param {!Map<string, string>} inheritance + * @returns {!Array<!Documentation.Class>} + */ + function recreateClassesWithInheritance(classes, inheritance) { + const classesByName = new Map(classes.map((cls) => [cls.name, cls])); + return classes.map((cls) => { + const membersMap = new Map(); + for (let wp = cls; wp; wp = classesByName.get(inheritance.get(wp.name))) { + for (const member of wp.membersArray) { + // Member was overridden. + const memberId = member.kind + ':' + member.name; + if (membersMap.has(memberId)) continue; + membersMap.set(memberId, member); + } + } + return new Documentation.Class(cls.name, Array.from(membersMap.values())); + }); + } + + /** + * @param {!ts.Node} node + */ + function visit(node) { + if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { + const symbol = node.name + ? checker.getSymbolAtLocation(node.name) + : node.symbol; + let className = symbol.getName(); + + if (className === '__class') { + let parent = node; + while (parent.parent) parent = parent.parent; + className = path.basename(parent.fileName, '.js'); + } + if (className && !excludeClasses.has(className)) { + classes.push(serializeClass(className, symbol, node)); + const parentClassName = parentClass(node); + if (parentClassName) inheritance.set(className, parentClassName); + excludeClasses.add(className); + } + } + ts.forEachChild(node, visit); + } + + function parentClass(classNode) { + for (const herigateClause of classNode.heritageClauses || []) { + for (const heritageType of herigateClause.types) { + const parentClassName = heritageType.expression.escapedText; + return parentClassName; + } + } + return null; + } + + function serializeSymbol(symbol, circular = []) { + const type = checker.getTypeOfSymbolAtLocation( + symbol, + symbol.valueDeclaration + ); + const name = symbol.getName(); + if (symbol.valueDeclaration && symbol.valueDeclaration.dotDotDotToken) { + try { + const innerType = serializeType(type.typeArguments[0], circular); + innerType.name = '...' + innerType.name; + return Documentation.Member.createProperty('...' + name, innerType); + } catch (error) { + /** + * DocLint struggles with the paramArgs type on CDPSession.send because + * it uses a complex type from the devtools-protocol method. Doclint + * isn't going to be here for much longer so we'll just silence this + * warning than try to add support which would warrant a huge rewrite. + */ + if (name !== 'paramArgs') throw error; + } + } + return Documentation.Member.createProperty( + name, + serializeType(type, circular) + ); + } + + /** + * @param {!ts.ObjectType} type + */ + function isRegularObject(type) { + if (type.isIntersection()) return true; + if (!type.objectFlags) return false; + if (!('aliasSymbol' in type)) return false; + if (type.getConstructSignatures().length) return false; + if (type.getCallSignatures().length) return false; + if (type.isLiteral()) return false; + if (type.isUnion()) return false; + + return true; + } + + /** + * @param {!ts.Type} type + * @returns {!Documentation.Type} + */ + function serializeType(type, circular = []) { + let typeName = checker.typeToString(type); + if ( + typeName === 'any' || + typeName === '{ [x: string]: string; }' || + typeName === '{}' + ) + typeName = 'Object'; + const nextCircular = [typeName].concat(circular); + + if (isRegularObject(type)) { + let properties = undefined; + if (!circular.includes(typeName)) + properties = type + .getProperties() + .map((property) => serializeSymbol(property, nextCircular)); + return new Documentation.Type('Object', properties); + } + if (type.isUnion() && typeName.includes('|')) { + const types = type.types.map((type) => serializeType(type, circular)); + const name = types.map((type) => type.name).join('|'); + const properties = [].concat(...types.map((type) => type.properties)); + return new Documentation.Type( + name.replace(/false\|true/g, 'boolean'), + properties + ); + } + if (type.typeArguments) { + const properties = []; + const innerTypeNames = []; + for (const typeArgument of type.typeArguments) { + const innerType = serializeType(typeArgument, nextCircular); + if (innerType.properties) properties.push(...innerType.properties); + innerTypeNames.push(innerType.name); + } + if ( + innerTypeNames.length === 0 || + (innerTypeNames.length === 1 && innerTypeNames[0] === 'void') + ) + return new Documentation.Type(type.symbol.name); + return new Documentation.Type( + `${type.symbol.name}<${innerTypeNames.join(', ')}>`, + properties + ); + } + return new Documentation.Type(typeName, []); + } + + /** + * @param {!ts.Symbol} symbol + * @returns {boolean} + */ + function symbolHasPrivateModifier(symbol) { + const modifiers = + (symbol.valueDeclaration && symbol.valueDeclaration.modifiers) || []; + return modifiers.some( + (modifier) => modifier.kind === ts.SyntaxKind.PrivateKeyword + ); + } + + /** + * @param {string} className + * @param {!ts.Symbol} symbol + * @returns {} + */ + function serializeClass(className, symbol, node) { + /** @type {!Array<!Documentation.Member>} */ + const members = classEvents.get(className) || []; + + for (const [name, member] of symbol.members || []) { + /* Before TypeScript we denoted private methods with an underscore + * but in TypeScript we use the private keyword + * hence we check for either here. + */ + if (name.startsWith('_') || symbolHasPrivateModifier(member)) continue; + + const memberType = checker.getTypeOfSymbolAtLocation( + member, + member.valueDeclaration + ); + const signature = memberType.getCallSignatures()[0]; + if (signature) members.push(serializeSignature(name, signature)); + else members.push(serializeProperty(name, memberType)); + } + + return new Documentation.Class(className, members); + } + + /** + * @param {string} name + * @param {!ts.Signature} signature + */ + function serializeSignature(name, signature) { + const parameters = signature.parameters.map((s) => serializeSymbol(s)); + const returnType = serializeType(signature.getReturnType()); + return Documentation.Member.createMethod( + name, + parameters, + returnType.name !== 'void' ? returnType : null + ); + } + + /** + * @param {string} name + * @param {!ts.Type} type + */ + function serializeProperty(name, type) { + return Documentation.Member.createProperty(name, serializeType(type)); + } +} diff --git a/remote/test/puppeteer/utils/doclint/check_public_api/MDBuilder.js b/remote/test/puppeteer/utils/doclint/check_public_api/MDBuilder.js new file mode 100644 index 0000000000..46a4e6cfce --- /dev/null +++ b/remote/test/puppeteer/utils/doclint/check_public_api/MDBuilder.js @@ -0,0 +1,402 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const Documentation = require('./Documentation'); +const commonmark = require('commonmark'); + +class MDOutline { + /** + * @param {!Page} page + * @param {string} text + * @returns {!MDOutline} + */ + static async create(page, text) { + // Render markdown as HTML. + const reader = new commonmark.Parser(); + const parsed = reader.parse(text); + const writer = new commonmark.HtmlRenderer(); + const html = writer.render(parsed); + + page.on('console', (msg) => { + console.log(msg.text()); + }); + // Extract headings. + await page.setContent(html); + const { classes, errors } = await page.evaluate(() => { + const classes = []; + const errors = []; + const headers = document.body.querySelectorAll('h3'); + for (let i = 0; i < headers.length; i++) { + const fragment = extractSiblingsIntoFragment( + headers[i], + headers[i + 1] + ); + classes.push(parseClass(fragment)); + } + return { classes, errors }; + + /** + * @param {HTMLLIElement} element + */ + function parseProperty(element) { + const clone = element.cloneNode(true); + const ul = clone.querySelector(':scope > ul'); + const str = parseComment( + extractSiblingsIntoFragment(clone.firstChild, ul) + ); + const name = str + .substring(0, str.indexOf('<')) + .replace(/\`/g, '') + .trim(); + const type = findType(str); + const properties = []; + const comment = str + .substring(str.indexOf('<') + type.length + 2) + .trim(); + // Strings have enum values instead of properties + if (!type.includes('string')) { + for (const childElement of element.querySelectorAll( + ':scope > ul > li' + )) { + const property = parseProperty(childElement); + property.required = property.comment.includes('***required***'); + properties.push(property); + } + } + return { + name, + type, + comment, + properties, + }; + } + + /** + * @param {string} str + * @returns {string} + */ + function findType(str) { + const start = str.indexOf('<') + 1; + let count = 1; + for (let i = start; i < str.length; i++) { + if (str[i] === '<') count++; + if (str[i] === '>') count--; + if (!count) return str.substring(start, i); + } + return 'unknown'; + } + + /** + * @param {DocumentFragment} content + */ + function parseClass(content) { + const members = []; + const headers = content.querySelectorAll('h4'); + const name = content.firstChild.textContent; + let extendsName = null; + let commentStart = content.firstChild.nextSibling; + const extendsElement = content.querySelector('ul'); + if ( + extendsElement && + extendsElement.textContent.trim().startsWith('extends:') + ) { + commentStart = extendsElement.nextSibling; + extendsName = extendsElement.querySelector('a').textContent; + } + const comment = parseComment( + extractSiblingsIntoFragment(commentStart, headers[0]) + ); + for (let i = 0; i < headers.length; i++) { + const fragment = extractSiblingsIntoFragment( + headers[i], + headers[i + 1] + ); + members.push(parseMember(fragment)); + } + return { + name, + comment, + extendsName, + members, + }; + } + + /** + * @param {Node} content + */ + function parseComment(content) { + for (const code of content.querySelectorAll('pre > code')) + code.replaceWith( + '```' + + code.className.substring('language-'.length) + + '\n' + + code.textContent + + '```' + ); + for (const code of content.querySelectorAll('code')) + code.replaceWith('`' + code.textContent + '`'); + for (const strong of content.querySelectorAll('strong')) + strong.replaceWith('**' + parseComment(strong) + '**'); + return content.textContent.trim(); + } + + /** + * @param {string} name + * @param {DocumentFragment} content + */ + function parseMember(content) { + const name = content.firstChild.textContent; + const args = []; + let returnType = null; + + const paramRegex = /^\w+\.[\w$]+\((.*)\)$/; + const matches = paramRegex.exec(name) || ['', '']; + const parameters = matches[1]; + const optionalStartIndex = parameters.indexOf('['); + const optinalParamsStr = + optionalStartIndex !== -1 + ? parameters.substring(optionalStartIndex).replace(/[\[\]]/g, '') + : ''; + const optionalparams = new Set( + optinalParamsStr + .split(',') + .filter((x) => x) + .map((x) => x.trim()) + ); + const ul = content.querySelector('ul'); + for (const element of content.querySelectorAll('h4 + ul > li')) { + if ( + element.matches('li') && + element.textContent.trim().startsWith('<') + ) { + returnType = parseProperty(element); + } else if ( + element.matches('li') && + element.firstChild.matches && + element.firstChild.matches('code') + ) { + const property = parseProperty(element); + property.required = !optionalparams.has(property.name); + args.push(property); + } else if ( + element.matches('li') && + element.firstChild.nodeType === Element.TEXT_NODE && + element.firstChild.textContent.toLowerCase().startsWith('return') + ) { + returnType = parseProperty(element); + const expectedText = 'returns: '; + let actualText = element.firstChild.textContent; + let angleIndex = actualText.indexOf('<'); + let spaceIndex = actualText.indexOf(' '); + angleIndex = angleIndex === -1 ? actualText.length : angleIndex; + spaceIndex = spaceIndex === -1 ? actualText.length : spaceIndex + 1; + actualText = actualText.substring( + 0, + Math.min(angleIndex, spaceIndex) + ); + if (actualText !== expectedText) + errors.push( + `${name} has mistyped 'return' type declaration: expected exactly '${expectedText}', found '${actualText}'.` + ); + } + } + const comment = parseComment( + extractSiblingsIntoFragment(ul ? ul.nextSibling : content) + ); + return { + name, + args, + returnType, + comment, + }; + } + + /** + * @param {!Node} fromInclusive + * @param {!Node} toExclusive + * @returns {!DocumentFragment} + */ + function extractSiblingsIntoFragment(fromInclusive, toExclusive) { + const fragment = document.createDocumentFragment(); + let node = fromInclusive; + while (node && node !== toExclusive) { + const next = node.nextSibling; + fragment.appendChild(node); + node = next; + } + return fragment; + } + }); + return new MDOutline(classes, errors); + } + + constructor(classes, errors) { + this.classes = []; + this.errors = errors; + const classHeading = /^class: (\w+)$/; + const constructorRegex = /^new (\w+)\((.*)\)$/; + const methodRegex = /^(\w+)\.([\w$]+)\((.*)\)$/; + const propertyRegex = /^(\w+)\.(\w+)$/; + const eventRegex = /^event: '(\w+)'$/; + let currentClassName = null; + let currentClassMembers = []; + let currentClassComment = ''; + let currentClassExtends = null; + for (const cls of classes) { + const match = cls.name.match(classHeading); + if (!match) continue; + currentClassName = match[1]; + currentClassComment = cls.comment; + currentClassExtends = cls.extendsName; + for (const member of cls.members) { + if (constructorRegex.test(member.name)) { + const match = member.name.match(constructorRegex); + handleMethod.call(this, member, match[1], 'constructor', match[2]); + } else if (methodRegex.test(member.name)) { + const match = member.name.match(methodRegex); + handleMethod.call(this, member, match[1], match[2], match[3]); + } else if (propertyRegex.test(member.name)) { + const match = member.name.match(propertyRegex); + handleProperty.call(this, member, match[1], match[2]); + } else if (eventRegex.test(member.name)) { + const match = member.name.match(eventRegex); + handleEvent.call(this, member, match[1]); + } + } + flushClassIfNeeded.call(this); + } + + function handleMethod(member, className, methodName, parameters) { + if ( + !currentClassName || + !className || + !methodName || + className.toLowerCase() !== currentClassName.toLowerCase() + ) { + this.errors.push(`Failed to process header as method: ${member.name}`); + return; + } + parameters = parameters.trim().replace(/[\[\]]/g, ''); + if (parameters !== member.args.map((arg) => arg.name).join(', ')) + this.errors.push( + `Heading arguments for "${ + member.name + }" do not match described ones, i.e. "${parameters}" != "${member.args + .map((a) => a.name) + .join(', ')}"` + ); + const args = member.args.map(createPropertyFromJSON); + let returnType = null; + let returnComment = ''; + if (member.returnType) { + const returnProperty = createPropertyFromJSON(member.returnType); + returnType = returnProperty.type; + returnComment = returnProperty.comment; + } + const method = Documentation.Member.createMethod( + methodName, + args, + returnType, + returnComment, + member.comment + ); + currentClassMembers.push(method); + } + + function createPropertyFromJSON(payload) { + const type = new Documentation.Type( + payload.type, + payload.properties.map(createPropertyFromJSON) + ); + const required = payload.required; + return Documentation.Member.createProperty( + payload.name, + type, + payload.comment, + required + ); + } + + function handleProperty(member, className, propertyName) { + if ( + !currentClassName || + !className || + !propertyName || + className.toLowerCase() !== currentClassName.toLowerCase() + ) { + this.errors.push( + `Failed to process header as property: ${member.name}` + ); + return; + } + const type = member.returnType ? member.returnType.type : null; + const properties = member.returnType ? member.returnType.properties : []; + currentClassMembers.push( + createPropertyFromJSON({ + type, + name: propertyName, + properties, + comment: member.comment, + }) + ); + } + + function handleEvent(member, eventName) { + if (!currentClassName || !eventName) { + this.errors.push(`Failed to process header as event: ${member.name}`); + return; + } + currentClassMembers.push( + Documentation.Member.createEvent( + eventName, + member.returnType && createPropertyFromJSON(member.returnType).type, + member.comment + ) + ); + } + + function flushClassIfNeeded() { + if (currentClassName === null) return; + this.classes.push( + new Documentation.Class( + currentClassName, + currentClassMembers, + currentClassExtends, + currentClassComment + ) + ); + currentClassName = null; + currentClassMembers = []; + } + } +} + +/** + * @param {!Page} page + * @param {!Array<!Source>} sources + * @returns {!Promise<{documentation: !Documentation, errors: !Array<string>}>} + */ +module.exports = async function (page, sources) { + const classes = []; + const errors = []; + for (const source of sources) { + const outline = await MDOutline.create(page, source.text()); + classes.push(...outline.classes); + errors.push(...outline.errors); + } + const documentation = new Documentation(classes); + return { documentation, errors }; +}; diff --git a/remote/test/puppeteer/utils/doclint/check_public_api/index.js b/remote/test/puppeteer/utils/doclint/check_public_api/index.js new file mode 100644 index 0000000000..84d5ca0f2a --- /dev/null +++ b/remote/test/puppeteer/utils/doclint/check_public_api/index.js @@ -0,0 +1,977 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const jsBuilder = require('./JSBuilder'); +const mdBuilder = require('./MDBuilder'); +const Documentation = require('./Documentation'); +const Message = require('../Message'); +const { + MODULES_TO_CHECK_FOR_COVERAGE, +} = require('../../../test/coverage-utils'); + +const EXCLUDE_PROPERTIES = new Set([ + 'Browser.create', + 'Headers.fromPayload', + 'Page.create', + 'JSHandle.toString', + 'TimeoutError.name', + /* This isn't an actual property, but a TypeScript generic. + * DocLint incorrectly parses it as a property. + */ + 'ElementHandle.ElementType', +]); + +/** + * @param {!Page} page + * @param {!Array<!Source>} mdSources + * @returns {!Promise<!Array<!Message>>} + */ +module.exports = async function lint(page, mdSources, jsSources) { + const mdResult = await mdBuilder(page, mdSources); + const jsResult = await jsBuilder(jsSources); + const jsDocumentation = filterJSDocumentation(jsResult.documentation); + const mdDocumentation = mdResult.documentation; + + const jsErrors = jsResult.errors; + jsErrors.push(...checkDuplicates(jsDocumentation)); + + const mdErrors = mdResult.errors; + mdErrors.push(...compareDocumentations(mdDocumentation, jsDocumentation)); + mdErrors.push(...checkDuplicates(mdDocumentation)); + mdErrors.push(...checkSorting(mdDocumentation)); + + // Push all errors with proper prefixes + const errors = jsErrors.map((error) => '[JavaScript] ' + error); + errors.push(...mdErrors.map((error) => '[MarkDown] ' + error)); + return errors.map((error) => Message.error(error)); +}; + +/** + * @param {!Documentation} doc + * @returns {!Array<string>} + */ +function checkSorting(doc) { + const errors = []; + for (const cls of doc.classesArray) { + const members = cls.membersArray; + + // Events should go first. + let eventIndex = 0; + for ( + ; + eventIndex < members.length && members[eventIndex].kind === 'event'; + ++eventIndex + ); + for ( + ; + eventIndex < members.length && members[eventIndex].kind !== 'event'; + ++eventIndex + ); + if (eventIndex < members.length) + errors.push( + `Events should go first. Event '${members[eventIndex].name}' in class ${cls.name} breaks order` + ); + + // Constructor should be right after events and before all other members. + const constructorIndex = members.findIndex( + (member) => member.kind === 'method' && member.name === 'constructor' + ); + if (constructorIndex > 0 && members[constructorIndex - 1].kind !== 'event') + errors.push(`Constructor of ${cls.name} should go before other methods`); + + // Events should be sorted alphabetically. + for (let i = 0; i < members.length - 1; ++i) { + const member1 = cls.membersArray[i]; + const member2 = cls.membersArray[i + 1]; + if (member1.kind !== 'event' || member2.kind !== 'event') continue; + if (member1.name > member2.name) + errors.push( + `Event '${member1.name}' in class ${cls.name} breaks alphabetic ordering of events` + ); + } + + // All other members should be sorted alphabetically. + for (let i = 0; i < members.length - 1; ++i) { + const member1 = cls.membersArray[i]; + const member2 = cls.membersArray[i + 1]; + if (member1.kind === 'event' || member2.kind === 'event') continue; + if (member1.kind === 'method' && member1.name === 'constructor') continue; + if (member1.name > member2.name) { + let memberName1 = `${cls.name}.${member1.name}`; + if (member1.kind === 'method') memberName1 += '()'; + let memberName2 = `${cls.name}.${member2.name}`; + if (member2.kind === 'method') memberName2 += '()'; + errors.push( + `Bad alphabetic ordering of ${cls.name} members: ${memberName1} should go after ${memberName2}` + ); + } + } + } + return errors; +} + +/** + * @param {!Documentation} jsDocumentation + * @returns {!Documentation} + */ +function filterJSDocumentation(jsDocumentation) { + const includedClasses = new Set(Object.keys(MODULES_TO_CHECK_FOR_COVERAGE)); + // Filter private classes and methods. + const classes = []; + for (const cls of jsDocumentation.classesArray) { + if (includedClasses && !includedClasses.has(cls.name)) continue; + const members = cls.membersArray.filter( + (member) => !EXCLUDE_PROPERTIES.has(`${cls.name}.${member.name}`) + ); + classes.push(new Documentation.Class(cls.name, members)); + } + return new Documentation(classes); +} + +/** + * @param {!Documentation} doc + * @returns {!Array<string>} + */ +function checkDuplicates(doc) { + const errors = []; + const classes = new Set(); + // Report duplicates. + for (const cls of doc.classesArray) { + if (classes.has(cls.name)) + errors.push(`Duplicate declaration of class ${cls.name}`); + classes.add(cls.name); + const members = new Set(); + for (const member of cls.membersArray) { + if (members.has(member.kind + ' ' + member.name)) + errors.push( + `Duplicate declaration of ${member.kind} ${cls.name}.${member.name}()` + ); + members.add(member.kind + ' ' + member.name); + const args = new Set(); + for (const arg of member.argsArray) { + if (args.has(arg.name)) + errors.push( + `Duplicate declaration of argument ${cls.name}.${member.name} "${arg.name}"` + ); + args.add(arg.name); + } + } + } + return errors; +} + +// All the methods from our EventEmitter that we don't document for each subclass. +const EVENT_LISTENER_METHODS = new Set([ + 'emit', + 'listenerCount', + 'off', + 'on', + 'once', + 'removeListener', + 'addListener', + 'removeAllListeners', +]); + +/* Methods that are defined in code but are not documented */ +const expectedNotFoundMethods = new Map([ + ['Browser', EVENT_LISTENER_METHODS], + ['BrowserContext', EVENT_LISTENER_METHODS], + ['CDPSession', EVENT_LISTENER_METHODS], + ['Page', EVENT_LISTENER_METHODS], + ['WebWorker', EVENT_LISTENER_METHODS], +]); + +/** + * @param {!Documentation} actual + * @param {!Documentation} expected + * @returns {!Array<string>} + */ +function compareDocumentations(actual, expected) { + const errors = []; + + const actualClasses = Array.from(actual.classes.keys()).sort(); + const expectedClasses = Array.from(expected.classes.keys()).sort(); + const classesDiff = diff(actualClasses, expectedClasses); + + /* These have been moved onto PuppeteerNode but we want to document them under + * Puppeteer. See https://github.com/puppeteer/puppeteer/pull/6504 for details. + */ + const expectedPuppeteerClassMissingMethods = new Set([ + 'createBrowserFetcher', + 'defaultArgs', + 'executablePath', + 'launch', + ]); + + for (const className of classesDiff.extra) + errors.push(`Non-existing class found: ${className}`); + + for (const className of classesDiff.missing) { + if (className === 'PuppeteerNode') { + continue; + } + errors.push(`Class not found: ${className}`); + } + + for (const className of classesDiff.equal) { + const actualClass = actual.classes.get(className); + const expectedClass = expected.classes.get(className); + const actualMethods = Array.from(actualClass.methods.keys()).sort(); + const expectedMethods = Array.from(expectedClass.methods.keys()).sort(); + const methodDiff = diff(actualMethods, expectedMethods); + + for (const methodName of methodDiff.extra) { + if ( + expectedPuppeteerClassMissingMethods.has(methodName) && + actualClass.name === 'Puppeteer' + ) { + continue; + } + errors.push(`Non-existing method found: ${className}.${methodName}()`); + } + + for (const methodName of methodDiff.missing) { + const missingMethodsForClass = expectedNotFoundMethods.get(className); + if (missingMethodsForClass && missingMethodsForClass.has(methodName)) + continue; + errors.push(`Method not found: ${className}.${methodName}()`); + } + + for (const methodName of methodDiff.equal) { + const actualMethod = actualClass.methods.get(methodName); + const expectedMethod = expectedClass.methods.get(methodName); + if (!actualMethod.type !== !expectedMethod.type) { + if (actualMethod.type) + errors.push( + `Method ${className}.${methodName} has unneeded description of return type` + ); + else + errors.push( + `Method ${className}.${methodName} is missing return type description` + ); + } else if (actualMethod.hasReturn) { + checkType( + `Method ${className}.${methodName} has the wrong return type: `, + actualMethod.type, + expectedMethod.type + ); + } + const actualArgs = Array.from(actualMethod.args.keys()); + const expectedArgs = Array.from(expectedMethod.args.keys()); + const argsDiff = diff(actualArgs, expectedArgs); + + if (argsDiff.extra.length || argsDiff.missing.length) { + /* Doclint cannot handle the parameter type of the CDPSession send method. + * so we just ignore it. + */ + const isCdpSessionSend = + className === 'CDPSession' && methodName === 'send'; + if (!isCdpSessionSend) { + const text = [ + `Method ${className}.${methodName}() fails to describe its parameters:`, + ]; + for (const arg of argsDiff.missing) + text.push(`- Argument not found: ${arg}`); + for (const arg of argsDiff.extra) + text.push(`- Non-existing argument found: ${arg}`); + errors.push(text.join('\n')); + } + } + + for (const arg of argsDiff.equal) + checkProperty( + `Method ${className}.${methodName}()`, + actualMethod.args.get(arg), + expectedMethod.args.get(arg) + ); + } + const actualProperties = Array.from(actualClass.properties.keys()).sort(); + const expectedProperties = Array.from( + expectedClass.properties.keys() + ).sort(); + const propertyDiff = diff(actualProperties, expectedProperties); + for (const propertyName of propertyDiff.extra) { + if (className === 'Puppeteer' && propertyName === 'product') { + continue; + } + errors.push(`Non-existing property found: ${className}.${propertyName}`); + } + for (const propertyName of propertyDiff.missing) + errors.push(`Property not found: ${className}.${propertyName}`); + + const actualEvents = Array.from(actualClass.events.keys()).sort(); + const expectedEvents = Array.from(expectedClass.events.keys()).sort(); + const eventsDiff = diff(actualEvents, expectedEvents); + for (const eventName of eventsDiff.extra) + errors.push( + `Non-existing event found in class ${className}: '${eventName}'` + ); + for (const eventName of eventsDiff.missing) + errors.push(`Event not found in class ${className}: '${eventName}'`); + } + + /** + * @param {string} source + * @param {!Documentation.Member} actual + * @param {!Documentation.Member} expected + */ + function checkProperty(source, actual, expected) { + checkType(source + ' ' + actual.name, actual.type, expected.type); + } + + /** + * @param {string} source + * @param {!string} actualName + * @param {!string} expectedName + */ + function namingMisMatchInTypeIsExpected(source, actualName, expectedName) { + /* The DocLint tooling doesn't deal well with generics in TypeScript + * source files. We could fix this but the longterm plan is to + * auto-generate documentation from TS. So instead we document here + * the methods that use generics that DocLint trips up on and if it + * finds a mismatch that matches one of the cases below it doesn't + * error. This still means we're protected from accidental changes, as + * if the mismatch doesn't exactly match what's described below + * DocLint will fail. + */ + const expectedNamingMismatches = new Map([ + [ + 'Method CDPSession.send() method', + { + actualName: 'string', + expectedName: 'T', + }, + ], + [ + 'Method CDPSession.send() params', + { + actualName: 'Object', + expectedName: 'CommandParameters[T]', + }, + ], + [ + 'Method ElementHandle.click() options', + { + actualName: 'Object', + expectedName: 'ClickOptions', + }, + ], + [ + 'Method ElementHandle.press() options', + { + actualName: 'Object', + expectedName: 'PressOptions', + }, + ], + [ + 'Method ElementHandle.press() key', + { + actualName: 'string', + expectedName: 'KeyInput', + }, + ], + [ + 'Method Keyboard.down() key', + { + actualName: 'string', + expectedName: 'KeyInput', + }, + ], + [ + 'Method Keyboard.press() key', + { + actualName: 'string', + expectedName: 'KeyInput', + }, + ], + [ + 'Method Keyboard.up() key', + { + actualName: 'string', + expectedName: 'KeyInput', + }, + ], + [ + 'Method Mouse.down() options', + { + actualName: 'Object', + expectedName: 'MouseOptions', + }, + ], + [ + 'Method Mouse.up() options', + { + actualName: 'Object', + expectedName: 'MouseOptions', + }, + ], + [ + 'Method Mouse.wheel() options', + { + actualName: 'Object', + expectedName: 'MouseWheelOptions', + }, + ], + [ + 'Method Tracing.start() options', + { + actualName: 'Object', + expectedName: 'TracingOptions', + }, + ], + [ + 'Method Frame.waitForSelector() options', + { + actualName: 'Object', + expectedName: 'WaitForSelectorOptions', + }, + ], + [ + 'Method Frame.waitForXPath() options', + { + actualName: 'Object', + expectedName: 'WaitForSelectorOptions', + }, + ], + [ + 'Method HTTPRequest.abort() errorCode', + { + actualName: 'string', + expectedName: 'ErrorCode', + }, + ], + [ + 'Method Frame.goto() options.waitUntil', + { + actualName: + '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array', + expectedName: + '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleEvent>', + }, + ], + [ + 'Method Frame.waitForNavigation() options.waitUntil', + { + actualName: + '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array', + expectedName: + '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleEvent>', + }, + ], + [ + 'Method Frame.setContent() options.waitUntil', + { + actualName: + '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array', + expectedName: + '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleEvent>', + }, + ], + [ + 'Method Puppeteer.defaultArgs() options', + { + actualName: 'Object', + expectedName: 'ChromeArgOptions', + }, + ], + [ + 'Method Page.goBack() options.waitUntil', + { + actualName: + '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array', + expectedName: + '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleEvent>', + }, + ], + [ + 'Method Page.goForward() options.waitUntil', + { + actualName: + '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array', + expectedName: + '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleEvent>', + }, + ], + [ + 'Method Page.goto() options.waitUntil', + { + actualName: + '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array', + expectedName: + '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleEvent>', + }, + ], + [ + 'Method Page.reload() options.waitUntil', + { + actualName: + '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array', + expectedName: + '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleEvent>', + }, + ], + [ + 'Method Page.setContent() options.waitUntil', + { + actualName: + '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array', + expectedName: + '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleEvent>', + }, + ], + [ + 'Method Page.waitForNavigation() options.waitUntil', + { + actualName: + '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array', + expectedName: + '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleEvent>', + }, + ], + [ + 'Method BrowserContext.overridePermissions() permissions', + { + actualName: 'Array<string>', + expectedName: 'Array<PermissionType>', + }, + ], + [ + 'Method Puppeteer.createBrowserFetcher() options', + { + actualName: 'Object', + expectedName: 'BrowserFetcherOptions', + }, + ], + [ + 'Method Page.authenticate() credentials', + { + actualName: 'Object', + expectedName: 'Credentials', + }, + ], + [ + 'Method Page.emulateMediaFeatures() features', + { + actualName: 'Array<Object>', + expectedName: 'Array<MediaFeature>', + }, + ], + [ + 'Method Page.emulate() options.viewport', + { + actualName: 'Object', + expectedName: 'Viewport', + }, + ], + [ + 'Method Page.setViewport() options.viewport', + { + actualName: 'Object', + expectedName: 'Viewport', + }, + ], + [ + 'Method Page.setViewport() viewport', + { + actualName: 'Object', + expectedName: 'Viewport', + }, + ], + [ + 'Method Page.connect() options.defaultViewport', + { + actualName: 'Object', + expectedName: 'Viewport', + }, + ], + [ + 'Method Puppeteer.connect() options.defaultViewport', + { + actualName: 'Object', + expectedName: 'Viewport', + }, + ], + [ + 'Method Puppeteer.launch() options.defaultViewport', + { + actualName: 'Object', + expectedName: 'Viewport', + }, + ], + [ + 'Method Page.launch() options.defaultViewport', + { + actualName: 'Object', + expectedName: 'Viewport', + }, + ], + [ + 'Method Page.goBack() options', + { + actualName: 'Object', + expectedName: 'WaitForOptions', + }, + ], + [ + 'Method Page.goForward() options', + { + actualName: 'Object', + expectedName: 'WaitForOptions', + }, + ], + [ + 'Method Page.reload() options', + { + actualName: 'Object', + expectedName: 'WaitForOptions', + }, + ], + [ + 'Method Page.waitForNavigation() options', + { + actualName: 'Object', + expectedName: 'WaitForOptions', + }, + ], + [ + 'Method Page.pdf() options', + { + actualName: 'Object', + expectedName: 'PDFOptions', + }, + ], + [ + 'Method Page.screenshot() options', + { + actualName: 'Object', + expectedName: 'ScreenshotOptions', + }, + ], + [ + 'Method Page.setContent() options', + { + actualName: 'Object', + expectedName: 'WaitForOptions', + }, + ], + [ + 'Method Page.setCookie() ...cookies', + { + actualName: '...Object', + expectedName: '...CookieParam', + }, + ], + [ + 'Method Page.emulateVisionDeficiency() type', + { + actualName: 'string', + expectedName: 'Object', + }, + ], + [ + 'Method Accessibility.snapshot() options', + { + actualName: 'Object', + expectedName: 'SnapshotOptions', + }, + ], + [ + 'Method Browser.waitForTarget() options', + { + actualName: 'Object', + expectedName: 'WaitForTargetOptions', + }, + ], + [ + 'Method EventEmitter.emit() event', + { + actualName: 'string|symbol', + expectedName: 'EventType', + }, + ], + [ + 'Method EventEmitter.listenerCount() event', + { + actualName: 'string|symbol', + expectedName: 'EventType', + }, + ], + [ + 'Method EventEmitter.off() event', + { + actualName: 'string|symbol', + expectedName: 'EventType', + }, + ], + [ + 'Method EventEmitter.on() event', + { + actualName: 'string|symbol', + expectedName: 'EventType', + }, + ], + [ + 'Method EventEmitter.once() event', + { + actualName: 'string|symbol', + expectedName: 'EventType', + }, + ], + [ + 'Method EventEmitter.removeListener() event', + { + actualName: 'string|symbol', + expectedName: 'EventType', + }, + ], + [ + 'Method EventEmitter.addListener() event', + { + actualName: 'string|symbol', + expectedName: 'EventType', + }, + ], + [ + 'Method EventEmitter.removeAllListeners() event', + { + actualName: 'string|symbol', + expectedName: 'EventType', + }, + ], + [ + 'Method Coverage.startCSSCoverage() options', + { + actualName: 'Object', + expectedName: 'CSSCoverageOptions', + }, + ], + [ + 'Method Coverage.startJSCoverage() options', + { + actualName: 'Object', + expectedName: 'JSCoverageOptions', + }, + ], + [ + 'Method Mouse.click() options.button', + { + actualName: '"left"|"right"|"middle"', + expectedName: 'MouseButton', + }, + ], + [ + 'Method Frame.click() options.button', + { + actualName: '"left"|"right"|"middle"', + expectedName: 'MouseButton', + }, + ], + [ + 'Method Page.click() options.button', + { + actualName: '"left"|"right"|"middle"', + expectedName: 'MouseButton', + }, + ], + [ + 'Method HTTPRequest.continue() overrides', + { + actualName: 'Object', + expectedName: 'ContinueRequestOverrides', + }, + ], + [ + 'Method HTTPRequest.respond() response', + { + actualName: 'Object', + expectedName: 'ResponseForRequest', + }, + ], + [ + 'Method Frame.addScriptTag() options', + { + actualName: 'Object', + expectedName: 'FrameAddScriptTagOptions', + }, + ], + [ + 'Method Frame.addStyleTag() options', + { + actualName: 'Object', + expectedName: 'FrameAddStyleTagOptions', + }, + ], + [ + 'Method Frame.waitForFunction() options', + { + actualName: 'Object', + expectedName: 'FrameWaitForFunctionOptions', + }, + ], + [ + 'Method BrowserContext.overridePermissions() permissions', + { + actualName: 'Array<string>', + expectedName: 'Array<Object>', + }, + ], + [ + 'Method Puppeteer.connect() options', + { + actualName: 'Object', + expectedName: 'ConnectOptions', + }, + ], + ]); + + const expectedForSource = expectedNamingMismatches.get(source); + if (!expectedForSource) return false; + + const namingMismatchIsExpected = + expectedForSource.actualName === actualName && + expectedForSource.expectedName === expectedName; + + return namingMismatchIsExpected; + } + + /** + * @param {string} source + * @param {!Documentation.Type} actual + * @param {!Documentation.Type} expected + */ + function checkType(source, actual, expected) { + // TODO(@JoelEinbinder): check functions and Serializable + if (actual.name.includes('unction') || actual.name.includes('Serializable')) + return; + // We don't have nullchecks on for TypeScript + const actualName = actual.name.replace(/[\? ]/g, ''); + // TypeScript likes to add some spaces + const expectedName = expected.name.replace(/\ /g, ''); + const namingMismatchIsExpected = namingMisMatchInTypeIsExpected( + source, + actualName, + expectedName + ); + if (expectedName !== actualName && !namingMismatchIsExpected) + errors.push(`${source} ${actualName} != ${expectedName}`); + + /* If we got a naming mismatch and it was expected, don't check the properties + * as they will likely be considered "wrong" by DocLint too. + */ + if (namingMismatchIsExpected) return; + + /* Some methods cause errors in the property checks for an unknown reason + * so we support a list of methods whose parameters are not checked. + */ + const skipPropertyChecksOnMethods = new Set([ + 'Method Page.deleteCookie() ...cookies', + 'Method Page.setCookie() ...cookies', + 'Method Puppeteer.connect() options', + ]); + if (skipPropertyChecksOnMethods.has(source)) return; + + const actualPropertiesMap = new Map( + actual.properties.map((property) => [property.name, property.type]) + ); + const expectedPropertiesMap = new Map( + expected.properties.map((property) => [property.name, property.type]) + ); + const propertiesDiff = diff( + Array.from(actualPropertiesMap.keys()).sort(), + Array.from(expectedPropertiesMap.keys()).sort() + ); + for (const propertyName of propertiesDiff.extra) + errors.push(`${source} has unexpected property ${propertyName}`); + for (const propertyName of propertiesDiff.missing) + errors.push(`${source} is missing property ${propertyName}`); + for (const propertyName of propertiesDiff.equal) + checkType( + source + '.' + propertyName, + actualPropertiesMap.get(propertyName), + expectedPropertiesMap.get(propertyName) + ); + } + + return errors; +} + +/** + * @param {!Array<string>} actual + * @param {!Array<string>} expected + * @returns {{extra: !Array<string>, missing: !Array<string>, equal: !Array<string>}} + */ +function diff(actual, expected) { + const N = actual.length; + const M = expected.length; + if (N === 0 && M === 0) return { extra: [], missing: [], equal: [] }; + if (N === 0) return { extra: [], missing: expected.slice(), equal: [] }; + if (M === 0) return { extra: actual.slice(), missing: [], equal: [] }; + const d = new Array(N); + const bt = new Array(N); + for (let i = 0; i < N; ++i) { + d[i] = new Array(M); + bt[i] = new Array(M); + for (let j = 0; j < M; ++j) { + const top = val(i - 1, j); + const left = val(i, j - 1); + if (top > left) { + d[i][j] = top; + bt[i][j] = 'extra'; + } else { + d[i][j] = left; + bt[i][j] = 'missing'; + } + const diag = val(i - 1, j - 1); + if (actual[i] === expected[j] && d[i][j] < diag + 1) { + d[i][j] = diag + 1; + bt[i][j] = 'eq'; + } + } + } + // Backtrack results. + let i = N - 1; + let j = M - 1; + const missing = []; + const extra = []; + const equal = []; + while (i >= 0 && j >= 0) { + switch (bt[i][j]) { + case 'extra': + extra.push(actual[i]); + i -= 1; + break; + case 'missing': + missing.push(expected[j]); + j -= 1; + break; + case 'eq': + equal.push(actual[i]); + i -= 1; + j -= 1; + break; + } + } + while (i >= 0) extra.push(actual[i--]); + while (j >= 0) missing.push(expected[j--]); + extra.reverse(); + missing.reverse(); + equal.reverse(); + return { extra, missing, equal }; + + function val(i, j) { + return i < 0 || j < 0 ? 0 : d[i][j]; + } +} diff --git a/remote/test/puppeteer/utils/doclint/cli.js b/remote/test/puppeteer/utils/doclint/cli.js new file mode 100755 index 0000000000..75775c65e5 --- /dev/null +++ b/remote/test/puppeteer/utils/doclint/cli.js @@ -0,0 +1,136 @@ +#!/usr/bin/env node +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const puppeteer = require('../..'); +const path = require('path'); +const Source = require('./Source'); + +const PROJECT_DIR = path.join(__dirname, '..', '..'); +const VERSION = require(path.join(PROJECT_DIR, 'package.json')).version; + +const RED_COLOR = '\x1b[31m'; +const YELLOW_COLOR = '\x1b[33m'; +const RESET_COLOR = '\x1b[0m'; + +run(); + +async function run() { + const startTime = Date.now(); + + /** @type {!Array<!Message>} */ + const messages = []; + let changedFiles = false; + + if (!VERSION.endsWith('-post')) { + const versions = await Source.readFile( + path.join(PROJECT_DIR, 'versions.js') + ); + versions.setText(versions.text().replace(`, 'NEXT'],`, `, '${VERSION}'],`)); + await versions.save(); + } + + // Documentation checks. + const readme = await Source.readFile(path.join(PROJECT_DIR, 'README.md')); + const contributing = await Source.readFile( + path.join(PROJECT_DIR, 'CONTRIBUTING.md') + ); + const api = await Source.readFile(path.join(PROJECT_DIR, 'docs', 'api.md')); + const troubleshooting = await Source.readFile( + path.join(PROJECT_DIR, 'docs', 'troubleshooting.md') + ); + const mdSources = [readme, api, troubleshooting, contributing]; + + const preprocessor = require('./preprocessor'); + messages.push(...(await preprocessor.runCommands(mdSources, VERSION))); + messages.push( + ...(await preprocessor.ensureReleasedAPILinks([readme], VERSION)) + ); + + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + const checkPublicAPI = require('./check_public_api'); + const tsSources = [ + /* Source.readdir doesn't deal with nested directories well. + * Rather than invest time here when we're going to remove this Doc tooling soon + * we'll just list the directories manually. + */ + ...(await Source.readdir(path.join(PROJECT_DIR, 'src'), 'ts')), + ...(await Source.readdir(path.join(PROJECT_DIR, 'src', 'common'), 'ts')), + ...(await Source.readdir(path.join(PROJECT_DIR, 'src', 'node'), 'ts')), + ]; + + const tsSourcesNoDefinitions = tsSources.filter( + (source) => !source.filePath().endsWith('.d.ts') + ); + + const jsSources = [ + ...(await Source.readdir(path.join(PROJECT_DIR, 'lib'))), + ...(await Source.readdir(path.join(PROJECT_DIR, 'lib', 'cjs'))), + ...(await Source.readdir( + path.join(PROJECT_DIR, 'lib', 'cjs', 'puppeteer', 'common') + )), + ...(await Source.readdir( + path.join(PROJECT_DIR, 'lib', 'cjs', 'puppeteer', 'node') + )), + ]; + const allSrcCode = [...jsSources, ...tsSourcesNoDefinitions]; + messages.push(...(await checkPublicAPI(page, mdSources, allSrcCode))); + + await browser.close(); + + for (const source of mdSources) { + if (!source.hasUpdatedText()) continue; + await source.save(); + changedFiles = true; + } + + // Report results. + const errors = messages.filter((message) => message.type === 'error'); + if (errors.length) { + console.log('DocLint Failures:'); + for (let i = 0; i < errors.length; ++i) { + let error = errors[i].text; + error = error.split('\n').join('\n '); + console.log(` ${i + 1}) ${RED_COLOR}${error}${RESET_COLOR}`); + } + } + const warnings = messages.filter((message) => message.type === 'warning'); + if (warnings.length) { + console.log('DocLint Warnings:'); + for (let i = 0; i < warnings.length; ++i) { + let warning = warnings[i].text; + warning = warning.split('\n').join('\n '); + console.log(` ${i + 1}) ${YELLOW_COLOR}${warning}${RESET_COLOR}`); + } + } + let clearExit = messages.length === 0; + if (changedFiles) { + if (clearExit) + console.log(`${YELLOW_COLOR}Some files were updated.${RESET_COLOR}`); + clearExit = false; + } + console.log(`${errors.length} failures, ${warnings.length} warnings.`); + + if (!clearExit && !process.env.TRAVIS) + console.log( + '\nIs your lib/ directory up to date? You might need to `npm run tsc`.\n' + ); + + const runningTime = Date.now() - startTime; + console.log(`DocLint Finished in ${runningTime / 1000} seconds`); + process.exit(clearExit ? 0 : 1); +} diff --git a/remote/test/puppeteer/utils/doclint/preprocessor/index.js b/remote/test/puppeteer/utils/doclint/preprocessor/index.js new file mode 100644 index 0000000000..340d973c9a --- /dev/null +++ b/remote/test/puppeteer/utils/doclint/preprocessor/index.js @@ -0,0 +1,165 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const Message = require('../Message'); + +module.exports.ensureReleasedAPILinks = function (sources, version) { + // Release version is everything that doesn't include "-". + const apiLinkRegex = /https:\/\/github.com\/puppeteer\/puppeteer\/blob\/v[^/]*\/docs\/api.md/gi; + const lastReleasedAPI = `https://github.com/puppeteer/puppeteer/blob/v${ + version.split('-')[0] + }/docs/api.md`; + + const messages = []; + for (const source of sources) { + const text = source.text(); + const newText = text.replace(apiLinkRegex, lastReleasedAPI); + if (source.setText(newText)) + messages.push(Message.warning(`GEN: updated ${source.projectPath()}`)); + } + return messages; +}; + +module.exports.runCommands = function (sources, version) { + // Release version is everything that doesn't include "-". + const isReleaseVersion = !version.includes('-'); + + const messages = []; + const commands = []; + for (const source of sources) { + const text = source.text(); + const commandStartRegex = /<!--\s*gen:([a-z-]+)\s*-->/gi; + const commandEndRegex = /<!--\s*gen:stop\s*-->/gi; + let start; + + while ((start = commandStartRegex.exec(text))) { + // eslint-disable-line no-cond-assign + commandEndRegex.lastIndex = commandStartRegex.lastIndex; + const end = commandEndRegex.exec(text); + if (!end) { + messages.push( + Message.error(`Failed to find 'gen:stop' for command ${start[0]}`) + ); + return messages; + } + const name = start[1]; + const from = commandStartRegex.lastIndex; + const to = end.index; + const originalText = text.substring(from, to); + commands.push({ name, from, to, originalText, source }); + commandStartRegex.lastIndex = commandEndRegex.lastIndex; + } + } + + const changedSources = new Set(); + // Iterate commands in reverse order so that edits don't conflict. + commands.sort((a, b) => b.from - a.from); + for (const command of commands) { + let newText = null; + if (command.name === 'version') + newText = isReleaseVersion ? 'v' + version : 'Tip-Of-Tree'; + else if (command.name === 'empty-if-release') + newText = isReleaseVersion ? '' : command.originalText; + else if (command.name === 'toc') + newText = generateTableOfContents( + command.source.text().substring(command.to) + ); + else if (command.name === 'versions-per-release') + newText = generateVersionsPerRelease(); + if (newText === null) + messages.push(Message.error(`Unknown command 'gen:${command.name}'`)); + else if (applyCommand(command, newText)) changedSources.add(command.source); + } + for (const source of changedSources) + messages.push(Message.warning(`GEN: updated ${source.projectPath()}`)); + return messages; +}; + +/** + * @param {{name: string, from: number, to: number, source: !Source}} command + * @param {string} editText + * @returns {boolean} + */ +function applyCommand(command, editText) { + const text = command.source.text(); + const newText = + text.substring(0, command.from) + editText + text.substring(command.to); + return command.source.setText(newText); +} + +function generateTableOfContents(mdText) { + const ids = new Set(); + const titles = []; + let insideCodeBlock = false; + for (const aLine of mdText.split('\n')) { + const line = aLine.trim(); + if (line.startsWith('```')) { + insideCodeBlock = !insideCodeBlock; + continue; + } + if (!insideCodeBlock && line.startsWith('#')) titles.push(line); + } + const tocEntries = []; + for (const title of titles) { + const [, nesting, name] = title.match(/^(#+)\s+(.*)$/); + const delinkifiedName = name.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1'); + const id = delinkifiedName + .trim() + .toLowerCase() + .replace(/\s/g, '-') + .replace(/[^-0-9a-zа-яё]/gi, ''); + let dedupId = id; + let counter = 0; + while (ids.has(dedupId)) dedupId = id + '-' + ++counter; + ids.add(dedupId); + tocEntries.push({ + level: nesting.length, + name: delinkifiedName, + id: dedupId, + }); + } + + const minLevel = Math.min(...tocEntries.map((entry) => entry.level)); + tocEntries.forEach((entry) => (entry.level -= minLevel)); + return ( + '\n' + + tocEntries + .map((entry) => { + const prefix = entry.level % 2 === 0 ? '-' : '*'; + const padding = ' '.repeat(entry.level); + return `${padding}${prefix} [${entry.name}](#${entry.id})`; + }) + .join('\n') + + '\n' + ); +} + +const generateVersionsPerRelease = () => { + const versionsPerRelease = require('../../../versions.js'); + const buffer = ['- Releases per Chromium version:']; + for (const [chromiumVersion, puppeteerVersion] of versionsPerRelease) { + if (puppeteerVersion === 'NEXT') continue; + buffer.push( + ` * Chromium ${chromiumVersion} - [Puppeteer ${puppeteerVersion}](https://github.com/puppeteer/puppeteer/blob/${puppeteerVersion}/docs/api.md)` + ); + } + buffer.push( + ` * [All releases](https://github.com/puppeteer/puppeteer/releases)` + ); + + const output = '\n' + buffer.join('\n') + '\n'; + return output; +}; diff --git a/remote/test/puppeteer/utils/doclint/preprocessor/preprocessor.spec.js b/remote/test/puppeteer/utils/doclint/preprocessor/preprocessor.spec.js new file mode 100644 index 0000000000..713785db8f --- /dev/null +++ b/remote/test/puppeteer/utils/doclint/preprocessor/preprocessor.spec.js @@ -0,0 +1,248 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const { runCommands, ensureReleasedAPILinks } = require('.'); +const Source = require('../Source'); +const expect = require('expect'); + +describe('doclint preprocessor specs', function () { + describe('ensureReleasedAPILinks', function () { + it('should work with non-release version', function () { + const source = new Source( + 'doc.md', + ` + [API](https://github.com/puppeteer/puppeteer/blob/v1.1.0/docs/api.md#class-page) + ` + ); + const messages = ensureReleasedAPILinks([source], '1.3.0-post'); + expect(messages.length).toBe(1); + expect(messages[0].type).toBe('warning'); + expect(messages[0].text).toContain('doc.md'); + expect(source.text()).toBe(` + [API](https://github.com/puppeteer/puppeteer/blob/v1.3.0/docs/api.md#class-page) + `); + }); + it('should work with release version', function () { + const source = new Source( + 'doc.md', + ` + [API](https://github.com/puppeteer/puppeteer/blob/v1.1.0/docs/api.md#class-page) + ` + ); + const messages = ensureReleasedAPILinks([source], '1.3.0'); + expect(messages.length).toBe(1); + expect(messages[0].type).toBe('warning'); + expect(messages[0].text).toContain('doc.md'); + expect(source.text()).toBe(` + [API](https://github.com/puppeteer/puppeteer/blob/v1.3.0/docs/api.md#class-page) + `); + }); + it('should keep main branch links intact', function () { + const source = new Source( + 'doc.md', + ` + [API](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#class-page) + ` + ); + const messages = ensureReleasedAPILinks([source], '1.3.0'); + expect(messages.length).toBe(0); + expect(source.text()).toBe(` + [API](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#class-page) + `); + }); + }); + + describe('runCommands', function () { + it('should throw for unknown command', function () { + const source = new Source( + 'doc.md', + ` + <!-- gen:unknown-command -->something<!-- gen:stop --> + ` + ); + const messages = runCommands([source], '1.1.1'); + expect(source.hasUpdatedText()).toBe(false); + expect(messages.length).toBe(1); + expect(messages[0].type).toBe('error'); + expect(messages[0].text).toContain('Unknown command'); + }); + describe('gen:version', function () { + it('should work', function () { + const source = new Source( + 'doc.md', + ` + Puppeteer <!-- gen:version -->XXX<!-- gen:stop --> + ` + ); + const messages = runCommands([source], '1.2.0'); + expect(messages.length).toBe(1); + expect(messages[0].type).toBe('warning'); + expect(messages[0].text).toContain('doc.md'); + expect(source.text()).toBe(` + Puppeteer <!-- gen:version -->v1.2.0<!-- gen:stop --> + `); + }); + it('should work for *-post versions', function () { + const source = new Source( + 'doc.md', + ` + Puppeteer <!-- gen:version -->XXX<!-- gen:stop --> + ` + ); + const messages = runCommands([source], '1.2.0-post'); + expect(messages.length).toBe(1); + expect(messages[0].type).toBe('warning'); + expect(messages[0].text).toContain('doc.md'); + expect(source.text()).toBe(` + Puppeteer <!-- gen:version -->Tip-Of-Tree<!-- gen:stop --> + `); + }); + it('should tolerate different writing', function () { + const source = new Source( + 'doc.md', + `Puppeteer v<!-- gEn:version -->WHAT +<!-- GEN:stop -->` + ); + runCommands([source], '1.1.1'); + expect(source.text()).toBe( + `Puppeteer v<!-- gEn:version -->v1.1.1<!-- GEN:stop -->` + ); + }); + it('should not tolerate missing gen:stop', function () { + const source = new Source('doc.md', `<!--GEN:version-->`); + const messages = runCommands([source], '1.2.0'); + expect(source.hasUpdatedText()).toBe(false); + expect(messages.length).toBe(1); + expect(messages[0].type).toBe('error'); + expect(messages[0].text).toContain(`Failed to find 'gen:stop'`); + }); + }); + describe('gen:empty-if-release', function () { + it('should clear text when release version', function () { + const source = new Source( + 'doc.md', + ` + <!-- gen:empty-if-release -->XXX<!-- gen:stop --> + ` + ); + const messages = runCommands([source], '1.1.1'); + expect(messages.length).toBe(1); + expect(messages[0].type).toBe('warning'); + expect(messages[0].text).toContain('doc.md'); + expect(source.text()).toBe(` + <!-- gen:empty-if-release --><!-- gen:stop --> + `); + }); + it('should keep text when non-release version', function () { + const source = new Source( + 'doc.md', + ` + <!-- gen:empty-if-release -->XXX<!-- gen:stop --> + ` + ); + const messages = runCommands([source], '1.1.1-post'); + expect(messages.length).toBe(0); + expect(source.text()).toBe(` + <!-- gen:empty-if-release -->XXX<!-- gen:stop --> + `); + }); + }); + describe('gen:toc', function () { + it('should work', () => { + const source = new Source( + 'doc.md', + `<!-- gen:toc -->XXX<!-- gen:stop --> + ### class: page + #### page.$ + #### page.$$` + ); + const messages = runCommands([source], '1.3.0'); + expect(messages.length).toBe(1); + expect(messages[0].type).toBe('warning'); + expect(messages[0].text).toContain('doc.md'); + expect(source.text()).toBe(`<!-- gen:toc --> +- [class: page](#class-page) + * [page.$](#page) + * [page.$$](#page-1) +<!-- gen:stop --> + ### class: page + #### page.$ + #### page.$$`); + }); + it('should work with code blocks', () => { + const source = new Source( + 'doc.md', + `<!-- gen:toc -->XXX<!-- gen:stop --> + ### class: page + + \`\`\`bash + # yo comment + \`\`\` + ` + ); + const messages = runCommands([source], '1.3.0'); + expect(messages.length).toBe(1); + expect(messages[0].type).toBe('warning'); + expect(messages[0].text).toContain('doc.md'); + expect(source.text()).toBe(`<!-- gen:toc --> +- [class: page](#class-page) +<!-- gen:stop --> + ### class: page + + \`\`\`bash + # yo comment + \`\`\` + `); + }); + it('should work with links in titles', () => { + const source = new Source( + 'doc.md', + `<!-- gen:toc -->XXX<!-- gen:stop --> + ### some [link](#foobar) here + ` + ); + const messages = runCommands([source], '1.3.0'); + expect(messages.length).toBe(1); + expect(messages[0].type).toBe('warning'); + expect(messages[0].text).toContain('doc.md'); + expect(source.text()).toBe(`<!-- gen:toc --> +- [some link here](#some-link-here) +<!-- gen:stop --> + ### some [link](#foobar) here + `); + }); + }); + it('should work with multiple commands', function () { + const source = new Source( + 'doc.md', + ` + <!-- gen:version -->XXX<!-- gen:stop --> + <!-- gen:empty-if-release -->YYY<!-- gen:stop --> + <!-- gen:version -->ZZZ<!-- gen:stop --> + ` + ); + const messages = runCommands([source], '1.1.1'); + expect(messages.length).toBe(1); + expect(messages[0].type).toBe('warning'); + expect(messages[0].text).toContain('doc.md'); + expect(source.text()).toBe(` + <!-- gen:version -->v1.1.1<!-- gen:stop --> + <!-- gen:empty-if-release --><!-- gen:stop --> + <!-- gen:version -->v1.1.1<!-- gen:stop --> + `); + }); + }); +}); diff --git a/remote/test/puppeteer/utils/fetch_devices.js b/remote/test/puppeteer/utils/fetch_devices.js new file mode 100755 index 0000000000..547b68842e --- /dev/null +++ b/remote/test/puppeteer/utils/fetch_devices.js @@ -0,0 +1,282 @@ +#!/usr/bin/env node +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const util = require('util'); +const fs = require('fs'); +const path = require('path'); +const puppeteer = require('..'); +const DEVICES_URL = + 'https://raw.githubusercontent.com/ChromeDevTools/devtools-frontend/master/front_end/emulated_devices/module.json'; + +const template = `/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = %s; +for (const device of module.exports) + module.exports[device.name] = device; +`; + +const help = `Usage: node ${path.basename(__filename)} [-u <from>] <outputPath> + -u, --url The URL to load devices descriptor from. If not set, + devices will be fetched from the tip-of-tree of DevTools + frontend. + + -h, --help Show this help message + +Fetch Chrome DevTools front-end emulation devices from given URL, convert them to puppeteer +devices and save to the <outputPath>. +`; + +const argv = require('minimist')(process.argv.slice(2), { + alias: { u: 'url', h: 'help' }, +}); + +if (argv.help) { + console.log(help); + return; +} + +const url = argv.url || DEVICES_URL; +const outputPath = argv._[0]; +if (!outputPath) { + console.log('ERROR: output file name is missing. Use --help for help.'); + return; +} + +main(url); + +async function main(url) { + const browser = await puppeteer.launch(); + const chromeVersion = (await browser.version()).split('/').pop(); + await browser.close(); + console.log('GET ' + url); + const text = await httpGET(url); + let json = null; + try { + json = JSON.parse(text); + } catch (error) { + console.error(`FAILED: error parsing response - ${error.message}`); + return; + } + const devicePayloads = json.extensions + .filter((extension) => extension.type === 'emulated-device') + .map((extension) => extension.device); + let devices = []; + for (const payload of devicePayloads) { + let names = []; + if (payload.title === 'iPhone 6/7/8') + names = ['iPhone 6', 'iPhone 7', 'iPhone 8']; + else if (payload.title === 'iPhone 6/7/8 Plus') + names = ['iPhone 6 Plus', 'iPhone 7 Plus', 'iPhone 8 Plus']; + else if (payload.title === 'iPhone 5/SE') names = ['iPhone 5', 'iPhone SE']; + else names = [payload.title]; + for (const name of names) { + const device = createDevice(chromeVersion, name, payload, false); + const landscape = createDevice(chromeVersion, name, payload, true); + devices.push(device); + if ( + landscape.viewport.width !== device.viewport.width || + landscape.viewport.height !== device.viewport.height + ) + devices.push(landscape); + } + } + devices = devices.filter((device) => device.viewport.isMobile); + devices.sort((a, b) => a.name.localeCompare(b.name)); + // Use single-quotes instead of double-quotes to conform with codestyle. + const serialized = JSON.stringify(devices, null, 2) + .replace(/'/g, `\\'`) + .replace(/"/g, `'`); + const result = util.format(template, serialized); + fs.writeFileSync(outputPath, result, 'utf8'); +} + +/** + * @param {string} chromeVersion + * @param {string} deviceName + * @param {*} descriptor + * @param {boolean} landscape + * @returns {!Object} + */ +function createDevice(chromeVersion, deviceName, descriptor, landscape) { + const devicePayload = loadFromJSONV1(descriptor); + const viewportPayload = landscape + ? devicePayload.horizontal + : devicePayload.vertical; + return { + name: deviceName + (landscape ? ' landscape' : ''), + userAgent: devicePayload.userAgent.includes('%s') + ? util.format(devicePayload.userAgent, chromeVersion) + : devicePayload.userAgent, + viewport: { + width: viewportPayload.width, + height: viewportPayload.height, + deviceScaleFactor: devicePayload.deviceScaleFactor, + isMobile: devicePayload.capabilities.includes('mobile'), + hasTouch: devicePayload.capabilities.includes('touch'), + isLandscape: landscape || false, + }, + }; +} + +/** + * @param {*} json + * @returns {?Object} + */ +function loadFromJSONV1(json) { + /** + * @param {*} object + * @param {string} key + * @param {string} type + * @param {*=} defaultValue + * @returns {*} + */ + function parseValue(object, key, type, defaultValue) { + if ( + typeof object !== 'object' || + object === null || + !object.hasOwnProperty(key) + ) { + if (typeof defaultValue !== 'undefined') return defaultValue; + throw new Error( + "Emulated device is missing required property '" + key + "'" + ); + } + const value = object[key]; + if (typeof value !== type || value === null) + throw new Error( + "Emulated device property '" + + key + + "' has wrong type '" + + typeof value + + "'" + ); + return value; + } + + /** + * @param {*} object + * @param {string} key + * @returns {number} + */ + function parseIntValue(object, key) { + const value = /** @type {number} */ (parseValue(object, key, 'number')); + if (value !== Math.abs(value)) + throw new Error("Emulated device value '" + key + "' must be integer"); + return value; + } + + /** + * @param {*} json + * @returns {!{width: number, height: number}} + */ + function parseOrientation(json) { + const result = {}; + const minDeviceSize = 50; + const maxDeviceSize = 9999; + result.width = parseIntValue(json, 'width'); + if ( + result.width < 0 || + result.width > maxDeviceSize || + result.width < minDeviceSize + ) + throw new Error('Emulated device has wrong width: ' + result.width); + + result.height = parseIntValue(json, 'height'); + if ( + result.height < 0 || + result.height > maxDeviceSize || + result.height < minDeviceSize + ) + throw new Error('Emulated device has wrong height: ' + result.height); + + return /** @type {!{width: number, height: number}} */ (result); + } + + const result = {}; + result.type = /** @type {string} */ (parseValue(json, 'type', 'string')); + result.userAgent = /** @type {string} */ (parseValue( + json, + 'user-agent', + 'string' + )); + + const capabilities = parseValue(json, 'capabilities', 'object', []); + if (!Array.isArray(capabilities)) + throw new Error('Emulated device capabilities must be an array'); + result.capabilities = []; + for (let i = 0; i < capabilities.length; ++i) { + if (typeof capabilities[i] !== 'string') + throw new Error('Emulated device capability must be a string'); + result.capabilities.push(capabilities[i]); + } + + result.deviceScaleFactor = /** @type {number} */ (parseValue( + json['screen'], + 'device-pixel-ratio', + 'number' + )); + if (result.deviceScaleFactor < 0 || result.deviceScaleFactor > 100) + throw new Error( + 'Emulated device has wrong deviceScaleFactor: ' + result.deviceScaleFactor + ); + + result.vertical = parseOrientation( + parseValue(json['screen'], 'vertical', 'object') + ); + result.horizontal = parseOrientation( + parseValue(json['screen'], 'horizontal', 'object') + ); + return result; +} + +/** + * @param {url} + * @returns {!Promise} + */ +function httpGET(url) { + let fulfill, reject; + const promise = new Promise((res, rej) => { + fulfill = res; + reject = rej; + }); + const driver = url.startsWith('https://') + ? require('https') + : require('http'); + const request = driver.get(url, (response) => { + let data = ''; + response.setEncoding('utf8'); + response.on('data', (chunk) => (data += chunk)); + response.on('end', () => fulfill(data)); + response.on('error', reject); + }); + request.on('error', reject); + return promise; +} diff --git a/remote/test/puppeteer/utils/prepare_puppeteer_core.js b/remote/test/puppeteer/utils/prepare_puppeteer_core.js new file mode 100755 index 0000000000..e1e9a64f2f --- /dev/null +++ b/remote/test/puppeteer/utils/prepare_puppeteer_core.js @@ -0,0 +1,27 @@ +#!/usr/bin/env node +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const fs = require('fs'); +const path = require('path'); + +const packagePath = path.join(__dirname, '..', 'package.json'); +const json = require(packagePath); + +json.name = 'puppeteer-core'; +delete json.scripts.install; +json.main = './cjs-entry-core.js'; +fs.writeFileSync(packagePath, JSON.stringify(json, null, ' ')); diff --git a/remote/test/puppeteer/utils/testserver/LICENSE b/remote/test/puppeteer/utils/testserver/LICENSE new file mode 100644 index 0000000000..afdfe50e72 --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/remote/test/puppeteer/utils/testserver/README.md b/remote/test/puppeteer/utils/testserver/README.md new file mode 100644 index 0000000000..a849eb873d --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/README.md @@ -0,0 +1,18 @@ +# TestServer + +This test server is used internally by Puppeteer to test Puppeteer itself. + +### Example + +```js +const {TestServer} = require('@pptr/testserver'); + +(async(() => { + const httpServer = await TestServer.create(__dirname, 8000), + const httpsServer = await TestServer.createHTTPS(__dirname, 8001) + httpServer.setRoute('/hello', (req, res) => { + res.end('Hello, world!'); + }); + console.log('HTTP and HTTPS servers are running!'); +})(); +``` diff --git a/remote/test/puppeteer/utils/testserver/cert.pem b/remote/test/puppeteer/utils/testserver/cert.pem new file mode 100644 index 0000000000..fd3838535a --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWDCCAkCgAwIBAgIUM8Tmw+D1j+eVz9x9So4zRVqFsKowDQYJKoZIhvcNAQEL +BQAwGjEYMBYGA1UEAwwPcHVwcGV0ZWVyLXRlc3RzMB4XDTIwMDUxMzA4MDQyOVoX +DTMwMDUxMTA4MDQyOVowGjEYMBYGA1UEAwwPcHVwcGV0ZWVyLXRlc3RzMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApWbbhgc6CnWywd8xGETT1mfLi3wi +KIbpAUHghLF4sj0jXz8vLh/4oicpQ12d6bsz+IAi7qrdXNh11P5nEej6/Gx4fWzB +gGdrJFGPqsvXuhYdzZAmy6xOaWcLIJeQ543bXv3YeST7EGRXJBc/ocTo2jIGTGjq +hksFaid910VQlX3KGOLTDMUCk00TeEYBTTUx47PWoIsxVqbl2RzVXRSWL5hlPWlW +29/BQtBGmsXxZyWtqqHudiUulGBSr4LcPyicZLI8nqCqD0ioS0TEmGh61nRBuwBa +xmLCvPmpt0+sDuOU+1bme3w8juvTVToBIFxGB86rADd3ys+8NeZzXqi+bQIDAQAB +o4GVMIGSMB0GA1UdDgQWBBT/m3vdkZpQyVQFdYrKHVoAHXDFODAfBgNVHSMEGDAW +gBT/m3vdkZpQyVQFdYrKHVoAHXDFODAPBgNVHRMBAf8EBTADAQH/MD8GA1UdEQQ4 +MDaCGHd3dy5wdXBwZXRlZXItdGVzdHMudGVzdIIad3d3LnB1cHBldGVlci10ZXN0 +cy0xLnRlc3QwDQYJKoZIhvcNAQELBQADggEBAI1qp5ZppV1R3e8XxzwwkFDPFN8W +Pe3AoqhAKyJnJl1NUn9q3sroEeSQRhODWUHCd7lENzhsT+3mzonNNkN9B/hq0rpK +KHHczXILDqdyuxH3LxQ1VHGE8VN2NbdkfobtzAsA3woiJxOuGeusXJnKB4kJQeIP +V+BMEZWeaSDC2PREkG7GOezmE1/WDUCYaorPw2whdCA5wJvTW3zXpJjYhfsld+5z +KuErx4OCxRJij73/BD9SpLxDEY1cdl819F1IvxsRGhmTIaSly2hQLrhOgo1jgZtV +FGCa6DSlXnQGLaV+N+ssR0lkCksNrNBVDfA1bP5bT/4VCcwUWwm9TUeF0Qo= +-----END CERTIFICATE----- diff --git a/remote/test/puppeteer/utils/testserver/index.js b/remote/test/puppeteer/utils/testserver/index.js new file mode 100644 index 0000000000..7d21009c94 --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/index.js @@ -0,0 +1,284 @@ +// @ts-nocheck + +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const http = require('http'); +const https = require('https'); +const url = require('url'); +const fs = require('fs'); +const path = require('path'); +const mime = require('mime'); +const WebSocketServer = require('ws').Server; + +const fulfillSymbol = Symbol('fullfil callback'); +const rejectSymbol = Symbol('reject callback'); + +class TestServer { + PORT = undefined; + PREFIX = undefined; + CROSS_PROCESS_PREFIX = undefined; + EMPTY_PAGE = undefined; + + /** + * @param {string} dirPath + * @param {number} port + * @returns {!TestServer} + */ + static async create(dirPath, port) { + const server = new TestServer(dirPath, port); + await new Promise((x) => server._server.once('listening', x)); + return server; + } + + /** + * @param {string} dirPath + * @param {number} port + * @returns {!TestServer} + */ + static async createHTTPS(dirPath, port) { + const server = new TestServer(dirPath, port, { + key: fs.readFileSync(path.join(__dirname, 'key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'cert.pem')), + passphrase: 'aaaa', + }); + await new Promise((x) => server._server.once('listening', x)); + return server; + } + + /** + * @param {string} dirPath + * @param {number} port + * @param {!Object=} sslOptions + */ + constructor(dirPath, port, sslOptions) { + if (sslOptions) + this._server = https.createServer(sslOptions, this._onRequest.bind(this)); + else this._server = http.createServer(this._onRequest.bind(this)); + this._server.on('connection', (socket) => this._onSocket(socket)); + this._wsServer = new WebSocketServer({ server: this._server }); + this._wsServer.on('connection', this._onWebSocketConnection.bind(this)); + this._server.listen(port); + this._dirPath = dirPath; + + this._startTime = new Date(); + this._cachedPathPrefix = null; + + /** @type {!Set<!net.Socket>} */ + this._sockets = new Set(); + + /** @type {!Map<string, function(!IncomingMessage, !ServerResponse)>} */ + this._routes = new Map(); + /** @type {!Map<string, !{username:string, password:string}>} */ + this._auths = new Map(); + /** @type {!Map<string, string>} */ + this._csp = new Map(); + /** @type {!Set<string>} */ + this._gzipRoutes = new Set(); + /** @type {!Map<string, !Promise>} */ + this._requestSubscribers = new Map(); + } + + _onSocket(socket) { + this._sockets.add(socket); + // ECONNRESET is a legit error given + // that tab closing simply kills process. + socket.on('error', (error) => { + if (error.code !== 'ECONNRESET') throw error; + }); + socket.once('close', () => this._sockets.delete(socket)); + } + + /** + * @param {string} pathPrefix + */ + enableHTTPCache(pathPrefix) { + this._cachedPathPrefix = pathPrefix; + } + + /** + * @param {string} path + * @param {string} username + * @param {string} password + */ + setAuth(path, username, password) { + this._auths.set(path, { username, password }); + } + + enableGzip(path) { + this._gzipRoutes.add(path); + } + + /** + * @param {string} path + * @param {string} csp + */ + setCSP(path, csp) { + this._csp.set(path, csp); + } + + async stop() { + this.reset(); + for (const socket of this._sockets) socket.destroy(); + this._sockets.clear(); + await new Promise((x) => this._server.close(x)); + } + + /** + * @param {string} path + * @param {function(!IncomingMessage, !ServerResponse)} handler + */ + setRoute(path, handler) { + this._routes.set(path, handler); + } + + /** + * @param {string} from + * @param {string} to + */ + setRedirect(from, to) { + this.setRoute(from, (req, res) => { + res.writeHead(302, { location: to }); + res.end(); + }); + } + + /** + * @param {string} path + * @returns {!Promise<!IncomingMessage>} + */ + waitForRequest(path) { + let promise = this._requestSubscribers.get(path); + if (promise) return promise; + let fulfill, reject; + promise = new Promise((f, r) => { + fulfill = f; + reject = r; + }); + promise[fulfillSymbol] = fulfill; + promise[rejectSymbol] = reject; + this._requestSubscribers.set(path, promise); + return promise; + } + + reset() { + this._routes.clear(); + this._auths.clear(); + this._csp.clear(); + this._gzipRoutes.clear(); + const error = new Error('Static Server has been reset'); + for (const subscriber of this._requestSubscribers.values()) + subscriber[rejectSymbol].call(null, error); + this._requestSubscribers.clear(); + } + + _onRequest(request, response) { + request.on('error', (error) => { + if (error.code === 'ECONNRESET') response.end(); + else throw error; + }); + request.postBody = new Promise((resolve) => { + let body = ''; + request.on('data', (chunk) => (body += chunk)); + request.on('end', () => resolve(body)); + }); + const pathName = url.parse(request.url).path; + if (this._auths.has(pathName)) { + const auth = this._auths.get(pathName); + const credentials = Buffer.from( + (request.headers.authorization || '').split(' ')[1] || '', + 'base64' + ).toString(); + if (credentials !== `${auth.username}:${auth.password}`) { + response.writeHead(401, { + 'WWW-Authenticate': 'Basic realm="Secure Area"', + }); + response.end('HTTP Error 401 Unauthorized: Access is denied'); + return; + } + } + // Notify request subscriber. + if (this._requestSubscribers.has(pathName)) { + this._requestSubscribers.get(pathName)[fulfillSymbol].call(null, request); + this._requestSubscribers.delete(pathName); + } + const handler = this._routes.get(pathName); + if (handler) { + handler.call(null, request, response); + } else { + const pathName = url.parse(request.url).path; + this.serveFile(request, response, pathName); + } + } + + /** + * @param {!IncomingMessage} request + * @param {!ServerResponse} response + * @param {string} pathName + */ + serveFile(request, response, pathName) { + if (pathName === '/') pathName = '/index.html'; + const filePath = path.join(this._dirPath, pathName.substring(1)); + + if ( + this._cachedPathPrefix !== null && + filePath.startsWith(this._cachedPathPrefix) + ) { + if (request.headers['if-modified-since']) { + response.statusCode = 304; // not modified + response.end(); + return; + } + response.setHeader('Cache-Control', 'public, max-age=31536000'); + response.setHeader('Last-Modified', this._startTime.toISOString()); + } else { + response.setHeader('Cache-Control', 'no-cache, no-store'); + } + if (this._csp.has(pathName)) + response.setHeader('Content-Security-Policy', this._csp.get(pathName)); + + fs.readFile(filePath, (err, data) => { + if (err) { + response.statusCode = 404; + response.end(`File not found: ${filePath}`); + return; + } + const mimeType = mime.getType(filePath); + const isTextEncoding = /^text\/|^application\/(javascript|json)/.test( + mimeType + ); + const contentType = isTextEncoding + ? `${mimeType}; charset=utf-8` + : mimeType; + response.setHeader('Content-Type', contentType); + if (this._gzipRoutes.has(pathName)) { + response.setHeader('Content-Encoding', 'gzip'); + const zlib = require('zlib'); + zlib.gzip(data, (_, result) => { + response.end(result); + }); + } else { + response.end(data); + } + }); + } + + _onWebSocketConnection(connection) { + connection.send('opened'); + } +} + +module.exports = { TestServer }; diff --git a/remote/test/puppeteer/utils/testserver/key.pem b/remote/test/puppeteer/utils/testserver/key.pem new file mode 100644 index 0000000000..cbc3acb229 --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQClZtuGBzoKdbLB +3zEYRNPWZ8uLfCIohukBQeCEsXiyPSNfPy8uH/iiJylDXZ3puzP4gCLuqt1c2HXU +/mcR6Pr8bHh9bMGAZ2skUY+qy9e6Fh3NkCbLrE5pZwsgl5Dnjdte/dh5JPsQZFck +Fz+hxOjaMgZMaOqGSwVqJ33XRVCVfcoY4tMMxQKTTRN4RgFNNTHjs9agizFWpuXZ +HNVdFJYvmGU9aVbb38FC0EaaxfFnJa2qoe52JS6UYFKvgtw/KJxksjyeoKoPSKhL +RMSYaHrWdEG7AFrGYsK8+am3T6wO45T7VuZ7fDyO69NVOgEgXEYHzqsAN3fKz7w1 +5nNeqL5tAgMBAAECggEAKPveo0xBHnxhidZzBM9xKixX7D0a/a3IKI6ZQmfzPz8U +97HhT+2OHyfS+qVEzribPRULEtZ1uV7Ne7R5958iKc/63yFGpTl6++nVzn1p++sl +AV2Zr1gHqehlgnLr7eRhmh0OOZ5nM32ZdhDorH3tMLu6gc5xZktKkS4t6Vx8hj3a +Docx+rbawp8GRd0p7I6vzIE3bsDab8hC+RTRO63q2G0BqgKwV9ZNtJxQgcDJ5L8N +6gtM2z5nKXAIOCbCQYa1PsrDh3IRA/ZNxEeA9G3YQjwlZYCWmdRRplgDraYxcTBO +oQGjaLwICNdcprMacPD6cCSgrI+PadzyMsAuk9SgpQKBgQDO9PT4gK40Pm+Damxv ++tWYBFmvn3vasmyolc1zVDltsxQbQTjKhVpTLXTTGmrIhDXEIIV9I4rg164WptQs +6Brp2EwYR7ZJIrjvXs/9i2QTW1ZXvhdiWpB3s+RXD5VHGovHUadcI6wOgw2Cl+Jk +zXjSIgyXKM99N1MAonuR7DyzTwKBgQDMmPX+9vWZMpS/gc6JLQiPPoGszE6tYjXg +W3LpRUNqmO0/bDDjslbebDgrGAmhlkJlxzH6gz96VmGm1evEGPEet3euy8S9zuM3 +LCgEM9Ulqa3JbInwtKupmKv76Im+XWLLSxAXbfiel1zFRRwxI99A3ad0QRZ6Bov5 +3cHJBwvzgwKBgAU5HW2gIcVjxgC1EOOKmxVpFrJd/gw48JEYpsTAXWqtWFaPwNUr +pGnw/b/OLN++pnS6tWPBH+Ioz1X3A+fWO8enE9SRCsKxw6UW6XzmpbHvXjB8ta5f +xsGeoqan2AahXuG659RlehQrro2bM7WDkgcLoPG3r/TjDo83ipLWOXn1AoGAKWiL +4R56dpcWI+xRsNG8ecFc3Ww8QDswTEg16aBrFJf+7GcpPexKSJn+hDpJOLsAlTjL +lLgbkNcKzIlfPkEOC/l175quJvxIYFI/hxo2eXjuA2ZERMNMOvb7V/CocC7WX+7B +Qvyu5OodjI+ANTHdbXNvAMhrlCbfDaMkJVuXv6ECgYBzvY4aYmVoFsr+72/EfLls +Dz9pi55tUUWc61w6ovd+iliawvXeGi4wibtTH4iGj/C2sJIaMmOD99NQ7Oi/x89D +oMgSUemkoFL8FGsZGyZ7szqxyON1jP42Bm2MQrW5kIf7Y4yaIGhoak5JNxn2JUyV +gupVbY1mQ1GTPByxHeLh1w== +-----END PRIVATE KEY----- diff --git a/remote/test/puppeteer/utils/testserver/package.json b/remote/test/puppeteer/utils/testserver/package.json new file mode 100644 index 0000000000..6cac90ffc5 --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pptr/testserver", + "version": "0.5.0", + "description": "testing server", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/puppeteer/puppeteer/tree/main/utils/testserver" + }, + "author": "The Chromium Authors", + "license": "Apache-2.0" +} diff --git a/remote/test/puppeteer/vendor/README.md b/remote/test/puppeteer/vendor/README.md new file mode 100644 index 0000000000..bca3d5e10f --- /dev/null +++ b/remote/test/puppeteer/vendor/README.md @@ -0,0 +1,13 @@ +# Vendoring third party dependencies + +Because we are working towards an agnostic Puppeteer that can run in any environment (see [#6125](https://github.com/puppeteer/puppeteer/issues/6125)) we cannot import common dependencies in a way that relies on Node's resolution to find them. For example, `import mitt from 'mitt'` works fine in Node, but in an ESM build running in the browser, the browser has no idea where to find `'mitt'`. + +Therefore we put all common dependencies into this directory, `vendor`. This means there are extra criteria for these dependencies; ideally they will not depend on any other modules. If they do, we should consider an alternative way of managing our dependencies. + +The process for updating a vendored dependency is: + +1. `npm install {DEP NAME HERE}` +2. `cp -r node_modules/DEP vendor` +3. Update `eslintrc.js` to forbid importing DEP directly (see the `Mitt` rule already defined in there). +4. Use the new DEP, and run `npm run tsc` to check everything compiles successfully. +5. If the dep ships as compiled JS, you may need to disable TypeScript checking the file. Add an entry to the `excludes` property of the TSConfig files in `vendor`. (again, see the entry that's already there for Mitt as an example). Don't forget to update both the ESM and CJS config files. diff --git a/remote/test/puppeteer/vendor/mitt/README.md b/remote/test/puppeteer/vendor/mitt/README.md new file mode 100644 index 0000000000..08a21bf7ad --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/README.md @@ -0,0 +1,179 @@ +<p align="center"> + <img src="https://i.imgur.com/BqsX9NT.png" width="300" height="300" alt="mitt"> + <br> + <a href="https://www.npmjs.org/package/mitt"><img src="https://img.shields.io/npm/v/mitt.svg" alt="npm"></a> + <img src="https://github.com/developit/mitt/workflows/CI/badge.svg" alt="build status"> + <a href="https://unpkg.com/mitt/dist/mitt.js"><img src="https://img.badgesize.io/https://unpkg.com/mitt/dist/mitt.js?compression=gzip" alt="gzip size"></a> +</p> + +# Mitt + +> Tiny 200b functional event emitter / pubsub. + +- **Microscopic:** weighs less than 200 bytes gzipped +- **Useful:** a wildcard `"*"` event type listens to all events +- **Familiar:** same names & ideas as [Node's EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) +- **Functional:** methods don't rely on `this` +- **Great Name:** somehow [mitt](https://npm.im/mitt) wasn't taken + +Mitt was made for the browser, but works in any JavaScript runtime. It has no dependencies and supports IE9+. + +## Table of Contents + +- [Install](#install) +- [Usage](#usage) +- [Examples & Demos](#examples--demos) +- [API](#api) +- [Contribute](#contribute) +- [License](#license) + +## Install + +This project uses [node](http://nodejs.org) and [npm](https://npmjs.com). Go check them out if you don't have them locally installed. + +```sh +$ npm install --save mitt +``` + +Then with a module bundler like [rollup](http://rollupjs.org/) or [webpack](https://webpack.js.org/), use as you would anything else: + +```javascript +// using ES6 modules +import mitt from 'mitt' + +// using CommonJS modules +var mitt = require('mitt') +``` + +The [UMD](https://github.com/umdjs/umd) build is also available on [unpkg](https://unpkg.com): + +```html +<script src="https://unpkg.com/mitt/dist/mitt.umd.js"></script> +``` + +You can find the library on `window.mitt`. + +## Usage + +```js +import mitt from 'mitt' + +const emitter = mitt() + +// listen to an event +emitter.on('foo', e => console.log('foo', e) ) + +// listen to all events +emitter.on('*', (type, e) => console.log(type, e) ) + +// fire an event +emitter.emit('foo', { a: 'b' }) + +// clearing all events +emitter.all.clear() + +// working with handler references: +function onFoo() {} +emitter.on('foo', onFoo) // listen +emitter.off('foo', onFoo) // unlisten +``` + +### Typescript + +```ts +import mitt from 'mitt'; +const emitter: mitt.Emitter = mitt(); +``` + +## Examples & Demos + +<a href="http://codepen.io/developit/pen/rjMEwW?editors=0110"> + <b>Preact + Mitt Codepen Demo</b> + <br> + <img src="https://i.imgur.com/CjBgOfJ.png" width="278" alt="preact + mitt preview"> +</a> + +* * * + +## API + +<!-- Generated by documentation.js. Update this documentation by updating the source code. --> + +#### Table of Contents + +- [mitt](#mitt) +- [all](#all) +- [on](#on) + - [Parameters](#parameters) +- [off](#off) + - [Parameters](#parameters-1) +- [emit](#emit) + - [Parameters](#parameters-2) + +### mitt + +Mitt: Tiny (~200b) functional event emitter / pubsub. + +Returns **Mitt** + +### all + +A Map of event names to registered handler functions. + +### on + +Register an event handler for the given type. + +#### Parameters + +- `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) \| [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** Type of event to listen for, or `"*"` for all events +- `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Function to call in response to given event + +### off + +Remove an event handler for the given type. + +#### Parameters + +- `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) \| [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** Type of event to unregister `handler` from, or `"*"` +- `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Handler function to remove + +### emit + +Invoke all handlers for the given type. +If present, `"*"` handlers are invoked after type-matched handlers. + +Note: Manually firing "\*" handlers is not supported. + +#### Parameters + +- `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) \| [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** The event type to invoke +- `evt` **Any?** Any value (object is recommended and powerful), passed to each handler + +## Contribute + +First off, thanks for taking the time to contribute! +Now, take a moment to be sure your contributions make sense to everyone else. + +### Reporting Issues + +Found a problem? Want a new feature? First of all see if your issue or idea has [already been reported](../../issues). +If don't, just open a [new clear and descriptive issue](../../issues/new). + +### Submitting pull requests + +Pull requests are the greatest contributions, so be sure they are focused in scope, and do avoid unrelated commits. + +- Fork it! +- Clone your fork: `git clone https://github.com/<your-username>/mitt` +- Navigate to the newly cloned directory: `cd mitt` +- Create a new branch for the new feature: `git checkout -b my-new-feature` +- Install the tools necessary for development: `npm install` +- Make your changes. +- Commit your changes: `git commit -am 'Add some feature'` +- Push to the branch: `git push origin my-new-feature` +- Submit a pull request with full remarks documenting your changes. + +## License + +[MIT License](https://opensource.org/licenses/MIT) © [Jason Miller](https://jasonformat.com/) diff --git a/remote/test/puppeteer/vendor/mitt/dist/mitt.es.js b/remote/test/puppeteer/vendor/mitt/dist/mitt.es.js new file mode 100644 index 0000000000..889e27282f --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/dist/mitt.es.js @@ -0,0 +1,2 @@ +export default function(n){return{all:n=n||new Map,on:function(t,e){var i=n.get(t);i&&i.push(e)||n.set(t,[e])},off:function(t,e){var i=n.get(t);i&&i.splice(i.indexOf(e)>>>0,1)},emit:function(t,e){(n.get(t)||[]).slice().map(function(n){n(e)}),(n.get("*")||[]).slice().map(function(n){n(t,e)})}}} +//# sourceMappingURL=mitt.es.js.map diff --git a/remote/test/puppeteer/vendor/mitt/dist/mitt.es.js.map b/remote/test/puppeteer/vendor/mitt/dist/mitt.es.js.map new file mode 100644 index 0000000000..6576278e2d --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/dist/mitt.es.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mitt.es.js","sources":["../src/index.ts"],"sourcesContent":["export type EventType = string | symbol;\n\n// An event handler can take an optional event argument\n// and should not return a value\nexport type Handler<T = any> = (event?: T) => void;\nexport type WildcardHandler = (type: EventType, event?: any) => void;\n\n// An array of all currently registered event handlers for a type\nexport type EventHandlerList = Array<Handler>;\nexport type WildCardEventHandlerList = Array<WildcardHandler>;\n\n// A map of event types and their corresponding event handlers.\nexport type EventHandlerMap = Map<EventType, EventHandlerList | WildCardEventHandlerList>;\n\nexport interface Emitter {\n\tall: EventHandlerMap;\n\n\ton<T = any>(type: EventType, handler: Handler<T>): void;\n\ton(type: '*', handler: WildcardHandler): void;\n\n\toff<T = any>(type: EventType, handler: Handler<T>): void;\n\toff(type: '*', handler: WildcardHandler): void;\n\n\temit<T = any>(type: EventType, event?: T): void;\n\temit(type: '*', event?: any): void;\n}\n\n/**\n * Mitt: Tiny (~200b) functional event emitter / pubsub.\n * @name mitt\n * @returns {Mitt}\n */\nexport default function mitt(all?: EventHandlerMap): Emitter {\n\tall = all || new Map();\n\n\treturn {\n\n\t\t/**\n\t\t * A Map of event names to registered handler functions.\n\t\t */\n\t\tall,\n\n\t\t/**\n\t\t * Register an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to listen for, or `\"*\"` for all events\n\t\t * @param {Function} handler Function to call in response to given event\n\t\t * @memberOf mitt\n\t\t */\n\t\ton<T = any>(type: EventType, handler: Handler<T>) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tconst added = handlers && handlers.push(handler);\n\t\t\tif (!added) {\n\t\t\t\tall.set(type, [handler]);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to unregister `handler` from, or `\"*\"`\n\t\t * @param {Function} handler Handler function to remove\n\t\t * @memberOf mitt\n\t\t */\n\t\toff<T = any>(type: EventType, handler: Handler<T>) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tif (handlers) {\n\t\t\t\thandlers.splice(handlers.indexOf(handler) >>> 0, 1);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Invoke all handlers for the given type.\n\t\t * If present, `\"*\"` handlers are invoked after type-matched handlers.\n\t\t *\n\t\t * Note: Manually firing \"*\" handlers is not supported.\n\t\t *\n\t\t * @param {string|symbol} type The event type to invoke\n\t\t * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler\n\t\t * @memberOf mitt\n\t\t */\n\t\temit<T = any>(type: EventType, evt: T) {\n\t\t\t((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); });\n\t\t\t((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); });\n\t\t}\n\t};\n}\n"],"names":["all","Map","on","type","handler","handlers","get","push","set","off","splice","indexOf","emit","evt","slice","map"],"mappings":"wBAgC6BA,GAG5B,MAAO,CAKNA,IAPDA,EAAMA,GAAO,IAAIC,IAehBC,YAAYC,EAAiBC,GAC5B,IAAMC,EAAWL,EAAIM,IAAIH,GACXE,GAAYA,EAASE,KAAKH,IAEvCJ,EAAIQ,IAAIL,EAAM,CAACC,KAUjBK,aAAaN,EAAiBC,GAC7B,IAAMC,EAAWL,EAAIM,IAAIH,GACrBE,GACHA,EAASK,OAAOL,EAASM,QAAQP,KAAa,EAAG,IAcnDQ,cAAcT,EAAiBU,IAC5Bb,EAAIM,IAAIH,IAAS,IAAyBW,QAAQC,IAAI,SAACX,GAAcA,EAAQS,MAC7Eb,EAAIM,IAAI,MAAQ,IAAiCQ,QAAQC,IAAI,SAACX,GAAcA,EAAQD,EAAMU"}
\ No newline at end of file diff --git a/remote/test/puppeteer/vendor/mitt/dist/mitt.js b/remote/test/puppeteer/vendor/mitt/dist/mitt.js new file mode 100644 index 0000000000..2bd0cf9e44 --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/dist/mitt.js @@ -0,0 +1,2 @@ +module.exports=function(n){return{all:n=n||new Map,on:function(e,t){var i=n.get(e);i&&i.push(t)||n.set(e,[t])},off:function(e,t){var i=n.get(e);i&&i.splice(i.indexOf(t)>>>0,1)},emit:function(e,t){(n.get(e)||[]).slice().map(function(n){n(t)}),(n.get("*")||[]).slice().map(function(n){n(e,t)})}}}; +//# sourceMappingURL=mitt.js.map diff --git a/remote/test/puppeteer/vendor/mitt/dist/mitt.js.map b/remote/test/puppeteer/vendor/mitt/dist/mitt.js.map new file mode 100644 index 0000000000..37f6f59ebd --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/dist/mitt.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mitt.js","sources":["../src/index.ts"],"sourcesContent":["export type EventType = string | symbol;\n\n// An event handler can take an optional event argument\n// and should not return a value\nexport type Handler<T = any> = (event?: T) => void;\nexport type WildcardHandler = (type: EventType, event?: any) => void;\n\n// An array of all currently registered event handlers for a type\nexport type EventHandlerList = Array<Handler>;\nexport type WildCardEventHandlerList = Array<WildcardHandler>;\n\n// A map of event types and their corresponding event handlers.\nexport type EventHandlerMap = Map<EventType, EventHandlerList | WildCardEventHandlerList>;\n\nexport interface Emitter {\n\tall: EventHandlerMap;\n\n\ton<T = any>(type: EventType, handler: Handler<T>): void;\n\ton(type: '*', handler: WildcardHandler): void;\n\n\toff<T = any>(type: EventType, handler: Handler<T>): void;\n\toff(type: '*', handler: WildcardHandler): void;\n\n\temit<T = any>(type: EventType, event?: T): void;\n\temit(type: '*', event?: any): void;\n}\n\n/**\n * Mitt: Tiny (~200b) functional event emitter / pubsub.\n * @name mitt\n * @returns {Mitt}\n */\nexport default function mitt(all?: EventHandlerMap): Emitter {\n\tall = all || new Map();\n\n\treturn {\n\n\t\t/**\n\t\t * A Map of event names to registered handler functions.\n\t\t */\n\t\tall,\n\n\t\t/**\n\t\t * Register an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to listen for, or `\"*\"` for all events\n\t\t * @param {Function} handler Function to call in response to given event\n\t\t * @memberOf mitt\n\t\t */\n\t\ton<T = any>(type: EventType, handler: Handler<T>) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tconst added = handlers && handlers.push(handler);\n\t\t\tif (!added) {\n\t\t\t\tall.set(type, [handler]);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to unregister `handler` from, or `\"*\"`\n\t\t * @param {Function} handler Handler function to remove\n\t\t * @memberOf mitt\n\t\t */\n\t\toff<T = any>(type: EventType, handler: Handler<T>) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tif (handlers) {\n\t\t\t\thandlers.splice(handlers.indexOf(handler) >>> 0, 1);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Invoke all handlers for the given type.\n\t\t * If present, `\"*\"` handlers are invoked after type-matched handlers.\n\t\t *\n\t\t * Note: Manually firing \"*\" handlers is not supported.\n\t\t *\n\t\t * @param {string|symbol} type The event type to invoke\n\t\t * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler\n\t\t * @memberOf mitt\n\t\t */\n\t\temit<T = any>(type: EventType, evt: T) {\n\t\t\t((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); });\n\t\t\t((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); });\n\t\t}\n\t};\n}\n"],"names":["all","Map","on","type","handler","handlers","get","push","set","off","splice","indexOf","emit","evt","slice","map"],"mappings":"wBAgC6BA,GAG5B,MAAO,CAKNA,IAPDA,EAAMA,GAAO,IAAIC,IAehBC,YAAYC,EAAiBC,GAC5B,IAAMC,EAAWL,EAAIM,IAAIH,GACXE,GAAYA,EAASE,KAAKH,IAEvCJ,EAAIQ,IAAIL,EAAM,CAACC,KAUjBK,aAAaN,EAAiBC,GAC7B,IAAMC,EAAWL,EAAIM,IAAIH,GACrBE,GACHA,EAASK,OAAOL,EAASM,QAAQP,KAAa,EAAG,IAcnDQ,cAAcT,EAAiBU,IAC5Bb,EAAIM,IAAIH,IAAS,IAAyBW,QAAQC,IAAI,SAACX,GAAcA,EAAQS,MAC7Eb,EAAIM,IAAI,MAAQ,IAAiCQ,QAAQC,IAAI,SAACX,GAAcA,EAAQD,EAAMU"}
\ No newline at end of file diff --git a/remote/test/puppeteer/vendor/mitt/dist/mitt.modern.js b/remote/test/puppeteer/vendor/mitt/dist/mitt.modern.js new file mode 100644 index 0000000000..0777f6de72 --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/dist/mitt.modern.js @@ -0,0 +1,2 @@ +export default function(e){return{all:e=e||new Map,on(t,n){const s=e.get(t);s&&s.push(n)||e.set(t,[n])},off(t,n){const s=e.get(t);s&&s.splice(s.indexOf(n)>>>0,1)},emit(t,n){(e.get(t)||[]).slice().map(e=>{e(n)}),(e.get("*")||[]).slice().map(e=>{e(t,n)})}}} +//# sourceMappingURL=mitt.modern.js.map diff --git a/remote/test/puppeteer/vendor/mitt/dist/mitt.modern.js.map b/remote/test/puppeteer/vendor/mitt/dist/mitt.modern.js.map new file mode 100644 index 0000000000..5f669b2d61 --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/dist/mitt.modern.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mitt.modern.js","sources":["../src/index.ts"],"sourcesContent":["export type EventType = string | symbol;\n\n// An event handler can take an optional event argument\n// and should not return a value\nexport type Handler<T = any> = (event?: T) => void;\nexport type WildcardHandler = (type: EventType, event?: any) => void;\n\n// An array of all currently registered event handlers for a type\nexport type EventHandlerList = Array<Handler>;\nexport type WildCardEventHandlerList = Array<WildcardHandler>;\n\n// A map of event types and their corresponding event handlers.\nexport type EventHandlerMap = Map<EventType, EventHandlerList | WildCardEventHandlerList>;\n\nexport interface Emitter {\n\tall: EventHandlerMap;\n\n\ton<T = any>(type: EventType, handler: Handler<T>): void;\n\ton(type: '*', handler: WildcardHandler): void;\n\n\toff<T = any>(type: EventType, handler: Handler<T>): void;\n\toff(type: '*', handler: WildcardHandler): void;\n\n\temit<T = any>(type: EventType, event?: T): void;\n\temit(type: '*', event?: any): void;\n}\n\n/**\n * Mitt: Tiny (~200b) functional event emitter / pubsub.\n * @name mitt\n * @returns {Mitt}\n */\nexport default function mitt(all?: EventHandlerMap): Emitter {\n\tall = all || new Map();\n\n\treturn {\n\n\t\t/**\n\t\t * A Map of event names to registered handler functions.\n\t\t */\n\t\tall,\n\n\t\t/**\n\t\t * Register an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to listen for, or `\"*\"` for all events\n\t\t * @param {Function} handler Function to call in response to given event\n\t\t * @memberOf mitt\n\t\t */\n\t\ton<T = any>(type: EventType, handler: Handler<T>) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tconst added = handlers && handlers.push(handler);\n\t\t\tif (!added) {\n\t\t\t\tall.set(type, [handler]);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to unregister `handler` from, or `\"*\"`\n\t\t * @param {Function} handler Handler function to remove\n\t\t * @memberOf mitt\n\t\t */\n\t\toff<T = any>(type: EventType, handler: Handler<T>) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tif (handlers) {\n\t\t\t\thandlers.splice(handlers.indexOf(handler) >>> 0, 1);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Invoke all handlers for the given type.\n\t\t * If present, `\"*\"` handlers are invoked after type-matched handlers.\n\t\t *\n\t\t * Note: Manually firing \"*\" handlers is not supported.\n\t\t *\n\t\t * @param {string|symbol} type The event type to invoke\n\t\t * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler\n\t\t * @memberOf mitt\n\t\t */\n\t\temit<T = any>(type: EventType, evt: T) {\n\t\t\t((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); });\n\t\t\t((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); });\n\t\t}\n\t};\n}\n"],"names":["all","Map","on","type","handler","handlers","get","push","set","off","splice","indexOf","emit","evt","slice","map"],"mappings":"wBAgC6BA,GAG5B,MAAO,CAKNA,IAPDA,EAAMA,GAAO,IAAIC,IAehBC,GAAYC,EAAiBC,GAC5B,MAAMC,EAAWL,EAAIM,IAAIH,GACXE,GAAYA,EAASE,KAAKH,IAEvCJ,EAAIQ,IAAIL,EAAM,CAACC,KAUjBK,IAAaN,EAAiBC,GAC7B,MAAMC,EAAWL,EAAIM,IAAIH,GACrBE,GACHA,EAASK,OAAOL,EAASM,QAAQP,KAAa,EAAG,IAcnDQ,KAAcT,EAAiBU,IAC5Bb,EAAIM,IAAIH,IAAS,IAAyBW,QAAQC,IAAKX,IAAcA,EAAQS,MAC7Eb,EAAIM,IAAI,MAAQ,IAAiCQ,QAAQC,IAAKX,IAAcA,EAAQD,EAAMU"}
\ No newline at end of file diff --git a/remote/test/puppeteer/vendor/mitt/dist/mitt.umd.js b/remote/test/puppeteer/vendor/mitt/dist/mitt.umd.js new file mode 100644 index 0000000000..ce0e0059ae --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/dist/mitt.umd.js @@ -0,0 +1,2 @@ +!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e=e||self).mitt=n()}(this,function(){return function(e){return{all:e=e||new Map,on:function(n,t){var f=e.get(n);f&&f.push(t)||e.set(n,[t])},off:function(n,t){var f=e.get(n);f&&f.splice(f.indexOf(t)>>>0,1)},emit:function(n,t){(e.get(n)||[]).slice().map(function(e){e(t)}),(e.get("*")||[]).slice().map(function(e){e(n,t)})}}}}); +//# sourceMappingURL=mitt.umd.js.map diff --git a/remote/test/puppeteer/vendor/mitt/dist/mitt.umd.js.map b/remote/test/puppeteer/vendor/mitt/dist/mitt.umd.js.map new file mode 100644 index 0000000000..642c894006 --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/dist/mitt.umd.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mitt.umd.js","sources":["../src/index.ts"],"sourcesContent":["export type EventType = string | symbol;\n\n// An event handler can take an optional event argument\n// and should not return a value\nexport type Handler<T = any> = (event?: T) => void;\nexport type WildcardHandler = (type: EventType, event?: any) => void;\n\n// An array of all currently registered event handlers for a type\nexport type EventHandlerList = Array<Handler>;\nexport type WildCardEventHandlerList = Array<WildcardHandler>;\n\n// A map of event types and their corresponding event handlers.\nexport type EventHandlerMap = Map<EventType, EventHandlerList | WildCardEventHandlerList>;\n\nexport interface Emitter {\n\tall: EventHandlerMap;\n\n\ton<T = any>(type: EventType, handler: Handler<T>): void;\n\ton(type: '*', handler: WildcardHandler): void;\n\n\toff<T = any>(type: EventType, handler: Handler<T>): void;\n\toff(type: '*', handler: WildcardHandler): void;\n\n\temit<T = any>(type: EventType, event?: T): void;\n\temit(type: '*', event?: any): void;\n}\n\n/**\n * Mitt: Tiny (~200b) functional event emitter / pubsub.\n * @name mitt\n * @returns {Mitt}\n */\nexport default function mitt(all?: EventHandlerMap): Emitter {\n\tall = all || new Map();\n\n\treturn {\n\n\t\t/**\n\t\t * A Map of event names to registered handler functions.\n\t\t */\n\t\tall,\n\n\t\t/**\n\t\t * Register an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to listen for, or `\"*\"` for all events\n\t\t * @param {Function} handler Function to call in response to given event\n\t\t * @memberOf mitt\n\t\t */\n\t\ton<T = any>(type: EventType, handler: Handler<T>) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tconst added = handlers && handlers.push(handler);\n\t\t\tif (!added) {\n\t\t\t\tall.set(type, [handler]);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to unregister `handler` from, or `\"*\"`\n\t\t * @param {Function} handler Handler function to remove\n\t\t * @memberOf mitt\n\t\t */\n\t\toff<T = any>(type: EventType, handler: Handler<T>) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tif (handlers) {\n\t\t\t\thandlers.splice(handlers.indexOf(handler) >>> 0, 1);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Invoke all handlers for the given type.\n\t\t * If present, `\"*\"` handlers are invoked after type-matched handlers.\n\t\t *\n\t\t * Note: Manually firing \"*\" handlers is not supported.\n\t\t *\n\t\t * @param {string|symbol} type The event type to invoke\n\t\t * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler\n\t\t * @memberOf mitt\n\t\t */\n\t\temit<T = any>(type: EventType, evt: T) {\n\t\t\t((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); });\n\t\t\t((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); });\n\t\t}\n\t};\n}\n"],"names":["all","Map","on","type","handler","handlers","get","push","set","off","splice","indexOf","emit","evt","slice","map"],"mappings":"6LAgC6BA,GAG5B,MAAO,CAKNA,IAPDA,EAAMA,GAAO,IAAIC,IAehBC,YAAYC,EAAiBC,GAC5B,IAAMC,EAAWL,EAAIM,IAAIH,GACXE,GAAYA,EAASE,KAAKH,IAEvCJ,EAAIQ,IAAIL,EAAM,CAACC,KAUjBK,aAAaN,EAAiBC,GAC7B,IAAMC,EAAWL,EAAIM,IAAIH,GACrBE,GACHA,EAASK,OAAOL,EAASM,QAAQP,KAAa,EAAG,IAcnDQ,cAAcT,EAAiBU,IAC5Bb,EAAIM,IAAIH,IAAS,IAAyBW,QAAQC,IAAI,SAACX,GAAcA,EAAQS,MAC7Eb,EAAIM,IAAI,MAAQ,IAAiCQ,QAAQC,IAAI,SAACX,GAAcA,EAAQD,EAAMU"}
\ No newline at end of file diff --git a/remote/test/puppeteer/vendor/mitt/index.d.ts b/remote/test/puppeteer/vendor/mitt/index.d.ts new file mode 100644 index 0000000000..55346dd976 --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/index.d.ts @@ -0,0 +1,21 @@ +export declare type EventType = string | symbol;
+export declare type Handler<T = any> = (event?: T) => void;
+export declare type WildcardHandler = (type: EventType, event?: any) => void;
+export declare type EventHandlerList = Array<Handler>;
+export declare type WildCardEventHandlerList = Array<WildcardHandler>;
+export declare type EventHandlerMap = Map<EventType, EventHandlerList | WildCardEventHandlerList>;
+export interface Emitter {
+ all: EventHandlerMap;
+ on<T = any>(type: EventType, handler: Handler<T>): void;
+ on(type: '*', handler: WildcardHandler): void;
+ off<T = any>(type: EventType, handler: Handler<T>): void;
+ off(type: '*', handler: WildcardHandler): void;
+ emit<T = any>(type: EventType, event?: T): void;
+ emit(type: '*', event?: any): void;
+}
+/**
+ * Mitt: Tiny (~200b) functional event emitter / pubsub.
+ * @name mitt
+ * @returns {Mitt}
+ */
+export default function mitt(all?: EventHandlerMap): Emitter;
diff --git a/remote/test/puppeteer/vendor/mitt/package.json b/remote/test/puppeteer/vendor/mitt/package.json new file mode 100644 index 0000000000..0105524a0d --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/package.json @@ -0,0 +1,141 @@ +{ + "_from": "mitt@latest", + "_id": "mitt@2.1.0", + "_inBundle": false, + "_integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==", + "_location": "/mitt", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "mitt@latest", + "name": "mitt", + "escapedName": "mitt", + "rawSpec": "latest", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER", + "/" + ], + "_resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz", + "_shasum": "f740577c23176c6205b121b2973514eade1b2230", + "_spec": "mitt@latest", + "_where": "/Users/jacktfranklin/src/puppeteer", + "authors": [ + "Jason Miller <jason@developit.ca>" + ], + "bugs": { + "url": "https://github.com/developit/mitt/issues" + }, + "bundleDependencies": false, + "deprecated": false, + "description": "Tiny 200b functional Event Emitter / pubsub.", + "devDependencies": { + "@types/chai": "^4.2.11", + "@types/mocha": "^7.0.2", + "@types/sinon": "^9.0.4", + "@types/sinon-chai": "^3.2.4", + "@typescript-eslint/eslint-plugin": "^3.0.1", + "@typescript-eslint/parser": "^3.0.1", + "chai": "^4.2.0", + "documentation": "^13.0.0", + "eslint": "^7.1.0", + "eslint-config-developit": "^1.2.0", + "esm": "^3.2.25", + "microbundle": "^0.12.3", + "mocha": "^8.0.1", + "npm-run-all": "^4.1.5", + "rimraf": "^3.0.2", + "sinon": "^9.0.2", + "sinon-chai": "^3.5.0", + "ts-node": "^8.10.2", + "typescript": "^3.9.3" + }, + "eslintConfig": { + "extends": [ + "developit", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "sourceType": "module" + }, + "env": { + "browser": true, + "mocha": true, + "jest": false, + "es6": true + }, + "globals": { + "expect": true + }, + "rules": { + "semi": [ + 2, + "always" + ], + "jest/valid-expect": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/explicit-function-return-type": 0, + "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-empty-function": 0 + } + }, + "eslintIgnore": [ + "dist", + "index.d.ts" + ], + "esmodules": "dist/mitt.modern.js", + "files": [ + "src", + "dist", + "index.d.ts" + ], + "homepage": "https://github.com/developit/mitt", + "jsnext:main": "dist/mitt.es.js", + "keywords": [ + "events", + "eventemitter", + "emitter", + "pubsub" + ], + "license": "MIT", + "main": "dist/mitt.js", + "mocha": { + "extension": [ + "ts" + ], + "require": [ + "ts-node/register", + "esm" + ], + "spec": [ + "test/*_test.ts" + ] + }, + "module": "dist/mitt.es.js", + "name": "mitt", + "repository": { + "type": "git", + "url": "git+https://github.com/developit/mitt.git" + }, + "scripts": { + "build": "npm-run-all --silent clean -p bundle -s docs", + "bundle": "microbundle", + "clean": "rimraf dist", + "docs": "documentation readme src/index.ts --section API -q --parse-extension ts", + "lint": "eslint src test --ext ts --ext js", + "mocha": "mocha test", + "release": "npm run -s build -s && npm t && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish", + "test": "npm-run-all --silent typecheck lint mocha test-types", + "test-types": "tsc test/test-types-compilation.ts --noEmit", + "typecheck": "tsc --noEmit" + }, + "source": "src/index.ts", + "typings": "index.d.ts", + "umd:main": "dist/mitt.umd.js", + "version": "2.1.0" +} diff --git a/remote/test/puppeteer/vendor/mitt/src/index.ts b/remote/test/puppeteer/vendor/mitt/src/index.ts new file mode 100644 index 0000000000..ae85607f7c --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/src/index.ts @@ -0,0 +1,85 @@ +export type EventType = string | symbol; + +// An event handler can take an optional event argument +// and should not return a value +export type Handler<T = any> = (event?: T) => void; +export type WildcardHandler = (type: EventType, event?: any) => void; + +// An array of all currently registered event handlers for a type +export type EventHandlerList = Array<Handler>; +export type WildCardEventHandlerList = Array<WildcardHandler>; + +// A map of event types and their corresponding event handlers. +export type EventHandlerMap = Map<EventType, EventHandlerList | WildCardEventHandlerList>; + +export interface Emitter { + all: EventHandlerMap; + + on<T = any>(type: EventType, handler: Handler<T>): void; + on(type: '*', handler: WildcardHandler): void; + + off<T = any>(type: EventType, handler: Handler<T>): void; + off(type: '*', handler: WildcardHandler): void; + + emit<T = any>(type: EventType, event?: T): void; + emit(type: '*', event?: any): void; +} + +/** + * Mitt: Tiny (~200b) functional event emitter / pubsub. + * @name mitt + * @returns {Mitt} + */ +export default function mitt(all?: EventHandlerMap): Emitter { + all = all || new Map(); + + return { + + /** + * A Map of event names to registered handler functions. + */ + all, + + /** + * Register an event handler for the given type. + * @param {string|symbol} type Type of event to listen for, or `"*"` for all events + * @param {Function} handler Function to call in response to given event + * @memberOf mitt + */ + on<T = any>(type: EventType, handler: Handler<T>) { + const handlers = all.get(type); + const added = handlers && handlers.push(handler); + if (!added) { + all.set(type, [handler]); + } + }, + + /** + * Remove an event handler for the given type. + * @param {string|symbol} type Type of event to unregister `handler` from, or `"*"` + * @param {Function} handler Handler function to remove + * @memberOf mitt + */ + off<T = any>(type: EventType, handler: Handler<T>) { + const handlers = all.get(type); + if (handlers) { + handlers.splice(handlers.indexOf(handler) >>> 0, 1); + } + }, + + /** + * Invoke all handlers for the given type. + * If present, `"*"` handlers are invoked after type-matched handlers. + * + * Note: Manually firing "*" handlers is not supported. + * + * @param {string|symbol} type The event type to invoke + * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler + * @memberOf mitt + */ + emit<T = any>(type: EventType, evt: T) { + ((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); }); + ((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); }); + } + }; +} diff --git a/remote/test/puppeteer/vendor/tsconfig.cjs.json b/remote/test/puppeteer/vendor/tsconfig.cjs.json new file mode 100644 index 0000000000..f73a8d57d0 --- /dev/null +++ b/remote/test/puppeteer/vendor/tsconfig.cjs.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base.json", + "exclude": [ + "mitt/dist" + ], + "compilerOptions": { + "composite": true, + "outDir": "../lib/cjs/vendor", + "module": "CommonJS" + } +} diff --git a/remote/test/puppeteer/vendor/tsconfig.esm.json b/remote/test/puppeteer/vendor/tsconfig.esm.json new file mode 100644 index 0000000000..1f0bae8e3d --- /dev/null +++ b/remote/test/puppeteer/vendor/tsconfig.esm.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base.json", + "exclude": [ + "mitt/dist" + ], + "compilerOptions": { + "composite": true, + "outDir": "../lib/esm/vendor", + "module": "esnext" + } +} diff --git a/remote/test/puppeteer/versions.js b/remote/test/puppeteer/versions.js new file mode 100644 index 0000000000..5a66247a4d --- /dev/null +++ b/remote/test/puppeteer/versions.js @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const versionsPerRelease = new Map([ + // This is a mapping from Chromium version => Puppeteer version. + // In Chromium roll patches, use 'NEXT' for the Puppeteer version. + ['88.0.4298.0', '5.5.0'], + ['87.0.4272.0', 'v5.4.0'], + ['86.0.4240.0', 'v5.3.0'], + ['85.0.4182.0', 'v5.2.1'], + ['84.0.4147.0', 'v5.1.0'], + ['83.0.4103.0', 'v3.1.0'], + ['81.0.4044.0', 'v3.0.0'], + ['80.0.3987.0', 'v2.1.0'], + ['79.0.3942.0', 'v2.0.0'], + ['78.0.3882.0', 'v1.20.0'], + ['77.0.3803.0', 'v1.19.0'], + ['76.0.3803.0', 'v1.17.0'], + ['75.0.3765.0', 'v1.15.0'], + ['74.0.3723.0', 'v1.13.0'], + ['73.0.3679.0', 'v1.12.2'], +]); + +module.exports = versionsPerRelease; diff --git a/remote/test/puppeteer/web-test-runner.config.js b/remote/test/puppeteer/web-test-runner.config.js new file mode 100644 index 0000000000..8cf8ad5361 --- /dev/null +++ b/remote/test/puppeteer/web-test-runner.config.js @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const { chromeLauncher } = require('@web/test-runner-chrome'); + +module.exports = { + files: ['test-browser/**/*.spec.js'], + browsers: [ + chromeLauncher({ + async createPage({ browser }) { + const page = await browser.newPage(); + page.evaluateOnNewDocument((wsEndpoint) => { + window.__ENV__ = { wsEndpoint }; + }, browser.wsEndpoint()); + + return page; + }, + }), + ], + plugins: [ + { + // turn expect UMD into an es module + name: 'esmify-expect', + transform(context) { + if (context.path === '/node_modules/expect/build-es5/index.js') { + return `const module = {}; const exports = {};\n${context.body};\n export default module.exports;`; + } + }, + }, + ], +}; diff --git a/remote/test/unit/test_Connection.js b/remote/test/unit/test_Connection.js new file mode 100644 index 0000000000..093daea338 --- /dev/null +++ b/remote/test/unit/test_Connection.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Connection } = ChromeUtils.import( + "chrome://remote/content/Connection.jsm" +); + +add_test(function test_Connection_splitMethod() { + for (const t of [42, null, true, {}, [], undefined]) { + Assert.throws( + () => Connection.splitMethod(t), + /TypeError/, + `${typeof t} throws` + ); + } + for (const s of ["", ".", "foo.", ".bar", "foo.bar.baz"]) { + Assert.throws( + () => Connection.splitMethod(s), + /Invalid method format: ".*"/, + `"${s}" throws` + ); + } + deepEqual(Connection.splitMethod("foo.bar"), { + domain: "foo", + command: "bar", + }); + + run_next_test(); +}); diff --git a/remote/test/unit/test_DomainCache.js b/remote/test/unit/test_DomainCache.js new file mode 100644 index 0000000000..e54ebbc314 --- /dev/null +++ b/remote/test/unit/test_DomainCache.js @@ -0,0 +1,127 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Domain } = ChromeUtils.import( + "chrome://remote/content/domains/Domain.jsm" +); +const { DomainCache } = ChromeUtils.import( + "chrome://remote/content/domains/DomainCache.jsm" +); + +class MockSession { + onEvent() {} +} + +const noopSession = new MockSession(); + +add_test(function test_DomainCache_constructor() { + new DomainCache(noopSession, {}); + + run_next_test(); +}); + +add_test(function test_DomainCache_domainSupportsMethod() { + const modules = { + Foo: class extends Domain { + bar() {} + }, + }; + const domains = new DomainCache(noopSession, modules); + + ok(domains.domainSupportsMethod("Foo", "bar")); + ok(!domains.domainSupportsMethod("Foo", "baz")); + ok(!domains.domainSupportsMethod("foo", "bar")); + + run_next_test(); +}); + +add_test(function test_DomainCache_get_invalidModule() { + Assert.throws(() => { + const domains = new DomainCache(noopSession, { Foo: undefined }); + domains.get("Foo"); + }, /UnknownMethodError/); + + run_next_test(); +}); + +add_test(function test_DomainCache_get_missingConstructor() { + Assert.throws(() => { + const domains = new DomainCache(noopSession, { Foo: {} }); + domains.get("Foo"); + }, /TypeError/); + + run_next_test(); +}); + +add_test(function test_DomainCache_get_superClassNotDomain() { + Assert.throws(() => { + const domains = new DomainCache(noopSession, { Foo: class {} }); + domains.get("Foo"); + }, /TypeError/); + + run_next_test(); +}); + +add_test(function test_DomainCache_get_constructs() { + let eventFired; + class Session { + onEvent(event) { + eventFired = event; + } + } + + let constructed = false; + class Foo extends Domain { + constructor() { + super(); + constructed = true; + } + } + + const session = new Session(); + const domains = new DomainCache(session, { Foo }); + + const foo = domains.get("Foo"); + ok(constructed); + ok(foo instanceof Foo); + + const event = {}; + foo.emit(event); + equal(event, eventFired); + + run_next_test(); +}); + +add_test(function test_DomainCache_size() { + class Foo extends Domain {} + const domains = new DomainCache(noopSession, { Foo }); + + equal(domains.size, 0); + domains.get("Foo"); + equal(domains.size, 1); + + run_next_test(); +}); + +add_test(function test_DomainCache_clear() { + let dtorCalled = false; + class Foo extends Domain { + destructor() { + dtorCalled = true; + } + } + + const domains = new DomainCache(noopSession, { Foo }); + + equal(domains.size, 0); + domains.get("Foo"); + equal(domains.size, 1); + + domains.clear(); + equal(domains.size, 0); + ok(dtorCalled); + + run_next_test(); +}); diff --git a/remote/test/unit/test_Error.js b/remote/test/unit/test_Error.js new file mode 100644 index 0000000000..a4d81483d2 --- /dev/null +++ b/remote/test/unit/test_Error.js @@ -0,0 +1,119 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +/* eslint-disable no-tabs */ + +const { + RemoteAgentError, + UnknownMethodError, + UnsupportedError, +} = ChromeUtils.import("chrome://remote/content/Error.jsm"); + +add_test(function test_RemoteAgentError_ctor() { + const e1 = new RemoteAgentError(); + equal(e1.name, "RemoteAgentError"); + equal(e1.message, ""); + equal(e1.cause, e1.message); + + const e2 = new RemoteAgentError("message"); + equal(e2.message, "message"); + equal(e2.cause, e2.message); + + const e3 = new RemoteAgentError("message", "cause"); + equal(e3.message, "message"); + equal(e3.cause, "cause"); + + run_next_test(); +}); + +add_test(function test_RemoteAgentError_notify() { + // nothing much we can test, except test that it doesn't throw + new RemoteAgentError().notify(); + + run_next_test(); +}); + +add_test(function test_RemoteAgentError_toString() { + const e = new RemoteAgentError("message"); + equal(e.toString(), RemoteAgentError.format(e)); + equal( + e.toString({ stack: true }), + RemoteAgentError.format(e, { stack: true }) + ); + + run_next_test(); +}); + +add_test(function test_RemoteAgentError_format() { + const { format } = RemoteAgentError; + + equal(format({ name: "HippoError" }), "HippoError"); + equal(format({ name: "HorseError", message: "neigh" }), "HorseError: neigh"); + + const dog = { + name: "DogError", + message: "woof", + stack: " one\ntwo\nthree ", + }; + equal(format(dog), "DogError: woof"); + equal( + format(dog, { stack: true }), + `DogError: woof: + one + two + three` + ); + + const cat = { + name: "CatError", + message: "meow", + stack: "four\nfive\nsix", + cause: dog, + }; + equal(format(cat), "CatError: meow"); + equal( + format(cat, { stack: true }), + `CatError: meow: + four + five + six +caused by: DogError: woof: + one + two + three` + ); + + run_next_test(); +}); + +add_test(function test_RemoteAgentError_fromJSON() { + const cdpErr = { + message: `TypeError: foo: + bar + baz`, + }; + const err = RemoteAgentError.fromJSON(cdpErr); + + equal(err.message, "TypeError: foo"); + equal(err.stack, "bar\nbaz"); + equal(err.cause, null); + + run_next_test(); +}); + +add_test(function test_UnsupportedError() { + ok(new UnsupportedError() instanceof RemoteAgentError); + run_next_test(); +}); + +add_test(function test_UnknownMethodError() { + ok(new UnknownMethodError() instanceof RemoteAgentError); + ok(new UnknownMethodError("domain").message.endsWith("domain")); + ok( + new UnknownMethodError("domain", "command").message.endsWith( + "domain.command" + ) + ); + run_next_test(); +}); diff --git a/remote/test/unit/test_Format.js b/remote/test/unit/test_Format.js new file mode 100644 index 0000000000..1787bdb491 --- /dev/null +++ b/remote/test/unit/test_Format.js @@ -0,0 +1,119 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { truncate, pprint } = ChromeUtils.import( + "chrome://remote/content/Format.jsm" +); + +const MAX_STRING_LENGTH = 250; +const HALF = "x".repeat(MAX_STRING_LENGTH / 2); + +add_test(function test_pprint() { + equal('[object Object] {"foo":"bar"}', pprint`${{ foo: "bar" }}`); + + equal("[object Number] 42", pprint`${42}`); + equal("[object Boolean] true", pprint`${true}`); + equal("[object Undefined] undefined", pprint`${undefined}`); + equal("[object Null] null", pprint`${null}`); + + let complexObj = { toJSON: () => "foo" }; + equal('[object Object] "foo"', pprint`${complexObj}`); + + let cyclic = {}; + cyclic.me = cyclic; + equal("[object Object] <cyclic object value>", pprint`${cyclic}`); + + let el = { + hasAttribute: attr => attr in el, + getAttribute: attr => (attr in el ? el[attr] : null), + nodeType: 1, + localName: "input", + id: "foo", + class: "a b", + href: "#", + name: "bar", + src: "s", + type: "t", + }; + equal( + '<input id="foo" class="a b" href="#" name="bar" src="s" type="t">', + pprint`${el}` + ); + + run_next_test(); +}); + +add_test(function test_truncate_empty() { + equal(truncate``, ""); + run_next_test(); +}); + +add_test(function test_truncate_noFields() { + equal(truncate`foo bar`, "foo bar"); + run_next_test(); +}); + +add_test(function test_truncate_multipleFields() { + equal(truncate`${0}`, "0"); + equal(truncate`${1}${2}${3}`, "123"); + equal(truncate`a${1}b${2}c${3}`, "a1b2c3"); + run_next_test(); +}); + +add_test(function test_truncate_primitiveFields() { + equal(truncate`${123}`, "123"); + equal(truncate`${true}`, "true"); + equal(truncate`${null}`, ""); + equal(truncate`${undefined}`, ""); + run_next_test(); +}); + +add_test(function test_truncate_string() { + equal(truncate`${"foo"}`, "foo"); + equal(truncate`${"x".repeat(250)}`, "x".repeat(250)); + equal(truncate`${"x".repeat(260)}`, `${HALF} ... ${HALF}`); + run_next_test(); +}); + +add_test(function test_truncate_array() { + equal(truncate`${["foo"]}`, JSON.stringify(["foo"])); + equal(truncate`${"foo"} ${["bar"]}`, `foo ${JSON.stringify(["bar"])}`); + equal( + truncate`${["x".repeat(260)]}`, + JSON.stringify([`${HALF} ... ${HALF}`]) + ); + + run_next_test(); +}); + +add_test(function test_truncate_object() { + equal(truncate`${{}}`, JSON.stringify({})); + equal(truncate`${{ foo: "bar" }}`, JSON.stringify({ foo: "bar" })); + equal( + truncate`${{ foo: "x".repeat(260) }}`, + JSON.stringify({ foo: `${HALF} ... ${HALF}` }) + ); + equal(truncate`${{ foo: ["bar"] }}`, JSON.stringify({ foo: ["bar"] })); + equal( + truncate`${{ foo: ["bar", { baz: 42 }] }}`, + JSON.stringify({ foo: ["bar", { baz: 42 }] }) + ); + + let complex = { + toString() { + return "hello world"; + }, + }; + equal(truncate`${complex}`, "hello world"); + + let longComplex = { + toString() { + return "x".repeat(260); + }, + }; + equal(truncate`${longComplex}`, `${HALF} ... ${HALF}`); + + run_next_test(); +}); diff --git a/remote/test/unit/test_Session.js b/remote/test/unit/test_Session.js new file mode 100644 index 0000000000..65d1b8bcf8 --- /dev/null +++ b/remote/test/unit/test_Session.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Session } = ChromeUtils.import( + "chrome://remote/content/sessions/Session.jsm" +); + +const connection = { + registerSession: () => {}, + transport: { + on: () => {}, + }, +}; + +class MockTarget { + constructor() {} + + get browsingContext() { + return { id: 42 }; + } + + get mm() { + return { + addMessageListener() {}, + removeMessageListener() {}, + loadFrameScript() {}, + sendAsyncMessage() {}, + }; + } +} + +add_test(function test_Session_destructor() { + const session = new Session(connection, new MockTarget()); + session.domains.get("Browser"); + equal(session.domains.size, 1); + session.destructor(); + equal(session.domains.size, 0); + + run_next_test(); +}); diff --git a/remote/test/unit/test_StreamRegistry.js b/remote/test/unit/test_StreamRegistry.js new file mode 100644 index 0000000000..ed68577540 --- /dev/null +++ b/remote/test/unit/test_StreamRegistry.js @@ -0,0 +1,172 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AsyncShutdown } = ChromeUtils.import( + "resource://gre/modules/AsyncShutdown.jsm" +); +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const { StreamRegistry } = ChromeUtils.import( + "chrome://remote/content/StreamRegistry.jsm" +); + +add_test(function test_constructor() { + const registry = new StreamRegistry(); + equal(registry.streams.size, 0); + + run_next_test(); +}); + +add_test(async function test_destructor() { + const registry = new StreamRegistry(); + const { file: file1, path: path1 } = await createFile("foo bar"); + const { file: file2, path: path2 } = await createFile("foo bar"); + + registry.add(file1); + registry.add(file2); + + equal(registry.streams.size, 2); + + await registry.destructor(); + equal(registry.streams.size, 0); + ok(!(await OS.File.exists(path1)), "temporary file has been removed"); + ok(!(await OS.File.exists(path2)), "temporary file has been removed"); + + run_next_test(); +}); + +add_test(async function test_addValidStreamType() { + const registry = new StreamRegistry(); + const { file } = await createFile("foo bar"); + + const handle = registry.add(file); + equal(registry.streams.size, 1, "A single stream has been added"); + equal(typeof handle, "string", "Handle is of type string"); + ok(registry.streams.has(handle), "Handle has been found"); + equal(registry.streams.get(handle), file, "Expected OS.File stream found"); + + run_next_test(); +}); + +add_test(async function test_addCreatesDifferentHandles() { + const registry = new StreamRegistry(); + const { file } = await createFile("foo bar"); + + const handle1 = registry.add(file); + equal(registry.streams.size, 1, "A single stream has been added"); + equal(typeof handle1, "string", "Handle is of type string"); + ok(registry.streams.has(handle1), "Handle has been found"); + equal(registry.streams.get(handle1), file, "Expected OS.File stream found"); + + const handle2 = registry.add(file); + equal(registry.streams.size, 2, "A single stream has been added"); + equal(typeof handle2, "string", "Handle is of type string"); + ok(registry.streams.has(handle2), "Handle has been found"); + equal(registry.streams.get(handle2), file, "Expected OS.File stream found"); + + notEqual(handle1, handle2, "Different handles have been generated"); + + run_next_test(); +}); + +add_test(async function test_addInvalidStreamType() { + const registry = new StreamRegistry(); + Assert.throws(() => registry.add(new Blob([])), /UnsupportedError/); + + run_next_test(); +}); + +add_test(async function test_getForValidHandle() { + const registry = new StreamRegistry(); + const { file } = await createFile("foo bar"); + const handle = registry.add(file); + + equal(registry.streams.size, 1, "A single stream has been added"); + equal(registry.get(handle), file, "Expected OS.File stream found"); + + run_next_test(); +}); + +add_test(async function test_getForInvalidHandle() { + const registry = new StreamRegistry(); + const { file } = await createFile("foo bar"); + registry.add(file); + + equal(registry.streams.size, 1, "A single stream has been added"); + Assert.throws(() => registry.get("foo"), /TypeError/); + + run_next_test(); +}); + +add_test(async function test_removeForValidHandle() { + const registry = new StreamRegistry(); + const { file: file1, path: path1 } = await createFile("foo bar"); + const { file: file2, path: path2 } = await createFile("foo bar"); + + const handle1 = registry.add(file1); + const handle2 = registry.add(file2); + + equal(registry.streams.size, 2); + + await registry.remove(handle1); + equal(registry.streams.size, 1); + ok( + !(await OS.File.exists(path1)), + "temporary file for first stream has been removed" + ); + equal(registry.get(handle2), file2, "Second stream has not been closed"); + ok( + await OS.File.exists(path2), + "temporary file for second stream hasn't been removed" + ); + + run_next_test(); +}); + +add_test(async function test_removeForInvalidHandle() { + const registry = new StreamRegistry(); + const { file } = await createFile("foo bar"); + registry.add(file); + + equal(registry.streams.size, 1, "A single stream has been added"); + await Assert.rejects(registry.remove("foo"), /TypeError/); + + run_next_test(); +}); + +async function createFile(contents, options = {}) { + let { path = null, remove = true } = options; + + if (!path) { + const basePath = OS.Path.join(OS.Constants.Path.tmpDir, "remote-agent.txt"); + const { file, path: tmpPath } = await OS.File.openUnique(basePath, { + humanReadable: true, + }); + await file.close(); + path = tmpPath; + } + + let encoder = new TextEncoder(); + let array = encoder.encode(contents); + + const count = await OS.File.writeAtomic(path, array, { + encoding: "utf-8", + tmpPath: path + ".tmp", + }); + equal(count, contents.length, "All data has been written to file"); + + const file = await OS.File.open(path); + + // Automatically remove the file once the test has finished + if (remove) { + registerCleanupFunction(async () => { + await file.close(); + await OS.File.remove(path, { ignoreAbsent: true }); + }); + } + + return { file, path }; +} diff --git a/remote/test/unit/test_Sync.js b/remote/test/unit/test_Sync.js new file mode 100644 index 0000000000..36150c4cc3 --- /dev/null +++ b/remote/test/unit/test_Sync.js @@ -0,0 +1,230 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const { PollPromise } = ChromeUtils.import("chrome://remote/content/Sync.jsm"); + +/** + * Mimic a DOM node for listening for events. + */ +class MockElement { + constructor() { + this.capture = false; + this.func = null; + this.eventName = null; + this.untrusted = false; + } + + addEventListener(name, func, capture, untrusted) { + this.eventName = name; + this.func = func; + if (capture != null) { + this.capture = capture; + } + if (untrusted != null) { + this.untrusted = untrusted; + } + } + + click() { + if (this.func) { + let details = { + capture: this.capture, + target: this, + type: this.eventName, + untrusted: this.untrusted, + }; + this.func(details); + } + } + + removeEventListener(name, func) { + this.capture = false; + this.func = null; + this.eventName = null; + this.untrusted = false; + } +} + +/** + * Mimic a message manager for sending messages. + */ +class MessageManager { + constructor() { + this.func = null; + this.message = null; + } + + addMessageListener(message, func) { + this.func = func; + this.message = message; + } + + removeMessageListener(message) { + this.func = null; + this.message = null; + } + + send(message, data) { + if (this.func) { + this.func({ + data, + message, + target: this, + }); + } + } +} + +/** + * Mimics nsITimer, but instead of using a system clock you can + * preprogram it to invoke the callback after a given number of ticks. + */ +class MockTimer { + constructor(ticksBeforeFiring) { + this.goal = ticksBeforeFiring; + this.ticks = 0; + this.cancelled = false; + } + + initWithCallback(cb, timeout, type) { + this.ticks++; + if (this.ticks >= this.goal) { + cb(); + } + } + + cancel() { + this.cancelled = true; + } +} + +add_test(function test_executeSoon_callback() { + // executeSoon() is already defined for xpcshell in head.js. As such import + // our implementation into a custom namespace. + let sync = {}; + ChromeUtils.import("chrome://remote/content/Sync.jsm", sync); + + for (let func of ["foo", null, true, [], {}]) { + Assert.throws(() => sync.executeSoon(func), /TypeError/); + } + + let a; + sync.executeSoon(() => { + a = 1; + }); + executeSoon(() => equal(1, a)); + + run_next_test(); +}); + +add_test(function test_PollPromise_funcTypes() { + for (let type of ["foo", 42, null, undefined, true, [], {}]) { + Assert.throws(() => new PollPromise(type), /TypeError/); + } + new PollPromise(() => {}); + new PollPromise(function() {}); + + run_next_test(); +}); + +add_test(function test_PollPromise_timeoutTypes() { + for (let timeout of ["foo", true, [], {}]) { + Assert.throws(() => new PollPromise(() => {}, { timeout }), /TypeError/); + } + for (let timeout of [1.2, -1]) { + Assert.throws(() => new PollPromise(() => {}, { timeout }), /RangeError/); + } + for (let timeout of [null, undefined, 42]) { + new PollPromise(resolve => resolve(1), { timeout }); + } + + run_next_test(); +}); + +add_test(function test_PollPromise_intervalTypes() { + for (let interval of ["foo", null, true, [], {}]) { + Assert.throws(() => new PollPromise(() => {}, { interval }), /TypeError/); + } + for (let interval of [1.2, -1]) { + Assert.throws(() => new PollPromise(() => {}, { interval }), /RangeError/); + } + new PollPromise(() => {}, { interval: 42 }); + + run_next_test(); +}); + +add_task(async function test_PollPromise_retvalTypes() { + for (let typ of [true, false, "foo", 42, [], {}]) { + strictEqual(typ, await new PollPromise(resolve => resolve(typ))); + } +}); + +add_task(async function test_PollPromise_rethrowError() { + let nevals = 0; + let err; + try { + await PollPromise(() => { + ++nevals; + throw new Error(); + }); + } catch (e) { + err = e; + } + equal(1, nevals); + ok(err instanceof Error); +}); + +add_task(async function test_PollPromise_noTimeout() { + let nevals = 0; + await new PollPromise((resolve, reject) => { + ++nevals; + nevals < 100 ? reject() : resolve(); + }); + equal(100, nevals); +}); + +add_task(async function test_PollPromise_zeroTimeout() { + // run at least once when timeout is 0 + let nevals = 0; + let start = new Date().getTime(); + await new PollPromise( + (resolve, reject) => { + ++nevals; + reject(); + }, + { timeout: 0 } + ); + let end = new Date().getTime(); + equal(1, nevals); + less(end - start, 500); +}); + +add_task(async function test_PollPromise_timeoutElapse() { + let nevals = 0; + let start = new Date().getTime(); + await new PollPromise( + (resolve, reject) => { + ++nevals; + reject(); + }, + { timeout: 100 } + ); + let end = new Date().getTime(); + lessOrEqual(nevals, 11); + greaterOrEqual(end - start, 100); +}); + +add_task(async function test_PollPromise_interval() { + let nevals = 0; + await new PollPromise( + (resolve, reject) => { + ++nevals; + reject(); + }, + { timeout: 100, interval: 100 } + ); + equal(2, nevals); +}); diff --git a/remote/test/unit/xpcshell.ini b/remote/test/unit/xpcshell.ini new file mode 100644 index 0000000000..8e004e8dd2 --- /dev/null +++ b/remote/test/unit/xpcshell.ini @@ -0,0 +1,14 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +[DEFAULT] +skip-if = appname == "thunderbird" + +[test_Connection.js] +[test_DomainCache.js] +[test_Error.js] +[test_Format.js] +[test_Session.js] +[test_StreamRegistry.js] +[test_Sync.js] |