{"id":21301713,"url":"https://github.com/funbox/phantom-lord","last_synced_at":"2025-07-11T20:31:16.837Z","repository":{"id":41587885,"uuid":"307661241","full_name":"funbox/phantom-lord","owner":"funbox","description":"Handy API for Headless Chromium","archived":false,"fork":false,"pushed_at":"2023-07-19T00:09:20.000Z","size":748,"stargazers_count":26,"open_issues_count":1,"forks_count":5,"subscribers_count":10,"default_branch":"master","last_synced_at":"2025-07-02T07:47:36.044Z","etag":null,"topics":["casperjs","e2e-tests","headless-chrome","phantomjs","puppeteer"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/funbox.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-10-27T10:20:30.000Z","updated_at":"2024-12-16T01:55:32.000Z","dependencies_parsed_at":"2024-11-15T22:03:02.515Z","dependency_job_id":"75fd82d1-1dfb-47fd-8766-823466dc7144","html_url":"https://github.com/funbox/phantom-lord","commit_stats":{"total_commits":266,"total_committers":26,"mean_commits":10.23076923076923,"dds":0.6616541353383458,"last_synced_commit":"e42fdc22802f2be9ca3a06ddcb0a9cfa9b5fe9d9"},"previous_names":[],"tags_count":87,"template":false,"template_full_name":null,"purl":"pkg:github/funbox/phantom-lord","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/funbox%2Fphantom-lord","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/funbox%2Fphantom-lord/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/funbox%2Fphantom-lord/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/funbox%2Fphantom-lord/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/funbox","download_url":"https://codeload.github.com/funbox/phantom-lord/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/funbox%2Fphantom-lord/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264892156,"owners_count":23679241,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["casperjs","e2e-tests","headless-chrome","phantomjs","puppeteer"],"created_at":"2024-11-21T15:50:30.703Z","updated_at":"2025-07-11T20:31:11.825Z","avatar_url":"https://github.com/funbox.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @funboxteam/phantom-lord\n\n\u003cimg align=\"right\" width=\"192\" height=\"192\"\n     alt=\"Phantom Lord avatar: Golden shield with a crowned phantom face on a black background\"\n     src=\"./logo.png\"\u003e\n\n[![npm](https://img.shields.io/npm/v/@funboxteam/phantom-lord.svg)](https://www.npmjs.com/package/@funboxteam/phantom-lord)\n\nHandy API for [Headless Chromium](https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md), \ninspired by [CasperJS](http://casperjs.org/).\n\nUseful for automated testing, creating website scrapers, and other tasks that require virtual browser.\n\n[По-русски](./README.ru.md)  \n\n## Rationale\n\nThere's a library allowing to write tests on Node.js and run them in a virtual browser — \n[Selenium](http://www.seleniumhq.org/). But from our point of view, it has two issues:  \n\n1. It's written in Java.\n2. Virtual browser does not always work well. \n\nIn case of any problems it's required to know three programming languages and their tools (Node.js, Java, C++),\notherwise it's hard to debug and takes too much time to solve them.\n\nTrying to solve these issues we'd written our own library — Phantom Lord.\n\n## Features\n\nHeadless Chromium is used as a virtual browser, which makes pages look the same as in the usual Chrome with a GUI.\n\n[Puppeteer](https://developers.google.com/web/tools/puppeteer/) is used to control the browser. Unlike CasperJS or \nPhantomJS all the commands are evaluated in Node.js, which allows developers to use ES2015 and other new features of JS,\nas well as any libraries written for Node.js. \n\n## Installation\n\n```bash\nnpm install --save-dev @funboxteam/phantom-lord\n```\n\n## Usage\n\nRequire the library:\n\n```js\nconst Browser = require('@funboxteam/phantom-lord');\n```\n\nCreate an instance of the browser, setup error handlers and run:\n\n```js\nconst browser = new Browser();\nbrowser.on('timeout', () =\u003e console.log('browser timeout!'));\nbrowser.on('error', () =\u003e console.log('browser error!'));\nawait browser.startRemoteBrowser();\n```\n\nNow you're able to run commands:\n\n```javascript\nawait browser.open('https://google.com');\nawait browser.waitForText('Google Search');\nawait browser.sendKeys('input[type=\"text\"]', 'hello');\nawait browser.click('input[value=\"Google Search\"]');\nawait browser.waitForUrl('google.com/search');\nawait browser.waitForText('results');\n```\n\nSince the library is just an API for interacting with Headless Chromium, additional tools should be used \nto write E2E tests. E.g. [Mocha](https://mochajs.org/) or \n[@funboxteam/frontend-tests-runner](https://github.com/funbox/frontend-tests-runner). \n\n\u003cdetails\u003e\n  \u003csummary\u003eExample of Mocha \u0026 Phantom Lord integration\u003c/summary\u003e\n  \n  ```js\n  const Browser = require('@funboxteam/phantom-lord');\n  let browser;\n  let restartReason;\n  let test;\n  \n  describe('should test google.com', function() {\n    // Do not use arrow fn here to allow Mocha to mock `this`\n    before(async function() {\n      browser = new Browser();\n  \n      browser.on('timeout', (e) =\u003e {\n        console.log('e2e-tests timeout!');\n\n        // Fail the test in case of timeout\n        test.callback(e);\n      });\n  \n      browser.on('error', (e) =\u003e {\n        console.log('e2e-tests error!');\n\n        // Fail the test in case of browser command error\n        test.callback(new Error(e)); \n      });\n  \n      // Handle Phantom Lord internal error (e.g. Chromium crash)\n      browser.on('phantomError', (e) =\u003e {\n        if (browser.testAlreadyFailed) {\n          console.log('Chromium error has occurred inside `afterEach`. Failing.');\n        } else {\n          console.log('Chromium error has occurred. Restarting the test.');\n          test.currentRetry(0);\n          test.retries(1);\n          restartReason = 'phantomError';\n          test.callback(new Error(e || 'Error'));\n        }\n      });\n  \n      // Handle Chromium exit\n      browser.on('exit', (code, signal) =\u003e {\n        if (browser.state === 'started' || browser.state === 'starting') {\n          console.log(`Unexpected Chromium exit with code '${code}' and signal '${signal}'. Restarting the test.`);\n          test.currentRetry(0);\n          test.retries(1);\n          restartReason = 'exit';\n          test.callback(new Error('Unexpected Chromium exit'));\n        }\n      });\n  \n      // Start the browser when all the handlers are set up\n      await browser.startRemoteBrowser();\n    });\n  \n    after(async function() {\n      // In the end we have to shut down the browser. Otherwise there will be zombie process.\n      await browser.exit();\n    });\n  \n    beforeEach(async function() {\n      test = this.currentTest;\n    });\n  \n    afterEach(async function() {\n      // In case of failing we can make a screenshot to help ourselves to debug\n      if (this.currentTest.state === 'failed') {\n        // If the test is failed because of the crash of Chromium it's useless to try to make a screenshot\n        if (browser.state !== 'started') {\n          console.log(`Not making a screenshot, because browser.state = ${browser.state}`);\n        } else {\n          let t = this.currentTest;\n          const p = [];\n          while (t) {\n            p.unshift(t.title);\n            t = t.parent;\n          }\n  \n          const time = new Date(parseInt(process.env.E2E_TESTS_START_TIMESTAMP, 10));\n          p.unshift(time.getTime());\n  \n          p.unshift('screenshots');\n          const fname = `${p.join('/')}.png`;\n          browser.testAlreadyFailed = true;\n  \n          await browser.capture(fname);\n        }\n      }\n  \n      // If the test has passed but there're still non-mocked requests then fail the test\n      if (browser.browserErrors.length \u003e 0 \u0026\u0026 this.currentTest.state !== 'failed') {\n        test.callback(new Error(browser.browserErrors[0].msg));\n      }\n  \n      // This command will close all the tabs, which leads to opening the new tab when `browser.open()` will be fired\n      await browser.closeAllPages();\n    });\n  \n    it('test 1', async () =\u003e {\n      await browser.open('https://google.com');\n      await browser.waitForText('Google Search');\n      await browser.sendKeys('input[type=\"text\"]', 'hello');\n      await browser.click('input[value=\"Google Search\"]');\n      await browser.waitForUrl('google.com/search');\n      await browser.waitForText('results'); // If this text won't be found on the page, the test will fail\n    });\n  \n    it('test 2', async () =\u003e {\n      await browser.open('https://google.com');\n      await browser.waitForText('Google Search');\n      await browser.sendKeys('input[type=\"text\"]', 'hello');\n      await browser.click('input[value=\"Google Search\"]');\n      await browser.waitForUrl('google.com/search');\n      await browser.waitForText('results'); // If this text won't be found on the page, the test will fail\n    });\n  });\n  ```\n  \n  Tabs management:\n  \n  ```js\n    it('should open link in a new tab', async () =\u003e {\n      await browser.open('https://google.com');\n\n      // Let's assume that click on this element will open a page in a new tab\n      await browser.click('[data-id=\"video\"]');\n  \n      // If the new tab won't be open, the test will fail\n      await browser.waitForTab(/google\\.com\\/video/);\n      // After the successful check the tab will be closed automatically\n    });\n  \n    it('should open link in a new tab and check it\\'s content', async () =\u003e {\n      await browser.open('https://google.com');\n      await browser.click('[data-id=\"video\"]');\n  \n      await browser.waitForTab(/google\\.com\\/video/, async () =\u003e {\n        // This check is evaluated on the page in the new tab\n        // If this text won't be found on the page in the new tab, the test will fail\n        await browser.waitForText('Videos');\n      });\n  \n      // This check is evaluated on the previous page in the previous tab\n      await browser.waitForText('Google Search');\n    });\n  ```\n\u003c/details\u003e\n\n## Commands\n\nThe list of available commands can be found in [lib/commands/index.js](./lib/commands/index.js).\n\n## Important things to know\n\n### Project root directory\n\nSome commands have to know the path to the project root. E.g. `capture` uses it to create a subdirectory for screenshots.\n\nTo find the project root directory Phantom Lord uses [app-root-path](https://www.npmjs.com/package/app-root-path) lib.\nAnd due to [some of its features](https://www.npmjs.com/package/app-root-path#primary-method) one should not store their\nproject in the directory named `node_modules` or anywhere in it's subdirectories.\n\n* Correct: `~/work/my-project/`.\n* Incorrect: `~/work/node_modules/my-project/`.\n\n### Launching the browser\n\n`browser.startRemoteBrowser()` is fired automatically when `browser.open()` is evaluated and the browser hadn't been\nlaunched.\n\nHowever, if one will try to run any command interacting with a page before launching the browser, they will get \n`notStarted` error.\n\n### Possible edge-cases of commands\n\n#### `sendKeys` \n\nWhen `sendKeys` is used to fill in an input with a mask, one should pass the third param (`caretPosition`) with `'start'`\nas a value. E.g.:\n\n```js\nawait browser.sendKeys('.text-field_masked input[type=text]', '9001234567', 'start');\n```\n\nUsually if an input has a mask implemented by some JS lib, then the lib sets `value` to the “empty mask” \n(e.g. `value=\"___ ___-__-__\"`) when input is focused. At the same time, default value of `caretPosition` is `'end'`,\nwhich means that the cursor will be placed after `___ ___-__-__`, and the passed text won't be entered, or will be \nentered incorrectly.\n\n### Events\n\nInstance of `RemoteBrowser` emits these events:\n\n* `error` — a critical error has occurred while evaluating a command;\n* `timeout` — command evaluation timeout has been reached;\n* `phantomError` — an error of sending command to Chromium has occurred\n  (usually it means that the process will crash soon);\n* `browserErrors` — JS errors have occurred on a page;\n* `exit` — Chromium has exited.\n\n`RemoteBrowser` inherits `EventEmitter`, thus to subscribe to events use `on`:\n\n```javascript\nbrowser.on('error', (e) =\u003e {\n  console.log(`Error: ${e}`);\n});\n```\n\n### States\n\nAt any moment of time `RemoteBrowser` instance may be in one of the following states:\n\n* `notStarted` — Chromium hasn't been started;\n* `starting` — Chromium is starting;\n* `started` — Chromium has been started and ready to evaluate commands (or evaluating them right now);\n* `error` — an error of sending command to Chromium has occurred, and the Chromium should be shut down;\n* `exiting` — Chromium is shutting down.\n\nUse `state` property to get the current state:\n\n```js\nconsole.log(`Current state: ${browser.state}`);\n```\n\n### Environment variables\n\n* `DEBUG` — boolean; turns on debug logging (sent commands, received replies, console messages, etc).\n* `BROWSER_ARGS` — string; allows to tune the browser. The value is JSON setting arguments for virtual browser launch. \n  It may contain the following keys:\n    * `viewportWidth` — number; width of the browser viewport (default: `1440`);\n    * `viewportHeight` — number; height of the browser viewport (default: `900`);\n    * `waitTimeout` — number; timeout for each waiting command (milliseconds) after which it will fail \n      in case of absence of the thing it is waiting for (default: `30000`);\n    * `slowMo` — number; slows evaluation of every command on the passed milliseconds (default: `0`). \n      The difference between this key and `E2E_TESTS_WITH_PAUSES` env var is the fact that `slowMo` affects all the actions\n      that work with the browser (clicks, navigation, data inputs, keys pressing, etc).\n    * `clearCookies` — boolean; clears browser cookies when creating a new page (default: `false`); \n* `E2E_TESTS_WITH_PAUSES` — boolean; increases the delay between waiting commands evaluation (`waitForUrl`, `waitForText`, etc).\n  It helps to find errors related to too fast checks evaluation.\n* `HEADLESS_OFF` — boolean; turns off Headless mode. The browser will launch with GUI, which will allow to see commands \n  evaluation and interact with it. It may be helpful in debug.\n\n### Stubs\n\nOne of the common tasks for E2E tests is to add stubs on a page. Phantom Lord can do it.\n\n#### addStubToQueue\n\nTo add the stubs use `addStubToQueue` function. It adds the passed subs to the array `window.stubs` on a page.\n\nThe function may be fired even before page loading. In this case the passed data will be added into `window.stubs` right\nafter the page loading.\n\nThe format of the stubs is completely up to you. One thing that should be noted here is\nthe fact that the passed data will be serialized, which means that they can't link to data from Node.js context.\n\n#### setRequestInterceptor\n\nAlso stubs can be done with `setRequestInterceptor` function.\nIf you pass it a callback it will be called on every network request.\nThe callback receives [HTTPRequest](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#class-httprequest) as an\nargument.\n\nUsage example:\n\n```js\nbrowser.setRequestInterceptor((request) =\u003e {\n  const apiPrefix = utils.url('/api');\n\n  if (request.url().indexOf(apiPrefix) === 0) {\n    const shortUrl = request.url().replace(apiPrefix, '');\n    let foundStub;\n\n    stubs.forEach((stub) =\u003e {\n      if (stub.method.toLowerCase() === request.method().toLowerCase() \u0026\u0026 stub.url === shortUrl) {\n        foundStub = stub;\n      }\n    });\n\n    if (foundStub) {\n      request.respond({\n        status: 200,\n        contentType: 'application/json',\n        body: JSON.stringify(foundStub.data),\n      });\n      return;\n    }\n\n    browser.browserErrors.push({ msg: `Stub not found: ${request.method()} ${shortUrl}` });\n  }\n\n  request.continue();\n});\n```\n\n### Local Storage\n\nEach browser launch is performed with a new profile with it's own unique directory. If any data is added to Local Storage,\nit's stored in that directory. And the directory is erased right after the browser closing.\n\n## Earlier versions compatibility\n\n### Page content\n\nThe previous versions of the lib used PhantomJS to launch the browser. PhantomJS does not have great support \nof the modern web features, and has “it's own point of view” to the page content. Which means that with the updating \nto the new version (based on Headless Chromium) some differences of page content parsing may be found.\n\nFor example PhantomJS ignores non-breaking spaces between words. E.g. it will parse `17\u0026nbsp;640` as “17640”, while\nHeadless Chromium will save the space and parse the string as “17 640”.\n\n**NB**. If the text content of an element contains non-breaking spaces they will be replaced with regular spaces \nby Phantom Lord (e.g. when using `waitForSelectorText`). So, if some tests fail with the error like this:\n\n```\nError: Expected text of '.dialog__content p' to be 'Do you want to delete your profile?', but it was 'Do you want to delete your profile?'\n```\n\nit probably means that the text _of the test_ was copied right from the page with all the non-breaking spaces.\nIn this case the test should be modified to replace non-breaking spaces with regular ones.\n\n### Click handling\n\nPay special attention to clicks on “invisible” elements. PhantomJS and Headless Chromium can click on element even when\nit's 0×0 sized. But if the element or one of its parents has `display: none` CSS property set, then Headless Chromium\nwon't be able to click on this element and will throw an `invisibleElement` error, because it won't be able to determine\nthe element's box model and coordinates.\n\nIn case of errors related to clicks on invisible elements, make sure that the elements or their parents do not have\nstyles that make them fully invisible. Otherwise run one more action before the click that will make invisible element \nvisible.\n\n### Local Storage clearing\n\nSince the previous versions of the library were based on PhantomJS, the unique path to Local Storage was created using \nthe Phantom Lord library itself and required manual cleaning by calling `Browser.deleteLocalStorageBaseDir()`.\n\nNow the calling of this function is no longer required. \n\n### Other compatibility issues\n\nIf you encounter any other issues related to differences of page display between PhantomJS and Headless Chromium \nwhile migrating tests from previous versions of the library to a version using Headless Chromium, \nplease [create an issue](https://github.com/funbox/phantom-lord/issues/new) to improve this section.\n\n## Development\n\n### Type declarations file\n\nThere's `index.d.ts` in the root of the project. It helps IDEs to highlight properties and methods of `RemoteBrowser`\nand contains the information about methods' arguments and returned values.\n\nIt's recommended to update the declaration file when new commands are added, old ones are removed or there are any other\nchanges of the class interface. \n\nFor safety reasons, there are tests that check the matching of the methods from the declaration file, \nthe commands from `lib/commands` and the `RemoteBrowser` methods.\n\n## Credits\n\nLuxury picture for the project was made by [Igor Garybaldi](https://pandabanda.com/).\n\n[![Sponsored by FunBox](https://funbox.ru/badges/sponsored_by_funbox_centered.svg)](https://funbox.ru)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffunbox%2Fphantom-lord","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffunbox%2Fphantom-lord","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffunbox%2Fphantom-lord/lists"}