[
  {
    "path": ".gitignore",
    "content": ".DS_Store\n.eslintrc.js\n.eslintignore\n.profile\n.vscode\nnode_modules\npackage-lock.json\nrpc/node_modules\nrpc/package-lock.json\nlib/.local-data"
  },
  {
    "path": ".npmignore",
    "content": "# repeats from .gitignore\n.DS_Store\n.eslintrc.js\n.eslintignore\n.profile\n.vscode\nnode_modules\npackage-lock.json\nrpc/node_modules\nrpc/package-lock.json\nlib/.local-data\n\n.npmignore\nexamples\nCONTRIBUTING.md\nAPI.md\nrpc/test.js\ntest\n"
  },
  {
    "path": "API.md",
    "content": "## API v0.9\n\n> This is a pre-release API, so it is a subject to change. Please use it at your own risk. Once API is validated, it will be bumped to v1.0 and preserved for backwards compatibility.\n\n##### Node side API\n\n- [carlo.enterTestMode()](#carloentertestmode)\n- [carlo.launch([options])](#carlolaunchoptions)\n- [class: App](#class-app)\n  * [event: 'exit'](#event-exit)\n  * [event: 'window'](#event-window)\n  * [App.browserForTest()](#appbrowserfortest)\n  * [App.createWindow(options)](#appcreatewindowoptions)\n  * [App.evaluate(pageFunction[, ...args])](#appevaluatepagefunction-args)\n  * [App.exit()](#appexit)\n  * [App.exposeFunction(name, carloFunction)](#appexposefunctionname-carlofunction)\n  * [App.load(uri[, ...params])](#apploaduri-params)\n  * [App.mainWindow()](#appmainwindow)\n  * [App.serveFolder(folder[, prefix])](#appservefolderfolder-prefix)\n  * [App.serveHandler(handler)](#appservehandlerhandler)\n  * [App.serveOrigin(base[, prefix])](#appserveoriginbase-prefix)\n  * [App.setIcon(image)](#appseticonimage)\n  * [App.windows()](#appwindows)\n- [class: HttpRequest](#class-httprequest)\n  * [HttpRequest.abort()](#httprequestabort)\n  * [HttpRequest.continue()](#httprequestcontinue)\n  * [HttpRequest.fail()](#httprequestfail)\n  * [HttpRequest.fulfill(options)](#httprequestfulfilloptions)\n  * [HttpRequest.headers()](#httprequestheaders)\n  * [HttpRequest.method()](#httprequestmethod)\n  * [HttpRequest.url()](#httprequesturl)\n- [class: Window](#class-window)\n  * [event: 'close'](#event-close)\n  * [Window.bounds()](#windowbounds)\n  * [Window.bringToFront()](#windowbringtofront)\n  * [Window.close()](#windowclose)\n  * [Window.evaluate(pageFunction[, ...args])](#windowevaluatepagefunction-args)\n  * [Window.exposeFunction(name, carloFunction)](#windowexposefunctionname-carlofunction)\n  * [Window.fullscreen()](#windowfullscreen)\n  * [Window.load(uri[, ...params])](#windowloaduri-params)\n  * [Window.maximize()](#windowmaximize)\n  * [Window.minimize()](#windowminimize)\n  * [Window.pageForTest()](#windowpagefortest)\n  * [Window.paramsForReuse()](#windowparamsforreuse)\n  * [Window.serveFolder(folder[, prefix])](#windowservefolderfolder-prefix)\n  * [Window.serveHandler(handler)](#windowservehandlerhandler)\n  * [Window.serveOrigin(base[, prefix])](#windowserveoriginbase-prefix)\n  * [Window.setBounds(bounds)](#windowsetboundsbounds)\n\n##### Web side API\n\n- [carlo.fileInfo(file)](#carlofileinfofile)\n- [carlo.loadParams()](#carloloadparams)\n\n#### carlo.enterTestMode()\n\nEnters headless test mode. In the test mode, Puppeteer browser and pages are available via\n[App.browserForTest()](#appbrowserfortest) and [Window.pageForTest()](#windowpagefortest) respectively.\nPlease refer to the Puppeteer [documentation](https://pptr.dev) for details on headless testing.\n\n#### carlo.launch([options])\n- `options` <[Object]> Set of configurable options to set on the app. Can have the following fields:\n  - `width` <[number]> App window width in pixels.\n  - `height` <[number]> App window height in pixels.\n  - `top`: <[number]> App window top offset in pixels.\n  - `left` <[number]> App window left offset in pixels.\n  - `bgcolor` <[string]> Background color using hex notation, defaults to `'#ffffff'`.\n  - `channel` <[Array]<[string]>> Browser to be used, defaults to `['stable']`:\n    - `'stable'` only uses locally installed stable channel Chrome.\n    - `'canary'` only uses Chrome SxS aka Canary.\n    - `'chromium'` downloads local version of Chromium compatible with the Puppeteer used.\n    - `'rXXXXXX'` a specific Chromium revision is used.\n  - `icon` <[Buffer]|[string]> Application icon to be used in the system dock. Either buffer containing PNG or a path to the PNG file on the file system. This feature is only available in Chrome M72+. One can use `'canary'` channel to see it in action before M72 hits stable.\n  - `paramsForReuse` <\\*> Optional parameters to share between Carlo instances. See [Window.paramsForReuse](#windowparamsforreuse) for details.\n  - `title` <[string]> Application title.\n  - `userDataDir` <[string]> Path to a [User Data Directory](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md). This folder is created upon the first app launch and contains user settings and Web storage data. Defaults to `'.profile'`.\n  - `executablePath` <[string]> Path to a Chromium or Chrome executable to run instead of the automatically located Chrome. If `executablePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). Carlo is only guaranteed to work with the latest Chrome stable version.\n  - `args` <[Array]<[string]>> Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](https://peter.sh/experiments/chromium-command-line-switches/).\n- `return`: <[Promise]<[App]>> Promise which resolves to the app instance.\n\nLaunches the browser.\n\n### class: App\n\n#### event: 'exit'\nEmitted when the last window closes.\n\n#### event: 'window'\n- <[Window]>\n\nEmitted when the new window opens. This can happen in the following situations:\n- [App.createWindow](#appcreatewindowoptions) was called.\n- [carlo.launch](#carlolaunchoptions) was called from the same or another instance of the Node app.\n- [window.open](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) was called from within the web page.\n\n#### App.browserForTest()\n- `return`: <[Browser]> Puppeteer browser object for testing.\n\n#### App.createWindow([options])\n- `options` <[Object]> Set of configurable options to set on the app. Can have the following fields:\n  - `width` <[number]> Window width in pixels, defaults to app width.\n  - `height` <[number]> Window height in pixels, defaults to app height.\n  - `top` <[number]> Window top in pixels, defaults to app top.\n  - `left` <[number]> Window left in pixels, defaults to app left.\n  - `bgcolor` <[string]> Background color using hex notation, defaults to app `bgcolor`.\n- `return`: <[Promise]<[Window]>> Promise which resolves to the window instance.\n\nCreates a new app window.\n\n#### App.evaluate(pageFunction[, ...args])\n\nShortcut to the main window's [Window.evaluate(pageFunction[, ...args])](#windowevaluatepagefunction-args).\n\n#### App.exit()\n- `return`: <[Promise]>\n\nCloses the browser window.\n\n#### App.exposeFunction(name, carloFunction)\n- `name` <[string]> Name of the function on the window object.\n- `carloFunction` <[function]> Callback function which will be called in Carlo's context.\n- `return`: <[Promise]>\n\nThe method adds a function called `name` on the pages' `window` object.\nWhen called, the function executes `carloFunction` in Node.js and returns a [Promise] which resolves to the return value of `carloFunction`.\n\nIf the `carloFunction` returns a [Promise], it will be awaited.\n\n> **NOTE** Functions installed via `App.exposeFunction` survive navigations.\n\nAn example of adding an `md5` function into the page:\n\n`main.js`\n```js\nconst carlo = require('carlo');\nconst crypto = require('crypto');\n\ncarlo.launch().then(async app => {\n  app.on('exit', () => process.exit());\n  app.serveFolder(__dirname);\n  await app.exposeFunction('md5', text =>  // <-- expose function\n    crypto.createHash('md5').update(text).digest('hex')\n  );\n  await app.load('index.html');\n});\n```\n\n`index.html`\n```html\n<script>\nmd5('digest').then(result => document.body.textContent = result);\n</script>\n```\n\n#### App.load(uri[, ...params])\n\nShortcut to the main window's [Window.load(uri[, ...params])](#windowloaduri-params).\n\n#### App.mainWindow()\n- `return`: <[Window]> Returns main window.\n\nRunning app guarantees to have main window. If current main window closes, a next open window\nbecomes the main one.\n\n#### App.serveFolder(folder[, prefix])\n- `folder` <[string]> Folder with web content to make available to Chrome.\n- `prefix` <[string]> Prefix of the URL path to serve from the given folder.\n\nMakes the content of the given folder available to the Chrome web app.\n\nAn example of adding a local `www` folder along with the `node_modules`:\n\n`main.js`\n```js\nconst carlo = require('carlo');\n\ncarlo.launch().then(async app => {\n  app.on('exit', () => process.exit());\n  app.serveFolder(`${__dirname}/www`);\n  app.serveFolder(`${__dirname}/node_modules`, 'node_modules');\n  await app.load('index.html');\n});\n```\n***www***/`index.html`\n```html\n<style>body { white-space: pre; }</style>\n<script>\nfetch('node_modules/carlo/package.json')\n    .then(response => response.text())\n    .then(text => document.body.textContent = text);\n</script>\n```\n\n#### App.serveHandler(handler)\n- `handler` <[Function]> Network handler callback accepting the [HttpRequest](#class-httprequest) parameter.\n\nAn example serving primitive `index.html`:\n```js\nconst carlo = require('carlo');\n\ncarlo.launch().then(async app => {\n  app.on('exit', () => process.exit());\n  app.serveHandler(request => {\n    if (request.url().endsWith('/index.html'))\n      request.fulfill({body: Buffer.from('<html>Hello World</hmtl>')});\n    else\n      request.continue();  // <-- user needs to resolve each request, otherwise it'll time out.\n  });\n  await app.load('index.html');  // <-- loads index.html served above.\n});\n```\n\nHandler function is called with every network request in this app. It can abort, continue or fulfill each request. Only single app-wide handler can be registered.\n\n#### App.serveOrigin(base[, prefix])\n- `base` <[string]> Base to serve web content from.\n- `prefix` <[string]> Prefix of the URL path to serve from the given folder.\n\nFetches Carlo content from the specified origin instead of reading it from the file system, eg `http://localhost:8080`. This mode can be used for the fast development mode available in web frameworks.\n\nAn example of adding the local `http://localhost:8080` origin:\n\n```js\nconst carlo = require('carlo');\n\ncarlo.launch().then(async app => {\n  app.on('exit', () => process.exit());\n  app.serveOrigin('http://localhost:8080');  // <-- fetch from the local server\n  app.serveFolder(__dirname);  // <-- won't be used\n  await app.load('index.html');\n});\n```\n\n#### App.setIcon(image)\n- `image`: <[Buffer]|[string]> Either buffer containing PNG or a path to the PNG file on the file system.\n\nSpecifies image to be used as an app icon in the system dock.\n\n> This feature is only available in Chrome M72+. One can use `'canary'` channel to see it in action before M72 hits stable.\n\n#### App.windows()\n- `return`: <[Array]<[Window]>> Returns all currently opened windows.\n\nRunning app guarantees to have at least one open window.\n\n### class: HttpRequest\n\nHandlers registered via [App.serveHandler](#appservehandlerhandler) and [Window.serveHandler](#windowservehandlerhandler) receive parameter of this upon every network request.\n\n#### HttpRequest.abort()\n- `return`: <[Promise]>\n\nAborts request. If request is a navigation request, navigation is aborted as well.\n\n#### HttpRequest.continue()\n\nProceeds with the default behavior for this request. Either serves it from the filesystem or defers to the browser.\n\n#### HttpRequest.fail()\n- `return`: <[Promise]>\n\nMarks the request as failed. If request is a navigation request, navigation is still committed, but to a location that fails to be fetched.\n\n#### HttpRequest.fulfill(options)\n- `options`: <[Object]>\n  - `status` <[number]> HTTP status code (200, 304, etc), defaults to 200.\n  - `headers` <[Object]> HTTP response headers.\n  - `body` <[Buffer]> Response body.\n- `return`: <[Promise]>\n\nFulfills the network request with the given data. `'Content-Length'` header is generated in case it is not listed in the headers.\n\n#### HttpRequest.headers()\n- `return`: <[Object]> HTTP headers\n\nNetwork request headers.\n\n#### HttpRequest.method()\n- `return`: <[string]> HTTP method\n\nHTTP method of this network request (GET, POST, etc).\n\n#### HttpRequest.url()\n- `return`: <[string]> HTTP URL\n\nNetwork request URL.\n\n### class: Window\n\n#### event: 'close'\nEmitted when the window closes.\n\n#### Window.bounds()\n- `return`: <[Promise]<[Object]>>\n  - `top` <[number]> Top offset in pixels.\n  - `left` <[number]> Left offset in pixels.\n  - `width` <[number]> Width in pixels.\n  - `height` <[number]> Height in pixels.\n\nReturns window bounds.\n\n#### Window.bringToFront()\n- `return`: <[Promise]>\n\nBrings this window to front.\n\n#### Window.close()\n- `return`: <[Promise]>\n\nCloses this window.\n\n#### Window.evaluate(pageFunction[, ...args])\n- `pageFunction` <[function]|[string]> Function to be evaluated in the page context.\n- `...args` <...[Serializable]> Arguments to pass to `pageFunction`.\n- `return`: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`.\n\nIf the function passed to the `Window.evaluate` returns a [Promise], then `Window.evaluate` would wait for the promise to resolve and return its value.\n\nIf the function passed to the `Window.evaluate` returns a non-[Serializable] value, then `Window.evaluate` resolves to `undefined`.\n\n```js\nconst result = await window.evaluate(() => navigator.userAgent);\nconsole.log(result);  // prints \"<UA>\" in Node console\n```\n\nPassing arguments to `pageFunction`:\n```js\nconst result = await window.evaluate(x => {\n  return Promise.resolve(8 * x);\n}, 7);\nconsole.log(result);  // prints \"56\" in Node console\n```\n\nA string can also be passed in instead of a function:\n```js\nconsole.log(await window.evaluate('1 + 2'));  // prints \"3\"\nconst x = 10;\nconsole.log(await window.evaluate(`1 + ${x}`));  // prints \"11\"\n```\n\n#### Window.exposeFunction(name, carloFunction)\n- `name` <[string]> Name of the function on the window object.\n- `carloFunction` <[function]> Callback function which will be called in Carlo's context.\n- `return`: <[Promise]>\n\nSame as [App.exposeFunction](#appexposefunctionname-carlofunction), but only applies to\nthe current window.\n\n> **NOTE** Functions installed via `Window.exposeFunction` survive navigations.\n\n#### Window.fullscreen()\n- `return`: <[Promise]>\n\nTurns the window into the full screen mode. Behavior is platform-specific.\n\n#### Window.load(uri[, ...params])\n- `uri` <[string]> Path to the resource relative to the folder passed into [`serveFolder()`].\n- `params` <\\*> Optional parameters to pass to the web application. Parameters can be\nprimitive types, <[Array]>, <[Object]> or <[rpc]> `handles`.\n- `return`: <[Promise]> Resolves upon DOMContentLoaded event in the web page.\n\nNavigates the corresponding web page to the given `uri`, makes given `params` available in the web page via [carlo.loadParams()](#carloloadparams).\n\n`main.js`\n```js\nconst carlo = require('carlo');\nconst { rpc } = require('carlo/rpc');\n\ncarlo.launch().then(async app => {\n  app.serveFolder(__dirname);\n  app.on('exit', () => process.exit());\n  await app.load('index.html', rpc.handle(new Backend));\n});\n\nclass Backend {\n  hello(name) {\n    console.log(`Hello ${name}`);\n    return 'Backend is happy';\n  }\n\n  setFrontend(frontend) {\n    // Node world can now use frontend RPC handle.\n    this.frontend_ = frontend;\n  }\n}\n```\n\n`index.html`\n```html\n<script>\nclass Frontend {}\n\nasync function load(backend) {\n  // Web world can now use backend RPC handle.\n  console.log(await backend.hello('from frontend'));\n  await backend.setFrontend(rpc.handle(new Frontend));\n}\n</script>\n<body>Open console</body>\n```\n\n#### Window.maximize()\n- `return`: <[Promise]>\n\nMaximizes the window. Behavior is platform-specific.\n\n#### Window.minimize()\n- `return`: <[Promise]>\n\nMinimizes the window. Behavior is platform-specific.\n\n#### Window.pageForTest()\n- `return`: <[Page]> Puppeteer page object for testing.\n\n#### Window.paramsForReuse()\n- `return`: <\\*> parameters.\n\nReturns the `options.paramsForReuse` value passed into the [carlo.launch](#carlolaunchoptions).\n\nThese parameters are useful when Carlo app is started multiple times:\n- First time the Carlo app is started, it successfully calls `carlo.launch` and opens the main window.\n- Second time the Carlo app is started, `carlo.launch` fails with the 'browser is already running' exception.\n- Despite the fact that second call to `carlo.launch` failed, a new window is created in the first Carlo app. This window contains `paramsForReuse` value that was specified in the second (failed) `carlo.launch` call.\n\nThis way app can pass initialization parameters such as command line, etc. to the singleton Carlo that owns the browser.\n\n#### Window.serveFolder(folder[, prefix])\n- `folder` <[string]> Folder with web content to make available to Chrome.\n- `prefix` <[string]> Prefix of the URL path to serve from the given folder.\n\nSame as [App.serveFolder(folder[, prefix])](#appservefolderfolder-prefix), but\nonly applies to current window.\n\n#### Window.serveHandler(handler)\n- `handler` <[Function]> Network handler callback accepting the [HttpRequest](#class-httprequest) parameter.\n\nSame as [App.serveHandler(handler)](#appservehandlerhandler), but only applies to the current window requests.\nOnly single window-level handler can be installed in window.\n\n#### Window.serveOrigin(base[, prefix])\n- `base` <[string]> Base to serve web content from.\n- `prefix` <[string]> Prefix of the URL path to serve from the given folder.\n\nSame as [App.serveOrigin(base[, prefix])](#appserveoriginbase-prefix), but\nonly applies to current window.\n\n#### Window.setBounds(bounds)\n- `bounds` <[Object]> Window bounds:\n  - `top` <[number]> Top offset in pixels.\n  - `left` <[number]> Left offset in pixels.\n  - `width` <[number]> Width in pixels.\n  - `height` <[number]> Height in pixels.\n- `return`: <[Promise]>\n\nSets window bounds. Parameters `top`, `left`, `width` and `height` are all optional. Dimension or\nthe offset is only applied when specified.\n\n#### carlo.fileInfo(file)\n- `file` <[File]> to get additional information for.\n- `return`: <[Promise]<[Object]>>\n  - `path` absolute path to the given file.\n\n> Available in Chrome M73+.\n\nReturns additional information about the file, otherwise not available to the web.\n\n\n#### carlo.loadParams()\n- `return`: <[Promise]<[Array]>> parameters passed into [window.load()](#windowloaduri-params).\n\nThis method is available in the Web world and returns parameters passed into the [window.load()](#windowloaduri-params). This is how Carlo passes initial set of <[rpc]> handles to Node objects into the web world.\n\n[`serveFolder()`]: #windowservefolderfolder-prefix\n[App]: #class-app\n[Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array \"Array\"\n[Browser]: https://pptr.dev/#?show=api-class-browser \"Browser\"\n[Buffer]: https://nodejs.org/api/buffer.html#buffer_class_buffer \"Buffer\"\n[File]: https://developer.mozilla.org/en-US/docs/Web/API/File \"File\"\n[Object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object \"Object\"\n[Page]: https://pptr.dev/#?show=api-class-page \"Page\"\n[Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise \"Promise\"\n[Serializable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Description \"Serializable\"\n[Window]: #class-window\n[boolean]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type \"Boolean\"\n[function]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function \"Function\"\n[number]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type \"Number\"\n[origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin \"Origin\"\n[rpc]: https://github.com/GoogleChromeLabs/carlo/blob/master/rpc/rpc.md \"rpc\"\n[string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type \"String\"\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# How to Contribute\n\nWe'd love to accept your patches and contributions to this project. There are\njust a few small guidelines you need to follow.\n\n## Contributor License Agreement\n\nContributions to this project must be accompanied by a Contributor License\nAgreement. You (or your employer) retain the copyright to your contribution;\nthis simply gives us permission to use and redistribute your contributions as\npart of the project. Head over to <https://cla.developers.google.com/> to see\nyour current agreements on file or to sign a new one.\n\nYou generally only need to submit a CLA once, so if you've already submitted one\n(even if it was for a different project), you probably don't need to do it\nagain.\n\n## Code reviews\n\nAll submissions, including submissions by project members, require review. We\nuse GitHub pull requests for this purpose. Consult\n[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more\ninformation on using pull requests.\n\n## Community Guidelines\n\nThis project follows [Google's Open Source Community\nGuidelines](https://opensource.google.com/conduct/).\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# Carlo - headful Node app framework\n\n### ❗Carlo is [no longer maintained](https://github.com/GoogleChromeLabs/carlo/issues/163#issuecomment-592238093). \n\n-----------------------\n\n\n> Carlo provides Node applications with [Google Chrome](https://www.google.com/chrome/) rendering capabilities, communicates with the locally-installed browser instance using the [Puppeteer](https://github.com/GoogleChrome/puppeteer/) project, and implements a remote call infrastructure for communication between Node and the browser.\n\n###### [API](https://github.com/GoogleChromeLabs/carlo/blob/master/API.md) | [FAQ](#faq) | [Contributing](https://github.com/GoogleChromeLabs/carlo/blob/master/CONTRIBUTING.md)\n\n![image](https://user-images.githubusercontent.com/883973/47826256-0531fc80-dd34-11e8-9c8d-c1b93a6ba631.png)\n\n<!-- [START usecases] -->\n###### What can I do?\n\nWith Carlo, users can create hybrid applications that use Web stack for rendering and Node for capabilities:\n- For Node applications, the web rendering stack lets users visualize the dynamic state of the app. \n- For Web applications, additional system capabilities are accessible from Node.\n- The application can be bundled into a single executable using [pkg](https://github.com/zeit/pkg).\n\n###### How does it work?\n\n- Carlo locates Google Chrome installed locally.\n- Launches Chrome and establishes a connection over the process pipe.\n- Exposes a high-level API for rendering in Chrome with the Node environment.\n\n<!-- [END usecases] -->\n\n<!-- [START getstarted] -->\n\n## Usage\n\nInstall Carlo\n\n#### npm\n```bash\nnpm i carlo\n# yarn add carlo\n```\n\n> Carlo requires at least Node v7.6.0.\n\n**Example** - Display local environment\n\nSave file as **example.js**\n\n```js\nconst carlo = require('carlo');\n\n(async () => {\n  // Launch the browser.\n  const app = await carlo.launch();\n\n  // Terminate Node.js process on app window closing.\n  app.on('exit', () => process.exit());\n\n  // Tell carlo where your web files are located.\n  app.serveFolder(__dirname);\n\n  // Expose 'env' function in the web environment.\n  await app.exposeFunction('env', _ => process.env);\n\n  // Navigate to the main page of your app.\n  await app.load('example.html');\n})();\n```\n\nSave file as **example.html**\n\n```html\n<script>\nasync function run() {\n  // Call the function that was exposed in Node.\n  const data = await env();\n  for (const type in data) {\n    const div = document.createElement('div');\n    div.textContent = `${type}: ${data[type]}`;\n    document.body.appendChild(div);\n  }\n}\n</script>\n<body onload=\"run()\">\n```\n\nRun your application:\n\n```bash\nnode example.js\n```\n\nCheck out [systeminfo](https://github.com/GoogleChromeLabs/carlo/tree/master/examples/systeminfo) and [terminal](https://github.com/GoogleChromeLabs/carlo/tree/master/examples/terminal) examples with richer UI and RPC-based communication between the Web and Node in the [examples](https://github.com/GoogleChromeLabs/carlo/tree/master/examples) folder.\n\n<!-- [END getstarted] -->\n\n## API\n\nCheck out the [API](https://github.com/GoogleChromeLabs/carlo/blob/master/API.md) to get familiar with Carlo.\n\n\n## Testing\n\nCarlo uses [Puppeteer](https://pptr.dev/) project for testing. Carlo application and all Carlo windows have\ncorresponding Puppeteer objects exposed for testing. Please refer to the [API](https://github.com/GoogleChromeLabs/carlo/blob/master/API.md) and the [systeminfo](https://github.com/GoogleChromeLabs/carlo/tree/master/examples/systeminfo) project for more details.\n\n## Contributing to Carlo\n\nLook at the [contributing guide](https://github.com/GoogleChromeLabs/carlo/blob/master/CONTRIBUTING.md) to get an overview of Carlo's development.\n\n<!-- [START faq] -->\n\n## FAQ\n\n#### Q: What was the motivation behind this project when we already have Electron and NW.js? How does this project differ from these platforms, how does it achieve something that is not possible/harder with Electron or NW.js?\n\n- One of the motivations of this project is to demonstrate how browsers that are installed locally can be used with Node out of the box.\n- Node v8 and Chrome v8 engines are decoupled in Carlo, providing a maintainable model with the ability to independently update underlying components. Carlo gives the user control over bundling and is more about productivity than branding.\n\n#### Q: Can a Node app using Carlo be packaged as a Desktop app?\n\nThe [pkg](https://github.com/zeit/pkg) project can be used to package a Node app as a Desktop app. Carlo does not provide branding configurability such as application icons or customizable menus, instead, Carlo focuses on productivity and Web/Node interoperability. Check out the [systeminfo](https://github.com/GoogleChromeLabs/carlo/tree/master/examples/systeminfo) example and call `pkg package.json` to see how it works.\n\n#### Q: What happens if the user does not have Chrome installed?\n\nCarlo prints an error message when Chrome can not be located.\n\n#### Q: What is the minimum Chrome version that Carlo supports?\n\nChrome Stable channel, versions 70.* are supported.\n\n\n<!-- [END faq] -->\n"
  },
  {
    "path": "examples/photobooth/README.md",
    "content": "### Usage\n\n> This example requires Chrome 72 (Chrome Canary) to function.\n\nInstall dependencies\n\n```bash\nnpm i\n```\n\nRun application\n\n```bash\nnpm start\n```\n\nOptionally package as executable\n\n```bash\npkg package.json\n```\n"
  },
  {
    "path": "examples/photobooth/main.js",
    "content": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use strict';\n\nconst carlo = require('carlo');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\n\n(async () => {\n  let app;\n  try {\n    app = await carlo.launch(\n      {\n        bgcolor: '#e6e8ec',\n        width: 800,\n        height: 648 + 24,\n        icon: path.join(__dirname, '/app_icon.png'),\n        channel: ['canary', 'stable'],\n        localDataDir: path.join(os.homedir(), '.carlophotobooth'),\n      });\n  } catch(e) {\n    // New window is opened in the running instance.\n    console.log('Reusing the running instance');\n    return;\n  }\n  app.on('exit', () => process.exit());\n  // New windows are opened when this app is started again from command line.\n  app.on('window', window => window.load('index.html'));\n  app.serveFolder(path.join(__dirname, '/www'));\n  await app.exposeFunction('saveImage', saveImage);\n  await app.load('index.html');\n})();\n\nfunction saveImage(base64) {\n  var buffer = Buffer.from(base64, 'base64')\n  if (!fs.existsSync('pictures'))\n    fs.mkdirSync('pictures');\n  const fileName = path.join('pictures', new Date().toISOString().replace(/:/g,'-') + '.jpeg');\n  fs.writeFileSync(fileName, buffer);\n}\n"
  },
  {
    "path": "examples/photobooth/package.json",
    "content": "{\n  \"name\": \"photobooth-app\",\n  \"version\": \"0.9.0\",\n  \"description\": \"Photo Booth App\",\n  \"main\": \"main.js\",\n  \"scripts\": {\n    \"bundle\": \"pkg package.json\",\n    \"start\": \"node main.js\"\n  },\n  \"bin\": {\n    \"photobooth-app\": \"./main.js\"\n  },\n  \"pkg\": {\n    \"scripts\": \"*.js\",\n    \"assets\": \"www/**/*\"\n  },\n  \"keywords\": [],\n  \"author\": \"The Chromium Authors\",\n  \"license\": \"Apache-2.0\",\n  \"dependencies\": {\n    \"carlo\": \"^0.9.0\",\n    \"systeminformation\": \"^3.45.9\"\n  },\n  \"devDependencies\": {\n    \"pkg\": \"^4.3.4\"\n  }\n}\n"
  },
  {
    "path": "examples/photobooth/www/index.html",
    "content": "<!--\n  Copyright 2018 Google Inc. All rights reserved.\n\n  Licensed under the Apache License, Version 2.0 (the \"License\");\n  you may not use this file except in compliance with the License.\n  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n  Unless required by applicable law or agreed to in writing, software\n  distributed under the License is distributed on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  See the License for the specific language governing permissions and\n  limitations under the License.\n -->\n\n<link rel=\"shortcut icon\" href=\"favicon.ico\" sizes=\"256x256\" />\n<title>PhotoBooth App</title>\n<style>\nbody {\n  background-color: #e6e8ec;\n  display: flex;\n  flex-direction: column;\n  margin: 0;\n}\n\ncanvas {\n  display: none;\n}\n\n.buttons {\n  display: flex;\n  justify-content: center;\n  color: #aaa;\n  flex-basis: 48px;\n}\n\n#button {\n  cursor: hand;\n  background-color: black;\n  -webkit-mask-image: url(camera.svg);\n  transform: scale(2, 2);\n  width: 24px;\n  height: 24px;\n  margin-top: 12px;\n}\n\n#button:hover {\n  background: red;\n}\n\n#button:active {\n  opacity: 0.4;\n}\n\n#video {\n  flex: auto;\n  transform: scaleX(-1);\n}\n\n.flashit {\n  animation: flash linear 100ms;\n}\n\n@keyframes flash {\n  0% { opacity: 1; } \n  50% { opacity: .1; } \n  100% { opacity: 1; }\n}\n\n</style>\n\n<script>\nasync function run(){  \n  const video = document.getElementById('video');\n  const button = document.getElementById('button');\n  video.srcObject = await navigator.mediaDevices.getUserMedia({ video: true });\n  button.addEventListener('click', () => captureScreenshot(), false);\n}\n\nfunction captureScreenshot() {\n  const video = document.getElementById('video');\n  const canvas = document.getElementById('canvas');\n  canvas.width = video.videoWidth;\n  canvas.height = video.videoHeight;\n  const context = canvas.getContext('2d');\n  context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);\n  saveImage(canvas.toDataURL('image/jpeg').substr('data:image/jpeg;base64,'.length));\n  video.classList.add('flashit');\n  setTimeout(() => video.classList.remove('flashit'), 1000);\n}\n</script>\n\n<body onload=\"run()\" tabIndex=\"0\">\n  <video autoplay id=\"video\"></video>\n  <canvas id=\"canvas\"></canvas>\n  <div class=\"buttons\">\n    <div id=\"button\"/>\n  </div>\n</body>\n\n</html>\n"
  },
  {
    "path": "examples/systeminfo/.gitignore",
    "content": "node_modules\npackage-lock.json\n.profile\n.DS_Store\n.vscode\n.idea"
  },
  {
    "path": "examples/systeminfo/README.md",
    "content": "### Usage\n\nInstall dependencies\n\n```bash\nnpm i\n```\n\nRun application\n\n```bash\nnpm start\n```\n\nOptionally package as executable\n\n```bash\npkg package.json\n```\n"
  },
  {
    "path": "examples/systeminfo/app.js",
    "content": "/**\n * Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use strict';\n\nconst carlo = require('carlo');\nconst os = require('os');\nconst path = require('path');\nconst si = require('systeminformation');\n\nasync function run() {\n  let app;\n  try {\n    app = await carlo.launch(\n        {\n          bgcolor: '#2b2e3b',\n          title: 'Systeminfo App',\n          width: 1000,\n          height: 500,\n          channel: ['canary', 'stable'],\n          icon: path.join(__dirname, '/app_icon.png'),\n          args: process.env.DEV === 'true' ? ['--auto-open-devtools-for-tabs'] : [],\n          localDataDir: path.join(os.homedir(), '.carlosysteminfo'),\n        });\n  } catch(e) {\n    // New window is opened in the running instance.\n    console.log('Reusing the running instance');\n    return;\n  }\n  app.on('exit', () => process.exit());\n  // New windows are opened when this app is started again from command line.\n  app.on('window', window => window.load('index.html'));\n  app.serveFolder(path.join(__dirname, 'www'));\n  await app.exposeFunction('systeminfo', systeminfo);\n  await app.load('index.html');\n  return app;\n}\n\nasync function systeminfo() {\n  const info = {};\n  await Promise.all([\n    si.battery().then(r => info.battery = r),\n    si.cpu().then(r => info.cpu = r),\n    si.osInfo().then(r => info.osInfo = r),\n  ]);\n  return info;\n}\n\nmodule.exports = { run };\n"
  },
  {
    "path": "examples/systeminfo/main.js",
    "content": "/**\n * Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use strict';\n\nrequire('./app.js').run();\n"
  },
  {
    "path": "examples/systeminfo/package.json",
    "content": "{\n  \"name\": \"systeminfo-app\",\n  \"version\": \"0.9.0\",\n  \"description\": \"System info example\",\n  \"main\": \"main.js\",\n  \"scripts\": {\n    \"bundle\": \"pkg package.json\",\n    \"start\": \"node main.js\",\n    \"test\": \"node test.js\"\n  },\n  \"bin\": {\n    \"systeminfo-app\": \"./main.js\"\n  },\n  \"pkg\": {\n    \"scripts\": \"*.js\",\n    \"assets\": \"www/**/*\"\n  },\n  \"keywords\": [],\n  \"author\": \"The Chromium Authors\",\n  \"license\": \"Apache-2.0\",\n  \"dependencies\": {\n    \"carlo\": \"^0.9.0\",\n    \"systeminformation\": \"^3.45.9\"\n  },\n  \"devDependencies\": {\n    \"pkg\": \"^4.3.4\",\n    \"@pptr/testrunner\": \"^0.5.0\"\n  }\n}\n"
  },
  {
    "path": "examples/systeminfo/test.js",
    "content": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst {TestRunner, Reporter, Matchers} = require('@pptr/testrunner');\n\nrequire('carlo').enterTestMode();\nconst { run } = require('./app');\n\n// Runner holds and runs all the tests\nconst runner = new TestRunner({\n  parallel: 1, // run 2 parallel threads\n  timeout: 3000, // setup timeout of 1 second per test\n});\n// Simple expect-like matchers\nconst {expect} = new Matchers();\n\n// Extract jasmine-like DSL into the global namespace\nconst {describe, xdescribe, fdescribe} = runner;\nconst {it, fit, xit} = runner;\nconst {beforeAll, beforeEach, afterAll, afterEach} = runner;\n\ndescribe('test', () => {\n  it('test columns', async(state, test) => {\n    const app = await run();\n    const page = app.mainWindow().pageForTest();\n    await page.waitForSelector('.header');\n    const columns = await page.$$eval('.header', nodes => nodes.map(n => n.textContent));\n    expect(columns.sort().join(',')).toBe('battery,cpu,osInfo');\n  });\n});\n\n// Reporter subscribes to TestRunner events and displays information in terminal\nnew Reporter(runner);\n\n// Run all tests.\nrunner.run();\n"
  },
  {
    "path": "examples/systeminfo/www/index.html",
    "content": "<!--\n  Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.\n\n  Licensed under the Apache License, Version 2.0 (the \"License\");\n  you may not use this file except in compliance with the License.\n  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n  Unless required by applicable law or agreed to in writing, software\n  distributed under the License is distributed on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  See the License for the specific language governing permissions and\n  limitations under the License.\n -->\n\n<html>\n<link rel=\"shortcut icon\" href=\"favicon.ico\" sizes=\"256x256\" />\n<style>\nbody {\n  color: #ddd;\n  display: flex;\n  justify-content: center;\n  background-color: #2b2e3b;\n  opacity: 0;\n  transition: opacity 2s;\n  font-family: Roboto;\n  overflow: hidden;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  flex: auto;\n  justify-content: center;\n}\n\n.heading {\n  font-size: 36px;\n  text-align: center;\n  margin: 25px 0;\n}\n\n#grids {\n  margin-top: 30px;\n  color: #ddd;\n  display: grid;\n  grid-template-columns: 33% 33% 33%;\n  grid-gap: 40px;\n  margin: 25px;\n  overflow: hidden;\n}\n\n.grid-placeholder {\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n  padding: 10px;\n}\n\n.grid {\n  flex: auto;\n  display: grid;\n  grid-template-columns: 1fr 4fr;\n  grid-gap: 4px;\n}\n\n.blur {\n  /**filter: blur(7px);*/\n}\n\n.value {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.header {\n  font-weight: bold;\n  grid-column: span 2;\n  border-bottom: 1px solid #999;\n}\n\n/* roboto-regular - latin */\n@font-face {\n  font-family: 'Roboto';\n  font-style: normal;\n  font-weight: 400;\n  src: url('fonts/roboto-v18-latin-regular.woff2') format('woff2');\n}\n</style>\n\n<title>Systeminfo App</title>\n<script>\nasync function onload() {\n  const data = await systeminfo();\n  const grids = document.getElementById('grids');\n  const blur = new Set(['serial', 'uuid', 'sku', 'hostname']);\n  const keys = Object.keys(data).sort();\n  for (const type of keys) {\n    const info = data[type];\n    const placeholder = createChild(grids, 'div', 'grid-placeholder');\n    const grid = createChild(placeholder, 'div', 'grid');\n    createChild(grid, 'div', 'header').textContent = type;\n    const infos = Object.keys(info).sort();\n    for (const key of infos) {\n      if (typeof info[key] === 'object') continue;\n      createChild(grid, 'div').textContent = key;\n      const value = createChild(grid, 'div', 'value');\n      value.textContent = info[key];\n      if (blur.has(key))\n        value.classList.add('blur');\n    }\n  }\n  document.body.style.opacity = 1;\n}\n\nfunction createChild(parent, tag, className) {\n  const elem = document.createElement(tag);\n  if (className)\n    elem.className = className;\n  parent.appendChild(elem);\n  return elem;\n}\n\n</script>\n<body onload=\"onload()\">\n  <div class=\"content\">\n    <div class=\"heading\">Welcome to Carlo!</div>\n    <div id=\"grids\"></div>\n  </div>\n</body>\n\n</html>\n"
  },
  {
    "path": "examples/terminal/.gitignore",
    "content": "node_modules\npackage-lock.json\n.profile\n.DS_Store\n.vscode\n.idea"
  },
  {
    "path": "examples/terminal/README.md",
    "content": "### Usage\n\nInstall dependencies\n\n```bash\nnpm i\n```\n\nRun application\n\n```bash\nnpm start\n```\n"
  },
  {
    "path": "examples/terminal/main.js",
    "content": "#!/usr/bin/env node\n\n/**\n * Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use strict';\n\nconst carlo = require('carlo');\nconst path = require('path');\nconst { rpc, rpc_process } = require('carlo/rpc');\n\nclass TerminalApp {\n  constructor() {\n    this.lastTop_ = 50;\n    this.lastLeft_ = 50;\n    this.launch_();\n    this.handle_ = rpc.handle(this);\n  }\n\n  async launch_() {\n    try {\n      this.app_ = await carlo.launch({\n        bgcolor: '#2b2e3b',\n        title: 'Terminal App',\n        width: 800,\n        height: 800,\n        channel: ['canary', 'stable'],\n        icon: path.join(__dirname, '/app_icon.png'),\n        top: this.lastTop_,\n        left: this.lastLeft_ });\n    } catch (e) {\n      console.log('Reusing the running instance');\n      return;\n    }\n    this.app_.on('exit', () => process.exit());\n    this.app_.serveFolder(path.join(__dirname, 'www'));\n    this.app_.serveFolder(path.join(__dirname, 'node_modules'), 'node_modules');\n    this.app_.on('window', win => this.initUI_(win));\n    this.initUI_(this.app_.mainWindow());\n  }\n\n  async newWindow() {\n    this.lastTop_ = (this.lastTop_ + 50) % 200;\n    this.lastLeft_ += 50;\n    const options = { top: this.lastTop_, left: this.lastLeft_ };\n    this.app_.createWindow(options);\n  }\n\n  async initUI_(win) {\n    const term = await rpc_process.spawn('worker.js');\n    win.load('index.html', this.handle_, term);\n  }\n}\n\nnew TerminalApp();\n"
  },
  {
    "path": "examples/terminal/package.json",
    "content": "{\n  \"name\": \"xterm-app\",\n  \"version\": \"0.9.0\",\n  \"description\": \"Terminal example\",\n  \"main\": \"main.js\",\n  \"scripts\": {\n    \"start\": \"node main.js\"\n  },\n  \"bin\": {\n    \"xterm-app\": \"./main.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"The Chromium Authors\",\n  \"license\": \"Apache-2.0\",\n  \"dependencies\": {\n    \"carlo\": \"^0.9.0\",\n    \"ndb-node-pty-prebuilt\": \"^0.8.0\",\n    \"xterm\": \"~3.8.1\"\n  }\n}\n"
  },
  {
    "path": "examples/terminal/worker.js",
    "content": "#!/usr/bin/env node\n\n/**\n * Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use strict';\n\nconst EventEmitter = require('events');\nconst os = require('os');\nconst pty = require('ndb-node-pty-prebuilt');\nconst { rpc, rpc_process } = require('carlo/rpc');\n\nclass Terminal extends EventEmitter {\n  constructor() {\n    super();\n    const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';\n    this.term_ = pty.spawn(shell, [], {\n      name: 'xterm-color',\n      cwd: process.env.PWD,\n      env: process.env\n    });\n    this.term_.on('data', data => this.emit('data', data));\n  }\n\n  on(event, func) {\n    // EventEmitter returns heavy object that we don't want to\n    // send over the wire.\n    super.on(event, func);\n  }\n\n  resize(cols, rows) {\n    this.term_.resize(cols, rows);\n  }\n\n  write(data) {\n    this.term_.write(data);\n  }\n\n  dispose() {\n    process.kill(this._term.pid);\n  }\n}\n\nrpc_process.init(() => rpc.handle(new Terminal));\n"
  },
  {
    "path": "examples/terminal/www/index.html",
    "content": "<!--\n  Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.\n\n  Licensed under the Apache License, Version 2.0 (the \"License\");\n  you may not use this file except in compliance with the License.\n  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n  Unless required by applicable law or agreed to in writing, software\n  distributed under the License is distributed on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  See the License for the specific language governing permissions and\n  limitations under the License.\n -->\n\n<html>\n<link rel=\"shortcut icon\" href=\"favicon.ico\" sizes=\"256x256\" />\n<link rel=\"stylesheet\" href=\"node_modules/xterm/dist/xterm.css\" />\n<title>Terminal App</title>\n<style>\nbody, #terminal {\n  display: flex;\n  flex: auto;\n}\n</style>\n<script src=\"node_modules/xterm/dist/xterm.js\"></script> \n<script src=\"node_modules/xterm/dist/addons/fit/fit.js\"></script> \n\n<script>\nTerminal.applyAddon(fit);\n\nasync function run() {\n  const [app, term] = await carlo.loadParams();\n  // Create ui control.\n  const termUI = new Terminal({cursorBlink: true});\n  termUI.open(document.getElementById('terminal'));\n  window.onresize = () => termUI.fit();\n\n  // Wire them together.\n  termUI.on('data', data => term.write(data));\n  term.on('data', rpc.handle(data => termUI.write(data)));\n  termUI.on('resize', size => term.resize(size.cols, size.rows));\n\n  // Init.\n  termUI.fit();\n  termUI.focus();\n  document.addEventListener('keydown', event => {\n    if (event.keyCode === 78 && (event.metaKey || event.ctrlKey)) {  // Ctrl+N\n      app.newWindow();\n      event.preventDefault();\n    }\n  });\n}\n</script> \n\n<body onload=\"run()\">\n  <div id=\"terminal\"></div>\n</body>\n</html>\n"
  },
  {
    "path": "examples/windows/README.md",
    "content": "### Usage\n\nInstall dependencies\n\n```bash\nnpm i\n```\n\nRun application\n\n```bash\nnpm start\n```\n"
  },
  {
    "path": "examples/windows/main.html",
    "content": "<!--\n  Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.\n\n  Licensed under the Apache License, Version 2.0 (the \"License\");\n  you may not use this file except in compliance with the License.\n  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n  Unless required by applicable law or agreed to in writing, software\n  distributed under the License is distributed on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  See the License for the specific language governing permissions and\n  limitations under the License.\n -->\n\n<head>\n<title>Main</title>\n<script>\nasync function run() {\n  const [backend] = await carlo.loadParams();\n  const alexaTop5 = [\n      'https://google.com', 'https://youtube.com',\n      'https://facebook.com', 'https://baidu.com',\n      'https://wikipedia.org'];\n  for (const url of alexaTop5) {\n    const button = document.createElement('button');\n    button.textContent = url;\n    button.onclick = () => backend.showMyWindow(url);\n    document.body.appendChild(button);\n    document.body.appendChild(document.createElement('br'));\n  }\n}\n</script>\n</head>\n<body onload=\"run()\"></body>\n"
  },
  {
    "path": "examples/windows/main.js",
    "content": "#!/usr/bin/env node\n\n/**\n * Copyright 2018 Google Inc., PhantomJS Authors All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use strict';\n\nconst carlo = require('carlo');\nconst { rpc } = require('carlo/rpc');\n\nclass Backend {\n  constructor(app) {\n    this.app_ = app;\n    this.windows_ = new Map();\n  }\n\n  showMyWindow(url) {\n    let windowPromise = this.windows_.get(url);\n    if (!windowPromise) {\n      windowPromise = this.createWindow_(url);\n      this.windows_.set(url, windowPromise);\n    }\n    windowPromise.then(w => w.bringToFront());\n  }\n\n  async createWindow_(url) {\n    const window = await this.app_.createWindow({width: 800, height: 600, top: 200, left: 10});\n    window.on('close', () => this.windows_.delete(url));\n    window.load(url);\n    return window;\n  }\n}\n\n(async() => {\n  const app = await carlo.launch(\n    {title: 'Main', width: 300, height: 100, top: 10, left: 10 });\n  app.on('exit', () => process.exit());\n  const mainWindow = app.mainWindow();\n  mainWindow.on('close', () => process.exit());\n  mainWindow.serveFolder(__dirname);\n  mainWindow.load('main.html', rpc.handle(new Backend(app)));\n})();\n"
  },
  {
    "path": "examples/windows/package.json",
    "content": "{\n  \"name\": \"windows-app\",\n  \"version\": \"0.9.0\",\n  \"description\": \"Multiple windows example\",\n  \"main\": \"main.js\",\n  \"scripts\": {\n    \"start\": \"node main.js\"\n  },\n  \"bin\": {\n    \"windows-app\": \"./main.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"The Chromium Authors\",\n  \"license\": \"Apache-2.0\",\n  \"dependencies\": {\n    \"carlo\": \"^0.9.0\"\n  }\n}\n"
  },
  {
    "path": "index.js",
    "content": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use strict';\n\nmodule.exports = require('./lib/carlo');\n"
  },
  {
    "path": "lib/carlo.js",
    "content": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use strict';\n\nconst path = require('path');\nconst puppeteer = require('puppeteer-core');\nconst findChrome = require('./find_chrome');\nconst {rpc} = require('../rpc');\nconst debugApp = require('debug')('carlo:app');\nconst debugServer = require('debug')('carlo:server');\nconst {Color} = require('./color');\nconst {HttpRequest} = require('./http_request');\n\nconst fs = require('fs');\nconst util = require('util');\nconst {URL} = require('url');\nconst EventEmitter = require('events');\nconst fsReadFile = util.promisify(fs.readFile);\n\nlet testMode = false;\n\nclass App extends EventEmitter {\n  /**\n   * @param {!Puppeteer.Browser} browser Puppeteer browser\n   * @param {!Object} options\n   */\n  constructor(browser, options) {\n    super();\n    this.browser_ = browser;\n    this.options_ = options;\n    this.windows_ = new Map();\n    this.exposedFunctions_ = [];\n    this.pendingWindows_ = new Map();\n    this.windowSeq_ = 0;\n    this.www_ = [];\n  }\n\n  async init_() {\n    debugApp('Configuring browser');\n    let page;\n    await Promise.all([\n      this.browser_.target().createCDPSession().then(session => {\n        this.session_ = session;\n        if (this.options_.icon)\n          this.setIcon(this.options_.icon);\n      }),\n      this.browser_.defaultBrowserContext().\n          overridePermissions('https://domain', [\n            'geolocation',\n            'midi',\n            'notifications',\n            'camera',\n            'microphone',\n            'clipboard-read',\n            'clipboard-write']),\n      this.browser_.pages().then(pages => page = pages[0])\n    ]);\n\n    this.browser_.on('targetcreated', this.targetCreated_.bind(this));\n\n    // Simulate the pageCreated sequence.\n    let callback;\n    const result = new Promise(f => callback = f);\n    this.pendingWindows_.set('', { options: this.options_, callback });\n    this.pageCreated_(page);\n    return result;\n  }\n\n  /**\n   * Close the app windows.\n   */\n  async exit() {\n    debugApp('app.exit...');\n    if (this.exited_)\n      return;\n    this.exited_ = true;\n    await this.browser_.close();\n    this.emit(App.Events.Exit);\n  }\n\n  /**\n   * @return {!<Window>} main window.\n   */\n  mainWindow() {\n    for (const window of this.windows_.values())\n      return window;\n  }\n\n  /**\n   * @param {!Object=} options\n   * @return {!Promise<Window>}\n   */\n  async createWindow(options = {}) {\n    options = Object.assign({}, this.options_, options);\n    const seq = String(++this.windowSeq_);\n    if (!this.windows_.size)\n      throw new Error('Needs at least one window to create more.');\n\n    const params = [];\n    for (const prop of ['top', 'left', 'width', 'height']) {\n      if (typeof options[prop] === 'number')\n        params.push(`${prop}=${options[prop]}`);\n    }\n\n    for (const page of this.windows_.keys()) {\n      page.evaluate(`window.open('about:blank?seq=${seq}', '', '${params.join(',')}')`);\n      break;\n    }\n\n    return new Promise(callback => {\n      this.pendingWindows_.set(seq, { options, callback });\n    });\n  }\n\n  /**\n   * @return {!Array<!Window>}\n   */\n  windows() {\n    return Array.from(this.windows_.values());\n  }\n\n  /**\n   * @param {string} name\n   * @param {function} func\n   * @return {!Promise}\n   */\n  exposeFunction(name, func) {\n    this.exposedFunctions_.push({name, func});\n    return Promise.all(this.windows().map(window => window.exposeFunction(name, func)));\n  }\n\n  /**\n   * @param {function()|string} pageFunction\n   * @param {!Array<*>} args\n   * @return {!Promise<*>}\n   */\n  evaluate(pageFunction, ...args) {\n    return this.mainWindow().evaluate(pageFunction, ...args);\n  }\n\n  /**\n   * @param {string=} folder Folder with the web content.\n   * @param {string=} prefix Only serve folder for requests with given prefix.\n   */\n  serveFolder(folder = '', prefix = '') {\n    this.www_.push({folder, prefix: wrapPrefix(prefix)});\n  }\n\n  /**\n   * Serves pages from given origin, eg `http://localhost:8080`.\n   * This can be used for the fast development mode available in web frameworks.\n   *\n   * @param {string} base\n   * @param {string=} prefix Only serve folder for requests with given prefix.\n   */\n  serveOrigin(base, prefix = '') {\n    this.www_.push({baseURL: new URL(base + '/'), prefix: wrapPrefix(prefix)});\n  }\n\n  /**\n   * Calls given handler for each request and allows called to handle it.\n   *\n   * @param {function(!Request)} handler to be used for each request.\n   */\n  serveHandler(handler) {\n    this.httpHandler_ = handler;\n  }\n\n  /**\n   * @param {string=} uri\n   * @param {...*} params\n   * @return {!Promise<*>}\n   */\n  async load(uri = '', ...params) {\n    return this.mainWindow().load(uri, ...params);\n  }\n\n  /**\n   * Set the application icon shown in the OS dock / task swicher.\n   * @param {string|!Buffer} dockIcon\n   */\n  async setIcon(icon) {\n    const buffer = typeof icon === 'string' ? await fsReadFile(icon) : icon;\n    this.session_.send('Browser.setDockTile',\n        { image: buffer.toString('base64') }).catch(e => {});\n  }\n\n  /**\n   * Puppeteer browser object for test.\n   * @return {!Puppeteer.Browser}\n   */\n  browserForTest() {\n    return this.browser_;\n  }\n\n  async targetCreated_(target) {\n    const page = await target.page();\n    if (!page)\n      return;\n    this.pageCreated_(page);\n  }\n\n  /**\n   * @param {!Puppeteer.Page} page\n   */\n  async pageCreated_(page) {\n    const url = page.url();\n    debugApp('Page created at', url);\n    const seq = url.startsWith('about:blank?seq=') ? url.substr('about:blank?seq='.length) : '';\n    const params = this.pendingWindows_.get(seq);\n    const { callback, options } = params || { options: this.options_ };\n    this.pendingWindows_.delete(seq);\n    const window = new Window(this, page, options);\n    await window.init_();\n    this.windows_.set(page, window);\n    if (callback)\n      callback(window);\n    this.emit(App.Events.Window, window);\n  }\n\n  /**\n   * @param {!Window}\n   */\n  windowClosed_(window) {\n    debugApp('window closed', window.loadURI_);\n    this.windows_.delete(window.page_);\n    if (!this.windows_.size)\n      this.exit();\n  }\n}\n\nApp.Events = {\n  Exit: 'exit',\n  Window: 'window'\n};\n\nclass Window extends EventEmitter {\n  /**\n   * @param {!App} app\n   * @param {!Puppeteer.Page} page Puppeteer page\n   * @param {!Object} options\n   */\n  constructor(app, page, options) {\n    super();\n    this.app_ = app;\n    this.options_ = Object.assign({}, app.options_, options);\n    this.www_ = [];\n    this.page_ = page;\n    this.page_.on('close', this.closed_.bind(this));\n    this.page_.on('domcontentloaded', this.domContentLoaded_.bind(this));\n    this.hostHandle_ = rpc.handle(new HostWindow(this));\n  }\n\n  async init_() {\n    debugApp('Configuring window');\n    const targetId = this.page_.target()._targetInfo.targetId;\n    const bgcolor = Color.parse(this.options_.bgcolor);\n    const bgcolorRGBA = bgcolor.canonicalRGBA();\n    this.session_ = await this.page_.target().createCDPSession();\n\n    await Promise.all([\n      this.session_.send('Runtime.evaluate', { expression: 'self.paramsForReuse', returnByValue: true }).\n        then(response => { this.paramsForReuse_ = response.result.value; }),\n      this.session_.send('Emulation.setDefaultBackgroundColorOverride',\n          {color: {r: bgcolorRGBA[0], g: bgcolorRGBA[1],\n            b: bgcolorRGBA[2], a: bgcolorRGBA[3] * 255}}),\n      this.app_.session_.send('Browser.getWindowForTarget', { targetId })\n          .then(this.initBounds_.bind(this)),\n      this.configureRpcOnce_(),\n      ...this.app_.exposedFunctions_.map(({name, func}) => this.exposeFunction(name, func))\n    ]);\n  }\n\n  /**\n   * @param {string} name\n   * @param {function} func\n   * @return {!Promise}\n   */\n  exposeFunction(name, func) {\n    debugApp('Exposing function', name);\n    return this.page_.exposeFunction(name, func);\n  }\n\n  /**\n   * @param {function()|string} pageFunction\n   * @param {!Array<*>} args\n   * @return {!Promise<*>}\n   */\n  evaluate(pageFunction, ...args) {\n    return this.page_.evaluate(pageFunction, ...args);\n  }\n\n  /**\n   * @param {string=} www Folder with the web content.\n   * @param {string=} prefix Only serve folder for requests with given prefix.\n   */\n  serveFolder(folder = '', prefix = '') {\n    this.www_.push({folder, prefix: wrapPrefix(prefix)});\n  }\n\n  /**\n   * Serves pages from given origin, eg `http://localhost:8080`.\n   * This can be used for the fast development mode available in web frameworks.\n   *\n   * @param {string} base\n   * @param {string=} prefix Only serve folder for requests with given prefix.\n   */\n  serveOrigin(base, prefix = '') {\n    this.www_.push({baseURL: new URL(base + '/'), prefix: wrapPrefix(prefix)});\n  }\n\n  /**\n   * Calls given handler for each request and allows called to handle it.\n   *\n   * @param {function(!Request)} handler to be used for each request.\n   */\n  serveHandler(handler) {\n    this.httpHandler_ = handler;\n  }\n\n  /**\n   * @param {string=} uri\n   * @param {...*} params\n   * @return {!Promise}\n   */\n  async load(uri = '', ...params) {\n    debugApp('Load page', uri);\n    this.loadURI_ = uri;\n    this.loadParams_ = params;\n    await this.initializeInterception_();\n    debugApp('Navigating the page to', this.loadURI_);\n\n    const result = new Promise(f => this.domContentLoadedCallback_ = f);\n    // Await here to process exceptions.\n    await this.page_.goto(new URL(this.loadURI_, 'https://domain/').toString(), {timeout: 0, waitFor: 'domcontentloaded'});\n    // Available in Chrome M73+.\n    this.session_.send('Page.resetNavigationHistory').catch(e => {});\n    // Make sure domContentLoaded callback is processed before we return.\n    // That indirection is here to handle debug-related reloads we did not call for.\n    return result;\n  }\n\n  initBounds_(result) {\n    this.windowId_ = result.windowId;\n    return this.setBounds({ top: this.options_.top,\n      left: this.options_.left,\n      width: this.options_.width,\n      height: this.options_.height });\n  }\n\n  /**\n   * Puppeteer page object for test.\n   * @return {!Puppeteer.Page}\n   */\n  pageForTest() {\n    return this.page_;\n  }\n\n  /**\n   * Returns value specified in the carlo.launch(options.paramsForReuse). This is handy\n   * when Carlo is reused across app runs. First Carlo app successfully starts the browser.\n   * Second carlo attempts to start the browser, but browser profile is already in use.\n   * Yet, new window is being opened in the first Carlo app. This new window returns\n   * options.paramsForReuse passed into the second Carlo. This was single app knows what to\n   * do with the additional windows.\n   *\n   * @return {*}\n   */\n  paramsForReuse() {\n    return this.paramsForReuse_;\n  }\n\n  async configureRpcOnce_() {\n    await this.page_.exposeFunction('receivedFromChild', data => this.receivedFromChild_(data));\n\n    const rpcFile = (await fsReadFile(__dirname + '/../rpc/rpc.js')).toString();\n    const features = [ require('./features/shortcuts.js'),\n                       require('./features/file_info.js') ];\n\n    await this.page_.evaluateOnNewDocument((rpcFile, features) => {\n      const module = { exports: {} };\n      eval(rpcFile);\n      self.rpc = module.exports;\n      self.carlo = {};\n      let argvCallback;\n      const argvPromise = new Promise(f => argvCallback = f);\n      self.carlo.loadParams = () => argvPromise;\n\n      function transport(receivedFromParent) {\n        self.receivedFromParent = receivedFromParent;\n        return receivedFromChild;\n      }\n\n      self.rpc.initWorld(transport, async(loadParams, win) => {\n        argvCallback(loadParams);\n\n        if (document.readyState === 'loading')\n          await new Promise(f => document.addEventListener('DOMContentLoaded', f));\n\n        for (const feature of features)\n          eval(`(${feature})`)(win);\n      });\n    }, rpcFile, features.map(f => f.toString()));\n  }\n\n  async domContentLoaded_() {\n    debugApp('Creating rpc world for page...');\n    const transport = receivedFromChild => {\n      this.receivedFromChild_ = receivedFromChild;\n      return data => {\n        const json = JSON.stringify(data);\n        if (this.session_._connection)\n          this.session_.send('Runtime.evaluate', {expression: `self.receivedFromParent(${json})`});\n      };\n    };\n    if (this._lastWebWorldId)\n      rpc.disposeWorld(this._lastWebWorldId);\n    const { worldId } = await rpc.createWorld(transport, this.loadParams_, this.hostHandle_);\n    debugApp('World created', worldId);\n    this._lastWebWorldId = worldId;\n\n    this.domContentLoadedCallback_();\n  }\n\n  async initializeInterception_() {\n    debugApp('Initializing network interception...');\n    if (this.interceptionInitialized_)\n      return;\n    if (this.www_.length + this.app_.www_.length === 0 && !this.httpHandler_ && !this.app_.httpHandler_)\n      return;\n    this.interceptionInitialized_ = true;\n    this.session_.on('Network.requestIntercepted', this.requestIntercepted_.bind(this));\n    return this.session_.send('Network.setRequestInterception', {patterns: [{urlPattern: '*'}]});\n  }\n\n  /**\n   * @param {!Object} request Intercepted request.\n   */\n  async requestIntercepted_(payload) {\n    debugServer('intercepted:', payload.request.url);\n    const handlers = [];\n    if (this.httpHandler_)\n      handlers.push(this.httpHandler_);\n    if (this.app_.httpHandler_)\n      handlers.push(this.app_.httpHandler_);\n    handlers.push(this.handleRequest_.bind(this));\n    new HttpRequest(this.session_, payload, handlers);\n  }\n\n  /**\n   * @param {!HttpRequest} request Intercepted request.\n   */\n  async handleRequest_(request) {\n    const url = new URL(request.url());\n    debugServer('request url:', url.toString());\n\n    if (url.hostname !== 'domain') {\n      request.deferToBrowser();\n      return;\n    }\n\n    const urlpathname = url.pathname;\n    for (const {prefix, folder, baseURL} of this.app_.www_.concat(this.www_)) {\n      debugServer('prefix:', prefix);\n      if (!urlpathname.startsWith(prefix))\n        continue;\n\n      const pathname = urlpathname.substr(prefix.length);\n      debugServer('pathname:', pathname);\n      if (baseURL) {\n        request.deferToBrowser({ url: String(new URL(pathname, baseURL)) });\n        return;\n      }\n      const fileName = path.join(folder, pathname);\n      if (!fs.existsSync(fileName))\n        continue;\n\n      const headers = { 'content-type': contentType(request, fileName) };\n      const body = await fsReadFile(fileName);\n      request.fulfill({ headers, body});\n      return;\n    }\n    request.deferToBrowser();\n  }\n\n  /**\n   * @return {{left: number, top: number, width: number, height: number}}\n   */\n  async bounds() {\n    const { bounds } = await this.app_.session_.send('Browser.getWindowBounds', { windowId: this.windowId_ });\n    return { left: bounds.left, top: bounds.top, width: bounds.width, height: bounds.height };\n  }\n\n  /**\n   * @param {{left: (number|undefined), top: (number|undefined), width: (number|undefined), height: (number|undefined)}} bounds\n   */\n  async setBounds(bounds) {\n    await this.app_.session_.send('Browser.setWindowBounds', { windowId: this.windowId_, bounds });\n  }\n\n  async fullscreen() {\n    const bounds = { windowState: 'fullscreen' };\n    await this.app_.session_.send('Browser.setWindowBounds', { windowId: this.windowId_, bounds });\n  }\n\n  async minimize() {\n    const bounds = { windowState: 'minimized' };\n    await this.app_.session_.send('Browser.setWindowBounds', { windowId: this.windowId_, bounds });\n  }\n\n  async maximize() {\n    const bounds = { windowState: 'maximized' };\n    await this.app_.session_.send('Browser.setWindowBounds', { windowId: this.windowId_, bounds });\n  }\n\n  bringToFront() {\n    return this.page_.bringToFront();\n  }\n\n  close() {\n    return this.page_.close();\n  }\n\n  closed_() {\n    rpc.dispose(this.hostHandle_);\n    this.app_.windowClosed_(this);\n    this.emit(Window.Events.Close);\n  }\n\n  /**\n   * @return {boolean}\n   */\n  isClosed() {\n    return this.page_.isClosed();\n  }\n}\n\nWindow.Events = {\n  Close: 'close',\n};\n\nconst imageContentTypes = new Map([\n  ['jpeg', 'image/jpeg'], ['jpg', 'image/jpeg'], ['svg', 'image/svg+xml'], ['gif', 'image/gif'], ['webp', 'image/webp'],\n  ['png', 'image/png'], ['ico', 'image/ico'], ['tiff', 'image/tiff'], ['tif', 'image/tiff'], ['bmp', 'image/bmp']\n]);\n\nconst fontContentTypes = new Map([\n  ['ttf', 'font/opentype'], ['otf', 'font/opentype'], ['ttc', 'font/opentype'], ['woff', 'application/font-woff']\n]);\n\n/**\n * @param {!HttpRequest} request\n * @param {!string} fileName\n */\nfunction contentType(request, fileName) {\n  const dotIndex = fileName.lastIndexOf('.');\n  const extension = fileName.substr(dotIndex + 1);\n  switch (request.resourceType()) {\n    case 'Document': return 'text/html';\n    case 'Script': return 'text/javascript';\n    case 'Stylesheet': return 'text/css';\n    case 'Image':\n      return imageContentTypes.get(extension) || 'image/png';\n    case 'Font':\n      return fontContentTypes.get(extension) || 'application/font-woff';\n  }\n}\n\n/**\n * @param {!Object=} options\n * @return {!App}\n */\nasync function launch(options = {}) {\n  debugApp('Launching Carlo', options);\n  options = Object.assign(options);\n  if (!options.bgcolor)\n    options.bgcolor = '#ffffff';\n  options.localDataDir = options.localDataDir || path.join(__dirname, '.local-data');\n\n  const { executablePath, type } = await findChrome(options);\n  if (!executablePath) {\n    console.error('Could not find Chrome installation, please make sure Chrome browser is installed from https://www.google.com/chrome/.');\n    process.exit(0);\n    return;\n  }\n\n  const targetPage = `\n    <title>${encodeURIComponent(options.title || '')}</title>\n    <style>html{background:${encodeURIComponent(options.bgcolor)};}</style>\n    <script>self.paramsForReuse = ${JSON.stringify(options.paramsForReuse || undefined)};</script>`;\n\n  const args = [\n    `--app=data:text/html,${targetPage}`,\n    `--enable-features=NetworkService,NetworkServiceInProcess`,\n  ];\n\n  if (options.args)\n    args.push(...options.args);\n  if (typeof options.width === 'number' && typeof options.height === 'number')\n    args.push(`--window-size=${options.width},${options.height}`);\n  if (typeof options.left === 'number' && typeof options.top === 'number')\n    args.push(`--window-position=${options.left},${options.top}`);\n\n  try {\n    const browser = await puppeteer.launch({\n      executablePath,\n      pipe: true,\n      defaultViewport: null,\n      headless: testMode,\n      userDataDir: options.userDataDir || path.join(options.localDataDir, `profile-${type}`),\n      args });\n    const app = new App(browser, options);\n    await app.init_();\n    return app;\n  } catch (e) {\n    if (e.toString().includes('Target closed'))\n      throw new Error('Could not start the browser or the browser was already running with the given profile.');\n    else\n      throw e;\n  }\n}\n\nclass HostWindow {\n  /**\n   * @param {!Window} win\n   */\n  constructor(win) {\n    this.window_ = win;\n  }\n\n  closeBrowser() {\n    // Allow rpc response to land.\n    setTimeout(() => this.window_.app_.exit(), 0);\n  }\n\n  async fileInfo(expression) {\n    const { result } = await this.window_.session_.send('Runtime.evaluate', { expression });\n    return this.window_.session_.send('DOM.getFileInfo', { objectId: result.objectId });\n  }\n}\n\nfunction enterTestMode() {\n  testMode = true;\n}\n\nfunction wrapPrefix(prefix) {\n  if (!prefix.startsWith('/')) prefix = '/' + prefix;\n  if (!prefix.endsWith('/')) prefix += '/';\n  return prefix;\n}\n\nmodule.exports = { launch, enterTestMode };\n"
  },
  {
    "path": "lib/color.js",
    "content": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use strict';\n\nclass Color {\n  /**\n   * @param {!Array.<number>} rgba\n   * @param {!Color.Format} format\n   * @param {string=} originalText\n   */\n  constructor(rgba, format, originalText) {\n    this._rgba = rgba;\n    this._originalText = originalText || null;\n    this._originalTextIsValid = !!this._originalText;\n    this._format = format;\n    if (typeof this._rgba[3] === 'undefined')\n      this._rgba[3] = 1;\n\n    for (let i = 0; i < 4; ++i) {\n      if (this._rgba[i] < 0) {\n        this._rgba[i] = 0;\n        this._originalTextIsValid = false;\n      }\n      if (this._rgba[i] > 1) {\n        this._rgba[i] = 1;\n        this._originalTextIsValid = false;\n      }\n    }\n  }\n\n  /**\n   * @param {string} text\n   * @return {?Color}\n   */\n  static parse(text) {\n    const value = text.toLowerCase().replace(/\\s+/g, '');\n    const simple = /^(?:#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8}))$/i;\n    let match = value.match(simple);\n    if (match) {\n      if (match[1]) {  // hex\n        let hex = match[1].toLowerCase();\n        let format;\n        if (hex.length === 3) {\n          format = Color.Format.ShortHEX;\n          hex = hex.charAt(0) + hex.charAt(0) + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2);\n        } else if (hex.length === 4) {\n          format = Color.Format.ShortHEXA;\n          hex = hex.charAt(0) + hex.charAt(0) + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2) +\n              hex.charAt(3) + hex.charAt(3);\n        } else if (hex.length === 6) {\n          format = Color.Format.HEX;\n        } else {\n          format = Color.Format.HEXA;\n        }\n        const r = parseInt(hex.substring(0, 2), 16);\n        const g = parseInt(hex.substring(2, 4), 16);\n        const b = parseInt(hex.substring(4, 6), 16);\n        let a = 1;\n        if (hex.length === 8)\n          a = parseInt(hex.substring(6, 8), 16) / 255;\n        return new Color([r / 255, g / 255, b / 255, a], format, text);\n      }\n\n      return null;\n    }\n\n    // rgb/rgba(), hsl/hsla()\n    match = text.toLowerCase().match(/^\\s*(?:(rgba?)|(hsla?))\\((.*)\\)\\s*$/);\n\n    if (match) {\n      const components = match[3].trim();\n      let values = components.split(/\\s*,\\s*/);\n      if (values.length === 1) {\n        values = components.split(/\\s+/);\n        if (values[3] === '/') {\n          values.splice(3, 1);\n          if (values.length !== 4)\n            return null;\n        } else if ((values.length > 2 && values[2].indexOf('/') !== -1) || (values.length > 3 && values[3].indexOf('/') !== -1)) {\n          const alpha = values.slice(2, 4).join('');\n          values = values.slice(0, 2).concat(alpha.split(/\\//)).concat(values.slice(4));\n        } else if (values.length >= 4) {\n          return null;\n        }\n      }\n      if (values.length !== 3 && values.length !== 4 || values.indexOf('') > -1)\n        return null;\n      const hasAlpha = (values[3] !== undefined);\n\n      if (match[1]) {  // rgb/rgba\n        const rgba = [\n          Color._parseRgbNumeric(values[0]), Color._parseRgbNumeric(values[1]),\n          Color._parseRgbNumeric(values[2]), hasAlpha ? Color._parseAlphaNumeric(values[3]) : 1\n        ];\n        if (rgba.indexOf(null) > -1)\n          return null;\n        return new Color(rgba, hasAlpha ? Color.Format.RGBA : Color.Format.RGB, text);\n      }\n\n      if (match[2]) {  // hsl/hsla\n        const hsla = [\n          Color._parseHueNumeric(values[0]), Color._parseSatLightNumeric(values[1]),\n          Color._parseSatLightNumeric(values[2]), hasAlpha ? Color._parseAlphaNumeric(values[3]) : 1\n        ];\n        if (hsla.indexOf(null) > -1)\n          return null;\n        const rgba = [];\n        Color.hsl2rgb(hsla, rgba);\n        return new Color(rgba, hasAlpha ? Color.Format.HSLA : Color.Format.HSL, text);\n      }\n    }\n\n    return null;\n  }\n\n  /**\n   * @param {string} value\n   * return {number}\n   */\n  static _parsePercentOrNumber(value) {\n    if (isNaN(value.replace('%', '')))\n      return null;\n    const parsed = parseFloat(value);\n\n    if (value.indexOf('%') !== -1) {\n      if (value.indexOf('%') !== value.length - 1)\n        return null;\n      return parsed / 100;\n    }\n    return parsed;\n  }\n\n  /**\n   * @param {string} value\n   * return {number}\n   */\n  static _parseRgbNumeric(value) {\n    const parsed = Color._parsePercentOrNumber(value);\n    if (parsed === null)\n      return null;\n\n    if (value.indexOf('%') !== -1)\n      return parsed;\n    return parsed / 255;\n  }\n\n  /**\n   * @param {string} value\n   * return {number}\n   */\n  static _parseHueNumeric(value) {\n    const angle = value.replace(/(deg|g?rad|turn)$/, '');\n    if (isNaN(angle) || value.match(/\\s+(deg|g?rad|turn)/))\n      return null;\n    const number = parseFloat(angle);\n\n    if (value.indexOf('turn') !== -1)\n      return number % 1;\n    else if (value.indexOf('grad') !== -1)\n      return (number / 400) % 1;\n    else if (value.indexOf('rad') !== -1)\n      return (number / (2 * Math.PI)) % 1;\n    return (number / 360) % 1;\n  }\n\n  /**\n   * @param {string} value\n   * return {number}\n   */\n  static _parseSatLightNumeric(value) {\n    if (value.indexOf('%') !== value.length - 1 || isNaN(value.replace('%', '')))\n      return null;\n    const parsed = parseFloat(value);\n    return Math.min(1, parsed / 100);\n  }\n\n  /**\n   * @param {string} value\n   * return {number}\n   */\n  static _parseAlphaNumeric(value) {\n    return Color._parsePercentOrNumber(value);\n  }\n\n  /**\n   * @param {!Array.<number>} hsl\n   * @param {!Array.<number>} out_rgb\n   */\n  static hsl2rgb(hsl, out_rgb) {\n    const h = hsl[0];\n    let s = hsl[1];\n    const l = hsl[2];\n\n    function hue2rgb(p, q, h) {\n      if (h < 0)\n        h += 1;\n      else if (h > 1)\n        h -= 1;\n\n      if ((h * 6) < 1)\n        return p + (q - p) * h * 6;\n      else if ((h * 2) < 1)\n        return q;\n      else if ((h * 3) < 2)\n        return p + (q - p) * ((2 / 3) - h) * 6;\n      else\n        return p;\n    }\n\n    if (s < 0)\n      s = 0;\n\n    let q;\n    if (l <= 0.5)\n      q = l * (1 + s);\n    else\n      q = l + s - (l * s);\n\n    const p = 2 * l - q;\n\n    const tr = h + (1 / 3);\n    const tg = h;\n    const tb = h - (1 / 3);\n\n    out_rgb[0] = hue2rgb(p, q, tr);\n    out_rgb[1] = hue2rgb(p, q, tg);\n    out_rgb[2] = hue2rgb(p, q, tb);\n    out_rgb[3] = hsl[3];\n  }\n\n  /**\n   * @return {!Color.Format}\n   */\n  format() {\n    return this._format;\n  }\n\n  /**\n   * @return {!Array.<number>} HSLA with components within [0..1]\n   */\n  hsla() {\n    if (this._hsla)\n      return this._hsla;\n    const r = this._rgba[0];\n    const g = this._rgba[1];\n    const b = this._rgba[2];\n    const max = Math.max(r, g, b);\n    const min = Math.min(r, g, b);\n    const diff = max - min;\n    const add = max + min;\n\n    let h;\n    if (min === max)\n      h = 0;\n    else if (r === max)\n      h = ((1 / 6 * (g - b) / diff) + 1) % 1;\n    else if (g === max)\n      h = (1 / 6 * (b - r) / diff) + 1 / 3;\n    else\n      h = (1 / 6 * (r - g) / diff) + 2 / 3;\n\n    const l = 0.5 * add;\n\n    let s;\n    if (l === 0)\n      s = 0;\n    else if (l === 1)\n      s = 0;\n    else if (l <= 0.5)\n      s = diff / add;\n    else\n      s = diff / (2 - add);\n\n    this._hsla = [h, s, l, this._rgba[3]];\n    return this._hsla;\n  }\n\n  /**\n   * @return {boolean}\n   */\n  hasAlpha() {\n    return this._rgba[3] !== 1;\n  }\n\n  /**\n   * @return {!Color.Format}\n   */\n  detectHEXFormat() {\n    let canBeShort = true;\n    for (let i = 0; i < 4; ++i) {\n      const c = Math.round(this._rgba[i] * 255);\n      if (c % 17) {\n        canBeShort = false;\n        break;\n      }\n    }\n\n    const hasAlpha = this.hasAlpha();\n    const cf = Color.Format;\n    if (canBeShort)\n      return hasAlpha ? cf.ShortHEXA : cf.ShortHEX;\n    return hasAlpha ? cf.HEXA : cf.HEX;\n  }\n\n  /**\n   * @return {?string}\n   */\n  asString(format) {\n    if (format === this._format && this._originalTextIsValid)\n      return this._originalText;\n\n    if (!format)\n      format = this._format;\n\n    /**\n     * @param {number} value\n     * @return {number}\n     */\n    function toRgbValue(value) {\n      return Math.round(value * 255);\n    }\n\n    /**\n     * @param {number} value\n     * @return {string}\n     */\n    function toHexValue(value) {\n      const hex = Math.round(value * 255).toString(16);\n      return hex.length === 1 ? '0' + hex : hex;\n    }\n\n    /**\n     * @param {number} value\n     * @return {string}\n     */\n    function toShortHexValue(value) {\n      return (Math.round(value * 255) / 17).toString(16);\n    }\n\n    switch (format) {\n      case Color.Format.Original:\n        return this._originalText;\n      case Color.Format.RGB:\n        if (this.hasAlpha())\n          return null;\n        return `rgb(${toRgbValue(this._rgba[0])}, ${toRgbValue(this._rgba[1])}, ${toRgbValue(this._rgba[2])})`;\n      case Color.Format.RGBA:\n        return `rgba(${toRgbValue(this._rgba[0])}, ${toRgbValue(this._rgba[1])}, ${toRgbValue(this._rgba[2])}, ${this._rgba[3]})`;\n      case Color.Format.HSL:\n        if (this.hasAlpha())\n          return null;\n        const hsl = this.hsla();\n        return `hsl(${Math.round(hsl[0] * 360)}, ${Math.round(hsl[1] * 100)}%, ${Math.round(hsl[2] * 100)}%)`;\n      case Color.Format.HSLA:\n        const hsla = this.hsla();\n        return `hsla(${Math.round(hsla[0] * 360)}, ${Math.round(hsla[1] * 100)}%, ${Math.round(hsla[2] * 100)}%, ${hsla[3]})`;\n      case Color.Format.HEXA:\n        return `#${toHexValue(this._rgba[0])}${toHexValue(this._rgba[1])}${toHexValue(this._rgba[2])}${toHexValue(this._rgba[3])}`.toLowerCase();\n      case Color.Format.HEX:\n        if (this.hasAlpha())\n          return null;\n        return `#${toHexValue(this._rgba[0])}${toHexValue(this._rgba[1])}${toHexValue(this._rgba[2])}`.toLowerCase();\n      case Color.Format.ShortHEXA:\n        const hexFormat = this.detectHEXFormat();\n        if (hexFormat !== Color.Format.ShortHEXA && hexFormat !== Color.Format.ShortHEX)\n          return null;\n        return `#${toShortHexValue(this._rgba[0])}${toShortHexValue(this._rgba[1])}${toShortHexValue(this._rgba[2])}${toShortHexValue(this._rgba[3])}`.toLowerCase();\n      case Color.Format.ShortHEX:\n        if (this.hasAlpha())\n          return null;\n        if (this.detectHEXFormat() !== Color.Format.ShortHEX)\n          return null;\n        return `#${toShortHexValue(this._rgba[0])}${toShortHexValue(this._rgba[1])}${toShortHexValue(this._rgba[2])}`.toLowerCase();\n    }\n\n    return this._originalText;\n  }\n\n  /**\n   * @return {!Array<number>}\n   */\n  rgba() {\n    return this._rgba.slice();\n  }\n\n  /**\n   * @return {!Array.<number>}\n   */\n  canonicalRGBA() {\n    const rgba = new Array(4);\n    for (let i = 0; i < 3; ++i)\n      rgba[i] = Math.round(this._rgba[i] * 255);\n    rgba[3] = this._rgba[3];\n    return rgba;\n  }\n}\n\n/** @type {!RegExp} */\nColor.Regex = /((?:rgb|hsl)a?\\([^)]+\\)|#[0-9a-fA-F]{8}|#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3,4}|\\b[a-zA-Z]+\\b(?!-))/g;\n\n/**\n * @enum {string}\n */\nColor.Format = {\n  Original: 'original',\n  HEX: 'hex',\n  ShortHEX: 'shorthex',\n  HEXA: 'hexa',\n  ShortHEXA: 'shorthexa',\n  RGB: 'rgb',\n  RGBA: 'rgba',\n  HSL: 'hsl',\n  HSLA: 'hsla'\n};\n\nmodule.exports = { Color };\n"
  },
  {
    "path": "lib/features/file_info.js",
    "content": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nmodule.exports = function install(hostWindow) {\n  let lastFileId = 0;\n  self.carlo.fileInfo = async(file) => {\n    const fileId = ++lastFileId;\n    self.carlo.fileInfo.files_.set(fileId, file);\n    const result = await hostWindow.fileInfo(`self.carlo.fileInfo.files_.get(${fileId})`);\n    self.carlo.fileInfo.files_.delete(fileId);\n    return result;\n  };\n\n  self.carlo.fileInfo.files_ = new Map();\n};\n"
  },
  {
    "path": "lib/features/shortcuts.js",
    "content": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nmodule.exports = function install(hostWindow) {\n  const ctrlOrCmdCodes = new Set(\n      ['KeyD', 'KeyE', 'KeyD', 'KeyG', 'KeyN', 'KeyO', 'KeyP', 'KeyQ', 'KeyR', 'KeyS',\n       'KeyT', 'KeyW', 'KeyY', 'Tab', 'PageUp', 'PageDown', 'F4']);\n  const cmdCodes = new Set(['BracketLeft', 'BracketRight', 'Comma']);\n  const cmdOptionCodes = new Set(['ArrowLeft', 'ArrowRight', 'KeyB']);\n  const ctrlShiftCodes = new Set(['KeyQ', 'KeyW']);\n  const altCodes = new Set(['Home', 'ArrowLeft', 'ArrowRight', 'F4']);\n\n  function preventDefaultShortcuts(event) {\n    let prevent = false;\n    if (navigator.userAgent.match(/Mac OS X/)) {\n      if (event.metaKey) {\n        if (event.keyCode > 48 && event.keyCode <= 57) // 1-9\n          prevent = true;\n        if (ctrlOrCmdCodes.has(event.code) || cmdCodes.has(event.code))\n          prevent = true;\n        if (event.shiftKey && cmdOptionCodes.has(event.code))\n          prevent = true;\n        if (event.code === 'ArrowLeft' || event.code === 'ArrowRight') {\n          if (!event.contentEditable && event.target.nodeName !== 'INPUT' && event.target.nodeName !== 'TEXTAREA')\n            prevent = true;\n        }\n      }\n    } else {\n      if (event.code === 'F4')\n        prevent = true;\n      if (event.ctrlKey) {\n        if (event.keyCode > 48 && event.keyCode <= 57) // 1-9\n          prevent = true;\n        if (ctrlOrCmdCodes.has(event.code))\n          prevent = true;\n        if (event.shiftKey && ctrlShiftCodes.has(event.code))\n          prevent = true;\n      }\n      if (event.altKey && altCodes.has(event.code))\n        prevent = true;\n    }\n\n    if (prevent)\n      event.preventDefault();\n  }\n\n  document.addEventListener('keydown', preventDefaultShortcuts, false);\n  document.addEventListener('keydown', event => {\n    if ((event.key === 'q' || event.key === 'Q') && (event.metaKey || event.ctrlKey)) {\n      hostWindow.closeBrowser();\n      event.preventDefault();\n    }\n  });\n};\n"
  },
  {
    "path": "lib/find_chrome.js",
    "content": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use strict';\n\nconst fs = require('fs');\nconst path = require('path');\nconst execSync = require('child_process').execSync;\nconst execFileSync = require('child_process').execFileSync;\nconst puppeteer = require('puppeteer-core');\n\nconst newLineRegex = /\\r?\\n/;\n\nfunction darwin(canary) {\n  const LSREGISTER = '/System/Library/Frameworks/CoreServices.framework' +\n      '/Versions/A/Frameworks/LaunchServices.framework' +\n      '/Versions/A/Support/lsregister';\n  const grepexpr = canary ? 'google chrome canary' : 'google chrome';\n  const result =\n      execSync(`${LSREGISTER} -dump  | grep -i \\'${grepexpr}\\\\?.app$\\' | awk \\'{$1=\"\"; print $0}\\'`);\n\n  const installations = new Set();\n  const paths = result.toString().split(newLineRegex).filter(a => a).map(a => a.trim());\n  paths.unshift(canary ? '/Applications/Google Chrome Canary.app' : '/Applications/Google Chrome.app');\n  for (const p of paths) {\n    if (p.startsWith('/Volumes'))\n      continue;\n    const inst = path.join(p, canary ? '/Contents/MacOS/Google Chrome Canary' : '/Contents/MacOS/Google Chrome');\n    if (canAccess(inst))\n      return inst;\n  }\n}\n\n/**\n * Look for linux executables in 3 ways\n * 1. Look into CHROME_PATH env variable\n * 2. Look into the directories where .desktop are saved on gnome based distro's\n * 3. Look for google-chrome-stable & google-chrome executables by using the which command\n */\nfunction linux(canary) {\n  let installations = [];\n\n  // Look into the directories where .desktop are saved on gnome based distro's\n  const desktopInstallationFolders = [\n    path.join(require('os').homedir(), '.local/share/applications/'),\n    '/usr/share/applications/',\n  ];\n  desktopInstallationFolders.forEach(folder => {\n    installations = installations.concat(findChromeExecutables(folder));\n  });\n\n  // Look for google-chrome(-stable) & chromium(-browser) executables by using the which command\n  const executables = [\n    'google-chrome-stable',\n    'google-chrome',\n    'chromium-browser',\n    'chromium',\n  ];\n  executables.forEach(executable => {\n    try {\n      const chromePath =\n          execFileSync('which', [executable], {stdio: 'pipe'}).toString().split(newLineRegex)[0];\n      if (canAccess(chromePath))\n        installations.push(chromePath);\n    } catch (e) {\n      // Not installed.\n    }\n  });\n\n  if (!installations.length)\n    throw new Error('The environment variable CHROME_PATH must be set to executable of a build of Chromium version 54.0 or later.');\n\n  const priorities = [\n    {regex: /chrome-wrapper$/, weight: 51},\n    {regex: /google-chrome-stable$/, weight: 50},\n    {regex: /google-chrome$/, weight: 49},\n    {regex: /chromium-browser$/, weight: 48},\n    {regex: /chromium$/, weight: 47},\n  ];\n\n  if (process.env.CHROME_PATH)\n    priorities.unshift({regex: new RegExp(`${process.env.CHROME_PATH}`), weight: 101});\n\n  return sort(uniq(installations.filter(Boolean)), priorities)[0];\n}\n\nfunction win32(canary) {\n  const suffix = canary ?\n    `${path.sep}Google${path.sep}Chrome SxS${path.sep}Application${path.sep}chrome.exe` :\n    `${path.sep}Google${path.sep}Chrome${path.sep}Application${path.sep}chrome.exe`;\n  const prefixes = [\n    process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)']\n  ].filter(Boolean);\n\n  let result;\n  prefixes.forEach(prefix => {\n    const chromePath = path.join(prefix, suffix);\n    if (canAccess(chromePath))\n      result = chromePath;\n  });\n  return result;\n}\n\nfunction sort(installations, priorities) {\n  const defaultPriority = 10;\n  return installations\n      // assign priorities\n      .map(inst => {\n        for (const pair of priorities) {\n          if (pair.regex.test(inst))\n            return {path: inst, weight: pair.weight};\n        }\n        return {path: inst, weight: defaultPriority};\n      })\n      // sort based on priorities\n      .sort((a, b) => (b.weight - a.weight))\n      // remove priority flag\n      .map(pair => pair.path);\n}\n\nfunction canAccess(file) {\n  if (!file)\n    return false;\n\n  try {\n    fs.accessSync(file);\n    return true;\n  } catch (e) {\n    return false;\n  }\n}\n\nfunction uniq(arr) {\n  return Array.from(new Set(arr));\n}\n\nfunction findChromeExecutables(folder) {\n  const argumentsRegex = /(^[^ ]+).*/; // Take everything up to the first space\n  const chromeExecRegex = '^Exec=\\/.*\\/(google-chrome|chrome|chromium)-.*';\n\n  const installations = [];\n  if (canAccess(folder)) {\n    // Output of the grep & print looks like:\n    //    /opt/google/chrome/google-chrome --profile-directory\n    //    /home/user/Downloads/chrome-linux/chrome-wrapper %U\n    let execPaths;\n\n    // Some systems do not support grep -R so fallback to -r.\n    // See https://github.com/GoogleChrome/chrome-launcher/issues/46 for more context.\n    try {\n      execPaths = execSync(`grep -ER \"${chromeExecRegex}\" ${folder} | awk -F '=' '{print $2}'`);\n    } catch (e) {\n      execPaths = execSync(`grep -Er \"${chromeExecRegex}\" ${folder} | awk -F '=' '{print $2}'`);\n    }\n\n    execPaths = execPaths.toString()\n        .split(newLineRegex)\n        .map(execPath => execPath.replace(argumentsRegex, '$1'));\n\n    execPaths.forEach(execPath => canAccess(execPath) && installations.push(execPath));\n  }\n\n  return installations;\n}\n\n/**\n * @return {!Promise<?string>}\n */\nasync function downloadChromium(options, targetRevision) {\n  const browserFetcher = puppeteer.createBrowserFetcher({ path: options.localDataDir });\n  const revision = targetRevision || require('puppeteer-core/package.json').puppeteer.chromium_revision;\n  const revisionInfo = browserFetcher.revisionInfo(revision);\n\n  // Do nothing if the revision is already downloaded.\n  if (revisionInfo.local)\n    return revisionInfo;\n\n  // Override current environment proxy settings with npm configuration, if any.\n  try {\n    console.log(`Downloading Chromium r${revision}...`);\n    const newRevisionInfo = await browserFetcher.download(revisionInfo.revision);\n    console.log('Chromium downloaded to ' + newRevisionInfo.folderPath);\n    let localRevisions = await browserFetcher.localRevisions();\n    localRevisions = localRevisions.filter(revision => revision !== revisionInfo.revision);\n    // Remove previous chromium revisions.\n    const cleanupOldVersions = localRevisions.map(revision => browserFetcher.remove(revision));\n    await Promise.all(cleanupOldVersions);\n    return newRevisionInfo;\n  } catch (error) {\n    console.error(`ERROR: Failed to download Chromium r${revision}!`);\n    console.error(error);\n    return null;\n  }\n}\n\nasync function findChrome(options) {\n  if (options.executablePath)\n    return { executablePath: options.executablePath, type: 'user' };\n\n  const config = new Set(options.channel || ['stable']);\n  let executablePath;\n  // Always prefer canary.\n  if (config.has('canary') || config.has('*')) {\n    if (process.platform === 'linux')\n      executablePath = linux(true);\n    else if (process.platform === 'win32')\n      executablePath = win32(true);\n    else if (process.platform === 'darwin')\n      executablePath = darwin(true);\n    if (executablePath)\n      return { executablePath, type: 'canary' };\n  }\n\n  // Then pick stable.\n  if (config.has('stable') || config.has('*')) {\n    if (process.platform === 'linux')\n      executablePath = linux();\n    else if (process.platform === 'win32')\n      executablePath = win32();\n    else if (process.platform === 'darwin')\n      executablePath = darwin();\n    if (executablePath)\n      return { executablePath, type: 'stable' };\n  }\n\n  // always prefer puppeteer revision of chromium\n  if (config.has('chromium') || config.has('*')) {\n    const revisionInfo = await downloadChromium(options);\n    return { executablePath: revisionInfo.executablePath, type: revisionInfo.revision };\n  }\n\n  for (const item of config) {\n    if (!item.startsWith('r'))\n      continue;\n    const revisionInfo = await downloadChromium(options, item.substring(1));\n    return { executablePath: revisionInfo.executablePath, type: revisionInfo.revision };\n  }\n\n  return {};\n}\n\nmodule.exports = findChrome;\n"
  },
  {
    "path": "lib/http_request.js",
    "content": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use strict';\n\nconst debugServer = require('debug')('carlo:server');\n\nconst statusTexts = {\n  '100': 'Continue',\n  '101': 'Switching Protocols',\n  '102': 'Processing',\n  '200': 'OK',\n  '201': 'Created',\n  '202': 'Accepted',\n  '203': 'Non-Authoritative Information',\n  '204': 'No Content',\n  '206': 'Partial Content',\n  '207': 'Multi-Status',\n  '208': 'Already Reported',\n  '209': 'IM Used',\n  '300': 'Multiple Choices',\n  '301': 'Moved Permanently',\n  '302': 'Found',\n  '303': 'See Other',\n  '304': 'Not Modified',\n  '305': 'Use Proxy',\n  '306': 'Switch Proxy',\n  '307': 'Temporary Redirect',\n  '308': 'Permanent Redirect',\n  '400': 'Bad Request',\n  '401': 'Unauthorized',\n  '402': 'Payment Required',\n  '403': 'Forbidden',\n  '404': 'Not Found',\n  '405': 'Method Not Allowed',\n  '406': 'Not Acceptable',\n  '407': 'Proxy Authentication Required',\n  '408': 'Request Timeout',\n  '409': 'Conflict',\n  '410': 'Gone',\n  '411': 'Length Required',\n  '412': 'Precondition Failed',\n  '413': 'Payload Too Large',\n  '414': 'URI Too Long',\n  '415': 'Unsupported Media Type',\n  '416': 'Range Not Satisfiable',\n  '417': 'Expectation Failed',\n  '418': 'I\\'m a teapot',\n  '421': 'Misdirected Request',\n  '422': 'Unprocessable Entity',\n  '423': 'Locked',\n  '424': 'Failed Dependency',\n  '426': 'Upgrade Required',\n  '428': 'Precondition Required',\n  '429': 'Too Many Requests',\n  '431': 'Request Header Fields Too Large',\n  '451': 'Unavailable For Legal Reasons',\n  '500': 'Internal Server Error',\n  '501': 'Not Implemented',\n  '502': 'Bad Gateway',\n  '503': 'Service Unavailable',\n  '504': 'Gateway Timeout',\n  '505': 'HTTP Version Not Supported',\n  '506': 'Variant Also Negotiates',\n  '507': 'Insufficient Storage',\n  '508': 'Loop Detected',\n  '510': 'Not Extended',\n  '511': 'Network Authentication Required',\n};\n\n/**\n * Intercepted request instance that can be resolved to the client's liking.\n */\nclass HttpRequest {\n  /**\n   * @param {!CDPSession} session\n   * @param {!Object} params\n   */\n  constructor(session, params, handlers) {\n    this.session_ = session;\n    this.params_ = params;\n    this.handlers_ = handlers;\n    this.done_ = false;\n    this.callNextHandler_();\n  }\n\n  /**\n   * @return {string}\n   */\n  url() {\n    return this.params_.request.url;\n  }\n\n  /**\n   * @return {string}\n   */\n  method() {\n    return this.params_.request.method;\n  }\n\n  /**\n   * @return {!Object<string, string>} HTTP request headers.\n   */\n  headers() {\n    return this.params_.request.headers || {};\n  }\n\n  /**\n   * @return {string}\n   */\n  resourceType() {\n    return this.params_.resourceType;\n  }\n\n  /**\n   * Aborts the request.\n   */\n  abort() {\n    debugServer('abort', this.url());\n    return this.resolve_({errorReason: 'Aborted'});\n  }\n\n\n  /**\n   * Fails the request.\n   */\n  fail() {\n    debugServer('fail', this.url());\n    return this.resolve_({errorReason: 'Failed'});\n  }\n\n  /**\n   * Falls through to the next handler.\n   */\n  continue() {\n    debugServer('continue', this.url());\n    return this.callNextHandler_();\n  }\n\n  /**\n   * Continues the request with the provided overrides to the url, method or\n   * headers.\n   *\n   * @param {{url: (string|undefined), method: (string|undefined),\n   *     headers: (!Object<string, string>|undefined)}|undefined} overrides\n   * Overrides to apply to the request before it hits network.\n   */\n  deferToBrowser(overrides) {\n    debugServer('deferToBrowser', this.url());\n    const params = {};\n    if (overrides && overrides.url) params.url = overrides.url;\n    if (overrides && overrides.method) params.method = overrides.method;\n    if (overrides && overrides.headers) params.headers = overrides.headers;\n    return this.resolve_(params);\n  }\n\n  /**\n   * Fulfills the request with the given data.\n   *\n   * @param {{status: number|undefined,\n   *          headers: !Object<string,string>|undefined,\n   *          body: !Buffer|undefined}} options\n   */\n  fulfill({status, headers, body}) {\n    debugServer('fulfill', this.url());\n    status = status || 200;\n    const responseHeaders = {};\n    if (headers) {\n      for (const header of Object.keys(headers))\n        responseHeaders[header.toLowerCase()] = headers[header];\n    }\n    if (body && !('content-length' in responseHeaders))\n      responseHeaders['content-length'] = Buffer.byteLength(body);\n\n    const statusText = statusTexts[status] || '';\n    const statusLine = `HTTP/1.1 ${status} ${statusText}`;\n\n    const CRLF = '\\r\\n';\n    let text = statusLine + CRLF;\n    for (const header of Object.keys(responseHeaders))\n      text += header + ': ' + responseHeaders[header] + CRLF;\n    text += CRLF;\n    let responseBuffer = Buffer.from(text, 'utf8');\n    if (body)\n      responseBuffer = Buffer.concat([responseBuffer, body]);\n\n    return this.resolve_({\n      interceptionId: this.interceptionId_,\n      rawResponse: responseBuffer.toString('base64')\n    });\n  }\n\n  callNextHandler_() {\n    debugServer('next handler', this.url());\n    const handler = this.handlers_.shift();\n    if (handler) {\n      handler(this);\n      return;\n    }\n    this.resolve_({});\n  }\n\n  /**\n   * Aborts the request.\n   * @param {!Object} params\n   */\n  async resolve_(params) {\n    debugServer('resolve', this.url());\n    if (this.done_) throw new Error('Already resolved given request');\n    params.interceptionId = this.params_.interceptionId;\n    this.done_ = true;\n    return this.session_.send('Network.continueInterceptedRequest', params);\n  }\n}\n\nmodule.exports = { HttpRequest };\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"carlo\",\n  \"version\": \"0.9.46\",\n  \"description\": \"Carlo is a framework for rendering Node data structures using Chrome browser.\",\n  \"repository\": \"github:GoogleChromeLabs/carlo\",\n  \"engines\": {\n    \"node\": \">=7.6.0\"\n  },\n  \"main\": \"index.js\",\n  \"directories\": {\n    \"lib\": \"lib\"\n  },\n  \"scripts\": {\n    \"lint\": \"([ \\\"$CI\\\" = true ] && eslint --quiet -f codeframe . || eslint .)\",\n    \"test\": \"node rpc/test.js && node test/test.js\",\n    \"headful-test\": \"node test/headful.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"The Chromium Authors\",\n  \"license\": \"Apache-2.0\",\n  \"dependencies\": {\n    \"debug\": \"^4.1.0\",\n    \"puppeteer-core\": \"~1.12.0\"\n  },\n  \"devDependencies\": {\n    \"eslint\": \"^5.8.0\",\n    \"@pptr/testrunner\": \"^0.5.0\",\n    \"@pptr/testserver\": \"^0.5.0\"\n  }\n}\n"
  },
  {
    "path": "rpc/index.js",
    "content": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use strict';\n\nmodule.exports = {\n  rpc: require('./rpc'),\n  rpc_process: require('./rpc_process')\n};\n"
  },
  {
    "path": "rpc/rpc.js",
    "content": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use strict';\n\n/** @typedef { !Array<string> } Address */\n/** @typedef {{ name: string, isFunc: boolean }} Descriptor */\n/** @typedef {function(function(data)): function(data)} Transport */\n\nconst handleSymbol = Symbol('handle');\n\n/**\n * Handle to the object. This handle has methods matching the methods of the\n * target object. Calling these methods calls them remotely over the low level\n * messaging transprot. Return values are delivered to the caller.\n */\nclass Handle {\n  /**\n   * @param {string} localAddress Address of this handle.\n   * @param {string} address Address of the primary handle this handle refers\n   *                 to. Primary handle is the one that lives in the same world\n   *                 as the actual object it refers to.\n   * @param {!Object} descriptor Target object spec descriptor (list of methods, etc.)\n   * @param {!Rpc} rpc\n   */\n  constructor(localAddress, address, descriptor, rpc) {\n    this.localAddress_ = localAddress;\n    this.address_ = address;\n    this.descriptor_ = descriptor;\n    this.rpc_ = rpc;\n    this.object_ = null;\n\n    const target = {};\n    target[handleSymbol] = this;\n    this.proxy_ = new Proxy(target, { get: Handle.proxyHandler_ });\n  }\n\n  /**\n   * We always return proxies to the user to encapsulate handle and marshall\n   * calls automatically.\n   */\n  static proxyHandler_(target, methodName, receiver) {\n    const handle = target[handleSymbol];\n    if (methodName === handleSymbol)\n      return handle;\n    if (typeof methodName !== 'string')\n      return;\n    if (methodName === 'then')\n      return target[methodName];\n    return handle.callMethod_.bind(handle, methodName);\n  }\n\n  /**\n   * Calls method on the target object.\n   *\n   * @param {string} method Method to call on the target object.\n   * @param {!Array<*>} args Call arguments. These can be either primitive\n   *                    types, other handles or JSON structures.\n   * @return {!Promise<*>} result, also primitive, JSON or handle.\n   */\n  async callMethod_(method, ...args) {\n    const message = {\n      m: method,\n      p: this.rpc_.wrap_(args)\n    };\n    const response = await this.rpc_.sendCommand_(this.address_, this.localAddress_, message);\n    return this.rpc_.unwrap_(response);\n  }\n\n  /**\n   * Dispatches external message on this handle.\n   * @param {string} message\n   * @return {!Promise<*>} result, also primitive, JSON or handle.\n   */\n  async dispatchMessage_(message) {\n    if (this.descriptor_.isFunc) {\n      const result = await this.object_(...this.rpc_.unwrap_(message.p));\n      return this.rpc_.wrap_(result);\n    }\n    if (message.m.startsWith('_') || message.m.endsWith('_'))\n      throw new Error(`Private members are not exposed over RPC: '${message.m}'`);\n\n    if (!(message.m in this.object_))\n      throw new Error(`There is no member '${message.m}' in '${this.descriptor_.name}'`);\n    const value = this.object_[message.m];\n    if (typeof value !== 'function') {\n      if (message.p.length)\n        throw new Error(`'${message.m}' is not a function, can't pass args '${message.p}'`);\n      return this.rpc_.wrap_(value);\n    }\n\n    const result = await this.object_[message.m](...this.rpc_.unwrap_(message.p));\n    return this.rpc_.wrap_(result);\n  }\n\n  /**\n   * Returns the proxy to this handle that is passed to the userland.\n   */\n  proxy() {\n    return this.proxy_;\n  }\n}\n\n/**\n * Main Rpc object. Keeps all the book keeping and performs message routing\n * between handles beloning to different worlds. Each 'world' has a singleton\n * 'rpc' instance.\n */\nclass Rpc {\n  constructor() {\n    this.lastHandleId_ = 0;\n    this.lastWorldId_ = 0;\n    this.worlds_ = new Map();\n    this.idToHandle_ = new Map();\n    this.lastMessageId_ = 0;\n    this.callbacks_ = new Map();\n\n    this.worldId_ = '.';\n    this.sendToParent_ = null;\n    this.cookieResponseCallbacks_ = new Map();\n    this.debug_ = false;\n  }\n\n  /**\n   * Each singleton rpc object has the world's parameters that parent world sent\n   * to them.\n   *\n   * @return {*}\n   */\n  params() {\n    return this.worldParams_;\n  }\n\n  /**\n   * Called in the parent world.\n   * Creates a child world with the given root handle.\n   *\n   * @param {!Transport} transport\n   *        - receives function that should be called upon messages from\n   *          the world and\n   *        - returns function that should be used to send messages to the\n   *          world\n   * @param {...*} args Params to pass to the child world.\n   * @return {!Promise<{worldId:string, *}>} returns the handles / parameters that child\n   *         world returned during the initialization.\n   */\n  createWorld(transport, ...args) {\n    const worldId = this.worldId_ + '/' + (++this.lastWorldId_);\n    const sendToChild = transport(this.routeMessage_.bind(this, false));\n    this.worlds_.set(worldId, sendToChild);\n    sendToChild({cookie: true, args: this.wrap_(args), worldId });\n    return new Promise(f => this.cookieResponseCallbacks_.set(worldId, f));\n  }\n\n  /**\n   * Called in the parent world.\n   * Disposes a child world with the given id.\n   *\n   * @param {string} worldId The world to dispose.\n   */\n  disposeWorld(worldId) {\n    if (!this.worlds_.has(worldId))\n      throw new Error('No world with given id exists');\n    this.worlds_.delete(worldId);\n  }\n\n  /**\n   * Called in the child world to initialize it.\n   * @param {!Transport} transport.\n   * @param {function(...*):!Promise<*>} initializer\n   */\n  initWorld(transport, initializer) {\n    this.sendToParent_ = transport(this.routeMessage_.bind(this, true));\n    return new Promise(f => this.cookieCallback_ = f)\n        .then(args => initializer ? initializer(...args) : undefined)\n        .then(response => this.sendToParent_(\n            {cookieResponse: true, worldId: this.worldId_, r: this.wrap_(response)}));\n  }\n\n  /**\n   * Creates a handle to the object.\n   * @param {!Object} object Object to create handle for\n   * @return {!Object}\n   */\n  handle(object) {\n    if (!object)\n      throw new Error('Can only create handles for objects');\n    if (typeof object === 'object' && handleSymbol in object)\n      throw new Error('Can not return handle to handle.');\n    const descriptor = this.describe_(object);\n    const address = [this.worldId_, descriptor.name + '#' + (++this.lastHandleId_)];\n    const handle = new Handle(address, address, descriptor, this);\n    handle.object_ = object;\n    this.idToHandle_.set(address[1], handle);\n    return handle.proxy();\n  }\n\n  /**\n   * Returns the object this handle points to. Only works on the local\n   * handles, otherwise returns null.\n   *\n   * @param {*} handle Primary object handle.\n   * @return {?Object}\n   */\n  object(proxy) {\n    return proxy[handleSymbol].object_ || null;\n  }\n\n  /**\n   * Disposes a handle to the object.\n   * @param {*} handle Primary object handle.\n   */\n  dispose(proxy) {\n    const handle = proxy[handleSymbol];\n    if (!handle.object_)\n      throw new Error('Can only dipose handle that was explicitly created with rpc.handle()');\n    this.idToHandle_.delete(handle.address_[1]);\n  }\n\n  /**\n   * Builds object descriptor.\n   * @return {!Descriptor}\n   */\n  describe_(o) {\n    if (typeof o === 'function')\n      return { isFunc: true };\n    return { name: o.constructor.name };\n  }\n\n  /**\n   * Wraps call argument as a protocol structures.\n   * @param {*} param\n   * @param {number=} maxDepth\n   * @return {*}\n   */\n  wrap_(param, maxDepth = 1000) {\n    if (!maxDepth)\n      throw new Error('Object reference chain is too long');\n    maxDepth--;\n    if (!param)\n      return param;\n\n    if (param[handleSymbol]) {\n      const handle = param[handleSymbol];\n      return { __rpc_a__: handle.address_, descriptor: handle.descriptor_ };\n    }\n\n    if (param instanceof Array)\n      return param.map(item => this.wrap_(item, maxDepth));\n\n    if (typeof param === 'object') {\n      const result = {};\n      for (const key in param)\n        result[key] = this.wrap_(param[key], maxDepth);\n      return result;\n    }\n\n    return param;\n  }\n\n  /**\n   * Unwraps call argument from the protocol structures.\n   * @param {!Object} param\n   * @return {*}\n   */\n  unwrap_(param) {\n    if (!param)\n      return param;\n    if (param.__rpc_a__) {\n      const handle = this.createHandle_(param.__rpc_a__, param.descriptor);\n      if (handle.descriptor_.isFunc)\n        return (...args) => handle.callMethod_('call', ...args);\n      return handle.proxy();\n    }\n\n    if (param instanceof Array)\n      return param.map(item => this.unwrap_(item));\n\n    if (typeof param === 'object') {\n      const result = {};\n      for (const key in param)\n        result[key] = this.unwrap_(param[key]);\n      return result;\n    }\n\n    return param;\n  }\n\n  /**\n   * Unwraps descriptor and creates a local world handle that will be associated\n   * with the primary handle at given address.\n   *\n   * @param {!Address} address Address of the primary wrapper.\n   * @param {!Descriptor} address Address of the primary wrapper.\n   * @return {!Handle}\n   */\n  createHandle_(address, descriptor) {\n    if (address[0] === this.worldId_) {\n      const existing = this.idToHandle_.get(address[1]);\n      if (existing)\n        return existing;\n    }\n\n    const localAddress = [this.worldId_, descriptor.name + '#' + (++this.lastHandleId_)];\n    return new Handle(localAddress, address, descriptor, this);\n  }\n\n  /**\n   * Sends message to the target handle and receive the response.\n   *\n   * @param {!Object} payload\n   * @return {!Promise<!Object>}\n   */\n  sendCommand_(to, from, message) {\n    const payload = { to, from, message, id: ++this.lastMessageId_ };\n    if (this.debug_)\n      console.log('\\nSEND', payload);\n    const result = new Promise((fulfill, reject) =>\n      this.callbacks_.set(payload.id, {fulfill, reject}));\n    this.routeMessage_(false, payload);\n    return result;\n  }\n\n  /**\n   * Routes message between the worlds.\n   *\n   * @param {!Object} payload\n   */\n  routeMessage_(fromParent, payload) {\n    if (this.debug_)\n      console.log(`\\nROUTE[${this.worldId_}]`, payload);\n\n    if (payload.cookie) {\n      this.worldId_ = payload.worldId;\n      this.cookieCallback_(this.unwrap_(payload.args));\n      this.cookieCallback_ = null;\n      return;\n    }\n\n    // If this is a cookie request, the world is being initialized.\n    if (payload.cookieResponse) {\n      const callback = this.cookieResponseCallbacks_.get(payload.worldId);\n      this.cookieResponseCallbacks_.delete(payload.worldId);\n      callback({ result: this.unwrap_(payload.r), worldId: payload.worldId });\n      return;\n    }\n\n    if (!fromParent && !this.isActiveWorld_(payload.from[0])) {\n      // Dispatching from the disposed world.\n      if (this.debug_)\n        console.log(`DROP ON THE FLOOR`);\n      return;\n    }\n\n    if (payload.to[0] === this.worldId_) {\n      if (this.debug_)\n        console.log(`ROUTED TO SELF`);\n      this.dispatchMessageLocally_(payload);\n      return;\n    }\n\n    for (const [worldId, worldSend] of this.worlds_) {\n      if (payload.to[0].startsWith(worldId)) {\n        if (this.debug_)\n          console.log(`ROUTED TO CHILD ${worldId}`);\n        worldSend(payload);\n        return;\n      }\n    }\n\n    if (payload.to[0].startsWith(this.worldId_)) {\n      // Sending to the disposed world.\n      if (this.debug_)\n        console.log(`DROP ON THE FLOOR`);\n      return;\n    }\n\n    if (this.debug_)\n      console.log(`ROUTED TO PARENT`);\n    this.sendToParent_(payload);\n  }\n\n  /**\n   * @param {!Address} address\n   * @return {boolean}\n   */\n  isActiveWorld_(worldId) {\n    if (this.worldId_ === worldId)\n      return true;\n    for (const wid of this.worlds_.keys()) {\n      if (worldId.startsWith(wid))\n        return true;\n    }\n    return false;\n  }\n\n  /**\n   * Message is routed from other worlds and hits rpc here.\n   *\n   * @param {!Object} payload\n   */\n  async dispatchMessageLocally_(payload) {\n    if (this.debug_)\n      console.log('\\nDISPATCH', payload);\n    // Dispatch the response.\n    if (typeof payload.rid === 'number') {\n      const {fulfill, reject} = this.callbacks_.get(payload.rid);\n      this.callbacks_.delete(payload.rid);\n      if (payload.e)\n        reject(new Error(payload.e));\n      else\n        fulfill(payload.r);\n      return;\n    }\n\n    const message = { from: payload.to, rid: payload.id, to: payload.from };\n    const handle = this.idToHandle_.get(payload.to[1]);\n    if (!handle) {\n      message.e = 'Object has been diposed.';\n    } else {\n      try {\n        message.r = await handle.dispatchMessage_(payload.message);\n      } catch (e) {\n        message.e = e.toString() + '\\n' + e.stack;\n      }\n    }\n    this.routeMessage_(false, message);\n  }\n}\n\nmodule.exports = new Rpc();\n"
  },
  {
    "path": "rpc/rpc.md",
    "content": "## RPC API\n\n> This is a pre-release API, so it is a subject to change. Please use it at your own risk. Once API is validated, it will be bumped to v1.0 and preserved for backwards compatibility.\n\n### Handles\n\nIn Carlo's RPC system one can obtain a `handle` to a local `object` and pass it between the execution\ncontexts. Execution contexts can be Chrome, Node, child processes or any other JavaScript\nexecution environment, local or remote.\n\n![rpc](https://user-images.githubusercontent.com/883973/48327354-0d6f1f00-e5f3-11e8-99dc-fef5f4ad53dc.png)\n\nCalling a method on the `handle` results in calling it on the actual `object`:\n\n```js\nclass Foo {\n  hello(name) { console.log(`hello ${name}`); }\n}\nconst foo = rpc.handle(new Foo());  // <-- Obtained handle to object.\nawait foo.hello('world');  // <-- Prints 'hello world'.\n```\n\n> By default, `handle` has access to all the *public* methods of the object.\nPublic methods are the ones not starting or ending with `_`.\n\nAll handle operations are async, notice how synchronous `hello` method became async when accessed\nvia the handle. The world where `handle` is created can access the actual `object`. When handle is no longer needed, the world that created it can dispose it:\n\n```js\nconst handle = rpc.handle(object);\nconst object = rpc.object(handle);\nrpc.dispose(handle);\n```\n\nProperties of the target object are similarly accessible via the handle:\n\n```js\nconst foo = rpc.handle({ myValue: 'value' });  // <-- Obtained handle to object.\nawait foo.myValue();  // <-- Returns 'value'.\n```\n\nHandles are passed between the worlds as arguments of the calls on other handles:\n\n`World 1`\n```js\nclass Parent {\n  constructor() {\n    this.children = [];\n  }\n  addChild(child) {\n    this.children.push(child);\n    return this.children.length - 1;\n  }\n}\n```\n\n`World 2`\n```js\nclass Child {}\n\nasync function main(parent) {  // parent is a handle to the object from World 1.\n  const child = rpc.handle(new Child);\n  // Call method on parent remotely, pass handle to child into it.\n  const ordinal = await parent.addChild(child);\n  console.log(`Added child #${ordinal}`);\n}\n```\n\n### Example\nFollowing is an end-to-end example of the RPC application that demonstrates the variety of remote\noperations that can be performed on handles:\n\n`family.js`\n\n```js\nconst rpc = require('rpc');\n\nclass Parent {\n  constructor() {\n    this.children = [];\n  }\n\n  addChild(child) {\n    const ordinal = this.children.length;\n    console.log(`Adding child #${ordinal}`);\n    child.setOrdinal(ordinal);\n\n    // Go over the children and make siblings aware of each other.\n    for (const c of this.children) {\n      c.setSibling(child);\n      child.setSibling(c);\n    }\n    this.children.push(child);\n    return ordinal;\n  }\n}\n\nclass Child {\n  constructor() {\n    // Obtain handle to self that is used in RPC.\n    this.handle_ = rpc.handle(this);\n  }\n\n  setOrdinal(ordinal) { this.ordinal_ = ordinal; }\n  ordinal() { return this.ordinal_; }\n\n  async setSibling(sibling) {\n    // Say hello to another sibling when it is reported.\n    const o = await sibling.ordinal();\n    console.log(`I am #${this.ordinal_} and I have a sibling #${o}`);\n    await sibling.hiSibling(this.handle_);\n  }\n\n  async hiSibling(sibling) {\n    const o = await sibling.ordinal();\n    console.log(`I am #${this.ordinal_} and my sibling #${o} is saying hello`);\n  }\n\n  dispose() {\n    rpc.dispose(this.handle_);\n  }\n}\n\nmodule.exports = { Parent, Child };\n```\n\n`main.js` runs in the main process.\n```js\nconst rpc = require('rpc');\nconst rpc_process = require('rpc_process');\nconst { Parent } = require('./family');\n\n(async () => {\n  // Create parent object in the main process, obtain the handle to it.\n  const parent = rpc.handle(new Parent());\n\n  // Create a child process and load worker.js there. Pass parent object\n  // into that new child world, assign return value to a child.\n  const child1 = await rpc_process.spawn(__dirname + '/worker.js', parent);\n  parent.addChild(child1);\n\n  // Do it again.\n  const child2 = await rpc_process.spawn(__dirname + '/worker.js', parent);\n  parent.addChild(child2);\n})();\n```\n\n`worker.js` runs in a child process.\n```js\nconst rpc = require('rpc');\nconst rpc_process = require('rpc_process');\nconst { Child } = require('./family');\n\nrpc_process.init(parent => {\n  // Note that parent is available in this context and we can call\n  // parent.addChild(rpc.handle(new Child)) here.\n\n  // But we prefer to simply return the handle to the newly created child\n  // into the parent world for the sake of this demo.\n  return rpc.handle(new Child());\n});\n```\n"
  },
  {
    "path": "rpc/rpc_process.js",
    "content": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use strict';\n\nconst child_process = require('child_process');\nconst rpc = require('./rpc');\n\nasync function spawn(fileName, ...args) {\n  const child = child_process.fork(fileName, [], {\n    detached: true, stdio: [0, 1, 2, 'ipc']\n  });\n\n  const transport = receivedFromChild => {\n    child.on('message', receivedFromChild);\n    return child.send.bind(child);\n  };\n  const { result } = await rpc.createWorld(transport, ...args);\n  return result;\n}\n\nfunction init(initializer) {\n  const transport = receivedFromParent => {\n    process.on('message', receivedFromParent);\n    return process.send.bind(process);\n  };\n  rpc.initWorld(transport, initializer);\n}\n\nmodule.exports = { spawn, init };\n"
  },
  {
    "path": "rpc/test.js",
    "content": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst {TestRunner, Reporter, Matchers} = require('@pptr/testrunner');\nconst rpc = require('./rpc');\n\n// Runner holds and runs all the tests\nconst runner = new TestRunner({\n  parallel: 1, // run 2 parallel threads\n  timeout: 1000, // setup timeout of 1 second per test\n});\n// Simple expect-like matchers\nconst {expect} = new Matchers();\n\n// Extract jasmine-like DSL into the global namespace\nconst {describe, xdescribe, fdescribe} = runner;\nconst {it, fit, xit} = runner;\nconst {beforeAll, beforeEach, afterAll, afterEach} = runner;\n\nasync function createChildWorld(rpc, initializer, ...args) {\n  let sendToParent;\n  let sendToChild;\n  function transport1(receivedFromChild) {\n    sendToParent = receivedFromChild;\n    return data => setTimeout(() => sendToChild(data), 0);\n  }\n  function transport2(receivedFromParent) {\n    sendToChild = receivedFromParent;\n    return data => setTimeout(() => sendToParent(data), 0);\n  }\n  const childRpc = new rpc.constructor();\n  childRpc.initWorld(transport2, p => initializer(p, childRpc));\n  await rpc.createWorld(transport1, ...args);\n  return childRpc;\n}\n\ndescribe('rpc', () => {\n  it('call method', async(state, test) => {\n    class Foo {\n      sum(a, b) { return a + b; }\n    }\n    const foo = rpc.handle(new Foo());\n    expect(await foo.sum(1, 3)).toBe(4);\n  });\n  it('call method with object', async(state, test) => {\n    class Foo {\n      sum(a, b) { return { value: a.value + b.value }; }\n    }\n    const foo = rpc.handle(new Foo());\n    const result = await foo.sum({value: 1}, {value: 3});\n    expect(result.value).toBe(4);\n  });\n  it('call method with array', async(state, test) => {\n    class Foo {\n      sum(arr) { return arr.reduce((a, c) => a + c, 0); }\n    }\n    const foo = rpc.handle(new Foo());\n    const result = await foo.sum([1, 2, 3, 4, 5]);\n    expect(result).toBe(15);\n  });\n  it('call method with objects with handles', async(state, test) => {\n    class Foo {\n      async call(val) { return await val.a[0].name(); }\n      name() { return 'name'; }\n    }\n    const foo = rpc.handle(new Foo());\n    const result = await foo.call({a: [foo]});\n    expect(result).toBe('name');\n  });\n  it('call method with object with recursive link', async(state, test) => {\n    class Foo {\n      async call(val) { return await val.a[0].name(); }\n      name() { return 'name'; }\n    }\n    const foo = rpc.handle(new Foo());\n    const a = {};\n    a.a = a;\n    try {\n      await foo.call({a});\n    } catch (e) {\n      expect(e.message).toBe('Object reference chain is too long');\n    }\n  });\n  it('call method that does not exist', async(state, test) => {\n    class Foo {\n    }\n    const foo = rpc.handle(new Foo());\n    try {\n      await foo.sum(1, 3);\n      expect(true).toBeFalsy();\n    } catch (e) {\n      expect(e.toString()).toContain('There is no member');\n    }\n  });\n  it('call private method', async(state, test) => {\n    const foo = rpc.handle({});\n    try {\n      await foo._sum(1, 3);\n      expect(true).toBeFalsy();\n    } catch (e) {\n      expect(e.toString()).toContain('Private members are not exposed over RPC');\n    }\n  });\n  it('call method exception', async(state, test) => {\n    class Foo {\n      sum(a, b) { return b + c; }\n    }\n    const foo = rpc.handle(new Foo());\n    try {\n      await foo.sum(1, 3);\n      expect(true).toBeFalsy();\n    } catch (e) {\n      expect(e.toString()).toContain('c is not defined');\n    }\n  });\n  it('call nested exception', async(state, test) => {\n    class Foo {\n      sum(a, b) { return rpc.handle(this).doSum(a, b); }\n      doSum(a, b) { return b + c; }\n    }\n    const foo = rpc.handle(new Foo());\n    try {\n      await foo.sum(1, 3);\n      expect(true).toBeFalsy();\n    } catch (e) {\n      expect(e.toString()).toContain('c is not defined');\n    }\n  });\n  it('handle to function', async(state, test) => {\n    class Foo {\n      call(callback) { return callback(); }\n    }\n    const foo = rpc.handle(new Foo());\n    let calls = 0;\n    await foo.call(rpc.handle(() => ++calls));\n    expect(calls).toBe(1);\n  });\n  it('handle to function exception', async(state, test) => {\n    class Foo {\n      call(callback) { return callback(); }\n    }\n    const foo = rpc.handle(new Foo());\n    const calls = 0;\n    try {\n      await foo.call(rpc.handle(() => ++calls));\n      expect(true).toBeFalsy();\n    } catch (e) {\n      expect(e.toString()).toContain('Assignment to constant');\n    }\n  });\n  it('access property', async(state, test) => {\n    const foo = rpc.handle({ value: 'Hello wold' });\n    expect(await foo.value()).toBe('Hello wold');\n  });\n  it('access property with params', async(state, test) => {\n    const foo = rpc.handle({ value: 'Hello wold' });\n    try {\n      expect(await foo.value(1)).toBe('Hello wold');\n      expect(true).toBeFalsy();\n    } catch (e) {\n      expect(e.toString()).toContain('is not a function');\n    }\n  });\n  it('materialize handle', async(state, test) => {\n    const object = {};\n    const handle = rpc.handle(object);\n    expect(rpc.object(handle) === object).toBeTruthy();\n  });\n  it('access disposed handle', async(state, test) => {\n    class Foo {\n      sum(a, b) { return b + c; }\n    }\n    const foo = rpc.handle(new Foo());\n    rpc.dispose(foo);\n    try {\n      await foo.sum(1, 2);\n      expect(true).toBeFalsy();\n    } catch (e) {\n      expect(e.toString()).toContain('Object has been diposed');\n    }\n  });\n  it('dedupe implicit handles in the same world', async(state, test) => {\n    let foo2;\n    class Foo { foo(f) { foo2 = f; }}\n    const foo = rpc.handle(new Foo());\n    await foo.foo(foo);\n    expect(foo === foo2).toBeTruthy();\n  });\n  it('handle to handle should throw', async(state, test) => {\n    const handle = rpc.handle({});\n    try {\n      rpc.handle(handle);\n      expect(true).toBeFalsy();\n    } catch (e) {\n      expect(e.toString()).toContain('Can not return handle to handle');\n    }\n  });\n  it('parent / child communication', async(state, test) => {\n    const messages = [];\n    class Root { hello(message) { messages.push(message); } }\n    const root = rpc.handle(new Root());\n    await createChildWorld(rpc, p => p.hello('one'), root);\n    await createChildWorld(rpc, p => p.hello('two'), root);\n    expect(messages.join(',')).toBe('one,two');\n  });\n  it('parent / grand child communication', async(state, test) => {\n    const messages = [];\n    class Root { hello(message) { messages.push(message); } }\n    const root = rpc.handle(new Root());\n    await createChildWorld(rpc, async(p, r) => {\n      await createChildWorld(r, p => p.hello('one'), p);\n    }, root);\n    expect(messages.join(',')).toBe('one');\n  });\n  it('child / child communication', async(state, test) => {\n    const messages = [];\n    class Parent {\n      constructor() { this.children_ = []; }\n      addChild(child) {\n        this.children_.forEach(c => { c.setSibling(child); child.setSibling(c); });\n        this.children_.push(child);\n      }\n    }\n    class Child {\n      constructor() {}\n      setSibling(sibling) {\n        sibling.helloSibling('hello');\n      }\n      helloSibling(message) {\n        messages.push(message);\n      }\n    }\n    const parent = rpc.handle(new Parent());\n    await createChildWorld(rpc, (p, r) => p.addChild(r.handle(new Child())), parent);\n    await createChildWorld(rpc, (p, r) => p.addChild(r.handle(new Child())), parent);\n    await new Promise(f => setTimeout(f, 0));\n    await new Promise(f => setTimeout(f, 0));\n    expect(messages.join(',')).toBe('hello,hello');\n  });\n  it('dispose world', async(state, test) => {\n    const messages = [];\n    class Root { hello(message) { messages.push(message); } }\n    const root = rpc.handle(new Root());\n    let childRoot;\n    const childRpc = await createChildWorld(rpc, r => childRoot = r, root);\n    childRoot.hello('hello');\n\n    await new Promise(f => setTimeout(f, 0));\n    rpc.disposeWorld(childRpc.worldId_);\n\n    childRoot.hello('hello');\n    await new Promise(f => setTimeout(f, 0));\n\n    expect(messages.join(',')).toBe('hello');\n  });\n  it('dispose world half way', async(state, test) => {\n    const messages = [];\n    let go;\n    class Root {\n      hello(message) { messages.push(message); return new Promise(f => go = f); }\n    }\n    const root = rpc.handle(new Root());\n    let childRoot;\n    const childRpc = await createChildWorld(rpc, r => childRoot = r, root);\n    childRoot.hello('hello').then(() => messages.push('should-not-happen'));\n    await new Promise(f => setTimeout(f, 0));\n    rpc.disposeWorld(childRpc.worldId_);\n    go();\n    await new Promise(f => setTimeout(f, 0));\n    await new Promise(f => setTimeout(f, 0));\n    expect(messages.join(',')).toBe('hello');\n  });\n});\n\n// Reporter subscribes to TestRunner events and displays information in terminal\nnew Reporter(runner);\n\n// Run all tests.\nrunner.run();\n"
  },
  {
    "path": "test/app.spec.js",
    "content": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst path = require('path');\n\nmodule.exports.addTests = function({testRunner, expect}) {\n\n  const {describe, xdescribe, fdescribe} = testRunner;\n  const {it, fit, xit} = testRunner;\n  const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;\n  const carlo = require('../lib/carlo');\n  const {rpc} = require('../rpc');\n\n  let app;\n\n  function staticHandler(data) {\n    return request => {\n      for (const entry of data) {\n        const url = new URL(request.url());\n        if (url.pathname === entry[0]) {\n          request.fulfill({ body: Buffer.from(entry[1]), headers: entry[2]});\n          return;\n        }\n      }\n      request.continue();\n    };\n  }\n\n  afterEach(async({server, httpsServer}) => {\n    try { await app.exit(); } catch (e) {}\n  });\n\n  describe('app basics', () => {\n    it('evaluate', async() => {\n      app = await carlo.launch();\n      const ua = await app.evaluate('navigator.userAgent');\n      expect(ua).toContain('HeadlessChrome');\n    });\n    it('exposeFunction', async() => {\n      app = await carlo.launch();\n      await app.exposeFunction('foobar', () => 42);\n      const result = await app.evaluate('foobar()');\n      expect(result).toBe(42);\n    });\n    it('app load', async() => {\n      app = await carlo.launch();\n      await app.load('data:text/plain,hello');\n      const result = await app.evaluate('document.body.textContent');\n      expect(result).toBe('hello');\n    });\n    it('mainWindow accessor', async() => {\n      app = await carlo.launch();\n      app.serveFolder(path.join(__dirname, 'folder'));\n      await app.load('index.html');\n      expect(app.mainWindow().pageForTest().url()).toBe('https://domain/index.html');\n    });\n    it('createWindow creates window', async() => {\n      app = await carlo.launch();\n      let window = await app.createWindow();\n      expect(window.pageForTest().url()).toBe('about:blank?seq=1');\n      window = await app.createWindow();\n      expect(window.pageForTest().url()).toBe('about:blank?seq=2');\n    });\n    it('exit event is emitted', async() => {\n      app = await carlo.launch();\n      let callback;\n      const onexit = new Promise(f => callback = f);\n      app.on('exit', callback);\n      await app.mainWindow().close();\n      await onexit;\n    });\n    it('window event is emitted', async() => {\n      app = await carlo.launch();\n      const windows = [];\n      app.on('window', window => windows.push(window));\n      const window1 = await app.createWindow();\n      const window2 = await app.createWindow();\n      expect(window1).toBe(windows[0]);\n      expect(window2).toBe(windows[1]);\n    });\n    it('window exposeFunction', async() => {\n      app = await carlo.launch();\n      await app.exposeFunction('appFunc', () => 'app');\n      const w1 = await app.createWindow();\n      await w1.exposeFunction('windowFunc', () => 'window');\n      const result1 = await w1.evaluate(async() => (await appFunc()) + (await windowFunc()));\n      expect(result1).toBe('appwindow');\n\n      const w2 = await app.createWindow();\n      const result2 = await w2.evaluate(async() => (await appFunc()) + self.windowFunc);\n      expect(result2).toBe('appundefined');\n    });\n  });\n\n  describe('http serve', () => {\n    it('serveFolder works', async() => {\n      app = await carlo.launch();\n      app.serveFolder(path.join(__dirname, 'folder'));\n      await app.load('index.html');\n      const result = await app.evaluate('document.body.textContent');\n      expect(result).toBe('hello file');\n    });\n    it('serveFolder prefix is respected works', async() => {\n      app = await carlo.launch();\n      app.serveFolder(path.join(__dirname, 'folder'), 'prefix');\n      await app.load('prefix/index.html');\n      const result = await app.evaluate('document.body.textContent');\n      expect(result).toBe('hello file');\n    });\n    it('serveOrigin works', async({server}) => {\n      app = await carlo.launch();\n      app.serveOrigin(server.PREFIX);\n      await app.load('index.html');\n      const result = await app.evaluate('document.body.textContent');\n      expect(result).toBe('hello http');\n    });\n    it('serveOrigin prefix is respected', async({server}) => {\n      app = await carlo.launch();\n      app.serveOrigin(server.PREFIX, 'prefix');\n      await app.load('prefix/index.html');\n      const result = await app.evaluate('document.body.textContent');\n      expect(result).toBe('hello http');\n    });\n    it('HttpRequest params', async() => {\n      app = await carlo.launch();\n      app.serveFolder(path.join(__dirname, 'folder'));\n      const log = [];\n      app.serveHandler(request => {\n        log.push({url: request.url(), method: request.method(), ua: ('User-Agent' in request.headers()) });\n        request.continue();\n      });\n      await app.load('index.html');\n      expect(JSON.stringify(log)).toBe('[{\"url\":\"https://domain/index.html\",\"method\":\"GET\",\"ua\":true}]');\n    });\n    it('serveHandler can fulfill', async() => {\n      app = await carlo.launch();\n      app.serveHandler(request => {\n        if (!request.url().endsWith('index.html')) {\n          request.continue();\n          return;\n        }\n        request.fulfill({ body: Buffer.from('hello handler') });\n      });\n      await app.load('index.html');\n      const result = await app.evaluate('document.body.textContent');\n      expect(result).toBe('hello handler');\n    });\n    it('serveHandler can continue to file', async() => {\n      app = await carlo.launch();\n      app.serveHandler(request => request.continue());\n      app.serveFolder(path.join(__dirname, 'folder'));\n      await app.load('index.html');\n      const result = await app.evaluate('document.body.textContent');\n      expect(result).toBe('hello file');\n    });\n    it('serveHandler can continue to http', async({server}) => {\n      app = await carlo.launch();\n      app.serveOrigin(server.PREFIX);\n      app.serveHandler(request => request.continue());\n      await app.load('index.html');\n      const result = await app.evaluate('document.body.textContent');\n      expect(result).toBe('hello http');\n    });\n    xit('serveHandler can abort', async() => {\n      app = await carlo.launch();\n      app.serveHandler(request => request.abort());\n      try {\n        await app.load('index.html');\n        expect(false).toBeTruthy();\n      } catch (e) {\n        expect(e.toString()).toContain('domain/index.html');\n      }\n    });\n    it('window serveFolder', async() => {\n      app = await carlo.launch();\n\n      const w1 = await app.createWindow();\n      await w1.serveFolder(path.join(__dirname, 'folder'));\n      await w1.load('index.html');\n      const result1 = await w1.evaluate('document.body.textContent');\n      expect(result1).toBe('hello file');\n\n      const w2 = await app.createWindow();\n      try {\n        await w2.load('index.html');\n        expect(false).toBeTruthy();\n      } catch (e) {\n        expect(e.toString()).toContain('domain/index.html');\n      }\n    });\n    it('navigation history is empty', async() => {\n      app = await carlo.launch({ channel: ['canary'] });\n      app.serveFolder(path.join(__dirname, 'folder'));\n      await app.load('index.html?1');\n      await app.load('index.html?2');\n      await app.load('index.html?3');\n      expect(await app.evaluate('history.length')).toBe(1);\n    });\n    it('fail navigation', async() => {\n      app = await carlo.launch();\n      app.serveFolder(path.join(__dirname, 'folder'));\n      app.serveHandler(async request => {\n        request.url() === 'https://domain/index.html' ? request.fail() : request.continue();\n      });\n      await app.load('redirect.html');\n      expect(await app.evaluate(`window.location.href`)).toBe('chrome-error://chromewebdata/');\n    });\n    it('abort navigation', async() => {\n      app = await carlo.launch();\n      app.serveFolder(path.join(__dirname, 'folder'));\n      app.serveHandler(async request => {\n        request.url() === 'https://domain/index.html' ? request.abort() : request.continue();\n      });\n      await app.load('redirect.html');\n      expect(await app.evaluate(`window.location.href`)).toBe('https://domain/redirect.html');\n    });\n  });\n\n  describe('features', () => {\n    it('carlo.fileInfo', async() => {\n      const files = [[\n        '/index.html', `\n        <script>\n        async function check() {\n          const input = document.getElementById('file');\n          const info = await self.carlo.fileInfo(input.files[0]);\n          checkFileInfo(info);\n        }\n        </script>\n        <body><input type=\"file\" id=\"file\"></body>`\n      ]];\n      app = await carlo.launch();\n      app.serveHandler(staticHandler(files));\n\n      let callback;\n      const result = new Promise(f => callback = f);\n      app.exposeFunction('checkFileInfo', callback);\n\n      await app.load('index.html');\n      const page = app.mainWindow().pageForTest();\n      const element = await page.evaluateHandle(`document.getElementById('file')`);\n      await element.uploadFile(__filename);\n      app.evaluate('check()');\n      const info = await result;\n      expect(info.path).toBe(__filename);\n    });\n  });\n\n  describe('rpc', () => {\n    it('load params are accessible', async() => {\n      const files = [[\n        '/index.html',\n        `<script>async function run() {\n           const [a, b] = await carlo.loadParams();\n           b.print(await a.val());\n         }\n         </script>\n         <body onload='run()'></body>`\n      ]];\n      app = await carlo.launch();\n      app.serveHandler(staticHandler(files));\n      let callback;\n      const result = new Promise(f => callback = f);\n      await app.load('index.html',\n          rpc.handle({ val: 42 }),\n          rpc.handle({ print: v => callback(v) }));\n      expect(await result).toBe(42);\n      // Allow b.print to dispatch.\n      await new Promise(f => setTimeout(f, 0));\n    });\n    it('load params are accessible after reload', async() => {\n      const files = [[\n        '/index.html',\n        `<script>async function run() {\n           if (!window.location.search) {\n             setTimeout(() => {\n               window.location.href += '?reload';\n             }, 0);\n             return;\n           }\n           const [a, b] = await carlo.loadParams();\n           b.print(await a.val());\n         }\n         </script>\n         <body onload='run()'></body>`\n      ]];\n      app = await carlo.launch();\n      app.serveHandler(staticHandler(files));\n      let callback;\n      const result = new Promise(f => callback = f);\n      await app.load('index.html',\n          rpc.handle({ val: 42 }),\n          rpc.handle({ print: v => callback(v) }));\n      expect(await result).toBe(42);\n      // Allow b.print to dispatch.\n      await new Promise(f => setTimeout(f, 0));\n    });\n  });\n\n};\n"
  },
  {
    "path": "test/color.spec.js",
    "content": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nmodule.exports.addTests = function({testRunner, expect}) {\n\n  const {describe, xdescribe, fdescribe} = testRunner;\n  const {it, fit, xit} = testRunner;\n  const {Color} = require('../lib/color');\n\n  describe('color', () => {\n    it('rgb1', async(state, test) => {\n      color = Color.parse('rgb(94, 126, 91)');\n      expect(color.asString(Color.Format.RGB)).toBe('rgb(94, 126, 91)');\n      expect(color.asString(Color.Format.RGBA)).toBe('rgba(94, 126, 91, 1)');\n      expect(color.asString(Color.Format.HSL)).toBe('hsl(115, 16%, 43%)');\n      expect(color.asString(Color.Format.HSLA)).toBe('hsla(115, 16%, 43%, 1)');\n      expect(color.asString(Color.Format.HEXA)).toBe('#5e7e5bff');\n      expect(color.asString(Color.Format.HEX)).toBe('#5e7e5b');\n      expect(color.asString(Color.Format.ShortHEXA)).toBe(null);\n      expect(color.asString(Color.Format.ShortHEX)).toBe(null);\n      expect(color.asString()).toBe('rgb(94, 126, 91)');\n    });\n    it('rgb2', async(state, test) => {\n      color = Color.parse('rgba(94 126 91)');\n      expect(color.asString(Color.Format.RGB)).toBe('rgba(94 126 91)');\n      expect(color.asString(Color.Format.RGBA)).toBe('rgba(94, 126, 91, 1)');\n      expect(color.asString(Color.Format.HSL)).toBe('hsl(115, 16%, 43%)');\n      expect(color.asString(Color.Format.HSLA)).toBe('hsla(115, 16%, 43%, 1)');\n      expect(color.asString(Color.Format.HEXA)).toBe('#5e7e5bff');\n      expect(color.asString(Color.Format.HEX)).toBe('#5e7e5b');\n      expect(color.asString(Color.Format.ShortHEXA)).toBe(null);\n      expect(color.asString(Color.Format.ShortHEX)).toBe(null);\n      expect(color.asString()).toBe('rgb(94, 126, 91)');\n    });\n    it('rgb3', async(state, test) => {\n      color = Color.parse('rgba(94, 126, 91, 0.5)');\n      expect(color.asString(Color.Format.RGB)).toBe(null);\n      expect(color.asString(Color.Format.RGBA)).toBe('rgba(94, 126, 91, 0.5)');\n      expect(color.asString(Color.Format.HSL)).toBe(null);\n      expect(color.asString(Color.Format.HSLA)).toBe('hsla(115, 16%, 43%, 0.5)');\n      expect(color.asString(Color.Format.HEXA)).toBe('#5e7e5b80');\n      expect(color.asString(Color.Format.HEX)).toBe(null);\n      expect(color.asString(Color.Format.ShortHEXA)).toBe(null);\n      expect(color.asString(Color.Format.ShortHEX)).toBe(null);\n      expect(color.asString()).toBe('rgba(94, 126, 91, 0.5)');\n    });\n    it('rgb4', async(state, test) => {\n      color = Color.parse('rgb(94 126 91 / 50%)');\n      expect(color.asString(Color.Format.RGB)).toBe(null);\n      expect(color.asString(Color.Format.RGBA)).toBe('rgb(94 126 91 / 50%)');\n      expect(color.asString(Color.Format.HSL)).toBe(null);\n      expect(color.asString(Color.Format.HSLA)).toBe('hsla(115, 16%, 43%, 0.5)');\n      expect(color.asString(Color.Format.HEXA)).toBe('#5e7e5b80');\n      expect(color.asString(Color.Format.HEX)).toBe(null);\n      expect(color.asString(Color.Format.ShortHEXA)).toBe(null);\n      expect(color.asString(Color.Format.ShortHEX)).toBe(null);\n      expect(color.asString()).toBe('rgba(94, 126, 91, 0.5)');\n    });\n    it('hsl1', async(state, test) => {\n      color = Color.parse('hsl(212, 55%, 32%)');\n      expect(color.asString(Color.Format.RGB)).toBe('rgb(37, 79, 126)');\n      expect(color.asString(Color.Format.RGBA)).toBe('rgba(37, 79, 126, 1)');\n      expect(color.asString(Color.Format.HSL)).toBe('hsl(212, 55%, 32%)');\n      expect(color.asString(Color.Format.HSLA)).toBe('hsla(212, 55%, 32%, 1)');\n      expect(color.asString(Color.Format.HEXA)).toBe('#254f7eff');\n      expect(color.asString(Color.Format.HEX)).toBe('#254f7e');\n      expect(color.asString(Color.Format.ShortHEXA)).toBe(null);\n      expect(color.asString(Color.Format.ShortHEX)).toBe(null);\n      expect(color.asString()).toBe('hsl(212, 55%, 32%)');\n    });\n    it('hsl2', async(state, test) => {\n      color = Color.parse('hsla(212 55% 32%)');\n      expect(color.asString(Color.Format.RGB)).toBe('rgb(37, 79, 126)');\n      expect(color.asString(Color.Format.RGBA)).toBe('rgba(37, 79, 126, 1)');\n      expect(color.asString(Color.Format.HSL)).toBe('hsla(212 55% 32%)');\n      expect(color.asString(Color.Format.HSLA)).toBe('hsla(212, 55%, 32%, 1)');\n      expect(color.asString(Color.Format.HEXA)).toBe('#254f7eff');\n      expect(color.asString(Color.Format.HEX)).toBe('#254f7e');\n      expect(color.asString(Color.Format.ShortHEXA)).toBe(null);\n      expect(color.asString(Color.Format.ShortHEX)).toBe(null);\n      expect(color.asString()).toBe('hsl(212, 55%, 32%)');\n    });\n    it('hsl3', async(state, test) => {\n      color = Color.parse('hsla(212, 55%, 32%, 0.5)');\n      expect(color.asString(Color.Format.RGB)).toBe(null);\n      expect(color.asString(Color.Format.RGBA)).toBe('rgba(37, 79, 126, 0.5)');\n      expect(color.asString(Color.Format.HSL)).toBe(null);\n      expect(color.asString(Color.Format.HSLA)).toBe('hsla(212, 55%, 32%, 0.5)');\n      expect(color.asString(Color.Format.HEXA)).toBe('#254f7e80');\n      expect(color.asString(Color.Format.HEX)).toBe(null);\n      expect(color.asString(Color.Format.ShortHEXA)).toBe(null);\n      expect(color.asString(Color.Format.ShortHEX)).toBe(null);\n      expect(color.asString()).toBe('hsla(212, 55%, 32%, 0.5)');\n    });\n    it('hsl4', async(state, test) => {\n      color = Color.parse('hsla(212  55%  32% /  50%)');\n      expect(color.asString(Color.Format.RGB)).toBe(null);\n      expect(color.asString(Color.Format.RGBA)).toBe('rgba(37, 79, 126, 0.5)');\n      expect(color.asString(Color.Format.HSL)).toBe(null);\n      expect(color.asString(Color.Format.HSLA)).toBe('hsla(212  55%  32% /  50%)');\n      expect(color.asString(Color.Format.HEXA)).toBe('#254f7e80');\n      expect(color.asString(Color.Format.HEX)).toBe(null);\n      expect(color.asString(Color.Format.ShortHEXA)).toBe(null);\n      expect(color.asString(Color.Format.ShortHEX)).toBe(null);\n      expect(color.asString()).toBe('hsla(212, 55%, 32%, 0.5)');\n    });\n    it('hsl5', async(state, test) => {\n      color = Color.parse('hsla(212deg 55% 32% / 50%)');\n      expect(color.asString(Color.Format.RGB)).toBe(null);\n      expect(color.asString(Color.Format.RGBA)).toBe('rgba(37, 79, 126, 0.5)');\n      expect(color.asString(Color.Format.HSL)).toBe(null);\n      expect(color.asString(Color.Format.HSLA)).toBe('hsla(212deg 55% 32% / 50%)');\n      expect(color.asString(Color.Format.HEXA)).toBe('#254f7e80');\n      expect(color.asString(Color.Format.HEX)).toBe(null);\n      expect(color.asString(Color.Format.ShortHEXA)).toBe(null);\n      expect(color.asString(Color.Format.ShortHEX)).toBe(null);\n      expect(color.asString()).toBe('hsla(212, 55%, 32%, 0.5)');\n    });\n    it('hex1', async(state, test) => {\n      color = Color.parse('#12345678');\n      expect(color.asString(Color.Format.RGB)).toBe(null);\n      expect(color.asString(Color.Format.RGBA)).toBe('rgba(18, 52, 86, 0.47058823529411764)');\n      expect(color.asString(Color.Format.HSL)).toBe(null);\n      expect(color.asString(Color.Format.HSLA)).toBe('hsla(210, 65%, 20%, 0.47058823529411764)');\n      expect(color.asString(Color.Format.HEXA)).toBe('#12345678');\n      expect(color.asString(Color.Format.HEX)).toBe(null);\n      expect(color.asString(Color.Format.ShortHEXA)).toBe(null);\n      expect(color.asString(Color.Format.ShortHEX)).toBe(null);\n      expect(color.asString()).toBe('#12345678');\n    });\n    it('hex2', async(state, test) => {\n      color = Color.parse('#00FFFF');\n      expect(color.asString(Color.Format.RGB)).toBe('rgb(0, 255, 255)');\n      expect(color.asString(Color.Format.RGBA)).toBe('rgba(0, 255, 255, 1)');\n      expect(color.asString(Color.Format.HSL)).toBe('hsl(180, 100%, 50%)');\n      expect(color.asString(Color.Format.HSLA)).toBe('hsla(180, 100%, 50%, 1)');\n      expect(color.asString(Color.Format.HEXA)).toBe('#00ffffff');\n      expect(color.asString(Color.Format.HEX)).toBe('#00FFFF');\n      expect(color.asString(Color.Format.ShortHEXA)).toBe('#0fff');\n      expect(color.asString(Color.Format.ShortHEX)).toBe('#0ff');\n      expect(color.asString()).toBe('#00ffff');\n    });\n    it('hex3', async(state, test) => {\n      color = Color.parse('#1234');\n      expect(color.asString(Color.Format.RGB)).toBe(null);\n      expect(color.asString(Color.Format.RGBA)).toBe('rgba(17, 34, 51, 0.26666666666666666)');\n      expect(color.asString(Color.Format.HSL)).toBe(null);\n      expect(color.asString(Color.Format.HSLA)).toBe('hsla(210, 50%, 13%, 0.26666666666666666)');\n      expect(color.asString(Color.Format.HEXA)).toBe('#11223344');\n      expect(color.asString(Color.Format.HEX)).toBe(null);\n      expect(color.asString(Color.Format.ShortHEXA)).toBe('#1234');\n      expect(color.asString(Color.Format.ShortHEX)).toBe(null);\n      expect(color.asString()).toBe('#1234');\n    });\n    it('hex4', async(state, test) => {\n      color = Color.parse('#0FF');\n      expect(color.asString(Color.Format.RGB)).toBe('rgb(0, 255, 255)');\n      expect(color.asString(Color.Format.RGBA)).toBe('rgba(0, 255, 255, 1)');\n      expect(color.asString(Color.Format.HSL)).toBe('hsl(180, 100%, 50%)');\n      expect(color.asString(Color.Format.HSLA)).toBe('hsla(180, 100%, 50%, 1)');\n      expect(color.asString(Color.Format.HEXA)).toBe('#00ffffff');\n      expect(color.asString(Color.Format.HEX)).toBe('#00ffff');\n      expect(color.asString(Color.Format.ShortHEXA)).toBe('#0fff');\n      expect(color.asString(Color.Format.ShortHEX)).toBe('#0FF');\n      expect(color.asString()).toBe('#0ff');\n    });\n  });\n\n};\n"
  },
  {
    "path": "test/folder/index.html",
    "content": "hello file"
  },
  {
    "path": "test/folder/redirect.html",
    "content": "<script>\n  window.location = 'https://domain/index.html';\n</script>\n"
  },
  {
    "path": "test/headful.js",
    "content": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst {TestRunner, Reporter, Matchers} = require('@pptr/testrunner');\n\nconst path = require('path');\nconst carlo = require('../lib/carlo');\n\n// Runner holds and runs all the tests\nconst testRunner = new TestRunner({\n  parallel: 1, // run 2 parallel threads\n  timeout: 3000, // setup timeout of 1 second per test\n});\nconst {expect} = new Matchers();\nconst {beforeAll, beforeEach, afterAll, afterEach} = testRunner;\nconst {describe, xdescribe, fdescribe} = testRunner;\nconst {it, fit, xit} = testRunner;\n\ndescribe('app reuse', () => {\n  fit('load returns value', async() => {\n    app = await carlo.launch();\n    let callback;\n    const windowPromise = new Promise(f => callback = f);\n    app.on('window', callback);\n\n    try {\n      await carlo.launch({paramsForReuse: {val: 42}});\n      expect(false).toBeTruthy();\n    } catch (e) {\n      expect(e.toString()).toContain('already running');\n    }\n\n    const window = await windowPromise;\n    expect(JSON.stringify(window.paramsForReuse())).toBe('{\"val\":42}');\n  });\n});\n\n// Reporter subscribes to TestRunner events and displays information in terminal\nnew Reporter(testRunner);\n\n// Run all tests.\ntestRunner.run();\n"
  },
  {
    "path": "test/http/index.html",
    "content": "hello http"
  },
  {
    "path": "test/test.js",
    "content": "/**\n * Copyright 2018 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst {TestRunner, Reporter, Matchers} = require('@pptr/testrunner');\nconst {TestServer} = require('@pptr/testserver');\n\nconst path = require('path');\nconst carlo = require('../lib/carlo');\ncarlo.enterTestMode();\n\n// Runner holds and runs all the tests\nconst testRunner = new TestRunner({\n  parallel: 1, // run 2 parallel threads\n  timeout: 3000, // setup timeout of 1 second per test\n});\nconst {expect} = new Matchers();\nconst {beforeAll, beforeEach, afterAll, afterEach} = testRunner;\n\nbeforeAll(async state => {\n  const assetsPath = path.join(__dirname, 'http');\n\n  const port = 8907 + state.parallelIndex * 2;\n  state.server = await TestServer.create(assetsPath, port);\n  state.server.PORT = port;\n  state.server.PREFIX = `http://localhost:${port}`;\n\n  const httpsPort = port + 1;\n  state.httpsServer = await TestServer.createHTTPS(assetsPath, httpsPort);\n  state.httpsServer.PORT = httpsPort;\n  state.httpsServer.PREFIX = `https://localhost:${httpsPort}`;\n});\n\nafterAll(async({server, httpsServer}) => {\n  await Promise.all([\n    server.stop(),\n    httpsServer.stop(),\n  ]);\n});\n\nbeforeEach(async({server, httpsServer}) => {\n  server.reset();\n  httpsServer.reset();\n});\n\nrequire('./app.spec.js').addTests({testRunner, expect});\nrequire('./color.spec.js').addTests({testRunner, expect});\n\n// Reporter subscribes to TestRunner events and displays information in terminal\nnew Reporter(testRunner);\n\n// Run all tests.\ntestRunner.run();\n"
  }
]