[
  {
    "path": ".eslintrc.yml",
    "content": "extends: \"eslint:recommended\"\nparserOptions:\n  ecmaVersion: 8\n  sourceType: \"module\"\nenv:\n  node: true\n  es6: true\nrules:\n  consistent-return: \"error\"\n  curly: \"error\"\n  default-case: \"error\"\n  dot-notation: \"error\"\n  eqeqeq: \"error\"\n  no-extend-native: \"error\"\n  no-implicit-coercion: \"error\"\n  no-loop-func: \"error\"\n  no-multi-spaces: \"error\"\n  no-throw-literal: \"error\"\n  global-require: \"error\"\n  no-path-concat: \"error\"\n  brace-style: [\"error\", \"1tbs\", {allowSingleLine: true}]\n  camelcase: \"error\"\n  consistent-this: [\"error\", \"self\"]\n  indent: [\"error\", \"tab\", {SwitchCase: 1}]\n  linebreak-style: [\"error\", \"unix\"]\n  eol-last: \"error\"\n  quotes: [\"error\", \"single\"]\n  semi: \"error\"\n  space-infix-ops: \"error\"\n  space-unary-ops: \"error\"\n  func-names: \"warn\"\n  space-before-function-paren: \"warn\"\n  no-spaced-func: \"warn\"\n  keyword-spacing: \"error\"\n  space-before-blocks: \"error\"\n  no-console: \"error\"\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"npm\"\n    directory: \"/\"\n    assignees:\n      - \"s0ph1e\"\n    open-pull-requests-limit: 10\n    schedule:\n      interval: \"weekly\"\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    assignees:\n      - \"aivus\"\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "content": "name: \"CodeQL\"\n\non:\n  push:\n    branches: [\"master\"]\n  pull_request:\n    branches: [\"master\"]\n  schedule:\n    - cron: '0 1 * * 2'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'javascript' ]\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v6\n\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v4\n      with:\n        languages: ${{ matrix.language }}\n        \n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v4\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v4\n      with:\n        category: \"/language:${{matrix.language}}\"\n"
  },
  {
    "path": ".github/workflows/node.js.yml",
    "content": "name: Node.js CI\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n  schedule:\n    - cron: '17 2 * * *'\n  workflow_dispatch: ~\n\njobs:\n  test:\n    timeout-minutes: 10\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        node-version:\n          - 20\n          - 22\n          - 24\n          - current\n        os:\n          - ubuntu-latest\n          - windows-latest\n        include:\n          - node-version: 24\n            os: macos-latest\n\n    steps:\n      - uses: actions/checkout@v6\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ matrix.node-version }}\n      - run: npm i\n      - name: Disable AppArmor\n        if: ${{ matrix.os == 'ubuntu-latest' }}\n        run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns\n      - run: npm test\n      - run: npm run eslint\n        if: ${{ matrix.node-version == '24' && matrix.os == 'ubuntu-latest' }}\n      - name: Publish Qlty code coverage\n        if: ${{ matrix.node-version == '24' && matrix.os == 'ubuntu-latest' }}\n        uses: qltysh/qlty-action/coverage@v2\n        with:\n          token: ${{ secrets.QLTY_COVERAGE_TOKEN }}\n          files: coverage/lcov.info\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Create a tag and publish to npm\n\non:\n  workflow_dispatch:\n    inputs:\n      bump:\n        description: 'Version bump type'\n        type: choice\n        required: true\n        default: 'minor'\n        options:\n          - patch\n          - minor\n          - major\n          - premajor\n          - preminor\n          - prepatch\n          - prerelease\n      preid:\n        description: 'Prerelease identifier (see \"npm version\") for pre* bumps'\n        type: string\n        required: false\n      npmPreTag:\n        description: 'NPM tag used for all pre* bumps'\n        type: string\n        default: 'next'\n        required: false\n      dryRun:\n        description: 'Run in \"dry run\" mode'\n        type: boolean\n        default: false\n        required: true\n\npermissions:\n  id-token: write\n  contents: write\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n        with:\n          # Required to allow push to master without the checks/PR\n          token: ${{ secrets.GH_PUSH_TOKEN }}\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: '24'\n          registry-url: 'https://registry.npmjs.org'\n\n      - name: Update npm\n        run: npm install -g npm@latest\n\n      - name: Install dependencies\n        run: npm install\n\n      - name: List of installed dependencies\n        run: npm ls -a\n\n      - name: Verifying provenance attestations\n        run: npm audit signatures\n\n      - name: Disable AppArmor\n        run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns\n\n      - name: Run tests\n        run: npm test\n\n      - name: Bump version and create tag\n        id: bump-version\n        env:\n          PREID_FLAG: ${{ startsWith(inputs.bump, 'pre') && inputs.preid && format('--preid {0}', inputs.preid) || '' }}\n        run: |\n          git config --global user.name \"github-actions[bot]\"\n          git config --global user.email \"github-actions[bot]@users.noreply.github.com\"\n          TAG=$(npm version ${{ github.event.inputs.bump }} $PREID_FLAG -m \"Release %s\")\n          echo \"Created tag: $TAG\"\n          echo \"tag=$TAG\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Publish to npm\n        env:\n          DRY_RUN_FLAG: ${{ inputs.dryRun && '--dry-run' || '' }}\n          TAG_FLAG: ${{ startsWith(inputs.bump, 'pre') && format('--tag {0}', inputs.npmPreTag) || ''}}\n        run: npm publish --provenance --access=public $DRY_RUN_FLAG $TAG_FLAG\n\n      - name: Push changes to master\n        if: ${{ !inputs.dryRun }}\n        run: |\n          git push origin master --follow-tags\n\n      - name: Create GitHub Release\n        if: ${{ !inputs.dryRun }}\n        uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0\n        with:\n          tag_name: ${{ steps.bump-version.outputs.tag }}\n          generate_release_notes: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/sponsors.yml",
    "content": "name: Generate Sponsors list\non:\n  workflow_dispatch:\n  schedule:\n    - cron: '31 6 * * *'\npermissions:\n  contents: write\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout 🛎️\n        uses: actions/checkout@v6\n        with:\n          # Required to allow push to master without the checks/PR\n          token: ${{ secrets.GH_PUSH_TOKEN }}\n\n      - name: Generate Sponsors 💖\n        uses: JamesIves/github-sponsors-readme-action@2fd9142e765f755780202122261dc85e78459405 # v1.6.0\n        with:\n          token: ${{ secrets.SOFIIA_SPONSORS_READ_TOKEN }}\n          file: 'README.md'\n          active-only: false\n          include-private: true\n\n      - name: Commit changes\n        run: |\n          git config --global user.name \"github-actions[bot]\"\n          git config --global user.email \"github-actions[bot]@users.noreply.github.com\"\n          git add README.md\n          \n          # Check if there are any staged changes\n          if git diff --cached --quiet; then\n            echo \"No changes to commit.\"\n            exit 0\n          fi\n          \n          git commit -m \"Update list of sponsors\"\n          git push origin master\n"
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.\n#\n# You can adjust the behavior by modifying this file.\n# For more information, see:\n# https://github.com/actions/stale\nname: Mark stale issues and pull requests\n\non:\n  workflow_dispatch: ~\n  schedule:\n    - cron: '39 3 * * *'\n\njobs:\n  stale:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n\n    steps:\n      - uses: actions/stale@v10\n        with:\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n          days-before-stale: 60\n          days-before-close: 7\n          \n          # Do not stale PRs\n          days-before-pr-stale: -1\n          days-before-pr-close: -1\n          \n          exempt-issue-labels: 'bug,maybe-later,help wanted'\n          stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'\n          stale-issue-label: 'wontfix'\n          # debug-only: true\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\npackage-lock.json\n.idea\ncoverage\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018-2023 Sofiia Antypenko\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "[![Version](https://img.shields.io/npm/v/website-scraper-puppeteer.svg?style=flat)](https://www.npmjs.org/package/website-scraper-puppeteer)\n[![Downloads](https://img.shields.io/npm/dm/website-scraper-puppeteer.svg?style=flat)](https://www.npmjs.org/package/website-scraper-puppeteer)\n[![Node.js CI](https://github.com/website-scraper/website-scraper-puppeteer/actions/workflows/node.js.yml/badge.svg)](https://github.com/website-scraper/website-scraper-puppeteer)\n[![Code Coverage](https://qlty.sh/gh/website-scraper/projects/website-scraper-puppeteer/coverage.svg)](https://qlty.sh/gh/website-scraper/projects/website-scraper-puppeteer)\n\n# website-scraper-puppeteer\nPlugin for [website-scraper](https://github.com/website-scraper/node-website-scraper) which returns html for dynamic websites using [puppeteer](https://github.com/puppeteer/puppeteer).\n\n## Sponsors\nMaintenance of this project is made possible by all the [contributors](https://github.com/website-scraper/website-scraper-puppeteer/graphs/contributors) and [sponsors](https://github.com/sponsors/s0ph1e).\nIf you'd like to sponsor this project and have your avatar or company logo appear below [click here](https://github.com/sponsors/s0ph1e). 💖\n\n<!-- sponsors --><a href=\"https://github.com/aivus\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;aivus.png\" width=\"60px\" alt=\"User avatar: Illia Antypenko\" /></a><a href=\"https://github.com/swissspidy\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;swissspidy.png\" width=\"60px\" alt=\"User avatar: Pascal Birchler\" /></a><a href=\"https://github.com/itscarlosrufo\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;itscarlosrufo.png\" width=\"60px\" alt=\"User avatar: Carlos Rufo\" /></a><a href=\"https://github.com/francescamarano\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;francescamarano.png\" width=\"60px\" alt=\"User avatar: Francesca Marano\" /></a><a href=\"https://github.com/github\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;github.png\" width=\"60px\" alt=\"User avatar: GitHub\" /></a><a href=\"https://github.com/Belrestro\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;Belrestro.png\" width=\"60px\" alt=\"User avatar: Andrew Vorobiov\" /></a><a href=\"https://github.com/Effiezhu\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;Effiezhu.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/slicemedia\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;slicemedia.png\" width=\"60px\" alt=\"User avatar: \" /></a><!-- sponsors -->\n\n## Requirements\n* nodejs version >= 20\n* website-scraper version >= 5\n\n## Installation\n```sh\nnpm install website-scraper website-scraper-puppeteer\n```\n\n## Usage\n```javascript\nimport scrape from 'website-scraper';\nimport PuppeteerPlugin from 'website-scraper-puppeteer';\n\nawait scrape({\n    urls: ['https://www.instagram.com/gopro/'],\n    directory: '/path/to/save',\n    plugins: [ \n      new PuppeteerPlugin({\n        launchOptions: { headless: \"new\" }, /* optional */\n        gotoOptions: { waitUntil: \"networkidle0\" }, /* optional */\n        scrollToBottom: { timeout: 10000, viewportN: 10 }, /* optional */\n      })\n    ]\n});\n```\nPuppeteer plugin constructor accepts next params:\n* `launchOptions` - *(optional)* - puppeteer launch options, can be found in [puppeteer docs](https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.puppeteerlaunchoptions.md)\n* `gotoOptions` - *(optional)* - puppeteer page.goto options, can be found in [puppeteer docs](https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.frame.goto.md#parameters)\n* `scrollToBottom` - *(optional)* - in some cases, the page needs to be scrolled down to render its assets (lazyloading). Because some pages can be really endless, the scrolldown process can be interrupted before reaching the bottom when one or both of the bellow limitations are reached:\n    * `timeout` - in milliseconds\n    * `viewportN` - viewport height multiplier\n\n## How it works\nIt starts Chromium in headless mode which just opens page and waits until page is loaded.\nIt is far from ideal because probably you need to wait until some resource is loaded or click some button or log in. Currently this module doesn't support such functionality.\n"
  },
  {
    "path": "lib/browserUtils/.eslintrc.yml",
    "content": "extends: '../../.eslintrc.yml'\nenv:\n  browser: true\n"
  },
  {
    "path": "lib/browserUtils/scrollToBottom.js",
    "content": "export default async (timeout, viewportN) => {\n\tawait new Promise((resolve) => {\n\t\tlet totalHeight = 0, distance = 200, duration = 0, maxHeight = window.innerHeight * viewportN;\n\t\tconst timer = setInterval(() => {\n\t\t\tduration += 200;\n\t\t\twindow.scrollBy(0, distance);\n\t\t\ttotalHeight += distance;\n\t\t\tif (totalHeight >= document.body.scrollHeight || duration >= timeout || totalHeight >= maxHeight) {\n\t\t\t\tclearInterval(timer);\n\t\t\t\tresolve();\n\t\t\t}\n\t\t}, 200);\n\t});\n};\n"
  },
  {
    "path": "lib/index.js",
    "content": "import puppeteer from '@website-scraper/puppeteer-version-wrapper';\nimport logger from './logger.js';\nimport scrollToBottomBrowser from './browserUtils/scrollToBottom.js';\n\nclass PuppeteerPlugin {\n\tconstructor ({\n\t\tlaunchOptions = {},\n\t\tgotoOptions = {},\n\t\tscrollToBottom = null,\n\t} = {}) {\n\t\tthis.launchOptions = launchOptions;\n\t\tthis.gotoOptions = gotoOptions;\n\t\tthis.scrollToBottom = scrollToBottom;\n\t\tthis.browser = null;\n\t\tthis.headers = {};\n\n\t\tlogger.info('init plugin', { launchOptions, scrollToBottom });\n\t}\n\n\tapply (registerAction) {\n\t\tregisterAction('beforeStart', async () => {\n\t\t\tthis.browser = await puppeteer.launch(this.launchOptions);\n\t\t});\n\n\t\tregisterAction('beforeRequest', async ({requestOptions}) => {\n\t\t\tif (hasValues(requestOptions.headers)) {\n\t\t\t\tthis.headers = Object.assign({}, requestOptions.headers);\n\t\t\t}\n\t\t\treturn {requestOptions};\n\t\t});\n\n\t\tregisterAction('afterResponse', async ({response}) => {\n\t\t\tconst contentType = response.headers['content-type'];\n\t\t\tconst isHtml = contentType && contentType.split(';')[0] === 'text/html';\n\t\t\tif (isHtml) {\n\t\t\t\tconst url = response.url;\n\t\t\t\tconst page = await this.browser.newPage();\n\n\t\t\t\tif (hasValues(this.headers)) {\n\t\t\t\t\tlogger.info('set headers to puppeteer page', this.headers);\n\t\t\t\t\tawait page.setExtraHTTPHeaders(this.headers);\n\t\t\t\t}\n\n\t\t\t\tawait page.goto(url, this.gotoOptions);\n\n\t\t\t\tif (this.scrollToBottom) {\n\t\t\t\t\tawait scrollToBottom(page, this.scrollToBottom.timeout, this.scrollToBottom.viewportN);\n\t\t\t\t}\n\n\t\t\t\tconst content = await page.content();\n\t\t\t\tawait page.close();\n\n\t\t\t\t// convert utf-8 -> binary string because website-scraper needs binary\n\t\t\t\treturn Buffer.from(content).toString('binary');\n\t\t\t} else {\n\t\t\t\treturn response.body;\n\t\t\t}\n\t\t});\n\n\t\tregisterAction('afterFinish', () => this.browser && this.browser.close());\n\t}\n}\n\nfunction hasValues (obj) {\n\treturn obj && Object.keys(obj).length > 0;\n}\n\n\nasync function scrollToBottom (page, timeout, viewportN) {\n\tlogger.info(`scroll puppeteer page to bottom ${viewportN} times with timeout = ${timeout}`);\n\n\tawait page.evaluate(scrollToBottomBrowser, timeout, viewportN);\n}\n\nexport default PuppeteerPlugin;\n"
  },
  {
    "path": "lib/logger.js",
    "content": "import debug from 'debug';\n\nconst appName = 'website-scraper-puppeteer';\nconst logLevels = ['error', 'warn', 'info', 'debug', 'log'];\n\nconst logger = {};\nlogLevels.forEach(logLevel => {\n\tlogger[logLevel] = debug(`${appName}:${logLevel}`);\n});\n\nexport default logger;\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"website-scraper-puppeteer\",\n  \"version\": \"2.0.0\",\n  \"description\": \"Plugin for website-scraper which returns html for dynamic websites using puppeteer\",\n  \"readmeFilename\": \"README.md\",\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./lib/index.js\"\n  },\n  \"keywords\": [\n    \"website-scraper\",\n    \"puppeteer\",\n    \"chromium\",\n    \"chrome\",\n    \"headless\",\n    \"html\"\n  ],\n  \"dependencies\": {\n    \"debug\": \"^4.1.1\",\n    \"@website-scraper/puppeteer-version-wrapper\": \"^1.0.0\"\n  },\n  \"peerDependencies\": {\n    \"website-scraper\": \"^6.0.0\"\n  },\n  \"devDependencies\": {\n    \"c8\": \"^11.0.0\",\n    \"chai\": \"^6.0.1\",\n    \"eslint\": \"^8.5.0\",\n    \"finalhandler\": \"^2.1.0\",\n    \"fs-extra\": \"^11.1.0\",\n    \"mocha\": \"^11.0.1\",\n    \"serve-static\": \"^2.2.0\",\n    \"website-scraper\": \"^6.0.0\"\n  },\n  \"scripts\": {\n    \"test\": \"c8 --all --reporter=text --reporter=lcov mocha --recursive --timeout 15000\",\n    \"eslint\": \"eslint lib/**\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/website-scraper/website-scraper-puppeteer.git\"\n  },\n  \"author\": \"Sofiia Antypenko <sofiia@antypenko.dev>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/website-scraper/website-scraper-puppeteer/issues\"\n  },\n  \"homepage\": \"https://github.com/website-scraper/website-scraper-puppeteer#readme\",\n  \"files\": [\n    \"lib\"\n  ],\n  \"engines\": {\n    \"node\": \">=20\"\n  }\n}\n"
  },
  {
    "path": "test/mock/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<title>Test</title>\n</head>\n<body>\n\n<div id=\"root\"></div>\n<div id=\"special-characters-test\"></div>\n\n<script>\n\twindow.onload = function() {\n\t\tdocument.getElementById('root').innerText = 'Hello world from JS!';\n\t\t/**\n\t\t * TODO: Original innerText \"저는 7년 동안 한국에서 살았어요. Слава Україні!\" was changed due to issues\n\t\t * with cheerio and website-scraper itself.\n\t\t * See https://github.com/cheeriojs/cheerio/pull/2280\n\t\t */\n\t\tdocument.getElementById('special-characters-test').innerText = '7년 동안 한국에서 살았어요. Слава Україні!';\n\t};\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "test/mock/navigation.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<title>Test</title>\n</head>\n<body>\n\n<div id=\"root\"></div>\n\n<script>\n\twindow.onload = function() {\n\t\twindow.location.replace('http://example.com');\n\t\tdocument.getElementById('root').innerText = 'Navigation blocked!';\n\t};\n</script>\n\n</body>\n</html>"
  },
  {
    "path": "test/puppeteer-plugin.test.js",
    "content": "import { expect } from 'chai';\nimport http from 'http';\nimport finalhandler from 'finalhandler';\nimport serveStatic from 'serve-static';\nimport fs from 'fs-extra';\nimport scrape from 'website-scraper';\nimport PuppeteerPlugin from '../lib/index.js';\n\nconst directory = './test/tmp';\nconst SERVE_WEBSITE_PORT = 4567;\n\ndescribe('Puppeteer plugin test', () => {\n\tlet result, content, server;\n\n\tbefore('start webserver', () => server = startWebserver(SERVE_WEBSITE_PORT));\n\tafter('stop webserver', () => server.close())\n\n\tdescribe('Dynamic content', () => {\n\t\tbefore('scrape website', async () => {\n\t\t\tresult = await scrape({\n\t\t\t\turls: [`http://localhost:${SERVE_WEBSITE_PORT}`],\n\t\t\t\tdirectory: directory,\n\t\t\t\tplugins: [ new PuppeteerPlugin({\n\t\t\t\t\tscrollToBottom: { timeout: 50, viewportN: 10 }\n\t\t\t\t}) ]\n\t\t\t});\n\t\t});\n\t\tbefore('get content from file', () => {\n\t\t\tcontent = fs.readFileSync(`${directory}/${result[0].filename}`).toString();\n\t\t});\n\t\tafter('delete dir', () => fs.removeSync(directory));\n\n\t\tit('should have 1 item in result array', () => {\n\t\t\texpect(result.length).eql(1);\n\t\t});\n\n\t\tit('should render dymanic website', async () => {\n\t\t\texpect(content).to.contain('<div id=\"root\">Hello world from JS!</div>');\n\t\t});\n\n\t\tit('should render special characters correctly', async () => {\n\t\t\texpect(content).to.contain('<div id=\"special-characters-test\">7년 동안 한국에서 살았어요. Слава Україні!</div>');\n\t\t});\n\t});\n});\n\nfunction startWebserver(port = 3000) {\n\tconst serve = serveStatic('./test/mock', {'index': ['index.html']});\n\tconst server = http.createServer(function onRequest (req, res) {\n\t\tserve(req, res, finalhandler(req, res))\n\t});\n\n\treturn server.listen(port)\n}\n"
  }
]