[
  {
    "path": ".devcontainer/Dockerfile",
    "content": "# See comments in devcontainer.json for details of setting the playwright tag.\nARG PLAYWRIGHT_TAG=\"v1.51.1-jammy\"\nFROM mcr.microsoft.com/playwright:${PLAYWRIGHT_TAG}\n\nWORKDIR /app\n"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:\n// https://github.com/microsoft/vscode-dev-containers/tree/v0.191.1/containers/typescript-node\n{\n  \"name\": \"Node.js & Playwright\",\n  \"build\": {\n    \"dockerfile\": \"Dockerfile\",\n    // Change to pick a different tag, see https://mcr.microsoft.com/en-us/product/playwright/tags\n    //\n    // IMPORTANT: The playwright image version must match @playwright/test version from package.json.\n    // Otherwise it will not work, the test version will look for the browser versions which it won't\n    // find on the image.\n    //\n    // pick '...-arm64' version if running on Apple Silicon.\n    \"args\": {\n      \"PLAYWRIGHT_TAG\": \"v1.51.1-jammy\"\n    }\n  },\n  // Add the IDs of extensions you want installed when the container is created.\n  \"customizations\": {\n    \"vscode\": {\n      \"extensions\": [\"dbaeumer.vscode-eslint\"]\n    }\n  },\n  // Use 'forwardPorts' to make a list of ports inside the container available locally.\n  \"forwardPorts\": [9000],\n  // Use 'postCreateCommand' to run commands after the container is created.\n  \"postCreateCommand\": \"yarn install && yarn build\"\n}\n"
  },
  {
    "path": ".eslintignore",
    "content": "dist/\nnode_modules/\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n  env: {\n    browser: true,\n    es2021: true\n  },\n  extends: [\"eslint:recommended\"],\n  overrides: [\n    {\n      env: {\n        node: true\n      },\n      files: [\".eslintrc.{js,cjs}\"],\n      parserOptions: {\n        sourceType: \"script\"\n      }\n    }\n  ],\n  parserOptions: {\n    ecmaVersion: \"latest\",\n    sourceType: \"module\"\n  },\n  rules: {\n    \"comma-dangle\": \"error\",\n    \"curly\": [\"error\", \"multi-line\"],\n    \"getter-return\": \"off\",\n    \"no-console\": \"off\",\n    \"no-duplicate-imports\": [\"error\"],\n    \"no-multi-spaces\": [\"error\", { \"exceptions\": { \"VariableDeclarator\": true }}],\n    \"no-multiple-empty-lines\": [\"error\", { \"max\": 2 }],\n    \"no-self-assign\": [\"error\", { \"props\": false }],\n    \"no-trailing-spaces\": [\"error\"],\n    \"no-unused-vars\": [\"error\", { argsIgnorePattern: \"_*\" }],\n    \"no-useless-escape\": \"off\",\n    \"no-var\": [\"error\"],\n    \"prefer-const\": [\"error\"],\n    \"semi\": [\"error\", \"never\"]\n  },\n  globals: {\n    test: true,\n    setup: true\n  }\n}\n"
  },
  {
    "path": ".github/scripts/publish-dev-build",
    "content": "#!/usr/bin/env bash\nset -eux\n\nDEV_BUILD_REPO_NAME=\"hotwired/dev-builds\"\nDEV_BUILD_ORIGIN_URL=\"https://${1}@github.com/${DEV_BUILD_REPO_NAME}.git\"\nBUILD_PATH=\"$HOME/publish-dev-build\"\n\nmkdir \"$BUILD_PATH\"\n\ncd \"$GITHUB_WORKSPACE\"\npackage_name=\"$(jq -r .name package.json)\"\npackage_files=( dist package.json )\ntag=\"${package_name}/${GITHUB_SHA:0:7}\"\n\nname=\"$(git log -n 1 --format=format:%cn)\"\nemail=\"$(git log -n 1 --format=format:%ce)\"\nsubject=\"$(git log -n 1 --format=format:%s)\"\ndate=\"$(git log -n 1 --format=format:%ai)\"\nurl=\"https://github.com/${GITHUB_REPOSITORY}/tree/${GITHUB_SHA}\"\nmessage=\"$tag $subject\"$'\\n\\n'\"$url\"\n\ncp -R \"${package_files[@]}\" \"$BUILD_PATH\"\n\ncd \"$BUILD_PATH\"\ngit init .\ngit remote add origin \"$DEV_BUILD_ORIGIN_URL\"\ngit symbolic-ref HEAD refs/heads/publish-dev-build\ngit add \"${package_files[@]}\"\n\nGIT_AUTHOR_DATE=\"$date\" GIT_COMMITTER_DATE=\"$date\" \\\nGIT_AUTHOR_NAME=\"$name\" GIT_COMMITTER_NAME=\"$name\" \\\nGIT_AUTHOR_EMAIL=\"$email\" GIT_COMMITTER_EMAIL=\"$email\" \\\n  git commit -m \"$message\"\n\ngit tag \"$tag\"\n[ \"$GITHUB_REF\" != \"refs/heads/main\" ] || git tag -f \"${package_name}/latest\"\ngit push -f --tags\n\necho done\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non: [push, pull_request]\n\njobs:\n  build:\n\n    runs-on: ubuntu-22.04\n\n    steps:\n    - uses: actions/checkout@v3\n    - uses: actions/setup-node@v3\n      with:\n        node-version: '22'\n        cache: 'yarn'\n    - run: yarn install --frozen-lockfile\n    - run: yarn run playwright install --with-deps\n    - run: yarn build\n\n    - name: Set Chrome Version\n      run: |\n        CHROMEVER=\"$(chromedriver --version | cut -d' ' -f2)\"\n        echo \"Actions ChromeDriver is $CHROMEVER\"\n        echo \"CHROMEVER=${CHROMEVER}\" >> $GITHUB_ENV\n\n    - name: Lint\n      run: yarn lint\n\n    - name: Unit Test\n      run: yarn test:unit\n\n    - name: Chrome Test\n      run: yarn test:browser --project=chrome\n\n    - name: Firefox Test\n      run: yarn test:browser --project=firefox\n\n    - uses: actions/upload-artifact@v4\n      with:\n        name: turbo-dist\n        path: dist/*\n"
  },
  {
    "path": ".github/workflows/dev-builds.yml",
    "content": "name: dev-builds\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n      - 'builds/**'\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: '22'\n          cache: 'yarn'\n\n      - run: yarn install --frozen-lockfile\n      - run: yarn build\n\n      - name: Publish dev build\n        run: .github/scripts/publish-dev-build '${{ secrets.DEV_BUILD_GITHUB_TOKEN }}'\n"
  },
  {
    "path": ".gitignore",
    "content": "/dist\n/node_modules\n/test-results\n*.log\npackage-lock.json\n"
  },
  {
    "path": ".prettierignore",
    "content": "dist/\nnode_modules/\n"
  },
  {
    "path": ".prettierrc.json",
    "content": "{\n  \"singleQuote\": false,\n  \"printWidth\": 120,\n  \"semi\": false,\n  \"trailingComma\" : \"none\"\n}\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n  // Use IntelliSense to learn about possible attributes.\n  // Hover to view descriptions of existing attributes.\n  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"name\": \"Turbo: Debug browser tests\",\n      \"cwd\": \"${workspaceFolder}\",\n      \"port\": 9229,\n      \"outputCapture\": \"std\",\n      \"internalConsoleOptions\": \"openOnSessionStart\",\n      \"runtimeExecutable\": \"yarn\",\n      \"runtimeArgs\": [\"test\"]\n    }\n  ]\n}\n"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n  // See https://go.microsoft.com/fwlink/?LinkId=733558\n  // for the documentation about the tasks.json format\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    {\n      \"label\": \"Turbo: Build dist directory\",\n      \"type\": \"shell\",\n      \"command\": \"yarn build\",\n      \"group\": {\n        \"kind\": \"build\",\n        \"isDefault\": true\n      }\n    },\n    {\n      \"label\": \"Turbo: Run tests\",\n      \"type\": \"shell\",\n      \"dependsOn\": \"Turbo: Build dist directory\",\n      \"command\": \"yarn test\",\n      \"group\": {\n        \"kind\": \"test\",\n        \"isDefault\": true\n      }\n    },\n    {\n      \"label\": \"Turbo: Start dev server\",\n      \"type\": \"shell\",\n      \"dependsOn\": \"Turbo: Build dist directory\",\n      \"command\": \"yarn start\",\n      \"problemMatcher\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nPlease see [our GitHub \"Releases\" page](https://github.com/hotwired/turbo/releases).\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, gender identity and expression, level of experience,\nnationality, personal appearance, race, religion, or sexual identity and\norientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\nadvances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting one of the project maintainers listed below. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Project Maintainers\n\n* Sam Stephenson <<sam@hey.com>>\n* Javan Makhmali <<javan@javan.us>>\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "[![Version](https://img.shields.io/npm/v/@hotwired/turbo)](https://www.npmjs.com/package/@hotwired/turbo)\n[![License](https://img.shields.io/github/license/hotwired/turbo)](https://github.com/hotwired/turbo)\n\n# Contributing\n\nNote that we have a [code of conduct](https://github.com/hotwired/turbo/blob/main/CODE_OF_CONDUCT.md). Please follow it in your interactions with this project.\n\n## Sending a Pull Request\n\nThe core team is monitoring for pull requests. We will review your pull request and either merge it, request changes to it, or close it with an explanation.\n\nBefore submitting a pull request, please:\n\n1. Fork the repository and create your branch.\n2. Follow the setup instructions in this file.\n3. If you’re fixing a bug or adding code that should be tested, add tests!\n4. Ensure the test suite passes.\n\n## Developing locally\n\nFirst, clone the `hotwired/turbo` repository and install dependencies:\n\n```bash\ngit clone https://github.com/hotwired/turbo.git\n```\n\n```bash\ncd turbo\nyarn install\n```\n\nThen create a branch for your changes:\n\n```bash\ngit checkout -b <your_branch_name>\n```\n\n### Testing\n\nTests are run through `yarn` using [Web Test Runner](https://modern-web.dev/docs/test-runner/overview/) with [Playwright](https://github.com/microsoft/playwright) for browser testing. Browser and runtime configuration can be found in [`web-test-runner.config.mjs`](./web-test-runner.config.mjs) and [`playwright.config.js`](./playwright.config.js).\n\nTo begin testing, install the browser drivers:\n\n```bash\nyarn playwright install --with-deps\n```\n\nThen build the source. Because tests are run against the compiled source (and are themselves compiled) be sure to run `yarn build` prior to testing. Alternatively, you can run `yarn watch` to build and watch for changes.\n\n```bash\nyarn build\n```\n\n### Running the test suite\n\nThe test suite can be run with `yarn`, using the test commands defined in [`package.json`](./package.json). To run all tests in all configured browsers:\n\n```bash\nyarn test\n```\n\nTo run just the unit or browser tests:\n\n```bash\nyarn test:unit\nyarn test:browser\n```\n\nBy default, tests are run in \"headless\" mode against all configured browsers (currently `chrome` and `firefox`). Use the `--headed` flag to run in normal mode. Use the `--project` flag to run against a particular browser.\n\n```bash\nyarn test:browser --project=firefox\nyarn test:browser --project=chrome\nyarn test:browser --project=chrome --headed\n```\n\n### Running a single test\n\nTo run a single test file, pass its path as an argument. To run a particular test case, append its starting line number after a colon.\n\n```bash\nyarn test:browser src/tests/functional/drive_tests.js\nyarn test:browser src/tests/functional/drive_tests.js:11\nyarn test:browser src/tests/functional/drive_tests.js:11 --project=chrome\n```\n\n### Running the local web server\n\nBecause tests are running headless in browsers, debugging can be difficult. Sometimes the simplest thing to do is load the test fixtures into the browser and navigate manually. To make this easier, a local web server is included.\n\nTo run the web server, ensure the source is built and start the server with `yarn`:\n\n```bash\nyarn build\nyarn start\n```\n\nThe web server is available on port 9000, serving from the project root. Fixture files are accessible by path. For example, the file at `src/tests/fixtures/rendering.html` will be accessible at <http://localhost:9000/src/tests/fixtures/rendering.html>.\n"
  },
  {
    "path": "MIT-LICENSE",
    "content": "Copyright (c) 37signals\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Turbo\n\nTurbo uses complementary techniques to dramatically reduce the amount of custom JavaScript that most web applications will need to write:\n\n* Turbo Drive accelerates links and form submissions by negating the need for full page reloads.\n* Turbo Frames decompose pages into independent contexts, which scope navigation and can be lazily loaded.\n* Turbo Streams deliver page changes over WebSocket or in response to form submissions using just HTML and a set of CRUD-like actions.\n* Turbo Native lets your majestic monolith form the center of your native iOS and Android apps, with seamless transitions between web and native sections.\n\nIt's all done by sending HTML over the wire. And for those instances when that's not enough, you can reach for the other side of Hotwire, and finish the job with [Stimulus](https://github.com/hotwired/stimulus).\n\nRead more on [turbo.hotwired.dev](https://turbo.hotwired.dev).\n\n## Contributing\n\nPlease read [CONTRIBUTING.md](./CONTRIBUTING.md).\n\n© 2026 37signals LLC.\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@hotwired/turbo\",\n  \"version\": \"8.0.23\",\n  \"description\": \"The speed of a single-page web application without having to write any JavaScript\",\n  \"module\": \"dist/turbo.es2017-esm.js\",\n  \"main\": \"dist/turbo.es2017-umd.js\",\n  \"files\": [\n    \"dist/*.js\",\n    \"dist/*.js.map\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/hotwired/turbo.git\"\n  },\n  \"keywords\": [\n    \"hotwire\",\n    \"turbo\",\n    \"browser\",\n    \"pushstate\"\n  ],\n  \"author\": \"37signals LLC\",\n  \"contributors\": [\n    \"Jeffrey Hardy <jeff@basecamp.com>\",\n    \"Javan Makhmali <javan@javan.us>\",\n    \"Sam Stephenson <sstephenson@gmail.com>\"\n  ],\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/hotwired/turbo/issues\"\n  },\n  \"homepage\": \"https://turbo.hotwired.dev\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"devDependencies\": {\n    \"@open-wc/testing\": \"^3.1.7\",\n    \"@playwright/test\": \"~1.51.1\",\n    \"@rollup/plugin-node-resolve\": \"13.1.3\",\n    \"@web/dev-server-esbuild\": \"^0.3.3\",\n    \"@web/test-runner\": \"^0.15.0\",\n    \"@web/test-runner-playwright\": \"^0.9.0\",\n    \"arg\": \"^5.0.1\",\n    \"body-parser\": \"^1.20.1\",\n    \"eslint\": \"^8.13.0\",\n    \"express\": \"^4.18.2\",\n    \"idiomorph\": \"~0.7.4\",\n    \"multer\": \"^2.0.2\",\n    \"rollup\": \"^2.35.1\"\n  },\n  \"scripts\": {\n    \"clean\": \"rm -fr dist\",\n    \"clean:win\": \"rmdir /s /q dist\",\n    \"build\": \"yarn install && rollup -c\",\n    \"build:win\": \"yarn install && rollup -c\",\n    \"watch\": \"rollup -wc\",\n    \"start\": \"node src/tests/server.mjs\",\n    \"test\": \"yarn test:unit && yarn test:browser\",\n    \"test:browser\": \"playwright test\",\n    \"test:unit\": \"NODE_OPTIONS=--inspect web-test-runner\",\n    \"test:unit:win\": \"SET NODE_OPTIONS=--inspect & web-test-runner\",\n    \"release\": \"yarn build && yarn publish\",\n    \"lint\": \"eslint . --ext .js\"\n  },\n  \"engines\": {\n    \"node\": \">= 18\"\n  }\n}\n"
  },
  {
    "path": "playwright.config.js",
    "content": "import { devices } from \"@playwright/test\"\n\nconst config = {\n  projects: [\n    {\n      name: \"chrome\",\n      use: {\n        ...devices[\"Desktop Chrome\"],\n        contextOptions: {\n          timeout: 10000\n        },\n        hasTouch: true\n      }\n    },\n    {\n      name: \"firefox\",\n      use: {\n        ...devices[\"Desktop Firefox\"],\n        contextOptions: {\n          timeout: 10000\n        },\n        hasTouch: true\n      }\n    }\n  ],\n  timeout: 10000,\n  browserStartTimeout: 10000,\n  retries: 2,\n  testDir: \"./src/tests/\",\n  testMatch: /(functional|integration)\\/.*_tests\\.js/,\n  webServer: {\n    command: \"yarn start\",\n    url: \"http://localhost:9000/src/tests/fixtures/test.js\",\n    timeout: 10000,\n    // eslint-disable-next-line no-undef\n    reuseExistingServer: !process.env.CI\n  },\n  use: {\n    baseURL: \"http://localhost:9000/\"\n  }\n}\n\nexport default config\n"
  },
  {
    "path": "rollup.config.js",
    "content": "import resolve from \"@rollup/plugin-node-resolve\"\n\nimport { version } from \"./package.json\"\nconst year = new Date().getFullYear()\nconst banner = `/*!\\nTurbo ${version}\\nCopyright © ${year} 37signals LLC\\n */`\n\nexport default [\n  {\n    input: \"src/index.js\",\n    output: [\n      {\n        name: \"Turbo\",\n        file: \"dist/turbo.es2017-umd.js\",\n        format: \"umd\",\n        banner\n      },\n      {\n        file: \"dist/turbo.es2017-esm.js\",\n        format: \"esm\",\n        banner\n      }\n    ],\n    plugins: [resolve()],\n    watch: {\n      include: \"src/**\"\n    }\n  }\n]\n"
  },
  {
    "path": "src/core/bardo.js",
    "content": "export class Bardo {\n  static async preservingPermanentElements(delegate, permanentElementMap, callback) {\n    const bardo = new this(delegate, permanentElementMap)\n    bardo.enter()\n    await callback()\n    bardo.leave()\n  }\n\n  constructor(delegate, permanentElementMap) {\n    this.delegate = delegate\n    this.permanentElementMap = permanentElementMap\n  }\n\n  enter() {\n    for (const id in this.permanentElementMap) {\n      const [currentPermanentElement, newPermanentElement] = this.permanentElementMap[id]\n      this.delegate.enteringBardo(currentPermanentElement, newPermanentElement)\n      this.replaceNewPermanentElementWithPlaceholder(newPermanentElement)\n    }\n  }\n\n  leave() {\n    for (const id in this.permanentElementMap) {\n      const [currentPermanentElement] = this.permanentElementMap[id]\n      this.replaceCurrentPermanentElementWithClone(currentPermanentElement)\n      this.replacePlaceholderWithPermanentElement(currentPermanentElement)\n      this.delegate.leavingBardo(currentPermanentElement)\n    }\n  }\n\n  replaceNewPermanentElementWithPlaceholder(permanentElement) {\n    const placeholder = createPlaceholderForPermanentElement(permanentElement)\n    permanentElement.replaceWith(placeholder)\n  }\n\n  replaceCurrentPermanentElementWithClone(permanentElement) {\n    const clone = permanentElement.cloneNode(true)\n    permanentElement.replaceWith(clone)\n  }\n\n  replacePlaceholderWithPermanentElement(permanentElement) {\n    const placeholder = this.getPlaceholderById(permanentElement.id)\n    placeholder?.replaceWith(permanentElement)\n  }\n\n  getPlaceholderById(id) {\n    return this.placeholders.find((element) => element.content == id)\n  }\n\n  get placeholders() {\n    return [...document.querySelectorAll(\"meta[name=turbo-permanent-placeholder][content]\")]\n  }\n}\n\nfunction createPlaceholderForPermanentElement(permanentElement) {\n  const element = document.createElement(\"meta\")\n  element.setAttribute(\"name\", \"turbo-permanent-placeholder\")\n  element.setAttribute(\"content\", permanentElement.id)\n  return element\n}\n"
  },
  {
    "path": "src/core/cache.js",
    "content": "import { setMetaContent } from \"../util\"\n\nexport class Cache {\n  constructor(session) {\n    this.session = session\n  }\n\n  clear() {\n    this.session.clearCache()\n  }\n\n  resetCacheControl() {\n    this.#setCacheControl(\"\")\n  }\n\n  exemptPageFromCache() {\n    this.#setCacheControl(\"no-cache\")\n  }\n\n  exemptPageFromPreview() {\n    this.#setCacheControl(\"no-preview\")\n  }\n\n  #setCacheControl(value) {\n    setMetaContent(\"turbo-cache-control\", value)\n  }\n}\n"
  },
  {
    "path": "src/core/config/drive.js",
    "content": "export const drive = {\n  enabled: true,\n  progressBarDelay: 500,\n  unvisitableExtensions: new Set(\n    [\n      \".7z\", \".aac\", \".apk\", \".avi\", \".bmp\", \".bz2\", \".css\", \".csv\", \".deb\", \".dmg\", \".doc\",\n      \".docx\", \".exe\", \".gif\", \".gz\", \".heic\", \".heif\", \".ico\", \".iso\", \".jpeg\", \".jpg\",\n      \".js\", \".json\", \".m4a\", \".mkv\", \".mov\", \".mp3\", \".mp4\", \".mpeg\", \".mpg\", \".msi\",\n      \".ogg\", \".ogv\", \".pdf\", \".pkg\", \".png\", \".ppt\", \".pptx\", \".rar\", \".rtf\",\n      \".svg\", \".tar\", \".tif\", \".tiff\", \".txt\", \".wav\", \".webm\", \".webp\", \".wma\", \".wmv\",\n      \".xls\", \".xlsx\", \".xml\", \".zip\"\n    ]\n  )\n}\n"
  },
  {
    "path": "src/core/config/forms.js",
    "content": "import { cancelEvent } from \"../../util\"\n\nconst submitter = {\n  \"aria-disabled\": {\n    beforeSubmit: submitter => {\n      submitter.setAttribute(\"aria-disabled\", \"true\")\n      submitter.addEventListener(\"click\", cancelEvent)\n    },\n\n    afterSubmit: submitter => {\n      submitter.removeAttribute(\"aria-disabled\")\n      submitter.removeEventListener(\"click\", cancelEvent)\n    }\n  },\n\n  \"disabled\": {\n    beforeSubmit: submitter => submitter.disabled = true,\n    afterSubmit: submitter => submitter.disabled = false\n  }\n}\n\nclass Config {\n  #submitter = null\n\n  constructor(config) {\n    Object.assign(this, config)\n  }\n\n  get submitter() {\n    return this.#submitter\n  }\n\n  set submitter(value) {\n    this.#submitter = submitter[value] || value\n  }\n}\n\nexport const forms = new Config({\n  mode: \"on\",\n  submitter: \"disabled\"\n})\n"
  },
  {
    "path": "src/core/config/index.js",
    "content": "import { drive } from \"./drive\"\nimport { forms } from \"./forms\"\n\nexport const config = {\n  drive,\n  forms\n}\n"
  },
  {
    "path": "src/core/drive/error_renderer.js",
    "content": "import { activateScriptElement } from \"../../util\"\nimport { Renderer } from \"../renderer\"\n\nexport class ErrorRenderer extends Renderer {\n  static renderElement(currentElement, newElement) {\n    const { documentElement, body } = document\n\n    documentElement.replaceChild(newElement, body)\n  }\n\n  async render() {\n    this.replaceHeadAndBody()\n    this.activateScriptElements()\n  }\n\n  replaceHeadAndBody() {\n    const { documentElement, head } = document\n    documentElement.replaceChild(this.newHead, head)\n    this.renderElement(this.currentElement, this.newElement)\n  }\n\n  activateScriptElements() {\n    for (const replaceableElement of this.scriptElements) {\n      const parentNode = replaceableElement.parentNode\n      if (parentNode) {\n        const element = activateScriptElement(replaceableElement)\n        parentNode.replaceChild(element, replaceableElement)\n      }\n    }\n  }\n\n  get newHead() {\n    return this.newSnapshot.headSnapshot.element\n  }\n\n  get scriptElements() {\n    return document.documentElement.querySelectorAll(\"script\")\n  }\n}\n"
  },
  {
    "path": "src/core/drive/form_submission.js",
    "content": "import { FetchRequest, FetchMethod, fetchMethodFromString, fetchEnctypeFromString, isSafe } from \"../../http/fetch_request\"\nimport { expandURL } from \"../url\"\nimport { clearBusyState, dispatch, getAttribute, getMetaContent, hasAttribute, markAsBusy } from \"../../util\"\nimport { StreamMessage } from \"../streams/stream_message\"\nimport { prefetchCache } from \"./prefetch_cache\"\nimport { config } from \"../config\"\n\nexport const FormSubmissionState = {\n  initialized: \"initialized\",\n  requesting: \"requesting\",\n  waiting: \"waiting\",\n  receiving: \"receiving\",\n  stopping: \"stopping\",\n  stopped: \"stopped\"\n}\n\nexport const FormEnctype = {\n  urlEncoded: \"application/x-www-form-urlencoded\",\n  multipart: \"multipart/form-data\",\n  plain: \"text/plain\"\n}\n\nexport class FormSubmission {\n  state = FormSubmissionState.initialized\n\n  static confirmMethod(message) {\n    return Promise.resolve(confirm(message))\n  }\n\n  constructor(delegate, formElement, submitter, mustRedirect = false) {\n    const method = getMethod(formElement, submitter)\n    const action = getAction(getFormAction(formElement, submitter), method)\n    const body = buildFormData(formElement, submitter)\n    const enctype = getEnctype(formElement, submitter)\n\n    this.delegate = delegate\n    this.formElement = formElement\n    this.submitter = submitter\n    this.fetchRequest = new FetchRequest(this, method, action, body, formElement, enctype)\n    this.mustRedirect = mustRedirect\n  }\n\n  get method() {\n    return this.fetchRequest.method\n  }\n\n  set method(value) {\n    this.fetchRequest.method = value\n  }\n\n  get action() {\n    return this.fetchRequest.url.toString()\n  }\n\n  set action(value) {\n    this.fetchRequest.url = expandURL(value)\n  }\n\n  get body() {\n    return this.fetchRequest.body\n  }\n\n  get enctype() {\n    return this.fetchRequest.enctype\n  }\n\n  get isSafe() {\n    return this.fetchRequest.isSafe\n  }\n\n  get location() {\n    return this.fetchRequest.url\n  }\n\n  // The submission process\n\n  async start() {\n    const { initialized, requesting } = FormSubmissionState\n    const confirmationMessage = getAttribute(\"data-turbo-confirm\", this.submitter, this.formElement)\n\n    if (typeof confirmationMessage === \"string\") {\n      const confirmMethod = typeof config.forms.confirm === \"function\" ?\n        config.forms.confirm :\n        FormSubmission.confirmMethod\n\n      const answer = await confirmMethod(confirmationMessage, this.formElement, this.submitter)\n      if (!answer) {\n        return\n      }\n    }\n\n    if (this.state == initialized) {\n      this.state = requesting\n      return this.fetchRequest.perform()\n    }\n  }\n\n  stop() {\n    const { stopping, stopped } = FormSubmissionState\n    if (this.state != stopping && this.state != stopped) {\n      this.state = stopping\n      this.fetchRequest.cancel()\n      return true\n    }\n  }\n\n  // Fetch request delegate\n\n  prepareRequest(request) {\n    if (!request.isSafe) {\n      const token = getCookieValue(getMetaContent(\"csrf-param\")) || getMetaContent(\"csrf-token\")\n      if (token) {\n        request.headers[\"X-CSRF-Token\"] = token\n      }\n    }\n\n    if (this.requestAcceptsTurboStreamResponse(request)) {\n      request.acceptResponseType(StreamMessage.contentType)\n    }\n  }\n\n  requestStarted(_request) {\n    this.state = FormSubmissionState.waiting\n    if (this.submitter) config.forms.submitter.beforeSubmit(this.submitter)\n    this.setSubmitsWith()\n    markAsBusy(this.formElement)\n    dispatch(\"turbo:submit-start\", {\n      target: this.formElement,\n      detail: { formSubmission: this }\n    })\n    this.delegate.formSubmissionStarted(this)\n  }\n\n  requestPreventedHandlingResponse(request, response) {\n    prefetchCache.clear()\n\n    this.result = { success: response.succeeded, fetchResponse: response }\n  }\n\n  requestSucceededWithResponse(request, response) {\n    if (response.clientError || response.serverError) {\n      this.delegate.formSubmissionFailedWithResponse(this, response)\n      return\n    }\n\n    prefetchCache.clear()\n\n    if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {\n      const error = new Error(\"Form responses must redirect to another location\")\n      this.delegate.formSubmissionErrored(this, error)\n    } else {\n      this.state = FormSubmissionState.receiving\n      this.result = { success: true, fetchResponse: response }\n      this.delegate.formSubmissionSucceededWithResponse(this, response)\n    }\n  }\n\n  requestFailedWithResponse(request, response) {\n    this.result = { success: false, fetchResponse: response }\n    this.delegate.formSubmissionFailedWithResponse(this, response)\n  }\n\n  requestErrored(request, error) {\n    this.result = { success: false, error }\n    this.delegate.formSubmissionErrored(this, error)\n  }\n\n  requestFinished(_request) {\n    this.state = FormSubmissionState.stopped\n    if (this.submitter) config.forms.submitter.afterSubmit(this.submitter)\n    this.resetSubmitterText()\n    clearBusyState(this.formElement)\n    dispatch(\"turbo:submit-end\", {\n      target: this.formElement,\n      detail: { formSubmission: this, ...this.result }\n    })\n    this.delegate.formSubmissionFinished(this)\n  }\n\n  // Private\n\n  setSubmitsWith() {\n    if (!this.submitter || !this.submitsWith) return\n\n    if (this.submitter.matches(\"button\")) {\n      this.originalSubmitText = this.submitter.innerHTML\n      this.submitter.innerHTML = this.submitsWith\n    } else if (this.submitter.matches(\"input\")) {\n      const input = this.submitter\n      this.originalSubmitText = input.value\n      input.value = this.submitsWith\n    }\n  }\n\n  resetSubmitterText() {\n    if (!this.submitter || !this.originalSubmitText) return\n\n    if (this.submitter.matches(\"button\")) {\n      this.submitter.innerHTML = this.originalSubmitText\n    } else if (this.submitter.matches(\"input\")) {\n      const input = this.submitter\n      input.value = this.originalSubmitText\n    }\n  }\n\n  requestMustRedirect(request) {\n    return !request.isSafe && this.mustRedirect\n  }\n\n  requestAcceptsTurboStreamResponse(request) {\n    return !request.isSafe || hasAttribute(\"data-turbo-stream\", this.submitter, this.formElement)\n  }\n\n  get submitsWith() {\n    return this.submitter?.getAttribute(\"data-turbo-submits-with\")\n  }\n}\n\nfunction buildFormData(formElement, submitter) {\n  const formData = new FormData(formElement)\n  const name = submitter?.getAttribute(\"name\")\n  const value = submitter?.getAttribute(\"value\")\n\n  if (name) {\n    formData.append(name, value || \"\")\n  }\n\n  return formData\n}\n\nfunction getCookieValue(cookieName) {\n  if (cookieName != null) {\n    const cookies = document.cookie ? document.cookie.split(\"; \") : []\n    const cookie = cookies.find((cookie) => cookie.startsWith(cookieName))\n    if (cookie) {\n      const value = cookie.split(\"=\").slice(1).join(\"=\")\n      return value ? decodeURIComponent(value) : undefined\n    }\n  }\n}\n\nfunction responseSucceededWithoutRedirect(response) {\n  return response.statusCode == 200 && !response.redirected\n}\n\nfunction getFormAction(formElement, submitter) {\n  const formElementAction = typeof formElement.action === \"string\" ? formElement.action : null\n\n  if (submitter?.hasAttribute(\"formaction\")) {\n    return submitter.getAttribute(\"formaction\") || \"\"\n  } else {\n    return formElement.getAttribute(\"action\") || formElementAction || \"\"\n  }\n}\n\nfunction getAction(formAction, fetchMethod) {\n  const action = expandURL(formAction)\n\n  if (isSafe(fetchMethod)) {\n    action.search = \"\"\n  }\n\n  return action\n}\n\nfunction getMethod(formElement, submitter) {\n  const method = submitter?.getAttribute(\"formmethod\") || formElement.getAttribute(\"method\") || \"\"\n  return fetchMethodFromString(method.toLowerCase()) || FetchMethod.get\n}\n\nfunction getEnctype(formElement, submitter) {\n  return fetchEnctypeFromString(submitter?.getAttribute(\"formenctype\") || formElement.enctype)\n}\n"
  },
  {
    "path": "src/core/drive/head_snapshot.js",
    "content": "import { elementIsStylesheet } from \"../../util\"\nimport { Snapshot } from \"../snapshot\"\n\nexport class HeadSnapshot extends Snapshot {\n  detailsByOuterHTML = this.children\n    .filter((element) => !elementIsNoscript(element))\n    .map((element) => elementWithoutNonce(element))\n    .reduce((result, element) => {\n      const { outerHTML } = element\n      const details =\n        outerHTML in result\n          ? result[outerHTML]\n          : {\n              type: elementType(element),\n              tracked: elementIsTracked(element),\n              elements: []\n            }\n      return {\n        ...result,\n        [outerHTML]: {\n          ...details,\n          elements: [...details.elements, element]\n        }\n      }\n    }, {})\n\n  get trackedElementSignature() {\n    return Object.keys(this.detailsByOuterHTML)\n      .filter((outerHTML) => this.detailsByOuterHTML[outerHTML].tracked)\n      .join(\"\")\n  }\n\n  getScriptElementsNotInSnapshot(snapshot) {\n    return this.getElementsMatchingTypeNotInSnapshot(\"script\", snapshot)\n  }\n\n  getStylesheetElementsNotInSnapshot(snapshot) {\n    return this.getElementsMatchingTypeNotInSnapshot(\"stylesheet\", snapshot)\n  }\n\n  getElementsMatchingTypeNotInSnapshot(matchedType, snapshot) {\n    return Object.keys(this.detailsByOuterHTML)\n      .filter((outerHTML) => !(outerHTML in snapshot.detailsByOuterHTML))\n      .map((outerHTML) => this.detailsByOuterHTML[outerHTML])\n      .filter(({ type }) => type == matchedType)\n      .map(({ elements: [element] }) => element)\n  }\n\n  get provisionalElements() {\n    return Object.keys(this.detailsByOuterHTML).reduce((result, outerHTML) => {\n      const { type, tracked, elements } = this.detailsByOuterHTML[outerHTML]\n      if (type == null && !tracked) {\n        return [...result, ...elements]\n      } else if (elements.length > 1) {\n        return [...result, ...elements.slice(1)]\n      } else {\n        return result\n      }\n    }, [])\n  }\n\n  getMetaValue(name) {\n    const element = this.findMetaElementByName(name)\n    return element ? element.getAttribute(\"content\") : null\n  }\n\n  findMetaElementByName(name) {\n    return Object.keys(this.detailsByOuterHTML).reduce((result, outerHTML) => {\n      const {\n        elements: [element]\n      } = this.detailsByOuterHTML[outerHTML]\n      return elementIsMetaElementWithName(element, name) ? element : result\n    }, undefined | undefined)\n  }\n}\n\nfunction elementType(element) {\n  if (elementIsScript(element)) {\n    return \"script\"\n  } else if (elementIsStylesheet(element)) {\n    return \"stylesheet\"\n  }\n}\n\nfunction elementIsTracked(element) {\n  return element.getAttribute(\"data-turbo-track\") == \"reload\"\n}\n\nfunction elementIsScript(element) {\n  const tagName = element.localName\n  return tagName == \"script\"\n}\n\nfunction elementIsNoscript(element) {\n  const tagName = element.localName\n  return tagName == \"noscript\"\n}\n\nfunction elementIsMetaElementWithName(element, name) {\n  const tagName = element.localName\n  return tagName == \"meta\" && element.getAttribute(\"name\") == name\n}\n\nfunction elementWithoutNonce(element) {\n  if (element.hasAttribute(\"nonce\")) {\n    element.setAttribute(\"nonce\", \"\")\n  }\n\n  return element\n}\n"
  },
  {
    "path": "src/core/drive/history.js",
    "content": "import { uuid } from \"../../util\"\n\nexport class History {\n  location\n  restorationIdentifier = uuid()\n  restorationData = {}\n  started = false\n  currentIndex = 0\n\n  constructor(delegate) {\n    this.delegate = delegate\n  }\n\n  start() {\n    if (!this.started) {\n      addEventListener(\"popstate\", this.onPopState, false)\n      this.currentIndex = history.state?.turbo?.restorationIndex || 0\n      this.started = true\n      this.replace(new URL(window.location.href))\n    }\n  }\n\n  stop() {\n    if (this.started) {\n      removeEventListener(\"popstate\", this.onPopState, false)\n      this.started = false\n    }\n  }\n\n  push(location, restorationIdentifier) {\n    this.update(history.pushState, location, restorationIdentifier)\n  }\n\n  replace(location, restorationIdentifier) {\n    this.update(history.replaceState, location, restorationIdentifier)\n  }\n\n  update(method, location, restorationIdentifier = uuid()) {\n    if (method === history.pushState) ++this.currentIndex\n\n    const state = { turbo: { restorationIdentifier, restorationIndex: this.currentIndex } }\n    method.call(history, state, \"\", location.href)\n    this.location = location\n    this.restorationIdentifier = restorationIdentifier\n  }\n\n  // Restoration data\n\n  getRestorationDataForIdentifier(restorationIdentifier) {\n    return this.restorationData[restorationIdentifier] || {}\n  }\n\n  updateRestorationData(additionalData) {\n    const { restorationIdentifier } = this\n    const restorationData = this.restorationData[restorationIdentifier]\n    this.restorationData[restorationIdentifier] = {\n      ...restorationData,\n      ...additionalData\n    }\n  }\n\n  // Scroll restoration\n\n  assumeControlOfScrollRestoration() {\n    if (!this.previousScrollRestoration) {\n      this.previousScrollRestoration = history.scrollRestoration ?? \"auto\"\n      history.scrollRestoration = \"manual\"\n    }\n  }\n\n  relinquishControlOfScrollRestoration() {\n    if (this.previousScrollRestoration) {\n      history.scrollRestoration = this.previousScrollRestoration\n      delete this.previousScrollRestoration\n    }\n  }\n\n  // Event handlers\n\n  onPopState = (event) => {\n    const { turbo } = event.state || {}\n    this.location = new URL(window.location.href)\n\n    if (turbo) {\n      const { restorationIdentifier, restorationIndex } = turbo\n      this.restorationIdentifier = restorationIdentifier\n      const direction = restorationIndex > this.currentIndex ? \"forward\" : \"back\"\n      this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction)\n      this.currentIndex = restorationIndex\n    } else {\n      this.currentIndex++\n      this.delegate.historyPoppedWithEmptyState(this.location)\n    }\n  }\n}\n"
  },
  {
    "path": "src/core/drive/limited_set.js",
    "content": "export class LimitedSet extends Set {\n  constructor(maxSize) {\n    super()\n    this.maxSize = maxSize\n  }\n\n  add(value) {\n    if (this.size >= this.maxSize) {\n      const iterator = this.values()\n      const oldestValue = iterator.next().value\n      this.delete(oldestValue)\n    }\n    super.add(value)\n  }\n}\n"
  },
  {
    "path": "src/core/drive/morphing_page_renderer.js",
    "content": "import { PageRenderer } from \"./page_renderer\"\nimport { dispatch } from \"../../util\"\nimport { morphElements, shouldRefreshFrameWithMorphing, closestFrameReloadableWithMorphing } from \"../morphing\"\n\nexport class MorphingPageRenderer extends PageRenderer {\n  static renderElement(currentElement, newElement) {\n    morphElements(currentElement, newElement, {\n      callbacks: {\n        beforeNodeMorphed: (node, newNode) => {\n          if (\n            shouldRefreshFrameWithMorphing(node, newNode) &&\n              !closestFrameReloadableWithMorphing(node)\n          ) {\n            node.reload()\n            return false\n          }\n          return true\n        }\n      }\n    })\n\n    dispatch(\"turbo:morph\", { detail: { currentElement, newElement } })\n  }\n\n  async preservingPermanentElements(callback) {\n    return await callback()\n  }\n\n  get renderMethod() {\n    return \"morph\"\n  }\n\n  get shouldAutofocus() {\n    return false\n  }\n}\n\n"
  },
  {
    "path": "src/core/drive/navigator.js",
    "content": "import { getVisitAction } from \"../../util\"\nimport { FormSubmission } from \"./form_submission\"\nimport { expandURL } from \"../url\"\nimport { Visit } from \"./visit\"\nimport { PageSnapshot } from \"./page_snapshot\"\n\nexport class Navigator {\n  constructor(delegate) {\n    this.delegate = delegate\n  }\n\n  proposeVisit(location, options = {}) {\n    if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {\n      this.delegate.visitProposedToLocation(location, options)\n    }\n  }\n\n  startVisit(locatable, restorationIdentifier, options = {}) {\n    this.stop()\n    this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, {\n      referrer: this.location,\n      ...options\n    })\n    this.currentVisit.start()\n  }\n\n  submitForm(form, submitter) {\n    this.stop()\n    this.formSubmission = new FormSubmission(this, form, submitter, true)\n\n    this.formSubmission.start()\n  }\n\n  stop() {\n    if (this.formSubmission) {\n      this.formSubmission.stop()\n      delete this.formSubmission\n    }\n\n    if (this.currentVisit) {\n      this.currentVisit.cancel()\n      delete this.currentVisit\n    }\n  }\n\n  get adapter() {\n    return this.delegate.adapter\n  }\n\n  get view() {\n    return this.delegate.view\n  }\n\n  get rootLocation() {\n    return this.view.snapshot.rootLocation\n  }\n\n  get history() {\n    return this.delegate.history\n  }\n\n  // Form submission delegate\n\n  formSubmissionStarted(formSubmission) {\n    // Not all adapters implement formSubmissionStarted\n    if (typeof this.adapter.formSubmissionStarted === \"function\") {\n      this.adapter.formSubmissionStarted(formSubmission)\n    }\n  }\n\n  async formSubmissionSucceededWithResponse(formSubmission, fetchResponse) {\n    if (formSubmission == this.formSubmission) {\n      const responseHTML = await fetchResponse.responseHTML\n      if (responseHTML) {\n        const shouldCacheSnapshot = formSubmission.isSafe\n        if (!shouldCacheSnapshot) {\n          this.view.clearSnapshotCache()\n        }\n\n        const { statusCode, redirected } = fetchResponse\n        const action = this.#getActionForFormSubmission(formSubmission, fetchResponse)\n        const visitOptions = {\n          action,\n          shouldCacheSnapshot,\n          response: { statusCode, responseHTML, redirected }\n        }\n        this.proposeVisit(fetchResponse.location, visitOptions)\n      }\n    }\n  }\n\n  async formSubmissionFailedWithResponse(formSubmission, fetchResponse) {\n    const responseHTML = await fetchResponse.responseHTML\n\n    if (responseHTML) {\n      const snapshot = PageSnapshot.fromHTMLString(responseHTML)\n      if (fetchResponse.serverError) {\n        await this.view.renderError(snapshot, this.currentVisit)\n      } else {\n        await this.view.renderPage(snapshot, false, true, this.currentVisit)\n      }\n      if (snapshot.refreshScroll !== \"preserve\") {\n        this.view.scrollToTop()\n      }\n      this.view.clearSnapshotCache()\n    }\n  }\n\n  formSubmissionErrored(formSubmission, error) {\n    console.error(error)\n  }\n\n  formSubmissionFinished(formSubmission) {\n    // Not all adapters implement formSubmissionFinished\n    if (typeof this.adapter.formSubmissionFinished === \"function\") {\n      this.adapter.formSubmissionFinished(formSubmission)\n    }\n  }\n\n  // Link prefetching\n\n  linkPrefetchingIsEnabledForLocation(location) {\n    // Not all adapters implement linkPrefetchingIsEnabledForLocation\n    if (typeof this.adapter.linkPrefetchingIsEnabledForLocation === \"function\") {\n      return this.adapter.linkPrefetchingIsEnabledForLocation(location)\n    }\n\n    return true\n  }\n\n  // Visit delegate\n\n  visitStarted(visit) {\n    this.delegate.visitStarted(visit)\n  }\n\n  visitCompleted(visit) {\n    this.delegate.visitCompleted(visit)\n    delete this.currentVisit\n  }\n\n  // Same-page links are no longer handled with a Visit.\n  // This method is still needed for Turbo Native adapters.\n  locationWithActionIsSamePage(location, action) {\n    return false\n  }\n\n  // Visits\n\n  get location() {\n    return this.history.location\n  }\n\n  get restorationIdentifier() {\n    return this.history.restorationIdentifier\n  }\n\n  #getActionForFormSubmission(formSubmission, fetchResponse) {\n    const { submitter, formElement } = formSubmission\n    return getVisitAction(submitter, formElement) || this.#getDefaultAction(fetchResponse)\n  }\n\n  #getDefaultAction(fetchResponse) {\n    const sameLocationRedirect = fetchResponse.redirected && fetchResponse.location.href === this.location?.href\n    return sameLocationRedirect ? \"replace\" : \"advance\"\n  }\n}\n"
  },
  {
    "path": "src/core/drive/page_renderer.js",
    "content": "import { activateScriptElement, elementIsStylesheet, waitForLoad } from \"../../util\"\nimport { Renderer } from \"../renderer\"\n\nexport class PageRenderer extends Renderer {\n  static renderElement(currentElement, newElement) {\n    if (document.body && newElement instanceof HTMLBodyElement) {\n      document.body.replaceWith(newElement)\n    } else {\n      document.documentElement.appendChild(newElement)\n    }\n  }\n\n  get shouldRender() {\n    return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical\n  }\n\n  get reloadReason() {\n    if (!this.newSnapshot.isVisitable) {\n      return {\n        reason: \"turbo_visit_control_is_reload\"\n      }\n    }\n\n    if (!this.trackedElementsAreIdentical) {\n      return {\n        reason: \"tracked_element_mismatch\"\n      }\n    }\n  }\n\n  async prepareToRender() {\n    this.#setLanguage()\n    await this.mergeHead()\n  }\n\n  async render() {\n    if (this.willRender) {\n      await this.replaceBody()\n    }\n  }\n\n  finishRendering() {\n    super.finishRendering()\n    if (!this.isPreview) {\n      this.focusFirstAutofocusableElement()\n    }\n  }\n\n  get currentHeadSnapshot() {\n    return this.currentSnapshot.headSnapshot\n  }\n\n  get newHeadSnapshot() {\n    return this.newSnapshot.headSnapshot\n  }\n\n  get newElement() {\n    return this.newSnapshot.element\n  }\n\n  #setLanguage() {\n    const { documentElement } = this.currentSnapshot\n    const { dir, lang } = this.newSnapshot\n\n    if (lang) {\n      documentElement.setAttribute(\"lang\", lang)\n    } else {\n      documentElement.removeAttribute(\"lang\")\n    }\n    if (dir) {\n      documentElement.setAttribute(\"dir\", dir)\n    } else {\n      documentElement.removeAttribute(\"dir\")\n    }\n  }\n\n  async mergeHead() {\n    const mergedHeadElements = this.mergeProvisionalElements()\n    const newStylesheetElements = this.copyNewHeadStylesheetElements()\n    this.copyNewHeadScriptElements()\n\n    await mergedHeadElements\n    await newStylesheetElements\n\n    if (this.willRender) {\n      this.removeUnusedDynamicStylesheetElements()\n    }\n  }\n\n  async replaceBody() {\n    await this.preservingPermanentElements(async () => {\n      this.activateNewBody()\n      await this.assignNewBody()\n    })\n  }\n\n  get trackedElementsAreIdentical() {\n    return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature\n  }\n\n  async copyNewHeadStylesheetElements() {\n    const loadingElements = []\n\n    for (const element of this.newHeadStylesheetElements) {\n      loadingElements.push(waitForLoad(element))\n\n      document.head.appendChild(element)\n    }\n\n    await Promise.all(loadingElements)\n  }\n\n  copyNewHeadScriptElements() {\n    for (const element of this.newHeadScriptElements) {\n      document.head.appendChild(activateScriptElement(element))\n    }\n  }\n\n  removeUnusedDynamicStylesheetElements() {\n    for (const element of this.unusedDynamicStylesheetElements) {\n      document.head.removeChild(element)\n    }\n  }\n\n  async mergeProvisionalElements() {\n    const newHeadElements = [...this.newHeadProvisionalElements]\n\n    for (const element of this.currentHeadProvisionalElements) {\n      if (!this.isCurrentElementInElementList(element, newHeadElements)) {\n        document.head.removeChild(element)\n      }\n    }\n\n    for (const element of newHeadElements) {\n      document.head.appendChild(element)\n    }\n  }\n\n  isCurrentElementInElementList(element, elementList) {\n    for (const [index, newElement] of elementList.entries()) {\n      // if title element...\n      if (element.tagName == \"TITLE\") {\n        if (newElement.tagName != \"TITLE\") {\n          continue\n        }\n        if (element.innerHTML == newElement.innerHTML) {\n          elementList.splice(index, 1)\n          return true\n        }\n      }\n\n      // if any other element...\n      if (newElement.isEqualNode(element)) {\n        elementList.splice(index, 1)\n        return true\n      }\n    }\n\n    return false\n  }\n\n  removeCurrentHeadProvisionalElements() {\n    for (const element of this.currentHeadProvisionalElements) {\n      document.head.removeChild(element)\n    }\n  }\n\n  copyNewHeadProvisionalElements() {\n    for (const element of this.newHeadProvisionalElements) {\n      document.head.appendChild(element)\n    }\n  }\n\n  activateNewBody() {\n    document.adoptNode(this.newElement)\n    this.deactivateNoscriptStylesheetElements()\n    this.activateNewBodyScriptElements()\n  }\n\n  deactivateNoscriptStylesheetElements() {\n    for (const noscriptElement of this.newElement.querySelectorAll(\"noscript\")) {\n      for (const child of [...noscriptElement.children]) {\n        if (elementIsStylesheet(child)) {\n          child.remove()\n        }\n      }\n    }\n  }\n\n  activateNewBodyScriptElements() {\n    for (const inertScriptElement of this.newBodyScriptElements) {\n      const activatedScriptElement = activateScriptElement(inertScriptElement)\n      inertScriptElement.replaceWith(activatedScriptElement)\n    }\n  }\n\n  async assignNewBody() {\n    await this.renderElement(this.currentElement, this.newElement)\n  }\n\n  get unusedDynamicStylesheetElements() {\n    return this.oldHeadStylesheetElements.filter((element) => {\n      return element.getAttribute(\"data-turbo-track\") === \"dynamic\"\n    })\n  }\n\n  get oldHeadStylesheetElements() {\n    return this.currentHeadSnapshot.getStylesheetElementsNotInSnapshot(this.newHeadSnapshot)\n  }\n\n  get newHeadStylesheetElements() {\n    return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot)\n  }\n\n  get newHeadScriptElements() {\n    return this.newHeadSnapshot.getScriptElementsNotInSnapshot(this.currentHeadSnapshot)\n  }\n\n  get currentHeadProvisionalElements() {\n    return this.currentHeadSnapshot.provisionalElements\n  }\n\n  get newHeadProvisionalElements() {\n    return this.newHeadSnapshot.provisionalElements\n  }\n\n  get newBodyScriptElements() {\n    return this.newElement.querySelectorAll(\"script\")\n  }\n}\n"
  },
  {
    "path": "src/core/drive/page_snapshot.js",
    "content": "import { elementIsStylesheet, parseHTMLDocument } from \"../../util\"\nimport { Snapshot } from \"../snapshot\"\nimport { expandURL } from \"../url\"\nimport { HeadSnapshot } from \"./head_snapshot\"\n\nexport class PageSnapshot extends Snapshot {\n  static fromHTMLString(html = \"\") {\n    return this.fromDocument(parseHTMLDocument(html))\n  }\n\n  static fromElement(element) {\n    return this.fromDocument(element.ownerDocument)\n  }\n\n  static fromDocument({ documentElement, body, head }) {\n    return new this(documentElement, body, new HeadSnapshot(head))\n  }\n\n  constructor(documentElement, body, headSnapshot) {\n    super(body)\n    this.documentElement = documentElement\n    this.headSnapshot = headSnapshot\n  }\n\n  clone() {\n    const clonedElement = this.element.cloneNode(true)\n\n    const selectElements = this.element.querySelectorAll(\"select\")\n    const clonedSelectElements = clonedElement.querySelectorAll(\"select\")\n\n    for (const [index, source] of selectElements.entries()) {\n      const clone = clonedSelectElements[index]\n      for (const option of clone.selectedOptions) option.selected = false\n      for (const option of source.selectedOptions) clone.options[option.index].selected = true\n    }\n\n    for (const clonedPasswordInput of clonedElement.querySelectorAll('input[type=\"password\"]')) {\n      clonedPasswordInput.value = \"\"\n    }\n\n    for (const clonedNoscriptElement of clonedElement.querySelectorAll(\"noscript\")) {\n      for (const child of [...clonedNoscriptElement.children]) {\n        if (elementIsStylesheet(child)) {\n          child.remove()\n        }\n      }\n    }\n\n    return new PageSnapshot(this.documentElement, clonedElement, this.headSnapshot)\n  }\n\n  get lang() {\n    return this.documentElement.getAttribute(\"lang\")\n  }\n\n  get dir() {\n    return this.documentElement.getAttribute(\"dir\")\n  }\n\n  get headElement() {\n    return this.headSnapshot.element\n  }\n\n  get rootLocation() {\n    const root = this.getSetting(\"root\") ?? \"/\"\n    return expandURL(root)\n  }\n\n  get cacheControlValue() {\n    return this.getSetting(\"cache-control\")\n  }\n\n  get isPreviewable() {\n    return this.cacheControlValue != \"no-preview\"\n  }\n\n  get isCacheable() {\n    return this.cacheControlValue != \"no-cache\"\n  }\n\n  get isVisitable() {\n    return this.getSetting(\"visit-control\") != \"reload\"\n  }\n\n  get prefersViewTransitions() {\n    const viewTransitionEnabled = this.getSetting(\"view-transition\") === \"true\" || this.headSnapshot.getMetaValue(\"view-transition\") === \"same-origin\"\n    return viewTransitionEnabled && !window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n  }\n\n  get refreshMethod() {\n    return this.getSetting(\"refresh-method\")\n  }\n\n  get refreshScroll() {\n    return this.getSetting(\"refresh-scroll\")\n  }\n\n  // Private\n\n  getSetting(name) {\n    return this.headSnapshot.getMetaValue(`turbo-${name}`)\n  }\n}\n"
  },
  {
    "path": "src/core/drive/page_view.js",
    "content": "import { nextEventLoopTick } from \"../../util\"\nimport { View } from \"../view\"\nimport { ErrorRenderer } from \"./error_renderer\"\nimport { MorphingPageRenderer } from \"./morphing_page_renderer\"\nimport { PageRenderer } from \"./page_renderer\"\nimport { PageSnapshot } from \"./page_snapshot\"\nimport { SnapshotCache } from \"./snapshot_cache\"\n\nexport class PageView extends View {\n  snapshotCache = new SnapshotCache(10)\n  lastRenderedLocation = new URL(location.href)\n  forceReloaded = false\n\n  shouldTransitionTo(newSnapshot) {\n    return this.snapshot.prefersViewTransitions && newSnapshot.prefersViewTransitions\n  }\n\n  renderPage(snapshot, isPreview = false, willRender = true, visit) {\n    const shouldMorphPage = this.isPageRefresh(visit) && (visit?.refresh?.method || this.snapshot.refreshMethod) === \"morph\"\n    const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer\n\n    const renderer = new rendererClass(this.snapshot, snapshot, isPreview, willRender)\n\n    if (!renderer.shouldRender) {\n      this.forceReloaded = true\n    } else {\n      visit?.changeHistory()\n    }\n\n    return this.render(renderer)\n  }\n\n  renderError(snapshot, visit) {\n    visit?.changeHistory()\n    const renderer = new ErrorRenderer(this.snapshot, snapshot, false)\n    return this.render(renderer)\n  }\n\n  clearSnapshotCache() {\n    this.snapshotCache.clear()\n  }\n\n  async cacheSnapshot(snapshot = this.snapshot) {\n    if (snapshot.isCacheable) {\n      this.delegate.viewWillCacheSnapshot()\n      const { lastRenderedLocation: location } = this\n      await nextEventLoopTick()\n      const cachedSnapshot = snapshot.clone()\n      this.snapshotCache.put(location, cachedSnapshot)\n      return cachedSnapshot\n    }\n  }\n\n  getCachedSnapshotForLocation(location) {\n    return this.snapshotCache.get(location)\n  }\n\n  isPageRefresh(visit) {\n    return !visit || (this.lastRenderedLocation.pathname === visit.location.pathname && visit.action === \"replace\")\n  }\n\n  shouldPreserveScrollPosition(visit) {\n    return this.isPageRefresh(visit) && (visit?.refresh?.scroll || this.snapshot.refreshScroll) === \"preserve\"\n  }\n\n  get snapshot() {\n    return PageSnapshot.fromElement(this.element)\n  }\n}\n"
  },
  {
    "path": "src/core/drive/prefetch_cache.js",
    "content": "import { LRUCache } from \"../lru_cache\"\nimport { toCacheKey } from \"../url\"\n\nconst PREFETCH_DELAY = 100\n\nclass PrefetchCache extends LRUCache {\n  #prefetchTimeout = null\n  #maxAges = {}\n\n  constructor(size = 1, prefetchDelay = PREFETCH_DELAY) {\n    super(size, toCacheKey)\n    this.prefetchDelay = prefetchDelay\n  }\n\n  putLater(url, request, ttl) {\n    this.#prefetchTimeout = setTimeout(() => {\n      request.perform()\n      this.put(url, request, ttl)\n      this.#prefetchTimeout = null\n    }, this.prefetchDelay)\n  }\n\n  put(url, request, ttl = cacheTtl) {\n    super.put(url, request)\n    this.#maxAges[toCacheKey(url)] = new Date(new Date().getTime() + ttl)\n  }\n\n  clear() {\n    super.clear()\n    if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout)\n  }\n\n  evict(key) {\n    super.evict(key)\n    delete this.#maxAges[key]\n  }\n\n  has(key) {\n    if (super.has(key)) {\n      const maxAge = this.#maxAges[toCacheKey(key)]\n\n      return maxAge && maxAge > Date.now()\n    } else {\n      return false\n    }\n  }\n}\n\nexport const cacheTtl = 10 * 1000\nexport const prefetchCache = new PrefetchCache()\n"
  },
  {
    "path": "src/core/drive/preloader.js",
    "content": "import { PageSnapshot } from \"./page_snapshot\"\nimport { FetchMethod, FetchRequest } from \"../../http/fetch_request\"\n\nexport class Preloader {\n  selector = \"a[data-turbo-preload]\"\n\n  constructor(delegate, snapshotCache) {\n    this.delegate = delegate\n    this.snapshotCache = snapshotCache\n  }\n\n  start() {\n    if (document.readyState === \"loading\") {\n      document.addEventListener(\"DOMContentLoaded\", this.#preloadAll)\n    } else {\n      this.preloadOnLoadLinksForView(document.body)\n    }\n  }\n\n  stop() {\n    document.removeEventListener(\"DOMContentLoaded\", this.#preloadAll)\n  }\n\n  preloadOnLoadLinksForView(element) {\n    for (const link of element.querySelectorAll(this.selector)) {\n      if (this.delegate.shouldPreloadLink(link)) {\n        this.preloadURL(link)\n      }\n    }\n  }\n\n  async preloadURL(link) {\n    const location = new URL(link.href)\n\n    if (this.snapshotCache.has(location)) {\n      return\n    }\n\n    const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams(), link)\n    await fetchRequest.perform()\n  }\n\n  // Fetch request delegate\n\n  prepareRequest(fetchRequest) {\n    fetchRequest.headers[\"X-Sec-Purpose\"] = \"prefetch\"\n  }\n\n  async requestSucceededWithResponse(fetchRequest, fetchResponse) {\n    try {\n      const responseHTML = await fetchResponse.responseHTML\n      const snapshot = PageSnapshot.fromHTMLString(responseHTML)\n\n      this.snapshotCache.put(fetchRequest.url, snapshot)\n    } catch (_) {\n      // If we cannot preload that is ok!\n    }\n  }\n\n  requestStarted(fetchRequest) {}\n\n  requestErrored(fetchRequest) {}\n\n  requestFinished(fetchRequest) {}\n\n  requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}\n\n  requestFailedWithResponse(fetchRequest, fetchResponse) {}\n\n  #preloadAll = () => {\n    this.preloadOnLoadLinksForView(document.body)\n  }\n}\n"
  },
  {
    "path": "src/core/drive/progress_bar.js",
    "content": "import { unindent, getCspNonce } from \"../../util\"\n\nexport const ProgressBarID = \"turbo-progress-bar\"\n\nexport class ProgressBar {\n  static animationDuration = 300 /*ms*/\n\n  static get defaultCSS() {\n    return unindent`\n      .turbo-progress-bar {\n        position: fixed;\n        display: block;\n        top: 0;\n        left: 0;\n        height: 3px;\n        background: #0076ff;\n        z-index: 2147483647;\n        transition:\n          width ${ProgressBar.animationDuration}ms ease-out,\n          opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in;\n        transform: translate3d(0, 0, 0);\n      }\n    `\n  }\n\n  hiding = false\n  value = 0\n  visible = false\n\n  constructor() {\n    this.stylesheetElement = this.createStylesheetElement()\n    this.progressElement = this.createProgressElement()\n    this.installStylesheetElement()\n    this.setValue(0)\n  }\n\n  show() {\n    if (!this.visible) {\n      this.visible = true\n      this.installProgressElement()\n      this.startTrickling()\n    }\n  }\n\n  hide() {\n    if (this.visible && !this.hiding) {\n      this.hiding = true\n      this.fadeProgressElement(() => {\n        this.uninstallProgressElement()\n        this.stopTrickling()\n        this.visible = false\n        this.hiding = false\n      })\n    }\n  }\n\n  setValue(value) {\n    this.value = value\n    this.refresh()\n  }\n\n  // Private\n\n  installStylesheetElement() {\n    document.head.insertBefore(this.stylesheetElement, document.head.firstChild)\n  }\n\n  installProgressElement() {\n    this.progressElement.style.width = \"0\"\n    this.progressElement.style.opacity = \"1\"\n    document.documentElement.insertBefore(this.progressElement, document.body)\n    this.refresh()\n  }\n\n  fadeProgressElement(callback) {\n    this.progressElement.style.opacity = \"0\"\n    setTimeout(callback, ProgressBar.animationDuration * 1.5)\n  }\n\n  uninstallProgressElement() {\n    if (this.progressElement.parentNode) {\n      document.documentElement.removeChild(this.progressElement)\n    }\n  }\n\n  startTrickling() {\n    if (!this.trickleInterval) {\n      this.trickleInterval = window.setInterval(this.trickle, ProgressBar.animationDuration)\n    }\n  }\n\n  stopTrickling() {\n    window.clearInterval(this.trickleInterval)\n    delete this.trickleInterval\n  }\n\n  trickle = () => {\n    this.setValue(this.value + Math.random() / 100)\n  }\n\n  refresh() {\n    requestAnimationFrame(() => {\n      this.progressElement.style.width = `${10 + this.value * 90}%`\n    })\n  }\n\n  createStylesheetElement() {\n    const element = document.createElement(\"style\")\n    element.type = \"text/css\"\n    element.textContent = ProgressBar.defaultCSS\n    const cspNonce = getCspNonce()\n    if (cspNonce) {\n      element.nonce = cspNonce\n    }\n    return element\n  }\n\n  createProgressElement() {\n    const element = document.createElement(\"div\")\n    element.className = \"turbo-progress-bar\"\n    return element\n  }\n}\n"
  },
  {
    "path": "src/core/drive/snapshot_cache.js",
    "content": "import { toCacheKey } from \"../url\"\nimport { LRUCache } from \"../lru_cache\"\n\nexport class SnapshotCache extends LRUCache {\n  constructor(size) {\n    super(size, toCacheKey)\n  }\n\n  get snapshots() {\n    return this.entries\n  }\n}\n"
  },
  {
    "path": "src/core/drive/view_transitioner.js",
    "content": "export class ViewTransitioner {\n  #viewTransitionStarted = false\n  #lastOperation = Promise.resolve()\n\n  renderChange(useViewTransition, render) {\n    if (useViewTransition && this.viewTransitionsAvailable && !this.#viewTransitionStarted) {\n      this.#viewTransitionStarted = true\n      this.#lastOperation = this.#lastOperation.then(async () => {\n        await document.startViewTransition(render).finished\n      })\n    } else {\n      this.#lastOperation = this.#lastOperation.then(render)\n    }\n\n    return this.#lastOperation\n  }\n\n  get viewTransitionsAvailable() {\n    return document.startViewTransition\n  }\n}\n"
  },
  {
    "path": "src/core/drive/visit.js",
    "content": "import { FetchMethod, FetchRequest } from \"../../http/fetch_request\"\nimport { getAnchor } from \"../url\"\nimport { PageSnapshot } from \"./page_snapshot\"\nimport { getHistoryMethodForAction, uuid } from \"../../util\"\nimport { StreamMessage } from \"../streams/stream_message\"\nimport { ViewTransitioner } from \"./view_transitioner\"\n\nconst defaultOptions = {\n  action: \"advance\",\n  historyChanged: false,\n  visitCachedSnapshot: () => {},\n  willRender: true,\n  updateHistory: true,\n  shouldCacheSnapshot: true,\n  acceptsStreamResponse: false,\n  refresh: {}\n}\n\nexport const TimingMetric = {\n  visitStart: \"visitStart\",\n  requestStart: \"requestStart\",\n  requestEnd: \"requestEnd\",\n  visitEnd: \"visitEnd\"\n}\n\nexport const VisitState = {\n  initialized: \"initialized\",\n  started: \"started\",\n  canceled: \"canceled\",\n  failed: \"failed\",\n  completed: \"completed\"\n}\n\nexport const SystemStatusCode = {\n  networkFailure: 0,\n  timeoutFailure: -1,\n  contentTypeMismatch: -2\n}\n\nexport const Direction = {\n  advance: \"forward\",\n  restore: \"back\",\n  replace: \"none\"\n}\n\nexport class Visit {\n  identifier = uuid() // Required by turbo-ios\n  timingMetrics = {}\n\n  followedRedirect = false\n  historyChanged = false\n  scrolled = false\n  shouldCacheSnapshot = true\n  acceptsStreamResponse = false\n  snapshotCached = false\n  state = VisitState.initialized\n  viewTransitioner = new ViewTransitioner()\n\n  constructor(delegate, location, restorationIdentifier, options = {}) {\n    this.delegate = delegate\n    this.location = location\n    this.restorationIdentifier = restorationIdentifier || uuid()\n\n    const {\n      action,\n      historyChanged,\n      referrer,\n      snapshot,\n      snapshotHTML,\n      response,\n      visitCachedSnapshot,\n      willRender,\n      updateHistory,\n      shouldCacheSnapshot,\n      acceptsStreamResponse,\n      direction,\n      refresh\n    } = {\n      ...defaultOptions,\n      ...options\n    }\n    this.action = action\n    this.historyChanged = historyChanged\n    this.referrer = referrer\n    this.snapshot = snapshot\n    this.snapshotHTML = snapshotHTML\n    this.response = response\n    this.isPageRefresh = this.view.isPageRefresh(this)\n    this.visitCachedSnapshot = visitCachedSnapshot\n    this.willRender = willRender\n    this.updateHistory = updateHistory\n    this.scrolled = !willRender\n    this.shouldCacheSnapshot = shouldCacheSnapshot\n    this.acceptsStreamResponse = acceptsStreamResponse\n    this.direction = direction || Direction[action]\n    this.refresh = refresh\n  }\n\n  get adapter() {\n    return this.delegate.adapter\n  }\n\n  get view() {\n    return this.delegate.view\n  }\n\n  get history() {\n    return this.delegate.history\n  }\n\n  get restorationData() {\n    return this.history.getRestorationDataForIdentifier(this.restorationIdentifier)\n  }\n\n  start() {\n    if (this.state == VisitState.initialized) {\n      this.recordTimingMetric(TimingMetric.visitStart)\n      this.state = VisitState.started\n      this.adapter.visitStarted(this)\n      this.delegate.visitStarted(this)\n    }\n  }\n\n  cancel() {\n    if (this.state == VisitState.started) {\n      if (this.request) {\n        this.request.cancel()\n      }\n      this.cancelRender()\n      this.state = VisitState.canceled\n    }\n  }\n\n  complete() {\n    if (this.state == VisitState.started) {\n      this.recordTimingMetric(TimingMetric.visitEnd)\n      this.adapter.visitCompleted(this)\n      this.state = VisitState.completed\n      this.followRedirect()\n\n      if (!this.followedRedirect) {\n        this.delegate.visitCompleted(this)\n      }\n    }\n  }\n\n  fail() {\n    if (this.state == VisitState.started) {\n      this.state = VisitState.failed\n      this.adapter.visitFailed(this)\n      this.delegate.visitCompleted(this)\n    }\n  }\n\n  changeHistory() {\n    if (!this.historyChanged && this.updateHistory) {\n      const actionForHistory = this.location.href === this.referrer?.href ? \"replace\" : this.action\n      const method = getHistoryMethodForAction(actionForHistory)\n      this.history.update(method, this.location, this.restorationIdentifier)\n      this.historyChanged = true\n    }\n  }\n\n  issueRequest() {\n    if (this.hasPreloadedResponse()) {\n      this.simulateRequest()\n    } else if (this.shouldIssueRequest() && !this.request) {\n      this.request = new FetchRequest(this, FetchMethod.get, this.location)\n      this.request.perform()\n    }\n  }\n\n  simulateRequest() {\n    if (this.response) {\n      this.startRequest()\n      this.recordResponse()\n      this.finishRequest()\n    }\n  }\n\n  startRequest() {\n    this.recordTimingMetric(TimingMetric.requestStart)\n    this.adapter.visitRequestStarted(this)\n  }\n\n  recordResponse(response = this.response) {\n    this.response = response\n    if (response) {\n      const { statusCode } = response\n      if (isSuccessful(statusCode)) {\n        this.adapter.visitRequestCompleted(this)\n      } else {\n        this.adapter.visitRequestFailedWithStatusCode(this, statusCode)\n      }\n    }\n  }\n\n  finishRequest() {\n    this.recordTimingMetric(TimingMetric.requestEnd)\n    this.adapter.visitRequestFinished(this)\n  }\n\n  loadResponse() {\n    if (this.response) {\n      const { statusCode, responseHTML } = this.response\n      this.render(async () => {\n        if (this.shouldCacheSnapshot) this.cacheSnapshot()\n        if (this.view.renderPromise) await this.view.renderPromise\n\n        if (isSuccessful(statusCode) && responseHTML != null) {\n          const snapshot = PageSnapshot.fromHTMLString(responseHTML)\n          await this.renderPageSnapshot(snapshot, false)\n\n          this.adapter.visitRendered(this)\n          this.complete()\n        } else {\n          await this.view.renderError(PageSnapshot.fromHTMLString(responseHTML), this)\n          this.adapter.visitRendered(this)\n          this.fail()\n        }\n      })\n    }\n  }\n\n  getCachedSnapshot() {\n    const snapshot = this.view.getCachedSnapshotForLocation(this.location) || this.getPreloadedSnapshot()\n\n    if (snapshot && (!getAnchor(this.location) || snapshot.hasAnchor(getAnchor(this.location)))) {\n      if (this.action == \"restore\" || snapshot.isPreviewable) {\n        return snapshot\n      }\n    }\n  }\n\n  getPreloadedSnapshot() {\n    if (this.snapshotHTML) {\n      return PageSnapshot.fromHTMLString(this.snapshotHTML)\n    }\n  }\n\n  hasCachedSnapshot() {\n    return this.getCachedSnapshot() != null\n  }\n\n  loadCachedSnapshot() {\n    const snapshot = this.getCachedSnapshot()\n    if (snapshot) {\n      const isPreview = this.shouldIssueRequest()\n      this.render(async () => {\n        this.cacheSnapshot()\n        if (this.isPageRefresh) {\n          this.adapter.visitRendered(this)\n        } else {\n          if (this.view.renderPromise) await this.view.renderPromise\n\n          await this.renderPageSnapshot(snapshot, isPreview)\n\n          this.adapter.visitRendered(this)\n          if (!isPreview) {\n            this.complete()\n          }\n        }\n      })\n    }\n  }\n\n  followRedirect() {\n    if (this.redirectedToLocation && !this.followedRedirect && this.response?.redirected) {\n      this.adapter.visitProposedToLocation(this.redirectedToLocation, {\n        action: \"replace\",\n        response: this.response,\n        shouldCacheSnapshot: false,\n        willRender: false\n      })\n      this.followedRedirect = true\n    }\n  }\n\n  // Fetch request delegate\n\n  prepareRequest(request) {\n    if (this.acceptsStreamResponse) {\n      request.acceptResponseType(StreamMessage.contentType)\n    }\n  }\n\n  requestStarted() {\n    this.startRequest()\n  }\n\n  requestPreventedHandlingResponse(_request, _response) {}\n\n  async requestSucceededWithResponse(request, response) {\n    const responseHTML = await response.responseHTML\n    const { redirected, statusCode } = response\n    if (responseHTML == undefined) {\n      this.recordResponse({\n        statusCode: SystemStatusCode.contentTypeMismatch,\n        redirected\n      })\n    } else {\n      this.redirectedToLocation = response.redirected ? response.location : undefined\n      this.recordResponse({ statusCode: statusCode, responseHTML, redirected })\n    }\n  }\n\n  async requestFailedWithResponse(request, response) {\n    const responseHTML = await response.responseHTML\n    const { redirected, statusCode } = response\n    if (responseHTML == undefined) {\n      this.recordResponse({\n        statusCode: SystemStatusCode.contentTypeMismatch,\n        redirected\n      })\n    } else {\n      this.recordResponse({ statusCode: statusCode, responseHTML, redirected })\n    }\n  }\n\n  requestErrored(_request, _error) {\n    this.recordResponse({\n      statusCode: SystemStatusCode.networkFailure,\n      redirected: false\n    })\n  }\n\n  requestFinished() {\n    this.finishRequest()\n  }\n\n  // Scrolling\n\n  performScroll() {\n    if (!this.scrolled && !this.view.forceReloaded && !this.view.shouldPreserveScrollPosition(this)) {\n      if (this.action == \"restore\") {\n        this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop()\n      } else {\n        this.scrollToAnchor() || this.view.scrollToTop()\n      }\n\n      this.scrolled = true\n    }\n  }\n\n  scrollToRestoredPosition() {\n    const { scrollPosition } = this.restorationData\n    if (scrollPosition) {\n      this.view.scrollToPosition(scrollPosition)\n      return true\n    }\n  }\n\n  scrollToAnchor() {\n    const anchor = getAnchor(this.location)\n    if (anchor != null) {\n      this.view.scrollToAnchor(anchor)\n      return true\n    }\n  }\n\n  // Instrumentation\n\n  recordTimingMetric(metric) {\n    this.timingMetrics[metric] = new Date().getTime()\n  }\n\n  getTimingMetrics() {\n    return { ...this.timingMetrics }\n  }\n\n  // Private\n\n  hasPreloadedResponse() {\n    return typeof this.response == \"object\"\n  }\n\n  shouldIssueRequest() {\n    if (this.action == \"restore\") {\n      return !this.hasCachedSnapshot()\n    } else {\n      return this.willRender\n    }\n  }\n\n  cacheSnapshot() {\n    if (!this.snapshotCached) {\n      this.view.cacheSnapshot(this.snapshot).then((snapshot) => snapshot && this.visitCachedSnapshot(snapshot))\n      this.snapshotCached = true\n    }\n  }\n\n  async render(callback) {\n    this.cancelRender()\n    await new Promise((resolve) => {\n      this.frame =\n        document.visibilityState === \"hidden\" ? setTimeout(() => resolve(), 0) : requestAnimationFrame(() => resolve())\n    })\n    await callback()\n    delete this.frame\n  }\n\n  async renderPageSnapshot(snapshot, isPreview) {\n    await this.viewTransitioner.renderChange(this.view.shouldTransitionTo(snapshot), async () => {\n      await this.view.renderPage(snapshot, isPreview, this.willRender, this)\n      this.performScroll()\n    })\n  }\n\n  cancelRender() {\n    if (this.frame) {\n      cancelAnimationFrame(this.frame)\n      delete this.frame\n    }\n  }\n}\n\nfunction isSuccessful(statusCode) {\n  return statusCode >= 200 && statusCode < 300\n}\n"
  },
  {
    "path": "src/core/errors.js",
    "content": "export class TurboFrameMissingError extends Error {}\n"
  },
  {
    "path": "src/core/frames/frame_controller.js",
    "content": "import { FrameElement, FrameLoadingStyle } from \"../../elements/frame_element\"\nimport { FetchMethod, FetchRequest } from \"../../http/fetch_request\"\nimport { FetchResponse } from \"../../http/fetch_response\"\nimport { AppearanceObserver } from \"../../observers/appearance_observer\"\nimport {\n  clearBusyState,\n  dispatch,\n  getAttribute,\n  parseHTMLDocument,\n  markAsBusy,\n  uuid,\n  getHistoryMethodForAction,\n  getVisitAction\n} from \"../../util\"\nimport { FormSubmission } from \"../drive/form_submission\"\nimport { Snapshot } from \"../snapshot\"\nimport { getAction, expandURL, urlsAreEqual, locationIsVisitable } from \"../url\"\nimport { FormSubmitObserver } from \"../../observers/form_submit_observer\"\nimport { FrameView } from \"./frame_view\"\nimport { LinkInterceptor } from \"./link_interceptor\"\nimport { FormLinkClickObserver } from \"../../observers/form_link_click_observer\"\nimport { FrameRenderer } from \"./frame_renderer\"\nimport { MorphingFrameRenderer } from \"./morphing_frame_renderer\"\nimport { session } from \"../index\"\nimport { StreamMessage } from \"../streams/stream_message\"\nimport { PageSnapshot } from \"../drive/page_snapshot\"\nimport { TurboFrameMissingError } from \"../errors\"\n\nexport class FrameController {\n  fetchResponseLoaded = (_fetchResponse) => Promise.resolve()\n  #currentFetchRequest = null\n  #resolveVisitPromise = () => {}\n  #connected = false\n  #hasBeenLoaded = false\n  #ignoredAttributes = new Set()\n  #shouldMorphFrame = false\n  action = null\n\n  constructor(element) {\n    this.element = element\n    this.view = new FrameView(this, this.element)\n    this.appearanceObserver = new AppearanceObserver(this, this.element)\n    this.formLinkClickObserver = new FormLinkClickObserver(this, this.element)\n    this.linkInterceptor = new LinkInterceptor(this, this.element)\n    this.restorationIdentifier = uuid()\n    this.formSubmitObserver = new FormSubmitObserver(this, this.element)\n  }\n\n  // Frame delegate\n\n  connect() {\n    if (!this.#connected) {\n      this.#connected = true\n      if (this.loadingStyle == FrameLoadingStyle.lazy) {\n        this.appearanceObserver.start()\n      } else {\n        this.#loadSourceURL()\n      }\n      this.formLinkClickObserver.start()\n      this.linkInterceptor.start()\n      this.formSubmitObserver.start()\n    }\n  }\n\n  disconnect() {\n    if (this.#connected) {\n      this.#connected = false\n      this.appearanceObserver.stop()\n      this.formLinkClickObserver.stop()\n      this.linkInterceptor.stop()\n      this.formSubmitObserver.stop()\n\n      if (!this.element.hasAttribute(\"recurse\")) {\n        this.#currentFetchRequest?.cancel()\n      }\n    }\n  }\n\n  disabledChanged() {\n    if (this.disabled) {\n      this.#currentFetchRequest?.cancel()\n    } else if (this.loadingStyle == FrameLoadingStyle.eager) {\n      this.#loadSourceURL()\n    }\n  }\n\n  sourceURLChanged() {\n    if (this.#isIgnoringChangesTo(\"src\")) return\n\n    if (!this.sourceURL) {\n      this.#currentFetchRequest?.cancel()\n    }\n\n    if (this.element.isConnected) {\n      this.complete = false\n    }\n\n    if (this.loadingStyle == FrameLoadingStyle.eager || this.#hasBeenLoaded) {\n      this.#loadSourceURL()\n    }\n  }\n\n  sourceURLReloaded() {\n    const { refresh, src } = this.element\n\n    this.#shouldMorphFrame = src && refresh === \"morph\"\n\n    this.element.removeAttribute(\"complete\")\n    this.element.src = null\n    this.element.src = src\n    return this.element.loaded\n  }\n\n  loadingStyleChanged() {\n    if (this.loadingStyle == FrameLoadingStyle.lazy) {\n      this.appearanceObserver.start()\n    } else {\n      this.appearanceObserver.stop()\n      this.#loadSourceURL()\n    }\n  }\n\n  async #loadSourceURL() {\n    if (this.enabled && this.isActive && !this.complete && this.sourceURL) {\n      this.element.loaded = this.#visit(expandURL(this.sourceURL))\n      this.appearanceObserver.stop()\n      await this.element.loaded\n      this.#hasBeenLoaded = true\n    }\n  }\n\n  async loadResponse(fetchResponse) {\n    if (fetchResponse.redirected || (fetchResponse.succeeded && fetchResponse.isHTML)) {\n      this.sourceURL = fetchResponse.response.url\n    }\n\n    try {\n      const html = await fetchResponse.responseHTML\n      if (html) {\n        const document = parseHTMLDocument(html)\n        const pageSnapshot = PageSnapshot.fromDocument(document)\n\n        if (pageSnapshot.isVisitable) {\n          await this.#loadFrameResponse(fetchResponse, document)\n        } else {\n          await this.#handleUnvisitableFrameResponse(fetchResponse)\n        }\n      }\n    } finally {\n      this.#shouldMorphFrame = false\n      this.fetchResponseLoaded = () => Promise.resolve()\n    }\n  }\n\n  // Appearance observer delegate\n\n  elementAppearedInViewport(element) {\n    this.proposeVisitIfNavigatedWithAction(element, getVisitAction(element))\n    this.#loadSourceURL()\n  }\n\n  // Form link click observer delegate\n\n  willSubmitFormLinkToLocation(link) {\n    return this.#shouldInterceptNavigation(link)\n  }\n\n  submittedFormLinkToLocation(link, _location, form) {\n    const frame = this.#findFrameElement(link)\n    if (frame) form.setAttribute(\"data-turbo-frame\", frame.id)\n  }\n\n  // Link interceptor delegate\n\n  shouldInterceptLinkClick(element, _location, _event) {\n    return this.#shouldInterceptNavigation(element)\n  }\n\n  linkClickIntercepted(element, location) {\n    this.#navigateFrame(element, location)\n  }\n\n  // Form submit observer delegate\n\n  willSubmitForm(element, submitter) {\n    return element.closest(\"turbo-frame\") == this.element && this.#shouldInterceptNavigation(element, submitter)\n  }\n\n  formSubmitted(element, submitter) {\n    if (this.formSubmission) {\n      this.formSubmission.stop()\n    }\n\n    this.formSubmission = new FormSubmission(this, element, submitter)\n\n    const { fetchRequest } = this.formSubmission\n    const frame = this.#findFrameElement(element, submitter)\n\n    this.prepareRequest(fetchRequest, frame)\n    this.formSubmission.start()\n  }\n\n  // Fetch request delegate\n\n  prepareRequest(request, frame = this) {\n    request.headers[\"Turbo-Frame\"] = frame.id\n\n    if (this.currentNavigationElement?.hasAttribute(\"data-turbo-stream\")) {\n      request.acceptResponseType(StreamMessage.contentType)\n    }\n  }\n\n  requestStarted(_request) {\n    markAsBusy(this.element)\n  }\n\n  requestPreventedHandlingResponse(_request, _response) {\n    this.#resolveVisitPromise()\n  }\n\n  async requestSucceededWithResponse(request, response) {\n    await this.loadResponse(response)\n    this.#resolveVisitPromise()\n  }\n\n  async requestFailedWithResponse(request, response) {\n    await this.loadResponse(response)\n    this.#resolveVisitPromise()\n  }\n\n  requestErrored(request, error) {\n    console.error(error)\n    this.#resolveVisitPromise()\n  }\n\n  requestFinished(_request) {\n    clearBusyState(this.element)\n  }\n\n  // Form submission delegate\n\n  formSubmissionStarted({ formElement }) {\n    markAsBusy(formElement, this.#findFrameElement(formElement))\n  }\n\n  formSubmissionSucceededWithResponse(formSubmission, response) {\n    const frame = this.#findFrameElement(formSubmission.formElement, formSubmission.submitter)\n\n    frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(formSubmission.submitter, formSubmission.formElement, frame))\n    frame.delegate.loadResponse(response)\n\n    if (!formSubmission.isSafe) {\n      session.clearCache()\n    }\n  }\n\n  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {\n    this.element.delegate.loadResponse(fetchResponse)\n    session.clearCache()\n  }\n\n  formSubmissionErrored(formSubmission, error) {\n    console.error(error)\n  }\n\n  formSubmissionFinished({ formElement }) {\n    clearBusyState(formElement, this.#findFrameElement(formElement))\n  }\n\n  // View delegate\n\n  allowsImmediateRender({ element: newFrame }, options) {\n    const event = dispatch(\"turbo:before-frame-render\", {\n      target: this.element,\n      detail: { newFrame, ...options },\n      cancelable: true\n    })\n\n    const {\n      defaultPrevented,\n      detail: { render }\n    } = event\n\n    if (this.view.renderer && render) {\n      this.view.renderer.renderElement = render\n    }\n\n    return !defaultPrevented\n  }\n\n  viewRenderedSnapshot(_snapshot, _isPreview, _renderMethod) {}\n\n  preloadOnLoadLinksForView(element) {\n    session.preloadOnLoadLinksForView(element)\n  }\n\n  viewInvalidated() {}\n\n  // Frame renderer delegate\n\n  willRenderFrame(currentElement, _newElement) {\n    this.previousFrameElement = currentElement.cloneNode(true)\n  }\n\n  visitCachedSnapshot = ({ element }) => {\n    const frame = element.querySelector(\"#\" + this.element.id)\n\n    if (frame && this.previousFrameElement) {\n      frame.replaceChildren(...this.previousFrameElement.children)\n    }\n\n    delete this.previousFrameElement\n  }\n\n  // Private\n\n  async #loadFrameResponse(fetchResponse, document) {\n    const newFrameElement = await this.extractForeignFrameElement(document.body)\n    const rendererClass = this.#shouldMorphFrame ? MorphingFrameRenderer : FrameRenderer\n\n    if (newFrameElement) {\n      const snapshot = new Snapshot(newFrameElement)\n      const renderer = new rendererClass(this, this.view.snapshot, snapshot, false, false)\n      if (this.view.renderPromise) await this.view.renderPromise\n      this.changeHistory()\n\n      await this.view.render(renderer)\n      this.complete = true\n      session.frameRendered(fetchResponse, this.element)\n      session.frameLoaded(this.element)\n      await this.fetchResponseLoaded(fetchResponse)\n    } else if (this.#willHandleFrameMissingFromResponse(fetchResponse)) {\n      this.#handleFrameMissingFromResponse(fetchResponse)\n    }\n  }\n\n  async #visit(url) {\n    const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams(), this.element)\n\n    this.#currentFetchRequest?.cancel()\n    this.#currentFetchRequest = request\n\n    return new Promise((resolve) => {\n      this.#resolveVisitPromise = () => {\n        this.#resolveVisitPromise = () => {}\n        this.#currentFetchRequest = null\n        resolve()\n      }\n      request.perform()\n    })\n  }\n\n  #navigateFrame(element, url, submitter) {\n    const frame = this.#findFrameElement(element, submitter)\n\n    frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(submitter, element, frame))\n\n    this.#withCurrentNavigationElement(element, () => {\n      frame.src = url\n    })\n  }\n\n  proposeVisitIfNavigatedWithAction(frame, action = null) {\n    this.action = action\n\n    if (this.action) {\n      const pageSnapshot = PageSnapshot.fromElement(frame).clone()\n      const { visitCachedSnapshot } = frame.delegate\n\n      frame.delegate.fetchResponseLoaded = async (fetchResponse) => {\n        if (frame.src) {\n          const { statusCode, redirected } = fetchResponse\n          const responseHTML = await fetchResponse.responseHTML\n          const response = { statusCode, redirected, responseHTML }\n          const options = {\n            response,\n            visitCachedSnapshot,\n            willRender: false,\n            updateHistory: false,\n            restorationIdentifier: this.restorationIdentifier,\n            snapshot: pageSnapshot\n          }\n\n          if (this.action) options.action = this.action\n\n          session.visit(frame.src, options)\n        }\n      }\n    }\n  }\n\n  changeHistory() {\n    if (this.action) {\n      const method = getHistoryMethodForAction(this.action)\n      session.history.update(method, expandURL(this.element.src || \"\"), this.restorationIdentifier)\n    }\n  }\n\n  async #handleUnvisitableFrameResponse(fetchResponse) {\n    console.warn(\n      `The response (${fetchResponse.statusCode}) from <turbo-frame id=\"${this.element.id}\"> is performing a full page visit due to turbo-visit-control.`\n    )\n\n    await this.#visitResponse(fetchResponse.response)\n  }\n\n  #willHandleFrameMissingFromResponse(fetchResponse) {\n    this.element.setAttribute(\"complete\", \"\")\n\n    const response = fetchResponse.response\n    const visit = async (url, options) => {\n      if (url instanceof Response) {\n        this.#visitResponse(url)\n      } else {\n        session.visit(url, options)\n      }\n    }\n\n    const event = dispatch(\"turbo:frame-missing\", {\n      target: this.element,\n      detail: { response, visit },\n      cancelable: true\n    })\n\n    return !event.defaultPrevented\n  }\n\n  #handleFrameMissingFromResponse(fetchResponse) {\n    this.view.missing()\n    this.#throwFrameMissingError(fetchResponse)\n  }\n\n  #throwFrameMissingError(fetchResponse) {\n    const message = `The response (${fetchResponse.statusCode}) did not contain the expected <turbo-frame id=\"${this.element.id}\"> and will be ignored. To perform a full page visit instead, set turbo-visit-control to reload.`\n    throw new TurboFrameMissingError(message)\n  }\n\n  async #visitResponse(response) {\n    const wrapped = new FetchResponse(response)\n    const responseHTML = await wrapped.responseHTML\n    const { location, redirected, statusCode } = wrapped\n\n    return session.visit(location, { response: { redirected, statusCode, responseHTML } })\n  }\n\n  #findFrameElement(element, submitter) {\n    const id = getAttribute(\"data-turbo-frame\", submitter, element) || this.element.getAttribute(\"target\")\n    const target = this.#getFrameElementById(id)\n\n    return target instanceof FrameElement ? target : this.element\n  }\n\n  async extractForeignFrameElement(container) {\n    let element\n    const id = CSS.escape(this.id)\n\n    try {\n      element = activateElement(container.querySelector(`turbo-frame#${id}`), this.sourceURL)\n      if (element) {\n        return element\n      }\n\n      element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.sourceURL)\n      if (element) {\n        await element.loaded\n        return await this.extractForeignFrameElement(element)\n      }\n    } catch (error) {\n      console.error(error)\n      return new FrameElement()\n    }\n\n    return null\n  }\n\n  #formActionIsVisitable(form, submitter) {\n    const action = getAction(form, submitter)\n\n    return locationIsVisitable(expandURL(action), this.rootLocation)\n  }\n\n  #shouldInterceptNavigation(element, submitter) {\n    const id = getAttribute(\"data-turbo-frame\", submitter, element) || this.element.getAttribute(\"target\")\n\n    if (element instanceof HTMLFormElement && !this.#formActionIsVisitable(element, submitter)) {\n      return false\n    }\n\n    if (!this.enabled || id == \"_top\") {\n      return false\n    }\n\n    if (id) {\n      const frameElement = this.#getFrameElementById(id)\n      if (frameElement) {\n        return !frameElement.disabled\n      } else if (id == \"_parent\") {\n        return false\n      }\n    }\n\n    if (!session.elementIsNavigatable(element)) {\n      return false\n    }\n\n    if (submitter && !session.elementIsNavigatable(submitter)) {\n      return false\n    }\n\n    return true\n  }\n\n  // Computed properties\n\n  get id() {\n    return this.element.id\n  }\n\n  get disabled() {\n    return this.element.disabled\n  }\n\n  get enabled() {\n    return !this.disabled\n  }\n\n  get sourceURL() {\n    if (this.element.src) {\n      return this.element.src\n    }\n  }\n\n  set sourceURL(sourceURL) {\n    this.#ignoringChangesToAttribute(\"src\", () => {\n      this.element.src = sourceURL ?? null\n    })\n  }\n\n  get loadingStyle() {\n    return this.element.loading\n  }\n\n  get isLoading() {\n    return this.formSubmission !== undefined || this.#resolveVisitPromise() !== undefined\n  }\n\n  get complete() {\n    return this.element.hasAttribute(\"complete\")\n  }\n\n  set complete(value) {\n    if (value) {\n      this.element.setAttribute(\"complete\", \"\")\n    } else {\n      this.element.removeAttribute(\"complete\")\n    }\n  }\n\n  get isActive() {\n    return this.element.isActive && this.#connected\n  }\n\n  get rootLocation() {\n    const meta = this.element.ownerDocument.querySelector(`meta[name=\"turbo-root\"]`)\n    const root = meta?.content ?? \"/\"\n    return expandURL(root)\n  }\n\n  #isIgnoringChangesTo(attributeName) {\n    return this.#ignoredAttributes.has(attributeName)\n  }\n\n  #ignoringChangesToAttribute(attributeName, callback) {\n    this.#ignoredAttributes.add(attributeName)\n    callback()\n    this.#ignoredAttributes.delete(attributeName)\n  }\n\n  #withCurrentNavigationElement(element, callback) {\n    this.currentNavigationElement = element\n    callback()\n    delete this.currentNavigationElement\n  }\n\n  #getFrameElementById(id) {\n    if (id != null) {\n      const element = id === \"_parent\" ?\n        this.element.parentElement.closest(\"turbo-frame\") :\n        document.getElementById(id)\n      if (element instanceof FrameElement) {\n        return element\n      }\n    }\n  }\n}\n\nfunction activateElement(element, currentURL) {\n  if (element) {\n    const src = element.getAttribute(\"src\")\n    if (src != null && currentURL != null && urlsAreEqual(src, currentURL)) {\n      throw new Error(`Matching <turbo-frame id=\"${element.id}\"> element has a source URL which references itself`)\n    }\n    if (element.ownerDocument !== document) {\n      element = document.importNode(element, true)\n    }\n\n    if (element instanceof FrameElement) {\n      element.connectedCallback()\n      element.disconnectedCallback()\n      return element\n    }\n  }\n}\n"
  },
  {
    "path": "src/core/frames/frame_redirector.js",
    "content": "import { FormSubmitObserver } from \"../../observers/form_submit_observer\"\nimport { FrameElement } from \"../../elements/frame_element\"\nimport { LinkInterceptor } from \"./link_interceptor\"\nimport { expandURL, getAction, locationIsVisitable } from \"../url\"\n\nexport class FrameRedirector {\n  constructor(session, element) {\n    this.session = session\n    this.element = element\n    this.linkInterceptor = new LinkInterceptor(this, element)\n    this.formSubmitObserver = new FormSubmitObserver(this, element)\n  }\n\n  start() {\n    this.linkInterceptor.start()\n    this.formSubmitObserver.start()\n  }\n\n  stop() {\n    this.linkInterceptor.stop()\n    this.formSubmitObserver.stop()\n  }\n\n  // Link interceptor delegate\n\n  shouldInterceptLinkClick(element, _location, _event) {\n    return this.#shouldRedirect(element)\n  }\n\n  linkClickIntercepted(element, url, event) {\n    const frame = this.#findFrameElement(element)\n    if (frame) {\n      frame.delegate.linkClickIntercepted(element, url, event)\n    }\n  }\n\n  // Form submit observer delegate\n\n  willSubmitForm(element, submitter) {\n    return (\n      element.closest(\"turbo-frame\") == null &&\n      this.#shouldSubmit(element, submitter) &&\n      this.#shouldRedirect(element, submitter)\n    )\n  }\n\n  formSubmitted(element, submitter) {\n    const frame = this.#findFrameElement(element, submitter)\n    if (frame) {\n      frame.delegate.formSubmitted(element, submitter)\n    }\n  }\n\n  #shouldSubmit(form, submitter) {\n    const action = getAction(form, submitter)\n    const meta = this.element.ownerDocument.querySelector(`meta[name=\"turbo-root\"]`)\n    const rootLocation = expandURL(meta?.content ?? \"/\")\n\n    return this.#shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation)\n  }\n\n  #shouldRedirect(element, submitter) {\n    const isNavigatable =\n      element instanceof HTMLFormElement\n        ? this.session.submissionIsNavigatable(element, submitter)\n        : this.session.elementIsNavigatable(element)\n\n    if (isNavigatable) {\n      const frame = this.#findFrameElement(element, submitter)\n      return frame ? frame != element.closest(\"turbo-frame\") : false\n    } else {\n      return false\n    }\n  }\n\n  #findFrameElement(element, submitter) {\n    const id = submitter?.getAttribute(\"data-turbo-frame\") || element.getAttribute(\"data-turbo-frame\")\n    if (id && id != \"_top\") {\n      const frame = this.element.querySelector(`#${id}:not([disabled])`)\n      if (frame instanceof FrameElement) {\n        return frame\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/core/frames/frame_renderer.js",
    "content": "import { activateScriptElement, nextRepaint } from \"../../util\"\nimport { Renderer } from \"../renderer\"\n\nexport class FrameRenderer extends Renderer {\n  static renderElement(currentElement, newElement) {\n    const destinationRange = document.createRange()\n    destinationRange.selectNodeContents(currentElement)\n    destinationRange.deleteContents()\n\n    const frameElement = newElement\n    const sourceRange = frameElement.ownerDocument?.createRange()\n    if (sourceRange) {\n      sourceRange.selectNodeContents(frameElement)\n      currentElement.appendChild(sourceRange.extractContents())\n    }\n  }\n\n  constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {\n    super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender)\n    this.delegate = delegate\n  }\n\n  get shouldRender() {\n    return true\n  }\n\n  async render() {\n    await nextRepaint()\n    this.preservingPermanentElements(() => {\n      this.loadFrameElement()\n    })\n    this.scrollFrameIntoView()\n    await nextRepaint()\n    this.focusFirstAutofocusableElement()\n    await nextRepaint()\n    this.activateScriptElements()\n  }\n\n  loadFrameElement() {\n    this.delegate.willRenderFrame(this.currentElement, this.newElement)\n    this.renderElement(this.currentElement, this.newElement)\n  }\n\n  scrollFrameIntoView() {\n    if (this.currentElement.autoscroll || this.newElement.autoscroll) {\n      const element = this.currentElement.firstElementChild\n      const block = readScrollLogicalPosition(this.currentElement.getAttribute(\"data-autoscroll-block\"), \"end\")\n      const behavior = readScrollBehavior(this.currentElement.getAttribute(\"data-autoscroll-behavior\"), \"auto\")\n\n      if (element) {\n        element.scrollIntoView({ block, behavior })\n        return true\n      }\n    }\n    return false\n  }\n\n  activateScriptElements() {\n    for (const inertScriptElement of this.newScriptElements) {\n      const activatedScriptElement = activateScriptElement(inertScriptElement)\n      inertScriptElement.replaceWith(activatedScriptElement)\n    }\n  }\n\n  get newScriptElements() {\n    return this.currentElement.querySelectorAll(\"script\")\n  }\n}\n\nfunction readScrollLogicalPosition(value, defaultValue) {\n  if (value == \"end\" || value == \"start\" || value == \"center\" || value == \"nearest\") {\n    return value\n  } else {\n    return defaultValue\n  }\n}\n\nfunction readScrollBehavior(value, defaultValue) {\n  if (value == \"auto\" || value == \"smooth\") {\n    return value\n  } else {\n    return defaultValue\n  }\n}\n"
  },
  {
    "path": "src/core/frames/frame_view.js",
    "content": "import { Snapshot } from \"../snapshot\"\nimport { View } from \"../view\"\n\nexport class FrameView extends View {\n  missing() {\n    this.element.innerHTML = `<strong class=\"turbo-frame-error\">Content missing</strong>`\n  }\n\n  get snapshot() {\n    return new Snapshot(this.element)\n  }\n}\n"
  },
  {
    "path": "src/core/frames/link_interceptor.js",
    "content": "import { findLinkFromClickTarget } from \"../../util\"\n\nexport class LinkInterceptor {\n  constructor(delegate, element) {\n    this.delegate = delegate\n    this.element = element\n  }\n\n  start() {\n    this.element.addEventListener(\"click\", this.clickBubbled)\n    document.addEventListener(\"turbo:click\", this.linkClicked)\n    document.addEventListener(\"turbo:before-visit\", this.willVisit)\n  }\n\n  stop() {\n    this.element.removeEventListener(\"click\", this.clickBubbled)\n    document.removeEventListener(\"turbo:click\", this.linkClicked)\n    document.removeEventListener(\"turbo:before-visit\", this.willVisit)\n  }\n\n  clickBubbled = (event) => {\n    if (this.clickEventIsSignificant(event)) {\n      this.clickEvent = event\n    } else {\n      delete this.clickEvent\n    }\n  }\n\n  linkClicked = (event) => {\n    if (this.clickEvent && this.clickEventIsSignificant(event)) {\n      if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {\n        this.clickEvent.preventDefault()\n        event.preventDefault()\n        this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent)\n      }\n    }\n    delete this.clickEvent\n  }\n\n  willVisit = (_event) => {\n    delete this.clickEvent\n  }\n\n  clickEventIsSignificant(event) {\n    const target = event.composed ? event.target?.parentElement : event.target\n    const element = findLinkFromClickTarget(target) || target\n\n    return element instanceof Element && element.closest(\"turbo-frame, html\") == this.element\n  }\n}\n"
  },
  {
    "path": "src/core/frames/morphing_frame_renderer.js",
    "content": "import { FrameRenderer } from \"./frame_renderer\"\nimport { morphChildren, shouldRefreshFrameWithMorphing, closestFrameReloadableWithMorphing } from \"../morphing\"\nimport { dispatch } from \"../../util\"\n\nexport class MorphingFrameRenderer extends FrameRenderer {\n  static renderElement(currentElement, newElement) {\n    dispatch(\"turbo:before-frame-morph\", {\n      target: currentElement,\n      detail: { currentElement, newElement }\n    })\n\n    morphChildren(currentElement, newElement, {\n      callbacks: {\n        beforeNodeMorphed: (node, newNode) => {\n          if (\n            shouldRefreshFrameWithMorphing(node, newNode) &&\n              closestFrameReloadableWithMorphing(node) === currentElement\n          ) {\n            node.reload()\n            return false\n          }\n          return true\n        }\n      }\n    })\n  }\n\n  async preservingPermanentElements(callback) {\n    return await callback()\n  }\n}\n\n"
  },
  {
    "path": "src/core/index.js",
    "content": "import { Session } from \"./session\"\nimport { PageRenderer } from \"./drive/page_renderer\"\nimport { PageSnapshot } from \"./drive/page_snapshot\"\nimport { FrameRenderer } from \"./frames/frame_renderer\"\nimport { fetch, recentRequests } from \"../http/fetch\"\nimport { config } from \"./config\"\nimport { MorphingPageRenderer } from \"./drive/morphing_page_renderer\"\nimport { MorphingFrameRenderer } from \"./frames/morphing_frame_renderer\"\n\nexport { morphChildren, morphElements } from \"./morphing\"\nexport { PageRenderer, PageSnapshot, FrameRenderer, fetch, config }\n\nconst session = new Session(recentRequests)\n\n// Rename `navigator` to avoid shadowing `window.navigator`\nconst { cache, navigator: sessionNavigator } = session\nexport { session, cache, sessionNavigator as navigator }\n\n/**\n * Starts the main session.\n * This initialises any necessary observers such as those to monitor\n * link interactions.\n */\nexport function start() {\n  session.start()\n}\n\n/**\n * Registers an adapter for the main session.\n *\n * @param adapter Adapter to register\n */\nexport function registerAdapter(adapter) {\n  session.registerAdapter(adapter)\n}\n\n/**\n * Performs an application visit to the given location.\n *\n * @param location Location to visit (a URL or path)\n * @param options Options to apply\n * @param options.action Type of history navigation to apply (\"restore\",\n * \"replace\" or \"advance\")\n * @param options.historyChanged Specifies whether the browser history has\n * already been changed for this visit or not\n * @param options.referrer Specifies the referrer of this visit such that\n * navigations to the same page will not result in a new history entry.\n * @param options.snapshotHTML Cached snapshot to render\n * @param options.response Response of the specified location\n */\nexport function visit(location, options) {\n  session.visit(location, options)\n}\n\n/**\n * Connects a stream source to the main session.\n *\n * @param source Stream source to connect\n */\nexport function connectStreamSource(source) {\n  session.connectStreamSource(source)\n}\n\n/**\n * Disconnects a stream source from the main session.\n *\n * @param source Stream source to disconnect\n */\nexport function disconnectStreamSource(source) {\n  session.disconnectStreamSource(source)\n}\n\n/**\n * Renders a stream message to the main session by appending it to the\n * current document.\n *\n * @param message Message to render\n */\nexport function renderStreamMessage(message) {\n  session.renderStreamMessage(message)\n}\n\n/**\n * Sets the delay after which the progress bar will appear during navigation.\n *\n * The progress bar appears after 500ms by default.\n *\n * Note that this method has no effect when used with the iOS or Android\n * adapters.\n *\n * @param delay Time to delay in milliseconds\n */\nexport function setProgressBarDelay(delay) {\n  console.warn(\n    \"Please replace `Turbo.setProgressBarDelay(delay)` with `Turbo.config.drive.progressBarDelay = delay`. The top-level function is deprecated and will be removed in a future version of Turbo.`\"\n  )\n  config.drive.progressBarDelay = delay\n}\n\nexport function setConfirmMethod(confirmMethod) {\n  console.warn(\n    \"Please replace `Turbo.setConfirmMethod(confirmMethod)` with `Turbo.config.forms.confirm = confirmMethod`. The top-level function is deprecated and will be removed in a future version of Turbo.`\"\n  )\n  config.forms.confirm = confirmMethod\n}\n\nexport function setFormMode(mode) {\n  console.warn(\n    \"Please replace `Turbo.setFormMode(mode)` with `Turbo.config.forms.mode = mode`. The top-level function is deprecated and will be removed in a future version of Turbo.`\"\n  )\n  config.forms.mode = mode\n}\n\n/**\n * Morph the state of the currentBody based on the attributes and contents of\n * the newBody. Morphing body elements may dispatch turbo:morph,\n * turbo:before-morph-element, turbo:before-morph-attribute, and\n * turbo:morph-element events.\n *\n * @param currentBody HTMLBodyElement destination of morphing changes\n * @param newBody HTMLBodyElement source of morphing changes\n */\nexport function morphBodyElements(currentBody, newBody) {\n  MorphingPageRenderer.renderElement(currentBody, newBody)\n}\n\n/**\n * Morph the child elements of the currentFrame based on the child elements of\n * the newFrame. Morphing turbo-frame elements may dispatch turbo:before-frame-morph,\n * turbo:before-morph-element, turbo:before-morph-attribute, and\n * turbo:morph-element events.\n *\n * @param currentFrame FrameElement destination of morphing children changes\n * @param newFrame FrameElement source of morphing children changes\n */\nexport function morphTurboFrameElements(currentFrame, newFrame) {\n  MorphingFrameRenderer.renderElement(currentFrame, newFrame)\n}\n"
  },
  {
    "path": "src/core/lru_cache.js",
    "content": "const identity = key => key\n\nexport class LRUCache {\n  keys = []\n  entries = {}\n  #toCacheKey\n\n  constructor(size, toCacheKey = identity) {\n    this.size = size\n    this.#toCacheKey = toCacheKey\n  }\n\n  has(key) {\n    return this.#toCacheKey(key) in this.entries\n  }\n\n  get(key) {\n    if (this.has(key)) {\n      const entry = this.read(key)\n      this.touch(key)\n      return entry\n    }\n  }\n\n  put(key, entry) {\n    this.write(key, entry)\n    this.touch(key)\n    return entry\n  }\n\n  clear() {\n    for (const key of Object.keys(this.entries)) {\n      this.evict(key)\n    }\n  }\n\n  // Private\n\n  read(key) {\n    return this.entries[this.#toCacheKey(key)]\n  }\n\n  write(key, entry) {\n    this.entries[this.#toCacheKey(key)] = entry\n  }\n\n  touch(key) {\n    key = this.#toCacheKey(key)\n    const index = this.keys.indexOf(key)\n    if (index > -1) this.keys.splice(index, 1)\n    this.keys.unshift(key)\n    this.trim()\n  }\n\n  trim() {\n    for (const key of this.keys.splice(this.size)) {\n      this.evict(key)\n    }\n  }\n\n  evict(key) {\n    delete this.entries[key]\n  }\n}\n"
  },
  {
    "path": "src/core/morphing.js",
    "content": "import { Idiomorph } from \"idiomorph\"\nimport { FrameElement } from \"../elements/frame_element\"\nimport { dispatch } from \"../util\"\nimport { urlsAreEqual } from \"./url\"\n\n/**\n * Morph the state of the currentElement based on the attributes and contents of\n * the newElement. Morphing may dispatch turbo:before-morph-element,\n * turbo:before-morph-attribute, and turbo:morph-element events.\n *\n * @param currentElement Element destination of morphing changes\n * @param newElement Element source of morphing changes\n */\nexport function morphElements(currentElement, newElement, { callbacks, ...options } = {}) {\n  Idiomorph.morph(currentElement, newElement, {\n    ...options,\n    callbacks: new DefaultIdiomorphCallbacks(callbacks)\n  })\n}\n\n/**\n * Morph the child elements of the currentElement based on the child elements of\n * the newElement. Morphing children may dispatch turbo:before-morph-element,\n * turbo:before-morph-attribute, and turbo:morph-element events.\n *\n * @param currentElement Element destination of morphing children changes\n * @param newElement Element source of morphing children changes\n */\nexport function morphChildren(currentElement, newElement, options = {}) {\n  morphElements(currentElement, newElement.childNodes, {\n    ...options,\n    morphStyle: \"innerHTML\"\n  })\n}\n\nexport function shouldRefreshFrameWithMorphing(currentFrame, newFrame) {\n  return currentFrame instanceof FrameElement &&\n    currentFrame.shouldReloadWithMorph && (!newFrame || areFramesCompatibleForRefreshing(currentFrame, newFrame)) &&\n    !currentFrame.closest(\"[data-turbo-permanent]\")\n}\n\nfunction areFramesCompatibleForRefreshing(currentFrame, newFrame) {\n  // newFrame cannot yet be an instance of FrameElement because custom\n  // elements don't get initialized until they're attached to the DOM, so\n  // test its Element#nodeName instead\n  return newFrame instanceof Element && newFrame.nodeName === \"TURBO-FRAME\" && currentFrame.id === newFrame.id &&\n  (!newFrame.getAttribute(\"src\") || urlsAreEqual(currentFrame.src, newFrame.getAttribute(\"src\")))\n}\n\nexport function closestFrameReloadableWithMorphing(node) {\n  return node.parentElement.closest(\"turbo-frame[src][refresh=morph]\")\n}\n\nclass DefaultIdiomorphCallbacks {\n  #beforeNodeMorphed\n\n  constructor({ beforeNodeMorphed } = {}) {\n    this.#beforeNodeMorphed = beforeNodeMorphed || (() => true)\n  }\n\n  beforeNodeAdded = (node) => {\n    return !(node.id && node.hasAttribute(\"data-turbo-permanent\") && document.getElementById(node.id))\n  }\n\n  beforeNodeMorphed = (currentElement, newElement) => {\n    if (currentElement instanceof Element) {\n      if (!currentElement.hasAttribute(\"data-turbo-permanent\") && this.#beforeNodeMorphed(currentElement, newElement)) {\n        const event = dispatch(\"turbo:before-morph-element\", {\n          cancelable: true,\n          target: currentElement,\n          detail: { currentElement, newElement }\n        })\n\n        return !event.defaultPrevented\n      } else {\n        return false\n      }\n    }\n  }\n\n  beforeAttributeUpdated = (attributeName, target, mutationType) => {\n    const event = dispatch(\"turbo:before-morph-attribute\", {\n      cancelable: true,\n      target,\n      detail: { attributeName, mutationType }\n    })\n\n    return !event.defaultPrevented\n  }\n\n  beforeNodeRemoved = (node) => {\n    return this.beforeNodeMorphed(node)\n  }\n\n  afterNodeMorphed = (currentElement, newElement) => {\n    if (currentElement instanceof Element) {\n      dispatch(\"turbo:morph-element\", {\n        target: currentElement,\n        detail: { currentElement, newElement }\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "src/core/native/browser_adapter.js",
    "content": "import { ProgressBar } from \"../drive/progress_bar\"\nimport { SystemStatusCode } from \"../drive/visit\"\nimport { uuid, dispatch } from \"../../util\"\nimport { locationIsVisitable } from \"../url\"\n\nexport class BrowserAdapter {\n  progressBar = new ProgressBar()\n\n  constructor(session) {\n    this.session = session\n  }\n\n  visitProposedToLocation(location, options) {\n    if (locationIsVisitable(location, this.navigator.rootLocation)) {\n      this.navigator.startVisit(location, options?.restorationIdentifier || uuid(), options)\n    } else {\n      window.location.href = location.toString()\n    }\n  }\n\n  visitStarted(visit) {\n    this.location = visit.location\n    this.redirectedToLocation = null\n\n    visit.loadCachedSnapshot()\n    visit.issueRequest()\n  }\n\n  visitRequestStarted(visit) {\n    this.progressBar.setValue(0)\n    if (visit.hasCachedSnapshot() || visit.action != \"restore\") {\n      this.showVisitProgressBarAfterDelay()\n    } else {\n      this.showProgressBar()\n    }\n  }\n\n  visitRequestCompleted(visit) {\n    visit.loadResponse()\n\n    if (visit.response.redirected) {\n      this.redirectedToLocation = visit.redirectedToLocation\n    }\n  }\n\n  visitRequestFailedWithStatusCode(visit, statusCode) {\n    switch (statusCode) {\n      case SystemStatusCode.networkFailure:\n      case SystemStatusCode.timeoutFailure:\n      case SystemStatusCode.contentTypeMismatch:\n        return this.reload({\n          reason: \"request_failed\",\n          context: {\n            statusCode\n          }\n        })\n      default:\n        return visit.loadResponse()\n    }\n  }\n\n  visitRequestFinished(_visit) {}\n\n  visitCompleted(_visit) {\n    this.progressBar.setValue(1)\n    this.hideVisitProgressBar()\n  }\n\n  pageInvalidated(reason) {\n    this.reload(reason)\n  }\n\n  visitFailed(_visit) {\n    this.progressBar.setValue(1)\n    this.hideVisitProgressBar()\n  }\n\n  visitRendered(_visit) {}\n\n  // Link prefetching\n\n  linkPrefetchingIsEnabledForLocation(location) {\n    return true\n  }\n\n  // Form Submission Delegate\n\n  formSubmissionStarted(_formSubmission) {\n    this.progressBar.setValue(0)\n    this.showFormProgressBarAfterDelay()\n  }\n\n  formSubmissionFinished(_formSubmission) {\n    this.progressBar.setValue(1)\n    this.hideFormProgressBar()\n  }\n\n  // Private\n\n  showVisitProgressBarAfterDelay() {\n    this.visitProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay)\n  }\n\n  hideVisitProgressBar() {\n    this.progressBar.hide()\n    if (this.visitProgressBarTimeout != null) {\n      window.clearTimeout(this.visitProgressBarTimeout)\n      delete this.visitProgressBarTimeout\n    }\n  }\n\n  showFormProgressBarAfterDelay() {\n    if (this.formProgressBarTimeout == null) {\n      this.formProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay)\n    }\n  }\n\n  hideFormProgressBar() {\n    this.progressBar.hide()\n    if (this.formProgressBarTimeout != null) {\n      window.clearTimeout(this.formProgressBarTimeout)\n      delete this.formProgressBarTimeout\n    }\n  }\n\n  showProgressBar = () => {\n    this.progressBar.show()\n  }\n\n  reload(reason) {\n    dispatch(\"turbo:reload\", { detail: reason })\n\n    window.location.href = (this.redirectedToLocation || this.location)?.toString() || window.location.href\n  }\n\n  get navigator() {\n    return this.session.navigator\n  }\n}\n"
  },
  {
    "path": "src/core/renderer.js",
    "content": "import { Bardo } from \"./bardo\"\n\nexport class Renderer {\n  #activeElement = null\n\n  static renderElement(currentElement, newElement) {\n    // Abstract method\n  }\n\n  constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {\n    this.currentSnapshot = currentSnapshot\n    this.newSnapshot = newSnapshot\n    this.isPreview = isPreview\n    this.willRender = willRender\n    this.renderElement = this.constructor.renderElement\n    this.promise = new Promise((resolve, reject) => (this.resolvingFunctions = { resolve, reject }))\n  }\n\n  get shouldRender() {\n    return true\n  }\n\n  get shouldAutofocus() {\n    return true\n  }\n\n  get reloadReason() {\n    return\n  }\n\n  prepareToRender() {\n    return\n  }\n\n  render() {\n    // Abstract method\n  }\n\n  finishRendering() {\n    if (this.resolvingFunctions) {\n      this.resolvingFunctions.resolve()\n      delete this.resolvingFunctions\n    }\n  }\n\n  async preservingPermanentElements(callback) {\n    await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback)\n  }\n\n  focusFirstAutofocusableElement() {\n    if (this.shouldAutofocus) {\n      const element = this.connectedSnapshot.firstAutofocusableElement\n      if (element) {\n        element.focus()\n      }\n    }\n  }\n\n  // Bardo delegate\n\n  enteringBardo(currentPermanentElement) {\n    if (this.#activeElement) return\n\n    if (currentPermanentElement.contains(this.currentSnapshot.activeElement)) {\n      this.#activeElement = this.currentSnapshot.activeElement\n    }\n  }\n\n  leavingBardo(currentPermanentElement) {\n    if (currentPermanentElement.contains(this.#activeElement) && this.#activeElement instanceof HTMLElement) {\n      this.#activeElement.focus()\n\n      this.#activeElement = null\n    }\n  }\n\n  get connectedSnapshot() {\n    return this.newSnapshot.isConnected ? this.newSnapshot : this.currentSnapshot\n  }\n\n  get currentElement() {\n    return this.currentSnapshot.element\n  }\n\n  get newElement() {\n    return this.newSnapshot.element\n  }\n\n  get permanentElementMap() {\n    return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot)\n  }\n\n  get renderMethod() {\n    return \"replace\"\n  }\n}\n"
  },
  {
    "path": "src/core/session.js",
    "content": "import { BrowserAdapter } from \"./native/browser_adapter\"\nimport { CacheObserver } from \"../observers/cache_observer\"\nimport { FormSubmitObserver } from \"../observers/form_submit_observer\"\nimport { FrameRedirector } from \"./frames/frame_redirector\"\nimport { History } from \"./drive/history\"\nimport { LinkPrefetchObserver } from \"../observers/link_prefetch_observer\"\nimport { LinkClickObserver } from \"../observers/link_click_observer\"\nimport { FormLinkClickObserver } from \"../observers/form_link_click_observer\"\nimport { getAction, expandURL, locationIsVisitable } from \"./url\"\nimport { Navigator } from \"./drive/navigator\"\nimport { PageObserver } from \"../observers/page_observer\"\nimport { ScrollObserver } from \"../observers/scroll_observer\"\nimport { StreamMessage } from \"./streams/stream_message\"\nimport { StreamMessageRenderer } from \"./streams/stream_message_renderer\"\nimport { StreamObserver } from \"../observers/stream_observer\"\nimport { clearBusyState, dispatch, findClosestRecursively, getVisitAction, markAsBusy, debounce } from \"../util\"\nimport { PageView } from \"./drive/page_view\"\nimport { FrameElement } from \"../elements/frame_element\"\nimport { Preloader } from \"./drive/preloader\"\nimport { Cache } from \"./cache\"\nimport { config } from \"./config\"\n\nexport class Session {\n  navigator = new Navigator(this)\n  history = new History(this)\n  view = new PageView(this, document.documentElement)\n  adapter = new BrowserAdapter(this)\n\n  pageObserver = new PageObserver(this)\n  cacheObserver = new CacheObserver()\n  linkPrefetchObserver = new LinkPrefetchObserver(this, document)\n  linkClickObserver = new LinkClickObserver(this, window)\n  formSubmitObserver = new FormSubmitObserver(this, document)\n  scrollObserver = new ScrollObserver(this)\n  streamObserver = new StreamObserver(this)\n  formLinkClickObserver = new FormLinkClickObserver(this, document.documentElement)\n  frameRedirector = new FrameRedirector(this, document.documentElement)\n  streamMessageRenderer = new StreamMessageRenderer()\n  cache = new Cache(this)\n\n  enabled = true\n  started = false\n  #pageRefreshDebouncePeriod = 150\n\n  constructor(recentRequests) {\n    this.recentRequests = recentRequests\n    this.preloader = new Preloader(this, this.view.snapshotCache)\n    this.debouncedRefresh = this.refresh\n    this.pageRefreshDebouncePeriod = this.pageRefreshDebouncePeriod\n  }\n\n  start() {\n    if (!this.started) {\n      this.pageObserver.start()\n      this.cacheObserver.start()\n      this.linkPrefetchObserver.start()\n      this.formLinkClickObserver.start()\n      this.linkClickObserver.start()\n      this.formSubmitObserver.start()\n      this.scrollObserver.start()\n      this.streamObserver.start()\n      this.frameRedirector.start()\n      this.history.start()\n      this.preloader.start()\n      this.started = true\n      this.enabled = true\n    }\n  }\n\n  disable() {\n    this.enabled = false\n  }\n\n  stop() {\n    if (this.started) {\n      this.pageObserver.stop()\n      this.cacheObserver.stop()\n      this.linkPrefetchObserver.stop()\n      this.formLinkClickObserver.stop()\n      this.linkClickObserver.stop()\n      this.formSubmitObserver.stop()\n      this.scrollObserver.stop()\n      this.streamObserver.stop()\n      this.frameRedirector.stop()\n      this.history.stop()\n      this.preloader.stop()\n      this.started = false\n    }\n  }\n\n  registerAdapter(adapter) {\n    this.adapter = adapter\n  }\n\n  visit(location, options = {}) {\n    const frameElement = options.frame ? document.getElementById(options.frame) : null\n\n    if (frameElement instanceof FrameElement) {\n      const action = options.action || getVisitAction(frameElement)\n\n      frameElement.delegate.proposeVisitIfNavigatedWithAction(frameElement, action)\n      frameElement.src = location.toString()\n    } else {\n      this.navigator.proposeVisit(expandURL(location), options)\n    }\n  }\n\n  refresh(url, options = {}) {\n    options = typeof options === \"string\" ? { requestId: options } : options\n\n    const { method, requestId, scroll } = options\n    const isRecentRequest = requestId && this.recentRequests.has(requestId)\n    const isCurrentUrl = url === document.baseURI\n    if (!isRecentRequest && !this.navigator.currentVisit && isCurrentUrl) {\n      this.visit(url, { action: \"replace\", shouldCacheSnapshot: false, refresh: { method, scroll } })\n    }\n  }\n\n  connectStreamSource(source) {\n    this.streamObserver.connectStreamSource(source)\n  }\n\n  disconnectStreamSource(source) {\n    this.streamObserver.disconnectStreamSource(source)\n  }\n\n  renderStreamMessage(message) {\n    this.streamMessageRenderer.render(StreamMessage.wrap(message))\n  }\n\n  clearCache() {\n    this.view.clearSnapshotCache()\n  }\n\n  setProgressBarDelay(delay) {\n    console.warn(\n      \"Please replace `session.setProgressBarDelay(delay)` with `session.progressBarDelay = delay`. The function is deprecated and will be removed in a future version of Turbo.`\"\n    )\n\n    this.progressBarDelay = delay\n  }\n\n  set progressBarDelay(delay) {\n    config.drive.progressBarDelay = delay\n  }\n\n  get progressBarDelay() {\n    return config.drive.progressBarDelay\n  }\n\n  set drive(value) {\n    config.drive.enabled = value\n  }\n\n  get drive() {\n    return config.drive.enabled\n  }\n\n  set formMode(value) {\n    config.forms.mode = value\n  }\n\n  get formMode() {\n    return config.forms.mode\n  }\n\n  get location() {\n    return this.history.location\n  }\n\n  get restorationIdentifier() {\n    return this.history.restorationIdentifier\n  }\n\n  get pageRefreshDebouncePeriod() {\n    return this.#pageRefreshDebouncePeriod\n  }\n\n  set pageRefreshDebouncePeriod(value) {\n    this.refresh = debounce(this.debouncedRefresh.bind(this), value)\n    this.#pageRefreshDebouncePeriod = value\n  }\n\n  // Preloader delegate\n\n  shouldPreloadLink(element) {\n    const isUnsafe = element.hasAttribute(\"data-turbo-method\")\n    const isStream = element.hasAttribute(\"data-turbo-stream\")\n    const frameTarget = element.getAttribute(\"data-turbo-frame\")\n    const frame = frameTarget == \"_top\" ?\n      null :\n      document.getElementById(frameTarget) || findClosestRecursively(element, \"turbo-frame:not([disabled])\")\n\n    if (isUnsafe || isStream || frame instanceof FrameElement) {\n      return false\n    } else {\n      const location = new URL(element.href)\n\n      return this.elementIsNavigatable(element) && locationIsVisitable(location, this.snapshot.rootLocation)\n    }\n  }\n\n  // History delegate\n\n  historyPoppedToLocationWithRestorationIdentifierAndDirection(location, restorationIdentifier, direction) {\n    if (this.enabled) {\n      this.navigator.startVisit(location, restorationIdentifier, {\n        action: \"restore\",\n        historyChanged: true,\n        direction\n      })\n    } else {\n      this.adapter.pageInvalidated({\n        reason: \"turbo_disabled\"\n      })\n    }\n  }\n\n  historyPoppedWithEmptyState(location) {\n    this.history.replace(location)\n    this.view.lastRenderedLocation = location\n    this.view.cacheSnapshot()\n  }\n\n  // Scroll observer delegate\n\n  scrollPositionChanged(position) {\n    this.history.updateRestorationData({ scrollPosition: position })\n  }\n\n  // Form click observer delegate\n\n  willSubmitFormLinkToLocation(link, location) {\n    return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation)\n  }\n\n  submittedFormLinkToLocation() {}\n\n  // Link hover observer delegate\n\n  canPrefetchRequestToLocation(link, location) {\n    return (\n      this.elementIsNavigatable(link) &&\n      locationIsVisitable(location, this.snapshot.rootLocation) &&\n      this.navigator.linkPrefetchingIsEnabledForLocation(location)\n    )\n  }\n\n  // Link click observer delegate\n\n  willFollowLinkToLocation(link, location, event) {\n    return (\n      this.elementIsNavigatable(link) &&\n      locationIsVisitable(location, this.snapshot.rootLocation) &&\n      this.applicationAllowsFollowingLinkToLocation(link, location, event)\n    )\n  }\n\n  followedLinkToLocation(link, location) {\n    const action = this.getActionForLink(link)\n    const acceptsStreamResponse = link.hasAttribute(\"data-turbo-stream\")\n\n    this.visit(location.href, { action, acceptsStreamResponse })\n  }\n\n  // Navigator delegate\n\n  allowsVisitingLocationWithAction(location, action) {\n    return this.applicationAllowsVisitingLocation(location)\n  }\n\n  visitProposedToLocation(location, options) {\n    extendURLWithDeprecatedProperties(location)\n    this.adapter.visitProposedToLocation(location, options)\n  }\n\n  // Visit delegate\n\n  visitStarted(visit) {\n    if (!visit.acceptsStreamResponse) {\n      markAsBusy(document.documentElement)\n      this.view.markVisitDirection(visit.direction)\n    }\n    extendURLWithDeprecatedProperties(visit.location)\n    this.notifyApplicationAfterVisitingLocation(visit.location, visit.action)\n  }\n\n  visitCompleted(visit) {\n    this.view.unmarkVisitDirection()\n    clearBusyState(document.documentElement)\n    this.notifyApplicationAfterPageLoad(visit.getTimingMetrics())\n  }\n\n  // Form submit observer delegate\n\n  willSubmitForm(form, submitter) {\n    const action = getAction(form, submitter)\n\n    return (\n      this.submissionIsNavigatable(form, submitter) &&\n      locationIsVisitable(expandURL(action), this.snapshot.rootLocation)\n    )\n  }\n\n  formSubmitted(form, submitter) {\n    this.navigator.submitForm(form, submitter)\n  }\n\n  // Page observer delegate\n\n  pageBecameInteractive() {\n    this.view.lastRenderedLocation = this.location\n    this.notifyApplicationAfterPageLoad()\n  }\n\n  pageLoaded() {\n    this.history.assumeControlOfScrollRestoration()\n  }\n\n  pageWillUnload() {\n    this.history.relinquishControlOfScrollRestoration()\n  }\n\n  // Stream observer delegate\n\n  receivedMessageFromStream(message) {\n    this.renderStreamMessage(message)\n  }\n\n  // Page view delegate\n\n  viewWillCacheSnapshot() {\n    this.notifyApplicationBeforeCachingSnapshot()\n  }\n\n  allowsImmediateRender({ element }, options) {\n    const event = this.notifyApplicationBeforeRender(element, options)\n    const {\n      defaultPrevented,\n      detail: { render }\n    } = event\n\n    if (this.view.renderer && render) {\n      this.view.renderer.renderElement = render\n    }\n\n    return !defaultPrevented\n  }\n\n  viewRenderedSnapshot(_snapshot, _isPreview, renderMethod) {\n    this.view.lastRenderedLocation = this.history.location\n    this.notifyApplicationAfterRender(renderMethod)\n  }\n\n  preloadOnLoadLinksForView(element) {\n    this.preloader.preloadOnLoadLinksForView(element)\n  }\n\n  viewInvalidated(reason) {\n    this.adapter.pageInvalidated(reason)\n  }\n\n  // Frame element\n\n  frameLoaded(frame) {\n    this.notifyApplicationAfterFrameLoad(frame)\n  }\n\n  frameRendered(fetchResponse, frame) {\n    this.notifyApplicationAfterFrameRender(fetchResponse, frame)\n  }\n\n  // Application events\n\n  applicationAllowsFollowingLinkToLocation(link, location, ev) {\n    const event = this.notifyApplicationAfterClickingLinkToLocation(link, location, ev)\n    return !event.defaultPrevented\n  }\n\n  applicationAllowsVisitingLocation(location) {\n    const event = this.notifyApplicationBeforeVisitingLocation(location)\n    return !event.defaultPrevented\n  }\n\n  notifyApplicationAfterClickingLinkToLocation(link, location, event) {\n    return dispatch(\"turbo:click\", {\n      target: link,\n      detail: { url: location.href, originalEvent: event },\n      cancelable: true\n    })\n  }\n\n  notifyApplicationBeforeVisitingLocation(location) {\n    return dispatch(\"turbo:before-visit\", {\n      detail: { url: location.href },\n      cancelable: true\n    })\n  }\n\n  notifyApplicationAfterVisitingLocation(location, action) {\n    return dispatch(\"turbo:visit\", { detail: { url: location.href, action } })\n  }\n\n  notifyApplicationBeforeCachingSnapshot() {\n    return dispatch(\"turbo:before-cache\")\n  }\n\n  notifyApplicationBeforeRender(newBody, options) {\n    return dispatch(\"turbo:before-render\", {\n      detail: { newBody, ...options },\n      cancelable: true\n    })\n  }\n\n  notifyApplicationAfterRender(renderMethod) {\n    return dispatch(\"turbo:render\", { detail: { renderMethod } })\n  }\n\n  notifyApplicationAfterPageLoad(timing = {}) {\n    return dispatch(\"turbo:load\", {\n      detail: { url: this.location.href, timing }\n    })\n  }\n\n  notifyApplicationAfterFrameLoad(frame) {\n    return dispatch(\"turbo:frame-load\", { target: frame })\n  }\n\n  notifyApplicationAfterFrameRender(fetchResponse, frame) {\n    return dispatch(\"turbo:frame-render\", {\n      detail: { fetchResponse },\n      target: frame,\n      cancelable: true\n    })\n  }\n\n  // Helpers\n\n  submissionIsNavigatable(form, submitter) {\n    if (config.forms.mode == \"off\") {\n      return false\n    } else {\n      const submitterIsNavigatable = submitter ? this.elementIsNavigatable(submitter) : true\n\n      if (config.forms.mode == \"optin\") {\n        return submitterIsNavigatable && form.closest('[data-turbo=\"true\"]') != null\n      } else {\n        return submitterIsNavigatable && this.elementIsNavigatable(form)\n      }\n    }\n  }\n\n  elementIsNavigatable(element) {\n    const container = findClosestRecursively(element, \"[data-turbo]\")\n    const withinFrame = findClosestRecursively(element, \"turbo-frame\")\n\n    // Check if Drive is enabled on the session or we're within a Frame.\n    if (config.drive.enabled || withinFrame) {\n      // Element is navigatable by default, unless `data-turbo=\"false\"`.\n      if (container) {\n        return container.getAttribute(\"data-turbo\") != \"false\"\n      } else {\n        return true\n      }\n    } else {\n      // Element isn't navigatable by default, unless `data-turbo=\"true\"`.\n      if (container) {\n        return container.getAttribute(\"data-turbo\") == \"true\"\n      } else {\n        return false\n      }\n    }\n  }\n\n  // Private\n\n  getActionForLink(link) {\n    return getVisitAction(link) || \"advance\"\n  }\n\n  get snapshot() {\n    return this.view.snapshot\n  }\n}\n\n// Older versions of the Turbo Native adapters referenced the\n// `Location#absoluteURL` property in their implementations of\n// the `Adapter#visitProposedToLocation()` and `#visitStarted()`\n// methods. The Location class has since been removed in favor\n// of the DOM URL API, and accordingly all Adapter methods now\n// receive URL objects.\n//\n// We alias #absoluteURL to #toString() here to avoid crashing\n// older adapters which do not expect URL objects. We should\n// consider removing this support at some point in the future.\n\nfunction extendURLWithDeprecatedProperties(url) {\n  Object.defineProperties(url, deprecatedLocationPropertyDescriptors)\n}\n\nconst deprecatedLocationPropertyDescriptors = {\n  absoluteURL: {\n    get() {\n      return this.toString()\n    }\n  }\n}\n"
  },
  {
    "path": "src/core/snapshot.js",
    "content": "import { queryAutofocusableElement } from \"../util\"\n\nexport class Snapshot {\n  constructor(element) {\n    this.element = element\n  }\n\n  get activeElement() {\n    return this.element.ownerDocument.activeElement\n  }\n\n  get children() {\n    return [...this.element.children]\n  }\n\n  hasAnchor(anchor) {\n    return this.getElementForAnchor(anchor) != null\n  }\n\n  getElementForAnchor(anchor) {\n    return anchor ? this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`) : null\n  }\n\n  get isConnected() {\n    return this.element.isConnected\n  }\n\n  get firstAutofocusableElement() {\n    return queryAutofocusableElement(this.element)\n  }\n\n  get permanentElements() {\n    return queryPermanentElementsAll(this.element)\n  }\n\n  getPermanentElementById(id) {\n    return getPermanentElementById(this.element, id)\n  }\n\n  getPermanentElementMapForSnapshot(snapshot) {\n    const permanentElementMap = {}\n\n    for (const currentPermanentElement of this.permanentElements) {\n      const { id } = currentPermanentElement\n      const newPermanentElement = snapshot.getPermanentElementById(id)\n      if (newPermanentElement) {\n        permanentElementMap[id] = [currentPermanentElement, newPermanentElement]\n      }\n    }\n\n    return permanentElementMap\n  }\n}\n\nexport function getPermanentElementById(node, id) {\n  return node.querySelector(`#${id}[data-turbo-permanent]`)\n}\n\nexport function queryPermanentElementsAll(node) {\n  return node.querySelectorAll(\"[id][data-turbo-permanent]\")\n}\n"
  },
  {
    "path": "src/core/streams/stream_actions.js",
    "content": "import { session } from \"../\"\nimport { morphElements, morphChildren } from \"../morphing\"\n\nexport const StreamActions = {\n  after() {\n    this.removeDuplicateTargetSiblings()\n    this.targetElements.forEach((e) => e.parentElement?.insertBefore(this.templateContent, e.nextSibling))\n  },\n\n  append() {\n    this.removeDuplicateTargetChildren()\n    this.targetElements.forEach((e) => e.append(this.templateContent))\n  },\n\n  before() {\n    this.removeDuplicateTargetSiblings()\n    this.targetElements.forEach((e) => e.parentElement?.insertBefore(this.templateContent, e))\n  },\n\n  prepend() {\n    this.removeDuplicateTargetChildren()\n    this.targetElements.forEach((e) => e.prepend(this.templateContent))\n  },\n\n  remove() {\n    this.targetElements.forEach((e) => e.remove())\n  },\n\n  replace() {\n    const method = this.getAttribute(\"method\")\n\n    this.targetElements.forEach((targetElement) => {\n      if (method === \"morph\") {\n        morphElements(targetElement, this.templateContent)\n      } else {\n        targetElement.replaceWith(this.templateContent)\n      }\n    })\n  },\n\n  update() {\n    const method = this.getAttribute(\"method\")\n\n    this.targetElements.forEach((targetElement) => {\n      if (method === \"morph\") {\n        morphChildren(targetElement, this.templateContent)\n      } else {\n        targetElement.innerHTML = \"\"\n        targetElement.append(this.templateContent)\n      }\n    })\n  },\n\n  refresh() {\n    const method = this.getAttribute(\"method\")\n    const requestId = this.requestId\n    const scroll = this.getAttribute(\"scroll\")\n\n    session.refresh(this.baseURI, { method, requestId, scroll })\n  }\n}\n"
  },
  {
    "path": "src/core/streams/stream_message.js",
    "content": "import { activateScriptElement, createDocumentFragment } from \"../../util\"\n\nexport class StreamMessage {\n  static contentType = \"text/vnd.turbo-stream.html\"\n\n  static wrap(message) {\n    if (typeof message == \"string\") {\n      return new this(createDocumentFragment(message))\n    } else {\n      return message\n    }\n  }\n\n  constructor(fragment) {\n    this.fragment = importStreamElements(fragment)\n  }\n}\n\nfunction importStreamElements(fragment) {\n  for (const element of fragment.querySelectorAll(\"turbo-stream\")) {\n    const streamElement = document.importNode(element, true)\n\n    for (const inertScriptElement of streamElement.templateElement.content.querySelectorAll(\"script\")) {\n      inertScriptElement.replaceWith(activateScriptElement(inertScriptElement))\n    }\n\n    element.replaceWith(streamElement)\n  }\n\n  return fragment\n}\n"
  },
  {
    "path": "src/core/streams/stream_message_renderer.js",
    "content": "import { Bardo } from \"../bardo\"\nimport { getPermanentElementById, queryPermanentElementsAll } from \"../snapshot\"\nimport { around, elementIsFocusable, nextRepaint, queryAutofocusableElement, uuid } from \"../../util\"\n\nexport class StreamMessageRenderer {\n  render({ fragment }) {\n    Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), () => {\n      withAutofocusFromFragment(fragment, () => {\n        withPreservedFocus(() => {\n          document.documentElement.appendChild(fragment)\n        })\n      })\n    })\n  }\n\n  // Bardo delegate\n\n  enteringBardo(currentPermanentElement, newPermanentElement) {\n    newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true))\n  }\n\n  leavingBardo() {}\n}\n\nfunction getPermanentElementMapForFragment(fragment) {\n  const permanentElementsInDocument = queryPermanentElementsAll(document.documentElement)\n  const permanentElementMap = {}\n  for (const permanentElementInDocument of permanentElementsInDocument) {\n    const { id } = permanentElementInDocument\n\n    for (const streamElement of fragment.querySelectorAll(\"turbo-stream\")) {\n      const elementInStream = getPermanentElementById(streamElement.templateElement.content, id)\n\n      if (elementInStream) {\n        permanentElementMap[id] = [permanentElementInDocument, elementInStream]\n      }\n    }\n  }\n\n  return permanentElementMap\n}\n\nasync function withAutofocusFromFragment(fragment, callback) {\n  const generatedID = `turbo-stream-autofocus-${uuid()}`\n  const turboStreams = fragment.querySelectorAll(\"turbo-stream\")\n  const elementWithAutofocus = firstAutofocusableElementInStreams(turboStreams)\n  let willAutofocusId = null\n\n  if (elementWithAutofocus) {\n    if (elementWithAutofocus.id) {\n      willAutofocusId = elementWithAutofocus.id\n    } else {\n      willAutofocusId = generatedID\n    }\n\n    elementWithAutofocus.id = willAutofocusId\n  }\n\n  callback()\n  await nextRepaint()\n\n  const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body\n\n  if (hasNoActiveElement && willAutofocusId) {\n    const elementToAutofocus = document.getElementById(willAutofocusId)\n\n    if (elementIsFocusable(elementToAutofocus)) {\n      elementToAutofocus.focus()\n    }\n    if (elementToAutofocus && elementToAutofocus.id == generatedID) {\n      elementToAutofocus.removeAttribute(\"id\")\n    }\n  }\n}\n\nasync function withPreservedFocus(callback) {\n  const [activeElementBeforeRender, activeElementAfterRender] = await around(callback, () => document.activeElement)\n\n  const restoreFocusTo = activeElementBeforeRender && activeElementBeforeRender.id\n\n  if (restoreFocusTo) {\n    const elementToFocus = document.getElementById(restoreFocusTo)\n\n    if (elementIsFocusable(elementToFocus) && elementToFocus != activeElementAfterRender) {\n      elementToFocus.focus()\n    }\n  }\n}\n\nfunction firstAutofocusableElementInStreams(nodeListOfStreamElements) {\n  for (const streamElement of nodeListOfStreamElements) {\n    const elementWithAutofocus = queryAutofocusableElement(streamElement.templateElement.content)\n\n    if (elementWithAutofocus) return elementWithAutofocus\n  }\n\n  return null\n}\n"
  },
  {
    "path": "src/core/url.js",
    "content": "import { config } from \"./config\"\n\nexport function expandURL(locatable) {\n  return new URL(locatable.toString(), document.baseURI)\n}\n\nexport function getAnchor(url) {\n  let anchorMatch\n  if (url.hash) {\n    return url.hash.slice(1)\n    // eslint-disable-next-line no-cond-assign\n  } else if ((anchorMatch = url.href.match(/#(.*)$/))) {\n    return anchorMatch[1]\n  }\n}\n\nexport function getAction(form, submitter) {\n  const action = submitter?.getAttribute(\"formaction\") || form.getAttribute(\"action\") || form.action\n\n  return expandURL(action)\n}\n\nexport function getExtension(url) {\n  return (getLastPathComponent(url).match(/\\.[^.]*$/) || [])[0] || \"\"\n}\n\nexport function isPrefixedBy(baseURL, url) {\n  const prefix = addTrailingSlash(url.origin + url.pathname)\n  return addTrailingSlash(baseURL.href) === prefix || baseURL.href.startsWith(prefix)\n}\n\nexport function locationIsVisitable(location, rootLocation) {\n  return isPrefixedBy(location, rootLocation) && !config.drive.unvisitableExtensions.has(getExtension(location))\n}\n\nexport function getLocationForLink(link) {\n  return expandURL(link.getAttribute(\"href\") || \"\")\n}\n\nexport function getRequestURL(url) {\n  const anchor = getAnchor(url)\n  return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href\n}\n\nexport function toCacheKey(url) {\n  return getRequestURL(url)\n}\n\nexport function urlsAreEqual(left, right) {\n  return expandURL(left).href == expandURL(right).href\n}\n\nfunction getPathComponents(url) {\n  return url.pathname.split(\"/\").slice(1)\n}\n\nfunction getLastPathComponent(url) {\n  return getPathComponents(url).slice(-1)[0]\n}\n\nfunction addTrailingSlash(value) {\n  return value.endsWith(\"/\") ? value : value + \"/\"\n}\n"
  },
  {
    "path": "src/core/view.js",
    "content": "import { getAnchor } from \"./url\"\n\nexport class View {\n  #resolveRenderPromise = (_value) => {}\n  #resolveInterceptionPromise = (_value) => {}\n\n  constructor(delegate, element) {\n    this.delegate = delegate\n    this.element = element\n  }\n\n  // Scrolling\n\n  scrollToAnchor(anchor) {\n    const element = this.snapshot.getElementForAnchor(anchor)\n    if (element) {\n      this.focusElement(element)\n      this.scrollToElement(element)\n    } else {\n      this.scrollToPosition({ x: 0, y: 0 })\n    }\n  }\n\n  scrollToAnchorFromLocation(location) {\n    this.scrollToAnchor(getAnchor(location))\n  }\n\n  scrollToElement(element) {\n    element.scrollIntoView()\n  }\n\n  focusElement(element) {\n    if (element instanceof HTMLElement) {\n      if (element.hasAttribute(\"tabindex\")) {\n        element.focus()\n      } else {\n        element.setAttribute(\"tabindex\", \"-1\")\n        element.focus()\n        element.removeAttribute(\"tabindex\")\n      }\n    }\n  }\n\n  scrollToPosition({ x, y }) {\n    this.scrollRoot.scrollTo(x, y)\n  }\n\n  scrollToTop() {\n    this.scrollToPosition({ x: 0, y: 0 })\n  }\n\n  get scrollRoot() {\n    return window\n  }\n\n  // Rendering\n\n  async render(renderer) {\n    const { isPreview, shouldRender, willRender, newSnapshot: snapshot } = renderer\n\n    // A workaround to ignore tracked element mismatch reloads when performing\n    // a promoted Visit from a frame navigation\n    const shouldInvalidate = willRender\n\n    if (shouldRender) {\n      try {\n        this.renderPromise = new Promise((resolve) => (this.#resolveRenderPromise = resolve))\n        this.renderer = renderer\n        await this.prepareToRenderSnapshot(renderer)\n\n        const renderInterception = new Promise((resolve) => (this.#resolveInterceptionPromise = resolve))\n        const options = { resume: this.#resolveInterceptionPromise, render: this.renderer.renderElement, renderMethod: this.renderer.renderMethod }\n        const immediateRender = this.delegate.allowsImmediateRender(snapshot, options)\n        if (!immediateRender) await renderInterception\n\n        await this.renderSnapshot(renderer)\n        this.delegate.viewRenderedSnapshot(snapshot, isPreview, this.renderer.renderMethod)\n        this.delegate.preloadOnLoadLinksForView(this.element)\n        this.finishRenderingSnapshot(renderer)\n      } finally {\n        delete this.renderer\n        this.#resolveRenderPromise(undefined)\n        delete this.renderPromise\n      }\n    } else if (shouldInvalidate) {\n      this.invalidate(renderer.reloadReason)\n    }\n  }\n\n  invalidate(reason) {\n    this.delegate.viewInvalidated(reason)\n  }\n\n  async prepareToRenderSnapshot(renderer) {\n    this.markAsPreview(renderer.isPreview)\n    await renderer.prepareToRender()\n  }\n\n  markAsPreview(isPreview) {\n    if (isPreview) {\n      this.element.setAttribute(\"data-turbo-preview\", \"\")\n    } else {\n      this.element.removeAttribute(\"data-turbo-preview\")\n    }\n  }\n\n  markVisitDirection(direction) {\n    this.element.setAttribute(\"data-turbo-visit-direction\", direction)\n  }\n\n  unmarkVisitDirection() {\n    this.element.removeAttribute(\"data-turbo-visit-direction\")\n  }\n\n  async renderSnapshot(renderer) {\n    await renderer.render()\n  }\n\n  finishRenderingSnapshot(renderer) {\n    renderer.finishRendering()\n  }\n}\n"
  },
  {
    "path": "src/elements/frame_element.js",
    "content": "export const FrameLoadingStyle = {\n  eager: \"eager\",\n  lazy: \"lazy\"\n}\n\n/**\n * Contains a fragment of HTML which is updated based on navigation within\n * it (e.g. via links or form submissions).\n *\n * @customElement turbo-frame\n * @example\n *   <turbo-frame id=\"messages\">\n *     <a href=\"/messages/expanded\">\n *       Show all expanded messages in this frame.\n *     </a>\n *\n *     <form action=\"/messages\">\n *       Show response from this form within this frame.\n *     </form>\n *   </turbo-frame>\n */\nexport class FrameElement extends HTMLElement {\n  static delegateConstructor = undefined\n\n  loaded = Promise.resolve()\n\n  static get observedAttributes() {\n    return [\"disabled\", \"loading\", \"src\"]\n  }\n\n  constructor() {\n    super()\n    this.delegate = new FrameElement.delegateConstructor(this)\n  }\n\n  connectedCallback() {\n    this.delegate.connect()\n  }\n\n  disconnectedCallback() {\n    this.delegate.disconnect()\n  }\n\n  reload() {\n    return this.delegate.sourceURLReloaded()\n  }\n\n  attributeChangedCallback(name) {\n    if (name == \"loading\") {\n      this.delegate.loadingStyleChanged()\n    } else if (name == \"src\") {\n      this.delegate.sourceURLChanged()\n    } else if (name == \"disabled\") {\n      this.delegate.disabledChanged()\n    }\n  }\n\n  /**\n   * Gets the URL to lazily load source HTML from\n   */\n  get src() {\n    return this.getAttribute(\"src\")\n  }\n\n  /**\n   * Sets the URL to lazily load source HTML from\n   */\n  set src(value) {\n    if (value) {\n      this.setAttribute(\"src\", value)\n    } else {\n      this.removeAttribute(\"src\")\n    }\n  }\n\n  /**\n   * Gets the refresh mode for the frame.\n   */\n  get refresh() {\n    return this.getAttribute(\"refresh\")\n  }\n\n  /**\n   * Sets the refresh mode for the frame.\n   */\n  set refresh(value) {\n    if (value) {\n      this.setAttribute(\"refresh\", value)\n    } else {\n      this.removeAttribute(\"refresh\")\n    }\n  }\n\n  get shouldReloadWithMorph() {\n    return this.src && this.refresh === \"morph\"\n  }\n\n  /**\n   * Determines if the element is loading\n   */\n  get loading() {\n    return frameLoadingStyleFromString(this.getAttribute(\"loading\") || \"\")\n  }\n\n  /**\n   * Sets the value of if the element is loading\n   */\n  set loading(value) {\n    if (value) {\n      this.setAttribute(\"loading\", value)\n    } else {\n      this.removeAttribute(\"loading\")\n    }\n  }\n\n  /**\n   * Gets the disabled state of the frame.\n   *\n   * If disabled, no requests will be intercepted by the frame.\n   */\n  get disabled() {\n    return this.hasAttribute(\"disabled\")\n  }\n\n  /**\n   * Sets the disabled state of the frame.\n   *\n   * If disabled, no requests will be intercepted by the frame.\n   */\n  set disabled(value) {\n    if (value) {\n      this.setAttribute(\"disabled\", \"\")\n    } else {\n      this.removeAttribute(\"disabled\")\n    }\n  }\n\n  /**\n   * Gets the autoscroll state of the frame.\n   *\n   * If true, the frame will be scrolled into view automatically on update.\n   */\n  get autoscroll() {\n    return this.hasAttribute(\"autoscroll\")\n  }\n\n  /**\n   * Sets the autoscroll state of the frame.\n   *\n   * If true, the frame will be scrolled into view automatically on update.\n   */\n  set autoscroll(value) {\n    if (value) {\n      this.setAttribute(\"autoscroll\", \"\")\n    } else {\n      this.removeAttribute(\"autoscroll\")\n    }\n  }\n\n  /**\n   * Determines if the element has finished loading\n   */\n  get complete() {\n    return !this.delegate.isLoading\n  }\n\n  /**\n   * Gets the active state of the frame.\n   *\n   * If inactive, source changes will not be observed.\n   */\n  get isActive() {\n    return this.ownerDocument === document && !this.isPreview\n  }\n\n  /**\n   * Sets the active state of the frame.\n   *\n   * If inactive, source changes will not be observed.\n   */\n  get isPreview() {\n    return this.ownerDocument?.documentElement?.hasAttribute(\"data-turbo-preview\")\n  }\n}\n\nfunction frameLoadingStyleFromString(style) {\n  switch (style.toLowerCase()) {\n    case \"lazy\":\n      return FrameLoadingStyle.lazy\n    default:\n      return FrameLoadingStyle.eager\n  }\n}\n"
  },
  {
    "path": "src/elements/index.js",
    "content": "import { FrameController } from \"../core/frames/frame_controller\"\nimport { FrameElement } from \"./frame_element\"\nimport { StreamElement } from \"./stream_element\"\nimport { StreamSourceElement } from \"./stream_source_element\"\n\nFrameElement.delegateConstructor = FrameController\n\nexport * from \"./frame_element\"\nexport * from \"./stream_element\"\nexport * from \"./stream_source_element\"\n\nif (customElements.get(\"turbo-frame\") === undefined) {\n  customElements.define(\"turbo-frame\", FrameElement)\n}\n\nif (customElements.get(\"turbo-stream\") === undefined) {\n  customElements.define(\"turbo-stream\", StreamElement)\n}\n\nif (customElements.get(\"turbo-stream-source\") === undefined) {\n  customElements.define(\"turbo-stream-source\", StreamSourceElement)\n}\n"
  },
  {
    "path": "src/elements/stream_element.js",
    "content": "import { StreamActions } from \"../core/streams/stream_actions\"\nimport { nextRepaint } from \"../util\"\n\n// <turbo-stream action=replace target=id><template>...\n\n/**\n * Renders updates to the page from a stream of messages.\n *\n * Using the `action` attribute, this can be configured one of eight ways:\n *\n * - `after` - inserts the result after the target\n * - `append` - appends the result to the target\n * - `before` - inserts the result before the target\n * - `prepend` - prepends the result to the target\n * - `refresh` - initiates a page refresh\n * - `remove` - removes the target\n * - `replace` - replaces the outer HTML of the target\n * - `update` - replaces the inner HTML of the target\n *\n * @customElement turbo-stream\n * @example\n *   <turbo-stream action=\"append\" target=\"dom_id\">\n *     <template>\n *       Content to append to target designated with the dom_id.\n *     </template>\n *   </turbo-stream>\n */\nexport class StreamElement extends HTMLElement {\n  static async renderElement(newElement) {\n    await newElement.performAction()\n  }\n\n  async connectedCallback() {\n    try {\n      await this.render()\n    } catch (error) {\n      console.error(error)\n    } finally {\n      this.disconnect()\n    }\n  }\n\n  async render() {\n    return (this.renderPromise ??= (async () => {\n      const event = this.beforeRenderEvent\n\n      if (this.dispatchEvent(event)) {\n        await nextRepaint()\n        await event.detail.render(this)\n      }\n    })())\n  }\n\n  disconnect() {\n    try {\n      this.remove()\n      // eslint-disable-next-line no-empty\n    } catch {}\n  }\n\n  /**\n   * Removes duplicate children (by ID)\n   */\n  removeDuplicateTargetChildren() {\n    this.duplicateChildren.forEach((c) => c.remove())\n  }\n\n  /**\n   * Gets the list of duplicate children (i.e. those with the same ID)\n   */\n  get duplicateChildren() {\n    const existingChildren = this.targetElements.flatMap((e) => [...e.children]).filter((c) => !!c.getAttribute(\"id\"))\n    const newChildrenIds = [...(this.templateContent?.children || [])].filter((c) => !!c.getAttribute(\"id\")).map((c) => c.getAttribute(\"id\"))\n\n    return existingChildren.filter((c) => newChildrenIds.includes(c.getAttribute(\"id\")))\n  }\n\n  /**\n  * Removes duplicate siblings (by ID)\n  */\n  removeDuplicateTargetSiblings() {\n    this.duplicateSiblings.forEach((c) => c.remove())\n  }\n\n  /**\n  * Gets the list of duplicate siblings (i.e. those with the same ID)\n  */\n  get duplicateSiblings() {\n    const existingChildren = this.targetElements.flatMap((e) => [...e.parentElement.children]).filter((c) => !!c.id)\n    const newChildrenIds = [...(this.templateContent?.children || [])].filter((c) => !!c.id).map((c) => c.id)\n\n    return existingChildren.filter((c) => newChildrenIds.includes(c.id))\n  }\n\n  /**\n   * Gets the action function to be performed.\n   */\n  get performAction() {\n    if (this.action) {\n      const actionFunction = StreamActions[this.action]\n      if (actionFunction) {\n        return actionFunction\n      }\n      this.#raise(\"unknown action\")\n    }\n    this.#raise(\"action attribute is missing\")\n  }\n\n  /**\n   * Gets the target elements which the template will be rendered to.\n   */\n  get targetElements() {\n    if (this.target) {\n      return this.targetElementsById\n    } else if (this.targets) {\n      return this.targetElementsByQuery\n    } else {\n      this.#raise(\"target or targets attribute is missing\")\n    }\n  }\n\n  /**\n   * Gets the contents of the main `<template>`.\n   */\n  get templateContent() {\n    return this.templateElement.content.cloneNode(true)\n  }\n\n  /**\n   * Gets the main `<template>` used for rendering\n   */\n  get templateElement() {\n    if (this.firstElementChild === null) {\n      const template = this.ownerDocument.createElement(\"template\")\n      this.appendChild(template)\n      return template\n    } else if (this.firstElementChild instanceof HTMLTemplateElement) {\n      return this.firstElementChild\n    }\n    this.#raise(\"first child element must be a <template> element\")\n  }\n\n  /**\n   * Gets the current action.\n   */\n  get action() {\n    return this.getAttribute(\"action\")\n  }\n\n  /**\n   * Gets the current target (an element ID) to which the result will\n   * be rendered.\n   */\n  get target() {\n    return this.getAttribute(\"target\")\n  }\n\n  /**\n   * Gets the current \"targets\" selector (a CSS selector)\n   */\n  get targets() {\n    return this.getAttribute(\"targets\")\n  }\n\n  /**\n   * Reads the request-id attribute\n   */\n  get requestId() {\n    return this.getAttribute(\"request-id\")\n  }\n\n  #raise(message) {\n    throw new Error(`${this.description}: ${message}`)\n  }\n\n  get description() {\n    return (this.outerHTML.match(/<[^>]+>/) ?? [])[0] ?? \"<turbo-stream>\"\n  }\n\n  get beforeRenderEvent() {\n    return new CustomEvent(\"turbo:before-stream-render\", {\n      bubbles: true,\n      cancelable: true,\n      detail: { newStream: this, render: StreamElement.renderElement }\n    })\n  }\n\n  get targetElementsById() {\n    const element = this.ownerDocument?.getElementById(this.target)\n\n    if (element !== null) {\n      return [element]\n    } else {\n      return []\n    }\n  }\n\n  get targetElementsByQuery() {\n    const elements = this.ownerDocument?.querySelectorAll(this.targets)\n\n    if (elements.length !== 0) {\n      return Array.prototype.slice.call(elements)\n    } else {\n      return []\n    }\n  }\n}\n"
  },
  {
    "path": "src/elements/stream_source_element.js",
    "content": "import { connectStreamSource, disconnectStreamSource } from \"../core/index\"\n\nexport class StreamSourceElement extends HTMLElement {\n  streamSource = null\n\n  connectedCallback() {\n    this.streamSource = this.src.match(/^ws{1,2}:/) ? new WebSocket(this.src) : new EventSource(this.src)\n\n    connectStreamSource(this.streamSource)\n  }\n\n  disconnectedCallback() {\n    if (this.streamSource) {\n      this.streamSource.close()\n\n      disconnectStreamSource(this.streamSource)\n    }\n  }\n\n  get src() {\n    return this.getAttribute(\"src\") || \"\"\n  }\n}\n"
  },
  {
    "path": "src/http/fetch.js",
    "content": "import { uuid } from \"../util\"\nimport { LimitedSet } from \"../core/drive/limited_set\"\n\nexport const recentRequests = new LimitedSet(20)\n\nfunction fetchWithTurboHeaders(url, options = {}) {\n  const modifiedHeaders = new Headers(options.headers || {})\n  const requestUID = uuid()\n  recentRequests.add(requestUID)\n  modifiedHeaders.append(\"X-Turbo-Request-Id\", requestUID)\n\n  return window.fetch(url, {\n    ...options,\n    headers: modifiedHeaders\n  })\n}\n\nexport { fetchWithTurboHeaders as fetch }\n"
  },
  {
    "path": "src/http/fetch_request.js",
    "content": "import { FetchResponse } from \"./fetch_response\"\nimport { expandURL } from \"../core/url\"\nimport { dispatch } from \"../util\"\nimport { fetch } from \"./fetch\"\n\nexport function fetchMethodFromString(method) {\n  switch (method.toLowerCase()) {\n    case \"get\":\n      return FetchMethod.get\n    case \"post\":\n      return FetchMethod.post\n    case \"put\":\n      return FetchMethod.put\n    case \"patch\":\n      return FetchMethod.patch\n    case \"delete\":\n      return FetchMethod.delete\n  }\n}\n\nexport const FetchMethod = {\n  get: \"get\",\n  post: \"post\",\n  put: \"put\",\n  patch: \"patch\",\n  delete: \"delete\"\n}\n\nexport function fetchEnctypeFromString(encoding) {\n  switch (encoding.toLowerCase()) {\n    case FetchEnctype.multipart:\n      return FetchEnctype.multipart\n    case FetchEnctype.plain:\n      return FetchEnctype.plain\n    default:\n      return FetchEnctype.urlEncoded\n  }\n}\n\nexport const FetchEnctype = {\n  urlEncoded: \"application/x-www-form-urlencoded\",\n  multipart: \"multipart/form-data\",\n  plain: \"text/plain\"\n}\n\nexport class FetchRequest {\n  abortController = new AbortController()\n  #resolveRequestPromise = (_value) => {}\n\n  constructor(delegate, method, location, requestBody = new URLSearchParams(), target = null, enctype = FetchEnctype.urlEncoded) {\n    const [url, body] = buildResourceAndBody(expandURL(location), method, requestBody, enctype)\n\n    this.delegate = delegate\n    this.url = url\n    this.target = target\n    this.fetchOptions = {\n      credentials: \"same-origin\",\n      redirect: \"follow\",\n      method: method.toUpperCase(),\n      headers: { ...this.defaultHeaders },\n      body: body,\n      signal: this.abortSignal,\n      referrer: this.delegate.referrer?.href\n    }\n    this.enctype = enctype\n  }\n\n  get method() {\n    return this.fetchOptions.method\n  }\n\n  set method(value) {\n    const fetchBody = this.isSafe ? this.url.searchParams : this.fetchOptions.body || new FormData()\n    const fetchMethod = fetchMethodFromString(value) || FetchMethod.get\n\n    this.url.search = \"\"\n\n    const [url, body] = buildResourceAndBody(this.url, fetchMethod, fetchBody, this.enctype)\n\n    this.url = url\n    this.fetchOptions.body = body\n    this.fetchOptions.method = fetchMethod.toUpperCase()\n  }\n\n  get headers() {\n    return this.fetchOptions.headers\n  }\n\n  set headers(value) {\n    this.fetchOptions.headers = value\n  }\n\n  get body() {\n    if (this.isSafe) {\n      return this.url.searchParams\n    } else {\n      return this.fetchOptions.body\n    }\n  }\n\n  set body(value) {\n    this.fetchOptions.body = value\n  }\n\n  get location() {\n    return this.url\n  }\n\n  get params() {\n    return this.url.searchParams\n  }\n\n  get entries() {\n    return this.body ? Array.from(this.body.entries()) : []\n  }\n\n  cancel() {\n    this.abortController.abort()\n  }\n\n  async perform() {\n    const { fetchOptions } = this\n    this.delegate.prepareRequest(this)\n    const event = await this.#allowRequestToBeIntercepted(fetchOptions)\n    try {\n      this.delegate.requestStarted(this)\n\n      if (event.detail.fetchRequest) {\n        this.response = event.detail.fetchRequest.response\n      } else {\n        this.response = fetch(this.url.href, fetchOptions)\n      }\n\n      const response = await this.response\n      return await this.receive(response)\n    } catch (error) {\n      if (error.name !== \"AbortError\") {\n        if (this.#willDelegateErrorHandling(error)) {\n          this.delegate.requestErrored(this, error)\n        }\n        throw error\n      }\n    } finally {\n      this.delegate.requestFinished(this)\n    }\n  }\n\n  async receive(response) {\n    const fetchResponse = new FetchResponse(response)\n    const event = dispatch(\"turbo:before-fetch-response\", {\n      cancelable: true,\n      detail: { fetchResponse },\n      target: this.target\n    })\n    if (event.defaultPrevented) {\n      this.delegate.requestPreventedHandlingResponse(this, fetchResponse)\n    } else if (fetchResponse.succeeded) {\n      this.delegate.requestSucceededWithResponse(this, fetchResponse)\n    } else {\n      this.delegate.requestFailedWithResponse(this, fetchResponse)\n    }\n    return fetchResponse\n  }\n\n  get defaultHeaders() {\n    return {\n      Accept: \"text/html, application/xhtml+xml\"\n    }\n  }\n\n  get isSafe() {\n    return isSafe(this.method)\n  }\n\n  get abortSignal() {\n    return this.abortController.signal\n  }\n\n  acceptResponseType(mimeType) {\n    this.headers[\"Accept\"] = [mimeType, this.headers[\"Accept\"]].join(\", \")\n  }\n\n  async #allowRequestToBeIntercepted(fetchOptions) {\n    const requestInterception = new Promise((resolve) => (this.#resolveRequestPromise = resolve))\n    const event = dispatch(\"turbo:before-fetch-request\", {\n      cancelable: true,\n      detail: {\n        fetchOptions,\n        url: this.url,\n        resume: this.#resolveRequestPromise\n      },\n      target: this.target\n    })\n    this.url = event.detail.url\n    if (event.defaultPrevented) await requestInterception\n\n    return event\n  }\n\n  #willDelegateErrorHandling(error) {\n    const event = dispatch(\"turbo:fetch-request-error\", {\n      target: this.target,\n      cancelable: true,\n      detail: { request: this, error: error }\n    })\n\n    return !event.defaultPrevented\n  }\n}\n\nexport function isSafe(fetchMethod) {\n  return fetchMethodFromString(fetchMethod) == FetchMethod.get\n}\n\nfunction buildResourceAndBody(resource, method, requestBody, enctype) {\n  const searchParams =\n    Array.from(requestBody).length > 0 ? new URLSearchParams(entriesExcludingFiles(requestBody)) : resource.searchParams\n\n  if (isSafe(method)) {\n    return [mergeIntoURLSearchParams(resource, searchParams), null]\n  } else if (enctype == FetchEnctype.urlEncoded) {\n    return [resource, searchParams]\n  } else {\n    return [resource, requestBody]\n  }\n}\n\nfunction entriesExcludingFiles(requestBody) {\n  const entries = []\n\n  for (const [name, value] of requestBody) {\n    if (value instanceof File) continue\n    else entries.push([name, value])\n  }\n\n  return entries\n}\n\nfunction mergeIntoURLSearchParams(url, requestBody) {\n  const searchParams = new URLSearchParams(entriesExcludingFiles(requestBody))\n\n  url.search = searchParams.toString()\n\n  return url\n}\n"
  },
  {
    "path": "src/http/fetch_response.js",
    "content": "import { expandURL } from \"../core/url\"\n\nexport class FetchResponse {\n  constructor(response) {\n    this.response = response\n  }\n\n  get succeeded() {\n    return this.response.ok\n  }\n\n  get failed() {\n    return !this.succeeded\n  }\n\n  get clientError() {\n    return this.statusCode >= 400 && this.statusCode <= 499\n  }\n\n  get serverError() {\n    return this.statusCode >= 500 && this.statusCode <= 599\n  }\n\n  get redirected() {\n    return this.response.redirected\n  }\n\n  get location() {\n    return expandURL(this.response.url)\n  }\n\n  get isHTML() {\n    return this.contentType && this.contentType.match(/^(?:text\\/([^\\s;,]+\\b)?html|application\\/xhtml\\+xml)\\b/)\n  }\n\n  get statusCode() {\n    return this.response.status\n  }\n\n  get contentType() {\n    return this.header(\"Content-Type\")\n  }\n\n  get responseText() {\n    return this.response.clone().text()\n  }\n\n  get responseHTML() {\n    if (this.isHTML) {\n      return this.response.clone().text()\n    } else {\n      return Promise.resolve(undefined)\n    }\n  }\n\n  header(name) {\n    return this.response.headers.get(name)\n  }\n}\n"
  },
  {
    "path": "src/http/index.js",
    "content": "export * from \"./fetch_request\"\nexport * from \"./fetch_response\"\n"
  },
  {
    "path": "src/index.js",
    "content": "import \"./polyfills\"\nimport \"./elements\"\nimport \"./script_warning\"\nimport { StreamActions } from \"./core/streams/stream_actions\"\n\nimport * as Turbo from \"./core\"\n\nwindow.Turbo = { ...Turbo, StreamActions }\nTurbo.start()\n\nexport { StreamActions }\nexport * from \"./core\"\nexport * from \"./elements\"\nexport * from \"./http\"\n"
  },
  {
    "path": "src/observers/appearance_observer.js",
    "content": "export class AppearanceObserver {\n  started = false\n\n  constructor(delegate, element) {\n    this.delegate = delegate\n    this.element = element\n    this.intersectionObserver = new IntersectionObserver(this.intersect)\n  }\n\n  start() {\n    if (!this.started) {\n      this.started = true\n      this.intersectionObserver.observe(this.element)\n    }\n  }\n\n  stop() {\n    if (this.started) {\n      this.started = false\n      this.intersectionObserver.unobserve(this.element)\n    }\n  }\n\n  intersect = (entries) => {\n    const lastEntry = entries.slice(-1)[0]\n    if (lastEntry?.isIntersecting) {\n      this.delegate.elementAppearedInViewport(this.element)\n    }\n  }\n}\n"
  },
  {
    "path": "src/observers/cache_observer.js",
    "content": "export class CacheObserver {\n  selector = \"[data-turbo-temporary]\"\n\n  started = false\n\n  start() {\n    if (!this.started) {\n      this.started = true\n      addEventListener(\"turbo:before-cache\", this.removeTemporaryElements, false)\n    }\n  }\n\n  stop() {\n    if (this.started) {\n      this.started = false\n      removeEventListener(\"turbo:before-cache\", this.removeTemporaryElements, false)\n    }\n  }\n\n  removeTemporaryElements = (_event) => {\n    for (const element of this.temporaryElements) {\n      element.remove()\n    }\n  }\n\n  get temporaryElements() {\n    return [...document.querySelectorAll(this.selector)]\n  }\n}\n"
  },
  {
    "path": "src/observers/form_link_click_observer.js",
    "content": "import { LinkClickObserver } from \"./link_click_observer\"\nimport { getVisitAction } from \"../util\"\n\nexport class FormLinkClickObserver {\n  constructor(delegate, element) {\n    this.delegate = delegate\n    this.linkInterceptor = new LinkClickObserver(this, element)\n  }\n\n  start() {\n    this.linkInterceptor.start()\n  }\n\n  stop() {\n    this.linkInterceptor.stop()\n  }\n\n  // Link hover observer delegate\n\n  canPrefetchRequestToLocation(link, location) {\n    return false\n  }\n\n  prefetchAndCacheRequestToLocation(link, location) {\n    return\n  }\n\n  // Link click observer delegate\n\n  willFollowLinkToLocation(link, location, originalEvent) {\n    return (\n      this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) &&\n      (link.hasAttribute(\"data-turbo-method\") || link.hasAttribute(\"data-turbo-stream\"))\n    )\n  }\n\n  followedLinkToLocation(link, location) {\n    const form = document.createElement(\"form\")\n\n    const type = \"hidden\"\n    for (const [name, value] of location.searchParams) {\n      form.append(Object.assign(document.createElement(\"input\"), { type, name, value }))\n    }\n\n    const action = Object.assign(location, { search: \"\" })\n    form.setAttribute(\"data-turbo\", \"true\")\n    form.setAttribute(\"action\", action.href)\n    form.setAttribute(\"hidden\", \"\")\n\n    const method = link.getAttribute(\"data-turbo-method\")\n    if (method) form.setAttribute(\"method\", method)\n\n    const turboFrame = link.getAttribute(\"data-turbo-frame\")\n    if (turboFrame) form.setAttribute(\"data-turbo-frame\", turboFrame)\n\n    const turboAction = getVisitAction(link)\n    if (turboAction) form.setAttribute(\"data-turbo-action\", turboAction)\n\n    const turboConfirm = link.getAttribute(\"data-turbo-confirm\")\n    if (turboConfirm) form.setAttribute(\"data-turbo-confirm\", turboConfirm)\n\n    const turboStream = link.hasAttribute(\"data-turbo-stream\")\n    if (turboStream) form.setAttribute(\"data-turbo-stream\", \"\")\n\n    this.delegate.submittedFormLinkToLocation(link, location, form)\n\n    document.body.appendChild(form)\n    form.addEventListener(\"turbo:submit-end\", () => form.remove(), { once: true })\n    requestAnimationFrame(() => form.requestSubmit())\n  }\n}\n"
  },
  {
    "path": "src/observers/form_submit_observer.js",
    "content": "import { doesNotTargetIFrame } from \"../util\"\n\nexport class FormSubmitObserver {\n  started = false\n\n  constructor(delegate, eventTarget) {\n    this.delegate = delegate\n    this.eventTarget = eventTarget\n  }\n\n  start() {\n    if (!this.started) {\n      this.eventTarget.addEventListener(\"submit\", this.submitCaptured, true)\n      this.started = true\n    }\n  }\n\n  stop() {\n    if (this.started) {\n      this.eventTarget.removeEventListener(\"submit\", this.submitCaptured, true)\n      this.started = false\n    }\n  }\n\n  submitCaptured = () => {\n    this.eventTarget.removeEventListener(\"submit\", this.submitBubbled, false)\n    this.eventTarget.addEventListener(\"submit\", this.submitBubbled, false)\n  }\n\n  submitBubbled = (event) => {\n    if (!event.defaultPrevented) {\n      const form = event.target instanceof HTMLFormElement ? event.target : undefined\n      const submitter = event.submitter || undefined\n\n      if (\n        form &&\n        submissionDoesNotDismissDialog(form, submitter) &&\n        submissionDoesNotTargetIFrame(form, submitter) &&\n        this.delegate.willSubmitForm(form, submitter)\n      ) {\n        event.preventDefault()\n        event.stopImmediatePropagation()\n        this.delegate.formSubmitted(form, submitter)\n      }\n    }\n  }\n}\n\nfunction submissionDoesNotDismissDialog(form, submitter) {\n  const method = submitter?.getAttribute(\"formmethod\") || form.getAttribute(\"method\")\n\n  return method != \"dialog\"\n}\n\nfunction submissionDoesNotTargetIFrame(form, submitter) {\n  const target = submitter?.getAttribute(\"formtarget\") || form.getAttribute(\"target\")\n\n  return doesNotTargetIFrame(target)\n}\n"
  },
  {
    "path": "src/observers/link_click_observer.js",
    "content": "import { getLocationForLink } from \"../core/url\"\nimport { doesNotTargetIFrame, findLinkFromClickTarget } from \"../util\"\n\nexport class LinkClickObserver {\n  started = false\n\n  constructor(delegate, eventTarget) {\n    this.delegate = delegate\n    this.eventTarget = eventTarget\n  }\n\n  start() {\n    if (!this.started) {\n      this.eventTarget.addEventListener(\"click\", this.clickCaptured, true)\n      this.started = true\n    }\n  }\n\n  stop() {\n    if (this.started) {\n      this.eventTarget.removeEventListener(\"click\", this.clickCaptured, true)\n      this.started = false\n    }\n  }\n\n  clickCaptured = () => {\n    this.eventTarget.removeEventListener(\"click\", this.clickBubbled, false)\n    this.eventTarget.addEventListener(\"click\", this.clickBubbled, false)\n  }\n\n  clickBubbled = (event) => {\n    if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {\n      const target = (event.composedPath && event.composedPath()[0]) || event.target\n      const link = findLinkFromClickTarget(target)\n      if (link && doesNotTargetIFrame(link.target)) {\n        const location = getLocationForLink(link)\n        if (this.delegate.willFollowLinkToLocation(link, location, event)) {\n          event.preventDefault()\n          this.delegate.followedLinkToLocation(link, location)\n        }\n      }\n    }\n  }\n\n  clickEventIsSignificant(event) {\n    return !(\n      (event.target && event.target.isContentEditable) ||\n      event.defaultPrevented ||\n      event.which > 1 ||\n      event.altKey ||\n      event.ctrlKey ||\n      event.metaKey ||\n      event.shiftKey\n    )\n  }\n}\n"
  },
  {
    "path": "src/observers/link_prefetch_observer.js",
    "content": "import { getLocationForLink } from \"../core/url\"\nimport {\n  dispatch,\n  getMetaContent,\n  findClosestRecursively\n} from \"../util\"\n\nimport { FetchMethod, FetchRequest } from \"../http/fetch_request\"\nimport { prefetchCache, cacheTtl } from \"../core/drive/prefetch_cache\"\n\nexport class LinkPrefetchObserver {\n  started = false\n  #prefetchedLink = null\n\n  constructor(delegate, eventTarget) {\n    this.delegate = delegate\n    this.eventTarget = eventTarget\n  }\n\n  start() {\n    if (this.started) return\n\n    if (this.eventTarget.readyState === \"loading\") {\n      this.eventTarget.addEventListener(\"DOMContentLoaded\", this.#enable, { once: true })\n    } else {\n      this.#enable()\n    }\n  }\n\n  stop() {\n    if (!this.started) return\n\n    this.eventTarget.removeEventListener(\"mouseenter\", this.#tryToPrefetchRequest, {\n      capture: true,\n      passive: true\n    })\n    this.eventTarget.removeEventListener(\"mouseleave\", this.#cancelRequestIfObsolete, {\n      capture: true,\n      passive: true\n    })\n\n    this.eventTarget.removeEventListener(\"turbo:before-fetch-request\", this.#tryToUsePrefetchedRequest, true)\n    this.started = false\n  }\n\n  #enable = () => {\n    this.eventTarget.addEventListener(\"mouseenter\", this.#tryToPrefetchRequest, {\n      capture: true,\n      passive: true\n    })\n    this.eventTarget.addEventListener(\"mouseleave\", this.#cancelRequestIfObsolete, {\n      capture: true,\n      passive: true\n    })\n\n    this.eventTarget.addEventListener(\"turbo:before-fetch-request\", this.#tryToUsePrefetchedRequest, true)\n    this.started = true\n  }\n\n  #tryToPrefetchRequest = (event) => {\n    if (getMetaContent(\"turbo-prefetch\") === \"false\") return\n\n    const target = event.target\n    const isLink = target.matches && target.matches(\"a[href]:not([target^=_]):not([download])\")\n\n    if (isLink && this.#isPrefetchable(target)) {\n      const link = target\n      const location = getLocationForLink(link)\n\n      if (this.delegate.canPrefetchRequestToLocation(link, location)) {\n        this.#prefetchedLink = link\n\n        const fetchRequest = new FetchRequest(\n          this,\n          FetchMethod.get,\n          location,\n          new URLSearchParams(),\n          target\n        )\n\n        fetchRequest.fetchOptions.priority = \"low\"\n\n        prefetchCache.putLater(location, fetchRequest, this.#cacheTtl)\n      }\n    }\n  }\n\n  #cancelRequestIfObsolete = (event) => {\n    if (event.target === this.#prefetchedLink) this.#cancelPrefetchRequest()\n  }\n\n  #cancelPrefetchRequest = () => {\n    prefetchCache.clear()\n    this.#prefetchedLink = null\n  }\n\n  #tryToUsePrefetchedRequest = (event) => {\n    if (event.target.tagName !== \"FORM\" && event.detail.fetchOptions.method === \"GET\") {\n      const cached = prefetchCache.get(event.detail.url)\n\n      if (cached) {\n        // User clicked link, use cache response\n        event.detail.fetchRequest = cached\n      }\n\n      prefetchCache.clear()\n    }\n  }\n\n  prepareRequest(request) {\n    const link = request.target\n\n    request.headers[\"X-Sec-Purpose\"] = \"prefetch\"\n\n    const turboFrame = link.closest(\"turbo-frame\")\n    const turboFrameTarget = link.getAttribute(\"data-turbo-frame\") || turboFrame?.getAttribute(\"target\") || turboFrame?.id\n\n    if (turboFrameTarget && turboFrameTarget !== \"_top\") {\n      request.headers[\"Turbo-Frame\"] = turboFrameTarget\n    }\n  }\n\n  // Fetch request interface\n\n  requestSucceededWithResponse() {}\n\n  requestStarted(fetchRequest) {}\n\n  requestErrored(fetchRequest) {}\n\n  requestFinished(fetchRequest) {}\n\n  requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}\n\n  requestFailedWithResponse(fetchRequest, fetchResponse) {}\n\n  get #cacheTtl() {\n    return Number(getMetaContent(\"turbo-prefetch-cache-time\")) || cacheTtl\n  }\n\n  #isPrefetchable(link) {\n    const href = link.getAttribute(\"href\")\n\n    if (!href) return false\n\n    if (unfetchableLink(link)) return false\n    if (linkToTheSamePage(link)) return false\n    if (linkOptsOut(link)) return false\n    if (nonSafeLink(link)) return false\n    if (eventPrevented(link)) return false\n\n    return true\n  }\n}\n\nconst unfetchableLink = (link) => {\n  return link.origin !== document.location.origin || ![\"http:\", \"https:\"].includes(link.protocol) || link.hasAttribute(\"target\")\n}\n\nconst linkToTheSamePage = (link) => {\n  return (link.pathname + link.search === document.location.pathname + document.location.search) || link.href.startsWith(\"#\")\n}\n\nconst linkOptsOut = (link) => {\n  if (link.getAttribute(\"data-turbo-prefetch\") === \"false\") return true\n  if (link.getAttribute(\"data-turbo\") === \"false\") return true\n\n  const turboPrefetchParent = findClosestRecursively(link, \"[data-turbo-prefetch]\")\n  if (turboPrefetchParent && turboPrefetchParent.getAttribute(\"data-turbo-prefetch\") === \"false\") return true\n\n  return false\n}\n\nconst nonSafeLink = (link) => {\n  const turboMethod = link.getAttribute(\"data-turbo-method\")\n  if (turboMethod && turboMethod.toLowerCase() !== \"get\") return true\n\n  if (isUJS(link)) return true\n  if (link.hasAttribute(\"data-turbo-confirm\")) return true\n  if (link.hasAttribute(\"data-turbo-stream\")) return true\n\n  return false\n}\n\nconst isUJS = (link) => {\n  return link.hasAttribute(\"data-remote\") || link.hasAttribute(\"data-behavior\") || link.hasAttribute(\"data-confirm\") || link.hasAttribute(\"data-method\")\n}\n\nconst eventPrevented = (link) => {\n  const event = dispatch(\"turbo:before-prefetch\", { target: link, cancelable: true })\n  return event.defaultPrevented\n}\n"
  },
  {
    "path": "src/observers/page_observer.js",
    "content": "export const PageStage = {\n  initial: 0,\n  loading: 1,\n  interactive: 2,\n  complete: 3\n}\n\nexport class PageObserver {\n  stage = PageStage.initial\n  started = false\n\n  constructor(delegate) {\n    this.delegate = delegate\n  }\n\n  start() {\n    if (!this.started) {\n      if (this.stage == PageStage.initial) {\n        this.stage = PageStage.loading\n      }\n      document.addEventListener(\"readystatechange\", this.interpretReadyState, false)\n      addEventListener(\"pagehide\", this.pageWillUnload, false)\n      this.started = true\n    }\n  }\n\n  stop() {\n    if (this.started) {\n      document.removeEventListener(\"readystatechange\", this.interpretReadyState, false)\n      removeEventListener(\"pagehide\", this.pageWillUnload, false)\n      this.started = false\n    }\n  }\n\n  interpretReadyState = () => {\n    const { readyState } = this\n    if (readyState == \"interactive\") {\n      this.pageIsInteractive()\n    } else if (readyState == \"complete\") {\n      this.pageIsComplete()\n    }\n  }\n\n  pageIsInteractive() {\n    if (this.stage == PageStage.loading) {\n      this.stage = PageStage.interactive\n      this.delegate.pageBecameInteractive()\n    }\n  }\n\n  pageIsComplete() {\n    this.pageIsInteractive()\n    if (this.stage == PageStage.interactive) {\n      this.stage = PageStage.complete\n      this.delegate.pageLoaded()\n    }\n  }\n\n  pageWillUnload = () => {\n    this.delegate.pageWillUnload()\n  }\n\n  get readyState() {\n    return document.readyState\n  }\n}\n"
  },
  {
    "path": "src/observers/scroll_observer.js",
    "content": "export class ScrollObserver {\n  started = false\n\n  constructor(delegate) {\n    this.delegate = delegate\n  }\n\n  start() {\n    if (!this.started) {\n      addEventListener(\"scroll\", this.onScroll, false)\n      this.onScroll()\n      this.started = true\n    }\n  }\n\n  stop() {\n    if (this.started) {\n      removeEventListener(\"scroll\", this.onScroll, false)\n      this.started = false\n    }\n  }\n\n  onScroll = () => {\n    this.updatePosition({ x: window.pageXOffset, y: window.pageYOffset })\n  }\n\n  // Private\n\n  updatePosition(position) {\n    this.delegate.scrollPositionChanged(position)\n  }\n}\n"
  },
  {
    "path": "src/observers/stream_observer.js",
    "content": "import { FetchResponse } from \"../http/fetch_response\"\nimport { StreamMessage } from \"../core/streams/stream_message\"\n\nexport class StreamObserver {\n  sources = new Set()\n  #started = false\n\n  constructor(delegate) {\n    this.delegate = delegate\n  }\n\n  start() {\n    if (!this.#started) {\n      this.#started = true\n      addEventListener(\"turbo:before-fetch-response\", this.inspectFetchResponse, false)\n    }\n  }\n\n  stop() {\n    if (this.#started) {\n      this.#started = false\n      removeEventListener(\"turbo:before-fetch-response\", this.inspectFetchResponse, false)\n    }\n  }\n\n  connectStreamSource(source) {\n    if (!this.streamSourceIsConnected(source)) {\n      this.sources.add(source)\n      source.addEventListener(\"message\", this.receiveMessageEvent, false)\n    }\n  }\n\n  disconnectStreamSource(source) {\n    if (this.streamSourceIsConnected(source)) {\n      this.sources.delete(source)\n      source.removeEventListener(\"message\", this.receiveMessageEvent, false)\n    }\n  }\n\n  streamSourceIsConnected(source) {\n    return this.sources.has(source)\n  }\n\n  inspectFetchResponse = (event) => {\n    const response = fetchResponseFromEvent(event)\n    if (response && fetchResponseIsStream(response)) {\n      event.preventDefault()\n      this.receiveMessageResponse(response)\n    }\n  }\n\n  receiveMessageEvent = (event) => {\n    if (this.#started && typeof event.data == \"string\") {\n      this.receiveMessageHTML(event.data)\n    }\n  }\n\n  async receiveMessageResponse(response) {\n    const html = await response.responseHTML\n    if (html) {\n      this.receiveMessageHTML(html)\n    }\n  }\n\n  receiveMessageHTML(html) {\n    this.delegate.receivedMessageFromStream(StreamMessage.wrap(html))\n  }\n}\n\nfunction fetchResponseFromEvent(event) {\n  const fetchResponse = event.detail?.fetchResponse\n  if (fetchResponse instanceof FetchResponse) {\n    return fetchResponse\n  }\n}\n\nfunction fetchResponseIsStream(response) {\n  const contentType = response.contentType ?? \"\"\n  return contentType.startsWith(StreamMessage.contentType)\n}\n"
  },
  {
    "path": "src/polyfills/index.js",
    "content": ""
  },
  {
    "path": "src/script_warning.js",
    "content": "import { unindent } from \"./util\"\n;(() => {\n  const scriptElement = document.currentScript\n  if (!scriptElement) return\n  if (scriptElement.hasAttribute(\"data-turbo-suppress-warning\")) return\n\n  let element = scriptElement.parentElement\n  while (element) {\n    if (element == document.body) {\n      return console.warn(\n        unindent`\n        You are loading Turbo from a <script> element inside the <body> element. This is probably not what you meant to do!\n\n        Load your application’s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.\n\n        For more information, see: https://turbo.hotwired.dev/handbook/building#working-with-script-elements\n\n        ——\n        Suppress this warning by adding a \"data-turbo-suppress-warning\" attribute to: %s\n      `,\n        scriptElement.outerHTML\n      )\n    }\n\n    element = element.parentElement\n  }\n})()\n"
  },
  {
    "path": "src/tests/fixtures/422.html",
    "content": "<html>\n  <head>\n    <title>Unprocessable Content</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n  </head>\n  <body>\n    <h1>Unprocessable Content</h1>\n\n    <turbo-frame id=\"frame\">\n      <h2>Frame: Unprocessable Content</h2>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/422_morph.html",
    "content": "<html>\n  <head>\n    <meta name=\"turbo-refresh-method\" content=\"morph\">\n    <meta name=\"turbo-refresh-scroll\" content=\"preserve\">\n\n    <title>Unprocessable Content</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n  </head>\n  <body>\n    <h1>Unprocessable Content</h1>\n\n    <turbo-frame id=\"frame\">\n      <h2>Frame: Unprocessable Content</h2>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/422_tall.html",
    "content": "<html>\n  <head>\n    <title>Unprocessable Content</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n  </head>\n  <body>\n    <main style=\"height: 1000vh\">\n      <h1>Unprocessable Content</h1>\n      <turbo-frame id=\"frame\">\n        <h2>Frame: Unprocessable Content</h2>\n      </turbo-frame>\n    </main>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/500.html",
    "content": "<html>\n  <head>\n    <title>Internal Server Error</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n  </head>\n  <body>\n    <h1>Internal Server Error</h1>\n\n    <turbo-frame id=\"frame\">\n      <h2>Frame: Internal Server Error</h2>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/additional_assets.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Additional assets</title>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"/src/tests/fixtures/test.css\">\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <noscript>\n      <link rel=\"stylesheet\" type=\"text/css\" href=\"/src/tests/fixtures/noscript.css\">\n    </noscript>\n  </head>\n  <body>\n    <h1>Additional assets</h1>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/additional_script.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Additional assets</title>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"/src/tests/fixtures/test.css\">\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <script id=\"additional\" src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>Additional assets</h1>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/async_script.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <script>\n      addEventListener(\"DOMContentLoaded\", function() {\n        setTimeout(function() {\n          var script = document.createElement(\"script\")\n          script.src = \"/dist/turbo.es2017-umd.js\"\n          script.setAttribute(\"async\", \"\")\n          document.head.appendChild(script)\n        }, 1)\n      })\n    </script>\n  </head>\n  <body>\n    <section>\n      <h1>Async script</h1>\n      <p><a href=\"async_script_2.html\" id=\"async-link\">Async script 2</a></p>\n    </section>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/async_script_2.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <script src=\"/dist/turbo.es2017-umd.js\" async></script>\n  </head>\n  <body>\n    <section>\n      <h1>Async script 2</h1>\n    </section>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/autofocus-inert.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Autofocus</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>Autofocus With Inert Elements</h1>\n    <dialog>\n      <button id=\"dialog-autofocus-element\" autofocus>dialog[autofocus]</button>\n    </dialog>\n    <details>\n      <button id=\"details-autofocus-element\" autofocus>details[autofocus]</button>\n    </details>\n    <div hidden>\n      <button id=\"hidden-autofocus-element\" autofocus>div[hidden][autofocus]</button>\n    </div>\n    <div inert>\n      <button id=\"inert-autofocus-element\" autofocus>div[inert][autofocus]</button>\n    </div>\n    <button id=\"disabled-autofocus-element\" disabled autofocus>button[disabled][autofocus]</button>\n    <form disabled>\n      <button id=\"visible-autofocus-element\" autofocus>button[autofocus]</button>\n    </form>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/autofocus.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Autofocus</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <meta name=\"turbo-refresh-method\" content=\"morph\">\n    <meta name=\"turbo-refresh-scroll\" content=\"preserve\">\n  </head>\n  <body>\n    <h1>Autofocus</h1>\n\n    <button autofocus id=\"first-autofocus-element\" type=\"button\">First [autofocus]</button>\n    <button autofocus id=\"second-autofocus-element\" type=\"button\">Second [autofocus]</button>\n\n    <a id=\"autofocus-inert-link\" href=\"/src/tests/fixtures/autofocus-inert.html\">autofocus-inert.html link</a>\n    <a id=\"frame-outer-link\" href=\"/src/tests/fixtures/frames/form.html\" data-turbo-frame=\"frame\">Outer #frame link to frames/form.html</a>\n\n    <turbo-frame id=\"frame\">\n      <a id=\"frame-inner-link\" href=\"/src/tests/fixtures/frames/form.html\">Inner #frame link to frames/form.html</a>\n    </turbo-frame>\n\n    <turbo-frame id=\"drives-frame\" target=\"frame\">\n      <a id=\"drives-frame-target-link\" href=\"/src/tests/fixtures/frames/form.html\">#drives-frame link to frames/form.html</a>\n    </turbo-frame>\n\n    <form id=\"form\" action=\"/__turbo/refresh\" method=\"post\" class=\"redirect\">\n      <input id=\"form-text\" type=\"text\" name=\"text\" value=\"\" autofocus>\n      <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/autofocus.html\">\n      <input type=\"hidden\" name=\"sleep\" value=\"50\">\n      <input id=\"form-submit\" type=\"submit\" value=\"form[method=post]\">\n    </form>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/bare.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Bare</title>\n  </head>\n  <body>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/body_noscript.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Body noscript test</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>Body noscript test</h1>\n    <p><a id=\"back-link\" href=\"/src/tests/fixtures/rendering.html\">Go back</a></p>\n    <noscript>\n      <link rel=\"stylesheet\" type=\"text/css\" href=\"/src/tests/fixtures/noscript.css\">\n    </noscript>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/body_noscript_with_content.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Body noscript with content</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>Body noscript with content</h1>\n    <p><a id=\"back-link\" href=\"/src/tests/fixtures/rendering.html\">Go back</a></p>\n    <noscript id=\"lazy-load-noscript\">\n      <img src=\"/src/tests/fixtures/logo.png\" alt=\"Lazy loaded image\">\n    </noscript>\n    <noscript id=\"mixed-noscript\">\n      <img src=\"/src/tests/fixtures/logo.png\" alt=\"Another image\">\n      <link rel=\"stylesheet\" type=\"text/css\" href=\"/src/tests/fixtures/noscript.css\">\n    </noscript>\n    <noscript id=\"alternate-stylesheet-noscript\">\n      <img src=\"/src/tests/fixtures/logo.png\" alt=\"Alternate stylesheet image\">\n      <link rel=\"alternate stylesheet\" type=\"text/css\" href=\"/src/tests/fixtures/noscript.css\">\n    </noscript>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/body_script.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Body script</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <meta name=\"turbo-cache-control\" content=\"no-preview\">\n  </head>\n  <body>\n    <h1>Body script</h1>\n    <script>\n      if (\"bodyScriptEvaluationCount\" in window) {\n        window.bodyScriptEvaluationCount++\n      } else {\n        window.bodyScriptEvaluationCount = 1\n      }\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/cache_observer.html",
    "content": "<!DOCTYPE html>\n <html>\n   <head>\n     <meta charset=\"utf-8\">\n     <title>Turbo</title>\n     <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n     <script src=\"/src/tests/fixtures/test.js\"></script>\n   </head>\n   <body>\n     <section>\n       <h1>Cache Observer</h1>\n       <div id=\"temporary\" data-turbo-temporary>data-turbo-temporary</div>\n       <p><a id=\"link\" href=\"/src/tests/fixtures/rendering.html\">rendering</a></p>\n       <p><a id=\"redirect-here-link\" href=\"/__turbo/redirect?path=/src/tests/fixtures/cache_observer.html\">Redirection link back to here</a></p>\n     </section>\n   </body>\n </html>\n"
  },
  {
    "path": "src/tests/fixtures/dir_rtl.html",
    "content": "<!doctype html>\n<html dir=\"rtl\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Turbo</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>html[dir=\"rtl\"]</h1>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/drive.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Drive</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>Drive</h1>\n\n    <div>\n      <a id=\"drive_enabled\" href=\"/src/tests/fixtures/drive.html\">Drive enabled link</a>\n      <a id=\"drive_enabled_external\" href=\"https://example.com\">Drive enabled external link</a>\n    </div>\n\n    <div data-turbo=\"false\">\n      <a id=\"drive_disabled\" href=\"/src/tests/fixtures/drive.html\">Drive disabled link</a>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/drive_disabled.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Drive (Disabled by Default)</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <script type=\"module\">\n      addEventListener(\"click\", event => {\n        if (event.target.id == \"requestSubmit\") {\n          event.preventDefault()\n          const form = event.target.closest('form')\n          form.requestSubmit()\n        }\n      })\n    </script>\n    <script>\n      Turbo.config.drive.enabled = false\n    </script>\n  </head>\n  <body>\n    <h1>Drive (Disabled by Default)</h1>\n\n    <div data-turbo=\"true\">\n      <a id=\"drive_enabled\" href=\"/src/tests/fixtures/drive_disabled.html\">Drive enabled link</a>\n    </div>\n\n    <div>\n      <a id=\"drive_disabled\" href=\"/src/tests/fixtures/drive_disabled.html\">Drive disabled link</a>\n    </div>\n\n    <form action=\"/__turbo/redirect\" method=\"post\" id=\"no_submitter_drive_enabled\" data-turbo=\"true\">\n      <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/form.html\">\n      <input type=\"hidden\" name=\"greeting\" value=\"Hello from a redirect\">\n      <a href=\"#\" id=\"requestSubmit\">Drive enabled submit via JS</a>\n    </form>\n\n    <turbo-frame id=\"frame\">\n      <h2>Hello from a frame</h2>\n\n      <a href=\"/src/tests/fixtures/drive_disabled.html\">Navigate #frame</a>\n      <form action=\"/src/tests/fixtures/drive_disabled.html\">\n        <button>Navigate #frame</button>\n      </form>\n      <custom-link-element link=\"/src/tests/fixtures/frames/frame.html\">\n        <span id=\"frame-navigation-with-slot\">Link in slot</span>\n      </custom-link-element>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/es_locale.html",
    "content": "<!DOCTYPE html>\n<html lang=\"es\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>html[lang=\"es\"]</h1>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/esm.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>ESM</title>\n    <script type=\"module\" data-turbo-track=\"reload\">\n      import \"/dist/turbo.es2017-esm.js\"\n    </script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <meta name=\"test\" content=\"foo\">\n  </head>\n  <body>\n    <h1>ESM</h1>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/eval_false_script.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>data-turbo-eval=false script</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>data-turbo-eval=false script</h1>\n    <script data-turbo-eval=\"false\">\n      if (\"bodyScriptEvaluationCount\" in window) {\n        window.bodyScriptEvaluationCount++\n      } else {\n        window.bodyScriptEvaluationCount = 1\n      }\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/form.html",
    "content": "<!DOCTYPE html>\n<html id=\"html\" data-skip-event-details=\"turbo:submit-start turbo:submit-end turbo:fetch-request-error\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Form</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <style id=\"form-fixture-styles\">\n      dialog {\n        display: block;\n        position: static;\n      }\n    </style>\n  </head>\n  <body>\n    <h1>Form</h1>\n    <div id=\"standard\">\n      <form id=\"standard-form\" action=\"/__turbo/redirect\" method=\"post\" class=\"redirect\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/form.html\">\n        <input type=\"hidden\" name=\"greeting\" value=\"Hello from a redirect\">\n        <input id=\"standard-post-form-submit\" type=\"submit\" value=\"form[method=post]\">\n      </form>\n      <form action=\"/__turbo/redirect\" method=\"get\" class=\"redirect\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/form.html\">\n        <input type=\"hidden\" name=\"greeting\" value=\"Hello from a redirect\">\n        <input id=\"standard-get-form-submit\" type=\"submit\" value=\"form[method=get]\">\n      </form>\n      <form action=\"/__turbo/redirect\" method=\"post\" class=\"redirect\" data-turbo-frame=\"_top\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/one.html\">\n        <button>form[data-turbo-frame=_top]</button>\n      </form>\n      <form action=\"/__turbo/redirect\" method=\"get\" data-turbo-stream class=\"redirect\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/form.html\">\n        <input type=\"hidden\" name=\"greeting\" value=\"Hello from a redirect\">\n        <input id=\"standard-get-form-with-stream-opt-in-submit\" type=\"submit\" value=\"form[method=get]\">\n      </form>\n      <form action=\"/__turbo/redirect\" class=\"redirect\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/form.html\">\n        <button id=\"standard-get-form-with-stream-opt-in-submitter\" data-turbo-stream>form[method=get] button[data-turbo-stream]</button>\n      </form>\n      <form action=\"/__turbo/redirect\" method=\"post\" class=\"redirect\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/form.html\">\n        <input type=\"hidden\" name=\"greeting\" value=\"Hello from a redirect\">\n        <input id=\"submits-with-form-input\" type=\"submit\" value=\"Save\" data-turbo-submits-with=\"Saving...\">\n      </form>\n      <form action=\"/__turbo/redirect\" method=\"post\" class=\"redirect\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/form.html\">\n        <input type=\"hidden\" name=\"sleep\" value=\"200\">\n        <input type=\"hidden\" name=\"greeting\" value=\"Hello from a redirect\">\n        <button id=\"submits-with-form-button\" type=\"submit\" data-turbo-submits-with=\"Saving...\">Save</button>\n      </form>\n      <hr>\n      <form>\n        <button id=\"form-action-none-q-a\" name=\"q\" value=\"a\">Submit ?q=a to form:not([action])</button>\n      </form>\n      <form action=\"/src/tests/fixtures/form.html\">\n        <button id=\"form-action-self-sort\" name=\"sort\" value=\"asc\">Submit ?sort=asc to form[action]</button>\n        <button id=\"form-action-self-q-b\" name=\"q\" value=\"b\">Submit ?q=b to form[action]</button>\n      </form>\n      <form action=\"/src/tests/fixtures/form.html?q=c\">\n        <button id=\"form-action-self-submit\">Submit form[action]</button>\n      </form>\n      <form action=\"/__turbo/redirect?path=%2Fsrc%2Ftests%2Ffixtures%2Fform.html%3Fsort%3Dasc\" method=\"post\">\n        <button id=\"form-action-post-redirect-self-q-b\" name=\"q\" value=\"b\">POST q=b to form[action]</button>\n      </form>\n      <hr>\n      <form action=\"/__turbo/messages\" method=\"post\" class=\"created\">\n        <input type=\"hidden\" name=\"content\" value=\"Hello!\">\n        <input type=\"submit\" style=\"\">\n      </form>\n      <form action=\"/__turbo/messages\" method=\"post\" class=\"no-content\">\n        <input type=\"hidden\" name=\"content\" value=\"Hello!\">\n        <input type=\"hidden\" name=\"status\" value=\"204\">\n        <input type=\"submit\" style=\"\">\n      </form>\n      <form action=\"/__turbo/redirect\" method=\"post\" class=\"no-enctype\">\n        <input type=\"submit\">\n      </form>\n      <form action=\"/__turbo/redirect\" method=\"post\" enctype=\"multipart/form-data\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/form.html\">\n        <input type=\"submit\">\n      </form>\n      <form action=\"/__turbo/redirect\" method=\"get\" enctype=\"multipart/form-data\">\n        <input type=\"submit\">\n      </form>\n      <form action=\"/__turbo/redirect\" method=\"get\" class=\"greeting\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/one.html\">\n        <input type=\"hidden\" name=\"greeting\" value=\"Hello from a form\">\n        <input type=\"submit\">\n      </form>\n      <form action=\"/__turbo/redirect\" method=\"post\" class=\"sleep\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/form.html\">\n        <input type=\"hidden\" name=\"sleep\" value=\"500\">\n        <input type=\"submit\">\n      </form>\n      <form action=\"/src/tests/fixtures/form.html?query=1\" method=\"get\" class=\"conflicting-values\">\n        <input type=\"hidden\" name=\"query\" value=\"2\">\n        <input type=\"submit\">\n      </form>\n      <form action=\"/__turbo/redirect\" method=\"post\" class=\"confirm\" data-turbo-confirm=\"Are you sure?\">\n        <input type=\"submit\">\n        <button type=\"submit\" name=\"greeting\" id=\"secondary_submitter\" data-turbo-confirm=\"Are you really sure?\" formaction=\"/__turbo/redirect?path=/src/tests/fixtures/one.html\" formmethod=\"post\" value=\"secondary_submitter\">Secondary action</button>\n      </form>\n    </div>\n    <hr>\n    <div id=\"no-action\">\n      <form class=\"single\">\n        <input type=\"hidden\" name=\"query\" value=\"1\">\n        <input type=\"submit\">\n      </form>\n      <form class=\"multiple\">\n        <input type=\"hidden\" name=\"query\" value=\"1\">\n        <input type=\"hidden\" name=\"query\" value=\"2\">\n        <input type=\"submit\">\n      </form>\n      <form method=\"get\" class=\"button-param\">\n        <input type=\"hidden\" name=\"query\" value=\"1\">\n        <button type=\"submit\" name=\"button\">Submit</button>\n      </form>\n    </div>\n    <div id=\"blank-formaction\">\n      <form action=\"/src/tests/fixtures/one.html\">\n        <button formaction=\"\">Submit</button>\n      </form>\n    </div>\n    <hr>\n    <div id=\"action-input\">\n      <form class=\"no-action\">\n        <input type=\"hidden\" name=\"action\" value=\"1\">\n        <input type=\"hidden\" name=\"query\" value=\"1\">\n        <input type=\"submit\">\n      </form>\n      <form class=\"action\" action=\"/src/tests/fixtures/one.html\">\n        <input type=\"hidden\" name=\"action\" value=\"1\">\n        <input type=\"hidden\" name=\"query\" value=\"1\">\n        <input type=\"submit\">\n      </form>\n    </div>\n    <hr>\n    <div id=\"reject\">\n      <form class=\"unprocessable_content\" action=\"/__turbo/reject\" method=\"post\" style=\"margin-top:100vh\">\n        <input type=\"hidden\" name=\"status\" value=\"422\">\n        <input type=\"submit\">\n      </form>\n      <form class=\"unprocessable_content_with_tall_form\" action=\"/__turbo/reject/tall\" method=\"post\">\n        <input type=\"hidden\" name=\"status\" value=\"422\">\n        <input type=\"submit\" style=\"margin-top:1000vh\">\n      </form>\n      <form id=\"reject-form\" class=\"internal_server_error\" action=\"/__turbo/reject\" method=\"post\">\n        <input type=\"hidden\" name=\"status\" value=\"500\">\n        <input type=\"submit\">\n      </form>\n    </div>\n    <hr>\n    <div id=\"submitter\">\n      <form action=\"/src/tests/fixtures/one.html\" method=\"get\">\n        <button type=\"submit\" formmethod=\"post\" formaction=\"/__turbo/redirect\"\n            name=\"path\" value=\"/src/tests/fixtures/two.html\">Submit</button>\n      </form>\n      <form action=\"/__turbo/redirect\" method=\"post\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/form.html\">\n        <input type=\"submit\" formenctype=\"multipart/form-data\">\n      </form>\n\n      <form action=\"/src/tests/fixtures/frames/form.html\" method=\"get\">\n        <button type=\"submit\" data-turbo-frame=\"frame\">GET to Frame</button>\n      </form>\n\n      <form action=\"/__turbo/redirect\" method=\"post\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/frames/form.html\">\n        <button type=\"submit\" data-turbo-frame=\"frame\">POST to Frame</button>\n      </form>\n    </div>\n    <hr>\n    <div id=\"turbo-false\">\n      <form action=\"/__turbo/redirect\" method=\"post\" data-turbo=\"false\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/one.html\">\n        <input type=\"submit\">\n      </form>\n\n      <form action=\"/__turbo/redirect\" method=\"post\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/one.html\">\n        <input type=\"submit\" data-turbo=\"false\">\n      </form>\n    </div>\n    <hr>\n    <div id=\"skipped\">\n      <dialog id=\"dialog-method\" open>\n        <form method=\"dialog\">\n          <button type=\"submit\">Close</button>\n        </form>\n      </dialog>\n\n      <dialog id=\"dialog-formmethod\" open>\n        <form action=\"/__turbo/redirect\" method=\"post\">\n          <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/one.html\">\n          <button formmethod=\"dialog\">Close</button>\n        </form>\n      </dialog>\n\n      <hr>\n      <dialog id=\"dialog-method-turbo-frame\" open>\n        <form action=\"/__turbo/redirect\" method=\"dialog\">\n          <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/frames/frame.html\">\n          <button>Close</button>\n        </form>\n      </dialog>\n\n      <dialog id=\"dialog-formmethod-turbo-frame\" open>\n        <form action=\"/__turbo/redirect\" method=\"post\" data-turbo-frame=\"frame\">\n          <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/frames/frame.html\">\n          <button formmethod=\"dialog\">Close</button>\n        </form>\n      </dialog>\n\n      <hr>\n      <form action=\"/src/tests/fixtures/one.html\" method=\"get\" target=\"iframe\">\n        <button>Submit iframe target</button>\n      </form>\n\n      <form action=\"/src/tests/fixtures/one.html\" method=\"get\">\n        <button formtarget=\"iframe\">Submit iframe formtarget</button>\n      </form>\n    </div>\n    <hr>\n    <div id=\"targets-frame\">\n      <form id=\"form_one\" action=\"/__turbo/redirect\" method=\"post\" data-turbo-frame=\"frame\" class=\"one\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/one.html\">\n        <button type=\"submit\">Submit</button>\n      </form>\n\n      <form action=\"/__turbo/redirect\" method=\"post\" data-turbo-frame=\"frame\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/frames/frame.html\">\n        <button id=\"targets-frame-post-form-submit\" type=\"submit\">targets-frame form[method=post]</button>\n      </form>\n\n      <form action=\"/__turbo/redirect\" method=\"get\" data-turbo-frame=\"frame\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/frames/frame.html\">\n        <button id=\"targets-frame-get-form-submit\" type=\"submit\">targets-frame form[method=get]</button>\n      </form>\n\n      <form action=\"/__turbo/redirect\" method=\"post\" data-turbo-frame=\"frame\" class=\"frame\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/frames/frame.html\">\n        <button type=\"submit\">Submit</button>\n      </form>\n    </div>\n    <turbo-frame id=\"frame\">\n      <h2>Frame: Form</h2>\n      <form action=\"/__turbo/redirect\" method=\"post\" class=\"redirect\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/frames/form.html\">\n        <input type=\"submit\">\n      </form>\n      <form action=\"/__turbo/redirect\" method=\"post\" class=\"redirect\" data-turbo-frame=\"_top\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/frames/form.html\">\n        <button>#frame form[data-turbo-frame=_top]</button>\n      </form>\n      <form action=\"/__turbo/messages\" method=\"post\" class=\"created\">\n        <input type=\"hidden\" name=\"content\" value=\"Hello!\">\n        <input type=\"submit\" style=\"\">\n      </form>\n      <form action=\"/__turbo/messages\" method=\"post\" class=\"no-content\">\n        <input type=\"hidden\" name=\"content\" value=\"Hello!\">\n        <input type=\"hidden\" name=\"status\" value=\"204\">\n        <input type=\"submit\" style=\"\">\n      </form>\n      <form class=\"unprocessable_content\" action=\"/__turbo/reject\" method=\"post\">\n        <input type=\"hidden\" name=\"status\" value=\"422\">\n        <input type=\"submit\">\n      </form>\n      <form class=\"internal_server_error\" action=\"/__turbo/reject\" method=\"post\">\n        <input type=\"hidden\" name=\"status\" value=\"500\">\n        <input type=\"submit\">\n      </form>\n      <form action=\"/__turbo/redirect\" method=\"post\" data-turbo=\"false\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/one.html\">\n        <input type=\"submit\">\n      </form>\n      <form action=\"/__turbo/redirect\" method=\"post\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/one.html\">\n        <input type=\"submit\" data-turbo=\"false\">\n      </form>\n      <form action=\"/__turbo/messages\" method=\"post\" class=\"stream\">\n        <input type=\"hidden\" name=\"type\" value=\"stream\">\n        <input type=\"hidden\" name=\"content\" value=\"Hello!\">\n        <input type=\"submit\">\n      </form>\n      <a href=\"/src/tests/fixtures/frames/frame.html\" data-turbo-method=\"get\" id=\"link-method-inside-frame\">Method link inside frame</a><br />\n      <a href=\"/src/tests/fixtures/frames/hello.html\" data-turbo-method=\"get\" data-turbo-frame=\"_top\" id=\"link-method-inside-frame-target-top\">Break-out of frame with method link inside frame</a><br />\n      <a href=\"/src/tests/fixtures/frames/hello.html\" data-turbo-method=\"get\" data-turbo-frame=\"hello\" id=\"link-method-inside-frame-with-target\">Method link inside frame targeting another frame</a><br />\n      <a href=\"/__turbo/messages?content=Link!&type=stream\" data-turbo-method=\"post\" id=\"stream-link-method-inside-frame\">Stream link inside frame</a>\n      <a href=\"/__turbo/messages?content=Link!&type=stream\" data-turbo-method=\"get\" data-turbo-stream id=\"stream-link-get-method-inside-frame\">Stream link GET inside frame</a>\n      <a href=\"/__turbo/messages?content=Link!&type=stream\" data-turbo-stream id=\"stream-link-inside-frame\">Stream link (no method) inside frame</a>\n      <a href=\"/__turbo/messages?content=Link!&type=stream\" data-turbo-method=\"post\" data-turbo-confirm=\"Are you sure?\" id=\"link-method-inside-frame-with-confirmation\"data-turbo-confirm=\"Are you sure?\">Stream link inside frame with confirmation</a>\n      <form>\n        <a href=\"/src/tests/fixtures/frames/frame.html\" data-turbo-method=\"get\" id=\"method-link-within-form-inside-frame\">Method link within form inside frame</a><br />\n        <a href=\"/__turbo/messages?content=Link!&type=stream\" data-turbo-method=\"post\" id=\"stream-link-method-within-form-inside-frame\">Stream link within form inside frame</a>\n      </form>\n      <form action=\"/__turbo/messages/1\" method=\"put\" class=\"stream put\">\n        <input type=\"hidden\" name=\"type\" value=\"stream\">\n        <input type=\"hidden\" name=\"content\" value=\"Hello!\">\n        <input type=\"submit\">\n      </form>\n      <form action=\"/src/tests/fixtures/one.html\" method=\"get\">\n        <button type=\"submit\" data-turbo-frame=\"_top\">Break-out of Frame with GET</button>\n      </form>\n      <form action=\"/__turbo/redirect\" method=\"post\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/one.html\">\n        <button type=\"submit\" data-turbo-frame=\"_top\">Break-out of Frame with POST</button>\n      </form>\n      <form action=\"/src/tests/fixtures/frames/hello.html\" method=\"get\" data-turbo-frame=\"hello\">\n        <button type=\"submit\">Navigate other Frame with GET</button>\n      </form>\n      <form action=\"/src/tests/fixtures/frames/hello.html\" method=\"get\">\n        <button type=\"submit\" data-turbo-frame=\"hello\">Navigate other Frame with GET</button>\n      </form>\n      <form action=\"/__turbo/redirect\" method=\"post\" data-turbo-frame=\"hello\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/frames/hello.html\">\n        <button type=\"submit\">Navigate other Frame with POST</button>\n      </form>\n      <form action=\"/__turbo/redirect\" method=\"post\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/frames/hello.html\">\n        <button type=\"submit\" data-turbo-frame=\"hello\">Navigate other Frame with POST</button>\n      </form>\n      <div id=\"messages\">\n      </div>\n      <form id=\"form-with-external-inputs\" action=\"/src/tests/fixtures/frames/hello.html\" data-turbo-action=\"replace\" data-turbo-frame=\"hello\"></form>\n      <select id=\"external-select\" form=\"form-with-external-inputs\" name=\"greeting\">\n        <option selected>Hello from a replace Visit</option>\n      </select>\n      <form id=\"form-with-buttons-triggered-by-js\" action=\"/__turbo/redirect\" data-turbo-frame=\"_top\" method=\"post\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/one.html\">\n        <button type=\"submit\" formaction=\"/src/tests/fixtures/frames/hello.html\" formmethod=\"get\" id=\"button-triggered-by-js\" data-turbo-action=\"replace\" data-turbo-frame=\"hello\">Navigate other Frame with GET</button>\n        <button type=\"button\" onclick=\"this.form.requestSubmit(document.getElementById('button-triggered-by-js'))\" id=\"request-submit-trigger\">Trigger Navigate other Frame with GET</button>\n      </form>\n    </turbo-frame>\n    <a href=\"/src/tests/fixtures/frames/hello.html\" data-turbo-method=\"get\" id=\"link-method-outside-frame\">Method link outside frame</a><br />\n    <a href=\"/__turbo/messages?content=Link!&type=stream\" data-turbo-method=\"post\" id=\"stream-link-method-outside-frame\">Stream link outside frame</a>\n    <form>\n      <a href=\"/src/tests/fixtures/frames/hello.html\" data-turbo-method=\"get\" id=\"link-method-within-form-outside-frame\">Method link within form outside frame</a><br />\n      <a href=\"/__turbo/messages?content=Link!&type=stream\" data-turbo-method=\"post\" id=\"stream-link-method-within-form-outside-frame\">Stream link within form outside frame</a>\n    </form>\n    <hr>\n    <form method=\"post\" action=\"https://httpbin.org/post\">\n      <button id=\"submit-external\">POST to https://httpbin.org/post</button>\n    </form>\n     <a href=\"/__turbo/redirect?path=%2Fsrc%2Ftests%2Ffixtures%2Fframes%2Fhello.html\" data-turbo-method=\"post\" data-turbo-frame=\"hello\" id=\"turbo-method-post-to-targeted-frame\">Turbo method post to targeted frame</a>\n     <a href=\"/__turbo/redirect?path=%2Fsrc%2Ftests%2Ffixtures%2Fframes%2Fhello.html\" data-turbo-method=\"post\" data-turbo-frame=\"hello\" target=\"\" id=\"turbo-method-post-empty-target\">Turbo method post with empty target</a>\n     <a href=\"/__turbo/redirect?path=%2Fsrc%2Ftests%2Ffixtures%2Fframes%2Fhello.html\" data-turbo-method=\"post\" data-turbo-frame=\"hello\" target id=\"turbo-method-post-bare-target\">Turbo method post with bare target</a>\n    <turbo-frame id=\"hello\"></turbo-frame>\n    <hr>\n\n    <turbo-frame id=\"ignored\">\n      <form method=\"post\" action=\"https://httpbin.org/post\">\n        <button id=\"submit-external-within-ignored\">POST to https://httpbin.org/post within #hello</button>\n      </form>\n    </turbo-frame>\n\n    <form method=\"post\" action=\"https://httpbin.org/post\">\n      <button id=\"submit-external\">POST to https://httpbin.org/post</button>\n    </form>\n\n    <form method=\"post\" action=\"https://httpbin.org/post\" data-turbo-frame=\"ignored\">\n      <button id=\"submit-external-target-ignored\">POST to https://httpbin.org/post targeting #hello</button>\n    </form>\n    <iframe name=\"iframe\"></iframe>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/form_mode.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Form</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <script>\n      const params = new URLSearchParams(window.location.search)\n\n      if (params.has(\"formMode\")) {\n        window.Turbo.setFormMode(params.get(\"formMode\"))\n      }\n    </script>\n  </head>\n  <body>\n    <h1>Form Mode</h1>\n\n    <form id=\"form\" action=\"/__turbo/redirect\" method=\"post\">\n      <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/form.html\">\n      <input type=\"hidden\" name=\"greeting\" value=\"Hello from a form\">\n      <button>submit #form</button>\n    </form>\n\n    <form id=\"form-without-submitter\" action=\"/__turbo/redirect\" method=\"post\">\n      <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/form.html\">\n      <input type=\"text\" name=\"greeting\" value=\"Hello from a form\">\n    </form>\n\n    <form id=\"turbo-enabled-form\" action=\"/__turbo/redirect\" method=\"post\" data-turbo=\"true\">\n      <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/form.html\">\n      <input type=\"hidden\" name=\"greeting\" value=\"Hello from a form[data-turbo=true]\">\n      <button>submit #turbo-enabled-form</button>\n    </form>\n\n    <form id=\"turbo-enabled-form-without-submitter\" action=\"/__turbo/redirect\" method=\"post\" data-turbo=\"true\">\n      <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/form.html\">\n      <input type=\"text\" name=\"greeting\" value=\"Hello from a form[data-turbo=true]\">\n    </form>\n\n    <button form=\"form\">Submit #form</button>\n    <button form=\"turbo-enabled-form\">Submit #turbo-enabled-form</button>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frame_navigation.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <link rel=\"icon\" href=\"data:image/x-icon;base64,AA\">\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <div id=\"container\">\n      <h1>Frame navigation tests</h1>\n      <a id=\"outside\" href=\"/src/tests/fixtures/frame_navigation.html\" data-turbo-frame=\"frame\">Outside Frame</a>\n\n      <custom-link-element id=\"outside-in-shadow-dom\" link=\"/src/tests/fixtures/frame_navigation.html\" data-turbo-frame=\"frame\">\n        Outside Frame in Shadow DOM\n      </custom-link-element>\n\n      <turbo-frame id=\"frame\">\n        <h2>Frame Navigation</h2>\n\n        <a id=\"inside\" href=\"/src/tests/fixtures/frame_navigation.html\">Inside Frame</a>\n        <a id=\"self\" href=\"/src/tests/fixtures/frame_navigation.html\" data-turbo-frame=\"_self\">Self Frame</a>\n        <a id=\"top\" href=\"/src/tests/fixtures/frame_navigation.html\" data-turbo-frame=\"_top\">Top</a>\n      </turbo-frame>\n\n      <turbo-frame id=\"empty-head\">\n        <p>The following link has a <code>data-turbo-action=\"advance\"</code> attribute and exists within a Turbo Frame. Tapping\n          it should replace the frame's contents, keeping the Index heading.</p>\n        <a id=\"link-to-frame-with-empty-head\" href=\"/src/tests/fixtures/frames/empty_head.html\" data-turbo-action=\"advance\">About (a link with data-turbo-action=\"advance\")</a>\n      </turbo-frame>\n\n      <div style=\"height: calc(100vh*2);\"></div>\n\n      <turbo-frame id=\"eager-loaded-frame\" src=\"/src/tests/fixtures/frames/frame_for_eager.html\" loading=\"lazy\"\n                   data-turbo-action=\"advance\">\n        <h2>Eager-loaded frame: Not Loaded</h2>\n      </turbo-frame>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frame_preloading.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frame Preloading</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <turbo-frame id=\"menu\" src=\"/src/tests/fixtures/frames/preloading.html\"></turbo-frame>\n\n    <turbo-frame id=\"hello\">\n      <a href=\"/src/tests/fixtures/frames/hello.html\" data-turbo-preload>Navigate #hello</a>\n    </turbo-frame>\n\n    <a href=\"/src/tests/fixtures/frames/hello.html\" data-turbo-frame=\"hello\" data-turbo-preload>Navigate #hello from the outside</a>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frame_refresh_after_navigation.html",
    "content": "<turbo-frame id=\"refresh-after-navigation\">\n  <h2 id=\"refresh-after-navigation-content\">Frame has been navigated</h2>\n</turbo-frame>\n"
  },
  {
    "path": "src/tests/fixtures/frame_refresh_morph.html",
    "content": "<turbo-frame id=\"refresh-morph\">\n  <h2>Loaded morphed frame</h2>\n</turbo-frame>\n"
  },
  {
    "path": "src/tests/fixtures/frame_refresh_reload.html",
    "content": "<turbo-frame id=\"refresh-reload\">\n  <h2>Loaded reloadable frame</h2>\n</turbo-frame>\n"
  },
  {
    "path": "src/tests/fixtures/frames/body_script.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Body Script</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <turbo-frame id=\"body-script\" data-loaded-from=\"/src/tests/fixtures/frames/body_script.html\">\n      <script>\n        if (\"frameScriptEvaluationCount\" in window) {\n          window.frameScriptEvaluationCount++\n        } else {\n          window.frameScriptEvaluationCount = 1\n        }\n      </script>\n      <a id=\"body-script-link\" href=\"/src/tests/fixtures/frames/body_script_2.html\">Load #body-script</a>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frames/body_script_2.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Body Script 2</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <turbo-frame id=\"body-script\" data-loaded-from=\"/src/tests/fixtures/frames/body_script_2.html\">\n      <script>\n        if (\"frameScriptEvaluationCount\" in window) {\n          window.frameScriptEvaluationCount++\n        } else {\n          window.frameScriptEvaluationCount = 1\n        }\n      </script>\n      <a id=\"body-script-link\" href=\"/src/tests/fixtures/frames/body_script.html\">Load #body-script</a>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frames/empty_head.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n</head>\n<body>\n  <turbo-frame id=\"empty-head\">\n    <h2>Frame updated</h2>\n  </turbo-frame>\n</body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frames/eval_false_script.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Eval False Script</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <turbo-frame id=\"eval-false-script\" data-loaded-from=\"/src/tests/fixtures/frames/eval_false_script.html\">\n      <script data-turbo-eval=\"false\">\n        if (\"frameScriptEvaluationCount\" in window) {\n          window.frameScriptEvaluationCount++\n        } else {\n          window.frameScriptEvaluationCount = 1\n        }\n      </script>\n      <a id=\"eval-false-script-link\" href=\"/src/tests/fixtures/frames/eval_false_script.html\">data-turbo-eval=false script</a></p>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frames/form-redirect.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Form Redirect</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>Page One Form</h1>\n    <turbo-frame id=\"form-redirect\">\n      <h2 id=\"form-redirect-header\">Form Redirect</h2>\n\n      <form action=\"/__turbo/redirect\" method=\"post\" enctype=\"multipart/form-data\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/frames/form-redirected.html\">\n        <input type=\"submit\" id=\"submit-form\">\n      </form>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frames/form-redirected.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Form Redirected</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>Form Redirected</h1>\n    <turbo-frame id=\"form-redirect\">\n      <h2 id=\"form-redirected-header\">Form Redirected</h2>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frames/form.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Form</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>Frames: Form</h1>\n\n    <turbo-frame id=\"frame\">\n      <form action=\"/__turbo/messages\" method=\"post\" class=\"stream\">\n        <input type=\"hidden\" name=\"content\" value=\"Hello!\">\n        <input id=\"frames-form-first-autofocus-element\" autofocus type=\"submit\">\n      </form>\n      <div id=\"messages\">\n        <div class=\"message\">Frame redirected</div>\n      </div>\n\n      <button type=\"button\" id=\"frames-form-second-autofocus-element\" autofocus>Second autofocus</button>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frames/frame.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Frame</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>Frames: #frame</h1>\n\n    <turbo-frame id=\"frame\" data-loaded-from=\"/src/tests/fixtures/frames/frame.html\">\n      <h2>Frame: Loaded</h2>\n    </turbo-frame>\n\n    <turbo-frame id=\"nested-child\">\n      <h2>Frame: Loaded</h2>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frames/frame_for_eager.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Frame for Eager</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <turbo-frame id=\"eager-loaded-frame\" >\n      <h2>Eager-loaded frame: Loaded</h2>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frames/hello.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Hello</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>Hello</h1>\n    <turbo-frame id=\"hello\">\n      <h2>Hello from a frame</h2>\n\n      <a href=\"/src/tests/fixtures/frames.html\">Navigate #hello</a>\n\n      <form>\n        <input id=\"permanent-input-in-frame\" type=\"text\" name=\"query\" placeholder=\"Permanent input in frame\" data-turbo-permanent>\n      </form>\n\n      <form id=\"permanent-form-in-frame\" data-turbo-permanent>\n        <input id=\"permanent-descendant-input-in-frame\" type=\"text\" name=\"query\" placeholder=\"Permanent descendant input in frame\">\n      </form>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frames/parent.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Nested Root: Parent</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>Nested Root: Parent</h1>\n\n    <turbo-frame id=\"nested-root\">\n      <h2>Parent: Loaded</h2>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frames/part.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Part</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <turbo-frame id=\"part\">\n      <h2>Frames: #frame-part</h2>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frames/preloading.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Preloading</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <turbo-frame id=\"menu\">\n      <a href=\"/src/tests/fixtures/preloaded.html\" id=\"frame_preload_anchor\" data-turbo-preload data-turbo-frame=\"_top\">Visit preloaded page</a>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frames/recursive.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Recursive</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <turbo-frame id=\"recursive\">\n      <turbo-frame id=\"composer\">\n        <details>\n          <summary>Recursive frame</summary>\n          <form action=\"/__turbo/redirect\" method=\"post\" class=\"redirect\">\n            <a href=\"/src/tests/fixtures/frames.html\">Link</a>\n            <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/frames.html\">\n            <input type=\"submit\">\n          </form>\n        </details>\n      </turbo-frame>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frames/self.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Self</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <turbo-frame id=\"frame\" src=\"/src/tests/fixtures/frames/self.html\">\n      <h2>Frames: Self</h2>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frames/unvisitable.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frame</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <meta name=\"turbo-visit-control\" content=\"reload\" />\n  </head>\n  <body>\n    <h1>Unvisitable page loaded</h1>\n\n    <turbo-frame id=\"missing\">\n      <h1>Frame content</h1>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/frames.html",
    "content": "<!DOCTYPE html>\n<html id=\"html\" data-skip-event-details=\"turbo:click turbo:before-render\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frame</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <script>\n      addEventListener(\"click\", ({ target }) => {\n        if (target.id == \"add-turbo-action-to-frame\") {\n          target.closest(\"turbo-frame\")?.setAttribute(\"data-turbo-action\", \"advance\")\n        } else if (target.id == \"remove-target-from-hello\") {\n          document.getElementById(\"hello\").removeAttribute(\"target\")\n        } else if (target.id == \"add-refresh-reload-to-frame\") {\n          target.closest(\"turbo-frame\")?.setAttribute(\"refresh\", \"reload\")\n        } else if (target.id == \"add-refresh-morph-to-frame\") {\n          target.closest(\"turbo-frame\")?.setAttribute(\"refresh\", \"morph\")\n        } else if (target.id == \"add-src-to-frame\") {\n          target.closest(\"turbo-frame\")?.setAttribute(\"src\", \"/src/tests/fixtures/frames.html\")\n        } else if (target.id == \"change-frame-id-to-different-nested-child\") {\n          target.closest(\"turbo-frame\")?.setAttribute(\"id\", \"different-nested-child\")\n        }\n      })\n    </script>\n    <style>\n      .push-off-screen { margin-top: 1000px; }\n    </style>\n  </head>\n  <body>\n    <h1>Frames</h1>\n\n    <turbo-frame id=\"frame\" data-loaded-from=\"/src/tests/fixtures/frames.html\">\n      <h2>Frames: #frame</h2>\n\n      <form action=\"/src/tests/fixtures/frames/frame.html\">\n        <button id=\"frame-form-get-no-redirect\">Navigate #frame without a redirect</button>\n      </form>\n      <button id=\"add-turbo-action-to-frame\" type=\"button\">Add [data-turbo-action=\"advance\"] to #frame</button>\n      <button id=\"add-refresh-reload-to-frame\" type=\"button\">Add [refresh=\"reload\"] to #frame</button>\n      <button id=\"add-refresh-morph-to-frame\" type=\"button\">Add [refresh=\"morph\"] to #frame</button>\n      <button id=\"add-src-to-frame\" type=\"button\">Add [src=\"/src/tests/fixtures/frames.html\"] to #frame so it can be reloaded</button>\n      <a id=\"link-frame\" href=\"/src/tests/fixtures/frames/frame.html\">Navigate #frame from within</a>\n      <a id=\"link-frame-with-search-params\" href=\"/src/tests/fixtures/frames/frame.html?key=value\">Navigate #frame with ?key=value</a>\n      <a id=\"link-nested-frame-action-advance\" href=\"/src/tests/fixtures/frames/frame.html\" data-turbo-action=\"advance\">Navigate #frame from within with a[data-turbo-action=\"advance\"]</a>\n      <a id=\"link-top\" href=\"/src/tests/fixtures/one.html\" data-turbo-frame=\"_top\">Visit one.html</a>\n      <input id=\"permanent-input\" data-turbo-permanent>\n      <form action=\"/src/tests/fixtures/one.html\" data-turbo-frame=\"_top\">\n        <button id=\"form-submit-top\">Visit one.html</button>\n      </form>\n    </turbo-frame>\n    <a id=\"outside-frame-form\" href=\"/src/tests/fixtures/frames/form.html\" data-turbo-frame=\"frame\">Navigate #frame to /frames/form.html</a>\n    <a id=\"outside-frame-link-with-frame-child\" href=\"/src/tests/fixtures/frames/form.html\" data-turbo-frame=\"frame\">\n      <turbo-frame id=\"ignored-frame\">Has a turbo-frame child</turbo-frame>\n    </a>\n\n    <a id=\"link-outside-frame-action-advance\" href=\"/src/tests/fixtures/frames/frame.html\" data-turbo-frame=\"frame\" data-turbo-action=\"advance\">Navigate #frame from outside with a[data-turbo-action=\"advance\"]</a>\n    <form id=\"form-get-frame-action-advance\" action=\"/__turbo/redirect\" data-turbo-frame=\"frame\" data-turbo-action=\"advance\">\n      <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/frames/frame.html\">\n      <button>Navigate #frame with GET form[data-turbo-action=\"advance\"]</button>\n    </form>\n\n    <form id=\"form-post-frame-action-advance\" method=\"post\" action=\"/__turbo/redirect\" data-turbo-frame=\"frame\" data-turbo-action=\"advance\">\n      <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/frames/frame.html\">\n      <button>Navigate #frame with POST form[data-turbo-action=\"advance\"]</button>\n    </form>\n\n    <form method=\"post\" action=\"/__turbo/redirect\" data-turbo-frame=\"frame\" data-turbo-action=\"advance\">\n      <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/frames/frame.html\">\n      <button id=\"button-frame-action-advance\" data-turbo-action=\"advance\">Navigate #frame with button[data-turbo-action=\"advance\"]</button>\n    </form>\n\n    <turbo-frame id=\"hello\" target=\"frame\">\n      <h2>Frames: #hello</h2>\n\n      <a href=\"/src/tests/fixtures/frames/frame.html\">Load #frame</a>\n      <button type=\"button\" id=\"remove-target-from-hello\">Remove #hello[target]</button>\n\n    </turbo-frame>\n\n    <a id=\"link-hello-advance\" href=\"/src/tests/fixtures/frames/hello.html\" data-turbo-frame=\"hello\" data-turbo-action=\"advance\">advance #hello</a>\n\n    <turbo-frame id=\"nested-root\" target=\"frame\">\n      <h2>Frames: #nested-root</h2>\n      <button id=\"add-refresh-morph-to-frame\" type=\"button\">Add [refresh=\"morph\"] to #frame</button>\n      <button id=\"add-src-to-frame\" type=\"button\">Add [src=\"/src/tests/fixtures/frames.html\"] to #frame so it can be reloaded</button>\n      <a id=\"inner-outer-frame-link\" href=\"/src/tests/fixtures/frames/frame.html\" data-turbo-frame=\"nested-child\">Inner/Outer frame link</a>\n      <form data-turbo-frame=\"nested-child\" method=\"get\" action=\"/src/tests/fixtures/frames/frame.html\">\n        <input id=\"inner-outer-frame-submit\" type=\"submit\" value=\"Inner/Outer form submit\">\n      </form>\n\n      <turbo-frame id=\"nested-child\">\n        <h2>Frames: #nested-child</h2>\n        <button id=\"add-refresh-morph-to-frame\" type=\"button\">Add [refresh=\"morph\"] to #frame</button>\n        <button id=\"add-src-to-frame\" type=\"button\">Add [src=\"/src/tests/fixtures/frames.html\"] to #frame so it can be reloaded</button>\n        <button id=\"change-frame-id-to-different-nested-child\" type=\"button\">Change id to different-nested-child</button>\n        <a href=\"/src/tests/fixtures/frames/frame.html\">Load #nested-child</a>\n        <a href=\"/src/tests/fixtures/one.html\" data-turbo-frame=\"_top\">Visit one.html</a>\n        <a id=\"link-parent\" href=\"/src/tests/fixtures/frames/parent.html\" data-turbo-frame=\"_parent\">Navigate parent</a>\n        <form action=\"/src/tests/fixtures/frames/parent.html\" data-turbo-frame=\"_parent\">\n          <button id=\"form-submit-parent\">Submit to parent</button>\n        </form>\n        <form method=\"get\" action=\"/src/tests/fixtures/one.html\" data-turbo-frame=\"_top\">\n          <input id=\"nested-child-navigate-form-top-submit\" type=\"submit\" value=\"Nested Child form submit\">\n        </form>\n\n        <turbo-frame id=\"nested-grandchild\">\n          <a id=\"grandchild-link-parent\" href=\"/src/tests/fixtures/frames/frame.html\" data-turbo-frame=\"_parent\">Navigate immediate parent only</a>\n        </turbo-frame>\n      </turbo-frame>\n\n      <turbo-frame id=\"nested-child-navigate-top\" target=\"_top\">\n        <button id=\"add-src-to-frame\" type=\"button\">Add [src=\"/src/tests/fixtures/frames.html\"] to #frame so it can be reloaded</button>\n        <form method=\"get\" action=\"/src/tests/fixtures/one.html\">\n          <input id=\"nested-child-navigate-top-submit\" type=\"submit\" value=\"Nested Child form submit\">\n        </form>\n      </turbo-frame>\n    </turbo-frame>\n\n    <turbo-frame id=\"navigate-top\" target=\"_top\">\n      <a href=\"/src/tests/fixtures/one.html?key=value\">Visit one.html?key=value</a>\n      <a href=\"/src/tests/fixtures/one.html\" data-turbo-frame=\"_self\">Visit self</a>\n    </turbo-frame>\n    <a id=\"outside-navigate-top-link\" href=\"/src/tests/fixtures/one.html\">Visit one.html from outside #navigate-top</a>\n\n    <turbo-frame id=\"top-level-parent\">\n      <a href=\"/src/tests/fixtures/one.html?key=value\" data-turbo-frame=\"_parent\">Visit one.html?key=value with _parent</a>\n    </turbo-frame>\n\n    <turbo-frame id=\"missing\">\n      <a id=\"missing-frame-link\" href=\"/src/tests/fixtures/frames/frame.html\">Missing frame</a>\n      <a id=\"missing-page-link\" href=\"/missing.html\">Missing page</a>\n      <a id=\"unvisitable-page-link\" href=\"/src/tests/fixtures/frames/unvisitable.html\">Unvisitable page</a>\n    </turbo-frame>\n\n    <turbo-frame id=\"body-script\" target=\"body-script\">\n      <a id=\"body-script-link\" href=\"/src/tests/fixtures/frames/body_script.html\">Load body-script</a>\n    </turbo-frame>\n\n    <turbo-frame id=\"eval-false-script\" target=\"eval-false-script\">\n      <a id=\"eval-false-script-link\" href=\"/src/tests/fixtures/frames/eval_false_script.html\">data-turbo-eval=false script</a></p>\n    </turbo-frame>\n\n    <turbo-frame id=\"recursive\" recurse=\"composer\" src=\"/src/tests/fixtures/frames/recursive.html\">\n    </turbo-frame>\n\n    <a id=\"frame-self\" href=\"/src/tests/fixtures/frames/self.html\" data-turbo-frame=\"frame\">Visit self.html</a>\n\n    <a id=\"navigate-form-redirect\" href=\"/src/tests/fixtures/frames/form-redirect.html\" data-turbo-frame=\"form-redirect\">Visit form-redirect.html</a>\n    <turbo-frame id=\"form-redirect\"></turbo-frame>\n\n    <a id=\"navigate-form-redirect-as-new\" href=\"/src/tests/fixtures/frames/form-redirect.html\">Visit form-redirect.html as new page</a>\n\n    <turbo-frame id=\"part\">\n      <a id=\"frame-part\" href=\"/src/tests/fixtures/frames/part.html\">Load #part</a>\n    </turbo-frame>\n\n    <a id=\"outer-frame-link\" href=\"/src/tests/fixtures/frames/frame.html\" data-turbo-frame=\"frame\">Outer frame link</a>\n    <form data-turbo-frame=\"frame\" method=\"get\" action=\"/src/tests/fixtures/frames/frame.html\">\n      <input id=\"outer-frame-submit\" type=\"submit\" value=\"Outer form submit\">\n    </form>\n\n    <hr class=\"push-off-screen\">\n    <input id=\"below-the-fold-input\">\n    <a id=\"below-the-fold-link-frame-action\" data-turbo-action=\"advance\" data-turbo-frame=\"frame\" href=\"/src/tests/fixtures/frames/frame.html\">Navigate #frame</a>\n\n    <a id=\"link-to-eager-loaded-frame\" href=\"/__turbo/redirect?path=/src/tests/fixtures/page_with_eager_frame.html\">Eager-loaded frame after GET redirect</a>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/greetings.ejs",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Greeting</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n  </head>\n  <body>\n    <h1><%= greeting %></h1>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/head_script.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Head script</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <script>\n      if (\"headScriptEvaluationCount\" in window) {\n        window.headScriptEvaluationCount++\n      } else {\n        window.headScriptEvaluationCount = 1\n      }\n    </script>\n  </head>\n  <body>\n    <h1>Head script</h1>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/headers.html",
    "content": "<html>\n\n<head>\n  <title>Headers</title>\n  <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n</head>\n\n<body>\n  <h1>Request Headers</h1>\n\n  <pre>$HEADERS</pre>\n</body>\n\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/hot_preloading.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"utf-8\">\n  <title>Page That Links to Preloading Page</title>\n  <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n  <script src=\"/src/tests/fixtures/test.js\"></script>\n</head>\n\n<body>\n  <a href=\"/src/tests/fixtures/preloading.html\" id=\"hot_preload_anchor\">Next page has preloading</a>\n</body>\n\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/hover_to_prefetch.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Hover to Prefetch</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <meta name=\"turbo-prefetch\" content=\"true\" />\n  </head>\n\n  <body>\n    <a href=\"/src/tests/fixtures/prefetched.html\" id=\"anchor_for_prefetch\">Hover to prefetch me</a>\n    <a href=\"/src/tests/fixtures/bare.html\" id=\"anchor_for_prefetch_other_href\">Hover to prefetch me</a>\n    <a href=\"/src/tests/fixtures/prefetched.html\" id=\"anchor_with_inner_elements\">\n      <span>Hover to prefetch me</span>\n    </a>\n    <a href=\"/src/tests/fixtures/prefetched.html\" id=\"anchor_with_turbo_stream\" data-turbo-stream>Hover to prefetch me</a>\n    <div data-turbo=\"false\">\n      <a href=\"/src/tests/fixtures/prefetched.html\" id=\"anchor_with_turbo_false_parent\">Won't prefetch when hovering me</a>\n    </div>\n    <div data-turbo-prefetch=\"false\">\n      <a href=\"/src/tests/fixtures/prefetched.html\" id=\"anchor_with_turbo_prefetch_false_parent\">Won't prefetch when hovering me</a>\n      <div data-turbo-prefetch=\"true\">\n        <a href=\"/src/tests/fixtures/prefetched.html\" id=\"anchor_with_turbo_prefetch_true_parent_within_turbo_prefetch_false_parent\">Hover to prefetch me</a>\n      </div>\n    </div>\n    <a href=\"/src/tests/fixtures/prefetched.html\" id=\"anchor_with_turbo_prefetch_false\" data-turbo-prefetch=\"false\"\n      >Won't prefetch when hovering me</a>\n    <a href=\"/src/tests/fixtures/prefetched.html\" id=\"anchor_with_turbo_false\" data-turbo=\"false\"\n      >Won't prefetch when hovering me</a>\n    <a href=\"/src/tests/fixtures/prefetched.html\" id=\"anchor_with_remote_true\" data-remote=\"true\"\n      >Won't prefetch when hovering me</a>\n    <a href=\"/src/tests/fixtures/prefetched.html\" id=\"anchor_with_turbo_stream\" data-turbo-stream=\"true\"\n      >Won't prefetch when hovering me</a>\n    <a href=\"/src/tests/fixtures/prefetched.html\" id=\"anchor_with_turbo_confirm\" data-turbo-confirm=\"Are you sure?\"\n      >Won't prefetch when hovering me</a>\n    <a href=\"/src/tests/fixtures/hover_to_prefetch.html\" id=\"anchor_for_same_location\"\n      >Won't prefetch when hovering me</a>\n    <a href=\"/src/tests/fixtures/prefetched.html?foo=bar\" id=\"anchor_for_same_location_with_query\"\n      >Hover to prefetch me</a>\n    <a href=\"https://example.com\" id=\"anchor_for_different_origin\">Won't prefetch when hovering me</a>\n    <a href=\"http://localhost:9000/src/tests/fixtures/prefetched.html\" id=\"anchor_with_whole_url\"\n      >Hover to prefetch me</a>\n    <a href=\"#some_anchor\" id=\"anchor_with_hash\">Won't prefetch when hovering me</a>\n    <a href=\"ftp://example.com\" id=\"anchor_with_ftp_protocol\">Won't prefetch when hovering me</a>\n    <a href=\"/src/tests/fixtures/prefetched.html\" target=\"prefetch_iframe\" id=\"anchor_with_iframe_target\"\n      >Won't prefetch when hovering me</a>\n    <a href=\"/src/tests/fixtures/prefetched.html\" data-turbo-method=\"post\" id=\"anchor_with_post_method\"\n      >Won't prefetch when hovering me</a>\n    <iframe src=\"/src/tests/fixtures/hover_to_prefetch_iframe.html\" name=\"prefetch_iframe\"> </iframe>\n\n    <turbo-frame id=\"frame_for_prefetch\">\n      <a href=\"/src/tests/fixtures/prefetched.html\" id=\"anchor_for_prefetch_in_frame\">Hover to prefetch me</a>\n    </turbo-frame>\n\n    <turbo-frame id=\"frame_for_prefetch_top\" target=\"_top\">\n      <a href=\"/src/tests/fixtures/prefetched.html\" id=\"anchor_for_prefetch_in_frame_target_top\">Hover to prefetch me</a>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/hover_to_prefetch_custom_cache_time.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Hover to Prefetch</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <meta name=\"turbo-prefetch\" content=\"true\" />\n    <meta name=\"turbo-prefetch-cache-time\" content=\"1\" />\n  </head>\n\n  <body>\n    <a href=\"/src/tests/fixtures/prefetched.html\" id=\"anchor_for_prefetch\">Hover to prefetch me</a>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/hover_to_prefetch_disabled.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Hover to Prefetch</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <meta name=\"turbo-prefetch\" content=\"false\" />\n  </head>\n\n  <body>\n    <a href=\"/src/tests/fixtures/prefetched.html\" id=\"anchor_for_prefetch\">Hover to prefetch me</a>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/hover_to_prefetch_iframe.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Hover to Prefetch</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <meta name=\"turbo-prefetch\" content=\"true\" />\n  </head>\n\n  <body>\n    <a href=\"/src/tests/fixtures/prefetched.html\" id=\"anchor_inside_iframe\">Hover to prefetch me</a>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/hover_to_prefetch_without_meta_tag_with_link_to_with_meta_tag.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Hover to Prefetch Not Enabled</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n  </head>\n\n  <body>\n    <!-- Margin top to avoid immediately hovering over a link on next page -->\n    <a\n      href=\"/src/tests/fixtures/hover_to_prefetch.html\"\n      id=\"anchor_for_page_with_meta_tag\"\n      style=\"display: block; margin-top: 30rem\"\n    >\n      Click to go to page with prefetch meta tag\n    </a>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/link_redirect.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <script src=\"/dist/turbo.es2017-umd.js\"></script>\n    <meta data-turbo-track=\"reload\" content=\"A\">\n  </head>\n  <body>\n    <a id=\"indirect\" href=\"/__turbo/redirect?path=/src/tests/fixtures/link_redirect_target.html\">Indirect link</a>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/link_redirect_target.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <script src=\"/dist/turbo.es2017-umd.js\"></script>\n    <meta data-turbo-track=\"reload\" content=\"B\">\n  </head>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/loading.html",
    "content": "<!DOCTYPE html>\n<html data-skip-event-details=\"turbo:before-render\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <details id=\"loading-lazy\">\n      <summary>Lazy-loaded</summary>\n\n      <turbo-frame id=\"hello\" src=\"/src/tests/fixtures/frames/hello.html\" loading=\"lazy\"></turbo-frame>\n    </details>\n\n    <a id=\"link-lazy-frame\" href=\"/src/tests/fixtures/frames.html\" data-turbo-frame=\"hello\">Navigate #loading-lazy turbo-frame</a>\n\n    <details id=\"loading-eager\">\n      <summary>Eager-loaded</summary>\n\n      <turbo-frame id=\"frame\" src=\"/src/tests/fixtures/frames/frame.html\" loading=\"eager\"></turbo-frame>\n    </details>\n\n    <p><a id=\"one\" href=\"one.html\">One</a></p>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/navigation.html",
    "content": "<!DOCTYPE html>\n<html id=\"html\" data-skip-event-details=\"turbo:submit-start turbo:submit-end\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"csp-nonce\" content=\"123\">\n    <title>Turbo</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <meta name=\"referrer\" content=\"origin-when-cross-origin\">\n  </head>\n  <body>\n    <a href=\"#main\">Skip Link</a>\n\n    <a href=\"#main\" id=\"refresh-link\" data-turbo-action=\"replace\">Refresh Link</a>\n\n    <a href=\"#ignored-link\" id=\"ignored-link\">Skipped Content</a>\n\n    <section id=\"main\">\n      <h1>Navigation</h1>\n      <p><a id=\"same-origin-unannotated-link\" href=\"/src/tests/fixtures/one.html\">Same-origin unannotated link</a></p>\n      <p><a id=\"same-origin-unannotated-link-search-params\" href=\"/src/tests/fixtures/one.html?key=value\">Same-origin unannotated link ?key=value</a></p>\n      <p><form id=\"same-origin-unannotated-form\" method=\"get\" action=\"/src/tests/fixtures/one.html\"><button>Same-origin unannotated form</button></form></p>\n      <p><a id=\"same-origin-replace-link\" href=\"/src/tests/fixtures/one.html\" data-turbo-action=\"replace\">Same-origin data-turbo-action=replace link</a></p>\n      <p><form id=\"same-origin-replace-form-get\" action=\"/src/tests/fixtures/one.html\" data-turbo-action=\"replace\"><button>Same-origin data-turbo-action=replace form</button></form></p>\n      <p><form id=\"same-origin-replace-form-submitter-get\" action=\"/src/tests/fixtures/one.html\"><button data-turbo-action=\"replace\">Same-origin data-turbo-action=replace form</button></form></p>\n      <form id=\"same-origin-replace-form-post\" method=\"post\" action=\"/__turbo/redirect\" data-turbo-action=\"replace\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/one.html\">\n        <button>Same-origin form[method=\"post\"][data-turbo-action=\"replace\"]</button>\n      </form>\n      <form id=\"same-origin-replace-form-submitter-post\" method=\"post\" action=\"/__turbo/redirect\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/one.html\">\n        <button data-turbo-action=\"replace\">Same-origin form[method=\"post\"] button[data-turbo-action=\"replace\"]</button>\n      </form>\n      <form id=\"form-post\" method=\"post\" action=\"/__turbo/redirect\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/one.html\">\n        <input type=\"submit\" id=\"form-post-submit\" value=\"Submit\"/>\n      </form>\n      <p><a id=\"same-origin-false-link\" href=\"/src/tests/fixtures/one.html\" data-turbo=\"false\">Same-origin data-turbo=false link</a></p>\n      <p data-turbo=\"false\"><a id=\"same-origin-unannotated-link-inside-false-container\" href=\"/src/tests/fixtures/one.html\">Same-origin unannotated link inside data-turbo=false container</a></p>\n      <p data-turbo=\"false\"><a id=\"same-origin-true-link-inside-false-container\" href=\"/src/tests/fixtures/one.html\" data-turbo=\"true\">Same-origin data-turbo=true link inside data-turbo=false container</a></p>\n      <p><a id=\"same-origin-anchored-link\" href=\"/src/tests/fixtures/one.html#element-id\">Same-origin anchored link</a></p>\n      <p><a id=\"same-origin-anchored-link-named\" href=\"/src/tests/fixtures/one.html#named-anchor\">Same-origin link to named anchor</a></p>\n      <p><a id=\"cross-origin-unannotated-link\" href=\"about:blank\">Cross-origin unannotated link</a></p>\n      <p><a id=\"same-origin-targeted-link\" href=\"/src/tests/fixtures/one.html\" target=\"_blank\">Same-origin targeted link</a></p>\n      <p><a id=\"self-targeted-link\" href=\"/src/tests/fixtures/one.html\" target=\"_self\">Explicit browser default target link</a></p>\n      <p><a id=\"empty-target-link\" href=\"/src/tests/fixtures/one.html\" target=\"\">Empty target attribute link</a></p>\n      <p><a id=\"bare-target-link\" href=\"/src/tests/fixtures/one.html\" target>Bare target attribute link</a></p>\n      <p><a id=\"same-origin-download-link\" href=\"/intentionally_missing_fake_download.html\" download=\"x.html\">Same-origin download link</a></p>\n      <svg width=\"600\" height=\"100\" viewbox=\"-300 -50 600 100\"><text><a id=\"same-origin-link-inside-svg-element\" href=\"/src/tests/fixtures/one.html\">Same-origin link inside SVG element</a></text></svg>\n      <svg width=\"600\" height=\"100\" viewbox=\"-300 -50 600 100\"><text><a id=\"cross-origin-link-inside-svg-element\" href=\"about:blank\">Cross-origin link inside SVG element</a></text></svg>\n      <p><a id=\"same-origin-get-link-form\" href=\"/src/tests/fixtures/navigation.html?a=one&b=two\" data-turbo-method=\"get\">Same-origin data-turbo-method=get link</a></p>\n      <p><a id=\"same-origin-replace-post-link\" href=\"/__turbo/redirect\" data-turbo-method=\"post\" data-turbo-action=\"replace\">Same-origin data-turbo-action=replace link with post method</a></p>\n      <p><a id=\"link-to-disabled-frame\" href=\"/src/tests/fixtures/frames/hello.html\" data-turbo-frame=\"hello\">Disabled turbo-frame</a></p>\n      <p><a id=\"autofocus-link\" href=\"/src/tests/fixtures/autofocus.html\">autofocus.html link</a></p>\n      <p><a id=\"redirection-link\" href=\"/__turbo/redirect?path=/src/tests/fixtures/one.html\">Redirection link</a></p>\n      <p><a id=\"headers-link\" href=\"/__turbo/headers\">Headers link</a></p>\n      <p><custom-link-element id=\"custom-link-element\" link=\"/src/tests/fixtures/one.html\" text=\"Same-origin unannotated custom element link\"></custom-link-element></p>\n      <p>\n        <a href=\"/src/tests/fixtures/one.html\">\n          <custom-button id=\"shadow-dom-drive-enabled\"></custom-button>\n        </a>\n      </p>\n      <p data-turbo=\"false\">\n        <a href=\"/src/tests/fixtures/one.html\">\n          <custom-button id=\"shadow-dom-drive-disabled\"></custom-button>\n        </a>\n      </p>\n      <p>\n        <custom-link-element link=\"/src/tests/fixtures/one.html\">\n          <span id=\"element-in-slot\">Link in slot</span>\n        </custom-link-element>\n      </p>\n      <p data-turbo=\"false\">\n        <custom-link-element link=\"/src/tests/fixtures/one.html\">\n          <span id=\"element-in-slot-disabled\">Link in slot (disabled)</span>\n        </custom-link-element>\n      </p>\n      <p>\n        <turbo-toggle turbo=\"false\">\n          <custom-link-element link=\"/src/tests/fixtures/one.html\">\n            <span id=\"element-in-nested-slot-disabled\">Link in slot (disabled)</span>\n          </custom-link-element>\n        </turbo-toggle>\n      </p>\n      <p>\n        <form action=\"/__turbo/redirect\" method=\"post\" class=\"redirect\" data-turbo-frame=\"_top\">\n          <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/one.html\">\n          <button id=\"redirect-submit\">Redirect to the another location</button>\n        </form>\n      </p>\n      <p>\n        <form action=\"/__turbo/refresh\" method=\"post\" class=\"redirect\">\n          <button id=\"refresh-submit\">Redirect to the current location</button>\n        </form>\n      </p>\n      <p><a id=\"delayed-link\" href=\"/__turbo/delayed_response\">Delayed link</a></p>\n      <p><a id=\"delayed-failure-link\" href=\"/__turbo/delayed_response?status=500\">Delayed failure link</a></p>\n      <p><a id=\"link-target-iframe\" href=\"/src/tests/fixtures/one.html\" target=\"iframe\">Targets iframe[name=\"iframe\"]</a></p>\n      <p><a id=\"link-target-empty-name-iframe\" href=\"/src/tests/fixtures/one.html\" target=\"\">Targets iframe[name=\"\"]</a></p>\n      <p>\n        <form id=\"form-target-blank\" action=\"/src/tests/fixtures/one.html\" target=\"_blank\">\n          <button>form[target=\"_blank\"]</button>\n        </form>\n      </p>\n      <p>\n        <form id=\"form-target-iframe\" action=\"/src/tests/fixtures/one.html\" target=\"iframe\">\n          <button>form[target=\"iframe\"]</button>\n        </form>\n      </p>\n      <p>\n        <form id=\"form-target-empty-name-iframe\" action=\"/src/tests/fixtures/one.html\" target=\"\">\n          <button>form[target=\"\"]</button>\n        </form>\n      </p>\n      <p>\n        <form action=\"/src/tests/fixtures/one.html\">\n          <button id=\"button-formtarget-blank\" formtarget=\"_blank\">button[formtarget=\"_blank\"]</button>\n          <button id=\"button-formtarget-iframe\" formtarget=\"iframe\">button[formtarget=\"iframe\"]</button>\n        </form>\n      </p>\n      <p>\n        <form action=\"/src/tests/fixtures/one.html\">\n          <button id=\"button-formtarget-empty-name-iframe\" formtarget=\"\">button[formtarget=\"\"]</button>\n        </form>\n      </p>\n      <p><a id=\"redirect-to-cache-observer\" href=\"/__turbo/redirect?path=/src/tests/fixtures/cache_observer.html\">Redirect to cache_observer.html</a></p>\n    </section>\n\n    <turbo-frame id=\"hello\" disabled></turbo-frame>\n\n    <iframe name=\"iframe\"></iframe>\n    <iframe name=\"\"></iframe>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/noscript.css",
    "content": ":root {\n  --black-if-noscript-evaluated: black;\n}\n"
  },
  {
    "path": "src/tests/fixtures/one.html",
    "content": "<!DOCTYPE html>\n<html id=\"one\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>One</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <meta name=\"test\" content=\"foo\">\n  </head>\n  <body>\n    <h1>One</h1>\n\n    <!--styles ensure that the element will be scrolled to top when navigated to via an anchored link -->\n    <a name=\"named-anchor\"></a>\n    <div id=\"element-id\" style=\"margin-top: 1em; height: 200vh\">An element with an ID</div>\n    <p><a id=\"redirection-link\" href=\"/__turbo/redirect?path=/src/tests/fixtures/visit.html\">Redirection link</a></p>\n    <p><a id=\"page-refresh-link\" data-turbo-action=\"replace\" href=\"/src/tests/fixtures/page_refresh.html\">Page refresh link</a></p>\n\n    <turbo-frame id=\"navigate-top\">\n      Replaced only the frame\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/page_refresh.html",
    "content": "<!DOCTYPE html>\n<html id=\"html\" data-skip-event-details=\"turbo:submit-start turbo:submit-end turbo:fetch-request-error\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"turbo-refresh-method\" content=\"morph\">\n    <meta name=\"turbo-refresh-scroll\" content=\"preserve\">\n\n    <title>Turbo</title>\n    <link rel=\"icon\" href=\"data:;base64,iVBORw0KGgo=\">\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <script type=\"module\">\n      import { Application, Controller } from \"https://unpkg.com/@hotwired/stimulus/dist/stimulus.js\"\n\n      const application = Application.start()\n\n      addEventListener(\"focusin\", ({ target }) => {\n        if (target instanceof HTMLInputElement && !target.hasAttribute(\"data-turbo-permanent\")) {\n          target.toggleAttribute(\"data-turbo-permanent\", true)\n\n          target.addEventListener(\"focusout\", () => target.toggleAttribute(\"data-turbo-permanent\", false), { once: true })\n        }\n      })\n\n      addEventListener(\"turbo:morph-element\", ({ target }) => {\n        for (const { element, context } of application.controllers) {\n          if (element === target) {\n            context.disconnect()\n            context.connect()\n          }\n        }\n      })\n\n      addEventListener(\"turbo:before-morph-attribute\", (event) => {\n        const { target, detail: { attributeName, mutationType } } = event\n\n        for (const { element, context } of application.controllers) {\n          const pattern = new RegExp(`data-${context.identifier}-\\\\w+-value`)\n\n          if (element === target) {\n            event.preventDefault()\n          }\n        }\n      })\n\n      application.register(\"test\", class extends Controller {\n        static targets = [\"output\"]\n        static values = { state: String }\n\n        capture({ target }) {\n          this.stateValue = target.value\n        }\n\n        outputTargetConnected(target) {\n          target.textContent = \"connected\"\n        }\n      })\n\n      document.getElementById(\"add-new-assets\").addEventListener(\"click\", () => {\n        const stylesheet = document.createElement(\"link\")\n        stylesheet.id = \"new-stylesheet\"\n        stylesheet.rel = \"stylesheet\"\n        stylesheet.href = \"/src/tests/fixtures/stylesheets/common.css\"\n        stylesheet.dataset.turboTrack = \"reload\"\n        document.head.appendChild(stylesheet)\n      })\n    </script>\n\n    <style>\n        body {\n            margin: 0;\n            padding: 0;\n\n            /* Ensure the page is large enough to scroll */\n            width: 150vw;\n            height: 150vh;\n        }\n    </style>\n  </head>\n  <body>\n    <h1 id=\"title\">Page to be refreshed</h1>\n\n    <a href=\"/src/tests/fixtures/page_refresh.html\" id=\"reload-link\">Reload</a>\n    <a href=\"/__turbo/delayed_response\" id=\"delayed_link\">Navigate with delayed response</a>\n\n    <turbo-frame id=\"refresh-morph\" src=\"/src/tests/fixtures/frame_refresh_morph.html\" refresh=\"morph\">\n      <h2>Frame to be morphed</h2>\n    </turbo-frame>\n\n    <turbo-frame id=\"refresh-reload\" src=\"/src/tests/fixtures/frame_refresh_reload.html\" refresh=\"reload\">\n      <h2>Frame to be reloaded</h2>\n    </turbo-frame>\n\n    <turbo-frame id=\"refresh-after-navigation\">\n      <h2>Frame to be navigated then reset to its initial state after reload</h2>\n      <a id=\"refresh-after-navigation-link\" href=\"/src/tests/fixtures/frame_refresh_after_navigation.html\">Navigate</a>\n    </turbo-frame>\n\n    <div id=\"preserve-me\" data-turbo-permanent>\n      Preserve me!\n\n      <turbo-frame id=\"remote-permanent-frame\" src=\"/src/tests/fixtures/remote_permanent_frame.html\">\n        <h2>Frame to be preserved</h2>\n      </turbo-frame>\n    </div>\n\n    <div id=\"stimulus-controller\" data-controller=\"test\" data-action=\"input->test#capture\">\n      <h3>Element with Stimulus controller</h3>\n\n      <div id=\"test-output\" data-test-target=\"output\">reset</div>\n      <input>\n    </div>\n\n    <form method=\"get\" data-turbo-action=\"replace\" oninput=\"this.requestSubmit()\">\n      <label>\n        Search\n        <input name=\"query\">\n      </label>\n      <button>Form with params to refresh the page</button>\n    </form>\n    <p><a id=\"replace-link\" data-turbo-action=\"replace\" href=\"/src/tests/fixtures/page_refresh.html?param=something\">Link with params to refresh the page</a></p>\n    <p><a id=\"refresh-link\" data-turbo-action=\"replace\" href=\"/src/tests/fixtures/page_refresh.html\">Link to the same page</a></p>\n    <p><a id=\"link\" href=\"/src/tests/fixtures/one.html\">Link to another page</a></p>\n\n    <form id=\"form\" action=\"/__turbo/refresh\" method=\"post\" class=\"redirect\">\n      <input id=\"form-text\" type=\"text\" name=\"text\" value=\"\">\n      <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/page_refresh.html\">\n      <input type=\"hidden\" name=\"sleep\" value=\"50\">\n      <input id=\"form-submit\" type=\"submit\" value=\"form[method=post]\">\n    </form>\n\n    <button id=\"add-new-assets\">Add new assets</button>\n\n    <div id=\"reject\">\n      <form class=\"unprocessable_content\" action=\"/__turbo/reject/morph\" method=\"post\" style=\"margin-top:100vh\">\n        <input type=\"hidden\" name=\"status\" value=\"422\">\n        <input type=\"submit\">\n      </form>\n    </div>\n\n    <div id=\"container\">\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/page_refresh_replace.html",
    "content": "<!DOCTYPE html>\n<html id=\"html\" data-skip-event-details=\"turbo:submit-start turbo:submit-end turbo:fetch-request-error\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"turbo-refresh-scroll\" content=\"preserve\">\n\n    <title>Turbo</title>\n    <link rel=\"icon\" href=\"data:;base64,iVBORw0KGgo=\">\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n\n    <style>\n      body {\n        margin: 0;\n        padding: 0;\n\n        /* Ensure the page is large enough to scroll */\n        width: 150vw;\n        height: 150vh;\n      }\n    </style>\n  </head>\n  <body>\n    <h1>Page to be refreshed</h1>\n\n    <turbo-frame id=\"refresh-morph\" src=\"/src/tests/fixtures/frame_refresh_morph.html\" refresh=\"morph\">\n      <h2>Frame to be morphed</h2>\n    </turbo-frame>\n\n    <turbo-frame id=\"refresh-reload\" src=\"/src/tests/fixtures/frame_refresh_reload.html\" refresh=\"reload\">\n      <h2>Frame to be reloaded</h2>\n    </turbo-frame>\n\n    <div id=\"preserve-me\" data-turbo-permanent>\n      Preserve me!\n    </div>\n\n    <form id=\"form\" action=\"/__turbo/refresh\" method=\"post\" class=\"redirect\">\n      <input type=\"text\" name=\"text\" value=\"\">\n      <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/page_refresh_replace.html\">\n      <input type=\"hidden\" name=\"sleep\" value=\"50\">\n      <input id=\"form-submit\" type=\"submit\" value=\"form[method=post]\">\n    </form>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/page_refresh_scroll_reset.html",
    "content": "<!DOCTYPE html>\n<html id=\"html\" data-skip-event-details=\"turbo:submit-start turbo:submit-end turbo:fetch-request-error\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"turbo-refresh-method\" content=\"morph\">\n    <meta name=\"turbo-refresh-scroll\" content=\"reset\">\n\n    <title>Turbo</title>\n    <link rel=\"icon\" href=\"data:;base64,iVBORw0KGgo=\">\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n\n    <style>\n      body {\n        margin: 0;\n        padding: 0;\n\n        /* Ensure the page is large enough to scroll */\n        width: 150vw;\n        height: 150vh;\n      }\n\n      #form {\n        position: absolute;\n        top: 120px;\n        left: 100px;\n      }\n    </style>\n  </head>\n  <body>\n    <h1>Page to be refreshed</h1>\n\n    <turbo-frame id=\"refreshed-frame\" src=\"/src/tests/fixtures/frame_refresh.html\">\n      <h2>Frame to be refreshed</h2>\n    </turbo-frame>\n\n    <form id=\"form\" action=\"/__turbo/refresh\" method=\"post\" class=\"redirect\">\n      <input type=\"text\" name=\"text\" value=\"\">\n      <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/page_refresh_scroll_reset.html\">\n      <input type=\"hidden\" name=\"sleep\" value=\"50\">\n      <input id=\"form-submit\" type=\"submit\" value=\"form[method=post]\">\n    </form>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/page_refresh_stream_action.html",
    "content": "<!DOCTYPE html>\n<html data-skip-event-details=\"turbo:submit-start turbo:submit-end\">\n<head>\n  <meta charset=\"utf-8\">\n  <title>Turbo Streams</title>\n  <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n  <script src=\"/src/tests/fixtures/test.js\"></script>\n</head>\n<body>\n  <form id=\"refresh\" method=\"post\" action=\"/__turbo/refreshes\">\n    <button>Refresh</button>\n    <input id=\"request-id\" type=\"hidden\" name=\"requestId\" value=\"\">\n  </form>\n\n  <div id=\"content\">\n    <span>Hello</span>\n    <a id=\"regular-link\" href=\"/src/tests/fixtures/one.html\">Regular link</a>\n  </div>\n</body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/page_refreshed.html",
    "content": "<!DOCTYPE html>\n<html data-skip-event-details=\"turbo:before-render\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>Refreshed page</h1>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/page_with_eager_frame.html",
    "content": "<!DOCTYPE html>\n<html id=\"html\" data-skip-event-details=\"turbo:before-render\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frame</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>Eager-loaded frame</h1>\n\n    <turbo-frame id=\"eager-loaded-frame\" src=\"/src/tests/fixtures/frames/frame_for_eager.html\" loading=\"eager\">\n      <h2>Eager-loaded frame: NOT Loaded...</h2>\n    </turbo-frame>\n\n\n    <a href=\"/src/tests/fixtures/frames.html\">Page /src/tests/fixtures/frames.html</a>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/pausable_rendering.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <script type=\"module\">\n      for (const event of [\"turbo:before-render\", \"turbo:before-frame-render\"]) {\n        addEventListener(event, function(event) {\n          event.preventDefault()\n          if (confirm(\"Continue rendering?\")) {\n            event.detail.resume()\n          }\n        })\n      }\n    </script>\n  </head>\n  <body>\n    <section>\n      <h1>Pausable Rendering</h1>\n      <p><a id=\"link\" href=\"/src/tests/fixtures/one.html\">Link</a></p>\n\n      <turbo-frame id=\"hello\">\n        <h2>Pausable Frame Rendering</h2>\n\n        <a id=\"frame-link\" href=\"/src/tests/fixtures/frames/hello.html\">#hello Frame Link</a>\n      </turbo-frame>\n    </section>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/pausable_requests.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <script type=\"module\">\n      addEventListener('turbo:before-fetch-request', function(event) {\n        event.preventDefault()\n        if (confirm('Continue request?')) {\n          event.detail.resume()\n        } else {\n          alert('Request aborted')\n        }\n      })\n    </script>\n  </head>\n  <body>\n    <section>\n      <h1>Pausable Requests</h1>\n      <p><a id=\"link\" href=\"/src/tests/fixtures/one.html\">Link</a></p>\n    </section>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/permanent_children.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"turbo-refresh-method\" content=\"morph\">\n    <meta name=\"turbo-refresh-scroll\" content=\"preserve\">\n\n    <title>Turbo</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <section>\n      <h1>Permanent children</h1>\n    </section>\n\n    <ul>\n      <li id=\"first-li\">\n        <input id=\"first-checkbox\" type=\"checkbox\" data-turbo-permanent>\n        <label for=\"first-checkbox\">First checkbox</label>\n      </li>\n      <li id=\"second-li\">\n        <input id=\"second-checkbox\" type=\"checkbox\" data-turbo-permanent>\n        <label for=\"second-checkbox\">Second checkbox</label>\n      </li>\n    </ul>\n\n    <form id=\"form\" action=\"/__turbo/refresh\" method=\"post\" class=\"redirect\">\n      <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/permanent_children.html\">\n      <input type=\"hidden\" name=\"sleep\" value=\"50\">\n      <input id=\"form-submit\" type=\"submit\" value=\"form[method=post]\">\n    </form>\n\n  </body>\n</html>\n\n"
  },
  {
    "path": "src/tests/fixtures/permanent_element.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <section>\n      <h1>Permanent element</h1>\n    </section>\n    <div id=\"permanent\" data-turbo-permanent>Permanent element</div>\n\n    <turbo-frame id=\"frame\">\n      <div id=\"permanent-in-frame\" data-turbo-permanent>Permanent element</div>\n    </turbo-frame>\n\n    <video id=\"permanent-video\" data-turbo-permanent>\n      <source src=\"/src/tests/fixtures/video.webm\" type=\"video/webm\">\n      <source src=\"/src/tests/fixtures/video.mp4\" type=\"video/mp4\">\n    </video>\n    <button id=\"permanent-video-button\" type=\"button\">Play</button>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/prefetched.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Prefetched Page</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n  </head>\n\n  <body>\n    Prefetched Page Content\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/preloaded.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"utf-8\">\n  <title>Preloaded Page</title>\n  <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n  <script src=\"/src/tests/fixtures/test.js\"></script>\n</head>\n\n<body>\n  <div>\n    This page was hopefully preloaded\n  </div>\n</body>\n\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/preloading.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"utf-8\">\n  <title>Preloading Page</title>\n  <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n</head>\n\n<body>\n  <a href=\"/src/tests/fixtures/preloaded.html\" id=\"preload_anchor\" data-turbo-preload>\n    Visit preloaded page\n  </a>\n\n  <a href=\"/__turbo/redirect?path=/src/tests/fixtures/one.html\" data-turbo-method=\"post\" data-turbo-preload>POST</a>\n\n  <a href=\"/src/tests/fixtures/one.html\" data-turbo-stream data-turbo-preload>[data-turbo-stream]</a>\n\n  <div data-turbo=\"false\">\n    <a href=\"/src/tests/fixtures/one.html\" data-turbo-preload>Visit page</a>\n  </div>\n\n  <a href=\"https://example.com\" data-turbo-preload>Navigate off-site</a>\n</body>\n\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/remote_permanent_frame.html",
    "content": "<turbo-frame id=\"remote-permanent-frame\">\n  <h2>Loaded permanent frame</h2>\n</turbo-frame>\n"
  },
  {
    "path": "src/tests/fixtures/rendering.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <script type=\"importmap\" nonce=\"123\">\n      { \"imports\": { \"turbo\": \"/dist/turbo.es2017-umd.js?123\"} }\n    </script>\n    <script type=\"module\">\n      const permanentVideoElementLoaded = new Promise(resolve => {\n        addEventListener(\"DOMContentLoaded\", () => {\n          const element = document.getElementById(\"permanent-video\")\n          element.addEventListener(\"canplaythrough\", () => resolve(element), { once: true })\n        })\n      })\n\n      addEventListener(\"click\", async event => {\n        if (event.target.id == \"permanent-video-button\") {\n          const element = await permanentVideoElementLoaded\n          element.addEventListener(\"timeupdate\", () => element.pause(), { once: true })\n          element.play()\n        }\n      })\n    </script>\n    <style>\n      .push-off-screen { margin-top: 1000px; }\n    </style>\n  </head>\n  <body>\n    <section>\n      <h1>Rendering</h1>\n      <p><a id=\"same-origin-link\" href=\"/src/tests/fixtures/one.html\">Same-origin link</a></p>\n      <p><a id=\"tracked-asset-change-link\" href=\"/src/tests/fixtures/tracked_asset_change.html\">Tracked asset change</a></p>\n      <form id=\"tracked-asset-change-form\" action=\"/__turbo/notfound\" method=\"post\"><button type=\"submit\">Submit</button></form>\n      <p><a id=\"tracked-nonce-tag-link\" href=\"/src/tests/fixtures/tracked_nonce_tag.html\">Tracked nonce tag</a></p>\n      <p><a id=\"additional-assets-link\" href=\"/src/tests/fixtures/additional_assets.html\">Additional assets</a></p>\n      <p><a id=\"additional-script-link\" href=\"/src/tests/fixtures/additional_script.html\">Additional script</a></p>\n      <p><a id=\"body-noscript-link\" href=\"/src/tests/fixtures/body_noscript.html\">Body noscript</a></p>\n      <p><a id=\"body-noscript-content-link\" href=\"/src/tests/fixtures/body_noscript_with_content.html\">Body noscript with content</a></p>\n      <p><a id=\"head-script-link\" href=\"/src/tests/fixtures/head_script.html\">Head script</a></p>\n      <p><a id=\"body-script-link\" href=\"/src/tests/fixtures/body_script.html\">Body script</a></p>\n      <p><a id=\"eval-false-script-link\" href=\"/src/tests/fixtures/eval_false_script.html\">data-turbo-eval=false script</a></p>\n      <p><a id=\"nonexistent-link\" href=\"/nonexistent\">Nonexistent link</a></p>\n      <p><a id=\"visit-control-reload-link\" href=\"/src/tests/fixtures/visit_control_reload.html\">Visit control: reload</a></p>\n      <p><a id=\"permanent-element-link\" href=\"/src/tests/fixtures/permanent_element.html\">Permanent element</a></p>\n      <p><a id=\"permanent-in-frame-element-link\" href=\"/src/tests/fixtures/permanent_element.html\" data-turbo-frame=\"frame\">Permanent element in frame</a></p>\n      <p><a id=\"delayed-link\" href=\"/__turbo/delayed_response\">Delayed link</a></p>\n      <p><a id=\"redirect-link\" href=\"/__turbo/redirect\">Redirect link</a></p>\n      <p><a id=\"es_locale_link\" href=\"/src/tests/fixtures/es_locale.html\">Change html[lang]</a></p>\n      <p><a id=\"dir-rtl\" href=\"/src/tests/fixtures/dir_rtl.html\">Change html[dir]</a></p>\n      <form>\n        <input type=\"text\" id=\"text-input\">\n        <input type=\"radio\" id=\"radio-input\">\n        <input type=\"checkbox\" id=\"checkbox-input\">\n        <textarea id=\"textarea\"></textarea>\n        <select id=\"select\">\n          <option value=\"1\">1</option>\n          <option value=\"2\">2</option>\n        </select>\n        <select id=\"select-multiple\" multiple>\n          <option value=\"1\">1</option>\n          <option value=\"2\">2</option>\n        </select>\n\n        <input type=\"password\" id=\"password-input\">\n\n        <input type=\"reset\" id=\"reset-input\">\n      </form>\n    </section>\n    <div id=\"permanent\" data-turbo-permanent>Rendering</div>\n\n    <form>\n      <input id=\"permanent-input\" type=\"text\" name=\"query\" placeholder=\"Permanent input\" data-turbo-permanent>\n    </form>\n\n    <form id=\"permanent-form\" data-turbo-permanent>\n      <input id=\"permanent-descendant-input\" type=\"text\" name=\"query\" placeholder=\"Permanent descendant input\">\n    </form>\n\n    <turbo-frame id=\"frame\">\n      <div id=\"permanent-in-frame\" data-turbo-permanent>Rendering</div>\n    </turbo-frame>\n\n    <turbo-frame id=\"hello\">\n      <form action=\"/__turbo/redirect\">\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/frames/hello.html\">\n        <input id=\"permanent-input-in-frame\" type=\"text\" name=\"query\" placeholder=\"Permanent input in frame\" data-turbo-permanent>\n      </form>\n\n      <form id=\"permanent-form-in-frame\" action=\"/__turbo/redirect\" data-turbo-permanent>\n        <input type=\"hidden\" name=\"path\" value=\"/src/tests/fixtures/frames/hello.html\">\n        <input id=\"permanent-descendant-input-in-frame\" type=\"text\" name=\"query\" placeholder=\"Permanent descendant input in frame\" data-turbo-permanent>\n      </form>\n    </turbo-frame>\n\n    <video id=\"permanent-video\" data-turbo-permanent>\n      <source src=\"/src/tests/fixtures/video.webm\" type=\"video/webm\">\n      <source src=\"/src/tests/fixtures/video.mp4\" type=\"video/mp4\">\n    </video>\n    <button id=\"permanent-video-button\" type=\"button\">Play</button>\n\n    <hr class=\"push-off-screen\">\n    <p><a id=\"below-the-fold-visit-control-reload-link-middle\" href=\"/src/tests/fixtures/visit_control_reload.html#middle\">Visit control: reload - middle</a></p>\n    <p><a id=\"below-the-fold-visit-control-reload-link\" href=\"/src/tests/fixtures/visit_control_reload.html\">Visit control: reload</a></p>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/response.js",
    "content": "document.getElementById(\"frame\").innerHTML = \"Content from UJS response\"\n"
  },
  {
    "path": "src/tests/fixtures/root/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <meta name=\"turbo-root\" content=\"/src/tests/fixtures/root\">\n  </head>\n  <body>\n    <section>\n      <p><a id=\"link-page-inside\" href=\"/src/tests/fixtures/root/page.html\">Link to page inside the root</a></p>\n      <p><a id=\"link-page-outside\" href=\"/src/tests/fixtures/one.html\">Link to page outside the root</a></p>\n      <p><a id=\"link-page-outside-prefix\" href=\"/src/tests/fixtures/rootlet.html\">Link to page outside the root having the root as a prefix</a></p>\n    </section>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/root/page.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <meta name=\"turbo-root\" content=\"/src/tests/fixtures/root\">\n  </head>\n  <body>\n    <section>\n      <p><a id=\"link-root\" href=\"/src/tests/fixtures/root\">Link to root</a></p>\n    </section>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/scroll/one.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Scroll: One</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <style>\n      html { scroll-behavior: smooth; }\n      .push-below-fold { margin-top: 100vh; }\n    </style>\n  </head>\n  <body>\n    <h1>Scroll: One</h1>\n    <hr class=\"push-below-fold\">\n    <a id=\"one-below-fold\" href=\"/src/tests/fixtures/scroll/two.html#two-below-fold\">two.html#two-below-fold</a>\n    <hr class=\"push-below-fold\">\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/scroll/two.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Scroll: Two</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <style>\n      html { scroll-behavior: smooth; }\n      .push-below-fold { margin-top: 100vh; }\n    </style>\n  </head>\n  <body>\n    <h1>Scroll: Two</h1>\n    <hr class=\"push-below-fold\">\n    <a id=\"two-below-fold\" href=\"/src/tests/fixtures/scroll/one.html#one-below-fold\">one.html#one-below-fold</a>\n    <hr class=\"push-below-fold\">\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/scroll_restoration.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Scroll Restoration</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <style>\n      li {\n        min-height: 50vh;\n      }\n    </style>\n  </head>\n  <body>\n    <ul>\n      <li id=\"one\">One</li>\n      <li id=\"two\">Two</li>\n      <li id=\"three\">Three</li>\n      <li id=\"four\">Four</li>\n      <li id=\"five\">Five</li>\n    </ul>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/stream.html",
    "content": "<!DOCTYPE html>\n<html data-skip-event-details=\"turbo:submit-start turbo:submit-end\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo Streams</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <form id=\"append-target\" method=\"post\" action=\"/__turbo/messages\">\n      <input type=\"hidden\" name=\"content\" value=\"Hello world!\">\n      <input type=\"hidden\" name=\"type\" value=\"stream\">\n      <input type=\"hidden\" name=\"id\" value=\"a-turbo-stream\">\n      <button>Create</button>\n    </form>\n\n    <form id=\"append-targets\" method=\"post\" action=\"/__turbo/messages\">\n      <input type=\"hidden\" name=\"content\" value=\"Hello CSS!\">\n      <input type=\"hidden\" name=\"targets\" value=\".messages\">\n      <input type=\"hidden\" name=\"type\" value=\"stream\">\n      <button>Replace</button>\n    </form>\n\n    <form id=\"async\" method=\"post\" action=\"/__turbo/messages\">\n      <input type=\"hidden\" name=\"content\" value=\"Hello world!\">\n      <button>Receive Message</button>\n    </form>\n\n    <div id=\"messages\">\n      <div class=\"message\">First</div>\n    </div>\n    <div id=\"messages_2\" class=\"messages\">\n      <div class=\"message\">Second</div>\n    </div>\n    <div id=\"messages_3\" class=\"messages\">\n      <div class=\"message\">Third</div>\n    </div>\n\n    <div id=\"container\">\n      <input id=\"container-element\">\n    </div>\n\n    <div id=\"message_1\">\n      <div>Morph me</div>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/stylesheets/common.css",
    "content": "body {\n  background-color: rgb(0, 0, 128);\n  color: rgb(0, 128, 0);\n  margin: 0;\n}\n"
  },
  {
    "path": "src/tests/fixtures/stylesheets/left.css",
    "content": "body {\n  background-color: rgb(128, 0, 0);\n  color: rgb(128, 0, 0);\n}\n"
  },
  {
    "path": "src/tests/fixtures/stylesheets/left.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Left</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"/src/tests/fixtures/stylesheets/common.css\" data-turbo-track=\"dynamic\">\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"/src/tests/fixtures/stylesheets/left.css\" data-turbo-track=\"dynamic\">\n\n    <style data-turbo-track=\"dynamic\">\n      body { margin-left: 20px; }\n      .left { margin-left: 20px; }\n    </style>\n\n    <script type=\"text/javascript\">\n      document.head.insertAdjacentHTML(\"beforeend\", `<style id=\"added-style\">body{background:red}</style>`)\n      document.head.insertAdjacentHTML(\"beforeend\", `<link id=\"added-link\" rel=\"stylesheet\" type=\"text/css\" href=\"/src/tests/fixtures/test.css\">`)\n    </script>\n  </head>\n\n  <body></body>\n    <h1>Left</h1>\n    <p><a id=\"go-right\" href=\"/src/tests/fixtures/stylesheets/right.html\">go right</a></p>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/stylesheets/right.css",
    "content": "body {\n  background-color: rgb(0, 128, 0);\n}\n"
  },
  {
    "path": "src/tests/fixtures/stylesheets/right.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Right</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"/src/tests/fixtures/stylesheets/common.css\" data-turbo-track=\"dynamic\">\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"/src/tests/fixtures/stylesheets/right.css\" data-turbo-track=\"dynamic\">\n\n    <style data-turbo-track=\"dynamic\">\n      body { margin-right: 20px; }\n      .right { margin-right: 20px; }\n    </style>\n  </head>\n\n  <body></body>\n    <h1>Right</h1>\n    <p><a id=\"go-left\" href=\"/src/tests/fixtures/stylesheets/left.html\">go left</a></p>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/tabs/three.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frame</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <turbo-frame id=\"tab-frame\" data-turbo-action=\"advance\">\n      <div>\n        <a id=\"tab-1\" href=\"/src/tests/fixtures/tabs.html\">Tab 1</a>\n        <a id=\"tab-2\" href=\"/src/tests/fixtures/tabs/two.html\">Tab 2</a>\n        <a id=\"tab-3\" href=\"/src/tests/fixtures/tabs/three.html\">Tab 3</a>\n      </div>\n\n      <div id=\"tab-content\">Three</div>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/tabs/two.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frame</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <turbo-frame id=\"tab-frame\" data-turbo-action=\"advance\">\n      <div>\n        <a id=\"tab-1\" href=\"/src/tests/fixtures/tabs.html\">Tab 1</a>\n        <a id=\"tab-2\" href=\"/src/tests/fixtures/tabs/two.html\">Tab 2</a>\n        <a id=\"tab-3\" href=\"/src/tests/fixtures/tabs/three.html\">Tab 3</a>\n      </div>\n\n      <div id=\"tab-content\">Two</div>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/tabs.html",
    "content": "<!DOCTYPE html>\n<html data-skip-event-details=\"turbo:fetch-request-error\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Tabs</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>Tabs</h1>\n\n    <turbo-frame id=\"tab-frame\" data-turbo-action=\"advance\">\n      <div>\n        <a id=\"tab-1\" href=\"/src/tests/fixtures/tabs.html\">Tab 1</a>\n        <a id=\"tab-2\" href=\"/src/tests/fixtures/tabs/two.html\">Tab 2</a>\n        <a id=\"tab-3\" href=\"/src/tests/fixtures/tabs/three.html\">Tab 3</a>\n      </div>\n\n      <div id=\"tab-content\">One</div>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/target.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Target</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>Target</h1>\n\n    <div id=\"target\">\n      Targeted element\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/test.css",
    "content": ":root {\n  --black-if-evaluated: black;\n}\n"
  },
  {
    "path": "src/tests/fixtures/test.js",
    "content": "(function (eventNames) {\n  function serializeToChannel(object, visited = new Set()) {\n    const returned = {}\n\n    for (const key in object) {\n      const value = object[key]\n\n      if (value instanceof URL) {\n        returned[key] = value.toJSON()\n      } else if (value instanceof Element) {\n        returned[key] = value.outerHTML\n      } else if (typeof value == \"object\") {\n        if (visited.has(value)) {\n          returned[key] = \"skipped to prevent infinitely recursing\"\n        } else {\n          visited.add(value)\n\n          returned[key] = serializeToChannel(value, visited)\n        }\n      } else {\n        returned[key] = value\n      }\n    }\n\n    return returned\n  }\n\n  window.eventLogs = []\n\n  for (let i = 0; i < eventNames.length; i++) {\n    const eventName = eventNames[i]\n    addEventListener(eventName, eventListener, false)\n  }\n\n  function eventListener(event) {\n    const skipped = document.documentElement.getAttribute(\"data-skip-event-details\") || \"\"\n\n    window.eventLogs.push([\n      event.type,\n      serializeToChannel(skipped.includes(event.type) ? {} : event.detail),\n      event.target.id\n    ])\n  }\n  window.mutationLogs = []\n\n  new MutationObserver((mutations) => {\n    for (const { attributeName, target } of mutations.filter(({ type }) => type == \"attributes\")) {\n      if (target instanceof Element) {\n        window.mutationLogs.push([attributeName, target.id, target.getAttribute(attributeName)])\n      }\n    }\n  }).observe(document, { subtree: true, childList: true, attributes: true })\n\n  window.bodyMutationLogs = []\n  addEventListener(\n    \"turbo:load\",\n    () => {\n      new MutationObserver((mutations) => {\n        for (const { addedNodes } of mutations) {\n          for (const { localName, outerHTML } of addedNodes) {\n            if (localName == \"body\") window.bodyMutationLogs.push([outerHTML])\n          }\n        }\n      }).observe(document.documentElement, { childList: true })\n    },\n    { once: true }\n  )\n})([\n  \"turbo:click\",\n  \"turbo:before-stream-render\",\n  \"turbo:before-cache\",\n  \"turbo:before-render\",\n  \"turbo:before-visit\",\n  \"turbo:load\",\n  \"turbo:render\",\n  \"turbo:before-prefetch\",\n  \"turbo:before-fetch-request\",\n  \"turbo:submit-start\",\n  \"turbo:submit-end\",\n  \"turbo:before-fetch-response\",\n  \"turbo:visit\",\n  \"turbo:before-frame-render\",\n  \"turbo:fetch-request-error\",\n  \"turbo:frame-load\",\n  \"turbo:frame-render\",\n  \"turbo:frame-missing\",\n  \"turbo:before-frame-morph\",\n  \"turbo:morph\",\n  \"turbo:before-morph-element\",\n  \"turbo:morph-element\",\n  \"turbo:before-morph-attribute\",\n  \"turbo:reload\"\n])\n\nwindow.visitLogs = []\n\naddEventListener(\"turbo:visit\", ({ detail }) => window.visitLogs.push(detail))\n\ncustomElements.define(\n  \"custom-link-element\",\n  class extends HTMLElement {\n    constructor() {\n      super()\n      this.attachShadow({ mode: \"open\" })\n    }\n    connectedCallback() {\n      this.shadowRoot.innerHTML = `\n      <a href=\"${this.getAttribute(\"link\")}\">\n        ${this.getAttribute(\"text\") || `<slot></slot>`}\n      </a>\n    `\n    }\n  }\n)\n\ncustomElements.define(\n  \"custom-button\",\n  class extends HTMLElement {\n    constructor() {\n      super()\n      this.attachShadow({ mode: \"open\" }).innerHTML = `\n      <span>\n        Drive in Shadow DOM\n      </span>\n    `\n    }\n  }\n)\n\ncustomElements.define(\n  \"turbo-toggle\",\n  class extends HTMLElement {\n    constructor() {\n      super()\n      this.attachShadow({ mode: \"open\" })\n    }\n    connectedCallback() {\n      this.shadowRoot.innerHTML = `\n      <div data-turbo=\"${this.getAttribute(\"turbo\") || \"true\"}\">\n        <slot></slot>\n      </div>\n    `\n    }\n  }\n)\n"
  },
  {
    "path": "src/tests/fixtures/tracked_asset_change.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Tracked asset change</title>\n    <script src=\"/dist/turbo.es2017-umd.js?123\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>Tracked asset change</h1>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/tracked_nonce_change.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Tracked nonce tag</title>\n    <script src=\"/dist/turbo.es2017-umd.js?123\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n  </head>\n  <body>\n    <h1>Tracked nonce tag</h1>\n    <p><a id=\"tracked-nonce-tag-link\" href=\"/src/tests/fixtures/tracked_nonce_tag.html?no=change\">Tracked nonce tag</a></p>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/transitions/left.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Left</title>\n    <meta name=\"turbo-view-transition\" content=\"true\" />\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n\n    <style>\n      .square {\n        display: block;\n        width: 100px;\n        height: 100px;\n        border-radius: 6px;\n        background-color: blue;\n        view-transition-name: square;\n      }\n\n      .square.right {\n        margin-left: auto;\n      }\n    </style>\n  </head>\n\n  <body style=\"background-color: orange\">\n    <h1>Left</h1>\n    <p><a id=\"go-right\" href=\"/src/tests/fixtures/transitions/right.html\">go right</a></p>\n    <div class=\"square\"></div>\n    <p><a id=\"go-other\" href=\"/src/tests/fixtures/transitions/other.html\">go other</a></p>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/transitions/left_legacy.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"utf-8\" />\n  <title>Left</title>\n  <meta name=\"view-transition\" content=\"same-origin\" />\n  <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n\n  <style>\n    .square {\n      display: block;\n      width: 100px;\n      height: 100px;\n      border-radius: 6px;\n      background-color: blue;\n      view-transition-name: square;\n    }\n\n    .square.right {\n      margin-left: auto;\n    }\n  </style>\n</head>\n\n<body style=\"background-color: orange\">\n  <h1>Left</h1>\n  <p><a id=\"go-right\" href=\"/src/tests/fixtures/transitions/right_legacy.html\">go right</a></p>\n  <div class=\"square\"></div>\n  <p><a id=\"go-other\" href=\"/src/tests/fixtures/transitions/other.html\">go other</a></p>\n</body>\n\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/transitions/other.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Other</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n  </head>\n\n  <body style=\"background-color: yellow\">\n    <h1>Other</h1>\n    <p><a id=\"go-left\" href=\"/src/tests/fixtures/transitions/left.html\">go left</a></p>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/transitions/right.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Right</title>\n    <meta name=\"turbo-view-transition\" content=\"true\" />\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n\n    <style>\n      .square {\n        display: block;\n        width: 100px;\n        height: 100px;\n        border-radius: 6px;\n        background-color: blue;\n        view-transition-name: square;\n      }\n\n      .square.right {\n        margin-left: auto;\n      }\n    </style>\n  </head>\n\n  <body style=\"background-color: red\">\n    <h1>Right</h1>\n    <p><a id=\"go-left\" href=\"/src/tests/fixtures/transitions/left.html\">go left</a></p>\n    <div class=\"square right\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/transitions/right_legacy.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"utf-8\">\n  <title>Right</title>\n  <meta name=\"view-transition\" content=\"same-origin\" />\n  <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n\n  <style>\n    .square {\n      display: block;\n      width: 100px;\n      height: 100px;\n      border-radius: 6px;\n      background-color: blue;\n      view-transition-name: square;\n    }\n\n    .square.right {\n      margin-left: auto;\n    }\n  </style>\n</head>\n\n<body style=\"background-color: red\">\n  <h1>Right</h1>\n  <p><a id=\"go-left\" href=\"/src/tests/fixtures/transitions/left_legacy.html\">go left</a></p>\n  <div class=\"square right\"></div>\n</body>\n\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/two.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Two</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <meta name=\"test\" content=\"foo\">\n  </head>\n  <body>\n    <h1>Two</h1>\n\n    <!--styles ensure that the element will be scrolled to top when navigated to via an anchored link -->\n    <a name=\"named-anchor\"></a>\n    <div id=\"element-id\" style=\"margin-top: 1em; height: 200vh\">An element with an ID</div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/ujs.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frame</title>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <script type=\"module\">\n      import Rails from \"https://ga.jspm.io/npm:@rails/ujs@7.0.1/lib/assets/compiled/rails-ujs.js\"\n\n      Rails.start()\n    </script>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n  </head>\n  <body>\n    <turbo-frame id=\"frame\">\n      <h2>Frames: #frame</h2>\n\n      <a data-remote=\"true\" href=\"/src/tests/fixtures/response.js\">navigate #frame to /src/tests/fixtures/frames/frame.html</a>\n      <form data-remote=\"true\" action=\"/src/tests/fixtures/frames/frame.html\">\n        <button>navigate #frame to /src/tests/fixtures/frames/frame.html</button>\n      </form>\n    </turbo-frame>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/umd.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>UMD</title>\n    <script type=\"module\" data-turbo-track=\"reload\">\n      import \"/dist/turbo.es2017-umd.js\"\n    </script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <meta name=\"test\" content=\"foo\">\n  </head>\n  <body>\n    <h1>UMD</h1>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/visit.html",
    "content": "<!DOCTYPE html>\n<html id=\"html\" data-skip-event-details=\"turbo:fetch-request-error\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <style>\n      .push-below-fold { margin-top: 100vh; }\n    </style>\n  </head>\n  <body>\n    <section>\n      <h1>Visit</h1>\n      <p><a id=\"same-origin-link\" href=\"/src/tests/fixtures/one.html\">Same-origin link</a></p>\n      <p><a id=\"same-origin-replace-link\" href=\"/src/tests/fixtures/one.html\" data-turbo-action=\"replace\">Same-origin replace link</a></p>\n      <p><a id=\"same-origin-link-search-params\" href=\"/src/tests/fixtures/one.html?key=value\">Same-origin link with ?key=value</a></p>\n      <p><a id=\"sample-response\" href=\"/src/tests/fixtures/one.html\">Sample response</a></p>\n      <p><a id=\"same-page-link\" href=\"/src/tests/fixtures/visit.html\">Same page link</a></p>\n      <hr class=\"push-below-fold\">\n      <p><a id=\"below-the-fold-link\" href=\"/src/tests/fixtures/one.html\">one.html</a></p>\n      <hr class=\"push-below-fold\">\n      <p><a id=\"stream-link\" href=\"/__turbo/stream-response?content=link&type=stream&key=value\" data-turbo-stream>Stream link with ?key=value</a></p>\n      <p><a id=\"cache-observer-link\" href=\"/src/tests/fixtures/cache_observer.html\">Link to cache observer</a></p>\n    </section>\n\n    <div id=\"messages\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/visit_control_reload.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Visit control: reload</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n    <script src=\"/src/tests/fixtures/test.js\"></script>\n    <meta name=\"turbo-visit-control\" content=\"reload\">\n    <style>\n      .push-off-screen { margin-top: 1000px; }\n    </style>\n   </head>\n   <body>\n     <h1>Visit control: reload</h1>\n\n    <hr class=\"push-off-screen\">\n\n    <h2 id=\"middle\">Middle the page</h2>\n    <hr class=\"push-off-screen\">\n    <h2>Down the page</h2>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/functional/async_script_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport { readEventLogs, visitAction } from \"../helpers/page\"\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/async_script.html\")\n  await readEventLogs(page)\n})\n\ntest(\"does not emit turbo:load when loaded asynchronously after DOMContentLoaded\", async ({ page }) => {\n  const events = await readEventLogs(page)\n\n  expect(events).toEqual([])\n})\n\ntest(\"following a link when loaded asynchronously after DOMContentLoaded\", async ({ page }) => {\n  await page.click(\"#async-link\")\n\n  expect(await visitAction(page)).toEqual(\"advance\")\n})\n"
  },
  {
    "path": "src/tests/functional/autofocus_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport { nextEventNamed, nextPageRefresh } from \"../helpers/page\"\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/autofocus.html\")\n})\n\ntest(\"autofocus first autofocus element on load\", async ({ page }) => {\n  await expect(page.locator(\"#first-autofocus-element\")).toBeFocused()\n})\n\ntest(\"autofocus first [autofocus] element on visit\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/navigation.html\")\n  await page.click(\"#autofocus-link\")\n  await expect(page.locator(\"#first-autofocus-element\")).toBeFocused()\n})\n\ntest(\"navigating a frame with a descendant link autofocuses [autofocus]:first-of-type\", async ({ page }) => {\n  await page.click(\"#frame-inner-link\")\n  await expect(page.locator(\"#frames-form-first-autofocus-element\")).toBeFocused()\n})\n\ntest(\"autofocus visible [autofocus] element on visit with inert elements\", async ({ page }) => {\n  await page.click(\"#autofocus-inert-link\")\n  await expect(page.locator(\"#visible-autofocus-element\")).toBeFocused()\n})\n\ntest(\"navigating a frame with a link targeting the frame autofocuses [autofocus]:first-of-type\", async ({\n  page\n}) => {\n  await page.click(\"#frame-outer-link\")\n  await expect(page.locator(\"#frames-form-first-autofocus-element\")).toBeFocused()\n})\n\ntest(\"navigating a frame with a turbo-frame targeting the frame autofocuses [autofocus]:first-of-type\", async ({\n  page\n}) => {\n  await page.click(\"#drives-frame-target-link\")\n  await expect(page.locator(\"#frames-form-first-autofocus-element\")).toBeFocused()\n})\n\ntest(\"receiving a Turbo Stream message with an [autofocus] element when the activeElement is the document\", async ({ page }) => {\n  // Ensure the [autofocus] element has been processed before blurring\n  await expect(page.locator(\"#first-autofocus-element\")).toBeFocused()\n\n  await page.evaluate(() => {\n    document.activeElement.blur()\n    window.Turbo.renderStreamMessage(`\n      <turbo-stream action=\"append\" targets=\"body\">\n        <template><input id=\"autofocus-from-stream\" autofocus></template>\n      </turbo-stream>\n    `)\n  })\n\n  await expect(page.locator(\"#autofocus-from-stream\")).toBeFocused()\n})\n\ntest(\"autofocus from a Turbo Stream message does not leak a placeholder [id]\", async ({ page }) => {\n  await page.evaluate(() => {\n    document.activeElement.blur()\n    window.Turbo.renderStreamMessage(`\n      <turbo-stream action=\"append\" targets=\"body\">\n        <template><div id=\"container-from-stream\"><input autofocus></div></template>\n      </turbo-stream>\n    `)\n  })\n\n  await expect(page.locator(\"#container-from-stream input\")).toBeFocused()\n})\n\ntest(\"receiving a Turbo Stream message with an [autofocus] element when an element within the document has focus\", async ({ page }) => {\n  await page.evaluate(() => {\n    window.Turbo.renderStreamMessage(`\n      <turbo-stream action=\"append\" targets=\"body\">\n        <template><input id=\"autofocus-from-stream\" autofocus></template>\n      </turbo-stream>\n    `)\n  })\n  await expect(page.locator(\"#first-autofocus-element\")).toBeFocused()\n})\n\ntest(\"don't focus on [autofocus] elements on page refreshes with morphing\", async ({ page }) => {\n  const input = await page.locator(\"#form input[autofocus]\")\n\n  const button = page.locator(\"#first-autofocus-element\")\n  await button.click()\n\n  await nextPageRefresh(page)\n\n  await expect(button).toBeFocused()\n  await expect(input).not.toBeFocused()\n\n  await page.locator(\"#form\").evaluate((form) => form.requestSubmit())\n\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n  await nextPageRefresh(page)\n\n  await expect(button).toBeFocused()\n})\n"
  },
  {
    "path": "src/tests/functional/cache_observer_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport { nextEventNamed } from \"../helpers/page\"\n\ntest(\"removes temporary elements\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/cache_observer.html\")\n\n  await expect(page.locator(\"#temporary\")).toHaveText(\"data-turbo-temporary\")\n\n  await page.click(\"#link\")\n  await nextEventNamed(page, \"turbo:load\")\n  await page.goBack()\n\n  await expect(page.locator(\"#temporary\")).not.toBeAttached()\n})\n\ntest(\"following a redirect renders [data-turbo-temporary] elements before the cache removes\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/navigation.html\")\n  await page.click(\"#redirect-to-cache-observer\")\n\n  await expect(page.locator(\"#temporary\")).toHaveText(\"data-turbo-temporary\")\n})\n"
  },
  {
    "path": "src/tests/functional/drive_disabled_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport {\n  getFromLocalStorage,\n  nextEventOnTarget,\n  setLocalStorageFromEvent,\n  visitAction,\n  withPathname,\n  withSearchParam\n} from \"../helpers/page\"\n\nconst path = \"/src/tests/fixtures/drive_disabled.html\"\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(path)\n})\n\ntest(\"drive disabled by default; click normal link\", async ({ page }) => {\n  await page.click(\"#drive_disabled\")\n\n  await expect(page).toHaveURL(withPathname(path))\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n\ntest(\"drive disabled by default; click link inside data-turbo='true'\", async ({ page }) => {\n  await page.click(\"#drive_enabled\")\n\n  await expect(page).toHaveURL(withPathname(path))\n  expect(await visitAction(page)).toEqual(\"advance\")\n})\n\ntest(\"drive disabled by default; submit form inside data-turbo='true'\", async ({ page }) => {\n  await setLocalStorageFromEvent(page, \"turbo:submit-start\", \"formSubmitted\", \"true\")\n\n  await page.click(\"#no_submitter_drive_enabled a#requestSubmit\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"greeting\", \"Hello from a redirect\"))\n  expect(await getFromLocalStorage(page, \"formSubmitted\")).toBeTruthy()\n  expect(await visitAction(page)).toEqual(\"advance\")\n})\n\ntest(\"drive disabled by default; links within <turbo-frame> navigate with Turbo\", async ({ page }) => {\n  await page.click(\"#frame a\")\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-render\")\n})\n\ntest(\"drive disabled by default; forms within <turbo-frame> navigate with Turbo\", async ({ page }) => {\n  await page.click(\"#frame button\")\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-render\")\n})\n\ntest(\"drive disabled by default; slot within <turbo-frame> navigate with Turbo\", async ({ page }) => {\n  await page.click(\"#frame-navigation-with-slot\")\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-render\")\n})\n"
  },
  {
    "path": "src/tests/functional/drive_stylesheet_merging_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport { cssClassIsDefined, getComputedStyle } from \"../helpers/page\"\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/stylesheets/left.html\")\n})\n\ntest(\"navigating removes unused dynamically tracked style elements\", async ({ page }) => {\n  const addedStyle = page.locator('style[id=\"added-style\"]')\n  const addedLink = page.locator('link[id=\"added-link\"]')\n  await expect(addedStyle).toBeAttached()\n  await expect(addedLink).toBeAttached()\n\n  await page.locator(\"#go-right\").click()\n\n  await expect(page.locator('link[rel=stylesheet][href=\"/src/tests/fixtures/stylesheets/common.css\"]')).toBeAttached()\n  await expect(page.locator('link[rel=stylesheet][href=\"/src/tests/fixtures/stylesheets/right.css\"]')).toBeAttached()\n  await expect(page.locator('link[rel=stylesheet][href=\"/src/tests/fixtures/stylesheets/left.css\"]')).not.toBeAttached()\n  expect(await getComputedStyle(page, \"body\", \"backgroundColor\")).toEqual(\"rgb(0, 128, 0)\")\n  expect(await getComputedStyle(page, \"body\", \"color\")).toEqual(\"rgb(0, 128, 0)\")\n\n  await expect(addedStyle).toBeAttached()\n  await expect(addedLink).toBeAttached()\n\n  expect(await cssClassIsDefined(page, \"right\")).toBeTruthy()\n  expect(await cssClassIsDefined(page, \"left\")).not.toBeTruthy()\n  expect(await getComputedStyle(page, \"body\", \"marginLeft\")).toEqual(\"0px\")\n  expect(await getComputedStyle(page, \"body\", \"marginRight\")).toEqual(\"20px\")\n})\n"
  },
  {
    "path": "src/tests/functional/drive_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport { visitAction, withPathname } from \"../helpers/page\"\n\nconst path = \"/src/tests/fixtures/drive.html\"\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(path)\n})\n\ntest(\"drive enabled by default; click normal link\", async ({ page }) => {\n  await page.click(\"#drive_enabled\")\n\n  await expect(page).toHaveURL(withPathname(path))\n})\n\ntest(\"drive to external link\", async ({ page }) => {\n  await page.route(\"https://example.com\", async (route) => {\n    await route.fulfill({ body: \"Hello from the outside world\" })\n  })\n\n  await page.click(\"#drive_enabled_external\")\n\n  await expect(page).toHaveURL(\"https://example.com/\")\n  await expect(page.locator(\"body\")).toHaveText(\"Hello from the outside world\")\n})\n\ntest(\"drive enabled by default; click link inside data-turbo='false'\", async ({ page }) => {\n  await page.click(\"#drive_disabled\")\n\n  await expect(page).toHaveURL(withPathname(path))\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n"
  },
  {
    "path": "src/tests/functional/drive_view_transition_legacy_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport { nextBody } from \"../helpers/page\"\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/transitions/left_legacy.html\")\n\n  await page.evaluate(`\n    document.startViewTransition = (callback) => {\n      window.startViewTransitionCalled = true\n      callback()\n    }\n  `)\n})\n\ntest(\"navigating triggers the view transition\", async ({ page }) => {\n  await page.locator(\"#go-right\").click()\n  await nextBody(page)\n\n  expect(await page.evaluate(`window.startViewTransitionCalled`)).toEqual(true)\n})\n\ntest(\"navigating does not trigger a view transition when meta tag not present\", async ({ page }) => {\n  await page.locator(\"#go-other\").click()\n  await nextBody(page)\n\n  expect(await page.evaluate(`window.startViewTransitionCalled`)).toEqual(undefined)\n})\n"
  },
  {
    "path": "src/tests/functional/drive_view_transition_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport { nextBody } from \"../helpers/page\"\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/transitions/left.html\")\n\n  await page.evaluate(`\n    document.startViewTransition = (callback) => {\n      window.startViewTransitionCalled = true\n      callback()\n    }\n  `)\n})\n\ntest(\"navigating triggers the view transition\", async ({ page }) => {\n  await page.locator(\"#go-right\").click()\n  await nextBody(page)\n\n  const called = await page.evaluate(`window.startViewTransitionCalled`)\n  expect(called).toEqual(true)\n})\n\ntest(\"navigating does not trigger a view transition when prefers reduced motion is reduce\", async ({ page }) => {\n  await page.emulateMedia({ reducedMotion: 'reduce' })\n  await page.locator(\"#go-right\").click()\n  await nextBody(page)\n\n  const called = await page.evaluate(`window.startViewTransitionCalled`)\n  expect(called).toEqual(undefined)\n})\n\ntest(\"navigating does not trigger a view transition when meta tag not present\", async ({ page }) => {\n  await page.locator(\"#go-other\").click()\n  await nextBody(page)\n\n  const called = await page.evaluate(`window.startViewTransitionCalled`)\n  expect(called).toEqual(undefined)\n})\n"
  },
  {
    "path": "src/tests/functional/form_mode_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport { getFromLocalStorage, setLocalStorageFromEvent } from \"../helpers/page\"\n\ntest(\"form submission with form mode off\", async ({ page }) => {\n  await gotoPageWithFormMode(page, \"off\")\n  await page.click(\"#turbo-enabled-form button\")\n\n  expect(await formSubmitStarted(page)).not.toBeTruthy()\n})\n\ntest(\"form submission without submitter with form mode off\", async ({ page }) => {\n  await gotoPageWithFormMode(page, \"off\")\n  await page.press(\"#turbo-enabled-form-without-submitter [type=text]\", \"Enter\")\n\n  expect(await formSubmitStarted(page)).not.toBeTruthy()\n})\n\ntest(\"form submission with form mode off from submitter outside form\", async ({ page }) => {\n  await gotoPageWithFormMode(page, \"off\")\n  await page.click(\"button[form=turbo-enabled-form]\")\n\n  expect(await formSubmitStarted(page)).not.toBeTruthy()\n})\n\ntest(\"form submission with form mode optin and form not enabled\", async ({ page }) => {\n  await gotoPageWithFormMode(page, \"optin\")\n  await page.click(\"#form button\")\n\n  expect(await formSubmitStarted(page)).not.toBeTruthy()\n})\n\ntest(\"form submission without submitter with form mode optin and form not enabled\", async ({ page }) => {\n  await gotoPageWithFormMode(page, \"optin\")\n  await page.press(\"#form-without-submitter [type=text]\", \"Enter\")\n\n  expect(await formSubmitStarted(page)).not.toBeTruthy()\n})\n\ntest(\"form submission with form mode optin and form not enabled from submitter outside form\", async ({ page }) => {\n  await gotoPageWithFormMode(page, \"optin\")\n  await page.click(\"button[form=form]\")\n\n  expect(await formSubmitStarted(page)).not.toBeTruthy()\n})\n\ntest(\"form submission with form mode optin and form enabled\", async ({ page }) => {\n  await gotoPageWithFormMode(page, \"optin\")\n  await page.click(\"#turbo-enabled-form button\")\n\n  expect(await formSubmitStarted(page)).toBeTruthy()\n})\n\ntest(\"form submission without submitter with form mode optin and form enabled\", async ({ page }) => {\n  await gotoPageWithFormMode(page, \"optin\")\n  await page.press(\"#turbo-enabled-form-without-submitter [type=text]\", \"Enter\")\n\n  expect(await formSubmitStarted(page)).toBeTruthy()\n})\n\ntest(\"form submission with form mode optin and form enabled from submitter outside form\", async ({ page }) => {\n  await gotoPageWithFormMode(page, \"optin\")\n  await page.click(\"button[form=turbo-enabled-form]\")\n\n  expect(await formSubmitStarted(page)).toBeTruthy()\n})\n\nasync function gotoPageWithFormMode(page, formMode) {\n  await page.goto(`/src/tests/fixtures/form_mode.html?formMode=${formMode}`)\n  await setLocalStorageFromEvent(page, \"turbo:submit-start\", \"formSubmitStarted\", \"true\")\n}\n\nfunction formSubmitStarted(page) {\n  return getFromLocalStorage(page, \"formSubmitStarted\")\n}\n"
  },
  {
    "path": "src/tests/functional/form_submission_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport {\n  getFromLocalStorage,\n  getSearchParam,\n  hasSelector,\n  isScrolledToTop,\n  nextAttributeMutationNamed,\n  nextBeat,\n  nextEventNamed,\n  nextEventOnTarget,\n  noNextEventNamed,\n  outerHTMLForSelector,\n  readEventLogs,\n  scrollToSelector,\n  setLocalStorageFromEvent,\n  visitAction,\n  withPathname,\n  withSearch,\n  withSearchParam\n} from \"../helpers/page\"\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/form.html\")\n  await setLocalStorageFromEvent(page, \"turbo:submit-start\", \"formSubmitStarted\", \"true\")\n  await setLocalStorageFromEvent(page, \"turbo:submit-end\", \"formSubmitEnded\", \"true\")\n  await readEventLogs(page)\n})\n\ntest(\"standard form submission renders a progress bar\", async ({ page }) => {\n  await page.evaluate(() => window.Turbo.setProgressBarDelay(0))\n  await page.click(\"#standard form.sleep input[type=submit]\")\n\n  await expect(page.locator(\".turbo-progress-bar\"), \"displays progress bar\").toBeAttached()\n\n  await nextEventNamed(page, \"turbo:load\")\n  await expect(page.locator(\".turbo-progress-bar\"), \"hides progress bar\").not.toBeAttached()\n})\n\ntest(\"form submission with confirmation confirmed\", async ({ page }) => {\n  page.on(\"dialog\", (alert) => {\n    expect(alert.message()).toEqual(\"Are you sure?\")\n    alert.accept()\n  })\n\n  await page.click(\"#standard form.confirm input[type=submit]\")\n\n  await nextEventNamed(page, \"turbo:load\")\n  expect(await formSubmitStarted(page)).toEqual(\"true\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n})\n\ntest(\"form submission with confirmation cancelled\", async ({ page }) => {\n  page.on(\"dialog\", (alert) => {\n    expect(alert.message()).toEqual(\"Are you sure?\")\n    alert.dismiss()\n  })\n  await page.click(\"#standard form.confirm input[type=submit]\")\n\n  expect(await formSubmitStarted(page)).toEqual(null)\n})\n\ntest(\"form submission with secondary submitter click - confirmation confirmed\", async ({ page }) => {\n  page.on(\"dialog\", (alert) => {\n    expect(alert.message()).toEqual(\"Are you really sure?\")\n    alert.accept()\n  })\n\n  await page.click(\"#standard form.confirm #secondary_submitter\")\n\n  await nextEventNamed(page, \"turbo:load\")\n  expect(await formSubmitStarted(page)).toEqual(\"true\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"advance\")\n  await expect(page).toHaveURL(withSearchParam(\"greeting\", \"secondary_submitter\"))\n})\n\ntest(\"form submission with secondary submitter click - confirmation cancelled\", async ({ page }) => {\n  page.on(\"dialog\", (alert) => {\n    expect(alert.message()).toEqual(\"Are you really sure?\")\n    alert.dismiss()\n  })\n\n  await page.click(\"#standard form.confirm #secondary_submitter\")\n\n  expect(await formSubmitStarted(page)).toEqual(null)\n})\n\ntest(\"from submission with confirmation overridden\", async ({ page }) => {\n  page.on(\"dialog\", (alert) => {\n    expect(alert.message()).toEqual(\"Overridden message\")\n    alert.accept()\n  })\n\n  await page.evaluate(() => window.Turbo.setConfirmMethod(() => Promise.resolve(confirm(\"Overridden message\"))))\n  await page.click(\"#standard form.confirm input[type=submit]\")\n\n  expect(await formSubmitStarted(page)).toEqual(\"true\")\n})\n\ntest(\"standard form submission does not render a progress bar before expiring the delay\", async ({ page }) => {\n  await page.evaluate(() => window.Turbo.setProgressBarDelay(500))\n  await page.click(\"#standard form.redirect input[type=submit]\")\n\n  await expect(page.locator(\".turbo-progress-bar\"), \"does not show progress bar before delay\").not.toBeAttached()\n})\n\ntest(\"standard POST form submission with redirect response\", async ({ page }) => {\n  await page.click(\"#standard form.redirect input[type=submit]\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"greeting\", \"Hello from a redirect\"))\n  expect(await formSubmitStarted(page)).toEqual(\"true\")\n  expect(await visitAction(page)).toEqual(\"advance\")\n  expect(\n    await nextAttributeMutationNamed(page, \"html\", \"aria-busy\"),\n    \"sets [aria-busy] on the document element\"\n  ).toEqual(\"true\")\n  expect(\n    await nextAttributeMutationNamed(page, \"html\", \"aria-busy\"),\n    \"removes [aria-busy] from the document element\"\n  ).toEqual(null)\n})\n\n\ntest(\"sets aria-busy on the form element during a form submission\", async ({ page }) => {\n  await page.click(\"#standard form.redirect input[type=submit]\")\n\n  await nextEventNamed(page, \"turbo:submit-start\")\n  expect(\n    await nextAttributeMutationNamed(page, \"standard-form\", \"aria-busy\"),\n    \"sets [aria-busy] on the form element\"\n  ).toEqual(\"true\")\n\n  await nextEventNamed(page, \"turbo:submit-end\")\n  expect(\n    await nextAttributeMutationNamed(page, \"standard-form\", \"aria-busy\"),\n    \"removes [aria-busy] from the form element\"\n  ).toEqual(null)\n})\n\ntest(\"standard POST form submission events\", async ({ page }) => {\n  await page.click(\"#standard-post-form-submit\")\n\n  expect(await formSubmitStarted(page), \"fires turbo:submit-start\").toEqual(\"true\")\n\n  const { fetchOptions } = await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  expect(fetchOptions.headers[\"Accept\"]).toContain(\"text/vnd.turbo-stream.html\")\n\n  await nextEventNamed(page, \"turbo:before-fetch-response\")\n\n  expect(await formSubmitEnded(page), \"fires turbo:submit-end\").toEqual(\"true\")\n\n  await nextEventNamed(page, \"turbo:before-visit\")\n  await nextEventNamed(page, \"turbo:visit\")\n  await nextEventNamed(page, \"turbo:before-render\")\n  await nextEventNamed(page, \"turbo:render\")\n  await nextEventNamed(page, \"turbo:load\")\n})\n\ntest(\"supports transforming a POST submission to a GET in a turbo:submit-start listener\", async ({ page }) => {\n  await page.evaluate(() =>\n    addEventListener(\"turbo:submit-start\", (({ detail }) => {\n      detail.formSubmission.method = \"get\"\n      detail.formSubmission.action = \"/src/tests/fixtures/one.html\"\n      detail.formSubmission.body.set(\"greeting\", \"Hello, from an event listener\")\n    }))\n  )\n  await page.click(\"#standard form[method=post] [type=submit]\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"h1\"), \"overrides the method and action\").toHaveText(\"One\")\n  await expect(page).toHaveURL(withSearchParam(\"greeting\", \"Hello, from an event listener\"))\n})\n\ntest(\"supports transforming a GET submission to a POST in a turbo:submit-start listener\", async ({ page }) => {\n  await page.evaluate(() =>\n    addEventListener(\"turbo:submit-start\", (({ detail }) => {\n      detail.formSubmission.method = \"POST\"\n      detail.formSubmission.body.set(\"path\", \"/src/tests/fixtures/one.html\")\n      detail.formSubmission.body.set(\"greeting\", \"Hello, from an event listener\")\n    }))\n  )\n  await page.click(\"#standard form[method=get] [type=submit]\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"h1\"), \"overrides the method and action\").toHaveText(\"One\")\n  await expect(page).toHaveURL(withSearchParam(\"greeting\", \"Hello, from an event listener\"))\n})\n\ntest(\"supports modifying the submission in a turbo:before-fetch-request listener\", async ({ page }) => {\n  await page.evaluate(() =>\n    addEventListener(\"turbo:before-fetch-request\", (({ detail }) => {\n      detail.url = new URL(\"/src/tests/fixtures/one.html\", document.baseURI)\n      detail.url.search = new URLSearchParams(detail.fetchOptions.body).toString()\n      detail.fetchOptions.body = null\n      detail.fetchOptions.method = \"get\"\n    }))\n  )\n  await page.click(\"#standard form[method=post] [type=submit]\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"h1\"), \"overrides the method and action\").toHaveText(\"One\")\n  await expect(page).toHaveURL(withSearchParam(\"greeting\", \"Hello from a redirect\"))\n})\n\ntest(\"standard POST form submission merges values from both searchParams and body\", async ({ page }) => {\n  await page.click(\"#form-action-post-redirect-self-q-b\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"q\", \"b\"))\n  await expect(page).toHaveURL(withSearchParam(\"sort\", \"asc\"))\n})\n\ntest(\"standard POST form submission toggles submitter [disabled] attribute\", async ({ page }) => {\n  await page.click(\"#standard-post-form-submit\")\n\n  expect(\n    await nextAttributeMutationNamed(page, \"standard-post-form-submit\", \"disabled\"),\n    \"sets [disabled] on the submitter\"\n  ).toEqual(\"\")\n  expect(\n    await nextAttributeMutationNamed(page, \"standard-post-form-submit\", \"disabled\"),\n    \"removes [disabled] from the submitter\"\n  ).toEqual(null)\n})\n\ntest(\"standard POST form submission toggles submitter [aria-disabled=true] attribute\", async ({ page }) => {\n  await page.evaluate(() => window.Turbo.config.forms.submitter = \"aria-disabled\")\n  await page.click(\"#standard-post-form-submit\")\n\n  expect(\n    await nextAttributeMutationNamed(page, \"standard-post-form-submit\", \"aria-disabled\"),\n    \"sets [aria-disabled=true] on the submitter\"\n  ).toEqual(\"true\")\n  expect(\n    await nextAttributeMutationNamed(page, \"standard-post-form-submit\", \"aria-disabled\"),\n    \"removes [aria-disabled] from the submitter\"\n  ).toEqual(null)\n})\n\ntest(\"replaces input value with data-turbo-submits-with on form submission\", async ({ page }) => {\n  page.click(\"#submits-with-form-input\")\n\n  expect(\n    await nextAttributeMutationNamed(page, \"submits-with-form-input\", \"value\"),\n    \"sets data-turbo-submits-with on the submitter\"\n  ).toEqual(\"Saving...\")\n\n  expect(\n    await nextAttributeMutationNamed(page, \"submits-with-form-input\", \"value\"),\n    \"restores the original submitter text value\"\n  ).toEqual(\"Save\")\n})\n\ntest(\"replaces button innerHTML with data-turbo-submits-with on form submission\", async ({ page }) => {\n  await page.click(\"#submits-with-form-button\")\n\n  await nextEventNamed(page, \"turbo:submit-start\")\n  await expect(\n    page.locator(\"#submits-with-form-button\"),\n    \"sets data-turbo-submits-with on the submitter\"\n  ).toHaveText(\"Saving...\")\n\n  await nextEventNamed(page, \"turbo:submit-end\")\n  await expect(\n    page.locator(\"#submits-with-form-button\"),\n    \"sets data-turbo-submits-with on the submitter\"\n  ).toHaveText(\"Save\")\n})\n\ntest(\"standard GET form submission\", async ({ page }) => {\n  await page.click(\"#standard form.greeting input[type=submit]\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"greeting\", \"Hello from a form\"))\n  expect(await formSubmitStarted(page)).toEqual(\"true\")\n  expect(await visitAction(page)).toEqual(\"advance\")\n  expect(\n    await nextAttributeMutationNamed(page, \"html\", \"aria-busy\"),\n    \"sets [aria-busy] on the document element\"\n  ).toEqual(\"true\")\n  expect(\n    await nextAttributeMutationNamed(page, \"html\", \"aria-busy\"),\n    \"removes [aria-busy] from the document element\"\n  ).toEqual(null)\n})\n\ntest(\"standard GET HTMLFormElement.requestSubmit() with Turbo Action\", async ({ page }) => {\n  await page.evaluate(() => {\n    const formControl = document.querySelector(\"#external-select\")\n\n    if (formControl && formControl.form) formControl.form.requestSubmit()\n  })\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"h1\"), \"Retains original page state\").toHaveText(\"Form\")\n  await expect(page.locator(\"#hello h2\"), \"navigates #hello turbo frame\").toHaveText(\"Hello from a frame\")\n  expect(await visitAction(page), \"reads Turbo Action from <form>\").toEqual(\"replace\")\n  await expect(page, \"promotes frame navigation to page Visit\").toHaveURL(withPathname(\"/src/tests/fixtures/frames/hello.html\"))\n  await expect(page, \"encodes <form> into request\").toHaveURL(withSearchParam(\"greeting\", \"Hello from a replace Visit\"))\n})\n\ntest(\"GET HTMLFormElement.requestSubmit() triggered by javascript\", async ({ page }) => {\n  await page.click(\"#request-submit-trigger\")\n\n  await expect(page.locator(\"#hello h2\"), \"navigates #hello turbo frame\").toHaveText(\"Hello from a frame\")\n  await expect(page, \"SubmitEvent was triggered without a submitter\").not.toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n})\n\ntest(\"standard GET form submission with [data-turbo-stream] declared on the form\", async ({ page }) => {\n  await page.click(\"#standard-get-form-with-stream-opt-in-submit\")\n\n  const { fetchOptions } = await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  expect(fetchOptions.headers[\"Accept\"]).toContain(\"text/vnd.turbo-stream.html\")\n})\n\ntest(\"standard GET form submission with [data-turbo-stream] declared on submitter\", async ({ page }) => {\n  await page.click(\"#standard-get-form-with-stream-opt-in-submitter\")\n\n  const { fetchOptions } = await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  expect(fetchOptions.headers[\"Accept\"]).toContain(\"text/vnd.turbo-stream.html\")\n})\n\ntest(\"standard GET form submission events\", async ({ page }) => {\n  await page.click(\"#standard-get-form-submit\")\n\n  expect(await formSubmitStarted(page), \"fires turbo:submit-start\").toEqual(\"true\")\n\n  const { fetchOptions } = await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  expect(fetchOptions.headers[\"Accept\"]).not.toContain(\"text/vnd.turbo-stream.html\")\n\n  await nextEventNamed(page, \"turbo:before-fetch-response\")\n\n  expect(await formSubmitEnded(page), \"fires turbo:submit-end\").toEqual(\"true\")\n\n  await nextEventNamed(page, \"turbo:before-visit\")\n  await nextEventNamed(page, \"turbo:visit\")\n  await nextEventNamed(page, \"turbo:before-cache\")\n  await nextEventNamed(page, \"turbo:before-render\")\n  await nextEventNamed(page, \"turbo:render\")\n  await nextEventNamed(page, \"turbo:load\")\n})\n\ntest(\"standard GET form submission does not incorporate the current page's URLSearchParams values into the submission\", async ({\n  page\n}) => {\n  await page.click(\"#form-action-self-sort\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"sort\", \"asc\"))\n\n  await page.click(\"#form-action-none-q-a\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page, \"navigates without omitted keys\").toHaveURL(withSearchParam(\"q\", \"a\"))\n})\n\ntest(\"standard GET form submission does not merge values into the [action] attribute\", async ({ page }) => {\n  await page.click(\"#form-action-self-sort\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"sort\", \"asc\"))\n\n  await page.click(\"#form-action-self-q-b\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page, \"navigates without omitted keys\").toHaveURL(withSearchParam(\"q\", \"b\"))\n})\n\ntest(\"standard GET form submission omits the [action] value's URLSearchParams from the submission\", async ({\n  page\n}) => {\n  await page.click(\"#form-action-self-submit\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page, \"navigates without omitted keys\").toHaveURL(withSearch(\"\"))\n})\n\ntest(\"standard GET form submission toggles submitter [disabled] attribute\", async ({ page }) => {\n  await page.click(\"#standard-get-form-submit\")\n\n  expect(\n    await nextAttributeMutationNamed(page, \"standard-get-form-submit\", \"disabled\"),\n    \"sets [disabled] on the submitter\"\n  ).toEqual(\"\")\n  expect(\n    await nextAttributeMutationNamed(page, \"standard-get-form-submit\", \"disabled\"),\n    \"removes [disabled] from the submitter\"\n  ).toEqual(null)\n})\n\ntest(\"standard GET form submission toggles submitter [aria-disabled] attribute\", async ({ page }) => {\n  await page.evaluate(() => window.Turbo.config.forms.submitter = \"aria-disabled\")\n  await page.click(\"#standard-get-form-submit\")\n\n  expect(\n    await nextAttributeMutationNamed(page, \"standard-get-form-submit\", \"aria-disabled\"),\n    \"sets [aria-disabled] on the submitter\"\n  ).toEqual(\"true\")\n  expect(\n    await nextAttributeMutationNamed(page, \"standard-get-form-submit\", \"aria-disabled\"),\n    \"removes [aria-disabled] from the submitter\"\n  ).toEqual(null)\n})\n\ntest(\"standard GET form submission appending keys\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/form.html?query=1\")\n  await page.click(\"#standard form.conflicting-values input[type=submit]\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"query\", \"2\"))\n})\n\ntest(\"standard form submission with empty created response\", async ({ page }) => {\n  const htmlBefore = await outerHTMLForSelector(page, \"body\")\n  const button = await page.locator(\"#standard form.created input[type=submit]\")\n  await button.click()\n  await nextBeat()\n\n  const htmlAfter = await outerHTMLForSelector(page, \"body\")\n  expect(htmlAfter).toEqual(htmlBefore)\n})\n\ntest(\"standard form submission with empty no-content response\", async ({ page }) => {\n  const htmlBefore = await outerHTMLForSelector(page, \"body\")\n  const button = await page.locator(\"#standard form.no-content input[type=submit]\")\n  await button.click()\n  await nextBeat()\n\n  const htmlAfter = await outerHTMLForSelector(page, \"body\")\n  expect(htmlAfter).toEqual(htmlBefore)\n})\n\ntest(\"standard POST form submission with multipart/form-data enctype\", async ({ page }) => {\n  await page.click(\"#standard form[method=post][enctype] input[type=submit]\")\n\n  await expect(\n    page,\n    \"submits a multipart/form-data request\"\n  ).toHaveURL((url) => {\n    const enctype = url.searchParams.get(\"enctype\")\n    return enctype?.startsWith(\"multipart/form-data\")\n  })\n})\n\ntest(\"standard GET form submission ignores enctype\", async ({ page }) => {\n  await page.click(\"#standard form[method=get][enctype] input[type=submit]\")\n\n  await expect(page, \"GET form submissions ignore enctype\").not.toHaveURL(url => url.searchParams.has(\"enctype\"))\n})\n\ntest(\"standard POST form submission without an enctype\", async ({ page }) => {\n  await page.click(\"#standard form[method=post].no-enctype input[type=submit]\")\n\n  await expect(\n    page,\n    \"submits a application/x-www-form-urlencoded request\"\n  ).toHaveURL((url) => {\n    const enctype = url.searchParams.get(\"enctype\")\n    return enctype?.startsWith(\"application/x-www-form-urlencoded\")\n  })\n})\n\ntest(\"no-action form submission with single parameter\", async ({ page }) => {\n  await page.click(\"#no-action form.single input[type=submit]\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"query\", \"1\"))\n\n  await page.click(\"#no-action form.single input[type=submit]\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"query\", \"1\"))\n\n  await page.goto(\"/src/tests/fixtures/form.html?query=2\")\n  await page.click(\"#no-action form.single input[type=submit]\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"query\", \"1\"))\n})\n\ntest(\"no-action form submission with multiple parameters\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/form.html?query=2\")\n  await page.click(\"#no-action form.multiple input[type=submit]\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"query\", [\"1\", \"2\"]))\n  await nextEventNamed(page, \"turbo:load\")\n\n  await page.click(\"#no-action form.multiple input[type=submit]\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"query\", [\"1\", \"2\"]))\n})\n\ntest(\"no-action form submission submitter parameters\", async ({ page }) => {\n  await page.click(\"#no-action form.button-param [type=submit]\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"query\", \"1\"))\n  await expect(page).toHaveURL(withSearchParam(\"button\", [\"\"]))\n\n  await page.click(\"#no-action form.button-param [type=submit]\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"query\", \"1\"))\n  await expect(page).toHaveURL(withSearchParam(\"button\", [\"\"]))\n})\n\ntest(\"submitter with blank formaction submits to the current page\", async ({ page }) => {\n  await page.click(\"#blank-formaction button\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page.locator(\"#blank-formaction\"), \"overrides form[action] navigation\").toBeAttached()\n})\n\ntest(\"input named action with no action attribute\", async ({ page }) => {\n  await page.click(\"#action-input form.no-action [type=submit]\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"action\", \"1\"))\n  await expect(page).toHaveURL(withSearchParam(\"query\", \"1\"))\n})\n\ntest(\"input named action with action attribute\", async ({ page }) => {\n  await page.click(\"#action-input form.action [type=submit]\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"action\", \"1\"))\n  await expect(page).toHaveURL(withSearchParam(\"query\", \"1\"))\n})\n\ntest(\"invalid form submission with unprocessable content status\", async ({ page }) => {\n  await page.click(\"#reject form.unprocessable_content input[type=submit]\")\n\n  await expect(page.locator(\"h1\"), \"renders the response HTML\").toHaveText(\"Unprocessable Content\")\n  await expect(page.locator(\"#frame form.reject\"), \"replaces entire page\").not.toBeAttached()\n})\n\ntest(\"invalid form submission with long form\", async ({ page }) => {\n  await scrollToSelector(page, \"#reject form.unprocessable_content_with_tall_form input[type=submit]\")\n  await page.click(\"#reject form.unprocessable_content_with_tall_form input[type=submit]\")\n\n  await expect(page.locator(\"h1\"), \"renders the response HTML\").toHaveText(\"Unprocessable Content\")\n  expect(await isScrolledToTop(page), \"page is scrolled to the top\").toBeTruthy()\n  await expect(page.locator(\"#frame form.reject\"), \"replaces entire page\").not.toBeAttached()\n})\n\ntest(\"invalid form submission with server error status\", async ({ page }) => {\n  await expect(page.locator(\"head > #form-fixture-styles\")).toBeAttached()\n  await page.click(\"#reject form.internal_server_error input[type=submit]\")\n\n  await expect(page.locator(\"h1\"), \"renders the response HTML\").toHaveText(\"Internal Server Error\")\n  await expect(page.locator(\"head > #form-fixture-styles\"), \"replaces head\").not.toBeAttached()\n  await expect(page.locator(\"#frame form.reject\"), \"replaces entire page\").not.toBeAttached()\n})\n\ntest(\"form submission with network error\", async ({ page }) => {\n  await page.context().setOffline(true)\n  await page.click(\"#reject-form [type=submit]\")\n  await nextEventOnTarget(page, \"reject-form\", \"turbo:fetch-request-error\")\n})\n\ntest(\"submitter form submission reads button attributes\", async ({ page }) => {\n  const button = await page.locator(\"#submitter form button[type=submit][formmethod=post]\")\n  await button.click()\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/two.html\"))\n  expect(await visitAction(page)).toEqual(\"advance\")\n})\n\ntest(\"submitter POST form submission with multipart/form-data formenctype\", async ({ page }) => {\n  await page.click(\"#submitter form[method=post]:not([enctype]) input[formenctype]\")\n\n  await expect(\n    page,\n    \"submits a multipart/form-data request\"\n  ).toHaveURL((url) => {\n    const enctype = url.searchParams.get(\"enctype\")\n    return enctype?.startsWith(\"multipart/form-data\")\n  })\n})\n\ntest(\"submitter GET submission from submitter with data-turbo-frame\", async ({ page }) => {\n  await page.click(\"#submitter form[method=get] [type=submit][data-turbo-frame]\")\n\n  await expect(page.locator(\"#frame div.message\")).toHaveText(\"Frame redirected\")\n  await expect(page.locator(\"h1\")).toHaveText(\"Form\")\n})\n\ntest(\"submitter POST submission from submitter with data-turbo-frame\", async ({ page }) => {\n  await page.click(\"#submitter form[method=post] [type=submit][data-turbo-frame]\")\n\n  await expect(page.locator(\"#frame div.message\")).toHaveText(\"Frame redirected\")\n  await expect(page.locator(\"h1\")).toHaveText(\"Form\")\n})\n\ntest(\"form[data-turbo-frame=_top] submission\", async ({ page }) => {\n  const form = await page.locator(\"#standard form.redirect[data-turbo-frame=_top]\")\n\n  await form.locator(\"button\").click()\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"One\")\n})\n\ntest(\"form[data-turbo-frame=_top] submission within frame\", async ({ page }) => {\n  const frame = await page.locator(\"turbo-frame#frame\")\n  const form = await frame.locator(\"form.redirect[data-turbo-frame=_top]\")\n\n  await form.locator(\"button\").click()\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"Frames: Form\")\n})\n\ntest(\"frame form GET submission from submitter with data-turbo-frame=_top\", async ({ page }) => {\n  await page.click(\"#frame form[method=get] [type=submit][data-turbo-frame=_top]\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"One\")\n})\n\ntest(\"frame form POST submission from submitter with data-turbo-frame=_top\", async ({ page }) => {\n  await page.click(\"#frame form[method=post] [type=submit][data-turbo-frame=_top]\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"One\")\n})\n\ntest(\"frame POST form targeting frame submission\", async ({ page }) => {\n  await page.click(\"#targets-frame-post-form-submit\")\n\n  expect(await formSubmitStarted(page), \"fires turbo:submit-start\").toEqual(\"true\")\n\n  const { fetchOptions } = await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  expect(fetchOptions.headers[\"Accept\"]).toContain(\"text/vnd.turbo-stream.html\")\n  expect(\"frame\").toEqual(fetchOptions.headers[\"Turbo-Frame\"])\n\n  await nextEventNamed(page, \"turbo:before-fetch-response\")\n\n  expect(await formSubmitEnded(page), \"fires turbo:submit-end\").toEqual(\"true\")\n\n  await nextEventNamed(page, \"turbo:frame-render\")\n  await nextEventNamed(page, \"turbo:frame-load\")\n\n  const otherEvents = await readEventLogs(page)\n  expect(otherEvents.length, \"no more events\").toEqual(0)\n\n  const src = (await page.getAttribute(\"#frame\", \"src\")) || \"\"\n  expect(new URL(src).pathname).toEqual(\"/src/tests/fixtures/frames/frame.html\")\n})\n\ntest(\"frame POST form targeting frame toggles submitter's [disabled] attribute\", async ({ page }) => {\n  await page.click(\"#targets-frame-post-form-submit\")\n\n  expect(\n    await nextAttributeMutationNamed(page, \"targets-frame-post-form-submit\", \"disabled\"),\n    \"sets [disabled] on the submitter\"\n  ).toEqual(\"\")\n  expect(\n    await nextAttributeMutationNamed(page, \"targets-frame-post-form-submit\", \"disabled\"),\n    \"removes [disabled] from the submitter\"\n  ).toEqual(null)\n})\n\ntest(\"frame POST form targeting frame toggles submitter's [aria-disabled] attribute\", async ({ page }) => {\n  await page.evaluate(() => window.Turbo.config.forms.submitter = \"aria-disabled\")\n  await page.click(\"#targets-frame-post-form-submit\")\n\n  expect(\n    await nextAttributeMutationNamed(page, \"targets-frame-post-form-submit\", \"aria-disabled\"),\n    \"sets [aria-disabled] on the submitter\"\n  ).toEqual(\"true\")\n  expect(\n    await nextAttributeMutationNamed(page, \"targets-frame-post-form-submit\", \"aria-disabled\"),\n    \"removes [aria-disabled] from the submitter\"\n  ).toEqual(null)\n})\n\ntest(\"frame GET form targeting frame submission\", async ({ page }) => {\n  await page.click(\"#targets-frame-get-form-submit\")\n\n  expect(await formSubmitStarted(page), \"fires turbo:submit-start\").toEqual(\"true\")\n\n  const { fetchOptions } = await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  expect(fetchOptions.headers[\"Accept\"]).not.toContain(\"text/vnd.turbo-stream.html\")\n  expect(fetchOptions.headers[\"Turbo-Frame\"]).toEqual(\"frame\")\n\n  await nextEventNamed(page, \"turbo:before-fetch-response\")\n\n  expect(await formSubmitEnded(page), \"fires turbo:submit-end\").toEqual(\"true\")\n\n  await nextEventNamed(page, \"turbo:frame-render\")\n  await nextEventNamed(page, \"turbo:frame-load\")\n\n  const otherEvents = await readEventLogs(page)\n  expect(otherEvents.length, \"no more events\").toEqual(0)\n\n  const src = (await page.getAttribute(\"#frame\", \"src\")) || \"\"\n  expect(new URL(src).pathname).toEqual(\"/src/tests/fixtures/frames/frame.html\")\n})\n\ntest(\"frame GET form targeting frame toggles submitter's [disabled] attribute\", async ({ page }) => {\n  await page.click(\"#targets-frame-get-form-submit\")\n\n  expect(\n    await nextAttributeMutationNamed(page, \"targets-frame-get-form-submit\", \"disabled\"),\n    \"sets [disabled] on the submitter\"\n  ).toEqual(\"\")\n  expect(\n    await nextAttributeMutationNamed(page, \"targets-frame-get-form-submit\", \"disabled\"),\n    \"removes [disabled] from the submitter\"\n  ).toEqual(null)\n})\n\ntest(\"frame GET form targeting frame toggles submitter's [aria-disabled] attribute\", async ({ page }) => {\n  await page.evaluate(() => window.Turbo.config.forms.submitter = \"aria-disabled\")\n  await page.click(\"#targets-frame-get-form-submit\")\n\n  expect(\n    await nextAttributeMutationNamed(page, \"targets-frame-get-form-submit\", \"aria-disabled\"),\n    \"sets [aria-disabled] on the submitter\"\n  ).toEqual(\"true\")\n  expect(\n    await nextAttributeMutationNamed(page, \"targets-frame-get-form-submit\", \"aria-disabled\"),\n    \"removes [aria-disabled] from the submitter\"\n  ).toEqual(null)\n})\n\ntest(\"frame form GET submission from submitter referencing another frame\", async ({ page }) => {\n  await page.click(\"#frame form[method=get] [type=submit][data-turbo-frame=hello]\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"Form\")\n  await expect(page.locator(\"#hello h2\")).toHaveText(\"Hello from a frame\")\n})\n\ntest(\"frame form POST submission from submitter referencing another frame\", async ({ page }) => {\n  await page.click(\"#frame form[method=post] [type=submit][data-turbo-frame=hello]\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"Form\")\n  await expect(page.locator(\"#hello h2\")).toHaveText(\"Hello from a frame\")\n})\n\ntest(\"frame form submission with redirect response\", async ({ page }) => {\n  const path = (await page.getAttribute(\"#frame form.redirect input[name=path]\", \"value\")) || \"\"\n  const url = new URL(path, \"http://localhost:9000\")\n  url.searchParams.set(\"enctype\", \"application/x-www-form-urlencoded;charset=UTF-8\")\n\n  const button = await page.locator(\"#frame form.redirect input[type=submit]\")\n  await button.click()\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")\n\n  await expect(page.locator(\"#frame form.redirect\")).not.toBeAttached()\n  await expect(page.locator(\"#frame div.message\")).toHaveText(\"Frame redirected\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page, \"does not redirect _top\").toHaveURL(withSearch(\"\"))\n  await expect(page.locator(\"#frame\"), \"redirects the target frame\").toHaveAttribute(\"src\", url.href)\n})\n\ntest(\"frame POST form submission toggles the ancestor frame's [aria-busy] attribute\", async ({ page }) => {\n  await page.click(\"#frame form.redirect input[type=submit]\")\n\n  expect(await nextAttributeMutationNamed(page, \"frame\", \"busy\"), \"sets [busy] on the #frame\").toEqual(\"\")\n  expect(await nextAttributeMutationNamed(page, \"frame\", \"aria-busy\"), \"sets [aria-busy] on the #frame\").toEqual(\"true\")\n  expect(await nextAttributeMutationNamed(page, \"frame\", \"busy\"), \"removes [busy] from the #frame\").toEqual(null)\n  expect(\n    await nextAttributeMutationNamed(page, \"frame\", \"aria-busy\"),\n    \"removes [aria-busy] from the #frame\"\n  ).toEqual(null)\n})\n\ntest(\"frame POST form submission toggles the target frame's [aria-busy] attribute\", async ({ page }) => {\n  await page.click('#targets-frame form.frame [type=\"submit\"]')\n\n  expect(await nextAttributeMutationNamed(page, \"frame\", \"busy\"), \"sets [busy] on the #frame\").toEqual(\"\")\n  expect(await nextAttributeMutationNamed(page, \"frame\", \"aria-busy\"), \"sets [aria-busy] on the #frame\").toEqual(\"true\")\n\n  await expect(page.locator(\"#frame h2\")).toHaveText(\"Frame: Loaded\")\n  expect(await nextAttributeMutationNamed(page, \"frame\", \"busy\"), \"removes [busy] from the #frame\").toEqual(null)\n  expect(\n    await nextAttributeMutationNamed(page, \"frame\", \"aria-busy\"),\n    \"removes [aria-busy] from the #frame\"\n  ).toEqual(null)\n})\n\ntest(\"frame form submission with empty created response\", async ({ page }) => {\n  const htmlBefore = await outerHTMLForSelector(page, \"#frame\")\n  const button = await page.locator(\"#frame form.created input[type=submit]\")\n  await button.click()\n  await nextBeat()\n\n  const htmlAfter = await outerHTMLForSelector(page, \"#frame\")\n  expect(htmlAfter).toEqual(htmlBefore)\n})\n\ntest(\"frame form submission with empty no-content response\", async ({ page }) => {\n  const htmlBefore = await outerHTMLForSelector(page, \"#frame\")\n  const button = await page.locator(\"#frame form.no-content input[type=submit]\")\n  await button.click()\n  await nextBeat()\n\n  const htmlAfter = await outerHTMLForSelector(page, \"#frame\")\n  expect(htmlAfter).toEqual(htmlBefore)\n})\n\ntest(\"frame form submission within a frame submits the Turbo-Frame header\", async ({ page }) => {\n  await page.click(\"#frame form.redirect input[type=submit]\")\n\n  const { fetchOptions } = await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  expect(fetchOptions.headers[\"Turbo-Frame\"], \"submits with the Turbo-Frame header\").toBeTruthy()\n})\n\ntest(\"invalid frame form submission with unprocessable content status\", async ({ page }) => {\n  await page.click(\"#frame form.unprocessable_content input[type=submit]\")\n\n  expect(await formSubmitStarted(page), \"fires turbo:submit-start\").toEqual(\"true\")\n  await nextEventNamed(page, \"turbo:before-fetch-request\")\n  await nextEventNamed(page, \"turbo:before-fetch-response\")\n  expect(await formSubmitEnded(page), \"fires turbo:submit-end\").toEqual(\"true\")\n  await nextEventNamed(page, \"turbo:frame-render\")\n  await nextEventNamed(page, \"turbo:frame-load\")\n\n  const otherEvents = await readEventLogs(page)\n  expect(otherEvents.length, \"no more events\").toEqual(0)\n\n  expect(await hasSelector(page, \"#reject form\"), \"only replaces frame\").toBeTruthy()\n  await expect(page.locator(\"#frame h2\")).toHaveText(\"Frame: Unprocessable Content\")\n})\n\ntest(\"invalid frame form submission with internal server error status\", async ({ page }) => {\n  await page.click(\"#frame form.internal_server_error input[type=submit]\")\n\n  expect(await formSubmitStarted(page), \"fires turbo:submit-start\").toEqual(\"true\")\n  await nextEventNamed(page, \"turbo:before-fetch-request\")\n  await nextEventNamed(page, \"turbo:before-fetch-response\")\n  expect(await formSubmitEnded(page), \"fires turbo:submit-end\").toEqual(\"true\")\n  await nextEventNamed(page, \"turbo:frame-render\")\n  await nextEventNamed(page, \"turbo:frame-load\")\n\n  const otherEvents = await readEventLogs(page)\n  expect(otherEvents.length, \"no more events\").toEqual(0)\n\n  expect(await hasSelector(page, \"#reject form\"), \"only replaces frame\").toBeTruthy()\n  await expect(page.locator(\"#frame h2\")).toHaveText(\"Frame: Internal Server Error\")\n})\n\ntest(\"frame form submission with stream response\", async ({ page }) => {\n  const button = await page.locator(\"#frame form.stream[method=post] input[type=submit]\")\n  await button.click()\n\n  await expect(page.locator(\"#frame div.message\")).toHaveText(\"Hello!\")\n  await expect(page.locator(\"#frame form.redirect\").first()).toBeAttached()\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  await expect(page.locator(\"#frame\"), \"does not change frame's src\").not.toHaveAttribute(\"src\")\n})\n\ntest(\"frame form submission with HTTP verb other than GET or POST\", async ({ page }) => {\n  await page.click(\"#frame form.put.stream input[type=submit]\")\n\n  await expect(page.locator(\"#frame div.message\")).toHaveText(\"1: Hello!\")\n  await expect(page.locator(\"#frame form.redirect\").first()).toBeAttached()\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n})\n\ntest(\"frame form submission with [data-turbo=false] on the form\", async ({ page }) => {\n  await page.click('#frame form[data-turbo=\"false\"] input[type=submit]')\n\n  await expect(page.locator(\"#element-id\")).toBeAttached()\n  expect(await formSubmitStarted(page)).toEqual(null)\n})\n\ntest(\"frame form submission with [data-turbo=false] on the submitter\", async ({ page }) => {\n  await page.click('#frame form:not([data-turbo]) input[data-turbo=\"false\"]')\n\n  await expect(page.locator(\"#element-id\")).toBeAttached()\n  expect(await formSubmitStarted(page)).toEqual(null)\n})\n\ntest(\"frame form submission ignores submissions with their defaultPrevented\", async ({ page }) => {\n  await page.evaluate(() => document.addEventListener(\"submit\", (event) => event.preventDefault(), true))\n  await page.click(\"#frame .redirect [type=submit]\")\n\n  await expect(page.locator(\"#frame h2\")).toHaveText(\"Frame: Form\")\n  await expect(page.locator(\"#frame\"), \"does not navigate frame\").not.toHaveAttribute(\"src\")\n})\n\ntest(\"form submission with [data-turbo=false] on the form\", async ({ page }) => {\n  await page.click('#turbo-false form[data-turbo=\"false\"] input[type=submit]')\n\n  await expect(page.locator(\"#element-id\")).toBeAttached()\n  expect(await formSubmitStarted(page)).toEqual(null)\n})\n\ntest(\"form submission with [data-turbo=false] on the submitter\", async ({ page }) => {\n  await page.click('#turbo-false form:not([data-turbo]) input[data-turbo=\"false\"]')\n\n  await expect(page.locator(\"#element-id\")).toBeAttached()\n  expect(await formSubmitStarted(page)).toEqual(null)\n})\n\ntest(\"form submission skipped within method=dialog\", async ({ page }) => {\n  const dialog = page.locator(\"#dialog-method\")\n  await dialog.click('[type=\"submit\"]')\n\n  await expect(dialog).not.toHaveAttribute(\"open\")\n  expect(await formSubmitStarted(page)).toEqual(null)\n})\n\ntest(\"form submission skipped with submitter formmethod=dialog\", async ({ page }) => {\n  const dialog = page.locator(\"#dialog-formmethod-turbo-frame\")\n  await dialog.click('[formmethod=\"dialog\"]')\n\n  await expect(dialog).not.toHaveAttribute(\"open\")\n  expect(await formSubmitStarted(page)).toEqual(null)\n})\n\ntest(\"form submission targeting frame skipped within method=dialog\", async ({ page }) => {\n  const dialog = page.locator(\"#dialog-formmethod-turbo-frame\")\n  await dialog.click(\"button\")\n\n  await expect(dialog).not.toHaveAttribute(\"open\")\n  expect(await formSubmitStarted(page)).toEqual(null)\n})\n\ntest(\"form submission targeting frame skipped with submitter formmethod=dialog\", async ({ page }) => {\n  const dialog = page.locator(\"#dialog-formmethod\")\n  await dialog.click('[formmethod=\"dialog\"]')\n\n  await expect(dialog).not.toHaveAttribute(\"open\")\n  expect(await formSubmitStarted(page)).toEqual(null)\n})\n\ntest(\"form submission targets disabled frame\", async ({ page }) => {\n  await page.evaluate(() => document.getElementById(\"frame\")?.setAttribute(\"disabled\", \"\"))\n  await page.click('#targets-frame form.one [type=\"submit\"]')\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n})\n\ntest(\"form submission targeting a frame submits the Turbo-Frame header\", async ({ page }) => {\n  await page.click(\"#targets-frame [type=submit]\")\n\n  const { fetchOptions } = await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  expect(fetchOptions.headers[\"Turbo-Frame\"], \"submits with the Turbo-Frame header\").toBeTruthy()\n})\n\ntest(\"form submission targeting another frame submits the Turbo-Frame header\", async ({ page }) => {\n  await page.click(\"#frame form[method=get][data-turbo-frame=hello] [type=submit]\")\n\n  const { fetchOptions } = await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  expect(fetchOptions.headers[\"Turbo-Frame\"], \"submits with the Turbo-Frame header\").toEqual(\"hello\")\n})\n\ntest(\"form submission with submitter referencing another frame submits the Turbo-Frame header\", async ({ page }) => {\n  await page.click(\"#frame form[method=get] [type=submit][data-turbo-frame=hello]\")\n\n  const { fetchOptions } = await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  expect(fetchOptions.headers[\"Turbo-Frame\"], \"submits with the Turbo-Frame header\").toEqual(\"hello\")\n})\n\ntest(\"link method form submission dispatches events from a connected <form> element\", async ({ page }) => {\n  await page.evaluate(() =>\n    new MutationObserver(([record]) => {\n      for (const form of record.addedNodes) {\n        if (form instanceof HTMLFormElement) form.id = \"a-form-link\"\n      }\n    }).observe(document.body, { childList: true })\n  )\n\n  await page.click(\"#stream-link-method-within-form-outside-frame\")\n  await nextEventOnTarget(page, \"a-form-link\", \"turbo:before-fetch-request\")\n  await nextEventOnTarget(page, \"a-form-link\", \"turbo:submit-start\")\n  await nextEventOnTarget(page, \"a-form-link\", \"turbo:before-fetch-response\")\n  await nextEventOnTarget(page, \"a-form-link\", \"turbo:submit-end\")\n\n  await expect(page.locator(\"a-form-link\"), \"the <form> is removed\").not.toBeAttached()\n})\n\ntest(\"link method form submission submits a single request\", async ({ page }) => {\n  let requestCounter = 0\n  page.on(\"request\", () => requestCounter++)\n\n  await page.click(\"#stream-link-method-within-form-outside-frame\")\n\n  const { fetchOptions } = await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  await noNextEventNamed(page, \"turbo:before-fetch-request\")\n  expect(fetchOptions.method, \"[data-turbo-method] overrides the GET method\").toEqual(\"POST\")\n  expect(requestCounter, \"submits a single HTTP request\").toEqual(1)\n})\n\ntest(\"link method form submission inside frame submits a single request\", async ({ page }) => {\n  let requestCounter = 0\n  page.on(\"request\", () => requestCounter++)\n\n  await page.click(\"#stream-link-method-inside-frame\")\n\n  const { fetchOptions } = await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  await noNextEventNamed(page, \"turbo:before-fetch-request\")\n  expect(fetchOptions.method, \"[data-turbo-method] overrides the GET method\").toEqual(\"POST\")\n  expect(requestCounter, \"submits a single HTTP request\").toEqual(1)\n})\n\ntest(\"link method form submission targeting frame submits a single request\", async ({ page }) => {\n  let requestCounter = 0\n  page.on(\"request\", (request) => {\n    if (request.url().includes(\"/__turbo/redirect\")) requestCounter++\n  })\n\n  await page.click(\"#turbo-method-post-to-targeted-frame\")\n\n  const { fetchOptions } = await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  await noNextEventNamed(page, \"turbo:before-fetch-request\")\n  expect(fetchOptions.method, \"[data-turbo-method] overrides the GET method\").toEqual(\"POST\")\n  await expect(page.locator(\"#hello h2\")).toHaveText(\"Hello from a frame\")\n  expect(requestCounter, \"submits a single HTTP request\").toEqual(1)\n})\n\ntest(\"link method form submission inside frame\", async ({ page }) => {\n  await page.click(\"#link-method-inside-frame\")\n\n  await expect(page.locator(\"#frame h2\")).toHaveText(\"Frame: Loaded\")\n  await expect(page.locator(\"#nested-child\")).not.toBeAttached()\n})\n\ntest(\"link method form submission inside frame with data-turbo-frame=_top\", async ({ page }) => {\n  await page.click(\"#link-method-inside-frame-target-top\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"Hello\")\n})\n\ntest(\"link method form submission inside frame with data-turbo-frame target\", async ({ page }) => {\n  await page.click(\"#link-method-inside-frame-with-target\")\n\n  await expect(page.locator(\"#hello h2\")).toHaveText(\"Hello from a frame\")\n  await expect(page.locator(\"h1\")).toHaveText(\"Form\")\n})\n\ntest(\"stream link method form submission inside frame\", async ({ page }) => {\n  await page.click(\"#stream-link-method-inside-frame\")\n\n  await expect(page.locator(\"#frame div.message\")).toHaveText(\"Link!\")\n})\n\ntest(\"stream link GET method form submission inside frame\", async ({ page }) => {\n  await page.click(\"#stream-link-get-method-inside-frame\")\n\n  const { fetchOptions } = await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  expect(fetchOptions.headers[\"Accept\"]).toContain(\"text/vnd.turbo-stream.html\")\n})\n\ntest(\"stream link inside frame\", async ({ page }) => {\n  await page.click(\"#stream-link-inside-frame\")\n\n  const { fetchOptions, url } = await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  expect(fetchOptions.headers[\"Accept\"]).toContain(\"text/vnd.turbo-stream.html\")\n  expect(getSearchParam(url, \"content\")).toEqual(\"Link!\")\n})\n\ntest(\"link method form submission within form inside frame\", async ({ page }) => {\n  await page.click(\"#stream-link-method-within-form-inside-frame\")\n\n  await expect(page.locator(\"#frame div.message\")).toHaveText(\"Link!\")\n})\n\ntest(\"link method form submission inside frame with confirmation confirmed\", async ({ page }) => {\n  page.on(\"dialog\", (dialog) => {\n    expect(dialog.message()).toEqual(\"Are you sure?\")\n    dialog.accept()\n  })\n\n  await page.click(\"#link-method-inside-frame-with-confirmation\")\n\n  await expect(page.locator(\"#frame div.message\")).toHaveText(\"Link!\")\n})\n\ntest(\"link method form submission inside frame with confirmation cancelled\", async ({ page }) => {\n  page.on(\"dialog\", (dialog) => {\n    expect(dialog.message()).toEqual(\"Are you sure?\")\n    dialog.dismiss()\n  })\n\n  await page.click(\"#link-method-inside-frame-with-confirmation\")\n\n  await expect(page.locator(\"#frame div.message\"), \"Not confirming form submission does not submit the form\").not.toBeAttached()\n})\n\ntest(\"link method form submission outside frame\", async ({ page }) => {\n  await page.click(\"#link-method-outside-frame\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"Hello\")\n})\n\ntest(\"following a link with [data-turbo-method] set and a target set navigates the target frame\", async ({\n  page\n}) => {\n  await page.click(\"#turbo-method-post-to-targeted-frame\")\n\n  await expect(page.locator(\"#hello h2\"), \"drives the turbo-frame\").toHaveText(\"Hello from a frame\")\n})\n\ntest(\"following a link with [data-turbo-method] and empty [target]\", async ({ page }) => {\n  await page.click(\"#turbo-method-post-empty-target\")\n\n  await expect(page.locator(\"#hello h2\"), \"drives the turbo-frame\").toHaveText(\"Hello from a frame\")\n})\n\ntest(\"following a link with [data-turbo-method] and bare [target]\", async ({ page }) => {\n  await page.click(\"#turbo-method-post-bare-target\")\n\n  await expect(page.locator(\"#hello h2\"), \"drives the turbo-frame\").toHaveText(\"Hello from a frame\")\n})\n\ntest(\"following a link with [data-turbo-method] and [data-turbo=true] set when html[data-turbo=false]\", async ({\n  page\n}) => {\n  const html = await page.locator(\"html\")\n  await html.evaluate((html) => html.setAttribute(\"data-turbo\", \"false\"))\n\n  const link = await page.locator(\"#turbo-method-post-to-targeted-frame\")\n  await link.evaluate((link) => link.setAttribute(\"data-turbo\", \"true\"))\n\n  await link.click()\n\n  await expect(page.locator(\"h1\"), \"does not navigate the full page\").toHaveText(\"Form\")\n  await expect(page.locator(\"#hello h2\"), \"drives the turbo-frame\").toHaveText(\"Hello from a frame\")\n})\n\ntest(\"following a link with [data-turbo-method] and [data-turbo=true] set when Turbo.session.drive = false\", async ({\n  page\n}) => {\n  await page.evaluate(() => (window.Turbo.config.drive.enabled = false))\n\n  const link = await page.locator(\"#turbo-method-post-to-targeted-frame\")\n  await link.evaluate((link) => link.setAttribute(\"data-turbo\", \"true\"))\n\n  await link.click()\n\n  await expect(page.locator(\"h1\"), \"does not navigate the full page\").toHaveText(\"Form\")\n  await expect(page.locator(\"#hello h2\"), \"drives the turbo-frame\").toHaveText(\"Hello from a frame\")\n})\n\ntest(\"following a link with [data-turbo-method] set when html[data-turbo=false]\", async ({ page }) => {\n  const html = await page.locator(\"html\")\n  await html.evaluate((html) => html.setAttribute(\"data-turbo\", \"false\"))\n\n  await page.click(\"#turbo-method-post-to-targeted-frame\")\n\n  await expect(page.locator(\"h1\"), \"treats link full-page navigation\").toHaveText(\"Hello\")\n})\n\ntest(\"following a link with [data-turbo-method] set when Turbo.session.drive = false\", async ({ page }) => {\n  await page.evaluate(() => (window.Turbo.config.drive = false))\n  await page.click(\"#turbo-method-post-to-targeted-frame\")\n\n  await expect(page.locator(\"h1\"), \"treats link full-page navigation\").toHaveText(\"Hello\")\n})\n\ntest(\"stream link method form submission outside frame\", async ({ page }) => {\n  await page.click(\"#stream-link-method-outside-frame\")\n\n  await expect(page.locator(\"#frame div.message\")).toHaveText(\"Link!\")\n})\n\ntest(\"link method form submission within form outside frame\", async ({ page }) => {\n  await page.click(\"#link-method-within-form-outside-frame\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"Hello\")\n})\n\ntest(\"stream link method form submission within form outside frame\", async ({ page }) => {\n  await page.click(\"#stream-link-method-within-form-outside-frame\")\n\n  await expect(page.locator(\"#frame div.message\")).toHaveText(\"Link!\")\n})\n\ntest(\"turbo:before-fetch-request fires on the form element\", async ({ page }) => {\n  await page.click('#targets-frame form.one [type=\"submit\"]')\n  expect(await nextEventOnTarget(page, \"form_one\", \"turbo:before-fetch-request\")).toBeTruthy()\n})\n\ntest(\"turbo:before-fetch-response fires on the form element\", async ({ page }) => {\n  await page.click('#targets-frame form.one [type=\"submit\"]')\n  expect(await nextEventOnTarget(page, \"form_one\", \"turbo:before-fetch-response\")).toBeTruthy()\n})\n\ntest(\"POST to external action ignored\", async ({ page }) => {\n  await page.click(\"#submit-external\")\n\n  await noNextEventNamed(page, \"turbo:before-fetch-request\")\n\n  await expect(page).toHaveURL(\"https://httpbin.org/post\")\n})\n\ntest(\"POST to external action within frame ignored\", async ({ page }) => {\n  await page.click(\"#submit-external-within-ignored\")\n\n  await noNextEventNamed(page, \"turbo:before-fetch-request\")\n\n  await expect(page).toHaveURL(\"https://httpbin.org/post\")\n})\n\ntest(\"POST to external action targeting frame ignored\", async ({ page }) => {\n  await page.click(\"#submit-external-target-ignored\")\n\n  await noNextEventNamed(page, \"turbo:before-fetch-request\")\n\n  await expect(page).toHaveURL(\"https://httpbin.org/post\")\n})\n\ntest(\"form submission skipped with form[target]\", async ({ page }) => {\n  await page.click(\"#skipped form[target] button\")\n  await nextBeat()\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  expect(await formSubmitEnded(page)).toEqual(null)\n})\n\ntest(\"form submission skipped with submitter button[formtarget]\", async ({ page }) => {\n  await page.click(\"#skipped [formtarget]\")\n  await nextBeat()\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/form.html\"))\n  expect(await formSubmitEnded(page)).toEqual(null)\n})\n\nfunction formSubmitStarted(page) {\n  return getFromLocalStorage(page, \"formSubmitStarted\")\n}\n\nfunction formSubmitEnded(page) {\n  return getFromLocalStorage(page, \"formSubmitEnded\")\n}\n"
  },
  {
    "path": "src/tests/functional/frame_navigation_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport { getFromLocalStorage, nextBeat, nextEventNamed, nextEventOnTarget, pathname, scrollToSelector, withPathname } from \"../helpers/page\"\n\ntest(\"frame navigation with descendant link\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/frame_navigation.html\")\n  await page.click(\"#inside\")\n\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")\n})\n\ntest(\"frame navigation with self link\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/frame_navigation.html\")\n  await page.click(\"#self\")\n\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")\n})\n\ntest(\"frame navigation with exterior link\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/frame_navigation.html\")\n  await page.click(\"#outside\")\n\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")\n})\n\ntest(\"frame navigation with exterior link in Shadow DOM\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/frame_navigation.html\")\n  await page.click(\"#outside-in-shadow-dom\")\n\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")\n})\n\ntest(\"frame navigation with data-turbo-action\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/frame_navigation.html\")\n  await page.click(\"#link-to-frame-with-empty-head\")\n  await nextBeat()\n\n  await nextEventOnTarget(page, \"empty-head\", \"turbo:frame-load\")\n\n  const frameText = page.locator(\"#empty-head h2\")\n  await expect(frameText).toHaveText(\"Frame updated\")\n\n  const titleText = page.locator(\"h1\")\n  await expect(titleText).toHaveText(\"Frame navigation tests\")\n})\n\ntest(\"frame navigation emits fetch-request-error event when offline\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/tabs.html\")\n  await page.context().setOffline(true)\n  await page.click(\"#tab-2\")\n  await nextEventOnTarget(page, \"tab-frame\", \"turbo:fetch-request-error\")\n})\n\ntest(\"lazy-loaded frame promotes navigation\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/frame_navigation.html\")\n\n  await expect(page.locator(\"#eager-loaded-frame h2\")).toHaveText(\"Eager-loaded frame: Not Loaded\")\n\n  await scrollToSelector(page, \"#eager-loaded-frame\")\n  await nextEventOnTarget(page, \"eager-loaded-frame\", \"turbo:frame-load\")\n\n  await expect(page.locator(\"#eager-loaded-frame h2\")).toHaveText(\"Eager-loaded frame: Loaded\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/frames/frame_for_eager.html\"))\n})\n\ntest(\"promoted frame navigation updates the URL before rendering\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/tabs.html\")\n\n  page.evaluate(() => {\n    addEventListener(\"turbo:before-frame-render\", () => {\n      localStorage.setItem(\"beforeRenderUrl\", window.location.pathname)\n      localStorage.setItem(\"beforeRenderContent\", document.querySelector(\"#tab-content\")?.textContent || \"\")\n    })\n  })\n\n  await page.click(\"#tab-2\")\n  await nextEventNamed(page, \"turbo:before-frame-render\")\n\n  expect(await getFromLocalStorage(page, \"beforeRenderUrl\")).toEqual(\"/src/tests/fixtures/tabs/two.html\")\n  expect(await getFromLocalStorage(page, \"beforeRenderContent\")).toEqual(\"One\")\n\n  await nextEventNamed(page, \"turbo:frame-render\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/tabs/two.html\"))\n  await expect(page.locator(\"#tab-content\")).toHaveText(\"Two\")\n})\n\ntest(\"promoted frame navigations are cached\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/tabs.html\")\n\n  await page.click(\"#tab-2\")\n  await nextEventOnTarget(page, \"tab-frame\", \"turbo:frame-load\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"#tab-content\")).toHaveText(\"Two\")\n  expect(pathname((await page.getAttribute(\"#tab-frame\", \"src\")) || \"\")).toEqual(\"/src/tests/fixtures/tabs/two.html\")\n  await expect(page.locator(\"#tab-frame\"), \"sets [complete]\").toHaveAttribute(\"complete\")\n\n  await page.click(\"#tab-3\")\n  await nextEventOnTarget(page, \"tab-frame\", \"turbo:frame-load\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"#tab-content\")).toHaveText(\"Three\")\n  expect(pathname((await page.getAttribute(\"#tab-frame\", \"src\")) || \"\")).toEqual(\"/src/tests/fixtures/tabs/three.html\")\n  await expect(page.locator(\"#tab-frame\"), \"sets [complete]\").toHaveAttribute(\"complete\")\n\n  await page.goBack()\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"#tab-content\")).toHaveText(\"Two\")\n  expect(pathname((await page.getAttribute(\"#tab-frame\", \"src\")) || \"\")).toEqual(\"/src/tests/fixtures/tabs/two.html\")\n  await expect(page.locator(\"#tab-frame\"), \"caches two.html with [complete]\").toHaveAttribute(\"complete\")\n\n  await page.goBack()\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"#tab-content\")).toHaveText(\"One\")\n  await expect(page.locator(\"#tab-frame\"), \"caches one.html without #tab-frame[src]\").not.toHaveAttribute(\"src\")\n  await expect(page.locator(\"#tab-frame\"), \"caches one.html without [complete]\").not.toHaveAttribute(\"complete\")\n})\n"
  },
  {
    "path": "src/tests/functional/frame_tests.js",
    "content": "import { test, expect } from \"@playwright/test\"\nimport {\n  attributeForSelector,\n  listenForEventOnTarget,\n  nextAttributeMutationNamed,\n  noNextAttributeMutationNamed,\n  nextBeat,\n  nextEventNamed,\n  nextEventOnTarget,\n  noNextEventNamed,\n  noNextEventOnTarget,\n  pathname,\n  propertyForSelector,\n  readEventLogs,\n  scrollPosition,\n  scrollToSelector,\n  withPathname,\n  withSearchParam\n} from \"../helpers/page\"\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/frames.html\")\n  await readEventLogs(page)\n})\n\ntest(\"navigating a frame with Turbo.visit\", async ({ page }) => {\n  const pathname = \"/src/tests/fixtures/frames/frame.html\"\n\n  await page.locator(\"#frame\").evaluate((frame) => frame.setAttribute(\"disabled\", \"\"))\n  await page.evaluate((pathname) => window.Turbo.visit(pathname, { frame: \"frame\" }), pathname)\n\n  await expect(page.locator(\"#frame h2\"), \"does not navigate a disabled frame\").toHaveText(\"Frames: #frame\")\n\n  await page.locator(\"#frame\").evaluate((frame) => frame.removeAttribute(\"disabled\"))\n  await page.evaluate((pathname) => window.Turbo.visit(pathname, { frame: \"frame\" }), pathname)\n\n  await expect(page.locator(\"#frame h2\"), \"navigates the target frame\").toHaveText(\"Frame: Loaded\")\n})\n\ntest(\"navigating a frame a second time does not leak event listeners\", async ({ page }) => {\n  await withoutChangingEventListenersCount(page, async () => {\n    await page.click(\"#outer-frame-link\")\n    await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")\n    await page.click(\"#outside-frame-form\")\n    await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")\n    await page.click(\"#outer-frame-link\")\n    await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")\n  })\n})\n\ntest(\"following a link preserves the current <turbo-frame> element's attributes\", async ({ page }) => {\n  const currentPath = pathname(page.url())\n\n  await page.click(\"#hello a\")\n\n  const frame = page.locator(\"turbo-frame#frame\")\n  await expect(frame).toHaveAttribute(\"data-loaded-from\", currentPath)\n  await expect(frame).toHaveAttribute(\"src\", await propertyForSelector(page, \"#hello a\", \"href\"))\n})\n\ntest(\"following a link sets the frame element's [src]\", async ({ page }) => {\n  await page.click(\"#link-frame-with-search-params\")\n\n  const { url } = await nextEventOnTarget(page, \"frame\", \"turbo:before-fetch-request\")\n  const fetchRequestUrl = new URL(url)\n\n  expect(fetchRequestUrl.pathname).toEqual(\"/src/tests/fixtures/frames/frame.html\")\n  expect(fetchRequestUrl.searchParams.get(\"key\"), \"fetch request encodes query parameters\").toEqual(\"value\")\n\n  await nextBeat()\n  const src = new URL((await attributeForSelector(page, \"#frame\", \"src\")) || \"\")\n\n  expect(src.pathname).toEqual(\"/src/tests/fixtures/frames/frame.html\")\n  expect(src.searchParams.get(\"key\"), \"[src] attribute encodes query parameters\").toEqual(\"value\")\n})\n\ntest(\"following a link doesn't set the frame element's [src] if the link has [data-turbo-stream]\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/form.html\")\n\n  const originalSrc = await page.getAttribute(\"#frame\", \"src\")\n\n  await page.click(\"#stream-link-get-method-inside-frame\")\n  await nextBeat()\n\n  const newSrc = await page.getAttribute(\"#frame\", \"src\")\n\n  expect(originalSrc, \"the turbo-frame src should not change after clicking the link\").toEqual(newSrc)\n})\n\ntest(\"a frame whose src references itself does not infinitely loop\", async ({ page }) => {\n  await page.click(\"#frame-self\")\n\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-render\")\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")\n\n  const otherEvents = await readEventLogs(page)\n  expect(otherEvents.length, \"no more events\").toEqual(0)\n})\n\ntest(\"following a link driving a frame toggles the [aria-busy=true] attribute\", async ({ page }) => {\n  await page.click(\"#hello a\")\n\n  expect(await nextAttributeMutationNamed(page, \"frame\", \"busy\"), \"sets [busy] on the #frame\").toEqual(\"\")\n  expect(\n    await nextAttributeMutationNamed(page, \"frame\", \"aria-busy\"),\n    \"sets [aria-busy=true] on the #frame\"\n  ).toEqual(\n    \"true\"\n  )\n  expect(await nextAttributeMutationNamed(page, \"frame\", \"busy\"), \"removes [busy] on the #frame\").toEqual(null)\n  expect(\n    await nextAttributeMutationNamed(page, \"frame\", \"aria-busy\"),\n    \"removes [aria-busy] from the #frame\"\n  ).toEqual(\n    null\n  )\n})\n\ntest(\"following an a[data-turbo-frame=_top] does not toggle the frame's [aria-busy=true] attribute\", async ({\n  page\n}) => {\n  await page.click(\"#frame #link-top\")\n\n  expect(await noNextAttributeMutationNamed(page, \"frame\", \"busy\"), \"does not toggle [busy] on parent frame\").toBeTruthy()\n  expect(\n    await noNextAttributeMutationNamed(page, \"frame\", \"aria-busy\"),\n    \"does not toggle [aria-busy=true] on parent frame\"\n  ).toBeTruthy()\n})\n\ntest(\"submitting a form[data-turbo-frame=_top] does not toggle the frame's [aria-busy=true] attribute\", async ({\n  page\n}) => {\n  await page.click(\"#frame #form-submit-top\")\n\n  expect(await noNextAttributeMutationNamed(page, \"frame\", \"busy\"), \"does not toggle [busy] on parent frame\").toBeTruthy()\n  expect(\n    await noNextAttributeMutationNamed(page, \"frame\", \"aria-busy\"),\n    \"does not toggle [aria-busy=true] on parent frame\"\n  ).toBeTruthy()\n})\n\ntest(\"successfully following a link to a page without a matching frame dispatches a turbo:frame-missing event\", async ({\n  page\n}) => {\n  await page.click(\"#missing-frame-link\")\n  const { response } = await nextEventOnTarget(page, \"missing\", \"turbo:frame-missing\")\n\n  expect(response.status).toEqual(200)\n})\n\ntest(\"successfully following a link to a page without a matching frame shows an error and throws an exception\", async ({\n  page\n}) => {\n  const [error] = await Promise.all([\n    page.waitForEvent(\"pageerror\"),\n    page.click(\"#missing-frame-link\")\n  ])\n\n  await expect(page.locator(\"#missing\")).toHaveText(\"Content missing\")\n\n  expect(error).toBeTruthy()\n  expect(error.message).toContain(`The response (200) did not contain the expected <turbo-frame id=\"missing\">`)\n})\n\ntest(\"successfully following a link to a page with `turbo-visit-control` `reload` performs a full page reload\", async ({\n  page\n}) => {\n  await page.click(\"#unvisitable-page-link\")\n  await page.getByText(\"Unvisitable page loaded\").waitFor()\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/frames/unvisitable.html\"))\n})\n\ntest(\"failing to follow a link to a page without a matching frame dispatches a turbo:frame-missing event\", async ({\n  page\n}) => {\n  await page.click(\"#missing-page-link\")\n  const { response } = await nextEventOnTarget(page, \"missing\", \"turbo:frame-missing\")\n\n  expect(response.status).toEqual(404)\n})\n\ntest(\"failing to follow a link to a page without a matching frame shows an error and throws an exception\", async ({\n  page\n}) => {\n  const [error] = await Promise.all([\n    page.waitForEvent(\"pageerror\"),\n    page.click(\"#missing-page-link\")\n  ])\n\n  await expect(page.locator(\"#missing\")).toHaveText(\"Content missing\")\n\n  expect(error).toBeTruthy()\n  expect(error.message).toContain(`The response (404) did not contain the expected <turbo-frame id=\"missing\">`)\n})\n\ntest(\"the turbo:frame-missing event following a link to a page without a matching frame can be handled\", async ({\n  page\n}) => {\n  await page.locator(\"#missing\").evaluate((frame) => {\n    frame.addEventListener(\n      \"turbo:frame-missing\",\n      (event) => {\n        if (event.target instanceof Element) {\n          event.preventDefault()\n          event.target.textContent = \"Overridden\"\n        }\n      },\n      { once: true }\n    )\n  })\n  await page.click(\"#missing-frame-link\")\n  await nextEventOnTarget(page, \"missing\", \"turbo:frame-missing\")\n\n  await expect(page.locator(\"#missing\")).toHaveText(\"Overridden\")\n})\n\ntest(\"the turbo:frame-missing event following a link to a page without a matching frame can drive a Visit\", async ({\n  page\n}) => {\n  await page.locator(\"#missing\").evaluate((frame) => {\n    frame.addEventListener(\n      \"turbo:frame-missing\",\n      (event) => {\n        event.preventDefault()\n        const { response, visit } = event.detail\n\n        visit(response)\n      },\n      { once: true }\n    )\n  })\n  await page.click(\"#missing-frame-link\")\n  await nextEventOnTarget(page, \"missing\", \"turbo:frame-missing\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"Frames: #frame\")\n  await expect(page.locator(\"turbo-frame#missing\")).not.toBeAttached()\n\n  await page.goBack()\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/frames.html\"))\n  await expect(page.locator(\"turbo-frame#missing\")).toBeAttached()\n})\n\ntest(\"following a link to a page with a matching frame does not dispatch a turbo:frame-missing event\", async ({\n  page\n}) => {\n  await page.click(\"#link-frame\")\n\n  expect(await noNextEventNamed(page, \"turbo:frame-missing\")).toBeTruthy()\n\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")\n\n  const src = await attributeForSelector(page, \"#frame\", \"src\")\n  expect(\n    src,\n    \"navigates frame without dispatching turbo:frame-missing\"\n  ).toContain(\n    \"/src/tests/fixtures/frames/frame.html\"\n  )\n})\n\ntest(\"following a link within a frame which has a target set navigates the target frame without morphing even when frame[refresh=morph]\", async ({ page }) => {\n  await page.click(\"#add-refresh-morph-to-frame\")\n  await page.click(\"#hello a\")\n  await nextBeat()\n\n  expect(await nextEventOnTarget(page, \"frame\", \"turbo:before-frame-render\")).toBeTruthy()\n  expect(await noNextEventOnTarget(page, \"frame\", \"turbo:before-frame-morph\")).toBeTruthy()\n  await expect(page.locator(\"#frame h2\")).toHaveText(\"Frame: Loaded\")\n})\n\ntest(\"navigating from within replaces the contents even with turbo-frame[refresh=morph]\", async ({ page }) => {\n  await page.click(\"#add-refresh-morph-to-frame\")\n  await page.click(\"#link-frame\")\n  await nextBeat()\n\n  expect(await nextEventOnTarget(page, \"frame\", \"turbo:before-frame-render\")).toBeTruthy()\n  expect(await noNextEventOnTarget(page, \"frame\", \"turbo:before-frame-morph\")).toBeTruthy()\n  await expect(page.locator(\"#frame h2\")).toHaveText(\"Frame: Loaded\")\n})\n\ntest(\"calling reload on a frame replaces the contents\", async ({ page }) => {\n  await page.click(\"#add-src-to-frame\")\n\n  await page.evaluate(() => document.getElementById(\"frame\").reload())\n\n  expect(await nextEventOnTarget(page, \"frame\", \"turbo:before-frame-render\")).toBeTruthy()\n  expect(await noNextEventOnTarget(page, \"frame\", \"turbo:before-frame-morph\")).toBeTruthy()\n})\n\ntest(\"calling reload on a frame[refresh=morph] morphs the contents\", async ({ page }) => {\n  await page.click(\"#add-src-to-frame\")\n  await page.click(\"#add-refresh-morph-to-frame\")\n\n  await page.evaluate(() => document.getElementById(\"frame\").reload())\n\n  expect(await nextEventOnTarget(page, \"frame\", \"turbo:before-frame-render\")).toBeTruthy()\n  expect(await nextEventOnTarget(page, \"frame\", \"turbo:before-frame-morph\")).toBeTruthy()\n})\n\ntest(\"calling reload on a frame[refresh=morph] reloads descendant frame[refresh=morph]s via reload() as well\", async ({ page }) => {\n  await page.click(\"#nested-root #add-refresh-morph-to-frame\")\n  await page.click(\"#nested-root #add-src-to-frame\")\n  await page.click(\"#nested-child #add-refresh-morph-to-frame\")\n  await page.click(\"#nested-child #add-src-to-frame\")\n  await page.click(\"#nested-child-navigate-top #add-src-to-frame\")\n\n  await page.evaluate(() => document.getElementById(\"nested-root\").reload())\n\n  // Only the frames marked with refresh=\"morph\" uses morphing\n  expect(await nextEventOnTarget(page, \"nested-root\", \"turbo:before-frame-morph\")).toBeTruthy()\n  expect(await nextEventOnTarget(page, \"nested-child\", \"turbo:before-frame-morph\")).toBeTruthy()\n  expect(await noNextEventOnTarget(page, \"nested-child-navigate-top\", \"turbo:before-frame-morph\")).toBeTruthy()\n})\n\ntest(\"calling reload on a frame[refresh=morph] preserves [data-turbo-permanent] elements\", async ({ page }) => {\n  await page.click(\"#add-src-to-frame\")\n  await page.click(\"#add-refresh-morph-to-frame\")\n  const input = await page.locator(\"#permanent-input\")\n\n  await input.fill(\"Preserve me\")\n  await page.evaluate(() => document.getElementById(\"frame\").reload())\n\n  await expect(input).toBeFocused()\n  await expect(input).toHaveValue(\"Preserve me\")\n})\n\ntest(\"following a link in rapid succession cancels the previous request\", async ({ page }) => {\n  await page.click(\"#outside-frame-form\")\n  await page.click(\"#outer-frame-link\")\n\n  await expect(page.locator(\"#frame h2\")).toHaveText(\"Frame: Loaded\")\n})\n\ntest(\"following a link within a descendant frame whose ancestor declares a target set navigates the descendant frame\", async ({\n  page\n}) => {\n  const selector = \"#nested-root[target=frame] #nested-child a:not([data-turbo-frame])\"\n  const link = await page.locator(selector)\n  const href = await propertyForSelector(page, selector, \"href\")\n\n  await link.click()\n\n  await expect(page.locator(\"#frame h2\")).toHaveText(\"Frames: #frame\")\n  await expect(page.locator(\"#nested-root > h2\")).toHaveText(\"Frames: #nested-root\")\n  await expect(page.locator(\"#nested-child\")).toHaveText(\"Frame: Loaded\")\n  await expect(page.locator(\"#frame\")).not.toHaveAttribute(\"src\")\n  await expect(page.locator(\"#nested-root\")).not.toHaveAttribute(\"src\")\n  await expect(page.locator(\"#nested-child\")).toHaveAttribute(\"src\", href)\n})\n\ntest(\"following a link that declares data-turbo-frame within a frame whose ancestor respects the override\", async ({\n  page\n}) => {\n  await page.click(\"#nested-root[target=frame] #nested-child a[data-turbo-frame]\")\n\n  await expect(page.locator(\"body > h1\")).toHaveText(\"One\")\n  await expect(page.locator(\"#frame\")).not.toBeAttached()\n  await expect(page.locator(\"#nested-root\")).not.toBeAttached()\n  await expect(page.locator(\"#nested-child\")).not.toBeAttached()\n})\n\ntest(\"following a form within a nested frame with form target top\", async ({ page }) => {\n  await page.click(\"#nested-child-navigate-form-top-submit\")\n\n  await expect(page.locator(\"body > h1\")).toHaveText(\"One\")\n  await expect(page.locator(\"#frame\")).not.toBeAttached()\n  await expect(page.locator(\"#nested-root\")).not.toBeAttached()\n  await expect(page.locator(\"#nested-child\")).not.toBeAttached()\n})\n\ntest(\"following a form within a nested frame with child frame target top\", async ({ page }) => {\n  await page.click(\"#nested-child-navigate-top-submit\")\n\n  await expect(page.locator(\"body > h1\")).toHaveText(\"One\")\n  await expect(page.locator(\"#frame\")).not.toBeAttached()\n  await expect(page.locator(\"#nested-root\")).not.toBeAttached()\n  await expect(page.locator(\"#nested-child-navigate-top\")).not.toBeAttached()\n})\n\ntest(\"following a link within a frame with target=_top navigates the page\", async ({ page }) => {\n  await expect(page.locator(\"#navigate-top\")).not.toHaveAttribute(\"src\")\n\n  await page.click(\"#navigate-top a:not([data-turbo-frame])\")\n\n  await expect(page.locator(\"body > h1\")).toHaveText(\"One\")\n  await expect(page.locator(\"#navigate-top a\")).not.toBeAttached()\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"key\", \"value\"))\n})\n\ntest(\"following a link that declares data-turbo-frame='_self' within a frame with target=_top navigates the frame itself\", async ({\n  page\n}) => {\n  await expect(page.locator(\"#navigate-top\")).not.toHaveAttribute(\"src\")\n\n  await page.click(\"#navigate-top a[data-turbo-frame='_self']\")\n\n  await expect(page.locator(\"body > h1\")).toHaveText(\"Frames\")\n  await expect(page.locator(\"#navigate-top\")).toHaveText(\"Replaced only the frame\")\n})\n\ntest(\"following a link with data-turbo-frame='_parent' within a nested frame navigates the parent frame\", async ({\n  page\n}) => {\n  await expect(page.locator(\"#nested-child\"), \"child frame exists before navigation\").toBeAttached()\n  await expect(page.locator(\"#nested-root > h2\")).toHaveText(\"Frames: #nested-root\")\n\n  await page.click(\"#nested-child #link-parent\")\n\n  await expect(page.locator(\"#nested-root > h2\"), \"parent frame content updated\").toHaveText(\"Parent: Loaded\")\n  await expect(page.locator(\"#nested-child\"), \"child frame removed when parent navigates\").not.toBeAttached()\n\n  const nestedRootSrc = await attributeForSelector(page, \"#nested-root\", \"src\")\n  expect(nestedRootSrc, \"parent frame src updated\").toContain(\"/src/tests/fixtures/frames/parent.html\")\n})\n\ntest(\"submitting a form with data-turbo-frame='_parent' within a nested frame navigates the parent frame\", async ({\n  page\n}) => {\n  await page.click(\"#nested-child #form-submit-parent\")\n\n  await expect(page.locator(\"#nested-root > h2\"), \"parent frame content updated via form\").toHaveText(\"Parent: Loaded\")\n  await expect(page.locator(\"#nested-child\", \"child frame removed after form submission to parent\")).not.toBeAttached()\n\n  const nestedRootSrc = await attributeForSelector(page, \"#nested-root\", \"src\")\n  expect(nestedRootSrc, \"parent frame src updated via form\").toContain(\"/src/tests/fixtures/frames/parent.html\")\n})\n\ntest(\"following a link with data-turbo-frame='_parent' navigates only the immediate parent frame\", async ({ page }) => {\n  await expect(page.locator(\"#nested-grandchild\", \"grandchild frame exists before navigation\")).toBeAttached()\n  await expect(page.locator(\"#nested-child > h2\")).toHaveText(\"Frames: #nested-child\")\n  await expect(page.locator(\"#nested-root > h2\")).toHaveText(\"Frames: #nested-root\")\n\n  await page.click(\"#nested-grandchild #grandchild-link-parent\")\n\n  await expect(page.locator(\"#nested-child > h2\"), \"immediate parent frame (#nested-child) was updated\").toHaveText(\"Frame: Loaded\")\n  await expect(page.locator(\"#nested-root > h2\"), \"grandparent frame (#nested-root) was NOT updated\").toHaveText(\"Frames: #nested-root\")\n\n  await expect(page.locator(\"#nested-grandchild\", \"grandchild frame removed when parent navigates\")).not.toBeAttached()\n})\n\ntest(\"following a link with data-turbo-frame='_parent' in a top-level frame navigates the page\", async ({ page }) => {\n  await expect(page.locator(\"#top-level-parent\")).not.toHaveAttribute(\"src\")\n\n  await page.click(\"#top-level-parent a\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"key\", \"value\"))\n  await expect(page.locator(\"body > h1\")).toHaveText(\"One\")\n  await expect(page.locator(\"#top-level-parent a\")).not.toBeAttached()\n})\n\ntest(\"following a link with data-turbo-frame='_parent' when parent frame is disabled navigates the page\", async ({ page }) => {\n  await page.locator(\"#nested-root\").evaluate((frame) => frame.setAttribute(\"disabled\", \"\"))\n\n  await page.click(\"#nested-child #link-parent\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/frames/parent.html\"))\n  await expect(page.locator(\"body > h1\"), \"navigates the page when parent frame is disabled\").toHaveText(\"Nested Root: Parent\")\n})\n\ntest(\"following a link to a page with a <turbo-frame recurse> which lazily loads a matching frame\", async ({\n  page\n}) => {\n  await page.click(\"#recursive summary\")\n\n  await expect(page.locator(\"#recursive details\")).toHaveAttribute(\"open\")\n\n  await page.click(\"#recursive a\")\n  await nextEventOnTarget(page, \"recursive\", \"turbo:frame-load\")\n  await nextEventOnTarget(page, \"composer\", \"turbo:frame-load\")\n\n  await expect(page.locator(\"#recursive details\")).not.toHaveAttribute(\"open\")\n})\n\ntest(\"submitting a form that redirects to a page with a <turbo-frame recurse> which lazily loads a matching frame\", async ({\n  page\n}) => {\n  await page.click(\"#recursive summary\")\n\n  await expect(page.locator(\"#recursive details\")).toHaveAttribute(\"open\")\n\n  await page.click(\"#recursive input[type=submit]\")\n  await nextEventOnTarget(page, \"recursive\", \"turbo:frame-load\")\n  await nextEventOnTarget(page, \"composer\", \"turbo:frame-load\")\n\n  await expect(page.locator(\"#recursive details\")).not.toHaveAttribute(\"open\")\n})\n\ntest(\"removing [disabled] attribute from eager-loaded frame navigates it\", async ({ page }) => {\n  await page.evaluate(() => document.getElementById(\"frame\")?.setAttribute(\"disabled\", \"\"))\n  await page.evaluate(() =>\n    document.getElementById(\"frame\")?.setAttribute(\"src\", \"/src/tests/fixtures/frames/frame.html\")\n  )\n\n  expect(\n    await noNextEventOnTarget(page, \"frame\", \"turbo:before-fetch-request\"),\n    \"[disabled] frames do not submit requests\"\n  ).toBeTruthy()\n\n  await page.evaluate(() => document.getElementById(\"frame\")?.removeAttribute(\"disabled\"))\n\n  await nextEventOnTarget(page, \"frame\", \"turbo:before-fetch-request\")\n})\n\ntest(\"evaluates frame script elements on each render\", async ({ page }) => {\n  expect(await frameScriptEvaluationCount(page)).toEqual(undefined)\n\n  await page.click(\"#body-script-link\")\n  await nextEventOnTarget(page, \"body-script\", \"turbo:frame-load\")\n  expect(await frameScriptEvaluationCount(page)).toEqual(1)\n\n  await page.click(\"#body-script-link\")\n  await nextEventOnTarget(page, \"body-script\", \"turbo:frame-load\")\n  expect(await frameScriptEvaluationCount(page)).toEqual(2)\n})\n\ntest(\"does not evaluate data-turbo-eval=false scripts\", async ({ page }) => {\n  await page.click(\"#eval-false-script-link\")\n  await nextBeat()\n  expect(await frameScriptEvaluationCount(page)).toEqual(undefined)\n})\n\ntest(\"redirecting in a form is still navigatable after redirect\", async ({ page }) => {\n  await page.click(\"#navigate-form-redirect\")\n  await nextEventOnTarget(page, \"form-redirect\", \"turbo:frame-load\")\n  await expect(page.locator(\"turbo-frame#form-redirect h2\")).toHaveText(\"Form Redirect\")\n\n  await page.click(\"#submit-form\")\n  await nextEventOnTarget(page, \"form-redirect\", \"turbo:frame-load\")\n  await expect(page.locator(\"turbo-frame#form-redirect h2\")).toHaveText(\"Form Redirected\")\n\n  await page.click(\"#navigate-form-redirect\")\n  await nextEventOnTarget(page, \"form-redirect\", \"turbo:frame-load\")\n\n  await expect(page.locator(\"turbo-frame#form-redirect h2\")).toHaveText(\"Form Redirect\")\n})\n\ntest(\"'turbo:frame-render' is triggered after frame has finished rendering\", async ({ page }) => {\n  await page.click(\"#frame-part\")\n\n  await nextEventNamed(page, \"turbo:frame-render\") // recursive\n  const { fetchResponse } = await nextEventNamed(page, \"turbo:frame-render\")\n\n  expect(fetchResponse.response.url).toContain(\"/src/tests/fixtures/frames/part.html\")\n})\n\ntest(\"navigating a frame from an outer link with a turbo-frame child fires events\", async ({ page }) => {\n  await page.click(\"#outside-frame-link-with-frame-child\")\n\n  await nextEventOnTarget(page, \"frame\", \"turbo:before-fetch-request\")\n  await nextEventOnTarget(page, \"frame\", \"turbo:before-fetch-response\")\n  const { fetchResponse } = await nextEventOnTarget(page, \"frame\", \"turbo:frame-render\")\n  expect(fetchResponse.response.url).toContain(\"/src/tests/fixtures/frames/form.html\")\n\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")\n\n  expect(await readEventLogs(page), \"no more events\").toHaveLength(0)\n})\n\ntest(\"navigating a frame from an outer form fires events\", async ({ page }) => {\n  await page.click(\"#outside-frame-form\")\n\n  await nextEventOnTarget(page, \"frame\", \"turbo:before-fetch-request\")\n  await nextEventOnTarget(page, \"frame\", \"turbo:before-fetch-response\")\n  const { fetchResponse } = await nextEventOnTarget(page, \"frame\", \"turbo:frame-render\")\n  expect(fetchResponse.response.url).toContain(\"/src/tests/fixtures/frames/form.html\")\n\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")\n\n  const otherEvents = await readEventLogs(page)\n  expect(otherEvents.length, \"no more events\").toEqual(0)\n})\n\ntest(\"navigating a frame from an outer link fires events\", async ({ page }) => {\n  await listenForEventOnTarget(page, \"outside-frame-form\", \"turbo:click\")\n  await page.click(\"#outside-frame-form\")\n\n  await nextEventOnTarget(page, \"outside-frame-form\", \"turbo:click\")\n  await nextEventOnTarget(page, \"frame\", \"turbo:before-fetch-request\")\n  await nextEventOnTarget(page, \"frame\", \"turbo:before-fetch-response\")\n  const { fetchResponse } = await nextEventOnTarget(page, \"frame\", \"turbo:frame-render\")\n  expect(fetchResponse.response.url).toContain(\"/src/tests/fixtures/frames/form.html\")\n\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")\n\n  const otherEvents = await readEventLogs(page)\n  expect(otherEvents.length, \"no more events\").toEqual(0)\n})\n\ntest(\"navigating a frame from an inner link fires events\", async ({ page }) => {\n  await listenForEventOnTarget(page, \"link-frame\", \"turbo:click\")\n  await page.click(\"#link-frame\")\n\n  await nextEventOnTarget(page, \"link-frame\", \"turbo:click\")\n  await nextEventOnTarget(page, \"frame\", \"turbo:before-fetch-request\")\n  await nextEventOnTarget(page, \"frame\", \"turbo:before-fetch-response\")\n  const { fetchResponse } = await nextEventOnTarget(page, \"frame\", \"turbo:frame-render\")\n  expect(fetchResponse.response.url).toContain(\"/src/tests/fixtures/frames/frame.html\")\n\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")\n\n  const otherEvents = await readEventLogs(page)\n  expect(otherEvents.length, \"no more events\").toEqual(0)\n})\n\ntest(\"navigating a frame targeting _top from an outer link fires events\", async ({ page }) => {\n  await listenForEventOnTarget(page, \"outside-navigate-top-link\", \"turbo:click\")\n  await page.click(\"#outside-navigate-top-link\")\n\n  await nextEventOnTarget(page, \"outside-navigate-top-link\", \"turbo:click\")\n  await nextEventOnTarget(page, \"html\", \"turbo:before-fetch-request\")\n  await nextEventOnTarget(page, \"html\", \"turbo:before-fetch-response\")\n  await nextEventOnTarget(page, \"html\", \"turbo:before-render\")\n  await nextEventOnTarget(page, \"html\", \"turbo:render\")\n  await nextEventOnTarget(page, \"html\", \"turbo:load\")\n\n  const otherEvents = await readEventLogs(page)\n  expect(otherEvents.length, \"no more events\").toEqual(0)\n})\n\ntest(\"invoking .reload() re-fetches the frame's content\", async ({ page }) => {\n  await page.click(\"#link-frame\")\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")\n  await page.evaluate(() => document.getElementById(\"frame\").reload())\n\n  const dispatchedEvents = await readEventLogs(page)\n\n  expect(\n    dispatchedEvents.map(([name, _, id]) => [id, name])\n  ).toEqual(\n    [\n      [\"frame\", \"turbo:before-fetch-request\"],\n      [\"frame\", \"turbo:before-fetch-response\"],\n      [\"frame\", \"turbo:before-frame-render\"],\n      [\"frame\", \"turbo:frame-render\"],\n      [\"frame\", \"turbo:frame-load\"]\n    ]\n  )\n})\n\ntest(\"following inner link reloads frame on every click\", async ({ page }) => {\n  await page.click(\"#hello a\")\n  await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  await page.click(\"#hello a\")\n  await nextEventNamed(page, \"turbo:before-fetch-request\")\n})\n\ntest(\"following outer link reloads frame on every click\", async ({ page }) => {\n  await page.click(\"#outer-frame-link\")\n  await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  await page.click(\"#outer-frame-link\")\n  await nextEventNamed(page, \"turbo:before-fetch-request\")\n})\n\ntest(\"following outer form reloads frame on every submit\", async ({ page }) => {\n  await page.click(\"#outer-frame-submit\")\n  await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  await page.click(\"#outer-frame-submit\")\n  await nextEventNamed(page, \"turbo:before-fetch-request\")\n})\n\ntest(\"an inner/outer link reloads frame on every click\", async ({ page }) => {\n  await page.click(\"#inner-outer-frame-link\")\n  await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  await page.click(\"#inner-outer-frame-link\")\n  await nextEventNamed(page, \"turbo:before-fetch-request\")\n})\n\ntest(\"an inner/outer form reloads frame on every submit\", async ({ page }) => {\n  await page.click(\"#inner-outer-frame-submit\")\n  await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  await page.click(\"#inner-outer-frame-submit\")\n  await nextEventNamed(page, \"turbo:before-fetch-request\")\n})\n\ntest(\"reconnecting after following a link does not reload the frame\", async ({ page }) => {\n  await page.click(\"#hello a\")\n  await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  await page.evaluate(() => {\n    window.savedElement = document.querySelector(\"#frame\")\n    window.savedElement?.remove()\n  })\n  await nextBeat()\n\n  await page.evaluate(() => {\n    if (window.savedElement) {\n      document.body.appendChild(window.savedElement)\n    }\n  })\n  await nextBeat()\n\n  const eventLogs = await readEventLogs(page)\n  const requestLogs = eventLogs.filter(([name]) => name == \"turbo:before-fetch-request\")\n  expect(requestLogs.length).toEqual(0)\n})\n\ntest(\"navigating pushing URL state from a frame navigation fires events\", async ({ page }) => {\n  await page.click(\"#link-outside-frame-action-advance\")\n\n  expect(\n    await nextAttributeMutationNamed(page, \"frame\", \"aria-busy\"),\n    \"sets aria-busy on the <turbo-frame>\"\n  ).toEqual(\n    \"true\"\n  )\n  await nextEventOnTarget(page, \"frame\", \"turbo:before-fetch-request\")\n  await nextEventOnTarget(page, \"frame\", \"turbo:before-fetch-response\")\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-render\")\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")\n  expect(await nextAttributeMutationNamed(page, \"frame\", \"aria-busy\"), \"removes aria-busy from the <turbo-frame>\").not.toBeTruthy()\n\n  expect(await nextAttributeMutationNamed(page, \"html\", \"aria-busy\"), \"sets aria-busy on the <html>\").toEqual(\"true\")\n  await nextEventOnTarget(page, \"html\", \"turbo:before-visit\")\n  await nextEventOnTarget(page, \"html\", \"turbo:visit\")\n  await nextEventOnTarget(page, \"html\", \"turbo:before-cache\")\n  await nextEventOnTarget(page, \"html\", \"turbo:before-render\")\n  await nextEventOnTarget(page, \"html\", \"turbo:render\")\n  await nextEventOnTarget(page, \"html\", \"turbo:load\")\n  expect(await nextAttributeMutationNamed(page, \"html\", \"aria-busy\"), \"removes aria-busy from the <html>\").not.toBeTruthy()\n})\n\ntest(\"navigating a frame with a form[method=get] that does not redirect still updates the [src]\", async ({\n  page\n}) => {\n  await page.click(\"#frame-form-get-no-redirect\")\n  await nextEventNamed(page, \"turbo:before-fetch-request\")\n  await nextEventNamed(page, \"turbo:before-fetch-response\")\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-render\")\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")\n\n  expect(await noNextEventOnTarget(page, \"frame\", \"turbo:before-fetch-request\")).toBeTruthy()\n\n  const src = (await attributeForSelector(page, \"#frame\", \"src\")) ?? \"\"\n\n  expect(src, \"updates src attribute\").toContain(\"/src/tests/fixtures/frames/frame.html\")\n  await expect(page.locator(\"h1\")).toHaveText(\"Frames\")\n  await expect(page.locator(\"#frame h2\")).toHaveText(\"Frame: Loaded\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/frames.html\"))\n})\n\ntest(\"navigating turbo-frame[data-turbo-action=advance] from within pushes URL state\", async ({ page }) => {\n  await page.click(\"#add-turbo-action-to-frame\")\n  await page.click(\"#link-frame\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  const title = page.locator(\"h1\")\n  const frameTitle = page.locator(\"#frame h2\")\n\n  await expect(title).toHaveText(\"Frames\")\n  await expect(frameTitle).toHaveText(\"Frame: Loaded\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/frames/frame.html\"))\n})\n\ntest(\"navigating turbo-frame[data-turbo-action=advance] from outside with target pushes URL state\", async ({ page }) => {\n  await page.click(\"#add-turbo-action-to-frame\")\n  await page.click(\"#hello a\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"Frames\")\n  await expect(page.locator(\"#frame h2\")).toHaveText(\"Frame: Loaded\")\n  expect(pathname(page.url())).toEqual(\"/src/tests/fixtures/frames/frame.html\")\n})\n\ntest(\"navigating turbo-frame[data-turbo-action=advance] with Turbo.visit pushes URL state\", async ({ page }) => {\n  const path = \"/src/tests/fixtures/frames/frame.html\"\n\n  await page.click(\"#add-turbo-action-to-frame\")\n  await page.evaluate((path) => window.Turbo.visit(path, { frame: \"frame\" }), path)\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"Frames\")\n  await expect(page.locator(\"#frame h2\")).toHaveText(\"Frame: Loaded\")\n  expect(pathname(page.url())).toEqual(path)\n})\n\ntest(\"navigating turbo-frame without advance with Turbo.visit specifying advance pushes URL state\", async ({ page }) => {\n  const path = \"/src/tests/fixtures/frames/frame.html\"\n\n  await page.evaluate((path) => window.Turbo.visit(path, { frame: \"frame\", action: \"advance\" }), path)\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"Frames\")\n  await expect(page.locator(\"#frame h2\")).toHaveText(\"Frame: Loaded\")\n  expect(pathname(page.url())).toEqual(path)\n})\n\ntest(\"navigating turbo-frame[data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state\", async ({\n  page\n}) => {\n  await page.click(\"#link-outside-frame-action-advance\")\n  await nextEventNamed(page, \"turbo:load\")\n  await page.click(\"#link-outside-frame-action-advance\")\n  await nextEventNamed(page, \"turbo:load\")\n  await page.click(\"#link-outside-frame-action-advance\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"#frame\"), \"clears turbo-frame[aria-busy]\").not.toHaveAttribute(\"aria-busy\")\n  await expect(page.locator(\"#html\"), \"clears html[aria-busy]\").not.toHaveAttribute(\"aria-busy\")\n  await expect(page.locator(\"#html\"), \"clears html[data-turbo-preview]\").not.toHaveAttribute(\"data-turbo-preview\")\n})\n\ntest(\"navigating a turbo-frame with an a[data-turbo-action=advance] preserves page state\", async ({ page }) => {\n  await scrollToSelector(page, \"#below-the-fold-input\")\n  await page.fill(\"#below-the-fold-input\", \"a value\")\n  await page.click(\"#below-the-fold-link-frame-action\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  const title = await page.locator(\"h1\")\n  const frameTitle = await page.locator(\"#frame h2\")\n  const src = (await attributeForSelector(page, \"#frame\", \"src\")) ?? \"\"\n\n  expect(src, \"updates src attribute\").toContain(\"/src/tests/fixtures/frames/frame.html\")\n  await expect(title).toHaveText(\"Frames\")\n  await expect(frameTitle).toHaveText(\"Frame: Loaded\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/frames/frame.html\"))\n  await expect(page.locator(\"#below-the-fold-input\"), \"preserves page state\").toHaveValue(\"a value\")\n\n  const { y } = await scrollPosition(page)\n  expect(y, \"preserves Y scroll position\").not.toEqual(0)\n})\n\ntest(\"a turbo-frame that has been driven by a[data-turbo-action] can be navigated normally\", async ({ page }) => {\n  await page.click(\"#remove-target-from-hello\")\n  await page.click(\"#link-hello-advance\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"Frames\")\n  await expect(page.locator(\"#hello h2\")).toHaveText(\"Hello from a frame\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/frames/hello.html\"))\n\n  await page.click(\"#hello a\")\n  await nextEventOnTarget(page, \"hello\", \"turbo:frame-load\")\n\n  expect(await noNextEventNamed(page, \"turbo:load\")).toBeTruthy()\n  await expect(page.locator(\"#hello h2\")).toHaveText(\"Frames: #hello\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/frames/hello.html\"))\n})\n\ntest(\"navigating turbo-frame from within with a[data-turbo-action=advance] pushes URL state\", async ({ page }) => {\n  await page.click(\"#link-nested-frame-action-advance\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  const title = page.locator(\"h1\")\n  const frameTitle = page.locator(\"#frame h2\")\n  const src = (await attributeForSelector(page, \"#frame\", \"src\")) ?? \"\"\n\n  expect(src, \"updates src attribute\").toContain(\"/src/tests/fixtures/frames/frame.html\")\n  await expect(title).toHaveText(\"Frames\")\n  await expect(frameTitle).toHaveText(\"Frame: Loaded\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/frames/frame.html\"))\n  await expect(page.locator(\"#frame\"), \"marks the frame as [complete]\").toHaveAttribute(\"complete\")\n})\n\ntest(\"navigating frame with a[data-turbo-action=advance] pushes URL state\", async ({ page }) => {\n  await page.click(\"#link-outside-frame-action-advance\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  const title = page.locator(\"h1\")\n  const frameTitle = page.locator(\"#frame h2\")\n  const src = (await attributeForSelector(page, \"#frame\", \"src\")) ?? \"\"\n\n  expect(src, \"updates src attribute\").toContain(\"/src/tests/fixtures/frames/frame.html\")\n  await expect(title).toHaveText(\"Frames\")\n  await expect(frameTitle).toHaveText(\"Frame: Loaded\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/frames/frame.html\"))\n  await expect(page.locator(\"#frame\"), \"marks the frame as [complete]\").toHaveAttribute(\"complete\")\n})\n\ntest(\"navigating frame with form[method=get][data-turbo-action=advance] pushes URL state\", async ({ page }) => {\n  await page.click(\"#form-get-frame-action-advance button\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  const title = page.locator(\"h1\")\n  const frameTitle = page.locator(\"#frame h2\")\n  const src = (await attributeForSelector(page, \"#frame\", \"src\")) ?? \"\"\n\n  expect(src, \"updates src attribute\").toContain(\"/src/tests/fixtures/frames/frame.html\")\n  await expect(title).toHaveText(\"Frames\")\n  await expect(frameTitle).toHaveText(\"Frame: Loaded\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/frames/frame.html\"))\n  await expect(page.locator(\"#frame\"), \"marks the frame as [complete]\").toHaveAttribute(\"complete\")\n})\n\ntest(\"navigating frame with form[method=get][data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state\", async ({\n  page\n}) => {\n  await page.click(\"#form-get-frame-action-advance button\")\n  await nextEventNamed(page, \"turbo:load\")\n  await page.click(\"#form-get-frame-action-advance button\")\n  await nextEventNamed(page, \"turbo:load\")\n  await page.click(\"#form-get-frame-action-advance button\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"#frame\"), \"clears turbo-frame[aria-busy]\").not.toHaveAttribute(\"aria-busy\")\n  await expect(page.locator(\"#html\"), \"clears html[aria-busy]\").not.toHaveAttribute(\"aria-busy\")\n  await expect(page.locator(\"#html\"), \"clears html[data-turbo-preview]\").not.toHaveAttribute(\"data-turbo-preview\")\n})\n\ntest(\"navigating frame with form[method=post][data-turbo-action=advance] pushes URL state\", async ({ page }) => {\n  await page.click(\"#form-post-frame-action-advance button\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  const title = page.locator(\"h1\")\n  const frameTitle = page.locator(\"#frame h2\")\n  const src = (await attributeForSelector(page, \"#frame\", \"src\")) ?? \"\"\n\n  expect(src, \"updates src attribute\").toContain(\"/src/tests/fixtures/frames/frame.html\")\n  await expect(title).toHaveText(\"Frames\")\n  await expect(frameTitle).toHaveText(\"Frame: Loaded\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/frames/frame.html\"))\n  await expect(page.locator(\"#frame\"), \"marks the frame as [complete]\").toHaveAttribute(\"complete\")\n})\n\ntest(\"navigating frame with form[method=post][data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state\", async ({\n  page\n}) => {\n  await page.click(\"#form-post-frame-action-advance button\")\n  await nextEventNamed(page, \"turbo:load\")\n  await page.click(\"#form-post-frame-action-advance button\")\n  await nextEventNamed(page, \"turbo:load\")\n  await page.click(\"#form-post-frame-action-advance button\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"#frame\"), \"clears turbo-frame[aria-busy]\").not.toHaveAttribute(\"aria-busy\")\n  await expect(page.locator(\"#html\"), \"clears html[aria-busy]\").not.toHaveAttribute(\"aria-busy\")\n  await expect(page.locator(\"#html\"), \"clears html[data-turbo-preview]\").not.toHaveAttribute(\"data-turbo-preview\")\n  await expect(page.locator(\"#frame\"), \"marks the frame as [complete]\").toHaveAttribute(\"complete\")\n})\n\ntest(\"navigating frame with button[data-turbo-action=advance] pushes URL state\", async ({ page }) => {\n  await page.click(\"#button-frame-action-advance\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  const title = page.locator(\"h1\")\n  const frameTitle = page.locator(\"#frame h2\")\n  const src = (await attributeForSelector(page, \"#frame\", \"src\")) ?? \"\"\n\n  expect(src, \"updates src attribute\").toContain(\"/src/tests/fixtures/frames/frame.html\")\n  await expect(title).toHaveText(\"Frames\")\n  await expect(frameTitle).toHaveText(\"Frame: Loaded\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/frames/frame.html\"))\n  await expect(page.locator(\"#frame\"), \"marks the frame as [complete]\").toHaveAttribute(\"complete\")\n})\n\ntest(\"navigating back after pushing URL state from a turbo-frame[data-turbo-action=advance] restores the frames previous contents\", async ({\n  page\n}) => {\n  await page.click(\"#add-turbo-action-to-frame\")\n  await page.click(\"#link-frame\")\n  await nextEventNamed(page, \"turbo:load\")\n  await page.goBack()\n  await nextEventNamed(page, \"turbo:load\")\n\n  const title = page.locator(\"h1\")\n  const frameTitle = page.locator(\"#frame h2\")\n\n  await expect(title).toHaveText(\"Frames\")\n  await expect(frameTitle).toHaveText(\"Frames: #frame\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/frames.html\"))\n  await expect(page.locator(\"#frame\")).not.toHaveAttribute(\"src\")\n  expect(await propertyForSelector(page, \"#frame\", \"src\")).toEqual(null)\n})\n\ntest(\"navigating back then forward after pushing URL state from a turbo-frame[data-turbo-action=advance] restores the frames next contents\", async ({\n  page\n}) => {\n  await page.click(\"#add-turbo-action-to-frame\")\n  await page.click(\"#link-frame\")\n  await nextEventNamed(page, \"turbo:load\")\n  await page.goBack()\n  await nextEventNamed(page, \"turbo:load\")\n  await page.goForward()\n  await nextEventNamed(page, \"turbo:load\")\n\n  const title = page.locator(\"h1\")\n  const frameTitle = page.locator(\"#frame h2\")\n  const src = (await attributeForSelector(page, \"#frame\", \"src\")) ?? \"\"\n\n  expect(src, \"updates src attribute\").toContain(\"/src/tests/fixtures/frames/frame.html\")\n  await expect(title).toHaveText(\"Frames\")\n  await expect(frameTitle).toHaveText(\"Frame: Loaded\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/frames/frame.html\"))\n  await expect(page.locator(\"#frame\"), \"marks the frame as [complete]\").toHaveAttribute(\"complete\")\n})\n\ntest(\"turbo:before-fetch-request fires on the frame element\", async ({ page }) => {\n  await page.click(\"#hello a\")\n  expect(await nextEventOnTarget(page, \"frame\", \"turbo:before-fetch-request\")).toBeTruthy()\n})\n\ntest(\"turbo:before-fetch-response fires on the frame element\", async ({ page }) => {\n  await page.click(\"#hello a\")\n  expect(await nextEventOnTarget(page, \"frame\", \"turbo:before-fetch-response\")).toBeTruthy()\n})\n\ntest(\"navigating a eager frame with a link[method=get] that does not fetch eager frame twice\", async ({\n  page\n}) => {\n  await page.click(\"#link-to-eager-loaded-frame\")\n\n  await nextBeat()\n\n  const eventLogs = await readEventLogs(page)\n  const fetchLogs = eventLogs.filter(\n    ([name, options]) =>\n      name == \"turbo:before-fetch-request\" && options?.url?.includes(\"/src/tests/fixtures/frames/frame_for_eager.html\")\n  )\n  expect(fetchLogs.length).toEqual(1)\n\n  const src = (await attributeForSelector(page, \"#eager-loaded-frame\", \"src\")) ?? \"\"\n  expect(src, \"updates src attribute\").toContain(\"/src/tests/fixtures/frames/frame_for_eager.html\")\n  await expect(page.locator(\"h1\")).toHaveText(\"Eager-loaded frame\")\n  await expect(page.locator(\"#eager-loaded-frame h2\")).toHaveText(\"Eager-loaded frame: Loaded\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/page_with_eager_frame.html\"))\n})\n\ntest(\"form submissions from frames clear snapshot cache\", async ({ page }) => {\n  await page.evaluate(() => {\n    document.querySelector(\"h1\").textContent = \"Changed\"\n  })\n\n  await expect(page.locator(\"h1\")).toHaveText(\"Changed\")\n\n  await page.click(\"#navigate-form-redirect-as-new\")\n  await expect(page.locator(\"h1\")).toHaveText(\"Page One Form\")\n  await page.click(\"#submit-form\")\n  await expect(page.locator(\"h2\")).toHaveText(\"Form Redirected\")\n  await page.goBack()\n\n  await expect(page.locator(\"h1\")).not.toHaveText(\"Changed\")\n})\n\nasync function withoutChangingEventListenersCount(page, callback) {\n  const name = \"eventListenersAttachedToDocument\"\n  const setup = () => {\n    return page.evaluate((name) => {\n      const context = window\n      context[name] = 0\n      context.originals = {\n        addEventListener: document.addEventListener,\n        removeEventListener: document.removeEventListener\n      }\n\n      document.addEventListener = (type, listener, options) => {\n        context.originals.addEventListener.call(document, type, listener, options)\n        context[name] += 1\n      }\n\n      document.removeEventListener = (type, listener, options) => {\n        context.originals.removeEventListener.call(document, type, listener, options)\n        context[name] -= 1\n      }\n\n      return context[name] || 0\n    }, name)\n  }\n\n  const teardown = () => {\n    return page.evaluate((name) => {\n      const context = window\n      const { addEventListener, removeEventListener } = context.originals\n\n      document.addEventListener = addEventListener\n      document.removeEventListener = removeEventListener\n\n      return context[name] || 0\n    }, name)\n  }\n\n  await nextBeat()\n  const originalCount = await setup()\n  await callback()\n  const finalCount = await teardown()\n\n  expect(finalCount, \"expected callback not to leak event listeners\").toEqual(originalCount)\n}\n\nfunction frameScriptEvaluationCount(page) {\n  return page.evaluate(() => window.frameScriptEvaluationCount)\n}\n"
  },
  {
    "path": "src/tests/functional/import_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\n\ntest(\"window variable with ESM\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/esm.html\")\n  await assertTurboInterface(page)\n})\n\ntest(\"window variable with UMD\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/umd.html\")\n  await assertTurboInterface(page)\n})\n\nasync function assertTurboInterface(page) {\n  await assertTypeOf(page, \"Turbo\", \"object\")\n  await assertTypeOf(page, \"Turbo.StreamActions\", \"object\")\n  await assertTypeOf(page, \"Turbo.start\", \"function\")\n  await assertTypeOf(page, \"Turbo.registerAdapter\", \"function\")\n  await assertTypeOf(page, \"Turbo.visit\", \"function\")\n  await assertTypeOf(page, \"Turbo.connectStreamSource\", \"function\")\n  await assertTypeOf(page, \"Turbo.disconnectStreamSource\", \"function\")\n  await assertTypeOf(page, \"Turbo.renderStreamMessage\", \"function\")\n  await assertTypeOf(page, \"Turbo.setProgressBarDelay\", \"function\")\n  await assertTypeOf(page, \"Turbo.setConfirmMethod\", \"function\")\n  await assertTypeOf(page, \"Turbo.setFormMode\", \"function\")\n  await assertTypeOf(page, \"Turbo.cache\", \"object\")\n  await assertTypeOf(page, \"Turbo.cache.clear\", \"function\")\n  await assertTypeOf(page, \"Turbo.navigator\", \"object\")\n  await assertTypeOf(page, \"Turbo.session\", \"object\")\n}\n\nasync function assertTypeOf(page, propertyName, propertyType) {\n  const type = await page.evaluate((propertyName) => {\n    const parts = propertyName.split(\".\")\n    let object = window\n    parts.forEach((_part, i) => {\n      object = object[parts[i]]\n    })\n    return typeof object\n  }, propertyName)\n\n  expect(type, `Expected ${propertyName} to be ${propertyType}`).toEqual(propertyType)\n}\n"
  },
  {
    "path": "src/tests/functional/link_prefetch_observer_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport { nextBeat, nextEventOnTarget, noNextEventNamed, noNextEventOnTarget, sleep } from \"../helpers/page\"\nimport fs from \"fs\"\nimport path from \"path\"\n\n// eslint-disable-next-line no-undef\nconst fixturesDir = path.join(process.cwd(), \"src\", \"tests\", \"fixtures\")\n\ntest.afterEach(() => {\n  fs.readdirSync(fixturesDir).forEach(file => {\n    if (file.startsWith(\"volatile_posts_database\")) {\n      fs.unlinkSync(path.join(fixturesDir, file))\n    }\n  })\n})\n\ntest(\"it prefetches the page\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n\n  const link = page.locator(\"#anchor_for_prefetch\")\n\n  await link.hover()\n  await nextEventOnTarget(page, \"anchor_for_prefetch\", \"turbo:before-prefetch\")\n  const { url, fetchOptions } = await nextEventOnTarget(page, \"anchor_for_prefetch\", \"turbo:before-fetch-request\")\n\n  await expect(link).toHaveJSProperty(\"href\", url)\n  expect(fetchOptions.headers[\"X-Sec-Purpose\"]).toEqual(\"prefetch\")\n  expect(fetchOptions.priority).toEqual(\"low\")\n\n  await link.hover()\n  await noNextEventOnTarget(page, \"anchor_for_prefetch\", \"turbo:before-fetch-request\")\n  await link.click()\n  await noNextEventOnTarget(page, \"anchor_for_prefetch\", \"turbo:before-fetch-request\")\n\n  await expect(page.locator(\"body\")).toHaveText(\"Prefetched Page Content\")\n})\n\ntest(\"it doesn't follow the link\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await hoverSelector({ page, selector: \"#anchor_for_prefetch\" })\n\n  await expect(page).toHaveTitle(\"Hover to Prefetch\")\n})\n\ntest(\"prefetches the page when link has a whole valid url as a href\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await assertPrefetchedOnHover({ page, selector: \"#anchor_with_whole_url\" })\n})\n\ntest(\"it prefetches the page when link has the same location but with a query string\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await assertPrefetchedOnHover({ page, selector: \"#anchor_for_same_location_with_query\" })\n})\n\ntest(\"it doesn't prefetch the page when link is inside an element with data-turbo=false\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await assertNotPrefetchedOnHover({ page, selector: \"#anchor_with_turbo_false_parent\" })\n})\n\ntest(\"it doesn't prefetch the page when link is inside an element with data-turbo-prefetch=false\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await assertNotPrefetchedOnHover({ page, selector: \"#anchor_with_turbo_prefetch_false_parent\" })\n})\n\ntest(\"it does prefech the page when link is inside a container with data-turbo-prefetch=true, that is within an element with data-turbo-prefetch=false\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await assertPrefetchedOnHover({ page, selector: \"#anchor_with_turbo_prefetch_true_parent_within_turbo_prefetch_false_parent\" })\n})\n\ntest(\"it doesn't prefetch the page when link has data-turbo-prefetch=false\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await assertNotPrefetchedOnHover({ page, selector: \"#anchor_with_turbo_prefetch_false\" })\n})\n\ntest(\"it doesn't prefetch the page when link has data-turbo=false\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await assertNotPrefetchedOnHover({ page, selector: \"#anchor_with_turbo_false\" })\n})\n\ntest(\"allows to cancel prefetch requests with custom logic\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n\n  const link = page.locator(\"#anchor_for_prefetch\")\n  await link.evaluate(a => a.addEventListener(\"turbo:before-prefetch\", event => event.preventDefault()))\n\n  await link.hover()\n  await nextEventOnTarget(page, \"anchor_for_prefetch\", \"turbo:before-prefetch\")\n  await noNextEventNamed(page, \"turbo:before-fetch-request\")\n  await link.click()\n\n  await expect(page.locator(\"body\")).toHaveText(\"Prefetched Page Content\")\n})\n\ntest(\"it doesn't prefetch UJS links\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await assertNotPrefetchedOnHover({ page, selector: \"#anchor_with_remote_true\" })\n})\n\ntest(\"it doesn't prefetch data-turbo-stream links\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await assertNotPrefetchedOnHover({ page, selector: \"#anchor_with_turbo_stream\" })\n})\n\ntest(\"it doesn't prefetch data-turbo-confirm links\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await assertNotPrefetchedOnHover({ page, selector: \"#anchor_with_turbo_confirm\" })\n})\n\ntest(\"it doesn't prefetch the page when link has the same location\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await assertNotPrefetchedOnHover({ page, selector: \"#anchor_for_same_location\" })\n})\n\ntest(\"it doesn't prefetch the page when link has a different origin\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await assertNotPrefetchedOnHover({ page, selector: \"#anchor_for_different_origin\" })\n})\n\ntest(\"it doesn't prefetch the page when link has a hash as a href\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await assertNotPrefetchedOnHover({ page, selector: \"#anchor_with_hash\" })\n})\n\ntest(\"it doesn't prefetch the page when link has a ftp protocol\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await assertNotPrefetchedOnHover({ page, selector: \"#anchor_with_ftp_protocol\" })\n})\n\ntest(\"it doesn't prefetch the page when links is valid but it's inside an iframe\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await assertNotPrefetchedOnHover({ page, selector: \"#anchor_with_iframe_target\" })\n})\n\ntest(\"it doesn't prefetch the page when link has a POST data-turbo-method\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await assertNotPrefetchedOnHover({ page, selector: \"#anchor_with_post_method\" })\n})\n\ntest(\"it doesn't prefetch the page when turbo-prefetch meta tag is set to false\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch_disabled.html\" })\n  await assertNotPrefetchedOnHover({ page, selector: \"#anchor_for_prefetch\" })\n})\n\ntest(\"it doesn't prefetch the page when turbo-prefetch meta tag is set to true, but is later set to false\", async ({\n  page\n}) => {\n  await goTo({ page, path: \"/hover_to_prefetch_custom_cache_time.html\" })\n  await assertPrefetchedOnHover({ page, selector: \"#anchor_for_prefetch\" })\n\n  await page.evaluate(() => {\n    const meta = document.querySelector('meta[name=\"turbo-prefetch\"]')\n    meta.setAttribute(\"content\", \"false\")\n  })\n\n  await sleep(10)\n  await page.mouse.move(0, 0)\n\n  await assertNotPrefetchedOnHover({ page, selector: \"#anchor_for_prefetch\" })\n})\n\ntest(\"it prefetches when visiting a page without the meta tag, then visiting a page with it\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch_without_meta_tag_with_link_to_with_meta_tag.html\" })\n\n  await clickSelector({ page, selector: \"#anchor_for_page_with_meta_tag\" })\n\n  await assertPrefetchedOnHover({ page, selector: \"#anchor_for_prefetch\" })\n})\n\ntest(\"it prefetches the page when turbo-prefetch-cache-time is set to 1\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch_custom_cache_time.html\" })\n  await assertPrefetchedOnHover({ page, selector: \"#anchor_for_prefetch\" })\n})\n\ntest(\"it caches the request for 1 millisecond when turbo-prefetch-cache-time is set to 1\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch_custom_cache_time.html\" })\n  await assertPrefetchedOnHover({ page, selector: \"#anchor_for_prefetch\" })\n\n  await sleep(10)\n  await page.mouse.move(0, 0)\n\n  await assertPrefetchedOnHover({ page, selector: \"#anchor_for_prefetch\" })\n})\n\ntest(\"it prefetches links with inner elements\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await assertPrefetchedOnHover({ page, selector: \"#anchor_with_inner_elements\" })\n})\n\ntest(\"it prefetches links inside a turbo frame\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n\n  await assertPrefetchedOnHover({ page, selector: \"#anchor_for_prefetch_in_frame\", callback: (request) => {\n    const turboFrameHeader = request.headers()[\"turbo-frame\"]\n    expect(turboFrameHeader).toEqual(\"frame_for_prefetch\")\n  }})\n})\n\n\ntest(\"doesn't include a turbo-frame header when the link is inside a turbo frame with a target=_top\", async ({ page}) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n\n  await assertPrefetchedOnHover({ page, selector: \"#anchor_for_prefetch_in_frame_target_top\", callback: (request) => {\n    const turboFrameHeader = request.headers()[\"turbo-frame\"]\n    expect(turboFrameHeader).toEqual(undefined)\n  }})\n})\n\ntest(\"it prefetches links with a delay\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n\n  await assertRequestNotMade(page, async () => {\n    await page.hover(\"#anchor_for_prefetch\")\n    await sleep(75)\n  })\n\n  await assertRequestMade(page, async () => {\n    await sleep(100)\n  })\n})\n\ntest(\"it cancels the prefetch request if the link is no longer hovered\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n\n  await assertRequestNotMade(page, async () => {\n    await page.hover(\"#anchor_for_prefetch\")\n    await sleep(75)\n  })\n\n  await assertRequestNotMade(page, async () => {\n    await page.mouse.move(0, 0)\n    await sleep(100)\n  })\n})\n\ntest(\"it resets the cache when a link is hovered\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n\n  let requestCount = 0\n  page.on(\"request\", async () => (requestCount++))\n\n  await page.hover(\"#anchor_for_prefetch\")\n  await sleep(200)\n\n  expect(requestCount).toEqual(1)\n  await page.mouse.move(0, 0)\n\n  await page.hover(\"#anchor_for_prefetch\")\n  await sleep(200)\n\n  expect(requestCount).toEqual(2)\n})\n\ntest(\"it does not make a network request when clicking on a link that has been prefetched\", async ({ page }) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await assertPrefetchedOnHover({ page, selector: \"#anchor_for_prefetch\" })\n  await assertRequestNotMadeOnClick({ page, selector: \"#anchor_for_prefetch\" })\n})\n\ntest(\"it follows the link using the cached response when clicking on a link that has been prefetched\", async ({\n  page\n}) => {\n  await goTo({ page, path: \"/hover_to_prefetch.html\" })\n  await hoverSelector({ page, selector: \"#anchor_for_prefetch\" })\n\n  await clickSelector({ page, selector: \"#anchor_for_prefetch\" })\n  await expect(page).toHaveTitle(\"Prefetched Page\")\n})\n\nconst assertPrefetchedOnHover = async ({ page, selector, callback }) => {\n  await assertRequestMade(page, async () => {\n    await hoverSelector({ page, selector })\n\n    await sleep(100)\n  }, callback)\n}\n\nconst assertNotPrefetchedOnHover = async ({ page, selector, callback }) => {\n  await assertRequestNotMade(page, async () => {\n    await hoverSelector({ page, selector })\n\n    await sleep(100)\n  }, callback)\n}\n\nconst assertRequestNotMadeOnClick = async ({ page, selector }) => {\n  await assertRequestNotMade(page, async () => {\n    await clickSelector({ page, selector })\n  })\n}\n\nconst assertRequestMade = async (page, action, callback) => {\n  let requestMade = false\n  page.on(\"request\", async (request) => requestMade = request)\n\n  await action()\n\n  expect(!!requestMade, \"Network request wasn't made when it should have been.\").toEqual(true)\n\n  if (callback) {\n    await callback(requestMade)\n  }\n}\n\nconst assertRequestNotMade = async (page, action, callback) => {\n  let requestMade = false\n  page.on(\"request\", async (request) => requestMade = request)\n\n  await action()\n\n  expect(!!requestMade, \"Network request was made when it should not have been.\").toEqual(false)\n\n  if (callback) {\n    await callback(requestMade)\n  }\n}\n\nconst goTo = async ({ page, path }) => {\n  await page.goto(`/src/tests/fixtures${path}`)\n  await nextBeat()\n}\n\nconst hoverSelector = async ({ page, selector }) => {\n  await page.hover(selector)\n  await nextBeat()\n}\n\nconst clickSelector = async ({ page, selector }) => {\n  await page.click(selector)\n  await nextBeat()\n}\n"
  },
  {
    "path": "src/tests/functional/loading_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport {\n  attributeForSelector,\n  nextAttributeMutationNamed,\n  nextBeat,\n  nextBody,\n  nextEventNamed,\n  nextEventOnTarget,\n  noNextEventOnTarget,\n  readEventLogs\n} from \"../helpers/page\"\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/loading.html\")\n  await readEventLogs(page)\n})\n\ntest(\"eager loading within a details element\", async ({ page }) => {\n  await expect(page.locator(\"#loading-eager turbo-frame#frame h2\")).toBeAttached()\n  await expect(page.locator(\"#loading-eager turbo-frame\"), \"has [complete] attribute\").toHaveAttribute(\"complete\")\n})\n\ntest(\"lazy loading within a details element\", async ({ page }) => {\n  await expect(page.locator(\"#loading-lazy turbo-frame h2\")).not.toBeAttached()\n  await expect(page.locator(\"#loading-lazy turbo-frame\")).not.toHaveAttribute(\"complete\")\n\n  await page.click(\"#loading-lazy summary\")\n\n  await expect(page.locator(\"#loading-lazy turbo-frame h2\")).toHaveText(\"Hello from a frame\")\n  await expect(page.locator(\"#loading-lazy turbo-frame\"), \"has [complete] attribute\").toHaveAttribute(\"complete\")\n})\n\ntest(\"changing loading attribute from lazy to eager loads frame\", async ({ page }) => {\n  const frame = page.locator(\"#loading-lazy turbo-frame\")\n\n  await expect(frame.locator(\"h2\")).not.toBeAttached()\n\n  await frame.evaluate((frame) => frame.setAttribute(\"loading\", \"eager\"))\n\n  await page.click(\"#loading-lazy summary\")\n  await expect(frame.locator(\"h2\")).toHaveText(\"Hello from a frame\")\n})\n\ntest(\"navigating a visible frame with loading=lazy navigates\", async ({ page }) => {\n  await page.click(\"#loading-lazy summary\")\n\n  await expect(page.locator(\"#hello h2\")).toHaveText(\"Hello from a frame\")\n\n  await page.click(\"#hello a\")\n\n  await expect(page.locator(\"#hello h2\")).toHaveText(\"Frames: #hello\")\n})\n\ntest(\"changing src attribute on a frame with loading=lazy defers navigation\", async ({ page }) => {\n  const frame = page.locator(\"#loading-lazy turbo-frame\")\n\n  await frame.evaluate((frame) =>\n    frame.setAttribute(\"src\", \"/src/tests/fixtures/frames.html\")\n  )\n  await expect(frame.locator(\"h2\")).not.toBeAttached()\n\n  await page.click(\"#loading-lazy summary\")\n\n  await expect(frame.locator(\"h2\")).toHaveText(\"Frames: #hello\")\n})\n\ntest(\"changing src attribute on a frame with loading=eager navigates\", async ({ page }) => {\n  const frame = page.locator(\"#loading-eager turbo-frame\")\n\n  await frame.evaluate((frame) =>\n    frame.setAttribute(\"src\", \"/src/tests/fixtures/frames.html\")\n  )\n\n  await page.click(\"#loading-eager summary\")\n\n  await expect(frame.locator(\"h2\")).toHaveText(\"Frames: #frame\")\n})\n\ntest(\"reloading a frame reloads the content\", async ({ page }) => {\n  await page.click(\"#loading-eager summary\")\n  await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")\n\n  const frame = page.locator(\"#loading-eager turbo-frame#frame\")\n  await expect(frame.locator(\"h2\")).toBeAttached()\n  expect(await nextAttributeMutationNamed(page, \"frame\", \"complete\"), \"has [complete] attribute\").toEqual(\"\")\n\n  await frame.evaluate((frame) => frame.reload())\n  await expect(frame.locator(\"h2\")).toBeAttached()\n  expect(await nextAttributeMutationNamed(page, \"frame\", \"complete\"), \"clears [complete] attribute\").toEqual(null)\n})\n\ntest(\"navigating away from a page does not reload its frames\", async ({ page }) => {\n  await page.click(\"#one\")\n  await nextBody(page)\n\n  const eventLogs = await readEventLogs(page)\n  const requestLogs = eventLogs.filter(([name]) => name == \"turbo:before-fetch-request\")\n  expect(requestLogs.length).toEqual(1)\n})\n\ntest(\"changing [src] attribute on a [complete] frame with loading=lazy defers navigation\", async ({ page }) => {\n  await page.click(\"#loading-lazy summary\")\n  await nextEventOnTarget(page, \"hello\", \"turbo:frame-load\")\n\n  await expect(page.locator(\"#loading-lazy turbo-frame\"), \"lazy frame is complete\").toHaveAttribute(\"complete\")\n  await expect(page.locator(\"#hello h2\")).toHaveText(\"Hello from a frame\")\n\n  await page.click(\"#loading-lazy summary\")\n  await page.click(\"#one\")\n  await nextEventNamed(page, \"turbo:load\")\n  await page.goBack()\n  await nextEventNamed(page, \"turbo:load\")\n\n  expect(await noNextEventOnTarget(page, \"hello\", \"turbo:frame-load\")).toBeTruthy()\n\n  let src = new URL((await attributeForSelector(page, \"#hello\", \"src\")) || \"\")\n\n  await expect(page.locator(\"#loading-lazy turbo-frame\"), \"lazy frame is complete\").toHaveAttribute(\"complete\")\n  expect(src.pathname, \"lazy frame retains [src]\").toEqual(\"/src/tests/fixtures/frames/hello.html\")\n\n  await page.click(\"#link-lazy-frame\")\n\n  expect(await noNextEventOnTarget(page, \"hello\", \"turbo:frame-load\")).toBeTruthy()\n  await expect(page.locator(\"#loading-lazy turbo-frame\"), \"lazy frame is not complete\").not.toHaveAttribute(\"complete\")\n\n  await page.click(\"#loading-lazy summary\")\n  await nextEventOnTarget(page, \"hello\", \"turbo:frame-load\")\n\n  src = new URL((await attributeForSelector(page, \"#hello\", \"src\")) || \"\")\n\n  await expect(page.locator(\"#loading-lazy turbo-frame h2\")).toHaveText(\"Frames: #hello\")\n  await expect(page.locator(\"#loading-lazy turbo-frame\"), \"lazy frame is complete\").toHaveAttribute(\"complete\")\n  expect(src.pathname, \"lazy frame navigates\").toEqual(\"/src/tests/fixtures/frames.html\")\n})\n\ntest(\"navigating away from a page and then back does not reload its frames\", async ({ page }) => {\n  await page.click(\"#one\")\n  await nextBody(page)\n  await readEventLogs(page)\n  await page.goBack()\n  await nextBody(page)\n\n  const eventLogs = await readEventLogs(page)\n  const requestLogs = eventLogs.filter(([name]) => name == \"turbo:before-fetch-request\")\n  const requestsOnEagerFrame = requestLogs.filter((record) => record[2] == \"frame\")\n  const requestsOnLazyFrame = requestLogs.filter((record) => record[2] == \"hello\")\n\n  expect(requestsOnEagerFrame.length, \"does not reload eager frame\").toEqual(0)\n  expect(requestsOnLazyFrame.length, \"does not reload lazy frame\").toEqual(0)\n\n  await page.click(\"#loading-lazy summary\")\n  await nextEventOnTarget(page, \"hello\", \"turbo:before-fetch-request\")\n  await nextEventOnTarget(page, \"hello\", \"turbo:frame-render\")\n  await nextEventOnTarget(page, \"hello\", \"turbo:frame-load\")\n})\n\ntest(\"disconnecting and reconnecting a frame does not reload the frame\", async ({ page }) => {\n  await nextBeat()\n\n  await page.evaluate(() => {\n    window.savedElement = document.querySelector(\"#loading-eager\")\n    window.savedElement?.remove()\n  })\n  await nextBeat()\n\n  await page.evaluate(() => {\n    if (window.savedElement) {\n      document.body.appendChild(window.savedElement)\n    }\n  })\n  await nextBeat()\n\n  const eventLogs = await readEventLogs(page)\n  const requestLogs = eventLogs.filter(([name]) => name == \"turbo:before-fetch-request\")\n  expect(requestLogs.length).toEqual(0)\n})\n"
  },
  {
    "path": "src/tests/functional/navigation_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport {\n  clickWithoutScrolling,\n  isScrolledToSelector,\n  nextAttributeMutationNamed,\n  nextBeat,\n  nextBody,\n  nextEventNamed,\n  noNextEventNamed,\n  pathname,\n  pathnameForIFrame,\n  readEventLogs,\n  visitAction,\n  willChangeBody,\n  withHash,\n  withPathname,\n  withSearch,\n  withSearchParam\n} from \"../helpers/page\"\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/navigation.html\")\n  await readEventLogs(page)\n})\n\ntest(\"navigating renders a progress bar until the next turbo:load\", async ({ page }) => {\n  await page.evaluate(() => window.Turbo.setProgressBarDelay(0))\n  await page.click(\"#delayed-link\")\n\n  await expect(page.locator(\".turbo-progress-bar\"), \"displays progress bar\").toBeAttached()\n\n  await nextEventNamed(page, \"turbo:render\")\n  await expect(page.locator(\".turbo-progress-bar\"), \"displays progress bar\").toBeAttached()\n\n  await nextEventNamed(page, \"turbo:load\")\n  await expect(page.locator(\".turbo-progress-bar\"), \"hides progress bar\").not.toBeAttached()\n})\n\ntest(\"navigating does not render a progress bar before expiring the delay\", async ({ page }) => {\n  await page.evaluate(() => window.Turbo.setProgressBarDelay(1000))\n  await page.click(\"#same-origin-unannotated-link\")\n\n  await expect(page.locator(\".turbo-progress-bar\"), \"does not show progress bar before delay\").not.toBeAttached()\n})\n\ntest(\"navigating hides the progress bar on failure\", async ({ page }) => {\n  await page.evaluate(() => window.Turbo.setProgressBarDelay(0))\n  await page.click(\"#delayed-failure-link\")\n\n  await expect(page.locator(\".turbo-progress-bar\")).toBeAttached()\n  await expect(page.locator(\".turbo-progress-bar\")).not.toBeAttached()\n})\n\ntest(\"after loading the page\", async ({ page }) => {\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/navigation.html\"))\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n\ntest(\"following a same-origin unannotated link\", async ({ page }) => {\n  await page.click(\"#same-origin-unannotated-link\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"advance\")\n  expect(\n    await nextAttributeMutationNamed(page, \"html\", \"aria-busy\"),\n    \"sets [aria-busy] on the document element\"\n  ).toEqual(\n    \"true\"\n  )\n  expect(\n    await nextAttributeMutationNamed(page, \"html\", \"aria-busy\"),\n    \"removes [aria-busy] from the document element\"\n  ).toEqual(\n    null\n  )\n})\n\ntest(\"following a same-origin unannotated custom element link\", async ({ page }) => {\n  await nextBeat()\n  await page.evaluate(() => {\n    const shadowRoot = document.querySelector(\"#custom-link-element\")?.shadowRoot\n    const link = shadowRoot?.querySelector(\"a\")\n    link?.click()\n  })\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  await expect(page).toHaveURL(withSearch(\"\"))\n  expect(await visitAction(page)).toEqual(\"advance\")\n})\n\ntest(\"drive enabled; click an element in the shadow DOM wrapped by a link in the light DOM\", async ({ page }) => {\n  await page.click(\"#shadow-dom-drive-enabled span\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"advance\")\n})\n\ntest(\"drive disabled; click an element in the shadow DOM within data-turbo='false'\", async ({ page }) => {\n  await page.click(\"#shadow-dom-drive-disabled span\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n\ntest(\"drive enabled; click an element in the slot\", async ({ page }) => {\n  await page.click(\"#element-in-slot\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"advance\")\n})\n\ntest(\"drive disabled; click an element in the slot within data-turbo='false'\", async ({ page }) => {\n  await page.click(\"#element-in-slot-disabled\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n\ntest(\"drive disabled; click an element in the nested slot within data-turbo='false'\", async ({ page }) => {\n  await page.click(\"#element-in-nested-slot-disabled\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n\ntest(\"following a same-origin unannotated link with search params\", async ({ page }) => {\n  await page.click(\"#same-origin-unannotated-link-search-params\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  await expect(page).toHaveURL(withSearch(\"?key=value\"))\n  expect(await visitAction(page)).toEqual(\"advance\")\n})\n\ntest(\"following a same-origin unannotated form[method=GET]\", async ({ page }) => {\n  await page.click(\"#same-origin-unannotated-form button\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"advance\")\n})\n\ntest(\"following a same-origin data-turbo-method=get link\", async ({ page }) => {\n  await page.click(\"#same-origin-get-link-form\")\n  await nextEventNamed(page, \"turbo:submit-start\")\n  await nextEventNamed(page, \"turbo:submit-end\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/navigation.html\"))\n  await expect(page).toHaveURL(withSearchParam(\"a\", \"one\"))\n  await expect(page).toHaveURL(withSearchParam(\"b\", \"two\"))\n})\n\ntest(\"following a same-origin data-turbo-action=replace link\", async ({ page }) => {\n  await page.click(\"#same-origin-replace-link\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"replace\")\n})\n\ntest(\"following a same-origin GET form[data-turbo-action=replace]\", async ({ page }) => {\n  await page.click(\"#same-origin-replace-form-get button\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"replace\")\n})\n\ntest(\"following a same-origin GET form button[data-turbo-action=replace]\", async ({ page }) => {\n  await page.click(\"#same-origin-replace-form-submitter-get button\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"replace\")\n})\n\ntest(\"following a same-origin POST form[data-turbo-action=replace]\", async ({ page }) => {\n  await page.click(\"#same-origin-replace-form-post button\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"replace\")\n})\n\ntest(\"following a same-origin POST form button[data-turbo-action=replace]\", async ({ page }) => {\n  await page.click(\"#same-origin-replace-form-submitter-post button\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"replace\")\n})\n\ntest(\"following a POST form clears cache\", async ({ page }) => {\n  await page.evaluate(() => {\n    const cachedElement = document.createElement(\"some-cached-element\")\n    document.body.appendChild(cachedElement)\n  })\n\n  await page.click(\"#form-post-submit\")\n  await nextBeat() // 301 redirect response\n  await nextBeat() // 200 response\n  await page.goBack()\n  await expect(page.locator(\"some-cached-element\")).not.toBeAttached()\n})\n\ntest(\"following a same-origin POST link with data-turbo-action=replace\", async ({ page }) => {\n  await page.click(\"#same-origin-replace-post-link\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"replace\")\n})\n\ntest(\"following a same-origin data-turbo=false link\", async ({ page }) => {\n  await page.click(\"#same-origin-false-link\")\n  await page.waitForEvent(\"load\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n\ntest(\"following a same-origin unannotated link inside a data-turbo=false container\", async ({ page }) => {\n  await page.click(\"#same-origin-unannotated-link-inside-false-container\")\n  await page.waitForEvent(\"load\")\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n\ntest(\"following a same-origin data-turbo=true link inside a data-turbo=false container\", async ({ page }) => {\n  await page.click(\"#same-origin-true-link-inside-false-container\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"advance\")\n})\n\ntest(\"following a same-origin anchored link\", async ({ page }) => {\n  await page.click(\"#same-origin-anchored-link\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  await expect(page).toHaveURL(withHash(\"#element-id\"))\n  expect(await visitAction(page)).toEqual(\"advance\")\n  expect(await isScrolledToSelector(page, \"#element-id\")).toEqual(true)\n})\n\ntest(\"following a same-origin link to a named anchor\", async ({ page }) => {\n  await page.click(\"#same-origin-anchored-link-named\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  await expect(page).toHaveURL(withHash(\"#named-anchor\"))\n  expect(await visitAction(page)).toEqual(\"advance\")\n  expect(await isScrolledToSelector(page, \"[name=named-anchor]\")).toEqual(true)\n})\n\ntest(\"following a cross-origin unannotated link\", async ({ page }) => {\n  await page.click(\"#cross-origin-unannotated-link\")\n\n  await expect(page).toHaveURL(\"about:blank\")\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n\ntest(\"following a same-origin [target] link\", async ({ page }) => {\n  const [popup] = await Promise.all([page.waitForEvent(\"popup\"), page.click(\"#same-origin-targeted-link\")])\n\n  expect(pathname(popup.url())).toEqual(\"/src/tests/fixtures/one.html\")\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n\ntest(\"following a _self [target] link\", async ({ page }) => {\n  await page.click(\"#self-targeted-link\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"advance\")\n})\n\ntest(\"following an empty [target] link\", async ({ page }) => {\n  await page.click(\"#empty-target-link\")\n  await nextBody(page)\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"advance\")\n})\n\ntest(\"following a bare [target] link\", async ({ page }) => {\n  await page.click(\"#bare-target-link\")\n  await nextBody(page)\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"advance\")\n})\n\ntest(\"following a same-origin [download] link\", async ({ page }) => {\n  expect(\n    await willChangeBody(page, async () => {\n      await page.click(\"#same-origin-download-link\")\n      await nextBeat()\n    })\n  ).toEqual(false)\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/navigation.html\"))\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n\ntest(\"following a same-origin link inside an SVG element\", async ({ page }) => {\n  const link = page.locator(\"#same-origin-link-inside-svg-element\")\n  await link.focus()\n  await page.keyboard.press(\"Enter\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n\ntest(\"following a cross-origin link inside an SVG element\", async ({ page }) => {\n  const link = page.locator(\"#cross-origin-link-inside-svg-element\")\n  await link.focus()\n  await page.keyboard.press(\"Enter\")\n\n  await expect(page).toHaveURL(\"about:blank\")\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n\ntest(\"clicking the back button\", async ({ page }) => {\n  await page.click(\"#same-origin-unannotated-link\")\n  await nextEventNamed(page, \"turbo:load\")\n  await page.goBack()\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/navigation.html\"))\n  expect(await visitAction(page)).toEqual(\"restore\")\n})\n\ntest(\"clicking the forward button\", async ({ page }) => {\n  await page.click(\"#same-origin-unannotated-link\")\n  await nextEventNamed(page, \"turbo:load\")\n  await page.goBack()\n  await page.goForward()\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"restore\")\n})\n\ntest(\"form submissions that redirect to a different location have a default advance action\", async ({ page }) => {\n  await page.click(\"#redirect-submit\")\n  await nextEventNamed(page, \"turbo:load\")\n  expect(await visitAction(page)).toEqual(\"advance\")\n})\n\ntest(\"form submissions that redirect to the current location have a default replace action\", async ({ page }) => {\n  await page.click(\"#refresh-submit\")\n  await nextEventNamed(page, \"turbo:load\")\n  expect(await visitAction(page)).toEqual(\"replace\")\n})\n\ntest(\"link targeting a disabled turbo-frame navigates the page\", async ({ page }) => {\n  await page.click(\"#link-to-disabled-frame\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/frames/hello.html\"))\n})\n\ntest(\"skip link with hash-only path scrolls to the anchor without a visit\", async ({ page }) => {\n  expect(\n    await willChangeBody(page, async () => {\n      await page.click('a[href=\"#main\"]')\n    })\n  ).not.toBeTruthy()\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/navigation.html\"))\n  await expect(page).toHaveURL(withHash(\"#main\"))\n  expect(await isScrolledToSelector(page, \"#main\"), \"scrolled to #main\").toEqual(true)\n})\n\ntest(\"skip link with hash-only path moves focus and changes tab order\", async ({ page }) => {\n  await page.click('a[href=\"#main\"]')\n  await nextBeat()\n  await page.press(\"#main\", \"Tab\")\n\n  await expect(page.locator(\"#ignored-link\"), \"skips interactive elements before #main\").not.toBeFocused()\n  await expect(page.locator(\"#main *:focus\"), \"moves focus inside #main\").toBeFocused()\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/navigation.html\"))\n  await expect(page).toHaveURL(withHash(\"#main\"))\n})\n\ntest(\"same-page anchored replace link assumes the intention was a refresh\", async ({ page }) => {\n  await page.click(\"#refresh-link\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/navigation.html\"))\n  await expect(page).toHaveURL(withHash(\"#main\"))\n  expect(await isScrolledToSelector(page, \"#main\"), \"scrolled to #main\").toEqual(true)\n})\n\ntest(\"navigating back to anchored URL\", async ({ page }) => {\n  await clickWithoutScrolling(page, 'a[href=\"#main\"]', { hasText: \"Skip Link\" })\n  await nextBeat()\n\n  await clickWithoutScrolling(page, \"#same-origin-unannotated-link\")\n  await nextBody(page)\n  await nextBeat()\n\n  await page.goBack()\n  await nextBody(page)\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/navigation.html\"))\n  await expect(page).toHaveURL(withHash(\"#main\"))\n  expect(await isScrolledToSelector(page, \"#main\"), \"scrolled to #main\").toEqual(true)\n})\n\ntest(\"following a redirection\", async ({ page }) => {\n  await page.click(\"#redirection-link\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"replace\")\n  await expect(page.locator(\".turbo-progress-bar\")).not.toBeAttached()\n})\n\ntest(\"clicking the back button after redirection\", async ({ page }) => {\n  await page.click(\"#redirection-link\")\n  await nextBody(page)\n  await page.goBack()\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/navigation.html\"))\n  expect(await visitAction(page)).toEqual(\"restore\")\n})\n\ntest(\"same-page anchor visits do not trigger visit events\", async ({ page }) => {\n  const events = [\n    \"turbo:before-visit\",\n    \"turbo:visit\",\n    \"turbo:before-cache\",\n    \"turbo:before-render\",\n    \"turbo:render\",\n    \"turbo:load\"\n  ]\n\n  for (const eventName in events) {\n    await page.goto(\"/src/tests/fixtures/navigation.html\")\n    await page.click('a[href=\"#main\"]')\n    expect(await noNextEventNamed(page, eventName), `same-page links do not trigger ${eventName} events`).toEqual(true)\n  }\n})\n\ntest(\"correct referrer header\", async ({ page }) => {\n  page.click(\"#headers-link\")\n  await nextEventNamed(page, \"turbo:load\")\n  const pre = await page.textContent(\"pre\")\n  const headers = await JSON.parse(pre || \"\")\n  expect(\n    headers.referer,\n    `referer header is correctly set`\n  ).toEqual(\n    \"http://localhost:9000/src/tests/fixtures/navigation.html\"\n  )\n})\n\ntest(\"double-clicking on a link\", async ({ page }) => {\n  await page.click(\"#delayed-link\", { clickCount: 2 })\n  await nextBeat()\n\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page).toHaveURL(withPathname(\"/__turbo/delayed_response\"))\n  expect(await visitAction(page)).toEqual(\"advance\")\n})\n\ntest(\"does not fire turbo:load twice after following a redirect\", async ({ page }) => {\n  await page.evaluate(() => {\n    window.turboLoadCount = 0\n    addEventListener(\"turbo:load\", () => window.turboLoadCount++)\n  })\n\n  page.click(\"#redirection-link\")\n\n  await nextBeat() // 301 redirect response\n\n  await nextBeat() // 200 response\n  await nextBody(page)\n  await nextEventNamed(page, \"turbo:load\")\n  await nextBeat()\n\n  expect(await page.evaluate(() => window.turboLoadCount)).toEqual(1)\n})\n\ntest(\"navigating back whilst a visit is in-flight\", async ({ page }) => {\n  page.click(\"#delayed-link\")\n  await nextEventNamed(page, \"turbo:before-render\")\n  await page.goBack()\n\n  expect(\n    await nextEventNamed(page, \"turbo:visit\"),\n    \"navigating back whilst a visit is in-flight starts a non-silent Visit\"\n  ).toBeTruthy()\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/navigation.html\"))\n  expect(await visitAction(page)).toEqual(\"restore\")\n})\n\ntest(\"ignores links with a [target] attribute that target an iframe with a matching [name]\", async ({ page }) => {\n  await page.click(\"#link-target-iframe\")\n  await nextBeat()\n  await noNextEventNamed(page, \"turbo:load\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/navigation.html\"))\n  expect(await pathnameForIFrame(page, \"iframe\")).toEqual(\"/src/tests/fixtures/one.html\")\n})\n\ntest(\"ignores links with a [target] attribute that targets an iframe with [name='']\", async ({ page }) => {\n  await page.click(\"#link-target-empty-name-iframe\")\n  await nextBeat()\n  await noNextEventNamed(page, \"turbo:load\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n})\n\ntest(\"ignores forms with a [target=_blank] attribute\", async ({ page }) => {\n  const [popup] = await Promise.all([page.waitForEvent(\"popup\"), page.click(\"#form-target-blank button\")])\n\n  await expect(popup).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n})\n\ntest(\"ignores forms with a [target] attribute that targets an iframe with a matching [name]\", async ({ page }) => {\n  await page.click(\"#form-target-iframe button\")\n  await nextBeat()\n  await noNextEventNamed(page, \"turbo:load\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/navigation.html\"))\n  expect(await pathnameForIFrame(page, \"iframe\")).toEqual(\"/src/tests/fixtures/one.html\")\n})\n\ntest(\"ignores forms with a button[formtarget=_blank] attribute\", async ({ page }) => {\n  const [popup] = await Promise.all([page.waitForEvent(\"popup\"), page.click(\"#button-formtarget-blank\")])\n\n  await expect(popup).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n})\n\ntest(\"ignores forms with a button[formtarget] attribute that targets an iframe with [name='']\", async ({ page }) => {\n  await page.click(\"#form-target-empty-name-iframe button\")\n  await nextBeat()\n  await noNextEventNamed(page, \"turbo:load\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n})\n\ntest(\"ignores forms with a button[formtarget] attribute that targets an iframe with a matching [name]\", async ({\n  page\n}) => {\n  await page.click(\"#button-formtarget-iframe\")\n  await nextBeat()\n  await noNextEventNamed(page, \"turbo:load\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/navigation.html\"))\n  expect(await pathnameForIFrame(page, \"iframe\")).toEqual(\"/src/tests/fixtures/one.html\")\n})\n\ntest(\"ignores forms with a [target] attribute that target an iframe with [name='']\", async ({ page }) => {\n  await page.click(\"#button-formtarget-empty-name-iframe\")\n  await nextBeat()\n  await noNextEventNamed(page, \"turbo:load\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n})\n"
  },
  {
    "path": "src/tests/functional/page_refresh_stream_action_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport { nextPageRefresh, readEventLogs, withPathname } from \"../helpers/page\"\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh_stream_action.html\")\n})\n\ntest(\"test refreshing the page\", async ({ page }) => {\n  const content = page.locator(\"#content\")\n  await expect(content).toHaveText(/Hello/)\n\n  await content.evaluate((content) => content.innerHTML = \"\")\n  await expect(content).not.toHaveText(/Hello/)\n\n  await page.click(\"#refresh button\")\n\n  await expect(content).toHaveText(/Hello/)\n})\n\ntest(\"don't refresh the page on self-originated request ids\", async ({ page }) => {\n  const content = page.locator(\"#content\")\n  await expect(content).toHaveText(/Hello/)\n\n  await content.evaluate((content) => content.innerHTML = \"\")\n  await page.evaluate(() => { window.Turbo.session.recentRequests.add(\"123\") })\n\n  await page.locator(\"#request-id\").evaluate((input) => input.value = \"123\")\n  await page.click(\"#refresh button\")\n  await nextPageRefresh(page)\n\n  await expect(content).not.toHaveText(/Hello/)\n})\n\ntest(\"fetch injects a Turbo-Request-Id with a UID generated automatically\", async ({ page }) => {\n  const response1 = await fetchRequestId(page)\n  const response2 = await fetchRequestId(page)\n\n  expect(response1).not.toEqual(response2)\n\n  for (const response of [response1, response2]) {\n    expect(response).toMatch(/.+-.+-.+-.+/)\n  }\n})\n\ntest(\"debounce stream page refreshes\", async ({ page }) => {\n  await page.click(\"#refresh button\")\n  await page.click(\"#refresh button\")\n  await nextPageRefresh(page)\n  await page.click(\"#refresh button\")\n  await nextPageRefresh(page)\n\n  const eventLogs = await readEventLogs(page)\n  const requestLogs = eventLogs.filter(([name]) => name == \"turbo:visit\")\n  expect(requestLogs.length).toEqual(2)\n})\n\ntest(\"debounced refresh of stale URL does not hijack new location navigated to\", async ({ page }) => {\n  await setLongerPageRefreshDebouncePeriod(page)\n\n  await page.click(\"#refresh button\")\n  await page.click(\"#regular-link\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n})\n\nasync function fetchRequestId(page) {\n  return await page.evaluate(async () => {\n    const response = await window.Turbo.fetch(\"/__turbo/request_id_header\")\n    return response.text()\n  })\n}\n\nasync function setLongerPageRefreshDebouncePeriod(page, period = 500) {\n  return page.evaluate((period) => window.Turbo.session.pageRefreshDebouncePeriod = period, period)\n}\n"
  },
  {
    "path": "src/tests/functional/page_refresh_tests.js",
    "content": "import { test, expect } from \"@playwright/test\"\nimport {\n  nextEventNamed,\n  nextEventOnTarget,\n  noNextEventOnTarget,\n  noNextEventNamed,\n  getSearchParam,\n  refreshWithStream\n} from \"../helpers/page\"\n\ntest(\"renders a page refresh with morphing\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  await page.click(\"#form-submit\")\n  await nextEventNamed(page, \"turbo:before-render\", { renderMethod: \"morph\" })\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n})\n\ntest(\"async page refresh with turbo-stream\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  await expect(page.locator(\"#title\")).toHaveText(\"Page to be refreshed\")\n\n  await page.evaluate(() => document.querySelector(\"#title\").innerText = \"Updated\")\n  await expect(page.locator(\"#title\")).toHaveText(\"Updated\")\n  await refreshWithStream(page)\n\n  await expect(page.locator(\"#title\")).not.toHaveText(\"Updated\")\n  await expect(page.locator(\"#title\")).toHaveText(\"Page to be refreshed\")\n  expect(await noNextEventNamed(page, \"turbo:before-cache\")).toBeTruthy()\n})\n\ntest(\"async page refresh with turbo-stream sequentially initiate Visits\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n  await refreshWithStream(page)\n  await nextEventNamed(page, \"turbo:morph\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await refreshWithStream(page)\n  await nextEventNamed(page, \"turbo:morph\")\n  await nextEventNamed(page, \"turbo:load\")\n})\n\ntest(\"async page refresh with turbo-stream does not interrupt an initiated Visit\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n  await page.click(\"#delayed_link\")\n  await refreshWithStream(page)\n\n  await expect(page.locator(\"h1\")).toHaveText(\"One\")\n})\n\ntest(\"dispatches a turbo:before-morph-element and turbo:morph-element event for each morphed element\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n  await page.fill(\"#form-text\", \"Morph me\")\n  await page.click(\"#form-submit\")\n\n  await nextEventOnTarget(page, \"form-text\", \"turbo:before-morph-element\")\n  await nextEventOnTarget(page, \"form-text\", \"turbo:morph-element\")\n})\n\ntest(\"preventing a turbo:before-morph-element prevents the morph\", async ({ page }) => {\n  const input = await page.locator(\"#form-text\")\n  const submit = await page.locator(\"#form-submit\")\n\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n  await input.evaluate((input) => input.addEventListener(\"turbo:before-morph-element\", (event) => event.preventDefault()))\n  await input.fill(\"Morph me\")\n  await submit.click()\n\n  await nextEventOnTarget(page, \"form-text\", \"turbo:before-morph-element\")\n  await noNextEventOnTarget(page, \"form-text\", \"turbo:morph-element\")\n\n  await expect(input).toHaveValue(\"Morph me\")\n})\n\ntest(\"turbo:morph-element Stimulus listeners can handle morphing\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  await expect(page.locator(\"#test-output\")).toHaveText(\"connected\")\n\n  await page.fill(\"#form-text\", \"Ignore me\")\n  await page.click(\"#form-submit\")\n\n  await expect(page.locator(\"#form-text\")).toHaveValue(\"\")\n  await expect(page.locator(\"#test-output\")).toHaveText(\"connected\")\n})\n\ntest(\"turbo:before-morph-attribute Stimulus listeners can handle morphing attributes\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n  const controller = page.locator(\"#stimulus-controller\")\n  const input = controller.locator(\"input\")\n\n  await expect(page.locator(\"#test-output\")).toHaveText(\"connected\")\n\n  await input.fill(\"controller state\")\n  await page.fill(\"#form-text\", \"Ignore me\")\n  await page.click(\"#form-submit\")\n\n  const { mutationType } = await nextEventOnTarget(page, \"stimulus-controller\", \"turbo:before-morph-attribute\", { attributeName: \"data-test-state-value\" })\n\n  await expect(mutationType).toEqual(\"update\")\n  await expect(controller).toHaveAttribute(\"data-test-state-value\", \"controller state\")\n  await expect(page.locator(\"#form-text\")).toHaveValue(\"\")\n  await expect(page.locator(\"#test-output\")).toHaveText(\"connected\")\n})\n\ntest(\"page refreshes cause a reload when assets change\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  await page.click(\"#add-new-assets\")\n  await expect(page.locator(\"#new-stylesheet\")).toHaveCount(1)\n  await page.click(\"#form-submit\")\n\n  await nextEventNamed(page, \"turbo:load\")\n  await expect(page.locator(\"#new-stylesheet\")).toHaveCount(0)\n})\n\ntest(\"reloading when assets change uses the response URL of a prior redirect\", async ({page}) => {\n  const destinations = []\n\n  page.on(\"request\", (request) => {\n    const path = new URL(request.url()).pathname\n\n    if (path === \"/__turbo/redirect\") {\n      destinations.push(\"redirect\")\n    } else if (path === \"/src/tests/fixtures/link_redirect_target.html\") {\n      destinations.push(\"target\")\n    }\n  })\n\n  await page.goto(\"/src/tests/fixtures/link_redirect.html\")\n  await page.click(\"#indirect\")\n\n  await page.waitForURL(\"/src/tests/fixtures/link_redirect_target.html\")\n\n  expect(destinations, \"redirects once\").toEqual(expect.arrayContaining([\"redirect\", \"target\", \"target\"]))\n})\n\ntest(\"renders a page refresh with morphing when the paths are the same but search params are different\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  await page.click(\"#replace-link\")\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n})\n\ntest(\"renders a page refresh with morphing when the GET form paths are the same but search params are different\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  const input = page.locator(\"form[method=get] input[name=query]\")\n\n  await input.fill(\"Search\")\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n\n  await expect(input).toBeFocused()\n  expect(getSearchParam(page.url(), \"query\")).toEqual(\"Search\")\n\n  await input.press(\"?\")\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n\n  await expect(input).toBeFocused()\n  expect(getSearchParam(page.url(), \"query\")).toEqual(\"Search?\")\n})\n\ntest(\"doesn't morph when the turbo-refresh-method meta tag is not 'morph'\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh_replace.html\")\n\n  await page.click(\"#form-submit\")\n  expect(await noNextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })).toBeTruthy()\n})\n\ntest(\"doesn't morph when the navigation doesn't go to the same URL\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  await page.click(\"#link\")\n  await expect(page.locator(\"h1\")).toHaveText(\"One\")\n\n  expect(await noNextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })).toBeTruthy()\n})\n\ntest(\"uses morphing to only update remote frames marked with refresh='morph'\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  await page.click(\"#form-submit\")\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n\n  // Only the frame marked with refresh=\"morph\" uses morphing\n  expect(await nextEventOnTarget(page, \"refresh-morph\", \"turbo:before-frame-morph\")).toBeTruthy()\n  expect(await noNextEventOnTarget(page, \"refresh-reload\", \"turbo:before-frame-morph\")).toBeTruthy()\n\n  await expect(page.locator(\"#refresh-morph\")).toHaveText(\"Loaded morphed frame\")\n\n  // Regular turbo-frames also gets reloaded since their complete attribute is removed\n  await expect(page.locator(\"#refresh-reload\")).toHaveText(\"Loaded reloadable frame\")\n})\n\ntest(\"overrides the meta value to render with replace when the Turbo Stream has [method=replace] attribute\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  await page.evaluate(() => document.body.insertAdjacentHTML(\"beforeend\", `<turbo-stream action=\"refresh\" method=\"replace\"></turbo-stream>`))\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"replace\" })\n})\n\ntest(\"don't refresh frames contained in [data-turbo-permanent] elements\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  // Set the frame's text since the final assertion cannot be noNextEventOnTarget as that is passing even when the frame reloads.\n  const frame = page.locator(\"#remote-permanent-frame\")\n  await frame.evaluate((frame) => frame.textContent = \"Frame to be preserved\")\n\n  await page.click(\"#form-submit\")\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n\n  await expect(page.locator(\"#remote-permanent-frame\")).toHaveText(\"Frame to be preserved\")\n})\n\ntest(\"frames marked with refresh='morph' are excluded from full page morphing\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  await page.evaluate(() => document.getElementById(\"refresh-morph\").setAttribute(\"data-modified\", \"true\"))\n\n  await page.click(\"#form-submit\")\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n\n  await expect(page.locator(\"#refresh-morph\")).toHaveAttribute(\"data-modified\", \"true\")\n  await expect(page.locator(\"#refresh-morph\")).toHaveText(\"Loaded morphed frame\")\n})\n\ntest(\"navigated frames without refresh attribute are reset after morphing\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  await page.click(\"#refresh-after-navigation-link\")\n\n  await expect(\n    page.locator(\"#refresh-after-navigation-content\"),\n    \"navigates theframe\"\n  ).toBeAttached()\n\n  await page.click(\"#form-submit\")\n\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n\n  await expect(\n    page.locator(\"#refresh-after-navigation-link\"),\n    \"resets the frame\"\n  ).toBeAttached()\n\n  await expect(\n    page.locator(\"#refresh-after-navigation-content\"),\n    \"does not reload the frame\"\n  ).not.toBeAttached()\n})\n\ntest(\"frames with refresh='morph' are preserved when missing from new content\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  await page.evaluate(() => {\n    const frame = document.getElementById(\"refresh-morph\")\n    frame.id = \"missing-frame\" // Change ID so to simulate a removed frame\n  })\n\n  await page.click(\"#form-submit\")\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n\n  await expect(page.locator(\"#missing-frame\"), \"the frame is preserved\").toBeAttached()\n})\n\ntest(\"it preserves the scroll position when the turbo-refresh-scroll meta tag is 'preserve'\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  await page.evaluate(() => window.scrollTo(10, 10))\n  await assertPageScroll(page, 10, 10)\n\n  // not using page.locator(\"#form-submit\").click() because it can reset the scroll position\n  await page.evaluate(() => document.getElementById(\"form-submit\")?.click())\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n\n  await assertPageScroll(page, 10, 10)\n})\n\ntest(\"overrides the meta value to reset the scroll position when the Turbo Stream has [scroll=reset] attribute\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  await page.evaluate(() => window.scrollTo(10, 10))\n  await assertPageScroll(page, 10, 10)\n\n  await page.evaluate(() => document.body.insertAdjacentHTML(\"beforeend\", `<turbo-stream action=\"refresh\" scroll=\"reset\"></turbo-stream>`))\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n\n  await assertPageScroll(page, 0, 0)\n})\n\ntest(\"it does not preserve the scroll position on regular 'advance' navigations, despite of using a 'preserve' option\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  await page.evaluate(() => window.scrollTo(10, 10))\n  await assertPageScroll(page, 10, 10)\n\n  await page.evaluate(() => document.getElementById(\"reload-link\").click())\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"replace\" })\n\n  await assertPageScroll(page, 0, 0)\n})\n\ntest(\"it resets the scroll position when the turbo-refresh-scroll meta tag is 'reset'\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh_scroll_reset.html\")\n\n  await page.evaluate(() => window.scrollTo(10, 10))\n  await assertPageScroll(page, 10, 10)\n\n  // not using page.locator(\"#form-submit\").click() because it can reset the scroll position\n  await page.evaluate(() => document.getElementById(\"form-submit\")?.click())\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n\n  await assertPageScroll(page, 0, 0)\n})\n\ntest(\"it preserves focus across morphs\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  const input = await page.locator(\"#form input[type=text]\")\n\n  await input.fill(\"Preserve me\")\n  await input.press(\"Enter\")\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n\n  await expect(input).toBeFocused()\n  await expect(input).toHaveValue(\"Preserve me\")\n})\n\ntest(\"it preserves focus and the [data-turbo-permanent] element's value across morphs\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  const input = await page.locator(\"#form input[type=text]\")\n\n  await input.evaluate((element) => element.setAttribute(\"data-turbo-permanent\", \"\"))\n  await input.fill(\"Preserve me\")\n  await input.press(\"Enter\")\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n\n  await expect(input).toBeFocused()\n  await expect(input).toHaveValue(\"Preserve me\")\n})\n\ntest(\"it preserves data-turbo-permanent elements\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  await page.evaluate(() => {\n    const element = document.getElementById(\"preserve-me\")\n    element.textContent = \"Preserve me, I have a family!\"\n  })\n\n  await expect(page.locator(\"#preserve-me\")).toHaveText(\"Preserve me, I have a family!\")\n\n  await page.click(\"#form-submit\")\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n\n  await expect(page.locator(\"#preserve-me\")).toHaveText(\"Preserve me, I have a family!\")\n})\n\ntest(\"it preserves data-turbo-permanent elements that don't match when their ids do\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  await page.evaluate(() => {\n    const element = document.getElementById(\"preserve-me\")\n\n    element.textContent = \"Preserve me, I have a family!\"\n    document.getElementById(\"container\").append(element)\n  })\n\n  await expect(page.locator(\"#preserve-me\")).toHaveText(\"Preserve me, I have a family!\")\n\n  await page.click(\"#form-submit\")\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n\n  await expect(page.locator(\"#preserve-me\")).toHaveText(\"Preserve me, I have a family!\")\n})\n\ntest(\"it preserves data-turbo-permanent children\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/permanent_children.html\")\n\n  await page.evaluate(() => {\n    // simulate result of client-side drag-and-drop reordering\n    document.getElementById(\"first-li\").before(document.getElementById(\"second-li\"))\n\n    // set state of data-turbo-permanent checkbox\n    document.getElementById(\"second-checkbox\").checked = true\n  })\n\n  // morph page back to original li ordering\n  await page.click(\"#form-submit\")\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n\n  // data-turbo-permanent checkbox should still be checked\n  await expect(\n    page.locator(\"#second-checkbox:checked\"),\n    \"retains state of data-turbo-permanent child\"\n  ).toBeAttached()\n})\n\ntest(\"renders unprocessable content responses with morphing\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  await page.click(\"#reject form.unprocessable_content input[type=submit]\")\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n\n  const title = page.locator(\"h1\")\n  await expect(title, \"renders the response HTML\").toHaveText(\"Unprocessable Content\")\n  await expect(page.locator(\"#frame form.reject\"), \"replaces entire page\").not.toBeAttached()\n})\n\ntest(\"doesn't render previews when morphing\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/page_refresh.html\")\n\n  await page.click(\"#link\")\n  await page.click(\"#page-refresh-link\")\n  await page.click(\"#refresh-link\")\n  await nextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n  await noNextEventNamed(page, \"turbo:render\", { renderMethod: \"morph\" })\n\n  const title = page.locator(\"h1\")\n  await expect(title).toHaveText(\"Page to be refreshed\")\n})\n\nasync function assertPageScroll(page, top, left) {\n  const [scrollTop, scrollLeft] = await page.evaluate(() => {\n    return [\n      document.documentElement.scrollTop || document.body.scrollTop,\n      document.documentElement.scrollLeft || document.body.scrollLeft\n    ]\n  })\n\n  expect(scrollTop).toEqual(top)\n  expect(scrollLeft).toEqual(left)\n}\n"
  },
  {
    "path": "src/tests/functional/pausable_rendering_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/pausable_rendering.html\")\n})\n\ntest(\"pauses and resumes rendering\", async ({ page }) => {\n  page.on(\"dialog\", (dialog) => {\n    expect(dialog.message()).toEqual(\"Continue rendering?\")\n    dialog.accept()\n  })\n\n  await page.click(\"#link\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"One\")\n})\n\ntest(\"aborts rendering\", async ({ page }) => {\n  const [firstDialog] = await Promise.all([page.waitForEvent(\"dialog\"), page.click(\"#link\")])\n\n  expect(firstDialog.message()).toEqual(\"Continue rendering?\")\n\n  firstDialog.dismiss()\n\n  await expect(page.locator(\"h1\")).toHaveText(\"Pausable Rendering\")\n})\n\ntest(\"pauses and resumes rendering a Frame\", async ({ page }) => {\n  page.on(\"dialog\", (dialog) => {\n    expect(dialog.message()).toEqual(\"Continue rendering?\")\n    dialog.accept()\n  })\n\n  await page.click(\"#frame-link\")\n\n  await expect(page.locator(\"#hello h2\")).toHaveText(\"Hello from a frame\")\n})\n\ntest(\"aborts rendering a Frame\", async ({ page }) => {\n  page.on(\"dialog\", (dialog) => {\n    expect(dialog.message()).toEqual(\"Continue rendering?\")\n    dialog.dismiss()\n  })\n\n  await expect(page.locator(\"#hello h2\")).toHaveText(\"Pausable Frame Rendering\")\n})\n"
  },
  {
    "path": "src/tests/functional/pausable_requests_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport { nextBeat } from \"../helpers/page\"\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/pausable_requests.html\")\n})\n\ntest(\"pauses and resumes request\", async ({ page }) => {\n  page.once(\"dialog\", (dialog) => {\n    expect(dialog.message()).toEqual(\"Continue request?\")\n    dialog.accept()\n  })\n\n  await page.click(\"#link\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"One\")\n})\n\ntest(\"aborts request\", async ({ page }) => {\n  const dialogMessages = []\n\n  page.on(\"dialog\", async (dialog) => {\n    dialogMessages.push(dialog.message())\n    if (dialog.message() === \"Continue request?\") {\n      await dialog.dismiss()\n    } else {\n      await dialog.accept()\n    }\n  })\n\n  await page.click(\"#link\")\n  await nextBeat()\n  await nextBeat()\n\n  expect(dialogMessages).toContain(\"Continue request?\")\n  expect(dialogMessages).toContain(\"Request aborted\")\n  await expect(page.locator(\"h1\")).toHaveText(\"Pausable Requests\")\n})\n"
  },
  {
    "path": "src/tests/functional/preloader_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport { nextEventOnTarget } from \"../helpers/page\"\n\ntest(\"preloads snapshot on initial load\", async ({ page }) => {\n  // contains `a[rel=\"preload\"][href=\"http://localhost:9000/src/tests/fixtures/preloaded.html\"]`\n  await page.goto(\"/src/tests/fixtures/preloading.html\")\n\n  const preloadLink = await page.locator(\"#preload_anchor\")\n  const href = await preloadLink.evaluate((link) => link.href)\n\n  expect(await urlInSnapshotCache(page, href)).toEqual(true)\n})\n\ntest(\"preloading dispatch turbo:before-fetch-{request,response} events\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/preloading.html\")\n\n  const link = await page.locator(\"#preload_anchor\")\n  const href = await link.evaluate((link) => link.href)\n\n  const { url, fetchOptions } = await nextEventOnTarget(page, \"preload_anchor\", \"turbo:before-fetch-request\")\n  const { fetchResponse } = await nextEventOnTarget(page, \"preload_anchor\", \"turbo:before-fetch-response\")\n\n  expect(href, \"dispatches request during preloading\").toEqual(url)\n  expect(fetchOptions.headers.Accept).toEqual(\"text/html, application/xhtml+xml\")\n  expect(fetchResponse.response.url).toEqual(href)\n})\n\ntest(\"preloads snapshot on page visit\", async ({ page }) => {\n  // contains `a[rel=\"preload\"][href=\"http://localhost:9000/src/tests/fixtures/preloading.html\"]`\n  await page.goto(\"/src/tests/fixtures/hot_preloading.html\")\n  await page.click(\"#hot_preload_anchor\")\n\n  const preloadLink = await page.locator(\"#preload_anchor\")\n  const href = await preloadLink.evaluate((link) => link.href)\n\n  expect(await urlInSnapshotCache(page, href)).toEqual(true)\n})\n\ntest(\"preloads anchor from frame that will drive the page\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/frame_preloading.html\")\n  await nextEventOnTarget(page, \"menu\", \"turbo:frame-load\")\n\n  const preloadLink = await page.locator(\"#menu a[data-turbo-frame=_top]\")\n  const href = await preloadLink.evaluate((link) => link.href)\n\n  expect(await urlInSnapshotCache(page, href)).toEqual(true)\n})\n\ntest(\"does not preload anchor off-site\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/preloading.html\")\n\n  const link = await page.locator(\"a[href*=https]\")\n  const href = await link.evaluate((link) => link.href)\n\n  expect(await urlInSnapshotCache(page, href)).toEqual(false)\n})\n\ntest(\"does not preload anchor that will drive an ancestor frame\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/frame_preloading.html\")\n\n  const preloadLink = await page.locator(\"#hello a[data-turbo-preload]\")\n  const href = await preloadLink.evaluate((link) => link.href)\n\n  expect(await urlInSnapshotCache(page, href)).toEqual(false)\n})\n\ntest(\"does not preload anchor that will drive a target frame\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/frame_preloading.html\")\n\n  const link = await page.locator(\"a[data-turbo-frame=hello]\")\n  const href = await link.evaluate((link) => link.href)\n\n  expect(await urlInSnapshotCache(page, href)).toEqual(false)\n})\n\ntest(\"does not preload a link with [data-turbo=false]\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/preloading.html\")\n\n  const link = await page.locator(\"[data-turbo=false] a\")\n  const href = await link.evaluate((link) => link.href)\n\n  expect(await urlInSnapshotCache(page, href)).toEqual(false)\n})\n\ntest(\"does not preload a link with [data-turbo-method]\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/preloading.html\")\n\n  const preloadLink = await page.locator(\"a[data-turbo-method]\")\n  const href = await preloadLink.evaluate((link) => link.href)\n\n  expect(await urlInSnapshotCache(page, href)).toEqual(false)\n})\n\nfunction urlInSnapshotCache(page, href) {\n  return page.evaluate((href) => {\n    const preloadedUrl = new URL(href)\n    const cache = window.Turbo.session.preloader.snapshotCache\n\n    return cache.has(preloadedUrl)\n  }, href)\n}\n"
  },
  {
    "path": "src/tests/functional/rendering_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport {\n  clearLocalStorage,\n  disposeAll,\n  isScrolledToTop,\n  nextBeat,\n  nextBody,\n  nextBodyMutation,\n  nextEventNamed,\n  noNextBodyMutation,\n  readEventLogs,\n  scrollToSelector,\n  sleep,\n  strictElementEquals,\n  textContent,\n  visitAction,\n  withPathname\n} from \"../helpers/page\"\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/rendering.html\")\n  await clearLocalStorage(page)\n  await readEventLogs(page)\n})\n\ntest(\"triggers before-render and render events\", async ({ page }) => {\n  await page.click(\"#same-origin-link\")\n  const { newBody } = await nextEventNamed(page, \"turbo:before-render\", { renderMethod: \"replace\" })\n\n  await expect(page.locator(\"h1\")).toHaveText(\"One\")\n\n  await nextEventNamed(page, \"turbo:render\")\n  expect(newBody).toEqual(await page.evaluate(() => document.body.outerHTML))\n})\n\ntest(\"triggers before-render, render, and load events for error pages\", async ({ page }) => {\n  await page.click(\"#nonexistent-link\")\n  const { newBody } = await nextEventNamed(page, \"turbo:before-render\")\n\n  expect(await textContent(page, newBody)).toEqual(\"\\nCannot GET /nonexistent\\n\\n\\n\")\n\n  await nextEventNamed(page, \"turbo:render\")\n  expect(newBody).toEqual(await page.evaluate(() => document.body.outerHTML))\n\n  await nextEventNamed(page, \"turbo:load\")\n})\n\ntest(\"reloads when tracked elements change\", async ({ page }) => {\n  await page.evaluate(() =>\n    window.addEventListener(\n      \"turbo:reload\",\n      (e) => {\n        localStorage.setItem(\"reloadReason\", e.detail.reason)\n      },\n      { once: true }\n    )\n  )\n\n  await page.click(\"#tracked-asset-change-link\")\n  await page.waitForURL(\"**/tracked_asset_change.html\")\n\n  const reason = await page.evaluate(() => localStorage.getItem(\"reloadReason\"))\n  expect(await visitAction(page)).toEqual(\"load\")\n  expect(reason).toEqual(\"tracked_element_mismatch\")\n})\n\ntest(\"reloads when tracked elements change due to failed form submission\", async ({ page }) => {\n  await page.click(\"#tracked-asset-change-form button\")\n  await nextBeat()\n\n  await page.evaluate(() => {\n    window.addEventListener(\n      \"turbo:reload\",\n      (e) => {\n        localStorage.setItem(\"reason\", e.detail.reason)\n      },\n      { once: true }\n    )\n\n    window.addEventListener(\n      \"beforeunload\",\n      () => {\n        localStorage.setItem(\"unloaded\", \"true\")\n      },\n      { once: true }\n    )\n  })\n\n  await page.click(\"#tracked-asset-change-form button\")\n  await nextBeat()\n\n  const reason = await page.evaluate(() => localStorage.getItem(\"reason\"))\n  const unloaded = await page.evaluate(() => localStorage.getItem(\"unloaded\"))\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/rendering.html\"))\n  expect(await visitAction(page)).toEqual(\"load\")\n  expect(reason).toEqual(\"tracked_element_mismatch\")\n  expect(unloaded).toEqual(\"true\")\n})\n\ntest(\"before-render event supports custom render function\", async ({ page }) => {\n  await page.evaluate(() =>\n    addEventListener(\"turbo:before-render\", (event) => {\n      const { detail } = event\n      const { render } = detail\n      detail.render = (currentElement, newElement) => {\n        newElement.insertAdjacentHTML(\"beforeend\", `<span id=\"custom-rendered\">Custom Rendered</span>`)\n        render(currentElement, newElement)\n      }\n    })\n  )\n  await page.click(\"#same-origin-link\")\n\n  await expect(page.locator(\"#custom-rendered\"), \"renders with custom function\").toHaveText(\"Custom Rendered\")\n})\n\ntest(\"before-render event supports async custom render function\", async ({ page }) => {\n  await page.evaluate(() => {\n    const nextEventLoopTick = () =>\n      new Promise((resolve) => {\n        setTimeout(() => resolve(), 0)\n      })\n\n    addEventListener(\"turbo:before-render\", (event) => {\n      const { detail } = event\n      const { render } = detail\n      detail.render = async (currentElement, newElement) => {\n        await nextEventLoopTick()\n\n        newElement.insertAdjacentHTML(\"beforeend\", `<span id=\"custom-rendered\">Custom Rendered</span>`)\n        render(currentElement, newElement)\n      }\n    })\n\n    addEventListener(\"turbo:load\", () => {\n      localStorage.setItem(\"renderedElement\", document.getElementById(\"custom-rendered\")?.textContent || \"\")\n    })\n  })\n  await page.click(\"#same-origin-link\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  const renderedElement = await page.evaluate(() => localStorage.getItem(\"renderedElement\"))\n\n  expect(renderedElement, \"renders with custom function\").toEqual(\"Custom Rendered\")\n})\n\ntest(\"wont reload when tracked elements has a nonce\", async ({ page }) => {\n  await page.click(\"#tracked-nonce-tag-link\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/tracked_nonce_tag.html\"))\n  expect(await visitAction(page)).toEqual(\"advance\")\n})\n\ntest(\"reloads when turbo-visit-control setting is reload\", async ({ page }) => {\n  await page.evaluate(() =>\n    window.addEventListener(\n      \"turbo:reload\",\n      (e) => {\n        localStorage.setItem(\"reloadReason\", e.detail.reason)\n      },\n      { once: true }\n    )\n  )\n\n  await page.click(\"#visit-control-reload-link\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/visit_control_reload.html\"))\n  const reason = await page.evaluate(() => localStorage.getItem(\"reloadReason\"))\n  expect(await visitAction(page)).toEqual(\"load\")\n  expect(reason).toEqual(\"turbo_visit_control_is_reload\")\n})\n\ntest(\"maintains scroll position before visit when turbo-visit-control setting is reload\", async ({ page }) => {\n  await scrollToSelector(page, \"#below-the-fold-visit-control-reload-link\")\n  expect(await isScrolledToTop(page), \"scrolled down\").toEqual(false)\n\n  await page.evaluate(() => localStorage.setItem(\"scrolls\", \"false\"))\n\n  page.evaluate(() =>\n    addEventListener(\"click\", () => {\n      addEventListener(\"scroll\", () => {\n        localStorage.setItem(\"scrolls\", \"true\")\n      })\n    })\n  )\n\n  page.click(\"#below-the-fold-visit-control-reload-link\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/visit_control_reload.html\"))\n  const scrolls = await page.evaluate(() => localStorage.getItem(\"scrolls\"))\n  expect(scrolls, \"scroll position is preserved\").toEqual(\"false\")\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n\ntest(\"changes the html[lang] attribute\", async ({ page }) => {\n  await page.click(\"#es_locale_link\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"html\")).toHaveAttribute(\"lang\", \"es\")\n})\n\ntest(\"changes the html[dir] attribute\", async ({ page }) => {\n  await page.click(\"#dir-rtl\")\n\n  await expect(page.locator(\"html\")).toHaveAttribute(\"dir\", \"rtl\")\n})\n\ntest(\"accumulates script elements in head\", async ({ page }) => {\n  const assetElements = () => page.$$('script')\n  const originalElements = await assetElements()\n\n  await page.click(\"#additional-script-link\")\n  await nextBody(page)\n  const newElements = await assetElements()\n  expect(await deepElementsEqual(page, newElements, originalElements)).toEqual(false)\n\n  await page.goBack()\n  await nextBody(page)\n  const finalElements = await assetElements()\n  expect(await deepElementsEqual(page, finalElements, newElements)).toEqual(true)\n\n  await disposeAll(...originalElements, ...newElements, ...finalElements)\n})\n\ntest(\"replaces provisional elements in head\", async ({ page }) => {\n  const provisionalElements = () => page.$$('head :not(script), head :not(style), head :not(link[rel=\"stylesheet\"])')\n  const originalElements = await provisionalElements()\n  const meta = page.locator(\"meta[name=test]\")\n  await expect(meta).toHaveCount(0)\n\n  await page.click(\"#same-origin-link\")\n  await expect(meta).toHaveCount(1)\n  const newElements = await provisionalElements()\n  expect(await deepElementsEqual(page, newElements, originalElements)).toEqual(false)\n\n  await page.goBack()\n  await expect(meta).toHaveCount(0)\n  const finalElements = await provisionalElements()\n  expect(await deepElementsEqual(page, finalElements, newElements)).toEqual(false)\n\n  await disposeAll(...originalElements, ...newElements, ...finalElements)\n})\n\ntest(\"evaluates head stylesheet elements\", async ({ page }) => {\n  expect(await isStylesheetEvaluated(page)).toEqual(false)\n\n  await page.click(\"#additional-assets-link\")\n  await nextEventNamed(page, \"turbo:render\")\n  expect(await isStylesheetEvaluated(page)).toEqual(true)\n})\n\ntest(\"does not evaluate head stylesheet elements inside noscript elements\", async ({ page }) => {\n  expect(await isNoscriptStylesheetEvaluated(page)).toEqual(false)\n\n  await page.click(\"#additional-assets-link\")\n  await nextEventNamed(page, \"turbo:render\")\n  expect(await isNoscriptStylesheetEvaluated(page)).toEqual(false)\n})\n\ntest(\"does not evaluate body stylesheet elements inside noscript elements\", async ({ page }) => {\n  expect(await isNoscriptStylesheetEvaluated(page)).toEqual(false)\n\n  await page.click(\"#body-noscript-link\")\n  await nextEventNamed(page, \"turbo:render\")\n  expect(await isNoscriptStylesheetEvaluated(page)).toEqual(false)\n})\n\ntest(\"preserves noscript elements with non-style content after navigation\", async ({ page }) => {\n  await page.click(\"#body-noscript-content-link\")\n  await nextEventNamed(page, \"turbo:render\")\n\n  const noscriptCount = await page.locator(\"#lazy-load-noscript\").count()\n  expect(noscriptCount).toEqual(1)\n})\n\ntest(\"removes only stylesheet elements from noscript with mixed content\", async ({ page }) => {\n  await page.click(\"#body-noscript-content-link\")\n  await nextEventNamed(page, \"turbo:render\")\n\n  const mixedNoscriptExists = await page.locator(\"#mixed-noscript\").count()\n  expect(mixedNoscriptExists).toEqual(1)\n\n  expect(await isNoscriptStylesheetEvaluated(page)).toEqual(false)\n})\n\ntest(\"removes alternate stylesheet elements from noscript\", async ({ page }) => {\n  await page.click(\"#body-noscript-content-link\")\n  await nextEventNamed(page, \"turbo:render\")\n\n  const noscriptExists = await page.locator(\"#alternate-stylesheet-noscript\").count()\n  expect(noscriptExists).toEqual(1)\n\n  expect(await isNoscriptStylesheetEvaluated(page)).toEqual(false)\n})\n\ntest(\"waits for CSS to be loaded before rendering\", async ({ page }) => {\n  let finishLoadingCSS = (_value) => {}\n  const promise = new Promise((resolve) => {\n    finishLoadingCSS = resolve\n  })\n  page.route(\"**/*.css\", async (route) => {\n    await promise\n    route.continue()\n  })\n\n  await page.click(\"#additional-assets-link\")\n\n  expect(await isStylesheetEvaluated(page)).toEqual(false)\n  await expect(page.locator(\"h1\")).not.toHaveText(\"Additional assets\")\n\n  finishLoadingCSS()\n\n  await nextEventNamed(page, \"turbo:render\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"Additional assets\")\n  expect(await isStylesheetEvaluated(page)).toEqual(true)\n})\n\ntest(\"waits for CSS to fail before rendering\", async ({ page }) => {\n  let finishLoadingCSS = (_value) => {}\n  const promise = new Promise((resolve) => {\n    finishLoadingCSS = resolve\n  })\n  page.route(\"**/*.css\", async (route) => {\n    await promise\n    route.abort()\n  })\n\n  await page.click(\"#additional-assets-link\")\n\n  expect(await isStylesheetEvaluated(page)).toEqual(false)\n  await expect(page.locator(\"h1\")).not.toHaveText(\"Additional assets\")\n\n  finishLoadingCSS()\n\n  await nextEventNamed(page, \"turbo:render\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"Additional assets\")\n  expect(await isStylesheetEvaluated(page)).toEqual(false)\n})\n\ntest(\"waits for some time, but renders if CSS takes too much to load\", async ({ page }) => {\n  let finishLoadingCSS = (_value) => {}\n  const promise = new Promise((resolve) => {\n    finishLoadingCSS = resolve\n  })\n  page.route(\"**/*.css\", async (route) => {\n    await promise\n    route.continue()\n  })\n\n  await page.click(\"#additional-assets-link\")\n  await sleep(3000)\n  await nextEventNamed(page, \"turbo:render\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"Additional assets\")\n  expect(await isStylesheetEvaluated(page)).toEqual(false)\n\n  finishLoadingCSS()\n  await nextBeat()\n\n  expect(await isStylesheetEvaluated(page)).toEqual(true)\n})\n\ntest(\"skip evaluates head script elements once\", async ({ page }) => {\n  expect(await headScriptEvaluationCount(page)).toEqual(undefined)\n\n  await page.click(\"#head-script-link\")\n  await nextEventNamed(page, \"turbo:render\")\n  expect(await headScriptEvaluationCount(page)).toEqual(1)\n\n  await page.goBack()\n  await nextEventNamed(page, \"turbo:render\")\n  expect(await headScriptEvaluationCount(page)).toEqual(1)\n\n  await page.click(\"#head-script-link\")\n  await nextEventNamed(page, \"turbo:render\")\n  expect(await headScriptEvaluationCount(page)).toEqual(1)\n})\n\ntest(\"evaluates body script elements on each render\", async ({ page }) => {\n  expect(await bodyScriptEvaluationCount(page)).toEqual(undefined)\n\n  await page.click(\"#body-script-link\")\n  await nextEventNamed(page, \"turbo:render\")\n  expect(await bodyScriptEvaluationCount(page)).toEqual(1)\n\n  await page.goBack()\n  await nextEventNamed(page, \"turbo:render\")\n  expect(await bodyScriptEvaluationCount(page)).toEqual(1)\n\n  await page.click(\"#body-script-link\")\n  await nextEventNamed(page, \"turbo:render\")\n  expect(await bodyScriptEvaluationCount(page)).toEqual(2)\n})\n\ntest(\"does not evaluate data-turbo-eval=false scripts\", async ({ page }) => {\n  await page.click(\"#eval-false-script-link\")\n  await nextEventNamed(page, \"turbo:render\")\n  expect(await bodyScriptEvaluationCount(page)).toEqual(undefined)\n})\n\ntest(\"preserves permanent elements\", async ({ page }) => {\n  const permanentElement = await page.locator(\"#permanent\")\n  await expect(permanentElement).toHaveText(\"Rendering\")\n\n  await page.click(\"#permanent-element-link\")\n  await nextEventNamed(page, \"turbo:render\")\n  expect(await strictElementEquals(permanentElement, await page.locator(\"#permanent\"))).toEqual(true)\n  await expect(permanentElement).toHaveText(\"Rendering\")\n\n  await page.goBack()\n  await nextEventNamed(page, \"turbo:render\")\n  expect(await strictElementEquals(permanentElement, await page.locator(\"#permanent\"))).toEqual(true)\n})\n\ntest(\"restores focus during page rendering when transposing the activeElement\", async ({ page }) => {\n  await page.press(\"#permanent-input\", \"Enter\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"#permanent-input\"), \"restores focus after page loads\").toBeFocused()\n})\n\ntest(\"restores focus during page rendering when transposing an ancestor of the activeElement\", async ({\n  page\n}) => {\n  await page.press(\"#permanent-descendant-input\", \"Enter\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"#permanent-descendant-input\"), \"restores focus after page loads\").toBeFocused()\n})\n\ntest(\"before-frame-render event supports custom render function within turbo-frames\", async ({ page }) => {\n  const frame = await page.locator(\"#frame\")\n  await frame.evaluate((frame) =>\n    frame.addEventListener(\"turbo:before-frame-render\", (event) => {\n      const { detail } = event\n      const { render } = detail\n      detail.render = (currentElement, newElement) => {\n        newElement.insertAdjacentHTML(\"beforeend\", `<span id=\"custom-rendered\">Custom Rendered Frame</span>`)\n        render(currentElement, newElement)\n      }\n    })\n  )\n\n  await page.click(\"#permanent-in-frame-element-link\")\n  await nextBeat()\n\n  await expect(page.locator(\"#frame #custom-rendered\"), \"renders with custom function\").toHaveText(\"Custom Rendered Frame\")\n})\n\ntest(\"preserves permanent elements within turbo-frames\", async ({ page }) => {\n  await expect(page.locator(\"#permanent-in-frame\")).toHaveText(\"Rendering\")\n\n  await page.click(\"#permanent-in-frame-element-link\")\n  await nextBeat()\n\n  await expect(page.locator(\"#permanent-in-frame\")).toHaveText(\"Rendering\")\n})\n\ntest(\"restores focus during turbo-frame rendering when transposing the activeElement\", async ({ page }) => {\n  await page.press(\"#permanent-input-in-frame\", \"Enter\")\n  await nextBeat()\n\n  await expect(page.locator(\"#permanent-input-in-frame\"), \"restores focus after page loads\").toBeFocused()\n})\n\ntest(\"restores focus during turbo-frame rendering when transposing a descendant of the activeElement\", async ({\n  page\n}) => {\n  await page.press(\"#permanent-descendant-input-in-frame\", \"Enter\")\n  await nextBeat()\n\n  await expect(page.locator(\"#permanent-descendant-input-in-frame\"), \"restores focus after page loads\").toBeFocused()\n})\n\ntest(\"preserves permanent element video playback\", async ({ page }) => {\n  const videoElement = await page.locator(\"#permanent-video\")\n  await page.click(\"#permanent-video-button\")\n  await sleep(500)\n\n  const timeBeforeRender = await videoElement.evaluate((video) => video.currentTime)\n  expect(timeBeforeRender, \"playback has started\").not.toEqual(0)\n\n  await page.click(\"#permanent-element-link\")\n  await nextBody(page)\n\n  const timeAfterRender = await videoElement.evaluate((video) => video.currentTime)\n  expect(timeAfterRender, \"element state is preserved\").toEqual(timeBeforeRender)\n})\n\ntest(\"preserves permanent element through Turbo Stream update\", async ({ page }) => {\n  await page.evaluate(() => {\n    window.Turbo.renderStreamMessage(`\n      <turbo-stream action=\"update\" target=\"frame\">\n        <template>\n          <div id=\"permanent-in-frame\" data-turbo-permanent>Ignored</div>\n        </template>\n      </turbo-stream>\n    `)\n  })\n  await nextBeat()\n\n  await expect(page.locator(\"#permanent-in-frame\")).toHaveText(\"Rendering\")\n})\n\ntest(\"preserves permanent element through Turbo Stream append\", async ({ page }) => {\n  await page.evaluate(() => {\n    window.Turbo.renderStreamMessage(`\n      <turbo-stream action=\"append\" target=\"frame\">\n        <template>\n          <div id=\"permanent-in-frame\" data-turbo-permanent>Ignored</div>\n        </template>\n      </turbo-stream>\n    `)\n  })\n  await nextBeat()\n\n  await expect(page.locator(\"#permanent-in-frame\")).toHaveText(\"Rendering\")\n})\n\ntest(\"preserves input values\", async ({ page }) => {\n  await page.fill(\"#text-input\", \"test\")\n  await page.click(\"#checkbox-input\")\n  await page.click(\"#radio-input\")\n  await page.fill(\"#textarea\", \"test\")\n  await page.selectOption(\"#select\", \"2\")\n  await page.selectOption(\"#select-multiple\", \"2\")\n\n  await page.click(\"#same-origin-link\")\n  await nextEventNamed(page, \"turbo:load\")\n  await page.goBack()\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"#text-input\")).toHaveValue(\"test\")\n  await expect(page.locator(\"#checkbox-input\")).toBeChecked()\n  await expect(page.locator(\"#radio-input\")).toBeChecked()\n  await expect(page.locator(\"#textarea\")).toHaveValue(\"test\")\n  await expect(page.locator(\"#select\")).toHaveValue(\"2\")\n  await expect(page.locator(\"#select-multiple\")).toHaveValue(\"2\")\n})\n\ntest(\"does not preserve password values\", async ({ page }) => {\n  await page.fill(\"#password-input\", \"test\")\n\n  await page.click(\"#same-origin-link\")\n  await nextEventNamed(page, \"turbo:load\")\n  await page.goBack()\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"#password-input\")).toHaveValue(\"\")\n})\n\ntest(\"<input type='reset'> clears values when restored from cache\", async ({ page }) => {\n  await page.fill(\"#text-input\", \"test\")\n  await page.click(\"#checkbox-input\")\n  await page.click(\"#radio-input\")\n  await page.fill(\"#textarea\", \"test\")\n  await page.selectOption(\"#select\", \"2\")\n  await page.selectOption(\"#select-multiple\", \"2\")\n\n  await page.click(\"#same-origin-link\")\n  await nextEventNamed(page, \"turbo:load\")\n  await page.goBack()\n  await nextEventNamed(page, \"turbo:load\")\n\n  await page.click(\"#reset-input\")\n\n  await expect(page.locator(\"#text-input\")).toHaveValue(\"\")\n  await expect(page.locator(\"#checkbox-input\")).not.toBeChecked()\n  await expect(page.locator(\"#radio-input\")).not.toBeChecked()\n  await expect(page.locator(\"#textarea\")).toHaveValue(\"\")\n  await expect(page.locator(\"#select\")).toHaveValue(\"1\")\n  await expect(page.locator(\"#select-multiple\")).toHaveValue(\"\")\n})\n\ntest(\"before-cache event\", async ({ page }) => {\n  await page.evaluate(() => {\n    addEventListener(\"turbo:before-cache\", () => (document.body.innerHTML = \"Modified\"), { once: true })\n  })\n  await page.click(\"#same-origin-link\")\n  await nextEventNamed(page, \"turbo:load\")\n  await page.goBack()\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"body\")).toHaveText(\"Modified\")\n})\n\ntest(\"mutation record-cache notification\", async ({ page }) => {\n  await modifyBodyAfterRemoval(page)\n  await page.click(\"#same-origin-link\")\n  await nextBody(page)\n  await page.goBack()\n\n  await expect(page.locator(\"body\")).toHaveText(\"Modified\")\n})\n\ntest(\"error pages\", async ({ page }) => {\n  await page.click(\"#nonexistent-link\")\n\n  await expect(page.locator(\"body\")).toHaveText(\"\\nCannot GET /nonexistent\\n\\n\\n\")\n})\n\ntest(\"rendering a redirect response replaces the body once and only once\", async ({ page }) => {\n  await page.click(\"#redirect-link\")\n  await nextBodyMutation(page)\n\n  expect(await noNextBodyMutation(page), \"replaces <body> element once\").toEqual(true)\n})\n\nfunction deepElementsEqual(page, left, right) {\n  return page.evaluate(\n    ([left, right]) => left.length == right.length && left.every((element) => right.includes(element)),\n    [left, right]\n  )\n}\n\nfunction headScriptEvaluationCount(page) {\n  return page.evaluate(() => window.headScriptEvaluationCount)\n}\n\nfunction bodyScriptEvaluationCount(page) {\n  return page.evaluate(() => window.bodyScriptEvaluationCount)\n}\n\nfunction isStylesheetEvaluated(page) {\n  return page.evaluate(\n    () => getComputedStyle(document.body).getPropertyValue(\"--black-if-evaluated\").trim() === \"black\"\n  )\n}\n\nfunction isNoscriptStylesheetEvaluated(page) {\n  return page.evaluate(\n    () => getComputedStyle(document.body).getPropertyValue(\"--black-if-noscript-evaluated\").trim() === \"black\"\n  )\n}\n\nfunction modifyBodyAfterRemoval(page) {\n  return page.evaluate(() => {\n    const { documentElement, body } = document\n    const observer = new MutationObserver((records) => {\n      for (const record of records) {\n        if (Array.from(record.removedNodes).indexOf(body) > -1) {\n          body.innerHTML = \"Modified\"\n          observer.disconnect()\n          break\n        }\n      }\n    })\n    observer.observe(documentElement, { childList: true })\n  })\n}\n"
  },
  {
    "path": "src/tests/functional/root_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport { visitAction, withPathname } from \"../helpers/page\"\n\ntest(\"visiting a location inside the root\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/root/index.html\")\n  await page.click(\"#link-page-inside\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/root/page.html\"))\n  expect(await visitAction(page)).not.toEqual(\"load\")\n})\n\ntest(\"visiting the root itself\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/root/page.html\")\n  await page.click(\"#link-root\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/root/\"))\n  expect(await visitAction(page)).not.toEqual(\"load\")\n})\n\ntest(\"visiting a location outside the root\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/root/index.html\")\n  await page.click(\"#link-page-outside\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n\ntest(\"visiting a location outside the root having the root as a prefix\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/root/index.html\")\n  await page.click(\"#link-page-outside-prefix\")\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/rootlet.html\"))\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n"
  },
  {
    "path": "src/tests/functional/scroll_restoration_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport {\n  nextBeat,\n  reloadPage,\n  scrollPosition,\n  scrollToSelector\n} from \"../helpers/page\"\n\ntest(\"landing on an anchor\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/scroll_restoration.html#three\")\n  await nextBeat()\n  const { y: yAfterLoading } = await scrollPosition(page)\n  expect(yAfterLoading).not.toEqual(0)\n})\n\ntest(\"reloading after scrolling\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/scroll_restoration.html\")\n  await scrollToSelector(page, \"#three\")\n  const { y: yAfterScrolling } = await scrollPosition(page)\n  expect(yAfterScrolling).not.toEqual(0)\n\n  await reloadPage(page)\n  const { y: yAfterReloading } = await scrollPosition(page)\n  expect(yAfterReloading).not.toEqual(0)\n})\n\ntest(\"returning from history\", async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/scroll_restoration.html\")\n  await scrollToSelector(page, \"#three\")\n  await page.goto(\"/src/tests/fixtures/bare.html\")\n  await page.goBack()\n\n  const { y: yAfterReturning } = await scrollPosition(page)\n  expect(yAfterReturning).not.toEqual(0)\n})\n"
  },
  {
    "path": "src/tests/functional/stream_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport {\n  nextEventNamed,\n  nextEventOnTarget,\n  noNextEventOnTarget,\n  readEventLogs\n} from \"../helpers/page\"\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/stream.html\")\n  await readEventLogs(page)\n})\n\ntest(\"receiving a stream message\", async ({ page }) => {\n  const messages = page.locator(\"#messages .message\")\n\n  await expect(messages).toHaveText([\"First\"])\n\n  await page.click(\"#append-target button\")\n\n  await expect(messages).toHaveText([\"First\", \"Hello world!\"])\n})\n\ntest(\"dispatches a turbo:before-stream-render event\", async ({ page }) => {\n  await page.click(\"#append-target button\")\n  await nextEventNamed(page, \"turbo:submit-end\")\n  const [[type, { newStream }, target]] = await readEventLogs(page, 1)\n\n  expect(type).toEqual(\"turbo:before-stream-render\")\n  expect(target).toEqual(\"a-turbo-stream\")\n  expect(newStream).toContain(`action=\"append\"`)\n  expect(newStream).toContain(`target=\"messages\"`)\n})\n\ntest(\"receiving a stream message with css selector target\", async ({ page }) => {\n  const messages2 = page.locator(\"#messages_2 .message\")\n  const messages3 = page.locator(\"#messages_3 .message\")\n\n  await expect(messages2).toHaveText([\"Second\"])\n  await expect(messages3).toHaveText([\"Third\"])\n\n  await page.click(\"#append-targets button\")\n\n  await expect(messages2).toHaveText([\"Second\", \"Hello CSS!\"])\n  await expect(messages3).toHaveText([\"Third\", \"Hello CSS!\"])\n})\n\ntest(\"receiving a message without a template\", async ({ page }) => {\n  await page.evaluate(() =>\n    window.Turbo.renderStreamMessage(`\n      <turbo-stream action=\"remove\" target=\"messages\"></turbo-stream>\n    `)\n  )\n\n  await expect(page.locator(\"#messages\"), \"removes target element\").not.toBeAttached()\n})\n\ntest(\"receiving a message with a <script> element\", async ({ page }) => {\n  await page.evaluate(() =>\n    window.Turbo.renderStreamMessage(`\n      <turbo-stream action=\"append\" target=\"messages\">\n        <template>\n          <script>\n            const messages = document.querySelector(\"#messages .message\")\n            messages.textContent = \"Hello from script\"\n          </script>\n        </template>\n      </turbo-stream>\n    `)\n  )\n\n  await expect(page.locator(\"#messages\")).toHaveText(\"Hello from script\")\n})\n\ntest(\"overriding with custom StreamActions\", async ({ page }) => {\n  const html = \"Rendered with Custom Action\"\n\n  await page.evaluate((html) => {\n    const CustomActions = {\n      customUpdate(newStream) {\n        for (const target of newStream.targetElements) target.innerHTML = html\n      }\n    }\n\n    addEventListener(\"turbo:before-stream-render\", ({ target, detail }) => {\n      const stream = target\n\n      const defaultRender = detail.render\n      detail.render = CustomActions[stream.action] || defaultRender\n    })\n\n    window.Turbo.renderStreamMessage(`\n      <turbo-stream action=\"customUpdate\" target=\"messages\">\n        <template></template>\n      </turbo-stream>\n    `)\n  }, html)\n\n  await expect(page.locator(\"#messages\"), \"evaluates custom StreamAction\").toHaveText(\"Rendered with Custom Action\")\n})\n\ntest(\"receiving a stream message over SSE\", async ({ page }) => {\n  await page.evaluate(() => {\n    document.body.insertAdjacentHTML(\n      \"afterbegin\",\n      `<turbo-stream-source id=\"stream-source\" src=\"/__turbo/messages\"></turbo-stream-source>`\n    )\n  })\n\n  const streamSource = page.locator(\"#stream-source\")\n\n  await expect(streamSource).toHaveJSProperty(\"streamSource.readyState\", await page.evaluate(() => EventSource.OPEN))\n\n  const messages = page.locator(\"#messages .message\")\n\n  await expect(messages).toHaveText([\"First\"])\n\n  await page.click(\"#async button\")\n\n  await expect(messages).toHaveText([\"First\", \"Hello world!\"])\n\n  const readyState = await streamSource.evaluate((element) => {\n    element.remove()\n\n    return element.streamSource.readyState\n  })\n  expect(readyState).toEqual(await page.evaluate(() => EventSource.CLOSED))\n\n  await page.click(\"#async button\")\n\n  await expect(messages).toHaveText([\"First\", \"Hello world!\"])\n})\n\ntest(\"receiving an update stream message preserves focus if the activeElement has an [id]\", async ({ page }) => {\n  await page.locator(\"input#container-element\").focus()\n  await page.evaluate(() => {\n    window.Turbo.renderStreamMessage(`\n      <turbo-stream action=\"update\" target=\"container\">\n        <template><textarea id=\"container-element\"></textarea></template>\n      </turbo-stream>\n    `)\n  })\n\n  await expect(page.locator(\"textarea#container-element:focus\")).toBeFocused()\n})\n\ntest(\"receiving a replace stream message preserves focus if the activeElement has an [id]\", async ({ page }) => {\n  await page.locator(\"input#container-element\").focus()\n  await page.evaluate(() => {\n    window.Turbo.renderStreamMessage(`\n      <turbo-stream action=\"replace\" target=\"container-element\">\n        <template><textarea id=\"container-element\"></textarea></template>\n      </turbo-stream>\n    `)\n  })\n\n  await expect(page.locator(\"textarea#container-element:focus\")).toBeFocused()\n})\n\ntest(\"receiving a remove stream message preserves focus blurs the activeElement\", async ({ page }) => {\n  await page.locator(\"#container-element\").focus()\n  await page.evaluate(() => {\n    window.Turbo.renderStreamMessage(`\n      <turbo-stream action=\"remove\" target=\"container-element\"></turbo-stream>\n    `)\n  })\n\n  await expect(page.locator(\":focus\")).not.toBeAttached()\n})\n\ntest(\"dispatches a turbo:before-morph-element & turbo:morph-element for each morph stream action\", async ({ page }) => {\n  await page.evaluate(() => {\n    window.Turbo.renderStreamMessage(`\n      <turbo-stream action=\"replace\" method=\"morph\" target=\"message_1\">\n        <template>\n          <div id=\"message_1\">\n            <h1>Morphed</h1>\n          </div>\n        </template>\n      </turbo-stream>\n    `)\n  })\n\n  await nextEventOnTarget(page, \"message_1\", \"turbo:before-morph-element\")\n  await nextEventOnTarget(page, \"message_1\", \"turbo:morph-element\")\n  await expect(page.locator(\"#message_1\")).toHaveText(\"Morphed\")\n})\n\ntest(\"preventing a turbo:before-morph-element prevents the morph\", async ({ page }) => {\n  await page.evaluate(() => {\n    addEventListener(\"turbo:before-morph-element\", (event) => {\n      event.preventDefault()\n    })\n  })\n\n  await page.evaluate(() => {\n    window.Turbo.renderStreamMessage(`\n      <turbo-stream action=\"replace\" method=\"morph\" target=\"message_1\">\n        <template>\n          <div id=\"message_1\">\n            <h1>Morphed</h1>\n          </div>\n        </template>\n      </turbo-stream>\n    `)\n  })\n\n  await nextEventOnTarget(page, \"message_1\", \"turbo:before-morph-element\")\n  await noNextEventOnTarget(page, \"message_1\", \"turbo:morph-element\")\n  await expect(page.locator(\"#message_1\")).toHaveText(\"Morph me\")\n})\n\ntest(\"rendering a stream message into the HTML executes it\", async ({ page }) => {\n  await page.evaluate(() => {\n    document.body.insertAdjacentHTML(\n      \"afterbegin\",\n      `\n        <turbo-stream action=\"append\" target=\"messages\">\n          <template>\n            <div class=\"message\">Hello world!</div>\n          </template>\n        </turbo-stream>\n      `\n    )\n  })\n\n  const messages = page.locator(\"#messages .message\")\n  await expect(messages).toHaveText([\"First\", \"Hello world!\"])\n})\n"
  },
  {
    "path": "src/tests/functional/visit_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport { get } from \"http\"\nimport {\n  cancelNextEvent,\n  getSearchParam,\n  isScrolledToSelector,\n  isScrolledToTop,\n  nextAttributeMutationNamed,\n  nextBeat,\n  nextEventNamed,\n  noNextAttributeMutationNamed,\n  readEventLogs,\n  reloadPage,\n  resetMutationLogs,\n  scrollToSelector,\n  visitAction,\n  willChangeBody,\n  withPathname\n} from \"../helpers/page\"\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/visit.html\")\n  await readEventLogs(page)\n})\n\ntest(\"programmatically visiting a same-origin location\", async ({ page }) => {\n  const urlBeforeVisit = page.url()\n  await visitLocation(page, \"/src/tests/fixtures/one.html\")\n\n  await expect(page).not.toHaveURL(urlBeforeVisit)\n  expect(await visitAction(page)).toEqual(\"advance\")\n\n  const { url: urlFromBeforeVisitEvent } = await nextEventNamed(page, \"turbo:before-visit\")\n  await expect(page).not.toHaveURL(withPathname(urlFromBeforeVisitEvent))\n\n  const { url: urlFromVisitEvent } = await nextEventNamed(page, \"turbo:visit\")\n  await expect(page).not.toHaveURL(withPathname(urlFromVisitEvent))\n\n  const { timing } = await nextEventNamed(page, \"turbo:load\")\n  expect(timing).toBeTruthy()\n})\n\ntest(\"skip programmatically visiting a cross-origin location falls back to window.location\", async ({ page }) => {\n  const urlBeforeVisit = page.url()\n  await visitLocation(page, \"about:blank\")\n  await nextBeat()\n\n  await expect(page).not.toHaveURL(urlBeforeVisit)\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n\ntest(\"visiting a location served with a known non-HTML content type\", async ({ page }) => {\n  const requestedUrls = []\n  page.on('request', (req) => { requestedUrls.push([req.resourceType(), req.url()]) })\n\n  const urlBeforeVisit = page.url()\n  await visitLocation(page, \"/src/tests/fixtures/svg.svg\")\n  await nextBeat()\n\n  const url = page.url()\n  const contentType = await contentTypeOfURL(url)\n  expect(contentType).toEqual(\"image/svg+xml\")\n\n  expect(requestedUrls).toEqual([\n    [\"document\", \"http://localhost:9000/src/tests/fixtures/svg.svg\"]\n  ])\n\n  await expect(page).not.toHaveURL(urlBeforeVisit)\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n\ntest(\"visiting a location served with an unknown non-HTML content type\", async ({ page }) => {\n  const requestedUrls = []\n  page.on('request', (req) => { requestedUrls.push([req.resourceType(), req.url()]) })\n\n  const urlBeforeVisit = page.url()\n  await visitLocation(page, \"/__turbo/file.unknown_svg\")\n  await nextBeat()\n\n  // Because the file extension is not a known extension, Turbo will request it first to\n  // determine the content type and only then refresh the full page to the provided location\n  expect(requestedUrls).toEqual([\n    [\"fetch\", \"http://localhost:9000/__turbo/file.unknown_svg\"],\n    [\"document\", \"http://localhost:9000/__turbo/file.unknown_svg\"]\n  ])\n\n  await expect(page).not.toHaveURL(urlBeforeVisit)\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n\ntest(\"visiting a location served with an unknown non-HTML content type added to the unvisitableExtensions set\", async ({ page }) => {\n  const requestedUrls = []\n  page.on('request', (req) => { requestedUrls.push([req.resourceType(), req.url()]) })\n\n  page.evaluate(() => {\n    window.Turbo.config.drive.unvisitableExtensions.add(\".unknown_svg\")\n  })\n\n  const urlBeforeVisit = page.url()\n  await visitLocation(page, \"/__turbo/file.unknown_svg\")\n  await nextBeat()\n\n  expect(requestedUrls).toEqual([\n    [\"document\", \"http://localhost:9000/__turbo/file.unknown_svg\"]\n  ])\n\n  await expect(page).not.toHaveURL(urlBeforeVisit)\n  expect(await visitAction(page)).toEqual(\"load\")\n})\n\ntest(\"visiting a location with a non-HTML extension\", async ({ page }) => {\n  await visitLocation(page, \"/__turbo/file.unknown_html\")\n  await nextBeat()\n\n  expect(await visitAction(page)).toEqual(\"advance\")\n})\n\ntest(\"refreshing a location with a non-HTML extension\", async ({ page }) => {\n  await page.goto(\"/__turbo/file.unknown_html\")\n  const urlBeforeVisit = page.url()\n\n  await visitLocation(page, \"/__turbo/file.unknown_html\")\n  await nextBeat()\n\n  await expect(page).toHaveURL(urlBeforeVisit)\n  expect(await visitAction(page)).toEqual(\"advance\")\n})\n\ntest(\"canceling a turbo:click event falls back to built-in browser navigation\", async ({ page }) => {\n  await cancelNextEvent(page, \"turbo:click\")\n  await Promise.all([page.waitForNavigation(), page.click(\"#same-origin-link\")])\n\n  await expect(page).toHaveURL(withPathname(\"/src/tests/fixtures/one.html\"))\n})\n\ntest(\"canceling a before-visit event prevents navigation\", async ({ page }) => {\n  await cancelNextVisit(page)\n  const urlBeforeVisit = page.url()\n\n  expect(\n    await willChangeBody(page, async () => {\n      await page.click(\"#same-origin-link\")\n      await nextBeat()\n    })\n  ).not.toBeTruthy()\n\n  await expect(page).toHaveURL(urlBeforeVisit)\n})\n\ntest(\"navigation by history is not cancelable\", async ({ page }) => {\n  await page.click(\"#same-origin-link\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"One\")\n\n  await cancelNextVisit(page)\n  await page.goBack()\n  await nextEventNamed(page, \"turbo:load\")\n\n  await expect(page.locator(\"h1\")).toHaveText(\"Visit\")\n})\n\ntest(\"turbo:before-fetch-request event.detail\", async ({ page }) => {\n  await page.click(\"#same-origin-link\")\n  const { url, fetchOptions } = await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  expect(fetchOptions.method).toEqual(\"GET\")\n  expect(url).toContain(\"/src/tests/fixtures/one.html\")\n})\n\ntest(\"turbo:before-fetch-request event.detail encodes searchParams\", async ({ page }) => {\n  await page.click(\"#same-origin-link-search-params\")\n  const { url } = await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  expect(url).toContain(\"/src/tests/fixtures/one.html?key=value\")\n})\n\ntest(\"turbo:before-fetch-response open new site\", async ({ page }) => {\n  page.evaluate(() =>\n    addEventListener(\n      \"turbo:before-fetch-response\",\n      async function eventListener(event) {\n        removeEventListener(\"turbo:before-fetch-response\", eventListener, false)\n        window.fetchResponseResult = {\n          responseText: await event.detail.fetchResponse.responseText,\n          responseHTML: await event.detail.fetchResponse.responseHTML\n        }\n      },\n      false\n    )\n  )\n\n  await page.click(\"#sample-response\")\n  await nextEventNamed(page, \"turbo:before-fetch-response\")\n\n  const fetchResponseResult = await page.evaluate(() => window.fetchResponseResult)\n\n  expect(fetchResponseResult.responseText.indexOf(\"An element with an ID\")).toBeGreaterThan(-1)\n  expect(fetchResponseResult.responseHTML.indexOf(\"An element with an ID\")).toBeGreaterThan(-1)\n})\n\ntest(\"visits with data-turbo-stream include MIME type & search params\", async ({ page }) => {\n  await page.click(\"#stream-link\")\n  const { fetchOptions, url } = await nextEventNamed(page, \"turbo:before-fetch-request\")\n\n  expect(fetchOptions.headers[\"Accept\"]).toContain(\"text/vnd.turbo-stream.html\")\n  expect(getSearchParam(url, \"key\")).toEqual(\"value\")\n})\n\ntest(\"visits with data-turbo-stream do not set aria-busy\", async ({ page }) => {\n  await page.click(\"#stream-link\")\n\n  expect(\n    await noNextAttributeMutationNamed(page, \"html\", \"aria-busy\"),\n    \"never sets [aria-busy] on the document element\"\n  ).toBeTruthy()\n})\n\ntest(\"cache does not override response after redirect\", async ({ page }) => {\n  await page.evaluate(() => {\n    const cachedElement = document.createElement(\"some-cached-element\")\n    document.body.appendChild(cachedElement)\n  })\n\n  await expect(page.locator(\"some-cached-element\")).toHaveCount(1)\n\n  await page.click(\"#same-origin-link\")\n  await nextBeat()\n  await page.click(\"#redirection-link\")\n\n  await expect(page.locator(\"some-cached-element\")).toHaveCount(0)\n})\n\ntest(\"cache does not hide temporary elements on the second visit after redirect\", async ({ page }) => {\n  await page.click(\"#cache-observer-link\")\n  await nextBeat()\n  await page.click(\"#redirect-here-link\")\n  await nextBeat() // 301 redirect response\n  await nextBeat() // 200 response\n\n  await expect(page.locator(\"#temporary\")).toHaveCount(1)\n\n  await page.click(\"#redirect-here-link\")\n  await nextBeat() // 301 redirect response\n  await nextBeat() // 200 response\n\n  await expect(page.locator(\"#temporary\")).toHaveCount(1)\n})\n\nfunction cancelNextVisit(page) {\n  return cancelNextEvent(page, \"turbo:before-visit\")\n}\n\nfunction contentTypeOfURL(url) {\n  return new Promise((resolve) => {\n    get(url, ({ headers }) => resolve(headers[\"content-type\"]))\n  })\n}\n\ntest(\"can scroll to element after click-initiated turbo:visit\", async ({ page }) => {\n  const id = \"below-the-fold-link\"\n  await page.evaluate((id) => {\n    addEventListener(\"turbo:load\", () => document.getElementById(id)?.scrollIntoView())\n  }, id)\n\n  expect(await isScrolledToTop(page), \"starts unscrolled\").toBeTruthy()\n\n  await page.click(\"#same-page-link\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  expect(await isScrolledToSelector(page, \"#\" + id), \"scrolls after click-initiated turbo:load\").toBeTruthy()\n})\n\ntest(\"can scroll to element after history-initiated turbo:visit\", async ({ page }) => {\n  const id = \"below-the-fold-link\"\n  await page.evaluate((id) => {\n    addEventListener(\"turbo:load\", () => document.getElementById(id)?.scrollIntoView())\n  }, id)\n\n  await scrollToSelector(page, \"#\" + id)\n  await page.click(\"#\" + id)\n  await nextEventNamed(page, \"turbo:load\")\n  await page.goBack()\n  await nextEventNamed(page, \"turbo:load\")\n\n  expect(await isScrolledToSelector(page, \"#\" + id), \"scrolls after history-initiated turbo:load\").toBeTruthy()\n})\n\ntest(\"Visit with network error\", async ({ page }) => {\n  await page.evaluate(() => {\n    addEventListener(\"turbo:fetch-request-error\", (event) => event.preventDefault())\n  })\n  await page.context().setOffline(true)\n  await page.click(\"#same-origin-link\")\n  await nextEventNamed(page, \"turbo:fetch-request-error\")\n})\n\ntest(\"Visit direction data attribute when clicking a link\", async ({ page }) => {\n  page.click(\"#same-origin-link\")\n  await assertVisitDirectionAttribute(page, \"forward\")\n})\n\ntest(\"Visit direction data attribute when navigating back\", async ({ page }) => {\n  await page.click(\"#same-origin-link\")\n  await nextEventNamed(page, \"turbo:load\")\n\n  await resetMutationLogs(page)\n\n  page.goBack()\n\n  await assertVisitDirectionAttribute(page, \"back\")\n})\n\ntest(\"Visit direction attribute when navigating forward\", async ({ page }) => {\n  await page.click(\"#same-origin-link\")\n  await nextEventNamed(page, \"turbo:load\")\n  await page.goBack()\n  await nextEventNamed(page, \"turbo:load\")\n\n  page.goForward()\n\n  await assertVisitDirectionAttribute(page, \"forward\")\n})\n\ntest(\"Visit direction attribute on a replace visit\", async ({ page }) => {\n  page.click(\"#same-origin-replace-link\")\n\n  await assertVisitDirectionAttribute(page, \"none\")\n})\n\ntest(\"Turbo history state after a reload\", async ({ page }) => {\n  await page.click(\"#same-origin-link\")\n  await nextEventNamed(page, \"turbo:load\")\n  await reloadPage(page)\n  expect(\n    await page.evaluate(() => window.history.state.turbo.restorationIndex),\n    \"restorationIndex is persisted between reloads\"\n  ).toEqual(\n    1\n  )\n})\n\nasync function visitLocation(page, location) {\n  return page.evaluate((location) => window.Turbo.visit(location), location)\n}\n\nasync function assertVisitDirectionAttribute(page, direction) {\n  expect(await nextAttributeMutationNamed(page, \"html\", \"data-turbo-visit-direction\")).toEqual(direction)\n  await expect(page.locator(\"[data-turbo-visit-direction]\")).not.toBeAttached()\n}\n"
  },
  {
    "path": "src/tests/helpers/dom_test_case.js",
    "content": "export class DOMTestCase {\n  fixtureElement = document.createElement(\"main\")\n\n  async setup() {\n    this.fixtureElement.hidden = true\n    document.body.insertAdjacentElement(\"afterbegin\", this.fixtureElement)\n  }\n\n  async teardown() {\n    this.fixtureElement.innerHTML = \"\"\n    this.fixtureElement.remove()\n  }\n\n  append(node) {\n    this.fixtureElement.appendChild(node)\n  }\n\n  find(selector) {\n    return this.fixtureElement.querySelector(selector)\n  }\n\n  get fixtureHTML() {\n    return this.fixtureElement.innerHTML\n  }\n\n  set fixtureHTML(html) {\n    this.fixtureElement.innerHTML = html\n  }\n}\n"
  },
  {
    "path": "src/tests/helpers/page.js",
    "content": "export function attributeForSelector(page, selector, attributeName) {\n  return page.locator(selector).getAttribute(attributeName)\n}\n\nexport function cancelNextEvent(page, eventName) {\n  return page.evaluate(\n    (eventName) => addEventListener(eventName, (event) => event.preventDefault(), { once: true }),\n    eventName\n  )\n}\n\nexport function clickWithoutScrolling(page, selector, options = {}) {\n  const element = page.locator(selector, options)\n\n  return element.evaluate((element) => element instanceof HTMLElement && element.click())\n}\n\nexport function clearLocalStorage(page) {\n  return page.evaluate(() => localStorage.clear())\n}\n\nexport function disposeAll(...handles) {\n  return Promise.all(handles.map((handle) => handle.dispose()))\n}\n\nexport function getComputedStyle(page, selector, propertyName) {\n  return page.evaluate(\n    ([selector, propertyName]) => {\n      const element = document.querySelector(selector)\n      return getComputedStyle(element)[propertyName]\n    },\n    [selector, propertyName]\n  )\n}\n\nexport function cssClassIsDefined(page, className) {\n  return page.evaluate((className) => {\n    for (const stylesheet of document.styleSheets) {\n      for (const rule of stylesheet.cssRules) {\n        if (rule instanceof CSSStyleRule && rule.selectorText == `.${className}`) {\n          return true\n        }\n      }\n    }\n  }, className)\n}\n\nexport function getFromLocalStorage(page, key) {\n  return page.evaluate((storageKey) => localStorage.getItem(storageKey), key)\n}\n\nexport function getSearchParam(url, key) {\n  return searchParams(url).get(key)\n}\n\nexport async function hasSelector(page, selector) {\n  return !!(await page.locator(selector).count())\n}\n\nexport async function isScrolledToSelector(page, selector) {\n  const boundingBox = await page\n    .locator(selector)\n    .evaluate((element) => (element instanceof HTMLElement ? { x: element.offsetLeft, y: element.offsetTop } : null))\n\n  if (boundingBox) {\n    const { y: pageY } = await scrollPosition(page)\n    const { y: elementY } = boundingBox\n    const offset = pageY - elementY\n    return Math.abs(offset) <= 2\n  } else {\n    return false\n  }\n}\n\nexport function nextBeat() {\n  return sleep(100)\n}\n\nexport function nextBody(_page, timeout = 500) {\n  return sleep(timeout)\n}\n\nexport async function reloadPage(page, timeout = 500) {\n  await Promise.all([\n    nextBody(page, timeout),\n    page.evaluate(() => window.location.reload())\n  ])\n}\n\nexport async function nextPageRefresh(page, timeout = 500) {\n  const pageRefreshDebouncePeriod = await page.evaluate(() => window.Turbo.session.pageRefreshDebouncePeriod)\n\n  return sleep(pageRefreshDebouncePeriod + timeout)\n}\n\nconst timeout = 2000\n\nexport async function nextEventNamed(page, eventName, expectedDetail = {}) {\n  let record\n  const startTime = new Date()\n  while (!record) {\n    if (new Date() - startTime > timeout) {\n      throw new Error(`Event ${eventName} with ${JSON.stringify(expectedDetail)} wasn't dispatched within ${timeout}ms`)\n    }\n    const records = await readEventLogs(page, 1)\n    record = records.find(([name, detail]) => {\n      return name == eventName && Object.entries(expectedDetail).every(([key, value]) => detail[key] === value)\n    })\n  }\n  return record[1]\n}\n\nexport async function nextEventOnTarget(page, elementId, eventName) {\n  let record\n  const startTime = new Date()\n  while (!record) {\n    if (new Date() - startTime > timeout) {\n      throw new Error(`Element ${elementId} didn't dispatch event ${eventName} within ${timeout}ms`)\n    }\n    const records = await readEventLogs(page, 1)\n    record = records.find(([name, _, id]) => name == eventName && id == elementId)\n  }\n  return record[1]\n}\n\nexport async function listenForEventOnTarget(page, elementId, eventName) {\n  return page.locator(\"#\" + elementId).evaluate((element, eventName) => {\n    const eventLogs = window.eventLogs\n\n    element.addEventListener(eventName, ({ target, type }) => {\n      if (target instanceof Element) {\n        eventLogs.push([type, {}, target.id])\n      }\n    })\n  }, eventName)\n}\n\nexport async function nextBodyMutation(page) {\n  let record\n  const startTime = new Date()\n  while (!record) {\n    if (new Date() - startTime > timeout) {\n      throw new Error(`body mutation didn't occur within ${timeout}ms`)\n    }\n    [record] = await readBodyMutationLogs(page, 1)\n  }\n  return record[0]\n}\n\nexport async function noNextBodyMutation(page) {\n  const records = await readBodyMutationLogs(page, 1)\n  return !records.some((record) => !!record)\n}\n\nexport async function nextAttributeMutationNamed(page, elementId, attributeName) {\n  let record\n  const startTime = new Date()\n  while (!record) {\n    if (new Date() - startTime > timeout) {\n      throw new Error(`Element ${elementId}'s ${attributeName} attribute mutation didn't occur within ${timeout}ms`)\n    }\n    const records = await readMutationLogs(page, 1)\n    record = records.find(([name, id]) => name == attributeName && id == elementId)\n  }\n  const attributeValue = record[2]\n  return attributeValue\n}\n\nexport async function noNextAttributeMutationNamed(page, elementId, attributeName) {\n  const records = await readMutationLogs(page, 1)\n  return !records.some(([name, _, target]) => name == attributeName && target == elementId)\n}\n\nexport async function noNextEventNamed(page, eventName, expectedDetail = {}) {\n  const records = await readEventLogs(page)\n  return !records.some(([name, detail]) => {\n    return name === eventName && Object.entries(expectedDetail).every(([key, value]) => value === detail[key])\n  })\n}\n\nexport async function noNextEventOnTarget(page, elementId, eventName) {\n  const records = await readEventLogs(page, 1)\n  return !records.some(([name, _, target]) => name == eventName && target == elementId)\n}\n\nexport async function outerHTMLForSelector(page, selector) {\n  const element = await page.locator(selector)\n  return element.evaluate((element) => element.outerHTML)\n}\n\nexport function pathname(url) {\n  const { pathname } = new URL(url)\n\n  return pathname\n}\n\nexport function withHash(value) {\n  return ({ hash }) => hash === value\n}\n\nexport function withPathname(value) {\n  return ({ pathname }) => pathname === value\n}\n\nexport function withSearch(value) {\n  return ({ search }) => search === value\n}\n\nexport function withSearchParam(name, value) {\n  return ({ searchParams }) => Array.isArray(value) ?\n    JSON.stringify(searchParams.getAll(name)) === JSON.stringify(value) :\n    searchParams.get(name) === value\n}\n\nexport async function pathnameForIFrame(page, name) {\n  const locator = await page.locator(`[name=\"${name}\"]`)\n  const location = await locator.evaluate((iframe) => iframe.contentWindow?.location)\n\n  if (location) {\n    return pathname(location.href)\n  } else {\n    return \"\"\n  }\n}\n\nexport function propertyForSelector(page, selector, propertyName) {\n  return page.locator(selector).evaluate((element, propertyName) => element[propertyName], propertyName)\n}\n\nexport function resetMutationLogs(page) {\n  return page.evaluate(() => {\n    window.mutationLogs = []\n  })\n}\n\nasync function readArray(page, identifier, length) {\n  return page.evaluate(\n    ({ identifier, length }) => {\n      const records = window[identifier]\n      if (records != null && typeof records.splice == \"function\") {\n        return records.splice(0, typeof length === \"undefined\" ? records.length : length)\n      } else {\n        return []\n      }\n    },\n    { identifier, length }\n  )\n}\n\nexport function readBodyMutationLogs(page, length) {\n  return readArray(page, \"bodyMutationLogs\", length)\n}\n\nexport function readEventLogs(page, length) {\n  return readArray(page, \"eventLogs\", length)\n}\n\nexport function readMutationLogs(page, length) {\n  return readArray(page, \"mutationLogs\", length)\n}\n\nexport function refreshWithStream(page) {\n  return page.evaluate(() => document.body.insertAdjacentHTML(\"beforeend\", `<turbo-stream action=\"refresh\"></turbo-stream>`))\n}\n\nexport function searchParams(url) {\n  const { searchParams } = new URL(url)\n\n  return searchParams\n}\n\nexport function setLocalStorageFromEvent(page, eventName, storageKey, storageValue) {\n  return page.evaluate(\n    ({ eventName, storageKey, storageValue }) => {\n      addEventListener(eventName, () => localStorage.setItem(storageKey, storageValue))\n    },\n    { eventName, storageKey, storageValue }\n  )\n}\n\nexport function scrollPosition(page) {\n  return page.evaluate(() => ({ x: window.scrollX, y: window.scrollY }))\n}\n\nexport async function isScrolledToTop(page) {\n  const { y: pageY } = await scrollPosition(page)\n  return pageY === 0\n}\n\nexport function scrollToSelector(page, selector) {\n  return page.locator(selector).scrollIntoViewIfNeeded()\n}\n\nexport function sleep(timeout = 0) {\n  return new Promise((resolve) => setTimeout(() => resolve(undefined), timeout))\n}\n\nexport async function strictElementEquals(left, right) {\n  return left.evaluate((left, right) => left === right, await right.elementHandle())\n}\n\nexport function textContent(page, html) {\n  return page.evaluate((html) => {\n    const parser = new DOMParser()\n    const { documentElement } = parser.parseFromString(html, \"text/html\")\n\n    return documentElement.textContent\n  }, html)\n}\n\nexport function visitAction(page) {\n  return page.evaluate(() => {\n    try {\n      const lastVisit = window.visitLogs[window.visitLogs.length - 1]\n\n      return lastVisit.action\n    } catch {\n      return \"load\"\n    }\n  })\n}\n\nexport async function willChangeBody(page, callback) {\n  const handles = []\n\n  try {\n    const originalBody = await page.evaluateHandle(() => document.body)\n    handles.push(originalBody)\n\n    await callback()\n\n    const latestBody = await page.evaluateHandle(() => document.body)\n    handles.push(latestBody)\n\n    return page.evaluate(({ originalBody, latestBody }) => originalBody !== latestBody, { originalBody, latestBody })\n  } finally {\n    disposeAll(...handles)\n  }\n}\n"
  },
  {
    "path": "src/tests/integration/ujs_tests.js",
    "content": "import { expect, test } from \"@playwright/test\"\nimport { nextEventOnTarget, noNextEventOnTarget } from \"../helpers/page\"\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto(\"/src/tests/fixtures/ujs.html\")\n})\n\ntest(\"allows UJS to intercept and cancel Turbo requests for anchors inside a turbo-frame\", async ({ page }) => {\n  await assertRequestLimit(page, 1, async () => {\n    await expect(page.locator(\"#frame h2\")).toHaveText(\"Frames: #frame\")\n\n    await page.click(\"#frame a[data-remote=true]\")\n\n    await expect(page.locator(\"#frame\")).toHaveText(\"Content from UJS response\")\n    expect(await noNextEventOnTarget(page, \"frame\", \"turbo:frame-load\")).toBeTruthy()\n  })\n})\n\ntest(\"handles [data-remote=true] forms within a turbo-frame\", async ({ page }) => {\n  await assertRequestLimit(page, 1, async () => {\n    await expect(page.locator(\"#frame h2\")).toHaveText(\"Frames: #frame\")\n\n    await page.click(\"#frame form[data-remote=true] button\")\n\n    expect(await nextEventOnTarget(page, \"frame\", \"turbo:frame-load\")).toBeTruthy()\n    await expect(page.locator(\"#frame h2\"), \"navigates the target frame\").toHaveText(\"Frame: Loaded\")\n  })\n})\n\nasync function assertRequestLimit(page, count, callback) {\n  let requestsStarted = 0\n  await page.on(\"request\", () => requestsStarted++)\n  await callback()\n\n  expect(requestsStarted, `only submits ${count} requests`).toEqual(count)\n}\n"
  },
  {
    "path": "src/tests/server.mjs",
    "content": "import express, { Router } from \"express\"\nimport bodyParser from \"body-parser\"\nimport multer from \"multer\"\nimport path from \"path\"\nimport url, { fileURLToPath } from \"url\"\nimport fs from \"fs\"\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = path.dirname(__filename)\n\nconst { json, urlencoded } = bodyParser\n\nconst router = Router()\nconst streamResponses = new Set()\n\nrouter.use(multer().none())\n\nrouter.use((request, response, next) => {\n  if (request.accepts([\"text/html\", \"application/xhtml+xml\", \"text/event-stream\"])) {\n    next()\n  } else {\n    response.sendStatus(422)\n  }\n})\n\nrouter.post(\"/redirect\", (request, response) => {\n  const { path, sleep, ...query } = request.body\n  const { pathname, query: searchParams } = url.parse(\n    path ?? request.query.path ?? \"/src/tests/fixtures/one.html\",\n    true\n  )\n  const enctype = request.get(\"Content-Type\")\n  if (enctype) {\n    query.enctype = enctype\n  }\n  setTimeout(\n    () => response.redirect(303, url.format({ pathname, query: { ...query, ...searchParams } })),\n    parseInt(sleep || \"0\", 10)\n  )\n})\n\nrouter.get(\"/redirect\", (request, response) => {\n  const { path, ...query } = request.query\n  const pathname = path ?? \"/src/tests/fixtures/one.html\"\n  const enctype = request.get(\"Content-Type\")\n  if (enctype) {\n    query.enctype = enctype\n  }\n  response.redirect(301, url.format({ pathname, query }))\n})\n\nrouter.post(\"/refresh\", (request, response) => {\n  const { sleep } = request.body\n  setTimeout(() => response.redirect(\"back\"), parseInt(sleep || \"0\", 10))\n})\n\nrouter.post(\"/reject/tall\", (request, response) => {\n  const { status } = request.body\n  const fixture = path.join(__dirname, `../../src/tests/fixtures/422_tall.html`)\n\n  response.status(parseInt(status || \"422\", 10)).sendFile(fixture)\n})\n\nrouter.post(\"/reject/morph\", (request, response) => {\n  const { status } = request.body\n  const fixture = path.join(__dirname, `../../src/tests/fixtures/422_morph.html`)\n\n  response.status(parseInt(status || \"422\", 10)).sendFile(fixture)\n})\n\nrouter.post(\"/reject\", (request, response) => {\n  const { status } = request.body\n  const fixture = path.join(__dirname, `../../src/tests/fixtures/${status}.html`)\n\n  response.status(parseInt(status || \"422\", 10)).sendFile(fixture)\n})\n\nrouter.get(\"/headers\", (request, response) => {\n  const template = fs.readFileSync(\"src/tests/fixtures/headers.html\").toString()\n  response\n    .type(\"html\")\n    .status(200)\n    .send(template.replace(\"$HEADERS\", JSON.stringify(request.headers, null, 4)))\n})\n\nrouter.get(\"/delayed_response\", (request, response) => {\n  const { status } = request.query\n  const fixture = path.join(__dirname, \"../../src/tests/fixtures/one.html\")\n  setTimeout(() => response.status(parseInt(status || \"200\")).sendFile(fixture), 1000)\n})\n\nrouter.post(\"/messages\", (request, response) => {\n  const params = { ...request.body, ...request.query }\n  const { content, id, status, type, target, targets } = params\n  if (typeof content == \"string\") {\n    receiveMessage(content, id, target)\n    if (type == \"stream\" && acceptsStreams(request)) {\n      response.type(\"text/vnd.turbo-stream.html; charset=utf-8\")\n      response.send(targets ? renderMessageForTargets(content, id, targets) : renderMessage(content, id, target))\n    } else {\n      response.sendStatus(parseInt(status || \"201\", 10))\n    }\n  } else {\n    response.sendStatus(422)\n  }\n})\n\nrouter.post(\"/refreshes\", (request, response) => {\n  const params = { ...request.body, ...request.query }\n  const { requestId } = params\n\n  if(acceptsStreams(request)){\n    response.type(\"text/vnd.turbo-stream.html; charset=utf-8\")\n    response.send(renderPageRefresh(requestId))\n  } else {\n    response.sendStatus(201)\n  }\n})\n\nrouter.get(\"/request_id_header\", (request, response) => {\n  const turboRequestHeader = request.get(\"X-Turbo-Request-Id\")\n\n  if (turboRequestHeader) {\n    response.send(turboRequestHeader);\n  } else {\n    response.status(404).send(\"X-Turbo-Request header not found\")\n  }\n})\n\nrouter.post(\"/notfound\", (request, response) => {\n  response.type(\"html\").status(404).send(\"<html><body><h1>Not found</h1></body></html>\")\n})\n\nrouter.get(\"/stream-response\", (request, response) => {\n  const params = { ...request.body, ...request.query }\n  const { content, target, targets } = params\n  if (acceptsStreams(request)) {\n    response.type(\"text/vnd.turbo-stream.html; charset=utf-8\")\n    response.send(targets ? renderMessageForTargets(content, null, targets) : renderMessage(content, target))\n  } else {\n    response.sendStatus(422)\n  }\n})\n\nrouter.put(\"/messages/:id\", (request, response) => {\n  const { content, type } = request.body\n  const { id } = request.params\n  if (typeof content == \"string\") {\n    receiveMessage(content, id)\n    if (type == \"stream\" && acceptsStreams(request)) {\n      response.type(\"text/vnd.turbo-stream.html; charset=utf-8\")\n      response.send(renderMessage(id + \": \" + content, id))\n    } else {\n      response.sendStatus(200)\n    }\n  } else {\n    response.sendStatus(422)\n  }\n})\n\nrouter.get(\"/messages\", (request, response) => {\n  response.set({\n    \"Cache-Control\": \"no-cache\",\n    \"Content-Type\": \"text/event-stream\",\n    Connection: \"keep-alive\",\n  })\n\n  response.on(\"close\", () => {\n    streamResponses.delete(response)\n    response.end()\n  })\n\n  response.flushHeaders()\n  response.write(\"data:\\n\\n\")\n  streamResponses.add(response)\n})\n\nrouter.get(\"/file.unknown_svg\", (request, response) => {\n  response.set({\n    \"Content-Type\": \"image/svg+xml\"\n  })\n  response.sendFile(path.join(__dirname, \"../../src/tests/fixtures/svg.svg\"))\n})\n\nrouter.get(\"/file.unknown_html\", (request, response) => {\n  response.set({\n    \"Content-Type\": \"text/html\"\n  })\n  response.sendFile(path.join(__dirname, \"../../src/tests/fixtures/visit.html\"))\n})\n\nfunction receiveMessage(content, id, target) {\n  const data = renderSSEData(renderMessage(content, id, target))\n  for (const response of streamResponses) {\n    console.log(\"delivering message to stream\", response.socket?.remotePort)\n    response.write(data)\n  }\n}\n\nfunction renderMessage(content, id, target = \"messages\") {\n  return `\n    <turbo-stream id=\"${id}\" action=\"append\" target=\"${target}\"><template>\n      <div class=\"message\">${escapeHTML(content)}</div>\n    </template></turbo-stream>\n  `\n}\n\nfunction renderMessageForTargets(content, id, targets) {\n  return `\n    <turbo-stream id=\"${id}\" action=\"append\" targets=\"${targets}\"><template>\n      <div class=\"message\">${escapeHTML(content)}</div>\n    </template></turbo-stream>\n  `\n}\n\nfunction renderPageRefresh(requestId) {\n  return `\n    <turbo-stream action=\"refresh\" request-id=\"${requestId}\"></turbo-stream>\n  `\n}\n\nfunction acceptsStreams(request) {\n  return !!request.accepts(\"text/vnd.turbo-stream.html\")\n}\n\nfunction renderSSEData(data) {\n  return (\n    `${data}`\n      .split(\"\\n\")\n      .map((line) => \"data:\" + line)\n      .join(\"\\n\") + \"\\n\\n\"\n  )\n}\n\nfunction escapeHTML(html) {\n  return html.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\")\n}\nconst app = express()\n\napp.use(json({ limit: \"1mb\" }), urlencoded({ extended: true }))\napp.use(express.static(\".\"))\napp.use(/\\/__turbo/, router)\n\nconst port = parseInt(process.env.PORT || \"9000\")\n\napp.listen(port, () => {\n  console.log(`Test server listening on port ${port}`)\n})\n\nexport const TestServer = router\n"
  },
  {
    "path": "src/tests/unit/deprecated_adapter_support_tests.js",
    "content": "import * as Turbo from \"../../index\"\nimport { assert } from \"@open-wc/testing\"\n\nclass DeprecatedAdapterSupportTest {\n  locations = []\n  // Adapter interface\n  visitProposedToLocation(location, _options) {\n    this.locations.push(location)\n  }\n\n  visitStarted(visit) {\n    this.locations.push(visit.location)\n    visit.cancel()\n  }\n\n  visitCompleted(_visit) {}\n\n  visitFailed(_visit) {}\n\n  visitRequestStarted(_visit) {}\n\n  visitRequestCompleted(_visit) {}\n\n  visitRequestFailedWithStatusCode(_visit, _statusCode) {}\n\n  visitRequestFinished(_visit) {}\n\n  visitRendered(_visit) {}\n\n  formSubmissionStarted(_formSubmission) {}\n\n  formSubmissionFinished(_formSubmission) {}\n\n  pageInvalidated() {}\n}\n\nlet adapter\n\nsetup(() => {\n  adapter = new DeprecatedAdapterSupportTest()\n  Turbo.registerAdapter(adapter)\n})\n\ntest(\"visit proposal location includes deprecated absoluteURL property\", async () => {\n  Turbo.navigator.proposeVisit(new URL(window.location.toString()))\n  assert.equal(adapter.locations.length, 1)\n\n  const [location] = adapter.locations\n  assert.equal(location.toString(), location.absoluteURL)\n})\n\ntest(\"visit start location includes deprecated absoluteURL property\", async () => {\n  Turbo.navigator.startVisit(window.location.toString(), \"123\")\n  assert.equal(adapter.locations.length, 1)\n\n  const [location] = adapter.locations\n  assert.equal(location.toString(), location.absoluteURL)\n})\n"
  },
  {
    "path": "src/tests/unit/export_tests.js",
    "content": "import { assert } from \"@open-wc/testing\"\nimport * as Turbo from \"../../\"\nimport { StreamActions } from \"../../\"\n\ntest(\"Turbo interface\", () => {\n  assert.equal(typeof Turbo.StreamActions, \"object\")\n  assert.equal(typeof Turbo.start, \"function\")\n  assert.equal(typeof Turbo.registerAdapter, \"function\")\n  assert.equal(typeof Turbo.visit, \"function\")\n  assert.equal(typeof Turbo.connectStreamSource, \"function\")\n  assert.equal(typeof Turbo.disconnectStreamSource, \"function\")\n  assert.equal(typeof Turbo.renderStreamMessage, \"function\")\n  assert.equal(typeof Turbo.setProgressBarDelay, \"function\")\n  assert.equal(typeof Turbo.setConfirmMethod, \"function\")\n  assert.equal(typeof Turbo.setFormMode, \"function\")\n  assert.equal(typeof Turbo.cache, \"object\")\n  assert.equal(typeof Turbo.config, \"object\")\n  assert.equal(typeof Turbo.cache.clear, \"function\")\n  assert.equal(typeof Turbo.navigator, \"object\")\n  assert.equal(typeof Turbo.session, \"object\")\n  assert.equal(typeof Turbo.session.drive, \"boolean\")\n  assert.equal(typeof Turbo.session.formMode, \"string\")\n  assert.equal(typeof Turbo.fetch, \"function\")\n  assert.equal(typeof Turbo.morphElements, \"function\")\n  assert.equal(typeof Turbo.morphChildren, \"function\")\n  assert.equal(typeof Turbo.morphBodyElements, \"function\")\n  assert.equal(typeof Turbo.morphTurboFrameElements, \"function\")\n})\n\ntest(\"Session interface\", () => {\n  const { session, config } = Turbo\n\n  assert.equal(true, session.drive)\n  assert.equal(true, config.drive.enabled)\n  assert.equal(\"on\", session.formMode)\n  assert.equal(\"on\", config.forms.mode)\n\n  session.drive = false\n  session.formMode = \"off\"\n\n  assert.equal(false, session.drive)\n  assert.equal(false, config.drive.enabled)\n  assert.equal(\"off\", session.formMode)\n  assert.equal(\"off\", config.forms.mode)\n})\n\ntest(\"StreamActions interface\", () => {\n  assert.equal(typeof StreamActions, \"object\")\n})\n"
  },
  {
    "path": "src/tests/unit/limited_set_tests.js",
    "content": "import { assert } from \"@open-wc/testing\"\nimport { LimitedSet } from \"../../core/drive/limited_set\"\n\ntest(\"add a limited number of elements\", () => {\n  const set = new LimitedSet(3)\n  set.add(1)\n  set.add(2)\n  set.add(3)\n  set.add(4)\n\n  assert.equal(set.size, 3)\n\n  assert.notInclude(set, 1)\n  assert.include(set, 2)\n  assert.include(set, 3)\n  assert.include(set, 4)\n})\n"
  },
  {
    "path": "src/tests/unit/native_adapter_support_tests.js",
    "content": "import * as Turbo from \"../../index\"\nimport { assert } from \"@open-wc/testing\"\n\nclass NativeAdapterSupportTest {\n  proposedVisits = []\n  startedVisits = []\n  completedVisits = []\n  startedVisitRequests = []\n  completedVisitRequests = []\n  failedVisitRequests = []\n  finishedVisitRequests = []\n  startedFormSubmissions = []\n  finishedFormSubmissions = []\n  linkPrefetchRequests = []\n\n  // Adapter interface\n\n  visitProposedToLocation(location, options) {\n    this.proposedVisits.push({ location, options })\n  }\n\n  visitStarted(visit) {\n    this.startedVisits.push(visit)\n  }\n\n  visitCompleted(visit) {\n    this.completedVisits.push(visit)\n  }\n\n  visitRequestStarted(visit) {\n    this.startedVisitRequests.push(visit)\n  }\n\n  visitRequestCompleted(visit) {\n    this.completedVisitRequests.push(visit)\n  }\n\n  visitRequestFailedWithStatusCode(visit, _statusCode) {\n    this.failedVisitRequests.push(visit)\n  }\n\n  visitRequestFinished(visit) {\n    this.finishedVisitRequests.push(visit)\n  }\n\n  visitRendered(_visit) {}\n\n  formSubmissionStarted(formSubmission) {\n    this.startedFormSubmissions.push(formSubmission)\n  }\n\n  formSubmissionFinished(formSubmission) {\n    this.finishedFormSubmissions.push(formSubmission)\n  }\n\n  pageInvalidated() {}\n\n  linkPrefetchingIsEnabledForLocation(location) {\n    this.linkPrefetchRequests.push(location)\n  }\n}\n\nlet adapter\n\nsetup(() => {\n  adapter = new NativeAdapterSupportTest()\n  Turbo.registerAdapter(adapter)\n})\n\ntest(\"navigator adapter is native adapter\", async () => {\n  assert.equal(adapter, Turbo.navigator.adapter)\n})\n\ntest(\"visit proposal location is proposed to adapter\", async () => {\n  const url = new URL(window.location.toString())\n\n  Turbo.navigator.proposeVisit(url)\n  assert.equal(adapter.proposedVisits.length, 1)\n\n  const [visit] = adapter.proposedVisits\n  assert.equal(visit.location, url)\n})\n\ntest(\"visit proposal external location is proposed to adapter\", async () => {\n  const url = new URL(\"https://example.com/\")\n\n  Turbo.navigator.proposeVisit(url)\n  assert.equal(adapter.proposedVisits.length, 1)\n\n  const [visit] = adapter.proposedVisits\n  assert.equal(visit.location, url)\n})\n\ntest(\"visit started notifies adapter\", async () => {\n  const locatable = window.location.toString()\n\n  Turbo.navigator.startVisit(locatable)\n  assert.equal(adapter.startedVisits.length, 1)\n\n  const [visit] = adapter.startedVisits\n  assert.equal(visit.location, locatable)\n})\n\ntest(\"test visit has cached snapshot returns boolean\", async () => {\n  const locatable = window.location.toString()\n\n  await Turbo.navigator.startVisit(locatable)\n\n  const [visit] = adapter.startedVisits\n  assert.equal(visit.hasCachedSnapshot(), false)\n})\n\ntest(\"visit completed notifies adapter\", async () => {\n  const locatable = window.location.toString()\n\n  Turbo.navigator.startVisit(locatable)\n\n  const [startedVisit] = adapter.startedVisits\n  startedVisit.complete()\n\n  const [completedVisit] = adapter.completedVisits\n  assert.equal(completedVisit.location, locatable)\n})\n\ntest(\"visit request started notifies adapter\", async () => {\n  const locatable = window.location.toString()\n\n  Turbo.navigator.startVisit(locatable)\n\n  const [startedVisit] = adapter.startedVisits\n  startedVisit.startRequest()\n  assert.equal(adapter.startedVisitRequests.length, 1)\n\n  const [startedVisitRequest] = adapter.startedVisitRequests\n  assert.equal(startedVisitRequest.location, locatable)\n})\n\ntest(\"visit request completed notifies adapter\", async () => {\n  const locatable = window.location.toString()\n\n  Turbo.navigator.startVisit(locatable)\n\n  const [startedVisit] = adapter.startedVisits\n  startedVisit.recordResponse({ statusCode: 200, responseHTML: \"responseHtml\", redirected: false })\n  assert.equal(adapter.completedVisitRequests.length, 1)\n\n  const [completedVisitRequest] = adapter.completedVisitRequests\n  assert.equal(completedVisitRequest.location, locatable)\n})\n\ntest(\"visit request failed notifies adapter\", async () => {\n  const locatable = window.location.toString()\n\n  Turbo.navigator.startVisit(locatable)\n\n  const [startedVisit] = adapter.startedVisits\n  startedVisit.recordResponse({ statusCode: 404, responseHTML: \"responseHtml\", redirected: false })\n  assert.equal(adapter.failedVisitRequests.length, 1)\n\n  const [failedVisitRequest] = adapter.failedVisitRequests\n  assert.equal(failedVisitRequest.location, locatable)\n})\n\ntest(\"visit request finished notifies adapter\", async () => {\n  const locatable = window.location.toString()\n\n  Turbo.navigator.startVisit(locatable)\n\n  const [startedVisit] = adapter.startedVisits\n  startedVisit.finishRequest()\n  assert.equal(adapter.finishedVisitRequests.length, 1)\n\n  const [finishedVisitRequest] = adapter.finishedVisitRequests\n  assert.equal(finishedVisitRequest.location, locatable)\n})\n\ntest(\"form submission started notifies adapter\", async () => {\n  Turbo.navigator.formSubmissionStarted(\"formSubmissionStub\")\n  assert.equal(adapter.startedFormSubmissions.length, 1)\n\n  const [startedFormSubmission] = adapter.startedFormSubmissions\n  assert.equal(startedFormSubmission, \"formSubmissionStub\")\n})\n\ntest(\"form submission finished notifies adapter\", async () => {\n  Turbo.navigator.formSubmissionFinished(\"formSubmissionStub\")\n  assert.equal(adapter.finishedFormSubmissions.length, 1)\n\n  const [finishedFormSubmission] = adapter.finishedFormSubmissions\n  assert.equal(finishedFormSubmission, \"formSubmissionStub\")\n})\n\n\ntest(\"visit follows redirect and proposes replace visit to adapter\", async () => {\n  const locatable = window.location.toString()\n  const redirectedLocation = \"https://example.com\"\n\n  Turbo.navigator.startVisit(locatable)\n\n  const [startedVisit] = adapter.startedVisits\n  startedVisit.redirectedToLocation = redirectedLocation\n  startedVisit.recordResponse({ statusCode: 200, responseHTML: \"responseHtml\", redirected: true })\n  startedVisit.complete()\n\n  assert.equal(adapter.completedVisitRequests.length, 1)\n  assert.equal(adapter.proposedVisits.length, 1)\n\n  const [visit] = adapter.proposedVisits\n  assert.equal(visit.location, redirectedLocation)\n  assert.equal(visit.options.action, \"replace\")\n})\n\ntest (\"link prefetch requests verify with adapter\", async () => {\n  const locatable = window.location.toString()\n\n  Turbo.navigator.linkPrefetchingIsEnabledForLocation(locatable)\n  assert.equal(adapter.linkPrefetchRequests.length, 1)\n\n  const [location] = adapter.linkPrefetchRequests\n  assert.equal(location, locatable)\n})\n"
  },
  {
    "path": "src/tests/unit/stream_element_tests.js",
    "content": "import { StreamElement } from \"../../elements\"\nimport { nextAnimationFrame } from \"../../util\"\nimport { DOMTestCase } from \"../helpers/dom_test_case\"\nimport { assert } from \"@open-wc/testing\"\nimport { sleep } from \"../helpers/page\"\nimport * as Turbo from \"../../index\"\n\nfunction createStreamElement(action, target, templateElement, attributes = {}) {\n  const element = new StreamElement()\n  if (action) element.setAttribute(\"action\", action)\n  if (target) element.setAttribute(\"target\", target)\n  if (templateElement) element.appendChild(templateElement)\n  Object.entries(attributes).forEach((attribute) => element.setAttribute(...attribute))\n  return element\n}\n\nfunction createTemplateElement(html) {\n  const element = document.createElement(\"template\")\n  element.innerHTML = html\n  return element\n}\n\nexport class StreamElementTests extends DOMTestCase {}\n\nlet subject\n\nsetup(() => {\n  subject = new StreamElementTests()\n  subject.setup()\n  subject.fixtureHTML = `<div><div id=\"hello\">Hello Turbo</div></div>`\n})\n\ntest(\"action=append\", async () => {\n  const element = createStreamElement(\"append\", \"hello\", createTemplateElement(\"<span> Streams</span>\"))\n  const element2 = createStreamElement(\"append\", \"hello\", createTemplateElement(\"<span> and more</span>\"))\n\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Hello Turbo\")\n\n  subject.append(element)\n  await nextAnimationFrame()\n\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Hello Turbo Streams\")\n  assert.isNull(element.parentElement)\n\n  subject.append(element2)\n  await nextAnimationFrame()\n\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Hello Turbo Streams and more\")\n  assert.isNull(element2.parentElement)\n})\n\ntest(\"action=append with a form template containing an input named id\", async () => {\n  const element = createStreamElement(\n    \"append\",\n    \"hello\",\n    createTemplateElement(' <form id=\"child_1\"><input type=\"hidden\" name=\"id\" value=\"First\"></form> tail1 ')\n  )\n  const element2 = createStreamElement(\n    \"append\",\n    \"hello\",\n    createTemplateElement(\n      '<form id=\"child_1\"><input type=\"hidden\" name=\"id\" value=\"New First\"></form> <form id=\"child_2\"><input type=\"hidden\" name=\"id\" value=\"Second\"></form> tail2 '\n    )\n  )\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Hello Turbo\")\n\n  subject.append(element)\n  await nextAnimationFrame()\n\n  assert.equal(subject.find(\"#hello\")?.innerHTML, 'Hello Turbo <form id=\"child_1\"><input type=\"hidden\" name=\"id\" value=\"First\"></form> tail1 ')\n  assert.isNull(element.parentElement)\n\n  subject.append(element2)\n  await nextAnimationFrame()\n\n  assert.equal(subject.find(\"#hello\")?.innerHTML, 'Hello Turbo  tail1 <form id=\"child_1\"><input type=\"hidden\" name=\"id\" value=\"New First\"></form> <form id=\"child_2\"><input type=\"hidden\" name=\"id\" value=\"Second\"></form> tail2 ')\n})\n\ntest(\"action=append with children ID already present in target\", async () => {\n  const element = createStreamElement(\"append\", \"hello\", createTemplateElement(' <div id=\"child_1\">First</div> tail1 '))\n  const element2 = createStreamElement(\n    \"append\",\n    \"hello\",\n    createTemplateElement('<div id=\"child_1\">New First</div> <div id=\"child_2\">Second</div> tail2 ')\n  )\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Hello Turbo\")\n\n  subject.append(element)\n  await nextAnimationFrame()\n\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Hello Turbo First tail1 \")\n  assert.isNull(element.parentElement)\n\n  subject.append(element2)\n  await nextAnimationFrame()\n\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Hello Turbo  tail1 New First Second tail2 \")\n})\n\ntest(\"action=prepend\", async () => {\n  const element = createStreamElement(\"prepend\", \"hello\", createTemplateElement(\"<span>Streams </span>\"))\n  const element2 = createStreamElement(\"prepend\", \"hello\", createTemplateElement(\"<span>and more </span>\"))\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Hello Turbo\")\n\n  subject.append(element)\n  await nextAnimationFrame()\n\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Streams Hello Turbo\")\n  assert.isNull(element.parentElement)\n\n  subject.append(element2)\n  await nextAnimationFrame()\n\n  assert.equal(subject.find(\"#hello\")?.textContent, \"and more Streams Hello Turbo\")\n  assert.isNull(element.parentElement)\n})\n\ntest(\"action=prepend with a form template containing an input named id\", async () => {\n  const element = createStreamElement(\"prepend\", \"hello\", createTemplateElement('<form id=\"child_1\"><input type=\"hidden\" name=\"id\" value=\"First\"></form> tail1 '))\n  const element2 = createStreamElement(\n    \"prepend\",\n    \"hello\",\n    createTemplateElement(\n      '<form id=\"child_1\"><input type=\"hidden\" name=\"id\" value=\"New First\"></form> <form id=\"child_2\"><input type=\"hidden\" name=\"id\" value=\"Second\"></form> tail2 '\n    )\n  )\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Hello Turbo\")\n\n  subject.append(element)\n  await nextAnimationFrame()\n\n  assert.equal(subject.find(\"#hello\")?.innerHTML, '<form id=\"child_1\"><input type=\"hidden\" name=\"id\" value=\"First\"></form> tail1 Hello Turbo')\n  assert.isNull(element.parentElement)\n\n  subject.append(element2)\n  await nextAnimationFrame()\n\n  assert.equal(subject.find(\"#hello\")?.innerHTML, '<form id=\"child_1\"><input type=\"hidden\" name=\"id\" value=\"New First\"></form> <form id=\"child_2\"><input type=\"hidden\" name=\"id\" value=\"Second\"></form> tail2  tail1 Hello Turbo')\n})\n\ntest(\"action=prepend with children ID already present in target\", async () => {\n  const element = createStreamElement(\"prepend\", \"hello\", createTemplateElement('<div id=\"child_1\">First</div> tail1 '))\n  const element2 = createStreamElement(\n    \"prepend\",\n    \"hello\",\n    createTemplateElement('<div id=\"child_1\">New First</div> <div id=\"child_2\">Second</div> tail2 ')\n  )\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Hello Turbo\")\n\n  subject.append(element)\n  await nextAnimationFrame()\n\n  assert.equal(subject.find(\"#hello\")?.textContent, \"First tail1 Hello Turbo\")\n  assert.isNull(element.parentElement)\n\n  subject.append(element2)\n  await nextAnimationFrame()\n\n  assert.equal(subject.find(\"#hello\")?.textContent, \"New First Second tail2  tail1 Hello Turbo\")\n})\n\ntest(\"action=remove\", async () => {\n  const element = createStreamElement(\"remove\", \"hello\")\n  assert.ok(subject.find(\"#hello\"))\n\n  subject.append(element)\n  await nextAnimationFrame()\n\n  assert.notOk(subject.find(\"#hello\"))\n  assert.isNull(element.parentElement)\n})\n\ntest(\"action=replace\", async () => {\n  const element = createStreamElement(\"replace\", \"hello\", createTemplateElement(`<h1 id=\"hello\">Hello Turbo</h1>`))\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Hello Turbo\")\n  assert.ok(subject.find(\"div#hello\"))\n\n  subject.append(element)\n  await nextAnimationFrame()\n\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Hello Turbo\")\n  assert.notOk(subject.find(\"div#hello\"))\n  assert.ok(subject.find(\"h1#hello\"))\n  assert.isNull(element.parentElement)\n})\n\ntest(\"action=update\", async () => {\n  const element = createStreamElement(\"update\", \"hello\", createTemplateElement(\"Goodbye Turbo\"))\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Hello Turbo\")\n\n  subject.append(element)\n  await nextAnimationFrame()\n\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Goodbye Turbo\")\n  assert.isNull(element.parentElement)\n})\n\ntest(\"action=after\", async () => {\n  const element = createStreamElement(\"after\", \"hello\", createTemplateElement(`<h1 id=\"after\">After Turbo</h1>`))\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Hello Turbo\")\n\n  subject.append(element)\n  await nextAnimationFrame()\n\n  assert.equal(subject.find(\"#hello\")?.nextSibling?.textContent, \"After Turbo\")\n  assert.ok(subject.find(\"div#hello\"))\n  assert.ok(subject.find(\"h1#after\"))\n  assert.isNull(element.parentElement)\n})\n\n\ntest(\"action=after with children ID already present in target\", async () => {\n  subject.fixtureHTML = `<div id=\"hello\"><div id=\"top\">Top</div><div id=\"middle\">Middle</div><div id=\"bottom\">Bottom</div></div>`\n  const element = createStreamElement(\"after\", \"top\", createTemplateElement(' <div id=\"middle\">New Middle</div> tail1 '))\n\n  subject.append(element)\n  await nextAnimationFrame()\n\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Top New Middle tail1 Bottom\")\n})\n\ntest(\"action=before\", async () => {\n  const element = createStreamElement(\"before\", \"hello\", createTemplateElement(`<h1 id=\"before\">Before Turbo</h1>`))\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Hello Turbo\")\n\n  subject.append(element)\n  await nextAnimationFrame()\n\n  assert.equal(subject.find(\"#hello\")?.previousSibling?.textContent, \"Before Turbo\")\n  assert.ok(subject.find(\"div#hello\"))\n  assert.ok(subject.find(\"h1#before\"))\n  assert.isNull(element.parentElement)\n})\n\n\ntest(\"action=before with children ID already present in target\", async () => {\n  subject.fixtureHTML = `<div id=\"hello\"><div id=\"top\">Top</div><div id=\"middle\">Middle</div><div id=\"bottom\">Bottom</div></div>`\n  const element = createStreamElement(\"before\", \"bottom\", createTemplateElement(' <div id=\"middle\">New Middle</div> tail1 '))\n\n  subject.append(element)\n  await nextAnimationFrame()\n\n  assert.equal(subject.find(\"#hello\")?.textContent, \"Top New Middle tail1 Bottom\")\n})\n\ntest(\"test action=refresh\", async () => {\n  document.body.setAttribute(\"data-modified\", \"\")\n  assert.ok(document.body.hasAttribute(\"data-modified\"))\n\n  const element = createStreamElement(\"refresh\")\n  subject.append(element)\n\n  await sleep(250)\n\n  assert.notOk(document.body.hasAttribute(\"data-modified\"))\n})\n\ntest(\"test action=refresh discarded when matching request id\", async () => {\n  Turbo.session.recentRequests.add(\"123\")\n\n  document.body.setAttribute(\"data-modified\", \"\")\n  assert.ok(document.body.hasAttribute(\"data-modified\"))\n\n  const element = createStreamElement(\"refresh\")\n  element.setAttribute(\"request-id\", \"123\")\n  subject.append(element)\n\n  await sleep(250)\n\n  assert.ok(document.body.hasAttribute(\"data-modified\"))\n})\n\ntest(\"action=replace method=morph\", async () => {\n  const templateElement = createTemplateElement(`<h1 id=\"hello\">Hello Turbo Morphed</h1>`)\n  const element = createStreamElement(\"replace\", \"hello\", templateElement, { method: \"morph\" })\n\n  assert.equal(subject.find(\"div#hello\")?.textContent, \"Hello Turbo\")\n\n  subject.append(element)\n  await nextAnimationFrame()\n\n  assert.notOk(subject.find(\"div#hello\"))\n  assert.equal(subject.find(\"h1#hello\")?.textContent, \"Hello Turbo Morphed\")\n})\n\ntest(\"action=replace method=morph with text content change\", async () => {\n  const templateElement = createTemplateElement(`<div id=\"hello\">Hello Turbo Morphed</div>`)\n  const element = createStreamElement(\"replace\", \"hello\", templateElement, { method: \"morph\" })\n\n  assert.equal(subject.find(\"div#hello\")?.textContent, \"Hello Turbo\")\n\n  subject.append(element)\n  await nextAnimationFrame()\n\n  assert.ok(subject.find(\"div#hello\"))\n  assert.equal(subject.find(\"div#hello\")?.textContent, \"Hello Turbo Morphed\")\n})\n\ntest(\"action=update method=morph\", async () => {\n  const templateElement = createTemplateElement(`<h1 id=\"hello-child-element\">Hello Turbo Morphed</h1>`)\n  const element = createStreamElement(\"update\", \"hello\", templateElement, { method: \"morph\" })\n  const target = subject.find(\"div#hello\")\n  assert.equal(target?.textContent, \"Hello Turbo\")\n  element.setAttribute(\"children-only\", true)\n\n  subject.append(element)\n\n  await nextAnimationFrame()\n\n  assert.ok(subject.find(\"div#hello\"))\n  assert.ok(subject.find(\"div#hello > h1#hello-child-element\"))\n  assert.equal(subject.find(\"div#hello > h1#hello-child-element\").textContent, \"Hello Turbo Morphed\")\n})\n"
  },
  {
    "path": "src/util.js",
    "content": "export function activateScriptElement(element) {\n  if (element.getAttribute(\"data-turbo-eval\") == \"false\") {\n    return element\n  } else {\n    const createdScriptElement = document.createElement(\"script\")\n    const cspNonce = getCspNonce()\n    if (cspNonce) {\n      createdScriptElement.nonce = cspNonce\n    }\n    createdScriptElement.textContent = element.textContent\n    createdScriptElement.async = false\n    copyElementAttributes(createdScriptElement, element)\n    return createdScriptElement\n  }\n}\n\nfunction copyElementAttributes(destinationElement, sourceElement) {\n  for (const { name, value } of sourceElement.attributes) {\n    destinationElement.setAttribute(name, value)\n  }\n}\n\nexport function createDocumentFragment(html) {\n  const template = document.createElement(\"template\")\n  template.innerHTML = html\n  return template.content\n}\n\nexport function dispatch(eventName, { target, cancelable, detail } = {}) {\n  const event = new CustomEvent(eventName, {\n    cancelable,\n    bubbles: true,\n    composed: true,\n    detail\n  })\n\n  if (target && target.isConnected) {\n    target.dispatchEvent(event)\n  } else {\n    document.documentElement.dispatchEvent(event)\n  }\n\n  return event\n}\n\nexport function cancelEvent(event) {\n  event.preventDefault()\n  event.stopImmediatePropagation()\n}\n\nexport function nextRepaint() {\n  if (document.visibilityState === \"hidden\") {\n    return nextEventLoopTick()\n  } else {\n    return nextAnimationFrame()\n  }\n}\n\nexport function nextAnimationFrame() {\n  return new Promise((resolve) => requestAnimationFrame(() => resolve()))\n}\n\nexport function nextEventLoopTick() {\n  return new Promise((resolve) => setTimeout(() => resolve(), 0))\n}\n\nexport function nextMicrotask() {\n  return Promise.resolve()\n}\n\nexport function parseHTMLDocument(html = \"\") {\n  return new DOMParser().parseFromString(html, \"text/html\")\n}\n\nexport function unindent(strings, ...values) {\n  const lines = interpolate(strings, values).replace(/^\\n/, \"\").split(\"\\n\")\n  const match = lines[0].match(/^\\s+/)\n  const indent = match ? match[0].length : 0\n  return lines.map((line) => line.slice(indent)).join(\"\\n\")\n}\n\nfunction interpolate(strings, values) {\n  return strings.reduce((result, string, i) => {\n    const value = values[i] == undefined ? \"\" : values[i]\n    return result + string + value\n  }, \"\")\n}\n\nexport function uuid() {\n  return Array.from({ length: 36 })\n    .map((_, i) => {\n      if (i == 8 || i == 13 || i == 18 || i == 23) {\n        return \"-\"\n      } else if (i == 14) {\n        return \"4\"\n      } else if (i == 19) {\n        return (Math.floor(Math.random() * 4) + 8).toString(16)\n      } else {\n        return Math.floor(Math.random() * 16).toString(16)\n      }\n    })\n    .join(\"\")\n}\n\nexport function getAttribute(attributeName, ...elements) {\n  for (const value of elements.map((element) => element?.getAttribute(attributeName))) {\n    if (typeof value == \"string\") return value\n  }\n\n  return null\n}\n\nexport function hasAttribute(attributeName, ...elements) {\n  return elements.some((element) => element && element.hasAttribute(attributeName))\n}\n\nexport function markAsBusy(...elements) {\n  for (const element of elements) {\n    if (element.localName == \"turbo-frame\") {\n      element.setAttribute(\"busy\", \"\")\n    }\n    element.setAttribute(\"aria-busy\", \"true\")\n  }\n}\n\nexport function clearBusyState(...elements) {\n  for (const element of elements) {\n    if (element.localName == \"turbo-frame\") {\n      element.removeAttribute(\"busy\")\n    }\n\n    element.removeAttribute(\"aria-busy\")\n  }\n}\n\nexport function waitForLoad(element, timeoutInMilliseconds = 2000) {\n  return new Promise((resolve) => {\n    const onComplete = () => {\n      element.removeEventListener(\"error\", onComplete)\n      element.removeEventListener(\"load\", onComplete)\n      resolve()\n    }\n\n    element.addEventListener(\"load\", onComplete, { once: true })\n    element.addEventListener(\"error\", onComplete, { once: true })\n    setTimeout(resolve, timeoutInMilliseconds)\n  })\n}\n\nexport function getHistoryMethodForAction(action) {\n  switch (action) {\n    case \"replace\":\n      return history.replaceState\n    case \"advance\":\n    case \"restore\":\n      return history.pushState\n  }\n}\n\nexport function isAction(action) {\n  return action == \"advance\" || action == \"replace\" || action == \"restore\"\n}\n\nexport function getVisitAction(...elements) {\n  const action = getAttribute(\"data-turbo-action\", ...elements)\n\n  return isAction(action) ? action : null\n}\n\nfunction getMetaElement(name) {\n  return document.querySelector(`meta[name=\"${name}\"]`)\n}\n\nexport function getMetaContent(name) {\n  const element = getMetaElement(name)\n  return element && element.content\n}\n\nexport function getCspNonce() {\n  const element = getMetaElement(\"csp-nonce\")\n\n  if (element) {\n    const { nonce, content } = element\n    return nonce == \"\" ? content : nonce\n  }\n}\n\nexport function setMetaContent(name, content) {\n  let element = getMetaElement(name)\n\n  if (!element) {\n    element = document.createElement(\"meta\")\n    element.setAttribute(\"name\", name)\n\n    document.head.appendChild(element)\n  }\n\n  element.setAttribute(\"content\", content)\n\n  return element\n}\n\nexport function findClosestRecursively(element, selector) {\n  if (element instanceof Element) {\n    return (\n      element.closest(selector) || findClosestRecursively(element.assignedSlot || element.getRootNode()?.host, selector)\n    )\n  }\n}\n\nexport function elementIsStylesheet(element) {\n  return element.localName === \"style\" ||\n    (element.localName === \"link\" && element.relList.contains(\"stylesheet\"))\n}\n\nexport function elementIsFocusable(element) {\n  const inertDisabledOrHidden = \"[inert], :disabled, [hidden], details:not([open]), dialog:not([open])\"\n\n  return !!element && element.closest(inertDisabledOrHidden) == null && typeof element.focus == \"function\"\n}\n\nexport function queryAutofocusableElement(elementOrDocumentFragment) {\n  return Array.from(elementOrDocumentFragment.querySelectorAll(\"[autofocus]\")).find(elementIsFocusable)\n}\n\nexport async function around(callback, reader) {\n  const before = reader()\n\n  callback()\n\n  await nextAnimationFrame()\n\n  const after = reader()\n\n  return [before, after]\n}\n\nexport function doesNotTargetIFrame(name) {\n  if (name === \"_blank\") {\n    return false\n  } else if (name) {\n    for (const element of document.getElementsByName(name)) {\n      if (element instanceof HTMLIFrameElement) return false\n    }\n\n    return true\n  } else {\n    return true\n  }\n}\n\nexport function findLinkFromClickTarget(target) {\n  const link = findClosestRecursively(target, \"a[href], a[xlink\\\\:href]\")\n\n  if (!link) return null\n  if (link.href.startsWith(\"#\")) return null\n  if (link.hasAttribute(\"download\")) return null\n\n  const linkTarget = link.getAttribute(\"target\")\n  if (linkTarget && linkTarget !== \"_self\") return null\n\n  return link\n}\n\nexport function debounce(fn, delay) {\n  let timeoutId = null\n\n  return (...args) => {\n    const callback = () => fn.apply(this, args)\n    clearTimeout(timeoutId)\n    timeoutId = setTimeout(callback, delay)\n  }\n}\n"
  },
  {
    "path": "web-test-runner.config.mjs",
    "content": "import { esbuildPlugin } from '@web/dev-server-esbuild'\nimport { playwrightLauncher } from '@web/test-runner-playwright'\n\n/** @type {import(\"@web/test-runner\").TestRunnerConfig} */\nexport default {\n  browsers: [\n    playwrightLauncher({\n      product: 'chromium',\n      launchOptions: {\n        timeout: 60000\n      }\n    }),\n    playwrightLauncher({\n      product: 'firefox',\n      launchOptions: {\n        timeout: 60000\n      }\n    }),\n    playwrightLauncher({\n      product: 'webkit',\n      launchOptions: {\n        timeout: 60000\n      }\n    })\n  ],\n  browserStartTimeout: 600000,\n  nodeResolve: true,\n  files: \"./src/tests/unit/**/*_tests.js\",\n  testFramework: {\n    config: {\n      ui: \"tdd\"\n    }\n  },\n  plugins: [\n    esbuildPlugin({ ts: true, target: \"es2020\" })\n  ]\n}"
  }
]