[
  {
    "path": ".formatter.exs",
    "content": "[\n  import_deps: [:phoenix],\n  plugins: [Phoenix.LiveView.HTMLFormatter],\n  inputs: [\"*.{ex,exs}\", \"{config,lib,test}/**/*.{ex,exs}\"],\n  tag_formatters: %{script: Prettier}\n]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n### Environment\n\n* Elixir version (please paste the output of `elixir -v`):\n```\n\n```\n* Phoenix and LiveView versions (`mix deps | grep -w 'phoenix\\|phoenix_live_view'`):\n```\n\n```\n* Operating system:\n- [ ] Windows\n- [ ] MacOS\n- [ ] Linux\n- [ ] Other (please specify): \n* Browsers (including version) you attempted to reproduce this bug on (the more the merrier):\n```\n\n```\n\n### Actual behavior\n\n<!--\nDescribe the actual behaviour. If you are seeing an error, include the full message and stacktrace. \n\nAlso please consider providing a single file app that reproduces the behaviour, you can start here:\nhttps://github.com/phoenixframework/phoenix_live_view/blob/main/.github/single-file-samples/main.exs\n\nFor more examples, you can have a look at the directory:\nhttps://github.com/phoenixframework/phoenix_live_view/blob/main/.github/single-file-samples/\n-->\n\n### Expected behavior\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "---\nblank_issues_enabled: true\n\ncontact_links:\n  - name: Ask questions, support, and general discussions\n    url: https://elixirforum.com/c/phoenix-forum\n    about: Ask questions, provide support, and more on Elixir Forum\n\n  - name: Propose new features\n    url: https://elixirforum.com/c/phoenix-forum\n    about: Propose new features on Elixir Forum\n"
  },
  {
    "path": ".github/extract-changelog.sh",
    "content": "#!/usr/bin/env bash\n#\n# Extract changelog for a specific version from CHANGELOG.md\n#\n# Usage: ./extract-changelog.sh <version>\n# Example: ./extract-changelog.sh v1.1.22\n\nset -euo pipefail\n\nVERSION=\"${1:-}\"\n\nif [[ -z \"$VERSION\" ]]; then\n    echo \"Usage: $0 <version>\" >&2\n    echo \"Example: $0 v1.1.22\" >&2\n    exit 1\nfi\n\n# Normalize version to include 'v' prefix\nVERSION=\"${VERSION#v}\"  # Remove 'v' if present\nVERSION=\"v${VERSION}\"   # Add 'v' prefix\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nCHANGELOG_PATH=\"${SCRIPT_DIR}/../CHANGELOG.md\"\n\nif [[ ! -f \"$CHANGELOG_PATH\" ]]; then\n    echo \"Error: CHANGELOG.md not found at $CHANGELOG_PATH\" >&2\n    exit 1\nfi\n\n# Extract the section for the specified version\n# Match from \"## vX.Y.Z\" until the next \"## v\" header\nawk -v version=\"$VERSION\" '\n    BEGIN { found = 0; printing = 0 }\n\n    # Match the start of our target version section\n    /^## v[0-9]/ {\n        if (printing) {\n            # We hit the next version, stop printing\n            exit\n        }\n        # Check if this line contains our version\n        if (index($0, \"## \" version \" \") > 0) {\n            found = 1\n            printing = 1\n            next  # Skip the version header line\n        }\n    }\n\n    printing { print }\n\n    END {\n        if (!found) {\n            print \"Error: Version \" version \" not found in changelog\" > \"/dev/stderr\"\n            exit 1\n        }\n    }\n' \"$CHANGELOG_PATH\"\n"
  },
  {
    "path": ".github/single-file-samples/main.exs",
    "content": "Application.put_env(:sample, Example.Endpoint,\n  http: [ip: {127, 0, 0, 1}, port: 5001],\n  adapter: Bandit.PhoenixAdapter,\n  server: true,\n  live_view: [signing_salt: \"aaaaaaaa\"],\n  secret_key_base: String.duplicate(\"a\", 64)\n)\n\nMix.install(\n  [\n    {:bandit, \"~> 1.8\"},\n    {:jason, \"~> 1.0\"},\n    {:phoenix, \"~> 1.8\"},\n    {:phoenix_html, \"~> 4.0\"},\n    # please test your issue using the latest version of LV from GitHub!\n    {:phoenix_live_view,\n     github: \"phoenixframework/phoenix_live_view\", branch: \"main\", override: true}\n  ]\n)\n\n# if you're trying to test a specific LV commit, it may be necessary to manually build\n# the JS assets. To do this, uncomment the following lines:\n# this needs mix and npm available in your path!\n#\n# path = Phoenix.LiveView.__info__(:compile)[:source] |> Path.dirname() |> Path.join(\"../\")\n# System.cmd(\"mix\", [\"deps.get\"], cd: path, into: IO.binstream())\n# System.cmd(\"npm\", [\"install\"], cd: Path.join(path, \"./assets\"), into: IO.binstream())\n# System.cmd(\"mix\", [\"assets.build\"], cd: path, into: IO.binstream())\n\ndefmodule Example.ErrorView do\n  def render(template, _), do: Phoenix.Controller.status_message_from_template(template)\nend\n\ndefmodule Example.HomeLive do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, :count, 0)}\n  end\n\n  def render(\"live.html\", assigns) do\n    ~H\"\"\"\n    <script src=\"/assets/phoenix/phoenix.js\">\n    </script>\n    <script src=\"/assets/phoenix_live_view/phoenix_live_view.js\">\n    </script>\n    <script src=\"/assets/phoenix_html/phoenix_html.js\">\n    </script>\n    <%!-- uncomment to use enable tailwind --%>\n    <%!-- <script src=\"https://cdn.tailwindcss.com\"></script> --%>\n    <script>\n      let liveSocket = new window.LiveView.LiveSocket(\"/live\", window.Phoenix.Socket)\n      liveSocket.connect()\n    </script>\n    <style>\n      * { font-size: 1.1em; }\n    </style>\n    {@inner_content}\n    \"\"\"\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    {@count}\n    <button phx-click=\"inc\">+</button>\n    <button phx-click=\"dec\">-</button>\n    \"\"\"\n  end\n\n  def handle_event(\"inc\", _params, socket) do\n    {:noreply, assign(socket, :count, socket.assigns.count + 1)}\n  end\n\n  def handle_event(\"dec\", _params, socket) do\n    {:noreply, assign(socket, :count, socket.assigns.count - 1)}\n  end\nend\n\ndefmodule Example.Router do\n  use Phoenix.Router\n  import Phoenix.LiveView.Router\n\n  pipeline :browser do\n    plug(:accepts, [\"html\"])\n  end\n\n  scope \"/\", Example do\n    pipe_through(:browser)\n\n    live(\"/\", HomeLive, :index)\n  end\nend\n\ndefmodule Example.Endpoint do\n  use Phoenix.Endpoint, otp_app: :sample\n  socket(\"/live\", Phoenix.LiveView.Socket)\n\n  plug Plug.Static, from: {:phoenix, \"priv/static\"}, at: \"/assets/phoenix\"\n  plug Plug.Static, from: {:phoenix_live_view, \"priv/static\"}, at: \"/assets/phoenix_live_view\"\n  plug Plug.Static, from: {:phoenix_html, \"priv/static\"}, at: \"/assets/phoenix_html\"\n\n  plug(Example.Router)\nend\n\n{:ok, _} = Supervisor.start_link([Example.Endpoint], strategy: :one_for_one)\nProcess.sleep(:infinity)\n"
  },
  {
    "path": ".github/single-file-samples/test.exs",
    "content": "Application.put_env(:phoenix, Example.Endpoint,\n  http: [ip: {127, 0, 0, 1}, port: 5001],\n  adapter: Bandit.PhoenixAdapter,\n  server: true,\n  live_view: [signing_salt: \"aaaaaaaa\"],\n  secret_key_base: String.duplicate(\"a\", 64)\n)\n\nMix.install(\n  [\n    {:bandit, \"~> 1.8\"},\n    {:jason, \"~> 1.0\"},\n    {:phoenix, \"~> 1.8\"},\n    # please test your issue using the latest version of LV from GitHub!\n    {:phoenix_live_view,\n     github: \"phoenixframework/phoenix_live_view\", branch: \"main\", override: true},\n    {:lazy_html, \">= 0.1.0\"}\n  ]\n)\n\nExUnit.start()\n\ndefmodule Example.ErrorView do\n  def render(template, _), do: Phoenix.Controller.status_message_from_template(template)\nend\n\ndefmodule Example.HomeLive do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  def mount(_params, _session, socket) do\n    socket\n    |> then(&{:ok, &1})\n  end\n\n  def render(\"live.html\", assigns) do\n    ~H\"\"\"\n    <script src=\"/assets/phoenix/phoenix.js\">\n    </script>\n    <script src=\"/assets/phoenix_live_view/phoenix_live_view.js\">\n    </script>\n    <script src=\"/assets/phoenix_html/phoenix_html.js\">\n    </script>\n    <script>\n      let liveSocket = new window.LiveView.LiveSocket(\"/live\", window.Phoenix.Socket)\n      liveSocket.connect()\n    </script>\n    <style>\n      * { font-size: 1.1em; }\n    </style>\n    {@inner_content}\n    \"\"\"\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <p>The LiveView content goes here</p>\n    \"\"\"\n  end\nend\n\ndefmodule Example.Router do\n  use Phoenix.Router\n  import Phoenix.LiveView.Router\n\n  pipeline :browser do\n    plug(:accepts, [\"html\"])\n  end\n\n  scope \"/\", Example do\n    pipe_through(:browser)\n\n    live(\"/\", HomeLive, :index)\n  end\nend\n\ndefmodule Example.Endpoint do\n  use Phoenix.Endpoint, otp_app: :phoenix\n  socket(\"/live\", Phoenix.LiveView.Socket)\n  plug Plug.Static, from: {:phoenix, \"priv/static\"}, at: \"/assets/phoenix\"\n  plug Plug.Static, from: {:phoenix_live_view, \"priv/static\"}, at: \"/assets/phoenix_live_view\"\n  plug Plug.Static, from: {:phoenix_html, \"priv/static\"}, at: \"/assets/phoenix_html\"\n  plug(Example.Router)\nend\n\ndefmodule Example.HomeLiveTest do\n  use ExUnit.Case\n\n  import Phoenix.ConnTest\n  import Plug.Conn\n  import Phoenix.LiveViewTest\n\n  @endpoint Example.Endpoint\n\n  test \"works properly\" do\n    conn = Phoenix.ConnTest.build_conn()\n\n    {:ok, _view, html} = live(conn, \"/\")\n\n    assert html =~ \"The LiveView content goes here\"\n  end\nend\n\n{:ok, _} = Supervisor.start_link([Example.Endpoint], strategy: :one_for_one)\nExUnit.run()\nProcess.sleep(:infinity)\n"
  },
  {
    "path": ".github/workflows/assets.yml",
    "content": "name: Assets\n\non:\n  push:\n    branches:\n      - main\n      - \"v*.*\"\n\njobs:\n  build:\n    runs-on: ubuntu-22.04\n    env:\n      elixir: 1.18.1\n      otp: 27.2\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Set up Elixir\n        uses: erlef/setup-beam@v1\n        with:\n          elixir-version: ${{ env.elixir }}\n          otp-version: ${{ env.otp }}\n\n      - name: Restore deps and _build cache\n        uses: actions/cache@v4\n        with:\n          path: |\n            deps\n            _build\n          key: ${{ runner.os }}-mix-${{ env.elixir }}-${{ env.otp }}-${{ hashFiles('**/mix.lock') }}-dev\n          restore-keys: |\n            ${{ runner.os }}-mix-${{ env.elixir }}-${{ env.otp }}-\n      - name: Install Dependencies\n        run: mix deps.get --only dev\n\n      - name: Set up Node.js 20.x\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20.x\n\n      - name: Restore npm cache\n        uses: actions/cache@v4\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}\n          restore-keys: |\n            ${{ runner.os }}-node-\n\n      - name: Install npm dependencies\n        run: npm ci\n\n      - name: Build assets\n        run: mix assets.build\n\n      - name: Push updated assets\n        id: push_assets\n        uses: stefanzweifel/git-auto-commit-action@v5\n        with:\n          commit_message: Update assets\n          file_pattern: priv/static\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n  pull_request:\n    branches:\n      - main\n\njobs:\n  mix_test:\n    name: mix test (OTP ${{matrix.otp}} | Elixir ${{matrix.elixir}})\n\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - elixir: 1.15.4\n            otp: 25.3\n\n          - elixir: 1.16.3\n            otp: 26.2\n\n          - elixir: 1.18.4\n            otp: 27.3\n\n          # update coverage report as well\n          - elixir: 1.19.1\n            otp: 28.1\n            lint: lint\n\n          # run against latest Elixir to catch warnings early\n          - elixir: main-otp-28\n            otp: maint-28\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Install inotify-tools\n        run: |\n          sudo apt update\n          sudo apt install -y inotify-tools\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Set up Elixir\n        uses: erlef/setup-beam@v1\n        with:\n          elixir-version: ${{ matrix.elixir }}\n          otp-version: ${{ matrix.otp }}\n\n      - name: Restore deps and _build cache\n        uses: actions/cache@v4\n        with:\n          path: |\n            deps\n            _build\n          key: deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}\n          restore-keys: |\n            deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}\n\n      - name: Install dependencies\n        run: mix deps.get\n\n      - name: Remove compiled application files\n        run: mix clean\n\n      - name: Compile dependencies\n        run: mix compile\n        if: ${{ !matrix.lint }}\n        env:\n          MIX_ENV: test\n\n      - name: Compile & lint dependencies\n        run: mix compile --warnings-as-errors\n        if: ${{ matrix.lint }} or ${{ matrix.elixir == 'main' }}\n        env:\n          MIX_ENV: test\n\n      - name: Compile without optional deps\n        run: mix compile --no-optional-deps\n        env:\n          MIX_ENV: dev\n\n      - name: Check if formatted\n        run: mix format --check-formatted\n        if: ${{ matrix.lint }}\n        env:\n          MIX_ENV: test\n\n      - name: Run tests\n        run: mix test --cover --export-coverage default --warnings-as-errors\n\n      - uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: mix-test-coverage-${{ matrix.otp }}-${{ matrix.elixir }}\n          path: cover/default.coverdata\n          retention-days: 7\n\n  npm_test:\n    name: npm test\n\n    strategy:\n      matrix:\n        include:\n          - elixir: 1.19.1\n            otp: 28.1\n\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Set up Elixir\n        uses: erlef/setup-beam@v1\n        with:\n          elixir-version: ${{ matrix.elixir }}\n          otp-version: ${{ matrix.otp }}\n\n      - name: Restore deps and _build cache\n        uses: actions/cache@v4\n        with:\n          path: |\n            deps\n            _build\n          key: deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}\n          restore-keys: |\n            deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}\n\n      - name: Install dependencies\n        run: mix deps.get --only test\n\n      - name: Set up Node.js 20.x\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20.x\n\n      - name: Restore npm cache\n        uses: actions/cache@v4\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}\n          restore-keys: |\n            ${{ runner.os }}-node-\n\n      - name: setup JS\n        run: npm run setup\n\n      - name: typecheck\n        run: npm run build && npm run typecheck:tests\n\n      - name: check lint and format\n        run: npm run js:lint && npm run js:format.check\n\n      - name: test\n        run: npm run js:test\n\n      - uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: js-unit-coverage\n          path: coverage/\n          retention-days: 7\n\n  e2e_test:\n    name: e2e test\n\n    strategy:\n      matrix:\n        include:\n          - elixir: 1.19.1\n            otp: 28.1\n\n    runs-on: ubuntu-latest\n    container:\n      image: mcr.microsoft.com/playwright:v1.58.2-noble\n    env:\n      ImageOS: ubuntu22\n      HOME: /root\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: install unzip\n        run: apt update && apt -y install unzip\n\n      - name: Set up Elixir\n        uses: erlef/setup-beam@v1\n        with:\n          elixir-version: ${{ matrix.elixir }}\n          otp-version: ${{ matrix.otp }}\n\n      - name: Restore deps and _build cache\n        uses: actions/cache@v4\n        with:\n          path: |\n            deps\n            _build\n          key: deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}\n          restore-keys: |\n            deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}\n\n      - name: Install dependencies\n        run: mix deps.get\n\n      - name: Restore npm cache\n        uses: actions/cache@v4\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}\n          restore-keys: |\n            ${{ runner.os }}-node-\n\n      - name: setup\n        run: npm run setup\n\n      - name: Run e2e tests\n        run: npm run e2e:test\n\n      - uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: playwright-report\n          path: playwright-report/\n          retention-days: 7\n\n      - uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: e2e-test-results\n          path: test/e2e/test-results/\n          retention-days: 7\n\n      - uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: mix-e2e-coverage\n          path: cover/e2e.coverdata\n          retention-days: 7\n\n  coverage_report:\n    name: coverage report\n\n    runs-on: ubuntu-latest\n    needs: [mix_test, npm_test, e2e_test]\n\n    strategy:\n      matrix:\n        include:\n          - elixir: 1.19.1\n            otp: 28.1\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Set up Elixir\n        uses: erlef/setup-beam@v1\n        with:\n          elixir-version: ${{ matrix.elixir }}\n          otp-version: ${{ matrix.otp }}\n\n      - name: Download mix unit coverage\n        uses: actions/download-artifact@v4\n        with:\n          # This needs to be updated when changing the test matrix\n          name: mix-test-coverage-${{ matrix.otp }}-${{ matrix.elixir }}\n          path: cover/\n\n      - name: Download mix e2e coverage\n        uses: actions/download-artifact@v4\n        with:\n          name: mix-e2e-coverage\n          path: cover/\n\n      - name: Restore deps and _build cache\n        uses: actions/cache@v4\n        with:\n          path: |\n            deps\n            _build\n          key: deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}\n          restore-keys: |\n            deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}\n\n      - name: Generate mix coverage report\n        run: mix test.coverage\n\n      - name: Download js-unit-coverage artifact\n        uses: actions/download-artifact@v4\n        with:\n          name: js-unit-coverage\n          path: coverage/\n\n      - name: Download e2e-test-results artifact\n        uses: actions/download-artifact@v4\n        with:\n          name: e2e-test-results\n          path: test/e2e/test-results/\n\n      - name: Set up Node.js 20.x\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20.x\n\n      - name: Merge coverage reports\n        run: npm install && npm run cover:merge\n\n      - name: Upload coverage report\n        uses: actions/upload-artifact@v4\n        with:\n          name: overall-coverage\n          path: cover/\n          retention-days: 7\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "content": "name: Docs\n\non:\n  push:\n    branches:\n      - main\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\njobs:\n  build:\n    runs-on: ubuntu-22.04\n    env:\n      elixir: 1.18.1\n      otp: 27.2\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Set up Elixir\n        uses: erlef/setup-beam@v1\n        with:\n          elixir-version: ${{ env.elixir }}\n          otp-version: ${{ env.otp }}\n\n      - name: Restore deps and _build cache\n        uses: actions/cache@v4\n        with:\n          path: |\n            deps\n            _build\n          key: ${{ runner.os }}-mix-${{ env.elixir }}-${{ env.otp }}-${{ hashFiles('**/mix.lock') }}-docs\n          restore-keys: |\n            ${{ runner.os }}-mix-${{ env.elixir }}-${{ env.otp }}-\n      - name: Install Dependencies\n        run: mix deps.get --only docs\n\n      - name: Build docs\n        run: mix docs\n\n      - name: Upload artifact\n        uses: actions/upload-pages-artifact@v3\n        with:\n          path: doc/\n\n  # Deployment job\n  deploy:\n    environment:\n      name: github-pages\n      url: ${{steps.deployment.outputs.page_url}}\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".github/workflows/github_release.yml",
    "content": "name: Creates a GitHub Release\n\non:\n  push:\n    tags:\n      - \"v*\"\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    env:\n      GITHUB_TOKEN: ${{ github.token }}\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Extract version from tag\n        id: version\n        run: echo \"version=${GITHUB_REF_NAME}\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Extract changelog for version\n        run: bash .github/extract-changelog.sh \"${{ steps.version.outputs.version }}\" > release_notes.md\n\n      - name: Create GitHub Release\n        run: |\n          PRERELEASE_FLAG=\"\"\n          if [[ \"${{ steps.version.outputs.version }}\" == *-rc* ]]; then\n            PRERELEASE_FLAG=\"--prerelease\"\n          fi\n          gh release create \"${{ steps.version.outputs.version }}\" \\\n            --title \"${{ steps.version.outputs.version }}\" \\\n            --notes-file release_notes.md \\\n            $PRERELEASE_FLAG\n"
  },
  {
    "path": ".github/workflows/npm-publish.yml",
    "content": "# https://docs.npmjs.com/trusted-publishers\nname: NPM Publish\n\non:\n  push:\n    tags:\n      - \"v*\"\n\npermissions:\n  id-token: write  # Required for OIDC\n  contents: read\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: \"20\"\n          registry-url: \"https://registry.npmjs.org\"\n\n      # Ensure npm 11.5.1 or later is installed\n      - name: Update npm\n        run: npm install -g npm@latest\n\n      - name: Determine npm tag\n        id: npm-tag\n        run: |\n          TAG=${GITHUB_REF#refs/tags/}\n          # Update this condition when bumping the major version!\n          if [[ $TAG == v1.0.* ]]; then\n            echo \"tag=old-version\" >> $GITHUB_OUTPUT\n          elif [[ $TAG == *-rc* ]]; then\n            echo \"tag=rc\" >> $GITHUB_OUTPUT\n          else\n            echo \"tag=latest\" >> $GITHUB_OUTPUT\n          fi\n\n      - name: Publish to npm\n        run: npm publish --tag ${{ steps.npm-tag.outputs.tag }}"
  },
  {
    "path": ".gitignore",
    "content": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up here.\n/cover/\n\n# The directory Mix downloads your dependencies sources to.\n/deps/\n\n# Where 3rd-party dependencies like ExDoc output generated docs.\n/doc/\n\n# Ignore .fetch files in case you like to edit your project deps locally.\n/.fetch\n\n# If the VM crashes, it generates a dump, let's ignore it too.\nerl_crash.dump\n\n# Also ignore archive artifacts (built via \"mix archive.build\").\n*.ez\n\n# Ignore package tarball (built via \"mix hex.build\").\nphoenix_live_view-*.tar\n\nnode_modules\n\n/test/e2e/test-results/\n/playwright-report/\n/coverage/\n/assets/js/types/\n"
  },
  {
    "path": ".igniter.exs",
    "content": "# This is a configuration file for igniter.\n# For option documentation, see https://hexdocs.pm/igniter/Igniter.Project.IgniterConfig.html\n# To keep it up to date, use `mix igniter.setup`\n[\n  module_location: :outside_matching_folder,\n  extensions: [],\n  deps_location: :last_list_literal,\n  source_folders: [\"lib\", \"test/support\"],\n  dont_move_files: [~r\"lib/mix\"]\n]\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog for v1.2\n\n## v1.2.0-rc.0 (Unreleased)\n\n### Enhancements\n\n* Add `phx-no-unused-field` to prevent sending `_unused` parameters to the server ([#3577](https://github.com/phoenixframework/phoenix_live_view/issues/3577))\n* Add `Phoenix.LiveView.JS.to_encodable/1` pushing JS commands via events ([#4060](https://github.com/phoenixframework/phoenix_live_view/pull/4060))\n  * `%JS{}` now also implements the `JSON.Encoder` and `Jason.Encoder` protocols\n* HTMLFormatter: Better preserve whitespace around tags and inside inline elements ([#3718](https://github.com/phoenixframework/phoenix_live_view/issues/3718))\n* HEEx: Allow to opt out of debug annotations for a module ([#4119](https://github.com/phoenixframework/phoenix_live_view/pull/4119))\n* HEEx: warn when missing a space between attributes ([#3999](https://github.com/phoenixframework/phoenix_live_view/issues/3999))\n* HTMLFormatter: Add `TagFormatter` behaviour for formatting `<style>` and `<script>` tags ([#4140](https://github.com/phoenixframework/phoenix_live_view/pull/4140))\n\n## v1.1\n\nThe CHANGELOG for v1.1 releases can be found [in the v1.1 branch](https://github.com/phoenixframework/phoenix_live_view/blob/v1.1/CHANGELOG.md).\n"
  },
  {
    "path": "LICENSE.md",
    "content": "# MIT License\n\nCopyright (c) 2018 Chris McCord\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": "# Phoenix LiveView\n\n[![Actions Status](https://github.com/phoenixframework/phoenix_live_view/workflows/CI/badge.svg)](https://github.com/phoenixframework/phoenix_live_view/actions?query=workflow%3ACI) [![Hex.pm](https://img.shields.io/hexpm/v/phoenix_live_view.svg)](https://hex.pm/packages/phoenix_live_view) [![Documentation](https://img.shields.io/badge/documentation-gray)](https://hexdocs.pm/phoenix_live_view)\n\nPhoenix LiveView enables rich, real-time user experiences with server-rendered HTML.\n\nVisit the [https://livebeats.fly.dev](https://livebeats.fly.dev/) demo to see\nthe kinds of applications you can build, or see a sneak peek below:\n\nhttps://user-images.githubusercontent.com/576796/162234098-31b580fe-e424-47e6-b01d-cd2cfcf823a9.mp4\n\n<br />\n\nLiveView ships by default in new Phoenix applications. After you\n[install Elixir](https://elixir-lang.org/install.html) on your machine,\nyou can create your first LiveView app in two steps:\n\n    $ mix archive.install hex phx_new\n    $ mix phx.new demo\n\n> If you have an older existing Phoenix app and you wish to add LiveView,\n> see [the previous installation guide](https://github.com/phoenixframework/phoenix_live_view/blob/v0.20.1/guides/introduction/installation.md).\n\n## Feature highlights\n\nLiveView brings a unified experience to building web applications. You no longer\nhave to split work between client and server, across different toolings, layers, and\nabstractions. Instead, LiveView enriches the server with a declarative and powerful\nmodel while keeping your code closer to your data (and ultimately your source of truth):\n\n  * **Declarative rendering:** Render HTML on the server over WebSockets with a declarative model, including an optional LongPolling fallback.\n\n  * **Rich templating language:** Enjoy HEEx: a templating language that supports function components, slots, HTML validation, verified routes, and more.\n\n  * **Diffs over the wire:** Instead of sending \"HTML over the wire\", LiveView knows exactly which parts of your templates change, sending minimal diffs over the wire after the initial render, reducing latency and bandwidth usage. The client leverages this information and optimizes the browser with 5-10x faster updates, compared to solutions that replace whole HTML fragments.\n\n  * **Live form validation:** LiveView supports real-time form validation out of the box. Create rich user interfaces with features like uploads, nested inputs, and [specialized recovery](https://hexdocs.pm/phoenix_live_view/form-bindings.html#recovery-following-crashes-or-disconnects).\n\n  * **File uploads:** Real-time file uploads with progress indicators and image previews. Process your uploads on the fly or submit them to your desired cloud service.\n\n  * **Rich integration API:** Use the rich integration API to interact with the client, with `phx-click`, `phx-focus`, `phx-blur`, `phx-submit`, and `phx-hook` included for cases where you have to write JavaScript.\n\n  * **Optimistic updates and transitions:** Perform optimistic updates and transitions with JavaScript commands via `Phoenix.LiveView.JS`.\n\n  * **Loose coupling:** Reuse more code via stateful components with loosely-coupled templates, state, and event handling — a must for enterprise application development.\n\n  * **Live navigation:** Enriched links and redirects are just more ways LiveView keeps your app light and performant. Clients load the minimum amount of content needed as users navigate around your app without any compromise in user experience.\n\n  * **Latency simulator:** Emulate how slow clients will interact with your application with the latency simulator.\n\n  * **Robust test suite:** Write tests with confidence alongside Phoenix LiveView built-in testing tools. No more running a whole browser alongside your tests.\n\n## Learning\n\nCheck our [comprehensive docs](https://hexdocs.pm/phoenix_live_view) to get started.\n\nThe Phoenix framework documentation also keeps a list of [community resources](https://hexdocs.pm/phoenix/community.html), including books, videos, and other materials, and some include LiveView too.\n\nAlso follow these announcements from the Phoenix team on LiveView for more examples and rationale:\n\n  * [LiveBeats: Building a Social Music App With Phoenix LiveView](https://fly.io/blog/livebeats/)\n\n  * [Build a real-time Twitter clone with LiveView](https://www.phoenixframework.org/blog/build-a-real-time-twitter-clone-in-15-minutes-with-live-view-and-phoenix-1-5)\n\n  * [Build a real-time Twitch clone with LiveView and Elixir WebRTC](https://www.youtube.com/watch?v=jziOb2Edfzk)\n\n  * [Initial announcement](https://dockyard.com/blog/2018/12/12/phoenix-liveview-interactive-real-time-apps-no-need-to-write-javascript)\n\n## Component systems\n\nWhen you create a new Phoenix project, it comes with a minimal component system to power Phoenix generators.\nIn case you want to enrich your developer experience, there are several component systems provided by the\ncommunity at different stages of development:\n\n* [Bloom](https://github.com/chrisgreg/bloom): The opinionated, open-source extension to Phoenix Core Components\n\n* [Doggo](https://github.com/woylie/doggo): Headless UI components for Phoenix\n\n* [Petal Components](https://github.com/petalframework/petal_components): Phoenix + Live View HEEX Components\n\n* [PrimerLive](https://github.com/ArthurClemens/primer_live): An implementation of GitHub's Primer Design System using Phoenix LiveView\n\n* [SaladUI](https://github.com/bluzky/salad_ui): Phoenix Liveview component library inspired by shadcn UI\n\n* [Mishka Chelekom](https://github.com/mishka-group/mishka_chelekom): Phoenix + LiveView UI kit and HEEx components\n\n* [Fluxon UI](https://fluxonui.com): Elegant and accessible UI components for Phoenix LiveView\n\n## LiveDebugger\n\n[LiveDebugger](https://github.com/software-mansion/live-debugger) is a debugging tool built specifically for Phoenix LiveView applications. It provides a real-time view into your LiveView processes, making it easier to understand the component hierarchy, inspect assigns, trace lifecycle callbacks, and troubleshoot issues more efficiently.\n\nTo get started, follow the setup instructions in the [LiveDebugger repository](https://github.com/software-mansion/live-debugger?tab=readme-ov-file#getting-started).\n\n## What makes LiveView unique?\n\nLiveView is server-centric. You no longer have to worry about managing\nboth client and server to keep things in sync. LiveView automatically\nupdates the client as changes happen on the server.\n\nLiveView is first rendered statically as part of regular HTTP requests,\nwhich provides quick times for \"First Meaningful Paint\", in addition to\nhelping search and indexing engines.\n\nThen LiveView uses a persistent connection between client and server.\nThis allows LiveView applications to react faster to user events as\nthere is less work to be done and less data to be sent compared to\nstateless requests that have to authenticate, decode, load, and encode\ndata on every request.\n\nWhen LiveView was first announced, many developers from different\nbackgrounds got inspired by the potential unlocked by LiveView to\nbuild rich, real-time user experiences. We believe LiveView is built\non top of a solid foundation that makes LiveView hard to replicate\nanywhere else:\n\n  * LiveView is built on top of the Elixir programming language and\n    functional programming, which provides a great model for reasoning\n    about your code and how your LiveView changes over time.\n\n  * By building on top of a [scalable platform](https://dockyard.com/blog/2016/08/09/phoenix-channels-vs-rails-action-cable),\n    LiveView scales well vertically (from small to large instances)\n    and horizontally (by adding more instances). This allows you to\n    continue shipping features when more and more users join your\n    application, instead of dealing with performance issues.\n\n  * LiveView applications are *distributed and real-time*. A LiveView\n    app can push events to users as those events happen anywhere in\n    the system. Do you want to notify a user that their best friend\n    just connected? This is easily done without a single line of\n    custom JavaScript and with no extra external dependencies\n    (no extra databases, no Redis, no extra message queues, etc.).\n\n  * LiveView performs change tracking: whenever you change a value on\n    the server, LiveView will send to the client only the values that\n    changed, drastically reducing the latency and the amount of data\n    sent over the wire. This is achievable thanks to Elixir's\n    immutability and its ability to treat code as data.\n\n## Browser Support\n\nAll current Chrome, Safari, Firefox, and MS Edge are supported.\nIE11 support is available with the following polyfills:\n\n```shell\n$ npm install --save --prefix assets mdn-polyfills url-search-params-polyfill formdata-polyfill child-replace-with-polyfill classlist-polyfill new-event-polyfill @webcomponents/template shim-keyboard-event-key core-js\n```\n\nNote: The `shim-keyboard-event-key` polyfill is also required for [MS Edge 12-18](https://caniuse.com/#feat=keyboardevent-key).\n\nNote: The `event-submitter-polyfill` package is also required for [MS Edge 12-80 &amp; Safari &lt; 15.4](https://caniuse.com/mdn-api_submitevent_submitter).\n\n```javascript\n// assets/js/app.js\nimport \"mdn-polyfills/Object.assign\"\nimport \"mdn-polyfills/CustomEvent\"\nimport \"mdn-polyfills/String.prototype.startsWith\"\nimport \"mdn-polyfills/Array.from\"\nimport \"mdn-polyfills/Array.prototype.find\"\nimport \"mdn-polyfills/Array.prototype.some\"\nimport \"mdn-polyfills/NodeList.prototype.forEach\"\nimport \"mdn-polyfills/Element.prototype.closest\"\nimport \"mdn-polyfills/Element.prototype.matches\"\nimport \"mdn-polyfills/Node.prototype.remove\"\nimport \"child-replace-with-polyfill\"\nimport \"url-search-params-polyfill\"\nimport \"formdata-polyfill\"\nimport \"classlist-polyfill\"\nimport \"new-event-polyfill\"\nimport \"@webcomponents/template\"\nimport \"shim-keyboard-event-key\"\nimport \"event-submitter-polyfill\"\nimport \"core-js/features/set\"\nimport \"core-js/features/url\"\n\nimport {Socket} from \"phoenix\"\nimport {LiveSocket} from \"phoenix_live_view\"\n...\n```\n\n## Contributing\n\nWe appreciate any contribution to LiveView.\n\nPlease see the Phoenix [Code of Conduct](https://github.com/phoenixframework/phoenix/blob/master/CODE_OF_CONDUCT.md) and [Contributing](https://github.com/phoenixframework/phoenix/blob/master/CONTRIBUTING.md) guides.\n\nRunning the Elixir tests:\n\n```bash\n$ mix deps.get\n$ mix test\n```\n\nRunning all JavaScript tests:\n```bash\n$ npm install\n$ npm run setup\n$ npm run test\n```\n\nRunning the JavaScript unit tests:\n\n```bash\n$ npm run setup\n$ npm run js:test\n# to automatically run tests for files that have been changed\n$ npm run js:test.watch\n```\n\nRunning the JavaScript end-to-end tests:\n\n```bash\n$ npm run setup\n$ npm run e2e:test\n```\n\nChecking test coverage:\n\n```bash\n$ npm run cover\n$ npm run cover:report\n```\n\nFormat the files:\n\n```bash\n$ mix format\n$ npm run js:format\n```\n\nJS contributions are very welcome, but please do not include an updated `priv/static/phoenix_live_view.js` in pull requests. The maintainers will update it as part of the release process.\n"
  },
  {
    "path": "assets/.prettierignore",
    "content": "js/types/\n"
  },
  {
    "path": "assets/.prettierrc",
    "content": ""
  },
  {
    "path": "assets/js/phoenix_live_view/aria.js",
    "content": "const ARIA = {\n  anyOf(instance, classes) {\n    return classes.find((name) => instance instanceof name);\n  },\n\n  isFocusable(el, interactiveOnly) {\n    return (\n      (el instanceof HTMLAnchorElement && el.rel !== \"ignore\") ||\n      (el instanceof HTMLAreaElement && el.href !== undefined) ||\n      (!el.disabled &&\n        this.anyOf(el, [\n          HTMLInputElement,\n          HTMLSelectElement,\n          HTMLTextAreaElement,\n          HTMLButtonElement,\n        ])) ||\n      el instanceof HTMLIFrameElement ||\n      (el.tabIndex >= 0 && el.getAttribute(\"aria-hidden\") !== \"true\") ||\n      (!interactiveOnly &&\n        el.getAttribute(\"tabindex\") !== null &&\n        el.getAttribute(\"aria-hidden\") !== \"true\")\n    );\n  },\n\n  attemptFocus(el, interactiveOnly) {\n    if (this.isFocusable(el, interactiveOnly)) {\n      try {\n        el.focus();\n      } catch {\n        // that's fine\n      }\n    }\n    return !!document.activeElement && document.activeElement.isSameNode(el);\n  },\n\n  focusFirstInteractive(el) {\n    let child = el.firstElementChild;\n    while (child) {\n      if (this.attemptFocus(child, true) || this.focusFirstInteractive(child)) {\n        return true;\n      }\n      child = child.nextElementSibling;\n    }\n  },\n\n  focusFirst(el) {\n    let child = el.firstElementChild;\n    while (child) {\n      if (this.attemptFocus(child) || this.focusFirst(child)) {\n        return true;\n      }\n      child = child.nextElementSibling;\n    }\n  },\n\n  focusLast(el) {\n    let child = el.lastElementChild;\n    while (child) {\n      if (this.attemptFocus(child) || this.focusLast(child)) {\n        return true;\n      }\n      child = child.previousElementSibling;\n    }\n  },\n};\nexport default ARIA;\n"
  },
  {
    "path": "assets/js/phoenix_live_view/browser.js",
    "content": "const Browser = {\n  canPushState() {\n    return typeof history.pushState !== \"undefined\";\n  },\n\n  dropLocal(localStorage, namespace, subkey) {\n    return localStorage.removeItem(this.localKey(namespace, subkey));\n  },\n\n  updateLocal(localStorage, namespace, subkey, initial, func) {\n    const current = this.getLocal(localStorage, namespace, subkey);\n    const key = this.localKey(namespace, subkey);\n    const newVal = current === null ? initial : func(current);\n    localStorage.setItem(key, JSON.stringify(newVal));\n    return newVal;\n  },\n\n  getLocal(localStorage, namespace, subkey) {\n    return JSON.parse(localStorage.getItem(this.localKey(namespace, subkey)));\n  },\n\n  updateCurrentState(callback) {\n    if (!this.canPushState()) {\n      return;\n    }\n    history.replaceState(\n      callback(history.state || {}),\n      \"\",\n      window.location.href,\n    );\n  },\n\n  pushState(kind, meta, to) {\n    if (this.canPushState()) {\n      if (to !== window.location.href) {\n        if (meta.type == \"redirect\" && meta.scroll) {\n          // If we're redirecting store the current scrollY for the current history state.\n          const currentState = history.state || {};\n          currentState.scroll = meta.scroll;\n          history.replaceState(currentState, \"\", window.location.href);\n        }\n\n        delete meta.scroll; // Only store the scroll in the redirect case.\n        history[kind + \"State\"](meta, \"\", to || null); // IE will coerce undefined to string\n\n        // when using navigate, we'd call pushState immediately before patching the DOM,\n        // jumping back to the top of the page, effectively ignoring the scrollIntoView;\n        // therefore we wait for the next frame (after the DOM patch) and only then try\n        // to scroll to the hashEl\n        window.requestAnimationFrame(() => {\n          const hashEl = this.getHashTargetEl(window.location.hash);\n\n          if (hashEl) {\n            hashEl.scrollIntoView();\n          } else if (meta.type === \"redirect\") {\n            window.scroll(0, 0);\n          }\n        });\n      }\n    } else {\n      this.redirect(to);\n    }\n  },\n\n  setCookie(name, value, maxAgeSeconds) {\n    const expires =\n      typeof maxAgeSeconds === \"number\" ? ` max-age=${maxAgeSeconds};` : \"\";\n    document.cookie = `${name}=${value};${expires} path=/`;\n  },\n\n  getCookie(name) {\n    return document.cookie.replace(\n      new RegExp(`(?:(?:^|.*;\\s*)${name}\\s*\\=\\s*([^;]*).*$)|^.*$`),\n      \"$1\",\n    );\n  },\n\n  deleteCookie(name) {\n    document.cookie = `${name}=; max-age=-1; path=/`;\n  },\n\n  redirect(\n    toURL,\n    flash,\n    navigate = (url) => {\n      window.location.href = url;\n    },\n  ) {\n    if (flash) {\n      this.setCookie(\"__phoenix_flash__\", flash, 60);\n    }\n    navigate(toURL);\n  },\n\n  localKey(namespace, subkey) {\n    return `${namespace}-${subkey}`;\n  },\n\n  getHashTargetEl(maybeHash) {\n    const hash = maybeHash.toString().substring(1);\n    if (hash === \"\") {\n      return;\n    }\n    return (\n      document.getElementById(hash) ||\n      document.querySelector(`a[name=\"${hash}\"]`)\n    );\n  },\n};\n\nexport default Browser;\n"
  },
  {
    "path": "assets/js/phoenix_live_view/constants.js",
    "content": "export const CONSECUTIVE_RELOADS = \"consecutive-reloads\";\nexport const MAX_RELOADS = 10;\nexport const RELOAD_JITTER_MIN = 5000;\nexport const RELOAD_JITTER_MAX = 10000;\nexport const FAILSAFE_JITTER = 30000;\nexport const PHX_EVENT_CLASSES = [\n  \"phx-click-loading\",\n  \"phx-change-loading\",\n  \"phx-submit-loading\",\n  \"phx-keydown-loading\",\n  \"phx-keyup-loading\",\n  \"phx-blur-loading\",\n  \"phx-focus-loading\",\n  \"phx-hook-loading\",\n];\nexport const PHX_DROP_TARGET_ACTIVE_CLASS = \"phx-drop-target-active\";\nexport const PHX_COMPONENT = \"data-phx-component\";\nexport const PHX_VIEW_REF = \"data-phx-view\";\nexport const PHX_LIVE_LINK = \"data-phx-link\";\nexport const PHX_TRACK_STATIC = \"track-static\";\nexport const PHX_LINK_STATE = \"data-phx-link-state\";\nexport const PHX_REF_LOADING = \"data-phx-ref-loading\";\nexport const PHX_REF_SRC = \"data-phx-ref-src\";\nexport const PHX_REF_LOCK = \"data-phx-ref-lock\";\nexport const PHX_PENDING_REFS = \"phx-pending-refs\";\nexport const PHX_TRACK_UPLOADS = \"track-uploads\";\nexport const PHX_UPLOAD_REF = \"data-phx-upload-ref\";\nexport const PHX_PREFLIGHTED_REFS = \"data-phx-preflighted-refs\";\nexport const PHX_DONE_REFS = \"data-phx-done-refs\";\nexport const PHX_DROP_TARGET = \"drop-target\";\nexport const PHX_ACTIVE_ENTRY_REFS = \"data-phx-active-refs\";\nexport const PHX_LIVE_FILE_UPDATED = \"phx:live-file:updated\";\nexport const PHX_SKIP = \"data-phx-skip\";\nexport const PHX_MAGIC_ID = \"data-phx-id\";\nexport const PHX_PRUNE = \"data-phx-prune\";\nexport const PHX_CONNECTED_CLASS = \"phx-connected\";\nexport const PHX_LOADING_CLASS = \"phx-loading\";\nexport const PHX_ERROR_CLASS = \"phx-error\";\nexport const PHX_CLIENT_ERROR_CLASS = \"phx-client-error\";\nexport const PHX_SERVER_ERROR_CLASS = \"phx-server-error\";\nexport const PHX_PARENT_ID = \"data-phx-parent-id\";\nexport const PHX_MAIN = \"data-phx-main\";\nexport const PHX_ROOT_ID = \"data-phx-root-id\";\nexport const PHX_VIEWPORT_TOP = \"viewport-top\";\nexport const PHX_VIEWPORT_BOTTOM = \"viewport-bottom\";\nexport const PHX_VIEWPORT_OVERRUN_TARGET = \"viewport-overrun-target\";\nexport const PHX_TRIGGER_ACTION = \"trigger-action\";\nexport const PHX_HAS_FOCUSED = \"phx-has-focused\";\nexport const FOCUSABLE_INPUTS = [\n  \"text\",\n  \"textarea\",\n  \"number\",\n  \"email\",\n  \"password\",\n  \"search\",\n  \"tel\",\n  \"url\",\n  \"date\",\n  \"time\",\n  \"datetime-local\",\n  \"color\",\n  \"range\",\n];\nexport const CHECKABLE_INPUTS = [\"checkbox\", \"radio\"];\nexport const PHX_HAS_SUBMITTED = \"phx-has-submitted\";\nexport const PHX_SESSION = \"data-phx-session\";\nexport const PHX_VIEW_SELECTOR = `[${PHX_SESSION}]`;\nexport const PHX_STICKY = \"data-phx-sticky\";\nexport const PHX_STATIC = \"data-phx-static\";\nexport const PHX_READONLY = \"data-phx-readonly\";\nexport const PHX_DISABLED = \"data-phx-disabled\";\nexport const PHX_DISABLE_WITH = \"disable-with\";\nexport const PHX_DISABLE_WITH_RESTORE = \"data-phx-disable-with-restore\";\nexport const PHX_HOOK = \"hook\";\nexport const PHX_DEBOUNCE = \"debounce\";\nexport const PHX_THROTTLE = \"throttle\";\nexport const PHX_UPDATE = \"update\";\nexport const PHX_STREAM = \"stream\";\nexport const PHX_STREAM_REF = \"data-phx-stream\";\nexport const PHX_PORTAL = \"data-phx-portal\";\nexport const PHX_TELEPORTED_REF = \"data-phx-teleported\";\nexport const PHX_TELEPORTED_SRC = \"data-phx-teleported-src\";\nexport const PHX_RUNTIME_HOOK = \"data-phx-runtime-hook\";\nexport const PHX_LV_PID = \"data-phx-pid\";\nexport const PHX_KEY = \"key\";\nexport const PHX_PRIVATE = \"phxPrivate\";\nexport const PHX_AUTO_RECOVER = \"auto-recover\";\nexport const PHX_NO_UNUSED_FIELD = \"no-unused-field\";\nexport const PHX_LV_DEBUG = \"phx:live-socket:debug\";\nexport const PHX_LV_PROFILE = \"phx:live-socket:profiling\";\nexport const PHX_LV_LATENCY_SIM = \"phx:live-socket:latency-sim\";\nexport const PHX_LV_HISTORY_POSITION = \"phx:nav-history-position\";\nexport const PHX_PROGRESS = \"progress\";\nexport const PHX_MOUNTED = \"mounted\";\nexport const PHX_RELOAD_STATUS = \"__phoenix_reload_status__\";\nexport const LOADER_TIMEOUT = 1;\nexport const MAX_CHILD_JOIN_ATTEMPTS = 3;\nexport const BEFORE_UNLOAD_LOADER_TIMEOUT = 200;\nexport const DISCONNECTED_TIMEOUT = 500;\nexport const BINDING_PREFIX = \"phx-\";\nexport const PUSH_TIMEOUT = 30000;\nexport const LINK_HEADER = \"x-requested-with\";\nexport const RESPONSE_URL_HEADER = \"x-response-url\";\nexport const DEBOUNCE_TRIGGER = \"debounce-trigger\";\nexport const THROTTLED = \"throttled\";\nexport const DEBOUNCE_PREV_KEY = \"debounce-prev-key\";\nexport const DEFAULTS = {\n  debounce: 300,\n  throttle: 300,\n};\nexport const PHX_PENDING_ATTRS = [PHX_REF_LOADING, PHX_REF_SRC, PHX_REF_LOCK];\n// Rendered\nexport const STATIC = \"s\";\nexport const ROOT = \"r\";\nexport const COMPONENTS = \"c\";\nexport const KEYED = \"k\";\nexport const KEYED_COUNT = \"kc\";\nexport const EVENTS = \"e\";\nexport const REPLY = \"r\";\nexport const TITLE = \"t\";\nexport const TEMPLATES = \"p\";\nexport const STREAM = \"stream\";\n"
  },
  {
    "path": "assets/js/phoenix_live_view/dom.js",
    "content": "import {\n  CHECKABLE_INPUTS,\n  DEBOUNCE_PREV_KEY,\n  DEBOUNCE_TRIGGER,\n  FOCUSABLE_INPUTS,\n  PHX_COMPONENT,\n  PHX_VIEW_REF,\n  PHX_TELEPORTED_REF,\n  PHX_HAS_FOCUSED,\n  PHX_HAS_SUBMITTED,\n  PHX_MAIN,\n  PHX_PARENT_ID,\n  PHX_PRIVATE,\n  PHX_REF_SRC,\n  PHX_REF_LOCK,\n  PHX_PENDING_ATTRS,\n  PHX_ROOT_ID,\n  PHX_SESSION,\n  PHX_STATIC,\n  PHX_UPLOAD_REF,\n  PHX_VIEW_SELECTOR,\n  PHX_STICKY,\n  PHX_EVENT_CLASSES,\n  THROTTLED,\n  PHX_PORTAL,\n  PHX_STREAM,\n} from \"./constants\";\n\nimport { logError } from \"./utils\";\n\nconst DOM = {\n  byId(id) {\n    return document.getElementById(id) || logError(`no id found for ${id}`);\n  },\n\n  removeClass(el, className) {\n    el.classList.remove(className);\n    if (el.classList.length === 0) {\n      el.removeAttribute(\"class\");\n    }\n  },\n\n  all(node, query, callback) {\n    if (!node) {\n      return [];\n    }\n    const array = Array.from(node.querySelectorAll(query));\n    if (callback) {\n      array.forEach(callback);\n    }\n    return array;\n  },\n\n  childNodeLength(html) {\n    const template = document.createElement(\"template\");\n    template.innerHTML = html;\n    return template.content.childElementCount;\n  },\n\n  isUploadInput(el) {\n    return el.type === \"file\" && el.getAttribute(PHX_UPLOAD_REF) !== null;\n  },\n\n  isAutoUpload(inputEl) {\n    return inputEl.hasAttribute(\"data-phx-auto-upload\");\n  },\n\n  findUploadInputs(node) {\n    const formId = node.id;\n    const inputsOutsideForm = this.all(\n      document,\n      `input[type=\"file\"][${PHX_UPLOAD_REF}][form=\"${formId}\"]`,\n    );\n    return this.all(node, `input[type=\"file\"][${PHX_UPLOAD_REF}]`).concat(\n      inputsOutsideForm,\n    );\n  },\n\n  findComponentNodeList(viewId, cid, doc = document) {\n    return this.all(\n      doc,\n      `[${PHX_VIEW_REF}=\"${viewId}\"][${PHX_COMPONENT}=\"${cid}\"]`,\n    );\n  },\n\n  isPhxDestroyed(node) {\n    return node.id && DOM.private(node, \"destroyed\") ? true : false;\n  },\n\n  wantsNewTab(e) {\n    const wantsNewTab =\n      e.ctrlKey || e.shiftKey || e.metaKey || (e.button && e.button === 1);\n    const isDownload =\n      e.target instanceof HTMLAnchorElement &&\n      e.target.hasAttribute(\"download\");\n    const isTargetBlank =\n      e.target.hasAttribute(\"target\") &&\n      e.target.getAttribute(\"target\").toLowerCase() === \"_blank\";\n    const isTargetNamedTab =\n      e.target.hasAttribute(\"target\") &&\n      !e.target.getAttribute(\"target\").startsWith(\"_\");\n    return wantsNewTab || isTargetBlank || isDownload || isTargetNamedTab;\n  },\n\n  isUnloadableFormSubmit(e) {\n    // Ignore form submissions intended to close a native <dialog> element\n    // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog#usage_notes\n    const isDialogSubmit =\n      (e.target && e.target.getAttribute(\"method\") === \"dialog\") ||\n      (e.submitter && e.submitter.getAttribute(\"formmethod\") === \"dialog\");\n\n    if (isDialogSubmit) {\n      return false;\n    } else {\n      return !e.defaultPrevented && !this.wantsNewTab(e);\n    }\n  },\n\n  isNewPageClick(e, currentLocation) {\n    const href =\n      e.target instanceof HTMLAnchorElement\n        ? e.target.getAttribute(\"href\")\n        : null;\n    let url;\n\n    if (e.defaultPrevented || href === null || this.wantsNewTab(e)) {\n      return false;\n    }\n    if (href.startsWith(\"mailto:\") || href.startsWith(\"tel:\")) {\n      return false;\n    }\n    if (e.target.isContentEditable) {\n      return false;\n    }\n\n    try {\n      url = new URL(href);\n    } catch {\n      try {\n        url = new URL(href, currentLocation);\n      } catch {\n        // bad URL, fallback to let browser try it as external\n        return true;\n      }\n    }\n\n    if (\n      url.host === currentLocation.host &&\n      url.protocol === currentLocation.protocol\n    ) {\n      if (\n        url.pathname === currentLocation.pathname &&\n        url.search === currentLocation.search\n      ) {\n        return url.hash === \"\" && !url.href.endsWith(\"#\");\n      }\n    }\n    return url.protocol.startsWith(\"http\");\n  },\n\n  markPhxChildDestroyed(el) {\n    if (this.isPhxChild(el)) {\n      el.setAttribute(PHX_SESSION, \"\");\n    }\n    this.putPrivate(el, \"destroyed\", true);\n  },\n\n  findPhxChildrenInFragment(html, parentId) {\n    const template = document.createElement(\"template\");\n    template.innerHTML = html;\n    return this.findPhxChildren(template.content, parentId);\n  },\n\n  isIgnored(el, phxUpdate) {\n    return (\n      (el.getAttribute(phxUpdate) || el.getAttribute(\"data-phx-update\")) ===\n      \"ignore\"\n    );\n  },\n\n  isPhxUpdate(el, phxUpdate, updateTypes) {\n    return (\n      el.getAttribute && updateTypes.indexOf(el.getAttribute(phxUpdate)) >= 0\n    );\n  },\n\n  findPhxSticky(el) {\n    return this.all(el, `[${PHX_STICKY}]`);\n  },\n\n  findPhxChildren(el, parentId) {\n    return this.all(el, `${PHX_VIEW_SELECTOR}[${PHX_PARENT_ID}=\"${parentId}\"]`);\n  },\n\n  findExistingParentCIDs(viewId, cids) {\n    // we only want to find parents that exist on the page\n    // if a cid is not on the page, the only way it can be added back to the page\n    // is if a parent adds it back, therefore if a cid does not exist on the page,\n    // we should not try to render it by itself (because it would be rendered twice,\n    // one by the parent, and a second time by itself)\n    const parentCids = new Set();\n    const childrenCids = new Set();\n\n    cids.forEach((cid) => {\n      this.all(\n        document,\n        `[${PHX_VIEW_REF}=\"${viewId}\"][${PHX_COMPONENT}=\"${cid}\"]`,\n      ).forEach((parent) => {\n        parentCids.add(cid);\n        this.all(parent, `[${PHX_VIEW_REF}=\"${viewId}\"][${PHX_COMPONENT}]`)\n          .map((el) => parseInt(el.getAttribute(PHX_COMPONENT)))\n          .forEach((childCID) => childrenCids.add(childCID));\n      });\n    });\n\n    childrenCids.forEach((childCid) => parentCids.delete(childCid));\n\n    return parentCids;\n  },\n\n  private(el, key) {\n    return el[PHX_PRIVATE] && el[PHX_PRIVATE][key];\n  },\n\n  deletePrivate(el, key) {\n    el[PHX_PRIVATE] && delete el[PHX_PRIVATE][key];\n  },\n\n  putPrivate(el, key, value) {\n    if (!el[PHX_PRIVATE]) {\n      el[PHX_PRIVATE] = {};\n    }\n    el[PHX_PRIVATE][key] = value;\n  },\n\n  updatePrivate(el, key, defaultVal, updateFunc) {\n    const existing = this.private(el, key);\n    if (existing === undefined) {\n      this.putPrivate(el, key, updateFunc(defaultVal));\n    } else {\n      this.putPrivate(el, key, updateFunc(existing));\n    }\n  },\n\n  syncPendingAttrs(fromEl, toEl) {\n    if (!fromEl.hasAttribute(PHX_REF_SRC)) {\n      return;\n    }\n    PHX_EVENT_CLASSES.forEach((className) => {\n      fromEl.classList.contains(className) && toEl.classList.add(className);\n    });\n    PHX_PENDING_ATTRS.filter((attr) => fromEl.hasAttribute(attr)).forEach(\n      (attr) => {\n        toEl.setAttribute(attr, fromEl.getAttribute(attr));\n      },\n    );\n  },\n\n  copyPrivates(target, source) {\n    if (source[PHX_PRIVATE]) {\n      target[PHX_PRIVATE] = source[PHX_PRIVATE];\n    }\n  },\n\n  putTitle(str) {\n    const titleEl = document.querySelector(\"title\");\n    if (titleEl) {\n      const { prefix, suffix, default: defaultTitle } = titleEl.dataset;\n      const isEmpty = typeof str !== \"string\" || str.trim() === \"\";\n      if (isEmpty && typeof defaultTitle !== \"string\") {\n        return;\n      }\n\n      const inner = isEmpty ? defaultTitle : str;\n      document.title = `${prefix || \"\"}${inner || \"\"}${suffix || \"\"}`;\n    } else {\n      document.title = str;\n    }\n  },\n\n  debounce(\n    el,\n    event,\n    phxDebounce,\n    defaultDebounce,\n    phxThrottle,\n    defaultThrottle,\n    asyncFilter,\n    callback,\n  ) {\n    let debounce = el.getAttribute(phxDebounce);\n    let throttle = el.getAttribute(phxThrottle);\n\n    if (debounce === \"\") {\n      debounce = defaultDebounce;\n    }\n    if (throttle === \"\") {\n      throttle = defaultThrottle;\n    }\n    const value = debounce || throttle;\n    switch (value) {\n      case null:\n        return callback();\n\n      case \"blur\":\n        this.incCycle(el, \"debounce-blur-cycle\", () => {\n          if (asyncFilter()) {\n            callback();\n          }\n        });\n        if (this.once(el, \"debounce-blur\")) {\n          el.addEventListener(\"blur\", () =>\n            this.triggerCycle(el, \"debounce-blur-cycle\"),\n          );\n        }\n        return;\n\n      default:\n        const timeout = parseInt(value);\n        const trigger = () =>\n          throttle ? this.deletePrivate(el, THROTTLED) : callback();\n        const currentCycle = this.incCycle(el, DEBOUNCE_TRIGGER, trigger);\n        if (isNaN(timeout)) {\n          return logError(`invalid throttle/debounce value: ${value}`);\n        }\n        if (throttle) {\n          let newKeyDown = false;\n          if (event.type === \"keydown\") {\n            const prevKey = this.private(el, DEBOUNCE_PREV_KEY);\n            this.putPrivate(el, DEBOUNCE_PREV_KEY, event.key);\n            newKeyDown = prevKey !== event.key;\n          }\n\n          if (!newKeyDown && this.private(el, THROTTLED)) {\n            return false;\n          } else {\n            callback();\n            const t = setTimeout(() => {\n              if (asyncFilter()) {\n                this.triggerCycle(el, DEBOUNCE_TRIGGER);\n              }\n            }, timeout);\n            this.putPrivate(el, THROTTLED, t);\n          }\n        } else {\n          setTimeout(() => {\n            if (asyncFilter()) {\n              this.triggerCycle(el, DEBOUNCE_TRIGGER, currentCycle);\n            }\n          }, timeout);\n        }\n\n        const form = el.form;\n        if (form && this.once(form, \"bind-debounce\")) {\n          form.addEventListener(\"submit\", () => {\n            Array.from(new FormData(form).entries(), ([name]) => {\n              const namedItem = form.elements.namedItem(name);\n              const input =\n                namedItem instanceof RadioNodeList ? namedItem[0] : namedItem;\n              if (input) {\n                this.incCycle(input, DEBOUNCE_TRIGGER);\n                this.deletePrivate(input, THROTTLED);\n              }\n            });\n          });\n        }\n        if (this.once(el, \"bind-debounce\")) {\n          el.addEventListener(\"blur\", () => {\n            // because we trigger the callback here,\n            // we also clear the throttle timeout to prevent the callback\n            // from being called again after the timeout fires\n            clearTimeout(this.private(el, THROTTLED));\n            this.triggerCycle(el, DEBOUNCE_TRIGGER);\n          });\n        }\n    }\n  },\n\n  triggerCycle(el, key, currentCycle) {\n    const [cycle, trigger] = this.private(el, key);\n    if (!currentCycle) {\n      currentCycle = cycle;\n    }\n    if (currentCycle === cycle) {\n      this.incCycle(el, key);\n      trigger();\n    }\n  },\n\n  once(el, key) {\n    if (this.private(el, key) === true) {\n      return false;\n    }\n    this.putPrivate(el, key, true);\n    return true;\n  },\n\n  incCycle(el, key, trigger = function () {}) {\n    let [currentCycle] = this.private(el, key) || [0, trigger];\n    currentCycle++;\n    this.putPrivate(el, key, [currentCycle, trigger]);\n    return currentCycle;\n  },\n\n  // maintains or adds privately used hook information\n  // fromEl and toEl can be the same element in the case of a newly added node\n  // fromEl and toEl can be any HTML node type, so we need to check if it's an element node\n  maintainPrivateHooks(fromEl, toEl, phxViewportTop, phxViewportBottom) {\n    // maintain the hooks created with createHook\n    if (\n      fromEl.hasAttribute &&\n      fromEl.hasAttribute(\"data-phx-hook\") &&\n      !toEl.hasAttribute(\"data-phx-hook\")\n    ) {\n      toEl.setAttribute(\"data-phx-hook\", fromEl.getAttribute(\"data-phx-hook\"));\n    }\n    // add hooks to elements with viewport attributes\n    if (\n      toEl.hasAttribute &&\n      (toEl.hasAttribute(phxViewportTop) ||\n        toEl.hasAttribute(phxViewportBottom))\n    ) {\n      toEl.setAttribute(\"data-phx-hook\", \"Phoenix.InfiniteScroll\");\n    }\n  },\n\n  putCustomElHook(el, hook) {\n    if (el.isConnected) {\n      el.setAttribute(\"data-phx-hook\", \"\");\n    } else {\n      console.error(`\n        hook attached to non-connected DOM element\n        ensure you are calling createHook within your connectedCallback. ${el.outerHTML}\n      `);\n    }\n    this.putPrivate(el, \"custom-el-hook\", hook);\n  },\n\n  getCustomElHook(el) {\n    return this.private(el, \"custom-el-hook\");\n  },\n\n  isUsedInput(el) {\n    return (\n      el.nodeType === Node.ELEMENT_NODE &&\n      (this.private(el, PHX_HAS_FOCUSED) || this.private(el, PHX_HAS_SUBMITTED))\n    );\n  },\n\n  resetForm(form) {\n    Array.from(form.elements).forEach((input) => {\n      this.deletePrivate(input, PHX_HAS_FOCUSED);\n      this.deletePrivate(input, PHX_HAS_SUBMITTED);\n    });\n  },\n\n  isPhxChild(node) {\n    return node.getAttribute && node.getAttribute(PHX_PARENT_ID);\n  },\n\n  isPhxSticky(node) {\n    return node.getAttribute && node.getAttribute(PHX_STICKY) !== null;\n  },\n\n  isChildOfAny(el, parents) {\n    return !!parents.find((parent) => parent.contains(el));\n  },\n\n  firstPhxChild(el) {\n    return this.isPhxChild(el) ? el : this.all(el, `[${PHX_PARENT_ID}]`)[0];\n  },\n\n  isPortalTemplate(el) {\n    return el.tagName === \"TEMPLATE\" && el.hasAttribute(PHX_PORTAL);\n  },\n\n  closestViewEl(el) {\n    // find the closest portal or view element, whichever comes first\n    const portalOrViewEl = el.closest(\n      `[${PHX_TELEPORTED_REF}],${PHX_VIEW_SELECTOR}`,\n    );\n    if (!portalOrViewEl) {\n      return null;\n    }\n    if (portalOrViewEl.hasAttribute(PHX_TELEPORTED_REF)) {\n      // PHX_TELEPORTED_REF is set to the id of the view that owns the portal element\n      return this.byId(portalOrViewEl.getAttribute(PHX_TELEPORTED_REF));\n    } else if (portalOrViewEl.hasAttribute(PHX_SESSION)) {\n      return portalOrViewEl;\n    }\n    return null;\n  },\n\n  dispatchEvent(target, name, opts = {}) {\n    let defaultBubble = true;\n    const isUploadTarget =\n      target.nodeName === \"INPUT\" && target.type === \"file\";\n    if (isUploadTarget && name === \"click\") {\n      defaultBubble = false;\n    }\n    const bubbles = opts.bubbles === undefined ? defaultBubble : !!opts.bubbles;\n    const eventOpts = {\n      bubbles: bubbles,\n      cancelable: true,\n      detail: opts.detail || {},\n    };\n    const event =\n      name === \"click\"\n        ? new MouseEvent(\"click\", eventOpts)\n        : new CustomEvent(name, eventOpts);\n    target.dispatchEvent(event);\n  },\n\n  cloneNode(node, html) {\n    if (typeof html === \"undefined\") {\n      return node.cloneNode(true);\n    } else {\n      const cloned = node.cloneNode(false);\n      cloned.innerHTML = html;\n      return cloned;\n    }\n  },\n\n  // merge attributes from source to target\n  // if an element is ignored, we only merge data attributes\n  // including removing data attributes that are no longer in the source\n  mergeAttrs(target, source, opts = {}) {\n    const exclude = new Set(opts.exclude || []);\n    const isIgnored = opts.isIgnored;\n    const sourceAttrs = source.attributes;\n    for (let i = sourceAttrs.length - 1; i >= 0; i--) {\n      const name = sourceAttrs[i].name;\n      if (!exclude.has(name)) {\n        const sourceValue = source.getAttribute(name);\n        if (\n          target.getAttribute(name) !== sourceValue &&\n          (!isIgnored || (isIgnored && name.startsWith(\"data-\")))\n        ) {\n          target.setAttribute(name, sourceValue);\n        }\n      } else {\n        // We exclude the value from being merged on focused inputs, because the\n        // user's input should always win.\n        // We can still assign it as long as the value property is the same, though.\n        // This prevents a situation where the updated hook is not being triggered\n        // when an input is back in its \"original state\", because the attribute\n        // was never changed, see:\n        // https://github.com/phoenixframework/phoenix_live_view/issues/2163\n        if (name === \"value\") {\n          const sourceValue = source.value ?? source.getAttribute(name);\n          if (target.value === sourceValue) {\n            // actually set the value attribute to sync it with the value property\n            target.setAttribute(\"value\", source.getAttribute(name));\n          }\n        }\n      }\n    }\n\n    const targetAttrs = target.attributes;\n    for (let i = targetAttrs.length - 1; i >= 0; i--) {\n      const name = targetAttrs[i].name;\n      if (isIgnored) {\n        if (\n          name.startsWith(\"data-\") &&\n          !source.hasAttribute(name) &&\n          !PHX_PENDING_ATTRS.includes(name)\n        ) {\n          target.removeAttribute(name);\n        }\n      } else {\n        if (!source.hasAttribute(name)) {\n          target.removeAttribute(name);\n        }\n      }\n    }\n  },\n\n  mergeFocusedInput(target, source) {\n    // skip selects because FF will reset highlighted index for any setAttribute\n    if (!(target instanceof HTMLSelectElement)) {\n      DOM.mergeAttrs(target, source, { exclude: [\"value\"] });\n    }\n\n    if (source.readOnly) {\n      target.setAttribute(\"readonly\", true);\n    } else {\n      target.removeAttribute(\"readonly\");\n    }\n  },\n\n  hasSelectionRange(el) {\n    return (\n      el.setSelectionRange && (el.type === \"text\" || el.type === \"textarea\")\n    );\n  },\n\n  restoreFocus(focused, selectionStart, selectionEnd) {\n    if (focused instanceof HTMLSelectElement) {\n      focused.focus();\n    }\n    if (!DOM.isTextualInput(focused)) {\n      return;\n    }\n\n    const wasFocused = focused.matches(\":focus\");\n    if (!wasFocused) {\n      focused.focus();\n    }\n    if (this.hasSelectionRange(focused)) {\n      focused.setSelectionRange(selectionStart, selectionEnd);\n    }\n  },\n\n  isFormInput(el) {\n    if (el.localName && customElements.get(el.localName)) {\n      // Custom Elements may be form associated. This allows them\n      // to participate within a form's lifecycle, including form\n      // validity and form submissions.\n      // The spec for Form Associated custom elements requires the\n      // custom element's class to contain a static boolean value of `formAssociated`\n      // which identifies this class as allowed to associate to a form.\n      // See https://html.spec.whatwg.org/dev/custom-elements.html#custom-elements-face-example\n      // for details.\n      return customElements.get(el.localName)[`formAssociated`];\n    }\n\n    return (\n      /^(?:input|select|textarea)$/i.test(el.tagName) && el.type !== \"button\"\n    );\n  },\n\n  syncAttrsToProps(el) {\n    if (\n      el instanceof HTMLInputElement &&\n      CHECKABLE_INPUTS.indexOf(el.type.toLocaleLowerCase()) >= 0\n    ) {\n      el.checked = el.getAttribute(\"checked\") !== null;\n    }\n  },\n\n  isTextualInput(el) {\n    return FOCUSABLE_INPUTS.indexOf(el.type) >= 0;\n  },\n\n  isNowTriggerFormExternal(el, phxTriggerExternal) {\n    return (\n      el.getAttribute &&\n      el.getAttribute(phxTriggerExternal) !== null &&\n      document.body.contains(el)\n    );\n  },\n\n  cleanChildNodes(container, phxUpdate) {\n    if (\n      DOM.isPhxUpdate(container, phxUpdate, [\"append\", \"prepend\", PHX_STREAM])\n    ) {\n      const toRemove = [];\n      container.childNodes.forEach((childNode) => {\n        if (!childNode.id) {\n          // Skip warning if it's an empty text node (e.g. a new-line)\n          const isEmptyTextNode =\n            childNode.nodeType === Node.TEXT_NODE &&\n            childNode.nodeValue.trim() === \"\";\n          if (!isEmptyTextNode && childNode.nodeType !== Node.COMMENT_NODE) {\n            logError(\n              \"only HTML element tags with an id are allowed inside containers with phx-update.\\n\\n\" +\n                `removing illegal node: \"${(childNode.outerHTML || childNode.nodeValue).trim()}\"\\n\\n`,\n            );\n          }\n          toRemove.push(childNode);\n        }\n      });\n      toRemove.forEach((childNode) => childNode.remove());\n    }\n  },\n\n  replaceRootContainer(container, tagName, attrs) {\n    const retainedAttrs = new Set([\n      \"id\",\n      PHX_SESSION,\n      PHX_STATIC,\n      PHX_MAIN,\n      PHX_ROOT_ID,\n    ]);\n    if (container.tagName.toLowerCase() === tagName.toLowerCase()) {\n      Array.from(container.attributes)\n        .filter((attr) => !retainedAttrs.has(attr.name.toLowerCase()))\n        .forEach((attr) => container.removeAttribute(attr.name));\n\n      Object.keys(attrs)\n        .filter((name) => !retainedAttrs.has(name.toLowerCase()))\n        .forEach((attr) => container.setAttribute(attr, attrs[attr]));\n\n      return container;\n    } else {\n      const newContainer = document.createElement(tagName);\n      Object.keys(attrs).forEach((attr) =>\n        newContainer.setAttribute(attr, attrs[attr]),\n      );\n      retainedAttrs.forEach((attr) =>\n        newContainer.setAttribute(attr, container.getAttribute(attr)),\n      );\n      newContainer.innerHTML = container.innerHTML;\n      container.replaceWith(newContainer);\n      return newContainer;\n    }\n  },\n\n  getSticky(el, name, defaultVal) {\n    const op = (DOM.private(el, \"sticky\") || []).find(\n      ([existingName]) => name === existingName,\n    );\n    if (op) {\n      const [_name, _op, stashedResult] = op;\n      return stashedResult;\n    } else {\n      return typeof defaultVal === \"function\" ? defaultVal() : defaultVal;\n    }\n  },\n\n  deleteSticky(el, name) {\n    this.updatePrivate(el, \"sticky\", [], (ops) => {\n      return ops.filter(([existingName, _]) => existingName !== name);\n    });\n  },\n\n  putSticky(el, name, op) {\n    const stashedResult = op(el);\n    this.updatePrivate(el, \"sticky\", [], (ops) => {\n      const existingIndex = ops.findIndex(\n        ([existingName]) => name === existingName,\n      );\n      if (existingIndex >= 0) {\n        ops[existingIndex] = [name, op, stashedResult];\n      } else {\n        ops.push([name, op, stashedResult]);\n      }\n      return ops;\n    });\n  },\n\n  applyStickyOperations(el) {\n    const ops = DOM.private(el, \"sticky\");\n    if (!ops) {\n      return;\n    }\n\n    ops.forEach(([name, op, _stashed]) => this.putSticky(el, name, op));\n  },\n\n  isLocked(el) {\n    return el.hasAttribute && el.hasAttribute(PHX_REF_LOCK);\n  },\n\n  attributeIgnored(attribute, ignoredAttributes) {\n    return ignoredAttributes.some(\n      (toIgnore) =>\n        attribute.name == toIgnore ||\n        toIgnore === \"*\" ||\n        (toIgnore.includes(\"*\") && attribute.name.match(toIgnore) != null),\n    );\n  },\n};\n\nexport default DOM;\n"
  },
  {
    "path": "assets/js/phoenix_live_view/dom_patch.js",
    "content": "import {\n  PHX_COMPONENT,\n  PHX_PRUNE,\n  PHX_ROOT_ID,\n  PHX_SESSION,\n  PHX_SKIP,\n  PHX_MAGIC_ID,\n  PHX_STATIC,\n  PHX_TRIGGER_ACTION,\n  PHX_UPDATE,\n  PHX_REF_SRC,\n  PHX_REF_LOCK,\n  PHX_STREAM,\n  PHX_STREAM_REF,\n  PHX_VIEWPORT_TOP,\n  PHX_VIEWPORT_BOTTOM,\n  PHX_PORTAL,\n  PHX_TELEPORTED_REF,\n  PHX_TELEPORTED_SRC,\n  PHX_RUNTIME_HOOK,\n} from \"./constants\";\n\nimport { detectDuplicateIds, detectInvalidStreamInserts, isCid } from \"./utils\";\nimport ElementRef from \"./element_ref\";\nimport DOM from \"./dom\";\nimport DOMPostMorphRestorer from \"./dom_post_morph_restorer\";\nimport morphdom from \"morphdom\";\n\nexport default class DOMPatch {\n  constructor(view, container, id, html, streams, targetCID, opts = {}) {\n    this.view = view;\n    this.liveSocket = view.liveSocket;\n    this.container = container;\n    this.id = id;\n    this.rootID = view.root.id;\n    this.html = html;\n    this.streams = streams;\n    this.streamInserts = {};\n    this.streamComponentRestore = {};\n    this.targetCID = targetCID;\n    this.cidPatch = isCid(this.targetCID);\n    this.pendingRemoves = [];\n    this.phxRemove = this.liveSocket.binding(\"remove\");\n    this.targetContainer = this.isCIDPatch()\n      ? this.targetCIDContainer(html)\n      : container;\n    this.callbacks = {\n      beforeadded: [],\n      beforeupdated: [],\n      beforephxChildAdded: [],\n      afteradded: [],\n      afterupdated: [],\n      afterdiscarded: [],\n      afterphxChildAdded: [],\n      aftertransitionsDiscarded: [],\n    };\n    this.withChildren = opts.withChildren || opts.undoRef || false;\n    this.undoRef = opts.undoRef;\n  }\n\n  before(kind, callback) {\n    this.callbacks[`before${kind}`].push(callback);\n  }\n  after(kind, callback) {\n    this.callbacks[`after${kind}`].push(callback);\n  }\n\n  trackBefore(kind, ...args) {\n    this.callbacks[`before${kind}`].forEach((callback) => callback(...args));\n  }\n\n  trackAfter(kind, ...args) {\n    this.callbacks[`after${kind}`].forEach((callback) => callback(...args));\n  }\n\n  markPrunableContentForRemoval() {\n    const phxUpdate = this.liveSocket.binding(PHX_UPDATE);\n    DOM.all(\n      this.container,\n      `[${phxUpdate}=append] > *, [${phxUpdate}=prepend] > *`,\n      (el) => {\n        el.setAttribute(PHX_PRUNE, \"\");\n      },\n    );\n  }\n\n  perform(isJoinPatch) {\n    const { view, liveSocket, html, container } = this;\n    let targetContainer = this.targetContainer;\n\n    if (this.isCIDPatch() && !this.targetContainer) {\n      return;\n    }\n\n    if (this.isCIDPatch()) {\n      // https://github.com/phoenixframework/phoenix_live_view/pull/3942\n      // we need to ensure that no parent is locked\n      const closestLock = targetContainer.closest(`[${PHX_REF_LOCK}]`);\n      // If the targetContainer itself is locked, that's okay.\n      // https://github.com/phoenixframework/phoenix_live_view/issues/4088\n      if (closestLock && !closestLock.isSameNode(targetContainer)) {\n        const clonedTree = DOM.private(closestLock, PHX_REF_LOCK);\n        if (clonedTree) {\n          // if a parent is locked with a cloned tree, we need to patch the cloned tree instead\n          targetContainer = clonedTree.querySelector(\n            `[data-phx-component=\"${this.targetCID}\"]`,\n          );\n        }\n      }\n    }\n\n    const focused = liveSocket.getActiveElement();\n    const { selectionStart, selectionEnd } =\n      focused && DOM.hasSelectionRange(focused) ? focused : {};\n    const phxUpdate = liveSocket.binding(PHX_UPDATE);\n    const phxViewportTop = liveSocket.binding(PHX_VIEWPORT_TOP);\n    const phxViewportBottom = liveSocket.binding(PHX_VIEWPORT_BOTTOM);\n    const phxTriggerExternal = liveSocket.binding(PHX_TRIGGER_ACTION);\n    const added = [];\n    const updates = [];\n    const appendPrependUpdates = [];\n\n    // as the portal target itself could be at the end of the DOM,\n    // it may not be present while morphing previous parts;\n    // therefore we apply all teleports after the morphing is done+\n    let portalCallbacks = [];\n\n    let externalFormTriggered = null;\n\n    const morph = (\n      targetContainer,\n      source,\n      withChildren = this.withChildren,\n    ) => {\n      const morphCallbacks = {\n        // normally, we are running with childrenOnly, as the patch HTML for a LV\n        // does not include the LV attrs (data-phx-session, etc.)\n        // when we are patching a live component, we do want to patch the root element as well;\n        // another case is the recursive patch of a stream item that was kept on reset (-> onBeforeNodeAdded)\n        childrenOnly:\n          targetContainer.getAttribute(PHX_COMPONENT) === null && !withChildren,\n        getNodeKey: (node) => {\n          if (DOM.isPhxDestroyed(node)) {\n            return null;\n          }\n          // If we have a join patch, then by definition there was no PHX_MAGIC_ID.\n          // This is important to reduce the amount of elements morphdom discards.\n          if (isJoinPatch) {\n            return node.id;\n          }\n          return (\n            node.id || (node.getAttribute && node.getAttribute(PHX_MAGIC_ID))\n          );\n        },\n        // skip indexing from children when container is stream\n        skipFromChildren: (from) => {\n          return from.getAttribute(phxUpdate) === PHX_STREAM;\n        },\n        // tell morphdom how to add a child\n        addChild: (parent, child) => {\n          const { ref, streamAt } = this.getStreamInsert(child);\n          if (ref === undefined) {\n            return parent.appendChild(child);\n          }\n\n          this.setStreamRef(child, ref);\n\n          // streaming\n          if (streamAt === 0) {\n            parent.insertAdjacentElement(\"afterbegin\", child);\n          } else if (streamAt === -1) {\n            const lastChild = parent.lastElementChild;\n            if (lastChild && !lastChild.hasAttribute(PHX_STREAM_REF)) {\n              const nonStreamChild = Array.from(parent.children).find(\n                (c) => !c.hasAttribute(PHX_STREAM_REF),\n              );\n              parent.insertBefore(child, nonStreamChild);\n            } else {\n              parent.appendChild(child);\n            }\n          } else if (streamAt > 0) {\n            const sibling = Array.from(parent.children)[streamAt];\n            parent.insertBefore(child, sibling);\n          }\n        },\n        onBeforeNodeAdded: (el) => {\n          // don't add update_only nodes if they did not already exist\n          if (\n            this.getStreamInsert(el)?.updateOnly &&\n            !this.streamComponentRestore[el.id]\n          ) {\n            return false;\n          }\n\n          DOM.maintainPrivateHooks(el, el, phxViewportTop, phxViewportBottom);\n          this.trackBefore(\"added\", el);\n\n          let morphedEl = el;\n          // this is a stream item that was kept on reset, recursively morph it\n          if (this.streamComponentRestore[el.id]) {\n            morphedEl = this.streamComponentRestore[el.id];\n            delete this.streamComponentRestore[el.id];\n            morph(morphedEl, el, true);\n          }\n\n          return morphedEl;\n        },\n        onNodeAdded: (el) => {\n          if (el.getAttribute) {\n            this.maybeReOrderStream(el, true);\n          }\n          // phx-portal handling\n          if (DOM.isPortalTemplate(el)) {\n            portalCallbacks.push(() => this.teleport(el, morph));\n          }\n\n          // hack to fix Safari handling of img srcset and video tags\n          if (el instanceof HTMLImageElement && el.srcset) {\n            // eslint-disable-next-line no-self-assign\n            el.srcset = el.srcset;\n          } else if (el instanceof HTMLVideoElement && el.autoplay) {\n            el.play();\n          }\n          if (DOM.isNowTriggerFormExternal(el, phxTriggerExternal)) {\n            externalFormTriggered = el;\n          }\n\n          // nested view handling\n          if (\n            (DOM.isPhxChild(el) && view.ownsElement(el)) ||\n            (DOM.isPhxSticky(el) && view.ownsElement(el.parentNode))\n          ) {\n            this.trackAfter(\"phxChildAdded\", el);\n          }\n\n          // data-phx-runtime-hook\n          if (el.nodeName === \"SCRIPT\" && el.hasAttribute(PHX_RUNTIME_HOOK)) {\n            this.handleRuntimeHook(el, source);\n          }\n\n          added.push(el);\n        },\n        onNodeDiscarded: (el) => this.onNodeDiscarded(el),\n        onBeforeNodeDiscarded: (el) => {\n          if (el.getAttribute && el.getAttribute(PHX_PRUNE) !== null) {\n            return true;\n          }\n          if (\n            el.parentElement !== null &&\n            el.id &&\n            DOM.isPhxUpdate(el.parentElement, phxUpdate, [\n              PHX_STREAM,\n              \"append\",\n              \"prepend\",\n            ])\n          ) {\n            return false;\n          }\n          // don't remove teleported elements\n          if (el.getAttribute && el.getAttribute(PHX_TELEPORTED_REF)) {\n            return false;\n          }\n          if (this.maybePendingRemove(el)) {\n            return false;\n          }\n          if (this.skipCIDSibling(el)) {\n            return false;\n          }\n\n          if (DOM.isPortalTemplate(el)) {\n            // if the portal template itself is removed, remove the teleported element as well;\n            // we also perform a check after morphdom is finished to catch parent removals\n            const teleportedEl = document.getElementById(\n              el.content.firstElementChild.id,\n            );\n            if (teleportedEl) {\n              teleportedEl.remove();\n              morphCallbacks.onNodeDiscarded(teleportedEl);\n              this.view.dropPortalElementId(teleportedEl.id);\n            }\n          }\n\n          return true;\n        },\n        onElUpdated: (el) => {\n          if (DOM.isNowTriggerFormExternal(el, phxTriggerExternal)) {\n            externalFormTriggered = el;\n          }\n          updates.push(el);\n          this.maybeReOrderStream(el, false);\n        },\n        onBeforeElUpdated: (fromEl, toEl) => {\n          // if we are patching the root target container and the id has changed, treat it as a new node\n          // by replacing the fromEl with the toEl, which ensures hooks are torn down and re-created\n          if (\n            fromEl.id &&\n            fromEl.isSameNode(targetContainer) &&\n            fromEl.id !== toEl.id\n          ) {\n            morphCallbacks.onNodeDiscarded(fromEl);\n            fromEl.replaceWith(toEl);\n            return morphCallbacks.onNodeAdded(toEl);\n          }\n          DOM.syncPendingAttrs(fromEl, toEl);\n          DOM.maintainPrivateHooks(\n            fromEl,\n            toEl,\n            phxViewportTop,\n            phxViewportBottom,\n          );\n          DOM.cleanChildNodes(toEl, phxUpdate);\n          if (this.skipCIDSibling(toEl)) {\n            // if this is a live component used in a stream, we may need to reorder it\n            this.maybeReOrderStream(fromEl);\n            return false;\n          }\n          if (DOM.isPhxSticky(fromEl)) {\n            [PHX_SESSION, PHX_STATIC, PHX_ROOT_ID]\n              .map((attr) => [\n                attr,\n                fromEl.getAttribute(attr),\n                toEl.getAttribute(attr),\n              ])\n              .forEach(([attr, fromVal, toVal]) => {\n                if (toVal && fromVal !== toVal) {\n                  fromEl.setAttribute(attr, toVal);\n                }\n              });\n\n            return false;\n          }\n          if (\n            DOM.isIgnored(fromEl, phxUpdate) ||\n            (fromEl.form && fromEl.form.isSameNode(externalFormTriggered))\n          ) {\n            this.trackBefore(\"updated\", fromEl, toEl);\n            DOM.mergeAttrs(fromEl, toEl, {\n              isIgnored: DOM.isIgnored(fromEl, phxUpdate),\n            });\n            updates.push(fromEl);\n            DOM.applyStickyOperations(fromEl);\n            return false;\n          }\n          if (\n            fromEl.type === \"number\" &&\n            fromEl.validity &&\n            fromEl.validity.badInput\n          ) {\n            return false;\n          }\n          // If the element has PHX_REF_SRC, it is loading or locked and awaiting an ack.\n          // If it's locked, we clone the fromEl tree and instruct morphdom to use\n          // the cloned tree as the source of the morph for this branch from here on out.\n          // We keep a reference to the cloned tree in the element's private data, and\n          // on ack (view.undoRefs), we morph the cloned tree with the true fromEl in the DOM to\n          // apply any changes that happened while the element was locked.\n          const isFocusedFormEl =\n            focused && fromEl.isSameNode(focused) && DOM.isFormInput(fromEl);\n          const focusedSelectChanged =\n            isFocusedFormEl && this.isChangedSelect(fromEl, toEl);\n          if (fromEl.hasAttribute(PHX_REF_SRC)) {\n            const ref = new ElementRef(fromEl);\n            // only perform the clone step if this is not a patch that unlocks\n            if (\n              ref.lockRef &&\n              (!this.undoRef || !ref.isLockUndoneBy(this.undoRef))\n            ) {\n              DOM.applyStickyOperations(fromEl);\n              const isLocked = fromEl.hasAttribute(PHX_REF_LOCK);\n              const clone = isLocked\n                ? DOM.private(fromEl, PHX_REF_LOCK) || fromEl.cloneNode(true)\n                : null;\n              if (clone) {\n                DOM.putPrivate(fromEl, PHX_REF_LOCK, clone);\n                if (!isFocusedFormEl) {\n                  fromEl = clone;\n                }\n              }\n            }\n          }\n\n          // nested view handling\n          if (DOM.isPhxChild(toEl)) {\n            const prevSession = fromEl.getAttribute(PHX_SESSION);\n            DOM.mergeAttrs(fromEl, toEl, { exclude: [PHX_STATIC] });\n            if (prevSession !== \"\") {\n              fromEl.setAttribute(PHX_SESSION, prevSession);\n            }\n            fromEl.setAttribute(PHX_ROOT_ID, this.rootID);\n            DOM.applyStickyOperations(fromEl);\n            return false;\n          }\n\n          // if we are undoing a lock, copy potentially nested clones over\n          if (this.undoRef && DOM.private(toEl, PHX_REF_LOCK)) {\n            DOM.putPrivate(\n              fromEl,\n              PHX_REF_LOCK,\n              DOM.private(toEl, PHX_REF_LOCK),\n            );\n          }\n          // now copy regular DOM.private data\n          DOM.copyPrivates(toEl, fromEl);\n\n          // phx-portal handling\n          if (DOM.isPortalTemplate(toEl)) {\n            portalCallbacks.push(() => this.teleport(toEl, morph));\n            // for the magicId optimization we need to ensure that the template contents\n            // are properly updated as they are used when restoring a cloned tree\n            // Note: we can't write fromEl.innerHTML = toEl.innerHTML because in Chrome\n            // the HTML parser would drop nested forms, even when it should not.\n            // https://issues.chromium.org/issues/490290430\n            fromEl.content.replaceChildren(toEl.content.cloneNode(true));\n            return false;\n          }\n\n          // skip patching focused inputs unless focus is a select that has changed options\n          if (\n            isFocusedFormEl &&\n            fromEl.type !== \"hidden\" &&\n            !focusedSelectChanged\n          ) {\n            this.trackBefore(\"updated\", fromEl, toEl);\n            DOM.mergeFocusedInput(fromEl, toEl);\n            DOM.syncAttrsToProps(fromEl);\n            updates.push(fromEl);\n            DOM.applyStickyOperations(fromEl);\n            return false;\n          } else {\n            // blur focused select if it changed so native UI is updated (ie safari won't update visible options)\n            if (focusedSelectChanged) {\n              fromEl.blur();\n            }\n            if (DOM.isPhxUpdate(toEl, phxUpdate, [\"append\", \"prepend\"])) {\n              appendPrependUpdates.push(\n                new DOMPostMorphRestorer(\n                  fromEl,\n                  toEl,\n                  toEl.getAttribute(phxUpdate),\n                ),\n              );\n            }\n\n            DOM.syncAttrsToProps(toEl);\n            DOM.applyStickyOperations(toEl);\n            this.trackBefore(\"updated\", fromEl, toEl);\n            return fromEl;\n          }\n        },\n      };\n\n      morphdom(targetContainer, source, morphCallbacks);\n    };\n\n    this.trackBefore(\"added\", container);\n    this.trackBefore(\"updated\", container, container);\n\n    liveSocket.time(\"morphdom\", () => {\n      this.streams.forEach(([ref, inserts, deleteIds, reset]) => {\n        inserts.forEach(([key, streamAt, limit, updateOnly]) => {\n          this.streamInserts[key] = { ref, streamAt, limit, reset, updateOnly };\n        });\n        if (reset !== undefined) {\n          DOM.all(document, `[${PHX_STREAM_REF}=\"${ref}\"]`, (child) => {\n            this.removeStreamChildElement(child);\n          });\n        }\n        deleteIds.forEach((id) => {\n          const child = document.getElementById(id);\n          if (child) {\n            this.removeStreamChildElement(child);\n          }\n        });\n      });\n\n      // clear stream items from the dead render if they are not inserted again\n      if (isJoinPatch) {\n        DOM.all(this.container, `[${phxUpdate}=${PHX_STREAM}]`)\n          // it is important to filter the element before removing them, as\n          // it may happen that streams are nested and the owner check fails if\n          // a parent is removed before a child\n          .filter((el) => this.view.ownsElement(el))\n          .forEach((el) => {\n            Array.from(el.children).forEach((child) => {\n              // we already performed the owner check, each child is guaranteed to be owned\n              // by the view. To prevent the nested owner check from failing in case of nested\n              // streams where the parent is removed before the child, we force the removal\n              this.removeStreamChildElement(child, true);\n            });\n          });\n      }\n\n      morph(targetContainer, html);\n\n      // normal patch complete, teleport elements now\n      // and handle nested teleportation up to depth 5\n      let teleportCount = 0;\n      while (portalCallbacks.length > 0 && teleportCount < 5) {\n        const copy = portalCallbacks.slice();\n        portalCallbacks = [];\n        copy.forEach((callback) => callback());\n        teleportCount++;\n      }\n\n      // check for any teleported elements that are not in the view any more\n      // and remove them\n      this.view.portalElementIds.forEach((id) => {\n        const el = document.getElementById(id);\n        if (el) {\n          const source = document.getElementById(\n            el.getAttribute(PHX_TELEPORTED_SRC),\n          );\n          if (!source) {\n            el.remove();\n            this.onNodeDiscarded(el);\n            this.view.dropPortalElementId(id);\n          }\n        }\n      });\n    });\n\n    if (liveSocket.isDebugEnabled()) {\n      detectDuplicateIds();\n      detectInvalidStreamInserts(this.streamInserts);\n      // warn if there are any inputs named \"id\"\n      Array.from(document.querySelectorAll(\"input[name=id]\")).forEach(\n        (node) => {\n          if (node instanceof HTMLInputElement && node.form) {\n            console.error(\n              'Detected an input with name=\"id\" inside a form! This will cause problems when patching the DOM.\\n',\n              node,\n            );\n          }\n        },\n      );\n    }\n\n    if (appendPrependUpdates.length > 0) {\n      liveSocket.time(\"post-morph append/prepend restoration\", () => {\n        appendPrependUpdates.forEach((update) => update.perform());\n      });\n    }\n\n    liveSocket.silenceEvents(() =>\n      DOM.restoreFocus(focused, selectionStart, selectionEnd),\n    );\n    DOM.dispatchEvent(document, \"phx:update\");\n    added.forEach((el) => this.trackAfter(\"added\", el));\n    updates.forEach((el) => this.trackAfter(\"updated\", el));\n\n    this.transitionPendingRemoves();\n\n    if (externalFormTriggered) {\n      liveSocket.unload();\n      // check for submitter and inject it as hidden input for external submit;\n      // In theory, it could happen that the stored submitter is outdated and doesn't\n      // exist in the DOM any more, but this is unlikely, so we just accept it for now.\n      const submitter = DOM.private(externalFormTriggered, \"submitter\");\n      if (submitter && submitter.name && targetContainer.contains(submitter)) {\n        const input = document.createElement(\"input\");\n        input.type = \"hidden\";\n        const formId = submitter.getAttribute(\"form\");\n        if (formId) {\n          input.setAttribute(\"form\", formId);\n        }\n        input.name = submitter.name;\n        input.value = submitter.value;\n        submitter.parentElement.insertBefore(input, submitter);\n      }\n      // use prototype's submit in case there's a form control with name or id of \"submit\"\n      // https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit\n      Object.getPrototypeOf(externalFormTriggered).submit.call(\n        externalFormTriggered,\n      );\n    }\n    return true;\n  }\n\n  onNodeDiscarded(el) {\n    // nested view handling\n    if (DOM.isPhxChild(el) || DOM.isPhxSticky(el)) {\n      this.liveSocket.destroyViewByEl(el);\n    }\n    this.trackAfter(\"discarded\", el);\n  }\n\n  maybePendingRemove(node) {\n    if (node.getAttribute && node.getAttribute(this.phxRemove) !== null) {\n      this.pendingRemoves.push(node);\n      return true;\n    } else {\n      return false;\n    }\n  }\n\n  removeStreamChildElement(child, force = false) {\n    // make sure to only remove elements owned by the current view\n    // see https://github.com/phoenixframework/phoenix_live_view/issues/3047\n    // and https://github.com/phoenixframework/phoenix_live_view/issues/3681\n    if (!force && !this.view.ownsElement(child)) {\n      return;\n    }\n\n    // we need to store the node if it is actually re-added in the same patch\n    // we do NOT want to execute phx-remove, we do NOT want to call onNodeDiscarded\n    if (this.streamInserts[child.id]) {\n      this.streamComponentRestore[child.id] = child;\n      child.remove();\n    } else {\n      // only remove the element now if it has no phx-remove binding\n      if (!this.maybePendingRemove(child)) {\n        child.remove();\n        this.onNodeDiscarded(child);\n      }\n    }\n  }\n\n  getStreamInsert(el) {\n    const insert = el.id ? this.streamInserts[el.id] : {};\n    return insert || {};\n  }\n\n  setStreamRef(el, ref) {\n    DOM.putSticky(el, PHX_STREAM_REF, (el) =>\n      el.setAttribute(PHX_STREAM_REF, ref),\n    );\n  }\n\n  maybeReOrderStream(el, isNew) {\n    const { ref, streamAt, reset } = this.getStreamInsert(el);\n    if (streamAt === undefined) {\n      return;\n    }\n\n    // we need to set the PHX_STREAM_REF here as well as addChild is invoked only for parents\n    this.setStreamRef(el, ref);\n\n    if (!reset && !isNew) {\n      // we only reorder if the element is new or it's a stream reset\n      return;\n    }\n\n    // check if the element has a parent element;\n    // it doesn't if we are currently recursively morphing (restoring a saved stream child)\n    // because the element is not yet added to the real dom;\n    // reordering does not make sense in that case anyway\n    if (!el.parentElement) {\n      return;\n    }\n\n    if (streamAt === 0) {\n      el.parentElement.insertBefore(el, el.parentElement.firstElementChild);\n    } else if (streamAt > 0) {\n      const children = Array.from(el.parentElement.children);\n      const oldIndex = children.indexOf(el);\n      if (streamAt >= children.length - 1) {\n        el.parentElement.appendChild(el);\n      } else {\n        const sibling = children[streamAt];\n        if (oldIndex > streamAt) {\n          el.parentElement.insertBefore(el, sibling);\n        } else {\n          el.parentElement.insertBefore(el, sibling.nextElementSibling);\n        }\n      }\n    }\n\n    this.maybeLimitStream(el);\n  }\n\n  maybeLimitStream(el) {\n    const { limit } = this.getStreamInsert(el);\n    const children = limit !== null && Array.from(el.parentElement.children);\n    if (limit && limit < 0 && children.length > limit * -1) {\n      children\n        .slice(0, children.length + limit)\n        .forEach((child) => this.removeStreamChildElement(child));\n    } else if (limit && limit >= 0 && children.length > limit) {\n      children\n        .slice(limit)\n        .forEach((child) => this.removeStreamChildElement(child));\n    }\n  }\n\n  transitionPendingRemoves() {\n    const { pendingRemoves, liveSocket } = this;\n    if (pendingRemoves.length > 0) {\n      liveSocket.transitionRemoves(pendingRemoves, () => {\n        pendingRemoves.forEach((el) => {\n          const child = DOM.firstPhxChild(el);\n          if (child) {\n            liveSocket.destroyViewByEl(child);\n          }\n          el.remove();\n        });\n        this.trackAfter(\"transitionsDiscarded\", pendingRemoves);\n      });\n    }\n  }\n\n  isChangedSelect(fromEl, toEl) {\n    if (!(fromEl instanceof HTMLSelectElement) || fromEl.multiple) {\n      return false;\n    }\n    if (fromEl.options.length !== toEl.options.length) {\n      return true;\n    }\n\n    // keep the current value\n    toEl.value = fromEl.value;\n\n    // in general we have to be very careful with using isEqualNode as it does not a reliable\n    // DOM tree equality check, but for selection attributes and options it works fine\n    return !fromEl.isEqualNode(toEl);\n  }\n\n  isCIDPatch() {\n    return this.cidPatch;\n  }\n\n  skipCIDSibling(el) {\n    return el.nodeType === Node.ELEMENT_NODE && el.hasAttribute(PHX_SKIP);\n  }\n\n  targetCIDContainer(html) {\n    if (!this.isCIDPatch()) {\n      return;\n    }\n    const [first, ...rest] = DOM.findComponentNodeList(\n      this.view.id,\n      this.targetCID,\n    );\n    if (rest.length === 0 && DOM.childNodeLength(html) === 1) {\n      return first;\n    } else {\n      return first && first.parentNode;\n    }\n  }\n\n  indexOf(parent, child) {\n    return Array.from(parent.children).indexOf(child);\n  }\n\n  teleport(el, morph) {\n    const targetSelector = el.getAttribute(PHX_PORTAL);\n    const portalContainer = document.querySelector(targetSelector);\n    if (!portalContainer) {\n      throw new Error(\n        \"portal target with selector \" + targetSelector + \" not found\",\n      );\n    }\n    // phx-portal templates must have a single root element, so we assume this to be\n    // the case here\n    const toTeleport = el.content.firstElementChild;\n    // the PHX_SKIP optimization can also apply inside of the <template> elements\n    if (this.skipCIDSibling(toTeleport)) {\n      return;\n    }\n    if (!toTeleport?.id) {\n      throw new Error(\n        \"phx-portal template must have a single root element with ID!\",\n      );\n    }\n    const existing = document.getElementById(toTeleport.id);\n    let portalTarget;\n    if (existing) {\n      // check if the element needs to be moved to another target\n      if (!portalContainer.contains(existing)) {\n        portalContainer.appendChild(existing);\n      }\n      // we already teleported in a previous patch\n      portalTarget = existing;\n    } else {\n      // create empty target and morph it recursively\n      portalTarget = document.createElement(toTeleport.tagName);\n      portalContainer.appendChild(portalTarget);\n    }\n    // mark the target as teleported;\n    // to prevent unnecessary attribute modifications, we set the attribute\n    // on the source and remove it after morphing (we could also just keep it)\n    // otherwise morphdom would remove it, as the ref is not present in the source\n    // and we'd need to set it back after each morph\n    toTeleport.setAttribute(PHX_TELEPORTED_REF, this.view.id);\n    toTeleport.setAttribute(PHX_TELEPORTED_SRC, el.id);\n    morph(portalTarget, toTeleport, true);\n    toTeleport.removeAttribute(PHX_TELEPORTED_REF);\n    toTeleport.removeAttribute(PHX_TELEPORTED_SRC);\n    // store a reference to the teleported element in the view\n    // to cleanup when the view is destroyed, in case the portal target\n    // is outside the view itself\n    this.view.pushPortalElementId(toTeleport.id);\n  }\n\n  handleRuntimeHook(el, source) {\n    // usually, scripts are not executed when morphdom adds them to the DOM\n    // we special case runtime colocated hooks\n    const name = el.getAttribute(PHX_RUNTIME_HOOK);\n    let nonce = el.hasAttribute(\"nonce\") ? el.getAttribute(\"nonce\") : null;\n    if (el.hasAttribute(\"nonce\")) {\n      const template = document.createElement(\"template\");\n      template.innerHTML = source;\n      nonce = template.content\n        .querySelector(`script[${PHX_RUNTIME_HOOK}=\"${CSS.escape(name)}\"]`)\n        .getAttribute(\"nonce\");\n    }\n    const script = document.createElement(\"script\");\n    script.textContent = el.textContent;\n    DOM.mergeAttrs(script, el, { isIgnored: false });\n    if (nonce) {\n      script.nonce = nonce;\n    }\n    el.replaceWith(script);\n    el = script;\n  }\n}\n"
  },
  {
    "path": "assets/js/phoenix_live_view/dom_post_morph_restorer.js",
    "content": "import { maybe } from \"./utils\";\n\nimport DOM from \"./dom\";\n\nexport default class DOMPostMorphRestorer {\n  constructor(containerBefore, containerAfter, updateType) {\n    const idsBefore = new Set();\n    const idsAfter = new Set(\n      [...containerAfter.children].map((child) => child.id),\n    );\n\n    const elementsToModify = [];\n\n    Array.from(containerBefore.children).forEach((child) => {\n      if (child.id) {\n        // all of our children should be elements with ids\n        idsBefore.add(child.id);\n        if (idsAfter.has(child.id)) {\n          const previousElementId =\n            child.previousElementSibling && child.previousElementSibling.id;\n          elementsToModify.push({\n            elementId: child.id,\n            previousElementId: previousElementId,\n          });\n        }\n      }\n    });\n\n    this.containerId = containerAfter.id;\n    this.updateType = updateType;\n    this.elementsToModify = elementsToModify;\n    this.elementIdsToAdd = [...idsAfter].filter((id) => !idsBefore.has(id));\n  }\n\n  // We do the following to optimize append/prepend operations:\n  //   1) Track ids of modified elements & of new elements\n  //   2) All the modified elements are put back in the correct position in the DOM tree\n  //      by storing the id of their previous sibling\n  //   3) New elements are going to be put in the right place by morphdom during append.\n  //      For prepend, we move them to the first position in the container\n  perform() {\n    const container = DOM.byId(this.containerId);\n    if (!container) {\n      return;\n    }\n    this.elementsToModify.forEach((elementToModify) => {\n      if (elementToModify.previousElementId) {\n        maybe(\n          document.getElementById(elementToModify.previousElementId),\n          (previousElem) => {\n            maybe(\n              document.getElementById(elementToModify.elementId),\n              (elem) => {\n                const isInRightPlace =\n                  elem.previousElementSibling &&\n                  elem.previousElementSibling.id == previousElem.id;\n                if (!isInRightPlace) {\n                  previousElem.insertAdjacentElement(\"afterend\", elem);\n                }\n              },\n            );\n          },\n        );\n      } else {\n        // This is the first element in the container\n        maybe(document.getElementById(elementToModify.elementId), (elem) => {\n          const isInRightPlace = elem.previousElementSibling == null;\n          if (!isInRightPlace) {\n            container.insertAdjacentElement(\"afterbegin\", elem);\n          }\n        });\n      }\n    });\n\n    if (this.updateType == \"prepend\") {\n      this.elementIdsToAdd.reverse().forEach((elemId) => {\n        maybe(document.getElementById(elemId), (elem) =>\n          container.insertAdjacentElement(\"afterbegin\", elem),\n        );\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "assets/js/phoenix_live_view/element_ref.js",
    "content": "import {\n  PHX_REF_LOADING,\n  PHX_REF_LOCK,\n  PHX_REF_SRC,\n  PHX_PENDING_REFS,\n  PHX_EVENT_CLASSES,\n  PHX_DISABLED,\n  PHX_READONLY,\n  PHX_DISABLE_WITH_RESTORE,\n} from \"./constants\";\n\nimport DOM from \"./dom\";\n\nexport default class ElementRef {\n  static onUnlock(el, callback) {\n    if (!DOM.isLocked(el) && !el.closest(`[${PHX_REF_LOCK}]`)) {\n      return callback();\n    }\n    const closestLock = el.closest(`[${PHX_REF_LOCK}]`);\n    const ref = closestLock\n      .closest(`[${PHX_REF_LOCK}]`)\n      .getAttribute(PHX_REF_LOCK);\n    closestLock.addEventListener(\n      `phx:undo-lock:${ref}`,\n      () => {\n        callback();\n      },\n      { once: true },\n    );\n  }\n\n  constructor(el) {\n    this.el = el;\n    this.loadingRef = el.hasAttribute(PHX_REF_LOADING)\n      ? parseInt(el.getAttribute(PHX_REF_LOADING), 10)\n      : null;\n    this.lockRef = el.hasAttribute(PHX_REF_LOCK)\n      ? parseInt(el.getAttribute(PHX_REF_LOCK), 10)\n      : null;\n  }\n\n  // public\n\n  maybeUndo(ref, phxEvent, eachCloneCallback) {\n    if (!this.isWithin(ref)) {\n      // we cannot undo the lock / loading now, as there is a newer one already set;\n      // we need to store the original ref we tried to send the undo event later\n      DOM.updatePrivate(this.el, PHX_PENDING_REFS, [], (pendingRefs) => {\n        pendingRefs.push(ref);\n        return pendingRefs;\n      });\n      return;\n    }\n\n    // undo locks and apply clones\n    this.undoLocks(ref, phxEvent, eachCloneCallback);\n\n    // undo loading states\n    this.undoLoading(ref, phxEvent);\n\n    // ensure undo events are fired for pending refs that\n    // are resolved by the current ref, otherwise we'd leak event listeners\n    DOM.updatePrivate(this.el, PHX_PENDING_REFS, [], (pendingRefs) => {\n      return pendingRefs.filter((pendingRef) => {\n        let opts = {\n          detail: { ref: pendingRef, event: phxEvent },\n          bubbles: true,\n          cancelable: false,\n        };\n        if (this.loadingRef && this.loadingRef > pendingRef) {\n          this.el.dispatchEvent(\n            new CustomEvent(`phx:undo-loading:${pendingRef}`, opts),\n          );\n        }\n        if (this.lockRef && this.lockRef > pendingRef) {\n          this.el.dispatchEvent(\n            new CustomEvent(`phx:undo-lock:${pendingRef}`, opts),\n          );\n        }\n        return pendingRef > ref;\n      });\n    });\n\n    // clean up if fully resolved\n    if (this.isFullyResolvedBy(ref)) {\n      this.el.removeAttribute(PHX_REF_SRC);\n    }\n  }\n\n  // private\n\n  isWithin(ref) {\n    return !(\n      this.loadingRef !== null &&\n      this.loadingRef > ref &&\n      this.lockRef !== null &&\n      this.lockRef > ref\n    );\n  }\n\n  // Check for cloned PHX_REF_LOCK element that has been morphed behind\n  // the scenes while this element was locked in the DOM.\n  // When we apply the cloned tree to the active DOM element, we must\n  //\n  //   1. execute pending mounted hooks for nodes now in the DOM\n  //   2. undo any ref inside the cloned tree that has since been ack'd\n  undoLocks(ref, phxEvent, eachCloneCallback) {\n    if (!this.isLockUndoneBy(ref)) {\n      return;\n    }\n\n    const clonedTree = DOM.private(this.el, PHX_REF_LOCK);\n    if (clonedTree) {\n      eachCloneCallback(clonedTree);\n      DOM.deletePrivate(this.el, PHX_REF_LOCK);\n    }\n    this.el.removeAttribute(PHX_REF_LOCK);\n\n    const opts = {\n      detail: { ref: ref, event: phxEvent },\n      bubbles: true,\n      cancelable: false,\n    };\n    this.el.dispatchEvent(\n      new CustomEvent(`phx:undo-lock:${this.lockRef}`, opts),\n    );\n  }\n\n  undoLoading(ref, phxEvent) {\n    if (!this.isLoadingUndoneBy(ref)) {\n      if (\n        this.canUndoLoading(ref) &&\n        this.el.classList.contains(\"phx-submit-loading\")\n      ) {\n        this.el.classList.remove(\"phx-change-loading\");\n      }\n      return;\n    }\n\n    if (this.canUndoLoading(ref)) {\n      this.el.removeAttribute(PHX_REF_LOADING);\n      const disabledVal = this.el.getAttribute(PHX_DISABLED);\n      const readOnlyVal = this.el.getAttribute(PHX_READONLY);\n      // restore inputs\n      if (readOnlyVal !== null) {\n        this.el.readOnly = readOnlyVal === \"true\" ? true : false;\n        this.el.removeAttribute(PHX_READONLY);\n      }\n      if (disabledVal !== null) {\n        this.el.disabled = disabledVal === \"true\" ? true : false;\n        this.el.removeAttribute(PHX_DISABLED);\n      }\n      // restore disables\n      const disableRestore = this.el.getAttribute(PHX_DISABLE_WITH_RESTORE);\n      if (disableRestore !== null) {\n        this.el.textContent = disableRestore;\n        this.el.removeAttribute(PHX_DISABLE_WITH_RESTORE);\n      }\n\n      const opts = {\n        detail: { ref: ref, event: phxEvent },\n        bubbles: true,\n        cancelable: false,\n      };\n      this.el.dispatchEvent(\n        new CustomEvent(`phx:undo-loading:${this.loadingRef}`, opts),\n      );\n    }\n\n    // remove classes\n    PHX_EVENT_CLASSES.forEach((name) => {\n      if (name !== \"phx-submit-loading\" || this.canUndoLoading(ref)) {\n        DOM.removeClass(this.el, name);\n      }\n    });\n  }\n\n  isLoadingUndoneBy(ref) {\n    return this.loadingRef === null ? false : this.loadingRef <= ref;\n  }\n  isLockUndoneBy(ref) {\n    return this.lockRef === null ? false : this.lockRef <= ref;\n  }\n\n  isFullyResolvedBy(ref) {\n    return (\n      (this.loadingRef === null || this.loadingRef <= ref) &&\n      (this.lockRef === null || this.lockRef <= ref)\n    );\n  }\n\n  // only remove the phx-submit-loading class if we are not locked\n  canUndoLoading(ref) {\n    return this.lockRef === null || this.lockRef <= ref;\n  }\n}\n"
  },
  {
    "path": "assets/js/phoenix_live_view/entry_uploader.js",
    "content": "import { logError } from \"./utils\";\n\nexport default class EntryUploader {\n  constructor(entry, config, liveSocket) {\n    const { chunk_size, chunk_timeout } = config;\n    this.liveSocket = liveSocket;\n    this.entry = entry;\n    this.offset = 0;\n    this.chunkSize = chunk_size;\n    this.chunkTimeout = chunk_timeout;\n    this.chunkTimer = null;\n    this.errored = false;\n    this.uploadChannel = liveSocket.channel(`lvu:${entry.ref}`, {\n      token: entry.metadata(),\n    });\n  }\n\n  error(reason) {\n    if (this.errored) {\n      return;\n    }\n    this.uploadChannel.leave();\n    this.errored = true;\n    clearTimeout(this.chunkTimer);\n    this.entry.error(reason);\n  }\n\n  upload() {\n    this.uploadChannel.onError((reason) => this.error(reason));\n    this.uploadChannel\n      .join()\n      .receive(\"ok\", (_data) => this.readNextChunk())\n      .receive(\"error\", (reason) => this.error(reason));\n  }\n\n  isDone() {\n    return this.offset >= this.entry.file.size;\n  }\n\n  readNextChunk() {\n    const reader = new window.FileReader();\n    const blob = this.entry.file.slice(\n      this.offset,\n      this.chunkSize + this.offset,\n    );\n    reader.onload = (e) => {\n      if (e.target.error === null) {\n        this.offset += /** @type {ArrayBuffer} */ (e.target.result).byteLength;\n        this.pushChunk(/** @type {ArrayBuffer} */ (e.target.result));\n      } else {\n        return logError(\"Read error: \" + e.target.error);\n      }\n    };\n    reader.readAsArrayBuffer(blob);\n  }\n\n  pushChunk(chunk) {\n    if (!this.uploadChannel.isJoined()) {\n      return;\n    }\n    this.uploadChannel\n      .push(\"chunk\", chunk, this.chunkTimeout)\n      .receive(\"ok\", () => {\n        this.entry.progress((this.offset / this.entry.file.size) * 100);\n        if (!this.isDone()) {\n          this.chunkTimer = setTimeout(\n            () => this.readNextChunk(),\n            this.liveSocket.getLatencySim() || 0,\n          );\n        }\n      })\n      .receive(\"error\", ({ reason }) => this.error(reason));\n  }\n}\n"
  },
  {
    "path": "assets/js/phoenix_live_view/global.d.ts",
    "content": "declare let LV_VSN: string;\n"
  },
  {
    "path": "assets/js/phoenix_live_view/hooks.js",
    "content": "import {\n  PHX_ACTIVE_ENTRY_REFS,\n  PHX_LIVE_FILE_UPDATED,\n  PHX_PREFLIGHTED_REFS,\n  PHX_UPLOAD_REF,\n  PHX_VIEWPORT_OVERRUN_TARGET,\n} from \"./constants\";\n\nimport LiveUploader from \"./live_uploader\";\nimport ARIA from \"./aria\";\n\nconst Hooks = {\n  LiveFileUpload: {\n    activeRefs() {\n      return this.el.getAttribute(PHX_ACTIVE_ENTRY_REFS);\n    },\n\n    preflightedRefs() {\n      return this.el.getAttribute(PHX_PREFLIGHTED_REFS);\n    },\n\n    mounted() {\n      this.js().ignoreAttributes(this.el, [\"value\"]);\n      this.preflightedWas = this.preflightedRefs();\n    },\n\n    updated() {\n      const newPreflights = this.preflightedRefs();\n      if (this.preflightedWas !== newPreflights) {\n        this.preflightedWas = newPreflights;\n        if (newPreflights === \"\") {\n          this.__view().cancelSubmit(this.el.form);\n        }\n      }\n\n      if (this.activeRefs() === \"\") {\n        this.el.value = null;\n      }\n      this.el.dispatchEvent(new CustomEvent(PHX_LIVE_FILE_UPDATED));\n    },\n  },\n\n  LiveImgPreview: {\n    mounted() {\n      this.ref = this.el.getAttribute(\"data-phx-entry-ref\");\n      this.inputEl = document.getElementById(\n        this.el.getAttribute(PHX_UPLOAD_REF),\n      );\n      LiveUploader.getEntryDataURL(this.inputEl, this.ref, (url) => {\n        this.url = url;\n        this.el.src = url;\n      });\n    },\n    destroyed() {\n      URL.revokeObjectURL(this.url);\n    },\n  },\n  FocusWrap: {\n    mounted() {\n      this.focusStart = this.el.firstElementChild;\n      this.focusEnd = this.el.lastElementChild;\n      this.focusStart.addEventListener(\"focus\", (e) => {\n        if (!e.relatedTarget || !this.el.contains(e.relatedTarget)) {\n          // Handle focus entering from outside (e.g. Tab when body is focused)\n          // https://github.com/phoenixframework/phoenix_live_view/issues/3636\n          const nextFocus = e.target.nextElementSibling;\n          ARIA.attemptFocus(nextFocus) || ARIA.focusFirst(nextFocus);\n        } else {\n          ARIA.focusLast(this.el);\n        }\n      });\n      this.focusEnd.addEventListener(\"focus\", (e) => {\n        if (!e.relatedTarget || !this.el.contains(e.relatedTarget)) {\n          // Handle focus entering from outside (e.g. Shift+Tab when body is focused)\n          // https://github.com/phoenixframework/phoenix_live_view/issues/3636\n          const nextFocus = e.target.previousElementSibling;\n          ARIA.attemptFocus(nextFocus) || ARIA.focusLast(nextFocus);\n        } else {\n          ARIA.focusFirst(this.el);\n        }\n      });\n      // only try to change the focus if it is not already inside\n      if (!this.el.contains(document.activeElement)) {\n        this.el.addEventListener(\"phx:show-end\", () => this.el.focus());\n        if (window.getComputedStyle(this.el).display !== \"none\") {\n          ARIA.focusFirst(this.el);\n        }\n      }\n    },\n  },\n};\n\nconst findScrollContainer = (el) => {\n  // the scroll event won't be fired on the html/body element even if overflow is set\n  // therefore we return null to instead listen for scroll events on document\n  if ([\"HTML\", \"BODY\"].indexOf(el.nodeName.toUpperCase()) >= 0) return null;\n  if ([\"scroll\", \"auto\"].indexOf(getComputedStyle(el).overflowY) >= 0)\n    return el;\n  return findScrollContainer(el.parentElement);\n};\n\nconst scrollTop = (scrollContainer) => {\n  if (scrollContainer) {\n    return scrollContainer.scrollTop;\n  } else {\n    return document.documentElement.scrollTop || document.body.scrollTop;\n  }\n};\n\nconst bottom = (scrollContainer) => {\n  if (scrollContainer) {\n    return scrollContainer.getBoundingClientRect().bottom;\n  } else {\n    // when we have no container, the whole page scrolls,\n    // therefore the bottom coordinate is the viewport height\n    return window.innerHeight || document.documentElement.clientHeight;\n  }\n};\n\nconst top = (scrollContainer) => {\n  if (scrollContainer) {\n    return scrollContainer.getBoundingClientRect().top;\n  } else {\n    // when we have no container the whole page scrolls,\n    // therefore the top coordinate is 0\n    return 0;\n  }\n};\n\nconst isAtViewportTop = (el, scrollContainer) => {\n  const rect = el.getBoundingClientRect();\n  return (\n    Math.ceil(rect.top) >= top(scrollContainer) &&\n    Math.ceil(rect.left) >= 0 &&\n    Math.floor(rect.top) <= bottom(scrollContainer)\n  );\n};\n\nconst isAtViewportBottom = (el, scrollContainer) => {\n  const rect = el.getBoundingClientRect();\n  return (\n    Math.ceil(rect.bottom) >= top(scrollContainer) &&\n    Math.ceil(rect.left) >= 0 &&\n    Math.floor(rect.bottom) <= bottom(scrollContainer)\n  );\n};\n\nconst isWithinViewport = (el, scrollContainer) => {\n  const rect = el.getBoundingClientRect();\n  return (\n    Math.ceil(rect.top) >= top(scrollContainer) &&\n    Math.ceil(rect.left) >= 0 &&\n    Math.floor(rect.top) <= bottom(scrollContainer)\n  );\n};\n\nHooks.InfiniteScroll = {\n  mounted() {\n    this.scrollContainer = findScrollContainer(this.el);\n    let scrollBefore = scrollTop(this.scrollContainer);\n    let topOverran = false;\n    const throttleInterval = 500;\n    let pendingOp = null;\n\n    const onTopOverrun = this.throttle(\n      throttleInterval,\n      (topEvent, firstChild) => {\n        pendingOp = () => true;\n        this.liveSocket.js().push(this.el, topEvent, {\n          value: { id: firstChild.id, _overran: true },\n          callback: () => {\n            pendingOp = null;\n          },\n        });\n      },\n    );\n\n    const onFirstChildAtTop = this.throttle(\n      throttleInterval,\n      (topEvent, firstChild) => {\n        pendingOp = () => firstChild.scrollIntoView({ block: \"start\" });\n        this.liveSocket.js().push(this.el, topEvent, {\n          value: { id: firstChild.id },\n          callback: () => {\n            pendingOp = null;\n            // make sure that the DOM is patched by waiting for the next tick\n            window.requestAnimationFrame(() => {\n              if (!isWithinViewport(firstChild, this.scrollContainer)) {\n                firstChild.scrollIntoView({ block: \"start\" });\n              }\n            });\n          },\n        });\n      },\n    );\n\n    const onLastChildAtBottom = this.throttle(\n      throttleInterval,\n      (bottomEvent, lastChild) => {\n        pendingOp = () => lastChild.scrollIntoView({ block: \"end\" });\n        this.liveSocket.js().push(this.el, bottomEvent, {\n          value: { id: lastChild.id },\n          callback: () => {\n            pendingOp = null;\n            // make sure that the DOM is patched by waiting for the next tick\n            window.requestAnimationFrame(() => {\n              if (!isWithinViewport(lastChild, this.scrollContainer)) {\n                lastChild.scrollIntoView({ block: \"end\" });\n              }\n            });\n          },\n        });\n      },\n    );\n\n    this.onScroll = (_e) => {\n      const scrollNow = scrollTop(this.scrollContainer);\n\n      if (pendingOp) {\n        scrollBefore = scrollNow;\n        return pendingOp();\n      }\n\n      const rect = this.findOverrunTarget();\n      const topEvent = this.el.getAttribute(\n        this.liveSocket.binding(\"viewport-top\"),\n      );\n      const bottomEvent = this.el.getAttribute(\n        this.liveSocket.binding(\"viewport-bottom\"),\n      );\n      const lastChild = this.el.lastElementChild;\n      const firstChild = this.el.firstElementChild;\n      const isScrollingUp = scrollNow < scrollBefore;\n      const isScrollingDown = scrollNow > scrollBefore;\n\n      // el overran while scrolling up\n      if (isScrollingUp && topEvent && !topOverran && rect.top >= 0) {\n        topOverran = true;\n        onTopOverrun(topEvent, firstChild);\n      } else if (isScrollingDown && topOverran && rect.top <= 0) {\n        topOverran = false;\n      }\n\n      if (\n        topEvent &&\n        isScrollingUp &&\n        isAtViewportTop(firstChild, this.scrollContainer)\n      ) {\n        onFirstChildAtTop(topEvent, firstChild);\n      } else if (\n        bottomEvent &&\n        isScrollingDown &&\n        isAtViewportBottom(lastChild, this.scrollContainer)\n      ) {\n        onLastChildAtBottom(bottomEvent, lastChild);\n      }\n      scrollBefore = scrollNow;\n    };\n\n    if (this.scrollContainer) {\n      this.scrollContainer.addEventListener(\"scroll\", this.onScroll);\n    } else {\n      window.addEventListener(\"scroll\", this.onScroll);\n    }\n  },\n\n  destroyed() {\n    if (this.scrollContainer) {\n      this.scrollContainer.removeEventListener(\"scroll\", this.onScroll);\n    } else {\n      window.removeEventListener(\"scroll\", this.onScroll);\n    }\n  },\n\n  throttle(interval, callback) {\n    let lastCallAt = 0;\n    let timer;\n\n    return (...args) => {\n      const now = Date.now();\n      const remainingTime = interval - (now - lastCallAt);\n\n      if (remainingTime <= 0 || remainingTime > interval) {\n        if (timer) {\n          clearTimeout(timer);\n          timer = null;\n        }\n        lastCallAt = now;\n        callback(...args);\n      } else if (!timer) {\n        timer = setTimeout(() => {\n          lastCallAt = Date.now();\n          timer = null;\n          callback(...args);\n        }, remainingTime);\n      }\n    };\n  },\n\n  findOverrunTarget() {\n    let rect;\n    const overrunTarget = this.el.getAttribute(\n      this.liveSocket.binding(PHX_VIEWPORT_OVERRUN_TARGET),\n    );\n    if (overrunTarget) {\n      const overrunEl = document.getElementById(overrunTarget);\n      if (overrunEl) {\n        rect = overrunEl.getBoundingClientRect();\n      } else {\n        throw new Error(\"did not find element with id \" + overrunTarget);\n      }\n    } else {\n      rect = this.el.getBoundingClientRect();\n    }\n    return rect;\n  },\n};\nexport default Hooks;\n"
  },
  {
    "path": "assets/js/phoenix_live_view/index.ts",
    "content": "/*\n================================================================================\nPhoenix LiveView JavaScript Client\n================================================================================\n\nSee the hexdocs at `https://hexdocs.pm/phoenix_live_view` for documentation.\n*/\n\nimport OriginalLiveSocket, { isUsedInput } from \"./live_socket\";\nimport DOM from \"./dom\";\nimport { ViewHook } from \"./view_hook\";\nimport View from \"./view\";\nimport { logError } from \"./utils\";\n\nimport type { EncodedJS, LiveSocketJSCommands } from \"./js_commands\";\nimport type { Hook, HooksOptions } from \"./view_hook\";\nimport type { Socket as PhoenixSocket } from \"phoenix\";\n\n/**\n * Options for configuring the LiveSocket instance.\n */\nexport interface LiveSocketOptions {\n  /**\n   * Defaults for phx-debounce and phx-throttle.\n   */\n  defaults?: {\n    /** The millisecond phx-debounce time. Defaults 300 */\n    debounce?: number;\n    /** The millisecond phx-throttle time. Defaults 300 */\n    throttle?: number;\n  };\n  /**\n   * An object or function for passing connect params.\n   * The function receives the element associated with a given LiveView. For example:\n   *\n   *     (el) => {view: el.getAttribute(\"data-my-view-name\", token: window.myToken}\n   *\n   */\n  params?:\n    | ((el: HTMLElement) => { [key: string]: any })\n    | { [key: string]: any };\n  /**\n   * The optional prefix to use for all phx DOM annotations.\n   *\n   * Defaults to \"phx-\".\n   */\n  bindingPrefix?: string;\n  /**\n   * Callbacks for LiveView hooks.\n   *\n   * See [Client hooks via `phx-hook`](https://hexdocs.pm/phoenix_live_view/js-interop.html#client-hooks-via-phx-hook) for more information.\n   */\n  hooks?: HooksOptions;\n  /** Callbacks for LiveView uploaders. */\n  uploaders?: { [key: string]: any }; // TODO: define more specifically\n  /** Delay in milliseconds before applying loading states. */\n  loaderTimeout?: number;\n  /** Delay in milliseconds before executing phx-disconnected commands. */\n  disconnectedTimeout?: number;\n  /** Maximum reloads before entering failsafe mode. */\n  maxReloads?: number;\n  /** Minimum time between normal reload attempts. */\n  reloadJitterMin?: number;\n  /** Maximum time between normal reload attempts. */\n  reloadJitterMax?: number;\n  /** Time between reload attempts in failsafe mode. */\n  failsafeJitter?: number;\n  /**\n   * Function to log debug information. For example:\n   *\n   *     (view, kind, msg, obj) => console.log(`${view.id} ${kind}: ${msg} - `, obj)\n   */\n  viewLogger?: (view: View, kind: string, msg: string, obj: any) => void;\n  /**\n   * Object mapping event names to functions for populating event metadata.\n   *\n   *     metadata: {\n   *       click: (e, el) => {\n   *         return {\n   *           ctrlKey: e.ctrlKey,\n   *           metaKey: e.metaKey,\n   *           detail: e.detail || 1,\n   *         }\n   *       },\n   *       keydown: (e, el) => {\n   *         return {\n   *           key: e.key,\n   *           ctrlKey: e.ctrlKey,\n   *           metaKey: e.metaKey,\n   *           shiftKey: e.shiftKey\n   *         }\n   *       }\n   *     }\n   *\n   */\n  metadata?: { [eventName: string]: (e: Event, el: HTMLElement) => object };\n  /**\n   * An optional Storage compatible object\n   * Useful when LiveView won't have access to `sessionStorage`. For example, This could\n   * happen if a site loads a cross-domain LiveView in an iframe.\n   *\n   * Example usage:\n   *\n   *     class InMemoryStorage {\n   *       constructor() { this.storage = {} }\n   *       getItem(keyName) { return this.storage[keyName] || null }\n   *       removeItem(keyName) { delete this.storage[keyName] }\n   *       setItem(keyName, keyValue) { this.storage[keyName] = keyValue }\n   *     }\n   */\n  sessionStorage?: Storage;\n  /**\n   * An optional Storage compatible object\n   * Useful when LiveView won't have access to `localStorage`.\n   *\n   * See `sessionStorage` for an example.\n   */\n  localStorage?: Storage;\n  /**\n   * If set to `true`, `phx-change` events will be blocked (will not fire)\n   * while the user is composing input using an IME (Input Method Editor).\n   * This is determined by the `e.isComposing` property on keyboard events,\n   * which is `true` when the user is in the process of entering composed characters (for example,\n   * when typing Japanese or Chinese using romaji or pinyin input methods).\n   * By default, `phx-change` will not be blocked during a composition session,\n   * but note that there were issues reported in older versions of Safari,\n   * where a LiveView patch to the input caused unexpected behavior.\n   *\n   * For more information, see\n   * - https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/isComposing\n   * - https://github.com/phoenixframework/phoenix_live_view/issues/3322\n   *\n   * Defaults to `false`.\n   */\n  blockPhxChangeWhileComposing?: boolean;\n  /** DOM callbacks. */\n  dom?: {\n    /**\n     * An optional function to modify the behavior of querying elements in JS commands.\n     * @param sourceEl - The source element, e.g. the button that was clicked.\n     * @param query - The query value.\n     * @param defaultQuery - A default query function that can be used if no custom query should be applied.\n     * @returns A list of DOM elements.\n     */\n    jsQuerySelectorAll?: (\n      sourceEl: HTMLElement,\n      query: string,\n      defaultQuery: () => Element[],\n    ) => Element[];\n    /**\n     * When defined, called with a start callback that needs to be called\n     * to perform the actual patch. Failing to call the start callback causes\n     * the page to become stuck.\n     *\n     * This can be used to delay patches in order to perform view transitions,\n     * for example:\n     *\n     * ```javascript\n     * let liveSocket = new LiveSocket(\"/live\", Socket, {\n     *   dom: {\n     *     onDocumentPatch(start) {\n     *       document.startViewTransition(start);\n     *     }\n     *   }\n     * })\n     * ```\n     *\n     * It is strongly advised to call start as quickly as possible.\n     */\n    onDocumentPatch?: (start: () => void) => void;\n    /**\n     * Called immediately before a DOM patch is applied.\n     */\n    onPatchStart?: (container: HTMLElement) => void;\n    /**\n     * Called immediately after a DOM patch is applied.\n     */\n    onPatchEnd?: (container: HTMLElement) => void;\n    /**\n     * Called when a new DOM node is added.\n     */\n    onNodeAdded?: (node: Node) => void;\n    /**\n     * Called before an element is updated.\n     */\n    onBeforeElUpdated?: (fromEl: Element, toEl: Element) => void;\n  };\n  /** Allow passthrough of other options to the Phoenix Socket constructor. */\n  [key: string]: any;\n}\n\n/**\n * Interface describing the public API of a LiveSocket instance.\n */\nexport interface LiveSocketInstanceInterface {\n  /**\n   * Returns the version of the LiveView client.\n   */\n  version(): string;\n  /**\n   * Returns true if profiling is enabled. See `enableProfiling` and `disableProfiling`.\n   */\n  isProfileEnabled(): boolean;\n  /**\n   * Returns true if debugging is enabled. See `enableDebug` and `disableDebug`.\n   */\n  isDebugEnabled(): boolean;\n  /**\n   * Returns true if debugging is disabled. See `enableDebug` and `disableDebug`.\n   */\n  isDebugDisabled(): boolean;\n  /**\n   * Enables debugging.\n   *\n   * When debugging is enabled, the LiveView client will log debug information to the console.\n   * See [Debugging client events](https://hexdocs.pm/phoenix_live_view/js-interop.html#debugging-client-events) for more information.\n   */\n  enableDebug(): void;\n  /**\n   * Enables profiling.\n   *\n   * When profiling is enabled, the LiveView client will log profiling information to the console.\n   */\n  enableProfiling(): void;\n  /**\n   * Disables debugging.\n   */\n  disableDebug(): void;\n  /**\n   * Disables profiling.\n   */\n  disableProfiling(): void;\n  /**\n   * Enables latency simulation.\n   *\n   * When latency simulation is enabled, the LiveView client will add a delay to requests and responses from the server.\n   * See [Simulating Latency](https://hexdocs.pm/phoenix_live_view/js-interop.html#simulating-latency) for more information.\n   */\n  enableLatencySim(upperBoundMs: number): void;\n  /**\n   * Disables latency simulation.\n   */\n  disableLatencySim(): void;\n  /**\n   * Returns the current latency simulation upper bound.\n   */\n  getLatencySim(): number | null;\n  /**\n   * Returns the Phoenix Socket instance.\n   */\n  getSocket(): PhoenixSocket;\n  /**\n   * Connects to the LiveView server.\n   */\n  connect(): void;\n  /**\n   * Disconnects from the LiveView server.\n   */\n  disconnect(callback?: () => void): void;\n  /**\n   * Can be used to replace the transport used by the underlying Phoenix Socket.\n   */\n  replaceTransport(transport: any): void;\n  /**\n   * Executes an encoded JS command, targeting the given element.\n   *\n   * See [`Phoenix.LiveView.JS`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.JS.html) for more information.\n   */\n  execJS(\n    el: HTMLElement,\n    encodedJS: EncodedJS,\n    eventType?: string | null,\n  ): void;\n  /**\n   * Returns an object with methods to manipulate the DOM and execute JavaScript.\n   * The applied changes integrate with server DOM patching.\n   *\n   * See [JavaScript interoperability](https://hexdocs.pm/phoenix_live_view/js-interop.html) for more information.\n   */\n  js(): LiveSocketJSCommands;\n}\n\n/**\n * Interface describing the LiveSocket constructor.\n */\nexport interface LiveSocketConstructor {\n  /**\n   * Creates a new LiveSocket instance.\n   *\n   * @param endpoint - The string WebSocket endpoint, ie, `\"wss://example.com/live\"`,\n   *                                               `\"/live\"` (inherited host & protocol)\n   * @param socket - the required Phoenix Socket class imported from \"phoenix\". For example:\n   *\n   *     import {Socket} from \"phoenix\"\n   *     import {LiveSocket} from \"phoenix_live_view\"\n   *     let liveSocket = new LiveSocket(\"/live\", Socket, {...})\n   *\n   * @param opts - Optional configuration.\n   */\n  new (\n    endpoint: string,\n    socket: typeof PhoenixSocket,\n    opts?: LiveSocketOptions,\n  ): LiveSocketInstanceInterface;\n}\n\n// because LiveSocket is in JS (for now), we cast it to our defined TypeScript constructor.\nconst LiveSocket = OriginalLiveSocket as unknown as LiveSocketConstructor;\n\n/** Creates a hook instance for the given element and callbacks.\n *\n * @param el - The element to associate with the hook.\n * @param callbacks - The list of hook callbacks, such as mounted,\n *   updated, destroyed, etc.\n *\n * *Note*: `createHook` must be called from the `connectedCallback` lifecycle\n * which is triggered after the element has been added to the DOM. If you try\n * to call `createHook` from the constructor, an error will be logged.\n *\n * Furthermore, you can only start using the hook's APIs after the `mounted`\n * callback of the hook has been called. If you try to call them earlier,\n * an error will be logged.\n *\n * @example\n *\n * class MyComponent extends HTMLElement {\n *   connectedCallback(){\n *     let onLiveViewMounted = () => this.hook.pushEvent(...))\n *     this.hook = createHook(this, {mounted: onLiveViewMounted})\n *   }\n * }\n *\n * @returns Returns the Hook instance for the custom element.\n */\nfunction createHook(el: HTMLElement, callbacks: Hook): ViewHook {\n  let existingHook = DOM.getCustomElHook(el);\n  if (existingHook) {\n    return existingHook;\n  }\n\n  if (!el.hasAttribute(\"id\")) {\n    logError(\n      \"Elements passed to createHook need to have a unique id attribute\",\n      el,\n    );\n  }\n\n  let hook = new ViewHook(View.closestView(el), el, callbacks);\n  DOM.putCustomElHook(el, hook);\n  return hook;\n}\n\nexport { LiveSocket, isUsedInput, createHook, ViewHook, Hook, HooksOptions };\n"
  },
  {
    "path": "assets/js/phoenix_live_view/js.js",
    "content": "import DOM from \"./dom\";\nimport ARIA from \"./aria\";\n\nconst focusStack = [];\nconst default_transition_time = 200;\n\nconst JS = {\n  // private\n  exec(e, eventType, phxEvent, view, sourceEl, defaults) {\n    const [defaultKind, defaultArgs] = defaults || [\n      null,\n      { callback: defaults && defaults.callback },\n    ];\n    const commands = Array.isArray(phxEvent)\n      ? phxEvent\n      : typeof phxEvent === \"string\" && phxEvent.startsWith(\"[\")\n        ? JSON.parse(phxEvent)\n        : [[defaultKind, defaultArgs]];\n\n    commands.forEach(([kind, args]) => {\n      if (kind === defaultKind) {\n        // always prefer the args, but keep existing keys from the defaultArgs\n        args = { ...defaultArgs, ...args };\n        args.callback = args.callback || defaultArgs.callback;\n      }\n      this.filterToEls(view.liveSocket, sourceEl, args).forEach((el) => {\n        this[`exec_${kind}`](e, eventType, phxEvent, view, sourceEl, el, args);\n      });\n    });\n  },\n\n  isVisible(el) {\n    return !!(\n      el.offsetWidth ||\n      el.offsetHeight ||\n      el.getClientRects().length > 0\n    );\n  },\n\n  // returns true if any part of the element is inside the viewport\n  isInViewport(el) {\n    const rect = el.getBoundingClientRect();\n    const windowHeight =\n      window.innerHeight || document.documentElement.clientHeight;\n    const windowWidth =\n      window.innerWidth || document.documentElement.clientWidth;\n\n    return (\n      rect.right > 0 &&\n      rect.bottom > 0 &&\n      rect.left < windowWidth &&\n      rect.top < windowHeight\n    );\n  },\n\n  // private\n\n  // commands\n\n  exec_exec(e, eventType, phxEvent, view, sourceEl, el, { attr, to }) {\n    const encodedJS = el.getAttribute(attr);\n    if (!encodedJS) {\n      throw new Error(`expected ${attr} to contain JS command on \"${to}\"`);\n    }\n    view.liveSocket.execJS(el, encodedJS, eventType);\n  },\n\n  exec_dispatch(\n    e,\n    eventType,\n    phxEvent,\n    view,\n    sourceEl,\n    el,\n    { event, detail, bubbles, blocking },\n  ) {\n    detail = detail || {};\n    detail.dispatcher = sourceEl;\n    if (blocking) {\n      const promise = new Promise((resolve, _reject) => {\n        detail.done = resolve;\n      });\n      view.liveSocket.asyncTransition(promise);\n    }\n    DOM.dispatchEvent(el, event, { detail, bubbles });\n  },\n\n  exec_push(e, eventType, phxEvent, view, sourceEl, el, args) {\n    const {\n      event,\n      data,\n      target,\n      page_loading,\n      loading,\n      value,\n      dispatcher,\n      callback,\n    } = args;\n    const pushOpts = {\n      loading,\n      value,\n      target,\n      page_loading: !!page_loading,\n      originalEvent: e,\n    };\n    const targetSrc =\n      eventType === \"change\" && dispatcher ? dispatcher : sourceEl;\n    const phxTarget =\n      target || targetSrc.getAttribute(view.binding(\"target\")) || targetSrc;\n    const handler = (targetView, targetCtx) => {\n      if (!targetView.isConnected()) {\n        return;\n      }\n      if (eventType === \"change\") {\n        let { newCid, _target } = args;\n        _target =\n          _target || (DOM.isFormInput(sourceEl) ? sourceEl.name : undefined);\n        if (_target) {\n          pushOpts._target = _target;\n        }\n        targetView.pushInput(\n          sourceEl,\n          targetCtx,\n          newCid,\n          event || phxEvent,\n          pushOpts,\n          callback,\n        );\n      } else if (eventType === \"submit\") {\n        const { submitter } = args;\n        targetView.submitForm(\n          sourceEl,\n          targetCtx,\n          event || phxEvent,\n          submitter,\n          pushOpts,\n          callback,\n        );\n      } else {\n        targetView.pushEvent(\n          eventType,\n          sourceEl,\n          targetCtx,\n          event || phxEvent,\n          data,\n          pushOpts,\n          callback,\n        );\n      }\n    };\n    // in case of formRecovery, targetView and targetCtx are passed as argument\n    // as they are looked up in a template element, not the real DOM\n    if (args.targetView && args.targetCtx) {\n      handler(args.targetView, args.targetCtx);\n    } else {\n      view.withinTargets(phxTarget, handler);\n    }\n  },\n\n  exec_navigate(e, eventType, phxEvent, view, sourceEl, el, { href, replace }) {\n    view.liveSocket.historyRedirect(\n      e,\n      href,\n      replace ? \"replace\" : \"push\",\n      null,\n      sourceEl,\n    );\n  },\n\n  exec_patch(e, eventType, phxEvent, view, sourceEl, el, { href, replace }) {\n    view.liveSocket.pushHistoryPatch(\n      e,\n      href,\n      replace ? \"replace\" : \"push\",\n      sourceEl,\n    );\n  },\n\n  exec_focus(e, eventType, phxEvent, view, sourceEl, el) {\n    ARIA.attemptFocus(el);\n    // in case the JS.focus command is in a JS.show/hide/toggle chain, for show we need\n    // to wait for JS.show to have updated the element's display property (see exec_toggle)\n    // but that run in nested animation frames, therefore we need to use them here as well\n    window.requestAnimationFrame(() => {\n      window.requestAnimationFrame(() => ARIA.attemptFocus(el));\n    });\n  },\n\n  exec_focus_first(e, eventType, phxEvent, view, sourceEl, el) {\n    ARIA.focusFirstInteractive(el) || ARIA.focusFirst(el);\n    // if you wonder about the nested animation frames, see exec_focus\n    window.requestAnimationFrame(() => {\n      window.requestAnimationFrame(\n        () => ARIA.focusFirstInteractive(el) || ARIA.focusFirst(el),\n      );\n    });\n  },\n\n  exec_push_focus(e, eventType, phxEvent, view, sourceEl, el) {\n    focusStack.push(el || sourceEl);\n  },\n\n  exec_pop_focus(_e, _eventType, _phxEvent, _view, _sourceEl, _el) {\n    const el = focusStack.pop();\n    if (el) {\n      el.focus();\n      // if you wonder about the nested animation frames, see exec_focus\n      window.requestAnimationFrame(() => {\n        window.requestAnimationFrame(() => el.focus());\n      });\n    }\n  },\n\n  exec_add_class(\n    e,\n    eventType,\n    phxEvent,\n    view,\n    sourceEl,\n    el,\n    { names, transition, time, blocking },\n  ) {\n    this.addOrRemoveClasses(el, names, [], transition, time, view, blocking);\n  },\n\n  exec_remove_class(\n    e,\n    eventType,\n    phxEvent,\n    view,\n    sourceEl,\n    el,\n    { names, transition, time, blocking },\n  ) {\n    this.addOrRemoveClasses(el, [], names, transition, time, view, blocking);\n  },\n\n  exec_toggle_class(\n    e,\n    eventType,\n    phxEvent,\n    view,\n    sourceEl,\n    el,\n    { names, transition, time, blocking },\n  ) {\n    this.toggleClasses(el, names, transition, time, view, blocking);\n  },\n\n  exec_toggle_attr(\n    e,\n    eventType,\n    phxEvent,\n    view,\n    sourceEl,\n    el,\n    { attr: [attr, val1, val2] },\n  ) {\n    this.toggleAttr(el, attr, val1, val2);\n  },\n\n  exec_ignore_attrs(e, eventType, phxEvent, view, sourceEl, el, { attrs }) {\n    this.ignoreAttrs(el, attrs);\n  },\n\n  exec_transition(\n    e,\n    eventType,\n    phxEvent,\n    view,\n    sourceEl,\n    el,\n    { time, transition, blocking },\n  ) {\n    this.addOrRemoveClasses(el, [], [], transition, time, view, blocking);\n  },\n\n  exec_toggle(\n    e,\n    eventType,\n    phxEvent,\n    view,\n    sourceEl,\n    el,\n    { display, ins, outs, time, blocking },\n  ) {\n    this.toggle(eventType, view, el, display, ins, outs, time, blocking);\n  },\n\n  exec_show(\n    e,\n    eventType,\n    phxEvent,\n    view,\n    sourceEl,\n    el,\n    { display, transition, time, blocking },\n  ) {\n    this.show(eventType, view, el, display, transition, time, blocking);\n  },\n\n  exec_hide(\n    e,\n    eventType,\n    phxEvent,\n    view,\n    sourceEl,\n    el,\n    { display, transition, time, blocking },\n  ) {\n    this.hide(eventType, view, el, display, transition, time, blocking);\n  },\n\n  exec_set_attr(\n    e,\n    eventType,\n    phxEvent,\n    view,\n    sourceEl,\n    el,\n    { attr: [attr, val] },\n  ) {\n    this.setOrRemoveAttrs(el, [[attr, val]], []);\n  },\n\n  exec_remove_attr(e, eventType, phxEvent, view, sourceEl, el, { attr }) {\n    this.setOrRemoveAttrs(el, [], [attr]);\n  },\n\n  ignoreAttrs(el, attrs) {\n    DOM.putPrivate(el, \"JS:ignore_attrs\", {\n      apply: (fromEl, toEl) => {\n        let fromAttributes = Array.from(fromEl.attributes);\n        let fromAttributeNames = fromAttributes.map((attr) => attr.name);\n        Array.from(toEl.attributes)\n          .filter((attr) => {\n            return !fromAttributeNames.includes(attr.name);\n          })\n          .forEach((attr) => {\n            if (DOM.attributeIgnored(attr, attrs)) {\n              toEl.removeAttribute(attr.name);\n            }\n          });\n        fromAttributes.forEach((attr) => {\n          if (DOM.attributeIgnored(attr, attrs)) {\n            toEl.setAttribute(attr.name, attr.value);\n          }\n        });\n      },\n    });\n  },\n\n  onBeforeElUpdated(fromEl, toEl) {\n    const ignoreAttrs = DOM.private(fromEl, \"JS:ignore_attrs\");\n    if (ignoreAttrs) {\n      ignoreAttrs.apply(fromEl, toEl);\n    }\n  },\n\n  // utils for commands\n\n  show(eventType, view, el, display, transition, time, blocking) {\n    if (!this.isVisible(el)) {\n      this.toggle(\n        eventType,\n        view,\n        el,\n        display,\n        transition,\n        null,\n        time,\n        blocking,\n      );\n    }\n  },\n\n  hide(eventType, view, el, display, transition, time, blocking) {\n    if (this.isVisible(el)) {\n      this.toggle(\n        eventType,\n        view,\n        el,\n        display,\n        null,\n        transition,\n        time,\n        blocking,\n      );\n    }\n  },\n\n  toggle(eventType, view, el, display, ins, outs, time, blocking) {\n    time = time || default_transition_time;\n    const [inClasses, inStartClasses, inEndClasses] = ins || [[], [], []];\n    const [outClasses, outStartClasses, outEndClasses] = outs || [[], [], []];\n    if (inClasses.length > 0 || outClasses.length > 0) {\n      if (this.isVisible(el)) {\n        const onStart = () => {\n          this.addOrRemoveClasses(\n            el,\n            outStartClasses,\n            inClasses.concat(inStartClasses).concat(inEndClasses),\n          );\n          window.requestAnimationFrame(() => {\n            this.addOrRemoveClasses(el, outClasses, []);\n            window.requestAnimationFrame(() =>\n              this.addOrRemoveClasses(el, outEndClasses, outStartClasses),\n            );\n          });\n        };\n        const onEnd = () => {\n          this.addOrRemoveClasses(el, [], outClasses.concat(outEndClasses));\n          DOM.putSticky(\n            el,\n            \"toggle\",\n            (currentEl) => (currentEl.style.display = \"none\"),\n          );\n          el.dispatchEvent(new Event(\"phx:hide-end\"));\n        };\n        el.dispatchEvent(new Event(\"phx:hide-start\"));\n        if (blocking === false) {\n          onStart();\n          setTimeout(onEnd, time);\n        } else {\n          view.transition(time, onStart, onEnd);\n        }\n      } else {\n        if (eventType === \"remove\") {\n          return;\n        }\n        const onStart = () => {\n          this.addOrRemoveClasses(\n            el,\n            inStartClasses,\n            outClasses.concat(outStartClasses).concat(outEndClasses),\n          );\n          const stickyDisplay = display || this.defaultDisplay(el);\n          window.requestAnimationFrame(() => {\n            // first add the starting + active class, THEN make the element visible\n            // otherwise if we toggled the visibility earlier css animations\n            // would flicker, as the element becomes visible before the active animation\n            // class is set (see https://github.com/phoenixframework/phoenix_live_view/issues/3456)\n            this.addOrRemoveClasses(el, inClasses, []);\n            // addOrRemoveClasses uses a requestAnimationFrame itself, therefore we need to move the putSticky\n            // into the next requestAnimationFrame...\n            window.requestAnimationFrame(() => {\n              DOM.putSticky(\n                el,\n                \"toggle\",\n                (currentEl) => (currentEl.style.display = stickyDisplay),\n              );\n              this.addOrRemoveClasses(el, inEndClasses, inStartClasses);\n            });\n          });\n        };\n        const onEnd = () => {\n          this.addOrRemoveClasses(el, [], inClasses.concat(inEndClasses));\n          el.dispatchEvent(new Event(\"phx:show-end\"));\n        };\n        el.dispatchEvent(new Event(\"phx:show-start\"));\n        if (blocking === false) {\n          onStart();\n          setTimeout(onEnd, time);\n        } else {\n          view.transition(time, onStart, onEnd);\n        }\n      }\n    } else {\n      if (this.isVisible(el)) {\n        window.requestAnimationFrame(() => {\n          el.dispatchEvent(new Event(\"phx:hide-start\"));\n          DOM.putSticky(\n            el,\n            \"toggle\",\n            (currentEl) => (currentEl.style.display = \"none\"),\n          );\n          el.dispatchEvent(new Event(\"phx:hide-end\"));\n        });\n      } else {\n        window.requestAnimationFrame(() => {\n          el.dispatchEvent(new Event(\"phx:show-start\"));\n          const stickyDisplay = display || this.defaultDisplay(el);\n          DOM.putSticky(\n            el,\n            \"toggle\",\n            (currentEl) => (currentEl.style.display = stickyDisplay),\n          );\n          el.dispatchEvent(new Event(\"phx:show-end\"));\n        });\n      }\n    }\n  },\n\n  toggleClasses(el, classes, transition, time, view, blocking) {\n    window.requestAnimationFrame(() => {\n      const [prevAdds, prevRemoves] = DOM.getSticky(el, \"classes\", [[], []]);\n      const newAdds = classes.filter(\n        (name) => prevAdds.indexOf(name) < 0 && !el.classList.contains(name),\n      );\n      const newRemoves = classes.filter(\n        (name) => prevRemoves.indexOf(name) < 0 && el.classList.contains(name),\n      );\n      this.addOrRemoveClasses(\n        el,\n        newAdds,\n        newRemoves,\n        transition,\n        time,\n        view,\n        blocking,\n      );\n    });\n  },\n\n  toggleAttr(el, attr, val1, val2) {\n    if (el.hasAttribute(attr)) {\n      if (val2 !== undefined) {\n        // toggle between val1 and val2\n        if (el.getAttribute(attr) === val1) {\n          this.setOrRemoveAttrs(el, [[attr, val2]], []);\n        } else {\n          this.setOrRemoveAttrs(el, [[attr, val1]], []);\n        }\n      } else {\n        // remove attr\n        this.setOrRemoveAttrs(el, [], [attr]);\n      }\n    } else {\n      this.setOrRemoveAttrs(el, [[attr, val1]], []);\n    }\n  },\n\n  addOrRemoveClasses(el, adds, removes, transition, time, view, blocking) {\n    time = time || default_transition_time;\n    const [transitionRun, transitionStart, transitionEnd] = transition || [\n      [],\n      [],\n      [],\n    ];\n    if (transitionRun.length > 0) {\n      const onStart = () => {\n        this.addOrRemoveClasses(\n          el,\n          transitionStart,\n          [].concat(transitionRun).concat(transitionEnd),\n        );\n        window.requestAnimationFrame(() => {\n          this.addOrRemoveClasses(el, transitionRun, []);\n          window.requestAnimationFrame(() =>\n            this.addOrRemoveClasses(el, transitionEnd, transitionStart),\n          );\n        });\n      };\n      const onDone = () =>\n        this.addOrRemoveClasses(\n          el,\n          adds.concat(transitionEnd),\n          removes.concat(transitionRun).concat(transitionStart),\n        );\n      if (blocking === false) {\n        onStart();\n        setTimeout(onDone, time);\n      } else {\n        view.transition(time, onStart, onDone);\n      }\n      return;\n    }\n\n    window.requestAnimationFrame(() => {\n      const [prevAdds, prevRemoves] = DOM.getSticky(el, \"classes\", [[], []]);\n      const keepAdds = adds.filter(\n        (name) => prevAdds.indexOf(name) < 0 && !el.classList.contains(name),\n      );\n      const keepRemoves = removes.filter(\n        (name) => prevRemoves.indexOf(name) < 0 && el.classList.contains(name),\n      );\n      const newAdds = prevAdds\n        .filter((name) => removes.indexOf(name) < 0)\n        .concat(keepAdds);\n      const newRemoves = prevRemoves\n        .filter((name) => adds.indexOf(name) < 0)\n        .concat(keepRemoves);\n\n      DOM.putSticky(el, \"classes\", (currentEl) => {\n        currentEl.classList.remove(...newRemoves);\n        currentEl.classList.add(...newAdds);\n        return [newAdds, newRemoves];\n      });\n    });\n  },\n\n  setOrRemoveAttrs(el, sets, removes) {\n    const [prevSets, prevRemoves] = DOM.getSticky(el, \"attrs\", [[], []]);\n\n    const alteredAttrs = sets.map(([attr, _val]) => attr).concat(removes);\n    const newSets = prevSets\n      .filter(([attr, _val]) => !alteredAttrs.includes(attr))\n      .concat(sets);\n    const newRemoves = prevRemoves\n      .filter((attr) => !alteredAttrs.includes(attr))\n      .concat(removes);\n\n    DOM.putSticky(el, \"attrs\", (currentEl) => {\n      newRemoves.forEach((attr) => currentEl.removeAttribute(attr));\n      newSets.forEach(([attr, val]) => currentEl.setAttribute(attr, val));\n      return [newSets, newRemoves];\n    });\n  },\n\n  hasAllClasses(el, classes) {\n    return classes.every((name) => el.classList.contains(name));\n  },\n\n  isToggledOut(el, outClasses) {\n    return !this.isVisible(el) || this.hasAllClasses(el, outClasses);\n  },\n\n  filterToEls(liveSocket, sourceEl, { to }) {\n    const defaultQuery = () => {\n      if (typeof to === \"string\") {\n        return document.querySelectorAll(to);\n      } else if (to.closest) {\n        const toEl = sourceEl.closest(to.closest);\n        return toEl ? [toEl] : [];\n      } else if (to.inner) {\n        return sourceEl.querySelectorAll(to.inner);\n      }\n    };\n    return to\n      ? liveSocket.jsQuerySelectorAll(sourceEl, to, defaultQuery)\n      : [sourceEl];\n  },\n\n  defaultDisplay(el) {\n    return (\n      { tr: \"table-row\", td: \"table-cell\" }[el.tagName.toLowerCase()] || \"block\"\n    );\n  },\n\n  transitionClasses(val) {\n    if (!val) {\n      return null;\n    }\n\n    let [trans, tStart, tEnd] = Array.isArray(val)\n      ? val\n      : [val.split(\" \"), [], []];\n    trans = Array.isArray(trans) ? trans : trans.split(\" \");\n    tStart = Array.isArray(tStart) ? tStart : tStart.split(\" \");\n    tEnd = Array.isArray(tEnd) ? tEnd : tEnd.split(\" \");\n    return [trans, tStart, tEnd];\n  },\n};\n\nexport default JS;\n"
  },
  {
    "path": "assets/js/phoenix_live_view/js_commands.ts",
    "content": "import JS from \"./js\";\nimport LiveSocket from \"./live_socket\";\n\n/**\n * An encoded JS command. Use functions in the `Phoenix.LiveView.JS` module on\n * the server to create and compose JS commands.\n *\n * The underlying primitive type is considered opaque, and may change in future\n * versions.\n */\nexport type EncodedJS = string | Array<any>;\n\ntype Transition = string | string[];\n\n// Base options for commands involving transitions and timing\ntype BaseOpts = {\n  /**\n   * The CSS transition classes to set.\n   * Accepts a string of classes or a 3-tuple like:\n   * `[\"ease-out duration-300\", \"opacity-0\", \"opacity-100\"]`\n   */\n  transition?: Transition;\n  /** The transition duration in milliseconds. Defaults 200. */\n  time?: number;\n  /** Whether to block UI during transition. Defaults `true`. */\n  blocking?: boolean;\n};\n\ntype ShowOpts = BaseOpts & {\n  /** The CSS display value to set. Defaults \"block\". */\n  display?: string;\n};\n\ntype ToggleOpts = {\n  /** The CSS display value to set. Defaults \"block\". */\n  display?: string;\n  /**\n   * The CSS transition classes for showing.\n   * Accepts either the string of classes to apply when toggling in, or\n   * a 3-tuple containing the transition class, the class to apply\n   * to start the transition, and the ending transition class, such as:\n   * `[\"ease-out duration-300\", \"opacity-0\", \"opacity-100\"]`\n   */\n  in?: Transition;\n  /**\n   * The CSS transition classes for hiding.\n   * Accepts either string of classes to apply when toggling out, or\n   * a 3-tuple containing the transition class, the class to apply\n   * to start the transition, and the ending transition class, such as:\n   * `[\"ease-out duration-300\", \"opacity-100\", \"opacity-0\"]`\n   */\n  out?: Transition;\n  /** The transition duration in milliseconds. */\n  time?: number;\n  /** Whether to block UI during transition. Defaults `true`. */\n  blocking?: boolean;\n};\n\n// Options specific to the 'transition' command\ntype TransitionCommandOpts = {\n  /** The transition duration in milliseconds. */\n  time?: number;\n  /** Whether to block UI during transition. Defaults `true`. */\n  blocking?: boolean;\n};\n\ntype PushOpts = {\n  /** Data to be merged into the event payload. */\n  value?: any;\n  /** For targeting a LiveComponent by its ID, a component ID (number), or a CSS selector string. */\n  target?: HTMLElement | number | string;\n  /** Indicates if a page loading state should be shown. */\n  page_loading?: boolean;\n  [key: string]: any; // Allow other properties like 'cid', 'redirect', etc.\n};\n\ntype NavigationOpts = {\n  /** Whether to replace the current history entry instead of pushing a new one. */\n  replace?: boolean;\n};\n\n/**\n * Represents all possible JS commands that can be generated by the factory.\n * This is used as a base for LiveSocketJSCommands and HookJSCommands.\n */\ninterface AllJSCommands {\n  /**\n   * Executes an encoded JS command in the context of an element.\n   * This version is for general use via liveSocket.js().\n   *\n   * @param el - The element in whose context to execute the JS command.\n   * @param encodedJS - The encoded JS command with operations to execute.\n   */\n  exec(el: HTMLElement, encodedJS: EncodedJS): void;\n\n  /**\n   * Shows an element.\n   *\n   * @param el - The element to show.\n   * @param {ShowOpts} [opts={}] - Optional settings.\n   *   Accepts: `display`, `transition`, `time`, and `blocking`.\n   */\n  show(el: HTMLElement, opts?: ShowOpts): void;\n\n  /**\n   * Hides an element.\n   *\n   * @param el - The element to hide.\n   * @param [opts={}] - Optional settings.\n   *   Accepts: `transition`, `time`, and `blocking`.\n   */\n  hide(el: HTMLElement, opts?: BaseOpts): void;\n\n  /**\n   * Toggles the visibility of an element.\n   *\n   * @param el - The element to toggle.\n   * @param [opts={}] - Optional settings.\n   *   Accepts: `display`, `in`, `out`, `time`, and `blocking`.\n   */\n  toggle(el: HTMLElement, opts?: ToggleOpts): void;\n\n  /**\n   * Adds CSS classes to an element.\n   *\n   * @param el - The element to add classes to.\n   * @param names - The class name(s) to add.\n   * @param [opts={}] - Optional settings.\n   *   Accepts: `transition`, `time`, and `blocking`.\n   */\n  addClass(el: HTMLElement, names: string | string[], opts?: BaseOpts): void;\n\n  /**\n   * Removes CSS classes from an element.\n   *\n   * @param el - The element to remove classes from.\n   * @param names - The class name(s) to remove.\n   * @param [opts={}] - Optional settings.\n   *   Accepts: `transition`, `time`, and `blocking`.\n   */\n  removeClass(el: HTMLElement, names: string | string[], opts?: BaseOpts): void;\n\n  /**\n   * Toggles CSS classes on an element.\n   *\n   * @param el - The element to toggle classes on.\n   * @param names - The class name(s) to toggle.\n   * @param [opts={}] - Optional settings.\n   *   Accepts: `transition`, `time`, and `blocking`.\n   */\n  toggleClass(el: HTMLElement, names: string | string[], opts?: BaseOpts): void;\n\n  /**\n   * Applies a CSS transition to an element.\n   *\n   * @param el - The element to apply the transition to.\n   * @param transition - The transition class(es) to apply.\n   *   Accepts a string of classes to apply when transitioning or\n   *   a 3-tuple containing the transition class, the class to apply\n   *   to start the transition, and the ending transition class, such as:\n   *\n   *       [\"ease-out duration-300\", \"opacity-100\", \"opacity-0\"]\n   *\n   * @param [opts={}] - Optional settings for timing and blocking behavior.\n   *   Accepts: `time` and `blocking`.\n   */\n  transition(\n    el: HTMLElement,\n    transition: string | string[],\n    opts?: TransitionCommandOpts,\n  ): void;\n\n  /**\n   * Sets an attribute on an element.\n   *\n   * @param el - The element to set the attribute on.\n   * @param attr - The attribute name to set.\n   * @param val - The value to set for the attribute.\n   */\n  setAttribute(el: HTMLElement, attr: string, val: string): void;\n\n  /**\n   * Removes an attribute from an element.\n   *\n   * @param el - The element to remove the attribute from.\n   * @param attr - The attribute name to remove.\n   */\n  removeAttribute(el: HTMLElement, attr: string): void;\n\n  /**\n   * Toggles an attribute on an element between two values.\n   *\n   * @param el - The element to toggle the attribute on.\n   * @param attr - The attribute name to toggle.\n   * @param val1 - The first value to toggle between.\n   * @param val2 - The second value to toggle between.\n   */\n  toggleAttribute(\n    el: HTMLElement,\n    attr: string,\n    val1: string,\n    val2: string,\n  ): void;\n\n  /**\n   * Pushes an event to the server.\n   *\n   * @param el - An element that belongs to the target LiveView / LiveComponent or a component ID.\n   *   To target a LiveComponent by its ID, pass a separate `target` in the options.\n   * @param type - The event name to push.\n   * @param [opts={}] - Optional settings.\n   *   Accepts: `value`, `target`, `page_loading`.\n   */\n  push(el: HTMLElement, type: string, opts?: PushOpts): void;\n\n  /**\n   * Sends a navigation event to the server and updates the browser's pushState history.\n   *\n   * @param href - The URL to navigate to.\n   * @param [opts={}] - Optional settings.\n   *   Accepts: `replace`.\n   */\n  navigate(href: string, opts?: NavigationOpts): void;\n\n  /**\n   * Sends a patch event to the server and updates the browser's pushState history.\n   *\n   * @param href - The URL to patch to.\n   * @param [opts={}] - Optional settings.\n   *   Accepts: `replace`.\n   */\n  patch(href: string, opts?: NavigationOpts): void;\n\n  /**\n   * Mark attributes as ignored, skipping them when patching the DOM.\n   *\n   * @param el - The element to ignore attributes on.\n   * @param attrs - The attribute name or names to ignore.\n   */\n  ignoreAttributes(el: HTMLElement, attrs: string | string[]): void;\n}\n\nexport default (\n  liveSocket: LiveSocket,\n  eventType: string | null,\n): AllJSCommands => {\n  return {\n    exec(el, encodedJS) {\n      liveSocket.execJS(el, encodedJS, eventType);\n    },\n    show(el, opts = {}) {\n      const owner = liveSocket.owner(el);\n      JS.show(\n        eventType,\n        owner,\n        el,\n        opts.display,\n        JS.transitionClasses(opts.transition),\n        opts.time,\n        opts.blocking,\n      );\n    },\n    hide(el, opts = {}) {\n      const owner = liveSocket.owner(el);\n      JS.hide(\n        eventType,\n        owner,\n        el,\n        null,\n        JS.transitionClasses(opts.transition),\n        opts.time,\n        opts.blocking,\n      );\n    },\n    toggle(el, opts = {}) {\n      const owner = liveSocket.owner(el);\n      const inTransition = JS.transitionClasses(opts.in);\n      const outTransition = JS.transitionClasses(opts.out);\n      JS.toggle(\n        eventType,\n        owner,\n        el,\n        opts.display,\n        inTransition,\n        outTransition,\n        opts.time,\n        opts.blocking,\n      );\n    },\n    addClass(el, names, opts = {}) {\n      const classNames = Array.isArray(names) ? names : names.split(\" \");\n      const owner = liveSocket.owner(el);\n      JS.addOrRemoveClasses(\n        el,\n        classNames,\n        [],\n        JS.transitionClasses(opts.transition),\n        opts.time,\n        owner,\n        opts.blocking,\n      );\n    },\n    removeClass(el, names, opts = {}) {\n      const classNames = Array.isArray(names) ? names : names.split(\" \");\n      const owner = liveSocket.owner(el);\n      JS.addOrRemoveClasses(\n        el,\n        [],\n        classNames,\n        JS.transitionClasses(opts.transition),\n        opts.time,\n        owner,\n        opts.blocking,\n      );\n    },\n    toggleClass(el, names, opts = {}) {\n      const classNames = Array.isArray(names) ? names : names.split(\" \");\n      const owner = liveSocket.owner(el);\n      JS.toggleClasses(\n        el,\n        classNames,\n        JS.transitionClasses(opts.transition),\n        opts.time,\n        owner,\n        opts.blocking,\n      );\n    },\n    transition(el, transition, opts = {}) {\n      const owner = liveSocket.owner(el);\n      JS.addOrRemoveClasses(\n        el,\n        [],\n        [],\n        JS.transitionClasses(transition),\n        opts.time,\n        owner,\n        opts.blocking,\n      );\n    },\n    setAttribute(el, attr, val) {\n      JS.setOrRemoveAttrs(el, [[attr, val]], []);\n    },\n    removeAttribute(el, attr) {\n      JS.setOrRemoveAttrs(el, [], [attr]);\n    },\n    toggleAttribute(el, attr, val1, val2) {\n      JS.toggleAttr(el, attr, val1, val2);\n    },\n    push(el, type, opts = {}) {\n      liveSocket.withinOwners(el, (view) => {\n        const data = opts.value || {};\n        delete opts.value;\n        let e = new CustomEvent(\"phx:exec\", { detail: { sourceElement: el } });\n        JS.exec(e, eventType, type, view, el, [\"push\", { data, ...opts }]);\n      });\n    },\n    navigate(href, opts = {}) {\n      const customEvent = new CustomEvent(\"phx:exec\");\n      liveSocket.historyRedirect(\n        customEvent,\n        href,\n        opts.replace ? \"replace\" : \"push\",\n        null,\n        null,\n      );\n    },\n    patch(href, opts = {}) {\n      const customEvent = new CustomEvent(\"phx:exec\");\n      liveSocket.pushHistoryPatch(\n        customEvent,\n        href,\n        opts.replace ? \"replace\" : \"push\",\n        null,\n      );\n    },\n    ignoreAttributes(el, attrs) {\n      JS.ignoreAttrs(el, Array.isArray(attrs) ? attrs : [attrs]);\n    },\n  };\n};\n\n/**\n * JSCommands for use with `liveSocket.js()`.\n * Includes the general `exec` command that requires an element.\n */\nexport type LiveSocketJSCommands = AllJSCommands;\n\n/**\n * JSCommands for use within a Hook.\n * The `exec` command is tailored for hooks, not requiring an explicit element.\n */\nexport interface HookJSCommands extends Omit<AllJSCommands, \"exec\"> {\n  /**\n   * Executes a JS command in the context of the hook's element.\n   *\n   * @param encodedJS - The encoded JS command with operations to execute.\n   */\n  exec(encodedJS: EncodedJS): void;\n}\n"
  },
  {
    "path": "assets/js/phoenix_live_view/live_socket.js",
    "content": "import {\n  BINDING_PREFIX,\n  CONSECUTIVE_RELOADS,\n  DEFAULTS,\n  FAILSAFE_JITTER,\n  LOADER_TIMEOUT,\n  DISCONNECTED_TIMEOUT,\n  MAX_RELOADS,\n  PHX_DEBOUNCE,\n  PHX_DROP_TARGET,\n  PHX_HAS_FOCUSED,\n  PHX_KEY,\n  PHX_LINK_STATE,\n  PHX_LIVE_LINK,\n  PHX_LV_DEBUG,\n  PHX_LV_LATENCY_SIM,\n  PHX_LV_PROFILE,\n  PHX_LV_HISTORY_POSITION,\n  PHX_MAIN,\n  PHX_PARENT_ID,\n  PHX_VIEW_SELECTOR,\n  PHX_ROOT_ID,\n  PHX_THROTTLE,\n  PHX_TRACK_UPLOADS,\n  PHX_SESSION,\n  RELOAD_JITTER_MIN,\n  RELOAD_JITTER_MAX,\n  PHX_REF_SRC,\n  PHX_RELOAD_STATUS,\n  PHX_RUNTIME_HOOK,\n  PHX_DROP_TARGET_ACTIVE_CLASS,\n  PHX_TELEPORTED_SRC,\n} from \"./constants\";\n\nimport {\n  clone,\n  closestPhxBinding,\n  closure,\n  debug,\n  maybe,\n  logError,\n  eventContainsFiles,\n} from \"./utils\";\n\nimport Browser from \"./browser\";\nimport DOM from \"./dom\";\nimport Hooks from \"./hooks\";\nimport LiveUploader from \"./live_uploader\";\nimport View from \"./view\";\nimport JS from \"./js\";\nimport jsCommands from \"./js_commands\";\n\nexport const isUsedInput = (el) => DOM.isUsedInput(el);\n\nexport default class LiveSocket {\n  constructor(url, phxSocket, opts = {}) {\n    this.unloaded = false;\n    if (!phxSocket || phxSocket.constructor.name === \"Object\") {\n      throw new Error(`\n      a phoenix Socket must be provided as the second argument to the LiveSocket constructor. For example:\n\n          import {Socket} from \"phoenix\"\n          import {LiveSocket} from \"phoenix_live_view\"\n          let liveSocket = new LiveSocket(\"/live\", Socket, {...})\n      `);\n    }\n    this.socket = new phxSocket(url, opts);\n    this.bindingPrefix = opts.bindingPrefix || BINDING_PREFIX;\n    this.opts = opts;\n    this.params = closure(opts.params || {});\n    this.viewLogger = opts.viewLogger;\n    this.metadataCallbacks = opts.metadata || {};\n    this.defaults = Object.assign(clone(DEFAULTS), opts.defaults || {});\n    this.prevActive = null;\n    this.silenced = false;\n    this.main = null;\n    this.outgoingMainEl = null;\n    this.clickStartedAtTarget = null;\n    this.linkRef = 1;\n    this.roots = {};\n    this.href = window.location.href;\n    this.pendingLink = null;\n    this.currentLocation = clone(window.location);\n    this.hooks = opts.hooks || {};\n    this.uploaders = opts.uploaders || {};\n    this.loaderTimeout = opts.loaderTimeout || LOADER_TIMEOUT;\n    this.disconnectedTimeout = opts.disconnectedTimeout || DISCONNECTED_TIMEOUT;\n    /**\n     * @type {ReturnType<typeof setTimeout> | null}\n     */\n    this.reloadWithJitterTimer = null;\n    this.maxReloads = opts.maxReloads || MAX_RELOADS;\n    this.reloadJitterMin = opts.reloadJitterMin || RELOAD_JITTER_MIN;\n    this.reloadJitterMax = opts.reloadJitterMax || RELOAD_JITTER_MAX;\n    this.failsafeJitter = opts.failsafeJitter || FAILSAFE_JITTER;\n    this.localStorage = opts.localStorage || window.localStorage;\n    this.sessionStorage = opts.sessionStorage || window.sessionStorage;\n    this.boundTopLevelEvents = false;\n    this.boundEventNames = new Set();\n    this.blockPhxChangeWhileComposing =\n      opts.blockPhxChangeWhileComposing || false;\n    this.serverCloseRef = null;\n    this.domCallbacks = Object.assign(\n      {\n        jsQuerySelectorAll: null,\n        onPatchStart: closure(),\n        onPatchEnd: closure(),\n        onNodeAdded: closure(),\n        onBeforeElUpdated: closure(),\n      },\n      opts.dom || {},\n    );\n    this.transitions = new TransitionSet();\n    this.currentHistoryPosition =\n      parseInt(this.sessionStorage.getItem(PHX_LV_HISTORY_POSITION)) || 0;\n    window.addEventListener(\"pagehide\", (_e) => {\n      this.unloaded = true;\n    });\n    this.socket.onOpen(() => {\n      if (this.isUnloaded()) {\n        // reload page if being restored from back/forward cache and browser does not emit \"pageshow\"\n        window.location.reload();\n      }\n    });\n  }\n\n  // public\n\n  version() {\n    return LV_VSN;\n  }\n\n  isProfileEnabled() {\n    return this.sessionStorage.getItem(PHX_LV_PROFILE) === \"true\";\n  }\n\n  isDebugEnabled() {\n    return this.sessionStorage.getItem(PHX_LV_DEBUG) === \"true\";\n  }\n\n  isDebugDisabled() {\n    return this.sessionStorage.getItem(PHX_LV_DEBUG) === \"false\";\n  }\n\n  enableDebug() {\n    this.sessionStorage.setItem(PHX_LV_DEBUG, \"true\");\n  }\n\n  enableProfiling() {\n    this.sessionStorage.setItem(PHX_LV_PROFILE, \"true\");\n  }\n\n  disableDebug() {\n    this.sessionStorage.setItem(PHX_LV_DEBUG, \"false\");\n  }\n\n  disableProfiling() {\n    this.sessionStorage.removeItem(PHX_LV_PROFILE);\n  }\n\n  enableLatencySim(upperBoundMs) {\n    this.enableDebug();\n    console.log(\n      \"latency simulator enabled for the duration of this browser session. Call disableLatencySim() to disable\",\n    );\n    this.sessionStorage.setItem(PHX_LV_LATENCY_SIM, upperBoundMs);\n  }\n\n  disableLatencySim() {\n    this.sessionStorage.removeItem(PHX_LV_LATENCY_SIM);\n  }\n\n  getLatencySim() {\n    const str = this.sessionStorage.getItem(PHX_LV_LATENCY_SIM);\n    return str ? parseInt(str) : null;\n  }\n\n  getSocket() {\n    return this.socket;\n  }\n\n  connect() {\n    // enable debug by default if on localhost and not explicitly disabled\n    if (window.location.hostname === \"localhost\" && !this.isDebugDisabled()) {\n      this.enableDebug();\n    }\n    const doConnect = () => {\n      this.resetReloadStatus();\n      if (this.joinRootViews()) {\n        this.bindTopLevelEvents();\n        this.socket.connect();\n      } else if (this.main) {\n        this.socket.connect();\n      } else {\n        this.bindTopLevelEvents({ dead: true });\n      }\n      this.joinDeadView();\n    };\n    if (\n      [\"complete\", \"loaded\", \"interactive\"].indexOf(document.readyState) >= 0\n    ) {\n      doConnect();\n    } else {\n      document.addEventListener(\"DOMContentLoaded\", () => doConnect());\n    }\n  }\n\n  disconnect(callback) {\n    clearTimeout(this.reloadWithJitterTimer);\n    // remove the socket close listener to avoid trying to handle\n    // a server close event when it is actually caused by us disconnecting\n    if (this.serverCloseRef) {\n      this.socket.off(this.serverCloseRef);\n      this.serverCloseRef = null;\n    }\n    this.socket.disconnect(callback);\n  }\n\n  replaceTransport(transport) {\n    clearTimeout(this.reloadWithJitterTimer);\n    this.socket.replaceTransport(transport);\n    this.connect();\n  }\n\n  /**\n   * @param {HTMLElement} el\n   * @param {import(\"./js_commands\").EncodedJS} encodedJS\n   * @param {string | null} [eventType]\n   */\n  execJS(el, encodedJS, eventType = null) {\n    const e = new CustomEvent(\"phx:exec\", { detail: { sourceElement: el } });\n    this.owner(el, (view) => JS.exec(e, eventType, encodedJS, view, el));\n  }\n\n  /**\n   * Returns an object with methods to manipulate the DOM and execute JavaScript.\n   * The applied changes integrate with server DOM patching.\n   *\n   * @returns {import(\"./js_commands\").LiveSocketJSCommands}\n   */\n  js() {\n    return jsCommands(this, \"js\");\n  }\n\n  // private\n\n  unload() {\n    if (this.unloaded) {\n      return;\n    }\n    if (this.main && this.isConnected()) {\n      this.log(this.main, \"socket\", () => [\"disconnect for page nav\"]);\n    }\n    this.unloaded = true;\n    this.destroyAllViews();\n    this.disconnect();\n  }\n\n  triggerDOM(kind, args) {\n    this.domCallbacks[kind](...args);\n  }\n\n  time(name, func) {\n    if (!this.isProfileEnabled() || !console.time) {\n      return func();\n    }\n    console.time(name);\n    const result = func();\n    console.timeEnd(name);\n    return result;\n  }\n\n  log(view, kind, msgCallback) {\n    if (this.viewLogger) {\n      const [msg, obj] = msgCallback();\n      this.viewLogger(view, kind, msg, obj);\n    } else if (this.isDebugEnabled()) {\n      const [msg, obj] = msgCallback();\n      debug(view, kind, msg, obj);\n    }\n  }\n\n  requestDOMUpdate(callback) {\n    this.transitions.after(callback);\n  }\n\n  asyncTransition(promise) {\n    this.transitions.addAsyncTransition(promise);\n  }\n\n  transition(time, onStart, onDone = function () {}) {\n    this.transitions.addTransition(time, onStart, onDone);\n  }\n\n  onChannel(channel, event, cb) {\n    channel.on(event, (data) => {\n      const latency = this.getLatencySim();\n      if (!latency) {\n        cb(data);\n      } else {\n        setTimeout(() => cb(data), latency);\n      }\n    });\n  }\n\n  reloadWithJitter(view, log) {\n    clearTimeout(this.reloadWithJitterTimer);\n    this.disconnect();\n    const minMs = this.reloadJitterMin;\n    const maxMs = this.reloadJitterMax;\n    let afterMs = Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs;\n    const tries = Browser.updateLocal(\n      this.localStorage,\n      window.location.pathname,\n      CONSECUTIVE_RELOADS,\n      0,\n      (count) => count + 1,\n    );\n    if (tries >= this.maxReloads) {\n      afterMs = this.failsafeJitter;\n    }\n    this.reloadWithJitterTimer = setTimeout(() => {\n      // if view has recovered, such as transport replaced, then cancel\n      if (view.isDestroyed() || view.isConnected()) {\n        return;\n      }\n      view.destroy();\n      log\n        ? log()\n        : this.log(view, \"join\", () => [\n            `encountered ${tries} consecutive reloads`,\n          ]);\n      if (tries >= this.maxReloads) {\n        this.log(view, \"join\", () => [\n          `exceeded ${this.maxReloads} consecutive reloads. Entering failsafe mode`,\n        ]);\n      }\n      if (this.hasPendingLink()) {\n        window.location = this.pendingLink;\n      } else {\n        window.location.reload();\n      }\n    }, afterMs);\n  }\n\n  getHookDefinition(name) {\n    if (!name) {\n      return;\n    }\n    return (\n      this.maybeInternalHook(name) ||\n      this.hooks[name] ||\n      this.maybeRuntimeHook(name)\n    );\n  }\n\n  maybeInternalHook(name) {\n    return name && name.startsWith(\"Phoenix.\") && Hooks[name.split(\".\")[1]];\n  }\n\n  maybeRuntimeHook(name) {\n    const runtimeHook = document.querySelector(\n      `script[${PHX_RUNTIME_HOOK}=\"${CSS.escape(name)}\"]`,\n    );\n    if (!runtimeHook) {\n      return;\n    }\n    let callbacks = window[`phx_hook_${name}`];\n    if (!callbacks || typeof callbacks !== \"function\") {\n      logError(\"a runtime hook must be a function\", runtimeHook);\n      return;\n    }\n    const hookDefiniton = callbacks();\n    if (\n      hookDefiniton &&\n      (typeof hookDefiniton === \"object\" || typeof hookDefiniton === \"function\")\n    ) {\n      return hookDefiniton;\n    }\n    logError(\n      \"runtime hook must return an object with hook callbacks or an instance of ViewHook\",\n      runtimeHook,\n    );\n  }\n\n  isUnloaded() {\n    return this.unloaded;\n  }\n\n  isConnected() {\n    return this.socket.isConnected();\n  }\n\n  getBindingPrefix() {\n    return this.bindingPrefix;\n  }\n\n  binding(kind) {\n    return `${this.getBindingPrefix()}${kind}`;\n  }\n\n  channel(topic, params) {\n    return this.socket.channel(topic, params);\n  }\n\n  joinDeadView() {\n    const body = document.body;\n    if (\n      body &&\n      !this.isPhxView(body) &&\n      !this.isPhxView(document.firstElementChild)\n    ) {\n      const view = this.newRootView(body);\n      view.setHref(this.getHref());\n      view.joinDead();\n      if (!this.main) {\n        this.main = view;\n      }\n      window.requestAnimationFrame(() => {\n        view.execNewMounted();\n        // restore scroll position when navigating from an external / non-live page\n        this.maybeScroll(history.state?.scroll);\n      });\n    }\n  }\n\n  joinRootViews() {\n    let rootsFound = false;\n    DOM.all(\n      document,\n      `${PHX_VIEW_SELECTOR}:not([${PHX_PARENT_ID}])`,\n      (rootEl) => {\n        if (!this.getRootById(rootEl.id)) {\n          const view = this.newRootView(rootEl);\n          // stickies cannot be mounted at the router and therefore should not\n          // get a href set on them\n          if (!DOM.isPhxSticky(rootEl)) {\n            view.setHref(this.getHref());\n          }\n          view.join();\n          if (rootEl.hasAttribute(PHX_MAIN)) {\n            this.main = view;\n          }\n        }\n        rootsFound = true;\n      },\n    );\n    return rootsFound;\n  }\n\n  redirect(to, flash, reloadToken) {\n    if (reloadToken) {\n      Browser.setCookie(PHX_RELOAD_STATUS, reloadToken, 60);\n    }\n    this.unload();\n    Browser.redirect(to, flash);\n  }\n\n  replaceMain(\n    href,\n    flash,\n    callback = null,\n    linkRef = this.setPendingLink(href),\n  ) {\n    const liveReferer = this.currentLocation.href;\n    this.outgoingMainEl = this.outgoingMainEl || this.main.el;\n\n    const stickies = DOM.findPhxSticky(document) || [];\n    const removeEls = DOM.all(\n      this.outgoingMainEl,\n      `[${this.binding(\"remove\")}]`,\n    ).filter((el) => !DOM.isChildOfAny(el, stickies));\n\n    const newMainEl = DOM.cloneNode(this.outgoingMainEl, \"\");\n    this.main.showLoader(this.loaderTimeout);\n    this.main.destroy();\n\n    this.main = this.newRootView(newMainEl, flash, liveReferer);\n    this.main.setRedirect(href);\n    this.transitionRemoves(removeEls);\n    this.main.join((joinCount, onDone) => {\n      if (joinCount === 1 && this.commitPendingLink(linkRef)) {\n        this.requestDOMUpdate(() => {\n          // remove phx-remove els right before we replace the main element\n          removeEls.forEach((el) => el.remove());\n          stickies.forEach((el) => newMainEl.appendChild(el));\n          this.outgoingMainEl.replaceWith(newMainEl);\n          this.outgoingMainEl = null;\n          callback && callback(linkRef);\n          onDone();\n        });\n      }\n    });\n  }\n\n  transitionRemoves(elements, callback) {\n    const removeAttr = this.binding(\"remove\");\n    const silenceEvents = (e) => {\n      e.preventDefault();\n      e.stopImmediatePropagation();\n    };\n    elements.forEach((el) => {\n      // prevent all listeners we care about from bubbling to window\n      // since we are removing the element\n      for (const event of this.boundEventNames) {\n        el.addEventListener(event, silenceEvents, true);\n      }\n      this.execJS(el, el.getAttribute(removeAttr), \"remove\");\n    });\n    // remove the silenced listeners when transitions are done incase the element is re-used\n    // and call caller's callback as soon as we are done with transitions\n    this.requestDOMUpdate(() => {\n      elements.forEach((el) => {\n        for (const event of this.boundEventNames) {\n          el.removeEventListener(event, silenceEvents, true);\n        }\n      });\n      callback && callback();\n    });\n  }\n\n  isPhxView(el) {\n    return el.getAttribute && el.getAttribute(PHX_SESSION) !== null;\n  }\n\n  newRootView(el, flash, liveReferer) {\n    const view = new View(el, this, null, flash, liveReferer);\n    this.roots[view.id] = view;\n    return view;\n  }\n\n  owner(childEl, callback) {\n    let view;\n    const viewEl = DOM.closestViewEl(childEl);\n    if (viewEl) {\n      // it can happen that we find a view that is already destroyed;\n      // in that case we DO NOT want to fallback to the main element\n      view = this.getViewByEl(viewEl);\n    } else {\n      if (!childEl.isConnected) {\n        // if the element is not part of the DOM any more\n        // there's no owner and we should not do fall back\n        return null;\n      }\n      view = this.main;\n    }\n    return view && callback ? callback(view) : view;\n  }\n\n  withinOwners(childEl, callback) {\n    this.owner(childEl, (view) => callback(view, childEl));\n  }\n\n  getViewByEl(el) {\n    const rootId = el.getAttribute(PHX_ROOT_ID);\n    return maybe(this.getRootById(rootId), (root) =>\n      root.getDescendentByEl(el),\n    );\n  }\n\n  getRootById(id) {\n    return this.roots[id];\n  }\n\n  destroyAllViews() {\n    for (const id in this.roots) {\n      this.roots[id].destroy();\n      delete this.roots[id];\n    }\n    this.main = null;\n  }\n\n  destroyViewByEl(el) {\n    const root = this.getRootById(el.getAttribute(PHX_ROOT_ID));\n    if (root && root.id === el.id) {\n      root.destroy();\n      delete this.roots[root.id];\n    } else if (root) {\n      root.destroyDescendent(el.id);\n    }\n  }\n\n  getActiveElement() {\n    return document.activeElement;\n  }\n\n  dropActiveElement(view) {\n    if (this.prevActive && view.ownsElement(this.prevActive)) {\n      this.prevActive = null;\n    }\n  }\n\n  restorePreviouslyActiveFocus() {\n    if (\n      this.prevActive &&\n      this.prevActive !== document.body &&\n      this.prevActive instanceof HTMLElement\n    ) {\n      this.prevActive.focus();\n    }\n  }\n\n  blurActiveElement() {\n    this.prevActive = this.getActiveElement();\n    if (\n      this.prevActive !== document.body &&\n      this.prevActive instanceof HTMLElement\n    ) {\n      this.prevActive.blur();\n    }\n  }\n\n  /**\n   * @param {{dead?: boolean}} [options={}]\n   */\n  bindTopLevelEvents({ dead } = {}) {\n    if (this.boundTopLevelEvents) {\n      return;\n    }\n\n    this.boundTopLevelEvents = true;\n    // enter failsafe reload if server has gone away intentionally, such as \"disconnect\" broadcast\n    this.serverCloseRef = this.socket.onClose((event) => {\n      // failsafe reload if normal closure and we still have a main LV\n      if (event && event.code === 1000 && this.main) {\n        return this.reloadWithJitter(this.main);\n      }\n    });\n    document.body.addEventListener(\"click\", function () {}); // ensure all click events bubble for mobile Safari\n    window.addEventListener(\n      \"pageshow\",\n      (e) => {\n        if (e.persisted) {\n          // reload page if being restored from back/forward cache\n          this.getSocket().disconnect();\n          this.withPageLoading({ to: window.location.href, kind: \"redirect\" });\n          window.location.reload();\n        }\n      },\n      true,\n    );\n    if (!dead) {\n      this.bindNav();\n    }\n    this.bindClicks();\n    if (!dead) {\n      this.bindForms();\n    }\n    this.bind(\n      { keyup: \"keyup\", keydown: \"keydown\" },\n      (e, type, view, targetEl, phxEvent, _phxTarget) => {\n        const matchKey = targetEl.getAttribute(this.binding(PHX_KEY));\n        const pressedKey = e.key && e.key.toLowerCase(); // chrome clicked autocompletes send a keydown without key\n        if (matchKey && matchKey.toLowerCase() !== pressedKey) {\n          return;\n        }\n\n        const data = { key: e.key, ...this.eventMeta(type, e, targetEl) };\n        JS.exec(e, type, phxEvent, view, targetEl, [\"push\", { data }]);\n      },\n    );\n    this.bind(\n      { blur: \"focusout\", focus: \"focusin\" },\n      (e, type, view, targetEl, phxEvent, phxTarget) => {\n        if (!phxTarget) {\n          const data = { key: e.key, ...this.eventMeta(type, e, targetEl) };\n          JS.exec(e, type, phxEvent, view, targetEl, [\"push\", { data }]);\n        }\n      },\n    );\n    this.bind(\n      { blur: \"blur\", focus: \"focus\" },\n      (e, type, view, targetEl, phxEvent, phxTarget) => {\n        // blur and focus are triggered on document and window. Discard one to avoid dups\n        if (phxTarget === \"window\") {\n          const data = this.eventMeta(type, e, targetEl);\n          JS.exec(e, type, phxEvent, view, targetEl, [\"push\", { data }]);\n        }\n      },\n    );\n    this.on(\"dragover\", (e) => e.preventDefault());\n    this.on(\"dragenter\", (e) => {\n      const dropzone = closestPhxBinding(\n        e.target,\n        this.binding(PHX_DROP_TARGET),\n      );\n\n      if (!dropzone || !(dropzone instanceof HTMLElement)) {\n        return;\n      }\n\n      if (eventContainsFiles(e)) {\n        this.js().addClass(dropzone, PHX_DROP_TARGET_ACTIVE_CLASS);\n      }\n    });\n    this.on(\"dragleave\", (e) => {\n      const dropzone = closestPhxBinding(\n        e.target,\n        this.binding(PHX_DROP_TARGET),\n      );\n\n      if (!dropzone || !(dropzone instanceof HTMLElement)) {\n        return;\n      }\n\n      // Avoid add/remove jitter in the case that we drag into a new child and that child would\n      // resolve their closest drop target to the current dropzone element\n      const rect = dropzone.getBoundingClientRect();\n      if (\n        e.clientX <= rect.left ||\n        e.clientX >= rect.right ||\n        e.clientY <= rect.top ||\n        e.clientY >= rect.bottom\n      ) {\n        this.js().removeClass(dropzone, PHX_DROP_TARGET_ACTIVE_CLASS);\n      }\n    });\n    this.on(\"drop\", (e) => {\n      e.preventDefault();\n\n      const dropzone = closestPhxBinding(\n        e.target,\n        this.binding(PHX_DROP_TARGET),\n      );\n      if (!dropzone || !(dropzone instanceof HTMLElement)) {\n        return;\n      }\n      this.js().removeClass(dropzone, PHX_DROP_TARGET_ACTIVE_CLASS);\n\n      const dropTargetId = dropzone.getAttribute(this.binding(PHX_DROP_TARGET));\n      const dropTarget = dropTargetId && document.getElementById(dropTargetId);\n      const files = Array.from(e.dataTransfer.files || []);\n      if (\n        !dropTarget ||\n        !(dropTarget instanceof HTMLInputElement) ||\n        dropTarget.disabled ||\n        files.length === 0 ||\n        !(dropTarget.files instanceof FileList)\n      ) {\n        return;\n      }\n\n      LiveUploader.trackFiles(dropTarget, files, e.dataTransfer);\n      dropTarget.dispatchEvent(new Event(\"input\", { bubbles: true }));\n    });\n    this.on(PHX_TRACK_UPLOADS, (e) => {\n      const uploadTarget = e.target;\n      if (!DOM.isUploadInput(uploadTarget)) {\n        return;\n      }\n      const files = Array.from(e.detail.files || []).filter(\n        (f) => f instanceof File || f instanceof Blob,\n      );\n      LiveUploader.trackFiles(uploadTarget, files);\n      uploadTarget.dispatchEvent(new Event(\"input\", { bubbles: true }));\n    });\n  }\n\n  eventMeta(eventName, e, targetEl) {\n    const callback = this.metadataCallbacks[eventName];\n    return callback ? callback(e, targetEl) : {};\n  }\n\n  setPendingLink(href) {\n    this.linkRef++;\n    this.pendingLink = href;\n    this.resetReloadStatus();\n    return this.linkRef;\n  }\n\n  // anytime we are navigating or connecting, drop reload cookie in case\n  // we issue the cookie but the next request was interrupted and the server never dropped it\n  resetReloadStatus() {\n    Browser.deleteCookie(PHX_RELOAD_STATUS);\n  }\n\n  commitPendingLink(linkRef) {\n    if (this.linkRef !== linkRef) {\n      return false;\n    } else {\n      this.href = this.pendingLink;\n      this.pendingLink = null;\n      return true;\n    }\n  }\n\n  getHref() {\n    return this.href;\n  }\n\n  hasPendingLink() {\n    return !!this.pendingLink;\n  }\n\n  bind(events, callback) {\n    for (const event in events) {\n      const browserEventName = events[event];\n\n      this.on(browserEventName, (e) => {\n        const binding = this.binding(event);\n        const windowBinding = this.binding(`window-${event}`);\n        const targetPhxEvent =\n          e.target.getAttribute && e.target.getAttribute(binding);\n        if (targetPhxEvent) {\n          this.debounce(e.target, e, browserEventName, () => {\n            this.withinOwners(e.target, (view) => {\n              callback(e, event, view, e.target, targetPhxEvent, null);\n            });\n          });\n        } else {\n          DOM.all(document, `[${windowBinding}]`, (el) => {\n            const phxEvent = el.getAttribute(windowBinding);\n            this.debounce(el, e, browserEventName, () => {\n              this.withinOwners(el, (view) => {\n                callback(e, event, view, el, phxEvent, \"window\");\n              });\n            });\n          });\n        }\n      });\n    }\n  }\n\n  bindClicks() {\n    this.on(\"mousedown\", (e) => (this.clickStartedAtTarget = e.target));\n    this.bindClick(\"click\", \"click\");\n  }\n\n  bindClick(eventName, bindingName) {\n    const click = this.binding(bindingName);\n    window.addEventListener(\n      eventName,\n      (e) => {\n        let target = null;\n        // a synthetic click event (detail 0) will not have caused a mousedown event,\n        // therefore the clickStartedAtTarget is stale\n        if (e.detail === 0) this.clickStartedAtTarget = e.target;\n        const clickStartedAtTarget = this.clickStartedAtTarget || e.target;\n        // when searching the target for the click event, we always want to\n        // use the actual event target, see #3372\n        target = closestPhxBinding(e.target, click);\n        this.dispatchClickAway(e, clickStartedAtTarget);\n        this.clickStartedAtTarget = null;\n        const phxEvent = target && target.getAttribute(click);\n        if (!phxEvent) {\n          if (DOM.isNewPageClick(e, window.location)) {\n            this.unload();\n          }\n          return;\n        }\n\n        if (target.getAttribute(\"href\") === \"#\") {\n          e.preventDefault();\n        }\n\n        // noop if we are in the middle of awaiting an ack for this el already\n        if (target.hasAttribute(PHX_REF_SRC)) {\n          return;\n        }\n\n        this.debounce(target, e, \"click\", () => {\n          this.withinOwners(target, (view) => {\n            JS.exec(e, \"click\", phxEvent, view, target, [\n              \"push\",\n              { data: this.eventMeta(\"click\", e, target) },\n            ]);\n          });\n        });\n      },\n      false,\n    );\n  }\n\n  dispatchClickAway(e, clickStartedAt) {\n    const phxClickAway = this.binding(\"click-away\");\n    const portal = clickStartedAt.closest(`[${PHX_TELEPORTED_SRC}]`);\n    const portalStartedAt =\n      portal && DOM.byId(portal.getAttribute(PHX_TELEPORTED_SRC));\n    DOM.all(document, `[${phxClickAway}]`, (el) => {\n      let startedAt = clickStartedAt;\n      if (portal && !portal.contains(el)) {\n        // If we have a portal and the click-away element is not inside it,\n        // then treat the portal source as the starting point instead.\n        startedAt = portalStartedAt;\n      }\n      if (\n        !(\n          el.isSameNode(startedAt) ||\n          el.contains(startedAt) ||\n          // When clicking a link with custom method,\n          // phoenix_html triggers a click on a submit button\n          // of a hidden form appended to the body. For such cases\n          // where the clicked target is hidden, we skip click-away.\n          //\n          // Also, when we have a portal, we don't want to check the visibility\n          // of the portal source, as it's a <template> that is always not visible.\n          // Instead, check the visibility of the original click target.\n          !JS.isVisible(clickStartedAt)\n        )\n      ) {\n        this.withinOwners(el, (view) => {\n          const phxEvent = el.getAttribute(phxClickAway);\n          if (JS.isVisible(el) && JS.isInViewport(el)) {\n            JS.exec(e, \"click\", phxEvent, view, el, [\n              \"push\",\n              { data: this.eventMeta(\"click\", e, e.target) },\n            ]);\n          }\n        });\n      }\n    });\n  }\n\n  bindNav() {\n    if (!Browser.canPushState()) {\n      return;\n    }\n    if (history.scrollRestoration) {\n      history.scrollRestoration = \"manual\";\n    }\n    let scrollTimer = null;\n    window.addEventListener(\"scroll\", (_e) => {\n      clearTimeout(scrollTimer);\n      scrollTimer = setTimeout(() => {\n        Browser.updateCurrentState((state) =>\n          Object.assign(state, { scroll: window.scrollY }),\n        );\n      }, 100);\n    });\n    window.addEventListener(\n      \"popstate\",\n      (event) => {\n        if (!this.registerNewLocation(window.location)) {\n          return;\n        }\n        const { type, backType, id, scroll, position } = event.state || {};\n        const href = window.location.href;\n\n        // Compare positions to determine direction\n        const isForward = position > this.currentHistoryPosition;\n        const navType = isForward ? type : backType || type;\n\n        // Update current position\n        this.currentHistoryPosition = position || 0;\n        this.sessionStorage.setItem(\n          PHX_LV_HISTORY_POSITION,\n          this.currentHistoryPosition.toString(),\n        );\n\n        DOM.dispatchEvent(window, \"phx:navigate\", {\n          detail: {\n            href,\n            patch: navType === \"patch\",\n            pop: true,\n            direction: isForward ? \"forward\" : \"backward\",\n          },\n        });\n        this.requestDOMUpdate(() => {\n          const callback = () => {\n            this.maybeScroll(scroll);\n          };\n          if (\n            this.main.isConnected() &&\n            navType === \"patch\" &&\n            id === this.main.id\n          ) {\n            this.main.pushLinkPatch(event, href, null, callback);\n          } else {\n            this.replaceMain(href, null, callback);\n          }\n        });\n      },\n      false,\n    );\n    window.addEventListener(\n      \"click\",\n      (e) => {\n        const target = closestPhxBinding(e.target, PHX_LIVE_LINK);\n        const type = target && target.getAttribute(PHX_LIVE_LINK);\n        if (!type || !this.isConnected() || !this.main || DOM.wantsNewTab(e)) {\n          return;\n        }\n\n        // When wrapping an SVG element in an anchor tag, the href can be an SVGAnimatedString\n        const href =\n          target.href instanceof SVGAnimatedString\n            ? target.href.baseVal\n            : target.href;\n\n        const linkState = target.getAttribute(PHX_LINK_STATE);\n        e.preventDefault();\n        e.stopImmediatePropagation(); // do not bubble click to regular phx-click bindings\n        if (this.pendingLink === href) {\n          return;\n        }\n\n        this.requestDOMUpdate(() => {\n          if (type === \"patch\") {\n            this.pushHistoryPatch(e, href, linkState, target);\n          } else if (type === \"redirect\") {\n            this.historyRedirect(e, href, linkState, null, target);\n          } else {\n            throw new Error(\n              `expected ${PHX_LIVE_LINK} to be \"patch\" or \"redirect\", got: ${type}`,\n            );\n          }\n          const phxClick = target.getAttribute(this.binding(\"click\"));\n          if (phxClick) {\n            this.requestDOMUpdate(() => this.execJS(target, phxClick, \"click\"));\n          }\n        });\n      },\n      false,\n    );\n  }\n\n  maybeScroll(scroll) {\n    if (typeof scroll === \"number\") {\n      requestAnimationFrame(() => {\n        window.scrollTo(0, scroll);\n      }); // the body needs to render before we scroll.\n    }\n  }\n\n  dispatchEvent(event, payload = {}) {\n    DOM.dispatchEvent(window, `phx:${event}`, { detail: payload });\n  }\n\n  dispatchEvents(events) {\n    events.forEach(([event, payload]) => this.dispatchEvent(event, payload));\n  }\n\n  withPageLoading(info, callback) {\n    DOM.dispatchEvent(window, \"phx:page-loading-start\", { detail: info });\n    const done = () =>\n      DOM.dispatchEvent(window, \"phx:page-loading-stop\", { detail: info });\n    return callback ? callback(done) : done;\n  }\n\n  pushHistoryPatch(e, href, linkState, targetEl) {\n    if (!this.isConnected() || !this.main.isMain()) {\n      return Browser.redirect(href);\n    }\n\n    this.withPageLoading({ to: href, kind: \"patch\" }, (done) => {\n      this.main.pushLinkPatch(e, href, targetEl, (linkRef) => {\n        this.historyPatch(href, linkState, linkRef);\n        done();\n      });\n    });\n  }\n\n  historyPatch(href, linkState, linkRef = this.setPendingLink(href)) {\n    if (!this.commitPendingLink(linkRef)) {\n      return;\n    }\n\n    // Increment position for new state\n    this.currentHistoryPosition++;\n    this.sessionStorage.setItem(\n      PHX_LV_HISTORY_POSITION,\n      this.currentHistoryPosition.toString(),\n    );\n\n    // store the type for back navigation\n    Browser.updateCurrentState((state) => ({ ...state, backType: \"patch\" }));\n\n    Browser.pushState(\n      linkState,\n      {\n        type: \"patch\",\n        id: this.main.id,\n        position: this.currentHistoryPosition,\n      },\n      href,\n    );\n\n    DOM.dispatchEvent(window, \"phx:navigate\", {\n      detail: { patch: true, href, pop: false, direction: \"forward\" },\n    });\n    this.registerNewLocation(window.location);\n  }\n\n  historyRedirect(e, href, linkState, flash, targetEl) {\n    const clickLoading = targetEl && e.isTrusted && e.type !== \"popstate\";\n    if (clickLoading) {\n      targetEl.classList.add(\"phx-click-loading\");\n    }\n    if (!this.isConnected() || !this.main.isMain()) {\n      return Browser.redirect(href, flash);\n    }\n\n    // convert to full href if only path prefix\n    if (/^\\/$|^\\/[^\\/]+.*$/.test(href)) {\n      const { protocol, host } = window.location;\n      href = `${protocol}//${host}${href}`;\n    }\n    const scroll = window.scrollY;\n    this.withPageLoading({ to: href, kind: \"redirect\" }, (done) => {\n      this.replaceMain(href, flash, (linkRef) => {\n        if (linkRef === this.linkRef) {\n          // Increment position for new state\n          this.currentHistoryPosition++;\n          this.sessionStorage.setItem(\n            PHX_LV_HISTORY_POSITION,\n            this.currentHistoryPosition.toString(),\n          );\n\n          // store the type for back navigation\n          Browser.updateCurrentState((state) => ({\n            ...state,\n            backType: \"redirect\",\n          }));\n\n          Browser.pushState(\n            linkState,\n            {\n              type: \"redirect\",\n              id: this.main.id,\n              scroll: scroll,\n              position: this.currentHistoryPosition,\n            },\n            href,\n          );\n\n          DOM.dispatchEvent(window, \"phx:navigate\", {\n            detail: { href, patch: false, pop: false, direction: \"forward\" },\n          });\n          this.registerNewLocation(window.location);\n        }\n        // explicitly undo click-loading class\n        // (in case it originated in a sticky live view, otherwise it would be removed anyway)\n        if (clickLoading) {\n          targetEl.classList.remove(\"phx-click-loading\");\n        }\n        done();\n      });\n    });\n  }\n\n  registerNewLocation(newLocation) {\n    const { pathname, search } = this.currentLocation;\n    if (pathname + search === newLocation.pathname + newLocation.search) {\n      return false;\n    } else {\n      this.currentLocation = clone(newLocation);\n      return true;\n    }\n  }\n\n  bindForms() {\n    let iterations = 0;\n    let externalFormSubmitted = false;\n\n    // disable forms on submit that track phx-change but perform external submit\n    this.on(\"submit\", (e) => {\n      const phxSubmit = e.target.getAttribute(this.binding(\"submit\"));\n      const phxChange = e.target.getAttribute(this.binding(\"change\"));\n      if (!externalFormSubmitted && phxChange && !phxSubmit) {\n        externalFormSubmitted = true;\n        e.preventDefault();\n        this.withinOwners(e.target, (view) => {\n          view.disableForm(e.target);\n          // safari needs next tick\n          window.requestAnimationFrame(() => {\n            if (DOM.isUnloadableFormSubmit(e)) {\n              this.unload();\n            }\n            e.target.submit();\n          });\n        });\n      }\n    });\n\n    this.on(\"submit\", (e) => {\n      const phxEvent = e.target.getAttribute(this.binding(\"submit\"));\n      if (!phxEvent) {\n        if (DOM.isUnloadableFormSubmit(e)) {\n          this.unload();\n        }\n        return;\n      }\n      e.preventDefault();\n      e.target.disabled = true;\n      this.withinOwners(e.target, (view) => {\n        JS.exec(e, \"submit\", phxEvent, view, e.target, [\n          \"push\",\n          { submitter: e.submitter },\n        ]);\n      });\n    });\n\n    for (const type of [\"change\", \"input\"]) {\n      this.on(type, (e) => {\n        if (\n          e instanceof CustomEvent &&\n          (e.target instanceof HTMLInputElement ||\n            e.target instanceof HTMLSelectElement ||\n            e.target instanceof HTMLTextAreaElement) &&\n          e.target.form === undefined\n        ) {\n          // throw on invalid JS.dispatch target and noop if CustomEvent triggered outside JS.dispatch\n          if (e.detail && e.detail.dispatcher) {\n            throw new Error(\n              `dispatching a custom ${type} event is only supported on input elements inside a form`,\n            );\n          }\n          return;\n        }\n        const phxChange = this.binding(\"change\");\n        const input = e.target;\n        if (this.blockPhxChangeWhileComposing && e.isComposing) {\n          const key = `composition-listener-${type}`;\n          if (!DOM.private(input, key)) {\n            DOM.putPrivate(input, key, true);\n            input.addEventListener(\n              \"compositionend\",\n              () => {\n                // trigger a new input/change event\n                input.dispatchEvent(new Event(type, { bubbles: true }));\n                DOM.deletePrivate(input, key);\n              },\n              { once: true },\n            );\n          }\n          return;\n        }\n        const inputEvent = input.getAttribute(phxChange);\n        const formEvent = input.form && input.form.getAttribute(phxChange);\n        const phxEvent = inputEvent || formEvent;\n        if (!phxEvent) {\n          return;\n        }\n        if (\n          input.type === \"number\" &&\n          input.validity &&\n          input.validity.badInput\n        ) {\n          return;\n        }\n\n        const dispatcher = inputEvent ? input : input.form;\n        const currentIterations = iterations;\n        iterations++;\n        const { at: at, type: lastType } =\n          DOM.private(input, \"prev-iteration\") || {};\n        // Browsers should always fire at least one \"input\" event before every \"change\"\n        // Ignore \"change\" events, unless there was no prior \"input\" event.\n        // This could happen if user code triggers a \"change\" event, or if the browser is non-conforming.\n        if (\n          at === currentIterations - 1 &&\n          type === \"change\" &&\n          lastType === \"input\"\n        ) {\n          return;\n        }\n\n        DOM.putPrivate(input, \"prev-iteration\", {\n          at: currentIterations,\n          type: type,\n        });\n\n        this.debounce(input, e, type, () => {\n          this.withinOwners(dispatcher, (view) => {\n            DOM.putPrivate(input, PHX_HAS_FOCUSED, true);\n            JS.exec(e, \"change\", phxEvent, view, input, [\n              \"push\",\n              { _target: e.target.name, dispatcher: dispatcher },\n            ]);\n          });\n        });\n      });\n    }\n    this.on(\"reset\", (e) => {\n      const form = e.target;\n      DOM.resetForm(form);\n      const input = Array.from(form.elements).find((el) => el.type === \"reset\");\n      if (input) {\n        // wait until next tick to get updated input value\n        window.requestAnimationFrame(() => {\n          input.dispatchEvent(\n            new Event(\"input\", { bubbles: true, cancelable: false }),\n          );\n        });\n      }\n    });\n  }\n\n  debounce(el, event, eventType, callback) {\n    if (eventType === \"blur\" || eventType === \"focusout\") {\n      return callback();\n    }\n\n    const phxDebounce = this.binding(PHX_DEBOUNCE);\n    const phxThrottle = this.binding(PHX_THROTTLE);\n    const defaultDebounce = this.defaults.debounce.toString();\n    const defaultThrottle = this.defaults.throttle.toString();\n\n    this.withinOwners(el, (view) => {\n      const asyncFilter = () =>\n        !view.isDestroyed() && document.body.contains(el);\n      DOM.debounce(\n        el,\n        event,\n        phxDebounce,\n        defaultDebounce,\n        phxThrottle,\n        defaultThrottle,\n        asyncFilter,\n        () => {\n          callback();\n        },\n      );\n    });\n  }\n\n  silenceEvents(callback) {\n    this.silenced = true;\n    callback();\n    this.silenced = false;\n  }\n\n  on(event, callback) {\n    this.boundEventNames.add(event);\n    window.addEventListener(event, (e) => {\n      if (!this.silenced) {\n        callback(e);\n      }\n    });\n  }\n\n  jsQuerySelectorAll(sourceEl, query, defaultQuery) {\n    const all = this.domCallbacks.jsQuerySelectorAll;\n    return all ? all(sourceEl, query, defaultQuery) : defaultQuery();\n  }\n}\n\nclass TransitionSet {\n  constructor() {\n    this.transitions = new Set();\n    this.promises = new Set();\n    this.pendingOps = [];\n  }\n\n  reset() {\n    this.transitions.forEach((timer) => {\n      clearTimeout(timer);\n      this.transitions.delete(timer);\n    });\n    this.promises.clear();\n    this.flushPendingOps();\n  }\n\n  after(callback) {\n    if (this.size() === 0) {\n      callback();\n    } else {\n      this.pushPendingOp(callback);\n    }\n  }\n\n  addTransition(time, onStart, onDone) {\n    onStart();\n    const timer = setTimeout(() => {\n      this.transitions.delete(timer);\n      onDone();\n      this.flushPendingOps();\n    }, time);\n    this.transitions.add(timer);\n  }\n\n  addAsyncTransition(promise) {\n    this.promises.add(promise);\n    promise.then(() => {\n      this.promises.delete(promise);\n      this.flushPendingOps();\n    });\n  }\n\n  pushPendingOp(op) {\n    this.pendingOps.push(op);\n  }\n\n  size() {\n    return this.transitions.size + this.promises.size;\n  }\n\n  flushPendingOps() {\n    if (this.size() > 0) {\n      return;\n    }\n    const op = this.pendingOps.shift();\n    if (op) {\n      op();\n      this.flushPendingOps();\n    }\n  }\n}\n"
  },
  {
    "path": "assets/js/phoenix_live_view/live_uploader.js",
    "content": "import {\n  PHX_DONE_REFS,\n  PHX_PREFLIGHTED_REFS,\n  PHX_UPLOAD_REF,\n} from \"./constants\";\n\nimport {} from \"./utils\";\n\nimport DOM from \"./dom\";\nimport UploadEntry from \"./upload_entry\";\n\nlet liveUploaderFileRef = 0;\n\nexport default class LiveUploader {\n  static genFileRef(file) {\n    const ref = file._phxRef;\n    if (ref !== undefined) {\n      return ref;\n    } else {\n      file._phxRef = (liveUploaderFileRef++).toString();\n      return file._phxRef;\n    }\n  }\n\n  static getEntryDataURL(inputEl, ref, callback) {\n    const file = this.activeFiles(inputEl).find(\n      (file) => this.genFileRef(file) === ref,\n    );\n    callback(URL.createObjectURL(file));\n  }\n\n  static hasUploadsInProgress(formEl) {\n    let active = 0;\n    DOM.findUploadInputs(formEl).forEach((input) => {\n      if (\n        input.getAttribute(PHX_PREFLIGHTED_REFS) !==\n        input.getAttribute(PHX_DONE_REFS)\n      ) {\n        active++;\n      }\n    });\n    return active > 0;\n  }\n\n  static serializeUploads(inputEl) {\n    const files = this.activeFiles(inputEl);\n    const fileData = {};\n    files.forEach((file) => {\n      const entry = { path: inputEl.name };\n      const uploadRef = inputEl.getAttribute(PHX_UPLOAD_REF);\n      fileData[uploadRef] = fileData[uploadRef] || [];\n      entry.ref = this.genFileRef(file);\n      entry.last_modified = file.lastModified;\n      entry.name = file.name || entry.ref;\n      entry.relative_path = file.webkitRelativePath;\n      entry.type = file.type;\n      entry.size = file.size;\n      if (typeof file.meta === \"function\") {\n        entry.meta = file.meta();\n      }\n      fileData[uploadRef].push(entry);\n    });\n    return fileData;\n  }\n\n  static clearFiles(inputEl) {\n    inputEl.value = null;\n    inputEl.removeAttribute(PHX_UPLOAD_REF);\n    DOM.putPrivate(inputEl, \"files\", []);\n  }\n\n  static untrackFile(inputEl, file) {\n    DOM.putPrivate(\n      inputEl,\n      \"files\",\n      DOM.private(inputEl, \"files\").filter((f) => !Object.is(f, file)),\n    );\n  }\n\n  /**\n   * @param {HTMLInputElement} inputEl\n   * @param {Array<File|Blob>} files\n   * @param {DataTransfer} [dataTransfer]\n   */\n  static trackFiles(inputEl, files, dataTransfer) {\n    if (inputEl.getAttribute(\"multiple\") !== null) {\n      const newFiles = files.filter(\n        (file) => !this.activeFiles(inputEl).find((f) => Object.is(f, file)),\n      );\n      DOM.updatePrivate(inputEl, \"files\", [], (existing) =>\n        existing.concat(newFiles),\n      );\n      inputEl.value = null;\n    } else {\n      // Reset inputEl files to align output with programmatic changes (i.e. drag and drop)\n      if (dataTransfer && dataTransfer.files.length > 0) {\n        inputEl.files = dataTransfer.files;\n      }\n      DOM.putPrivate(inputEl, \"files\", files);\n    }\n  }\n\n  static activeFileInputs(formEl) {\n    const fileInputs = DOM.findUploadInputs(formEl);\n    return Array.from(fileInputs).filter(\n      (el) => el.files && this.activeFiles(el).length > 0,\n    );\n  }\n\n  static activeFiles(input) {\n    return (DOM.private(input, \"files\") || []).filter((f) =>\n      UploadEntry.isActive(input, f),\n    );\n  }\n\n  static inputsAwaitingPreflight(formEl) {\n    const fileInputs = DOM.findUploadInputs(formEl);\n    return Array.from(fileInputs).filter(\n      (input) => this.filesAwaitingPreflight(input).length > 0,\n    );\n  }\n\n  static filesAwaitingPreflight(input) {\n    return this.activeFiles(input).filter(\n      (f) =>\n        !UploadEntry.isPreflighted(input, f) &&\n        !UploadEntry.isPreflightInProgress(f),\n    );\n  }\n\n  static markPreflightInProgress(entries) {\n    entries.forEach((entry) => UploadEntry.markPreflightInProgress(entry.file));\n  }\n\n  constructor(inputEl, view, onComplete) {\n    this.autoUpload = DOM.isAutoUpload(inputEl);\n    this.view = view;\n    this.onComplete = onComplete;\n    this._entries = Array.from(\n      LiveUploader.filesAwaitingPreflight(inputEl) || [],\n    ).map((file) => new UploadEntry(inputEl, file, view, this.autoUpload));\n\n    // prevent sending duplicate preflight requests\n    LiveUploader.markPreflightInProgress(this._entries);\n\n    this.numEntriesInProgress = this._entries.length;\n  }\n\n  isAutoUpload() {\n    return this.autoUpload;\n  }\n\n  entries() {\n    return this._entries;\n  }\n\n  initAdapterUpload(resp, onError, liveSocket) {\n    this._entries = this._entries.map((entry) => {\n      if (entry.isCancelled()) {\n        this.numEntriesInProgress--;\n        if (this.numEntriesInProgress === 0) {\n          this.onComplete();\n        }\n      } else {\n        entry.zipPostFlight(resp);\n        entry.onDone(() => {\n          this.numEntriesInProgress--;\n          if (this.numEntriesInProgress === 0) {\n            this.onComplete();\n          }\n        });\n      }\n      return entry;\n    });\n\n    const groupedEntries = this._entries.reduce((acc, entry) => {\n      if (!entry.meta) {\n        return acc;\n      }\n      const { name, callback } = entry.uploader(liveSocket.uploaders);\n      acc[name] = acc[name] || { callback: callback, entries: [] };\n      acc[name].entries.push(entry);\n      return acc;\n    }, {});\n\n    for (const name in groupedEntries) {\n      const { callback, entries } = groupedEntries[name];\n      callback(entries, onError, resp, liveSocket);\n    }\n  }\n}\n"
  },
  {
    "path": "assets/js/phoenix_live_view/rendered.js",
    "content": "import {\n  COMPONENTS,\n  TEMPLATES,\n  EVENTS,\n  PHX_COMPONENT,\n  PHX_VIEW_REF,\n  PHX_SKIP,\n  PHX_MAGIC_ID,\n  REPLY,\n  STATIC,\n  TITLE,\n  STREAM,\n  ROOT,\n  KEYED,\n  KEYED_COUNT,\n} from \"./constants\";\n\nimport { isObject, logError, isCid } from \"./utils\";\n\nconst VOID_TAGS = new Set([\n  \"area\",\n  \"base\",\n  \"br\",\n  \"col\",\n  \"command\",\n  \"embed\",\n  \"hr\",\n  \"img\",\n  \"input\",\n  \"keygen\",\n  \"link\",\n  \"meta\",\n  \"param\",\n  \"source\",\n  \"track\",\n  \"wbr\",\n]);\nconst quoteChars = new Set([\"'\", '\"']);\n\nexport const modifyRoot = (html, attrs, clearInnerHTML) => {\n  let i = 0;\n  let insideComment = false;\n  let beforeTag, afterTag, tag, tagNameEndsAt, id, newHTML;\n\n  const lookahead = html.match(/^(\\s*(?:<!--.*?-->\\s*)*)<([^\\s\\/>]+)/);\n  if (lookahead === null) {\n    throw new Error(`malformed html ${html}`);\n  }\n\n  i = lookahead[0].length;\n  beforeTag = lookahead[1];\n  tag = lookahead[2];\n  tagNameEndsAt = i;\n\n  // Scan the opening tag for id, if there is any\n  for (i; i < html.length; i++) {\n    if (html.charAt(i) === \">\") {\n      break;\n    }\n    if (html.charAt(i) === \"=\") {\n      const isId = html.slice(i - 3, i) === \" id\";\n      i++;\n      const char = html.charAt(i);\n      if (quoteChars.has(char)) {\n        const attrStartsAt = i;\n        i++;\n        for (i; i < html.length; i++) {\n          if (html.charAt(i) === char) {\n            break;\n          }\n        }\n        if (isId) {\n          id = html.slice(attrStartsAt + 1, i);\n          break;\n        }\n      }\n    }\n  }\n\n  let closeAt = html.length - 1;\n  insideComment = false;\n  while (closeAt >= beforeTag.length + tag.length) {\n    const char = html.charAt(closeAt);\n    if (insideComment) {\n      if (char === \"-\" && html.slice(closeAt - 3, closeAt) === \"<!-\") {\n        insideComment = false;\n        closeAt -= 4;\n      } else {\n        closeAt -= 1;\n      }\n    } else if (char === \">\" && html.slice(closeAt - 2, closeAt) === \"--\") {\n      insideComment = true;\n      closeAt -= 3;\n    } else if (char === \">\") {\n      break;\n    } else {\n      closeAt -= 1;\n    }\n  }\n  afterTag = html.slice(closeAt + 1, html.length);\n\n  const attrsStr = Object.keys(attrs)\n    .map((attr) => (attrs[attr] === true ? attr : `${attr}=\"${attrs[attr]}\"`))\n    .join(\" \");\n\n  if (clearInnerHTML) {\n    // Keep the id if any\n    const idAttrStr = id ? ` id=\"${id}\"` : \"\";\n    if (VOID_TAGS.has(tag)) {\n      newHTML = `<${tag}${idAttrStr}${attrsStr === \"\" ? \"\" : \" \"}${attrsStr}/>`;\n    } else {\n      newHTML = `<${tag}${idAttrStr}${attrsStr === \"\" ? \"\" : \" \"}${attrsStr}></${tag}>`;\n    }\n  } else {\n    const rest = html.slice(tagNameEndsAt, closeAt + 1);\n    newHTML = `<${tag}${attrsStr === \"\" ? \"\" : \" \"}${attrsStr}${rest}`;\n  }\n\n  return [newHTML, beforeTag, afterTag];\n};\n\nexport default class Rendered {\n  static extract(diff) {\n    const { [REPLY]: reply, [EVENTS]: events, [TITLE]: title } = diff;\n    delete diff[REPLY];\n    delete diff[EVENTS];\n    delete diff[TITLE];\n    return { diff, title, reply: reply || null, events: events || [] };\n  }\n\n  constructor(viewId, rendered) {\n    this.viewId = viewId;\n    this.rendered = {};\n    this.magicId = 0;\n    this.mergeDiff(rendered);\n  }\n\n  parentViewId() {\n    return this.viewId;\n  }\n\n  toString(onlyCids) {\n    const { buffer: str, streams: streams } = this.recursiveToString(\n      this.rendered,\n      this.rendered[COMPONENTS],\n      onlyCids,\n      true,\n      {},\n    );\n    return { buffer: str, streams: streams };\n  }\n\n  recursiveToString(\n    rendered,\n    components = rendered[COMPONENTS],\n    onlyCids,\n    changeTracking,\n    rootAttrs,\n  ) {\n    onlyCids = onlyCids ? new Set(onlyCids) : null;\n    const output = {\n      buffer: \"\",\n      components: components,\n      onlyCids: onlyCids,\n      streams: new Set(),\n    };\n    this.toOutputBuffer(rendered, null, output, changeTracking, rootAttrs);\n    return { buffer: output.buffer, streams: output.streams };\n  }\n\n  componentCIDs(diff) {\n    return Object.keys(diff[COMPONENTS] || {}).map((i) => parseInt(i));\n  }\n\n  isComponentOnlyDiff(diff) {\n    if (!diff[COMPONENTS]) {\n      return false;\n    }\n    return Object.keys(diff).length === 1;\n  }\n\n  getComponent(diff, cid) {\n    return diff[COMPONENTS][cid];\n  }\n\n  resetRender(cid) {\n    // we are racing a component destroy, it could not exist, so\n    // make sure that we don't try to set reset on undefined\n    if (this.rendered[COMPONENTS][cid]) {\n      this.rendered[COMPONENTS][cid].reset = true;\n    }\n  }\n\n  mergeDiff(diff) {\n    const newc = diff[COMPONENTS];\n    const cache = {};\n    delete diff[COMPONENTS];\n    this.rendered = this.mutableMerge(this.rendered, diff);\n    this.rendered[COMPONENTS] = this.rendered[COMPONENTS] || {};\n\n    if (newc) {\n      const oldc = this.rendered[COMPONENTS];\n\n      for (const cid in newc) {\n        newc[cid] = this.cachedFindComponent(cid, newc[cid], oldc, newc, cache);\n      }\n\n      for (const cid in newc) {\n        oldc[cid] = newc[cid];\n      }\n      diff[COMPONENTS] = newc;\n    }\n  }\n\n  cachedFindComponent(cid, cdiff, oldc, newc, cache) {\n    if (cache[cid]) {\n      return cache[cid];\n    } else {\n      let ndiff,\n        stat,\n        scid = cdiff[STATIC];\n\n      if (isCid(scid)) {\n        let tdiff;\n\n        if (scid > 0) {\n          tdiff = this.cachedFindComponent(scid, newc[scid], oldc, newc, cache);\n        } else {\n          tdiff = oldc[-scid];\n        }\n\n        stat = tdiff[STATIC];\n        ndiff = this.cloneMerge(tdiff, cdiff, true);\n        ndiff[STATIC] = stat;\n      } else {\n        ndiff =\n          cdiff[STATIC] !== undefined || oldc[cid] === undefined\n            ? cdiff\n            : this.cloneMerge(oldc[cid], cdiff, false);\n      }\n\n      cache[cid] = ndiff;\n      return ndiff;\n    }\n  }\n\n  mutableMerge(target, source) {\n    if (source[STATIC] !== undefined) {\n      return source;\n    } else {\n      this.doMutableMerge(target, source);\n      return target;\n    }\n  }\n\n  doMutableMerge(target, source) {\n    if (source[KEYED]) {\n      this.mergeKeyed(target, source);\n    } else {\n      for (const key in source) {\n        const val = source[key];\n        const targetVal = target[key];\n        const isObjVal = isObject(val);\n        if (isObjVal && val[STATIC] === undefined && isObject(targetVal)) {\n          this.doMutableMerge(targetVal, val);\n        } else {\n          target[key] = val;\n        }\n      }\n    }\n    if (target[ROOT]) {\n      target.newRender = true;\n    }\n  }\n\n  clone(diff) {\n    if (\"structuredClone\" in window) {\n      return structuredClone(diff);\n    } else {\n      // fallback for jest\n      return JSON.parse(JSON.stringify(diff));\n    }\n  }\n\n  // keyed comprehensions\n  mergeKeyed(target, source) {\n    // we need to clone the target since elements can move and otherwise\n    // it could happen that we modify an element that we'll need to refer to\n    // later\n    const clonedTarget = this.clone(target);\n    Object.entries(source[KEYED]).forEach(([i, entry]) => {\n      if (i === KEYED_COUNT) {\n        return;\n      }\n      if (Array.isArray(entry)) {\n        // [old_idx, diff]\n        // moved with diff\n        const [old_idx, diff] = entry;\n        target[KEYED][i] = clonedTarget[KEYED][old_idx];\n        this.doMutableMerge(target[KEYED][i], diff);\n      } else if (typeof entry === \"number\") {\n        // moved without diff\n        const old_idx = entry;\n        target[KEYED][i] = clonedTarget[KEYED][old_idx];\n      } else if (typeof entry === \"object\") {\n        // diff, same position\n        if (!target[KEYED][i]) {\n          target[KEYED][i] = {};\n        }\n        this.doMutableMerge(target[KEYED][i], entry);\n      }\n    });\n    // drop extra entries\n    if (source[KEYED][KEYED_COUNT] < target[KEYED][KEYED_COUNT]) {\n      for (\n        let i = source[KEYED][KEYED_COUNT];\n        i < target[KEYED][KEYED_COUNT];\n        i++\n      ) {\n        delete target[KEYED][i];\n      }\n    }\n    target[KEYED][KEYED_COUNT] = source[KEYED][KEYED_COUNT];\n    if (source[STREAM]) {\n      target[STREAM] = source[STREAM];\n    }\n    if (source[TEMPLATES]) {\n      target[TEMPLATES] = source[TEMPLATES];\n    }\n  }\n\n  // Merges cid trees together, copying statics from source tree.\n  //\n  // The `pruneMagicId` is passed to control pruning the magicId of the\n  // target. We must always prune the magicId when we are sharing statics\n  // from another component. If not pruning, we replicate the logic from\n  // mutableMerge, where we set newRender to true if there is a root\n  // (effectively forcing the new version to be rendered instead of skipped)\n  //\n  cloneMerge(target, source, pruneMagicId) {\n    let merged;\n    if (source[KEYED]) {\n      merged = this.clone(target);\n      this.mergeKeyed(merged, source);\n    } else {\n      merged = { ...target, ...source };\n      for (const key in merged) {\n        const val = source[key];\n        const targetVal = target[key];\n        if (isObject(val) && val[STATIC] === undefined && isObject(targetVal)) {\n          merged[key] = this.cloneMerge(targetVal, val, pruneMagicId);\n        } else if (val === undefined && isObject(targetVal)) {\n          merged[key] = this.cloneMerge(targetVal, {}, pruneMagicId);\n        }\n      }\n    }\n    if (pruneMagicId) {\n      delete merged.magicId;\n      delete merged.newRender;\n    } else if (target[ROOT]) {\n      merged.newRender = true;\n    }\n    return merged;\n  }\n\n  componentToString(cid) {\n    const { buffer: str, streams } = this.recursiveCIDToString(\n      this.rendered[COMPONENTS],\n      cid,\n      null,\n    );\n    const [strippedHTML, _before, _after] = modifyRoot(str, {});\n    return { buffer: strippedHTML, streams: streams };\n  }\n\n  pruneCIDs(cids) {\n    cids.forEach((cid) => delete this.rendered[COMPONENTS][cid]);\n  }\n\n  // private\n\n  get() {\n    return this.rendered;\n  }\n\n  isNewFingerprint(diff = {}) {\n    return !!diff[STATIC];\n  }\n\n  templateStatic(part, templates) {\n    if (typeof part === \"number\") {\n      return templates[part];\n    } else {\n      return part;\n    }\n  }\n\n  nextMagicID() {\n    this.magicId++;\n    return `m${this.magicId}-${this.parentViewId()}`;\n  }\n\n  // Converts rendered tree to output buffer.\n  //\n  // changeTracking controls if we can apply the PHX_SKIP optimization.\n  toOutputBuffer(rendered, templates, output, changeTracking, rootAttrs = {}) {\n    if (rendered[KEYED]) {\n      return this.comprehensionToBuffer(\n        rendered,\n        templates,\n        output,\n        changeTracking,\n      );\n    }\n\n    // Templates are a way of sharing statics between multiple rendered structs.\n    // Since LiveView 1.1, those can also appear at the root - for example if one renders\n    // two comprehensions that can share statics.\n    // Whenever we find templates, we need to use them recursively. Also, templates can\n    // be sent for each diff, not only for the initial one. We don't want to merge them\n    // though, so we always resolve them and remove them from the rendered object.\n    if (rendered[TEMPLATES]) {\n      templates = rendered[TEMPLATES];\n      delete rendered[TEMPLATES];\n    }\n\n    let { [STATIC]: statics } = rendered;\n    statics = this.templateStatic(statics, templates);\n    rendered[STATIC] = statics;\n    const isRoot = rendered[ROOT];\n    const prevBuffer = output.buffer;\n    if (isRoot) {\n      output.buffer = \"\";\n    }\n\n    // this condition is called when first rendering an optimizable function component.\n    // LC have their magicId previously set\n    if (changeTracking && isRoot && !rendered.magicId) {\n      rendered.newRender = true;\n      rendered.magicId = this.nextMagicID();\n    }\n\n    output.buffer += statics[0];\n    for (let i = 1; i < statics.length; i++) {\n      this.dynamicToBuffer(rendered[i - 1], templates, output, changeTracking);\n      output.buffer += statics[i];\n    }\n\n    // Applies the root tag \"skip\" optimization if supported, which clears\n    // the root tag attributes and innerHTML, and only maintains the magicId.\n    // We can only skip when changeTracking is supported,\n    // and when the root element hasn't experienced an unrendered merge (newRender true).\n    if (isRoot) {\n      let skip = false;\n      let attrs;\n      // When a LC is re-added to the page, we need to re-render the entire LC tree,\n      // therefore changeTracking is false; however, we need to keep all the magicIds\n      // from any function component so the next time the LC is updated, we can apply\n      // the skip optimization\n      if (changeTracking || rendered.magicId) {\n        skip = changeTracking && !rendered.newRender;\n        attrs = { [PHX_MAGIC_ID]: rendered.magicId, ...rootAttrs };\n      } else {\n        attrs = rootAttrs;\n      }\n      if (skip) {\n        attrs[PHX_SKIP] = true;\n      }\n      const [newRoot, commentBefore, commentAfter] = modifyRoot(\n        output.buffer,\n        attrs,\n        skip,\n      );\n      rendered.newRender = false;\n      output.buffer = prevBuffer + commentBefore + newRoot + commentAfter;\n    }\n  }\n\n  comprehensionToBuffer(rendered, templates, output, changeTracking) {\n    const keyedTemplates = templates || rendered[TEMPLATES];\n    const statics = this.templateStatic(rendered[STATIC], templates);\n    rendered[STATIC] = statics;\n    delete rendered[TEMPLATES];\n    for (let i = 0; i < rendered[KEYED][KEYED_COUNT]; i++) {\n      output.buffer += statics[0];\n      for (let j = 1; j < statics.length; j++) {\n        this.dynamicToBuffer(\n          rendered[KEYED][i][j - 1],\n          keyedTemplates,\n          output,\n          changeTracking,\n        );\n        output.buffer += statics[j];\n      }\n    }\n    // we don't need to store the rendered tree for streams\n    if (rendered[STREAM]) {\n      const stream = rendered[STREAM];\n      const [_ref, _inserts, deleteIds, reset] = stream || [null, {}, [], null];\n      if (\n        stream !== undefined &&\n        (rendered[KEYED][KEYED_COUNT] > 0 || deleteIds.length > 0 || reset)\n      ) {\n        delete rendered[STREAM];\n        rendered[KEYED] = {\n          [KEYED_COUNT]: 0,\n        };\n        output.streams.add(stream);\n      }\n    }\n  }\n\n  dynamicToBuffer(rendered, templates, output, changeTracking) {\n    if (typeof rendered === \"number\") {\n      const { buffer: str, streams } = this.recursiveCIDToString(\n        output.components,\n        rendered,\n        output.onlyCids,\n      );\n      output.buffer += str;\n      output.streams = new Set([...output.streams, ...streams]);\n    } else if (isObject(rendered)) {\n      this.toOutputBuffer(rendered, templates, output, changeTracking, {});\n    } else {\n      output.buffer += rendered;\n    }\n  }\n\n  recursiveCIDToString(components, cid, onlyCids) {\n    const component =\n      components[cid] || logError(`no component for CID ${cid}`, components);\n    const attrs = { [PHX_COMPONENT]: cid, [PHX_VIEW_REF]: this.viewId };\n    const skip = onlyCids && !onlyCids.has(cid);\n    // Two optimization paths apply here:\n    //\n    //   1. The onlyCids optimization works by the server diff telling us only specific\n    //     cid's have changed. This allows us to skip rendering any component that hasn't changed,\n    //     which ultimately sets PHX_SKIP root attribute and avoids rendering the innerHTML.\n    //\n    //   2. The root PHX_SKIP optimization generalizes to all HEEx function components, and\n    //     works in the same PHX_SKIP attribute fashion as 1, but the newRender tracking is done\n    //     at the general diff merge level. If we merge a diff with new dynamics, we necessarily have\n    //     experienced a change which must be a newRender, and thus we can't skip the render.\n    //\n    // Both optimization flows apply here. newRender is set based on the onlyCids optimization, and\n    // we track a deterministic magicId based on the cid.\n    //\n    // changeTracking is about the entire tree\n    // newRender is about the current root in the tree\n    //\n    // By default changeTracking is enabled, but we special case the flow where the client is pruning\n    // cids and the server adds the component back. In such cases, we explicitly disable changeTracking\n    // with resetRender for this cid, then re-enable it after the recursive call to skip the optimization\n    // for the entire component tree.\n    component.newRender = !skip;\n    component.magicId = `c${cid}-${this.parentViewId()}`;\n    // enable change tracking as long as the component hasn't been reset\n    const changeTracking = !component.reset;\n    const { buffer: html, streams } = this.recursiveToString(\n      component,\n      components,\n      onlyCids,\n      changeTracking,\n      attrs,\n    );\n    // disable reset after we've rendered\n    delete component.reset;\n\n    return { buffer: html, streams: streams };\n  }\n}\n"
  },
  {
    "path": "assets/js/phoenix_live_view/upload_entry.js",
    "content": "import {\n  PHX_ACTIVE_ENTRY_REFS,\n  PHX_LIVE_FILE_UPDATED,\n  PHX_PREFLIGHTED_REFS,\n} from \"./constants\";\n\nimport { channelUploader, logError } from \"./utils\";\n\nimport LiveUploader from \"./live_uploader\";\n\nexport default class UploadEntry {\n  static isActive(fileEl, file) {\n    const isNew = file._phxRef === undefined;\n    const activeRefs = fileEl.getAttribute(PHX_ACTIVE_ENTRY_REFS).split(\",\");\n    const isActive = activeRefs.indexOf(LiveUploader.genFileRef(file)) >= 0;\n    return file.size > 0 && (isNew || isActive);\n  }\n\n  static isPreflighted(fileEl, file) {\n    const preflightedRefs = fileEl\n      .getAttribute(PHX_PREFLIGHTED_REFS)\n      .split(\",\");\n    const isPreflighted =\n      preflightedRefs.indexOf(LiveUploader.genFileRef(file)) >= 0;\n    return isPreflighted && this.isActive(fileEl, file);\n  }\n\n  static isPreflightInProgress(file) {\n    return file._preflightInProgress === true;\n  }\n\n  static markPreflightInProgress(file) {\n    file._preflightInProgress = true;\n  }\n\n  constructor(fileEl, file, view, autoUpload) {\n    this.ref = LiveUploader.genFileRef(file);\n    this.fileEl = fileEl;\n    this.file = file;\n    this.view = view;\n    this.meta = null;\n    this._isCancelled = false;\n    this._isDone = false;\n    this._progress = 0;\n    this._lastProgressSent = -1;\n    this._onDone = function () {};\n    this._onElUpdated = this.onElUpdated.bind(this);\n    this.fileEl.addEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated);\n    this.autoUpload = autoUpload;\n  }\n\n  metadata() {\n    return this.meta;\n  }\n\n  progress(progress) {\n    this._progress = Math.floor(progress);\n    if (this._progress > this._lastProgressSent) {\n      if (this._progress >= 100) {\n        this._progress = 100;\n        this._lastProgressSent = 100;\n        this._isDone = true;\n        this.view.pushFileProgress(this.fileEl, this.ref, 100, () => {\n          LiveUploader.untrackFile(this.fileEl, this.file);\n          this._onDone();\n        });\n      } else {\n        this._lastProgressSent = this._progress;\n        this.view.pushFileProgress(this.fileEl, this.ref, this._progress);\n      }\n    }\n  }\n\n  isCancelled() {\n    return this._isCancelled;\n  }\n\n  cancel() {\n    this.file._preflightInProgress = false;\n    this._isCancelled = true;\n    this._isDone = true;\n    this._onDone();\n  }\n\n  isDone() {\n    return this._isDone;\n  }\n\n  error(reason = \"failed\") {\n    this.fileEl.removeEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated);\n    this.view.pushFileProgress(this.fileEl, this.ref, { error: reason });\n    if (!this.isAutoUpload()) {\n      LiveUploader.clearFiles(this.fileEl);\n    }\n  }\n\n  isAutoUpload() {\n    return this.autoUpload;\n  }\n\n  //private\n\n  onDone(callback) {\n    this._onDone = () => {\n      this.fileEl.removeEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated);\n      callback();\n    };\n  }\n\n  onElUpdated() {\n    const activeRefs = this.fileEl\n      .getAttribute(PHX_ACTIVE_ENTRY_REFS)\n      .split(\",\");\n    if (activeRefs.indexOf(this.ref) === -1) {\n      LiveUploader.untrackFile(this.fileEl, this.file);\n      this.cancel();\n    }\n  }\n\n  toPreflightPayload() {\n    return {\n      last_modified: this.file.lastModified,\n      name: this.file.name,\n      relative_path: this.file.webkitRelativePath,\n      size: this.file.size,\n      type: this.file.type,\n      ref: this.ref,\n      meta: typeof this.file.meta === \"function\" ? this.file.meta() : undefined,\n    };\n  }\n\n  uploader(uploaders) {\n    if (this.meta.uploader) {\n      const callback =\n        uploaders[this.meta.uploader] ||\n        logError(`no uploader configured for ${this.meta.uploader}`);\n      return { name: this.meta.uploader, callback: callback };\n    } else {\n      return { name: \"channel\", callback: channelUploader };\n    }\n  }\n\n  zipPostFlight(resp) {\n    this.meta = resp.entries[this.ref];\n    if (!this.meta) {\n      logError(`no preflight upload response returned with ref ${this.ref}`, {\n        input: this.fileEl,\n        response: resp,\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "assets/js/phoenix_live_view/utils.js",
    "content": "import { PHX_VIEW_SELECTOR } from \"./constants\";\n\nimport EntryUploader from \"./entry_uploader\";\n\nexport const logError = (msg, obj) => console.error && console.error(msg, obj);\n\nexport const isCid = (cid) => {\n  const type = typeof cid;\n  return type === \"number\" || (type === \"string\" && /^(0|[1-9]\\d*)$/.test(cid));\n};\n\nexport function detectDuplicateIds() {\n  const ids = new Set();\n  const elems = document.querySelectorAll(\"*[id]\");\n  for (let i = 0, len = elems.length; i < len; i++) {\n    if (ids.has(elems[i].id)) {\n      console.error(\n        `Multiple IDs detected: ${elems[i].id}. Ensure unique element ids.`,\n      );\n    } else {\n      ids.add(elems[i].id);\n    }\n  }\n}\n\nexport function detectInvalidStreamInserts(inserts) {\n  const errors = new Set();\n  Object.keys(inserts).forEach((id) => {\n    const streamEl = document.getElementById(id);\n    if (\n      streamEl &&\n      streamEl.parentElement &&\n      streamEl.parentElement.getAttribute(\"phx-update\") !== \"stream\"\n    ) {\n      errors.add(\n        `The stream container with id \"${streamEl.parentElement.id}\" is missing the phx-update=\"stream\" attribute. Ensure it is set for streams to work properly.`,\n      );\n    }\n  });\n  errors.forEach((error) => console.error(error));\n}\n\nexport const debug = (view, kind, msg, obj) => {\n  if (view.liveSocket.isDebugEnabled()) {\n    console.log(`${view.id} ${kind}: ${msg} - `, obj);\n  }\n};\n\n// wraps value in closure or returns closure\nexport const closure = (val) =>\n  typeof val === \"function\"\n    ? val\n    : function () {\n        return val;\n      };\n\nexport const clone = (obj) => {\n  return JSON.parse(JSON.stringify(obj));\n};\n\nexport const closestPhxBinding = (el, binding, borderEl) => {\n  do {\n    if (el.matches(`[${binding}]`) && !el.disabled) {\n      return el;\n    }\n    el = el.parentElement || el.parentNode;\n  } while (\n    el !== null &&\n    el.nodeType === 1 &&\n    !((borderEl && borderEl.isSameNode(el)) || el.matches(PHX_VIEW_SELECTOR))\n  );\n  return null;\n};\n\nexport const isObject = (obj) => {\n  return obj !== null && typeof obj === \"object\" && !(obj instanceof Array);\n};\n\nexport const isEqualObj = (obj1, obj2) =>\n  JSON.stringify(obj1) === JSON.stringify(obj2);\n\nexport const isEmpty = (obj) => {\n  for (const x in obj) {\n    return false;\n  }\n  return true;\n};\n\nexport const maybe = (el, callback) => el && callback(el);\n\nexport const channelUploader = function (entries, onError, resp, liveSocket) {\n  entries.forEach((entry) => {\n    const entryUploader = new EntryUploader(entry, resp.config, liveSocket);\n    entryUploader.upload();\n  });\n};\n\nexport const eventContainsFiles = (e) => {\n  if (e.dataTransfer.types) {\n    for (let i = 0; i < e.dataTransfer.types.length; i++) {\n      if (e.dataTransfer.types[i] === \"Files\") {\n        return true;\n      }\n    }\n  }\n  return false;\n};\n"
  },
  {
    "path": "assets/js/phoenix_live_view/view.js",
    "content": "import {\n  BEFORE_UNLOAD_LOADER_TIMEOUT,\n  CHECKABLE_INPUTS,\n  CONSECUTIVE_RELOADS,\n  PHX_AUTO_RECOVER,\n  PHX_COMPONENT,\n  PHX_VIEW_REF,\n  PHX_CONNECTED_CLASS,\n  PHX_DISABLE_WITH,\n  PHX_DISABLE_WITH_RESTORE,\n  PHX_DISABLED,\n  PHX_LOADING_CLASS,\n  PHX_ERROR_CLASS,\n  PHX_CLIENT_ERROR_CLASS,\n  PHX_SERVER_ERROR_CLASS,\n  PHX_HAS_FOCUSED,\n  PHX_HAS_SUBMITTED,\n  PHX_HOOK,\n  PHX_PARENT_ID,\n  PHX_PROGRESS,\n  PHX_READONLY,\n  PHX_REF_LOADING,\n  PHX_REF_SRC,\n  PHX_REF_LOCK,\n  PHX_ROOT_ID,\n  PHX_SESSION,\n  PHX_STATIC,\n  PHX_STICKY,\n  PHX_TRACK_STATIC,\n  PHX_TRACK_UPLOADS,\n  PHX_UPDATE,\n  PHX_UPLOAD_REF,\n  PHX_VIEW_SELECTOR,\n  PHX_MAIN,\n  PHX_MOUNTED,\n  PUSH_TIMEOUT,\n  PHX_VIEWPORT_TOP,\n  PHX_VIEWPORT_BOTTOM,\n  MAX_CHILD_JOIN_ATTEMPTS,\n  PHX_LV_PID,\n  PHX_NO_UNUSED_FIELD,\n  PHX_PORTAL,\n  PHX_TELEPORTED_REF,\n  PHX_TELEPORTED_SRC,\n} from \"./constants\";\n\nimport {\n  clone,\n  closestPhxBinding,\n  isEmpty,\n  isEqualObj,\n  logError,\n  maybe,\n  isCid,\n} from \"./utils\";\n\nimport Browser from \"./browser\";\nimport DOM from \"./dom\";\nimport ElementRef from \"./element_ref\";\nimport DOMPatch from \"./dom_patch\";\nimport LiveUploader from \"./live_uploader\";\nimport Rendered from \"./rendered\";\nimport { ViewHook } from \"./view_hook\";\nimport JS from \"./js\";\n\nimport morphdom from \"morphdom\";\n\nexport const prependFormDataKey = (key, prefix) => {\n  const isArray = key.endsWith(\"[]\");\n  // Remove the \"[]\" if it's an array\n  let baseKey = isArray ? key.slice(0, -2) : key;\n  // Replace last occurrence of key before a closing bracket or the end with key plus suffix\n  baseKey = baseKey.replace(/([^\\[\\]]+)(\\]?$)/, `${prefix}$1$2`);\n  // Add back the \"[]\" if it was an array\n  if (isArray) {\n    baseKey += \"[]\";\n  }\n  return baseKey;\n};\n\nexport default class View {\n  static closestView(el) {\n    const liveViewEl = el.closest(PHX_VIEW_SELECTOR);\n    return liveViewEl ? DOM.private(liveViewEl, \"view\") : null;\n  }\n\n  constructor(el, liveSocket, parentView, flash, liveReferer) {\n    this.isDead = false;\n    this.liveSocket = liveSocket;\n    this.flash = flash;\n    this.parent = parentView;\n    this.root = parentView ? parentView.root : this;\n    this.el = el;\n    // see https://github.com/phoenixframework/phoenix_live_view/pull/3721\n    // check if the element is already bound to a view\n    const boundView = DOM.private(this.el, \"view\");\n    if (boundView !== undefined && boundView.isDead !== true) {\n      logError(\n        `The DOM element for this view has already been bound to a view.\n\n        An element can only ever be associated with a single view!\n        Please ensure that you are not trying to initialize multiple LiveSockets on the same page.\n        This could happen if you're accidentally trying to render your root layout more than once.\n        Ensure that the template set on the LiveView is different than the root layout.\n      `,\n        { view: boundView },\n      );\n      throw new Error(\"Cannot bind multiple views to the same DOM element.\");\n    }\n    // bind the view to the element\n    DOM.putPrivate(this.el, \"view\", this);\n    this.id = this.el.id;\n    this.ref = 0;\n    this.lastAckRef = null;\n    this.childJoins = 0;\n    /**\n     * @type {ReturnType<typeof setTimeout> | null}\n     */\n    this.loaderTimer = null;\n    /**\n     * @type {ReturnType<typeof setTimeout> | null}\n     */\n    this.disconnectedTimer = null;\n    this.pendingDiffs = [];\n    this.pendingForms = new Set();\n    this.redirect = false;\n    this.href = null;\n    this.joinCount = this.parent ? this.parent.joinCount - 1 : 0;\n    this.joinAttempts = 0;\n    this.joinPending = true;\n    this.destroyed = false;\n    this.joinCallback = function (onDone) {\n      onDone && onDone();\n    };\n    this.stopCallback = function () {};\n    // usually, only the root LiveView stores pending\n    // join operations for all children (and itself),\n    // but in case of rejoins (joinCount > 1) each child\n    // stores its own events instead\n    this.pendingJoinOps = [];\n    this.viewHooks = {};\n    this.formSubmits = [];\n    this.children = this.parent ? null : {};\n    this.root.children[this.id] = {};\n    this.formsForRecovery = {};\n    this.channel = this.liveSocket.channel(`lv:${this.id}`, () => {\n      const url = this.href && this.expandURL(this.href);\n      return {\n        redirect: this.redirect ? url : undefined,\n        url: this.redirect ? undefined : url || undefined,\n        params: this.connectParams(liveReferer),\n        session: this.getSession(),\n        static: this.getStatic(),\n        flash: this.flash,\n        sticky: this.el.hasAttribute(PHX_STICKY),\n      };\n    });\n    this.portalElementIds = new Set();\n  }\n\n  setHref(href) {\n    this.href = href;\n  }\n\n  setRedirect(href) {\n    this.redirect = true;\n    this.href = href;\n  }\n\n  isMain() {\n    return this.el.hasAttribute(PHX_MAIN);\n  }\n\n  connectParams(liveReferer) {\n    const params = this.liveSocket.params(this.el);\n    const manifest = DOM.all(document, `[${this.binding(PHX_TRACK_STATIC)}]`)\n      .map((node) => node.src || node.href)\n      .filter((url) => typeof url === \"string\");\n\n    if (manifest.length > 0) {\n      params[\"_track_static\"] = manifest;\n    }\n    params[\"_mounts\"] = this.joinCount;\n    params[\"_mount_attempts\"] = this.joinAttempts;\n    params[\"_live_referer\"] = liveReferer;\n    this.joinAttempts++;\n\n    return params;\n  }\n\n  isConnected() {\n    return this.channel.canPush();\n  }\n\n  getSession() {\n    return this.el.getAttribute(PHX_SESSION);\n  }\n\n  getStatic() {\n    const val = this.el.getAttribute(PHX_STATIC);\n    return val === \"\" ? null : val;\n  }\n\n  destroy(callback = function () {}) {\n    this.destroyAllChildren();\n    this.destroyPortalElements();\n    this.destroyed = true;\n    DOM.deletePrivate(this.el, \"view\");\n    delete this.root.children[this.id];\n    if (this.parent) {\n      delete this.root.children[this.parent.id][this.id];\n    }\n    clearTimeout(this.loaderTimer);\n    const onFinished = () => {\n      callback();\n      for (const id in this.viewHooks) {\n        this.destroyHook(this.viewHooks[id]);\n      }\n    };\n\n    DOM.markPhxChildDestroyed(this.el);\n\n    this.log(\"destroyed\", () => [\"the child has been removed from the parent\"]);\n    this.channel\n      .leave()\n      .receive(\"ok\", onFinished)\n      .receive(\"error\", onFinished)\n      .receive(\"timeout\", onFinished);\n  }\n\n  setContainerClasses(...classes) {\n    this.el.classList.remove(\n      PHX_CONNECTED_CLASS,\n      PHX_LOADING_CLASS,\n      PHX_ERROR_CLASS,\n      PHX_CLIENT_ERROR_CLASS,\n      PHX_SERVER_ERROR_CLASS,\n    );\n    this.el.classList.add(...classes);\n  }\n\n  showLoader(timeout) {\n    clearTimeout(this.loaderTimer);\n    if (timeout) {\n      this.loaderTimer = setTimeout(() => this.showLoader(), timeout);\n    } else {\n      for (const id in this.viewHooks) {\n        this.viewHooks[id].__disconnected();\n      }\n      this.setContainerClasses(PHX_LOADING_CLASS);\n    }\n  }\n\n  execAll(binding) {\n    DOM.all(this.el, `[${binding}]`, (el) =>\n      this.liveSocket.execJS(el, el.getAttribute(binding)),\n    );\n  }\n\n  hideLoader() {\n    clearTimeout(this.loaderTimer);\n    clearTimeout(this.disconnectedTimer);\n    this.setContainerClasses(PHX_CONNECTED_CLASS);\n    this.execAll(this.binding(\"connected\"));\n  }\n\n  triggerReconnected() {\n    for (const id in this.viewHooks) {\n      this.viewHooks[id].__reconnected();\n    }\n  }\n\n  log(kind, msgCallback) {\n    this.liveSocket.log(this, kind, msgCallback);\n  }\n\n  transition(time, onStart, onDone = function () {}) {\n    this.liveSocket.transition(time, onStart, onDone);\n  }\n\n  // calls the callback with the view and target element for the given phxTarget\n  // targets can be:\n  //  * an element itself, then it is simply passed to liveSocket.owner;\n  //  * a CID (Component ID), then we first search the component's element in the DOM\n  //  * a selector, then we search the selector in the DOM and call the callback\n  //    for each element found with the corresponding owner view\n  withinTargets(phxTarget, callback, dom = document) {\n    // in the form recovery case we search in a template fragment instead of\n    // the real dom, therefore we optionally pass dom and viewEl\n\n    if (phxTarget instanceof HTMLElement || phxTarget instanceof SVGElement) {\n      return this.liveSocket.owner(phxTarget, (view) =>\n        callback(view, phxTarget),\n      );\n    }\n\n    if (isCid(phxTarget)) {\n      const targets = DOM.findComponentNodeList(this.id, phxTarget, dom);\n      if (targets.length === 0) {\n        logError(`no component found matching phx-target of ${phxTarget}`);\n      } else {\n        callback(this, parseInt(phxTarget));\n      }\n    } else {\n      const targets = Array.from(dom.querySelectorAll(phxTarget));\n      if (targets.length === 0) {\n        logError(\n          `nothing found matching the phx-target selector \"${phxTarget}\"`,\n        );\n      }\n      targets.forEach((target) =>\n        this.liveSocket.owner(target, (view) => callback(view, target)),\n      );\n    }\n  }\n\n  applyDiff(type, rawDiff, callback) {\n    this.log(type, () => [\"\", clone(rawDiff)]);\n    const { diff, reply, events, title } = Rendered.extract(rawDiff);\n\n    // Events are either [event, payload] or [event, payload, true]\n    // where the optional third element (true) indicates that the event should\n    // be dispatched before the DOM patch. This is useful in combination with\n    // the onDocumentPatch dom callback.\n    const ev = events.reduce(\n      (acc, args) => {\n        if (args.length === 3 && args[2] == true) {\n          acc.pre.push(args.slice(0, -1));\n        } else {\n          acc.post.push(args);\n        }\n        return acc;\n      },\n      { pre: [], post: [] },\n    );\n\n    this.liveSocket.dispatchEvents(ev.pre);\n\n    const update = () => {\n      callback({ diff, reply, events: ev.post });\n      if (typeof title === \"string\" || (type == \"mount\" && this.isMain())) {\n        window.requestAnimationFrame(() => DOM.putTitle(title));\n      }\n    };\n\n    if (\"onDocumentPatch\" in this.liveSocket.domCallbacks) {\n      this.liveSocket.triggerDOM(\"onDocumentPatch\", [update]);\n    } else {\n      update();\n    }\n  }\n\n  onJoin(resp) {\n    const { rendered, container, liveview_version, pid } = resp;\n    if (container) {\n      const [tag, attrs] = container;\n      this.el = DOM.replaceRootContainer(this.el, tag, attrs);\n    }\n    this.childJoins = 0;\n    this.joinPending = true;\n    this.flash = null;\n    if (this.root === this) {\n      this.formsForRecovery = this.getFormsForRecovery();\n    }\n    if (this.isMain() && window.history.state === null) {\n      // set initial history entry if this is the first page load (no history)\n      Browser.pushState(\"replace\", {\n        type: \"patch\",\n        id: this.id,\n        position: this.liveSocket.currentHistoryPosition,\n      });\n    }\n\n    if (liveview_version !== this.liveSocket.version()) {\n      console.warn(\n        `LiveView asset version mismatch. JavaScript version ${this.liveSocket.version()} vs. server ${liveview_version}. To avoid issues, please ensure that your assets use the same version as the server.`,\n      );\n    }\n\n    // The pid is only sent if\n    //\n    //    config :phoenix_live_view, :debug_attributes\n    //\n    // if set to true. It is to help debugging in development.\n    if (pid) {\n      this.el.setAttribute(PHX_LV_PID, pid);\n    }\n\n    Browser.dropLocal(\n      this.liveSocket.localStorage,\n      window.location.pathname,\n      CONSECUTIVE_RELOADS,\n    );\n    this.applyDiff(\"mount\", rendered, ({ diff, events }) => {\n      this.rendered = new Rendered(this.id, diff);\n      const [html, streams] = this.renderContainer(null, \"join\");\n      this.dropPendingRefs();\n      this.joinCount++;\n      this.joinAttempts = 0;\n\n      this.maybeRecoverForms(html, () => {\n        this.onJoinComplete(resp, html, streams, events);\n      });\n    });\n  }\n\n  dropPendingRefs() {\n    DOM.all(document, `[${PHX_REF_SRC}=\"${this.refSrc()}\"]`, (el) => {\n      el.removeAttribute(PHX_REF_LOADING);\n      el.removeAttribute(PHX_REF_SRC);\n      el.removeAttribute(PHX_REF_LOCK);\n    });\n  }\n\n  onJoinComplete({ live_patch }, html, streams, events) {\n    // In order to provide a better experience, we want to join\n    // all LiveViews first and only then apply their patches.\n    if (this.joinCount > 1 || (this.parent && !this.parent.isJoinPending())) {\n      return this.applyJoinPatch(live_patch, html, streams, events);\n    }\n\n    // One downside of this approach is that we need to find phxChildren\n    // in the html fragment, instead of directly on the DOM. The fragment\n    // also does not include PHX_STATIC, so we need to copy it over from\n    // the DOM.\n    const newChildren = DOM.findPhxChildrenInFragment(html, this.id).filter(\n      (toEl) => {\n        const fromEl = toEl.id && this.el.querySelector(`[id=\"${toEl.id}\"]`);\n        const phxStatic = fromEl && fromEl.getAttribute(PHX_STATIC);\n        if (phxStatic) {\n          toEl.setAttribute(PHX_STATIC, phxStatic);\n        }\n        // set PHX_ROOT_ID to prevent events from being dispatched to the root view\n        // while the child join is still pending\n        if (fromEl) {\n          fromEl.setAttribute(PHX_ROOT_ID, this.root.id);\n        }\n        return this.joinChild(toEl);\n      },\n    );\n\n    if (newChildren.length === 0) {\n      if (this.parent) {\n        this.root.pendingJoinOps.push([\n          this,\n          () => this.applyJoinPatch(live_patch, html, streams, events),\n        ]);\n        this.parent.ackJoin(this);\n      } else {\n        this.onAllChildJoinsComplete();\n        this.applyJoinPatch(live_patch, html, streams, events);\n      }\n    } else {\n      this.root.pendingJoinOps.push([\n        this,\n        () => this.applyJoinPatch(live_patch, html, streams, events),\n      ]);\n    }\n  }\n\n  attachTrueDocEl() {\n    this.el = DOM.byId(this.id);\n    this.el.setAttribute(PHX_ROOT_ID, this.root.id);\n  }\n\n  // this is invoked for dead and live views, so we must filter by\n  // by owner to ensure we aren't duplicating hooks across disconnect\n  // and connected states. This also handles cases where hooks exist\n  // in a root layout with a LV in the body\n  execNewMounted(parent = document) {\n    let phxViewportTop = this.binding(PHX_VIEWPORT_TOP);\n    let phxViewportBottom = this.binding(PHX_VIEWPORT_BOTTOM);\n    this.all(\n      parent,\n      `[${phxViewportTop}], [${phxViewportBottom}]`,\n      (hookEl) => {\n        DOM.maintainPrivateHooks(\n          hookEl,\n          hookEl,\n          phxViewportTop,\n          phxViewportBottom,\n        );\n        this.maybeAddNewHook(hookEl);\n      },\n    );\n    this.all(\n      parent,\n      `[${this.binding(PHX_HOOK)}], [data-phx-${PHX_HOOK}]`,\n      (hookEl) => {\n        this.maybeAddNewHook(hookEl);\n      },\n    );\n    this.all(parent, `[${this.binding(PHX_MOUNTED)}]`, (el) => {\n      this.maybeMounted(el);\n    });\n  }\n\n  all(parent, selector, callback) {\n    DOM.all(parent, selector, (el) => {\n      if (this.ownsElement(el)) {\n        callback(el);\n      }\n    });\n  }\n\n  applyJoinPatch(live_patch, html, streams, events) {\n    // in case of rejoins, we need to manually perform all\n    // pending ops\n    if (this.joinCount > 1) {\n      if (this.pendingJoinOps.length) {\n        this.pendingJoinOps.forEach((cb) => typeof cb === \"function\" && cb());\n        this.pendingJoinOps = [];\n      }\n    }\n    this.attachTrueDocEl();\n    const patch = new DOMPatch(this, this.el, this.id, html, streams, null);\n    patch.markPrunableContentForRemoval();\n    this.performPatch(patch, false, true);\n    this.joinNewChildren();\n    this.execNewMounted();\n\n    this.joinPending = false;\n    this.liveSocket.dispatchEvents(events);\n    this.applyPendingUpdates();\n\n    if (live_patch) {\n      const { kind, to } = live_patch;\n      this.liveSocket.historyPatch(to, kind);\n    }\n    this.hideLoader();\n    if (this.joinCount > 1) {\n      this.triggerReconnected();\n    }\n    this.stopCallback();\n  }\n\n  triggerBeforeUpdateHook(fromEl, toEl) {\n    this.liveSocket.triggerDOM(\"onBeforeElUpdated\", [fromEl, toEl]);\n    const hook = this.getHook(fromEl);\n    const isIgnored = hook && DOM.isIgnored(fromEl, this.binding(PHX_UPDATE));\n    if (\n      hook &&\n      !fromEl.isEqualNode(toEl) &&\n      !(isIgnored && isEqualObj(fromEl.dataset, toEl.dataset))\n    ) {\n      hook.__beforeUpdate();\n      return hook;\n    }\n  }\n\n  maybeMounted(el) {\n    const phxMounted = el.getAttribute(this.binding(PHX_MOUNTED));\n    const hasBeenInvoked = phxMounted && DOM.private(el, \"mounted\");\n    if (phxMounted && !hasBeenInvoked) {\n      this.liveSocket.execJS(el, phxMounted);\n      DOM.putPrivate(el, \"mounted\", true);\n    }\n  }\n\n  maybeAddNewHook(el) {\n    const newHook = this.addHook(el);\n    if (newHook) {\n      newHook.__mounted();\n    }\n  }\n\n  performPatch(patch, pruneCids, isJoinPatch = false) {\n    const removedEls = [];\n    let phxChildrenAdded = false;\n    const updatedHookIds = new Set();\n\n    this.liveSocket.triggerDOM(\"onPatchStart\", [patch.targetContainer]);\n\n    patch.after(\"added\", (el) => {\n      this.liveSocket.triggerDOM(\"onNodeAdded\", [el]);\n      const phxViewportTop = this.binding(PHX_VIEWPORT_TOP);\n      const phxViewportBottom = this.binding(PHX_VIEWPORT_BOTTOM);\n      DOM.maintainPrivateHooks(el, el, phxViewportTop, phxViewportBottom);\n      this.maybeAddNewHook(el);\n      if (el.getAttribute) {\n        this.maybeMounted(el);\n      }\n    });\n\n    patch.after(\"phxChildAdded\", (el) => {\n      if (DOM.isPhxSticky(el)) {\n        this.liveSocket.joinRootViews();\n      } else {\n        phxChildrenAdded = true;\n      }\n    });\n\n    patch.before(\"updated\", (fromEl, toEl) => {\n      const hook = this.triggerBeforeUpdateHook(fromEl, toEl);\n      if (hook) {\n        updatedHookIds.add(fromEl.id);\n      }\n      // trigger JS specific update logic (for example for JS.ignore_attributes)\n      JS.onBeforeElUpdated(fromEl, toEl);\n    });\n\n    patch.after(\"updated\", (el) => {\n      if (updatedHookIds.has(el.id)) {\n        this.getHook(el).__updated();\n      }\n    });\n\n    patch.after(\"discarded\", (el) => {\n      if (el.nodeType === Node.ELEMENT_NODE) {\n        removedEls.push(el);\n      }\n    });\n\n    patch.after(\"transitionsDiscarded\", (els) =>\n      this.afterElementsRemoved(els, pruneCids),\n    );\n    patch.perform(isJoinPatch);\n    this.afterElementsRemoved(removedEls, pruneCids);\n\n    this.liveSocket.triggerDOM(\"onPatchEnd\", [patch.targetContainer]);\n    return phxChildrenAdded;\n  }\n\n  afterElementsRemoved(elements, pruneCids) {\n    const destroyedCIDs = [];\n    elements.forEach((parent) => {\n      const components = DOM.all(\n        parent,\n        `[${PHX_VIEW_REF}=\"${this.id}\"][${PHX_COMPONENT}]`,\n      );\n      const hooks = DOM.all(\n        parent,\n        `[${this.binding(PHX_HOOK)}], [data-phx-hook]`,\n      );\n      components.concat(parent).forEach((el) => {\n        const cid = this.componentID(el);\n        if (\n          isCid(cid) &&\n          destroyedCIDs.indexOf(cid) === -1 &&\n          el.getAttribute(PHX_VIEW_REF) === this.id\n        ) {\n          destroyedCIDs.push(cid);\n        }\n      });\n      hooks.concat(parent).forEach((hookEl) => {\n        const hook = this.getHook(hookEl);\n        hook && this.destroyHook(hook);\n      });\n    });\n    // We should not pruneCids on joins. Otherwise, in case of\n    // rejoins, we may notify cids that no longer belong to the\n    // current LiveView to be removed.\n    if (pruneCids) {\n      this.maybePushComponentsDestroyed(destroyedCIDs);\n    }\n  }\n\n  joinNewChildren() {\n    DOM.findPhxChildren(document, this.id).forEach((el) => this.joinChild(el));\n  }\n\n  maybeRecoverForms(html, callback) {\n    const phxChange = this.binding(\"change\");\n    const oldForms = this.root.formsForRecovery;\n    // So why do we create a template element here?\n    // One way to recover forms would be to immediately apply the mount\n    // patch and then afterwards recover the forms. However, this would\n    // cause a flicker, because the mount patch would remove the form content\n    // until it is restored. Therefore LV decided to do form recovery with the\n    // raw HTML before it is applied and delay the mount patch until the form\n    // recovery events are done.\n    const template = document.createElement(\"template\");\n    template.innerHTML = html;\n\n    // we special case <.portal> here and teleport it into our temporary DOM for recovery\n    // as we'd otherwise not find teleported forms\n    DOM.all(template.content, `[${PHX_PORTAL}]`).forEach((portalTemplate) => {\n      template.content.firstElementChild.appendChild(\n        portalTemplate.content.firstElementChild,\n      );\n    });\n\n    // because we work with a template element, we must manually copy the attributes\n    // otherwise the owner / target helpers don't work properly\n    const rootEl = template.content.firstElementChild;\n    rootEl.id = this.id;\n    rootEl.setAttribute(PHX_ROOT_ID, this.root.id);\n    rootEl.setAttribute(PHX_SESSION, this.getSession());\n    rootEl.setAttribute(PHX_STATIC, this.getStatic());\n    rootEl.setAttribute(PHX_PARENT_ID, this.parent ? this.parent.id : null);\n\n    // we go over all form elements in the new HTML for the LV\n    // and look for old forms in the `formsForRecovery` object;\n    // the formsForRecovery can also contain forms from child views\n    const formsToRecover =\n      // we go over all forms in the new DOM; because this is only the HTML for the current\n      // view, we can be sure that all forms are owned by this view:\n      DOM.all(template.content, \"form\")\n        // only recover forms that have an id and are in the old DOM\n        .filter((newForm) => newForm.id && oldForms[newForm.id])\n        // abandon forms we already tried to recover to prevent looping a failed state\n        .filter((newForm) => !this.pendingForms.has(newForm.id))\n        // only recover if the form has the same phx-change value\n        .filter(\n          (newForm) =>\n            oldForms[newForm.id].getAttribute(phxChange) ===\n            newForm.getAttribute(phxChange),\n        )\n        .map((newForm) => {\n          return [oldForms[newForm.id], newForm];\n        });\n\n    if (formsToRecover.length === 0) {\n      return callback();\n    }\n\n    formsToRecover.forEach(([oldForm, newForm], i) => {\n      this.pendingForms.add(newForm.id);\n      // it is important to use the firstElementChild of the template content\n      // because when traversing a documentFragment using parentNode, we won't ever arrive at\n      // the fragment; as the template is always a LiveView, we can be sure that there is only\n      // one child on the root level\n      this.pushFormRecovery(\n        oldForm,\n        newForm,\n        template.content.firstElementChild,\n        () => {\n          this.pendingForms.delete(newForm.id);\n          // we only call the callback once all forms have been recovered\n          if (i === formsToRecover.length - 1) {\n            callback();\n          }\n        },\n      );\n    });\n  }\n\n  getChildById(id) {\n    return this.root.children[this.id][id];\n  }\n\n  getDescendentByEl(el) {\n    if (el.id === this.id) {\n      return this;\n    } else {\n      return this.children[el.getAttribute(PHX_PARENT_ID)]?.[el.id];\n    }\n  }\n\n  destroyDescendent(id) {\n    for (const parentId in this.root.children) {\n      for (const childId in this.root.children[parentId]) {\n        if (childId === id) {\n          return this.root.children[parentId][childId].destroy();\n        }\n      }\n    }\n  }\n\n  joinChild(el) {\n    const child = this.getChildById(el.id);\n    if (!child) {\n      const view = new View(el, this.liveSocket, this);\n      this.root.children[this.id][view.id] = view;\n      view.join();\n      this.childJoins++;\n      return true;\n    }\n  }\n\n  isJoinPending() {\n    return this.joinPending;\n  }\n\n  ackJoin(_child) {\n    this.childJoins--;\n\n    if (this.childJoins === 0) {\n      if (this.parent) {\n        this.parent.ackJoin(this);\n      } else {\n        this.onAllChildJoinsComplete();\n      }\n    }\n  }\n\n  onAllChildJoinsComplete() {\n    // we can clear pending form recoveries now that we've joined.\n    // They either all resolved or were abandoned\n    this.pendingForms.clear();\n    // we can also clear the formsForRecovery object to not keep old form elements around\n    this.formsForRecovery = {};\n    this.joinCallback(() => {\n      this.pendingJoinOps.forEach(([view, op]) => {\n        if (!view.isDestroyed()) {\n          op();\n        }\n      });\n      this.pendingJoinOps = [];\n    });\n  }\n\n  update(diff, events, isPending = false) {\n    if (\n      this.isJoinPending() ||\n      (this.liveSocket.hasPendingLink() && this.root.isMain())\n    ) {\n      // don't mutate if this is already a pending diff\n      if (!isPending) {\n        this.pendingDiffs.push({ diff, events });\n      }\n      return false;\n    }\n\n    this.rendered.mergeDiff(diff);\n    let phxChildrenAdded = false;\n\n    // When the diff only contains component diffs, then walk components\n    // and patch only the parent component containers found in the diff.\n    // Otherwise, patch entire LV container.\n    if (this.rendered.isComponentOnlyDiff(diff)) {\n      this.liveSocket.time(\"component patch complete\", () => {\n        const parentCids = DOM.findExistingParentCIDs(\n          this.id,\n          this.rendered.componentCIDs(diff),\n        );\n        parentCids.forEach((parentCID) => {\n          if (\n            this.componentPatch(\n              this.rendered.getComponent(diff, parentCID),\n              parentCID,\n            )\n          ) {\n            phxChildrenAdded = true;\n          }\n        });\n      });\n    } else if (!isEmpty(diff)) {\n      this.liveSocket.time(\"full patch complete\", () => {\n        const [html, streams] = this.renderContainer(diff, \"update\");\n        const patch = new DOMPatch(this, this.el, this.id, html, streams, null);\n        phxChildrenAdded = this.performPatch(patch, true);\n      });\n    }\n\n    this.liveSocket.dispatchEvents(events);\n    if (phxChildrenAdded) {\n      this.joinNewChildren();\n    }\n\n    return true;\n  }\n\n  renderContainer(diff, kind) {\n    return this.liveSocket.time(`toString diff (${kind})`, () => {\n      const tag = this.el.tagName;\n      // Don't skip any component in the diff nor any marked as pruned\n      // (as they may have been added back)\n      const cids = diff ? this.rendered.componentCIDs(diff) : null;\n      const { buffer: html, streams } = this.rendered.toString(cids);\n      return [`<${tag}>${html}</${tag}>`, streams];\n    });\n  }\n\n  componentPatch(diff, cid) {\n    if (isEmpty(diff)) return false;\n    const { buffer: html, streams } = this.rendered.componentToString(cid);\n    const patch = new DOMPatch(this, this.el, this.id, html, streams, cid);\n    const childrenAdded = this.performPatch(patch, true);\n    return childrenAdded;\n  }\n\n  getHook(el) {\n    return this.viewHooks[ViewHook.elementID(el)];\n  }\n\n  addHook(el) {\n    const hookElId = ViewHook.elementID(el);\n\n    // only ever try to add hooks to elements owned by this view\n    if (el.getAttribute && !this.ownsElement(el)) {\n      return;\n    }\n\n    if (hookElId && !this.viewHooks[hookElId]) {\n      if (ViewHook.deadHook(el)) {\n        // If the hook is on an element outside of the LiveView,\n        // it is initially mounted by the dead view (view.isDead).\n        // As soon as the main LiveView is connected, it is considered\n        // to be owned by it though, but since the live view has a new\n        // viewHooks object, we don't find it. We mark hooks on \"dead\"\n        // elements as such and just ignore them here.\n        return;\n      }\n      // hook created, but not attached (createHook for web component)\n      const hook =\n        DOM.getCustomElHook(el) ||\n        logError(`no hook found for custom element: ${el.id}`);\n      this.viewHooks[hookElId] = hook;\n      hook.__attachView(this);\n      return hook;\n    } else if (hookElId || !el.getAttribute) {\n      // no hook found\n      return;\n    } else {\n      // new hook found with phx-hook attribute\n      const hookName =\n        el.getAttribute(`data-phx-${PHX_HOOK}`) ||\n        el.getAttribute(this.binding(PHX_HOOK));\n\n      if (!hookName) {\n        return;\n      }\n\n      const hookDefinition = this.liveSocket.getHookDefinition(hookName);\n\n      if (hookDefinition) {\n        if (!el.id) {\n          logError(\n            `no DOM ID for hook \"${hookName}\". Hooks require a unique ID on each element.`,\n            el,\n          );\n          return;\n        }\n\n        let hookInstance;\n        try {\n          if (\n            typeof hookDefinition === \"function\" &&\n            hookDefinition.prototype instanceof ViewHook\n          ) {\n            // It's a class constructor (subclass of ViewHook)\n            hookInstance = new hookDefinition(this, el); // `this` is the View instance\n          } else if (\n            typeof hookDefinition === \"object\" &&\n            hookDefinition !== null\n          ) {\n            // It's an object literal, pass it to the ViewHook constructor for wrapping\n            hookInstance = new ViewHook(this, el, hookDefinition);\n          } else {\n            logError(\n              `Invalid hook definition for \"${hookName}\". Expected a class extending ViewHook or an object definition.`,\n              el,\n            );\n            return;\n          }\n        } catch (e) {\n          const errorMessage = e instanceof Error ? e.message : String(e);\n          logError(`Failed to create hook \"${hookName}\": ${errorMessage}`, el);\n          return;\n        }\n\n        this.viewHooks[ViewHook.elementID(hookInstance.el)] = hookInstance;\n        return hookInstance;\n      } else if (hookName !== null) {\n        logError(`unknown hook found for \"${hookName}\"`, el);\n      }\n    }\n  }\n\n  destroyHook(hook) {\n    // __destroyed clears the elementID from the hook, therefore\n    // we need to get it before calling __destroyed\n    const hookId = ViewHook.elementID(hook.el);\n    hook.__destroyed();\n    hook.__cleanup__();\n    delete this.viewHooks[hookId];\n  }\n\n  applyPendingUpdates() {\n    // To prevent race conditions where we might still be pending a new\n    // navigation or the join is still pending, `this.update` returns false\n    // if the diff was not applied.\n    this.pendingDiffs = this.pendingDiffs.filter(\n      ({ diff, events }) => !this.update(diff, events, true),\n    );\n    this.eachChild((child) => child.applyPendingUpdates());\n  }\n\n  eachChild(callback) {\n    const children = this.root.children[this.id] || {};\n    for (const id in children) {\n      callback(this.getChildById(id));\n    }\n  }\n\n  onChannel(event, cb) {\n    this.liveSocket.onChannel(this.channel, event, (resp) => {\n      if (this.isJoinPending()) {\n        // in case this is a rejoin (joinCount > 1) we store our own join ops\n        if (this.joinCount > 1) {\n          this.pendingJoinOps.push(() => cb(resp));\n        } else {\n          this.root.pendingJoinOps.push([this, () => cb(resp)]);\n        }\n      } else {\n        this.liveSocket.requestDOMUpdate(() => cb(resp));\n      }\n    });\n  }\n\n  bindChannel() {\n    // The diff event should be handled by the regular update operations.\n    // All other operations are queued to be applied only after join.\n    this.liveSocket.onChannel(this.channel, \"diff\", (rawDiff) => {\n      this.liveSocket.requestDOMUpdate(() => {\n        this.applyDiff(\"update\", rawDiff, ({ diff, events }) =>\n          this.update(diff, events),\n        );\n      });\n    });\n    this.onChannel(\"redirect\", ({ to, flash }) =>\n      this.onRedirect({ to, flash }),\n    );\n    this.onChannel(\"live_patch\", (redir) => this.onLivePatch(redir));\n    this.onChannel(\"live_redirect\", (redir) => this.onLiveRedirect(redir));\n    this.channel.onError((reason) => this.onError(reason));\n    this.channel.onClose((reason) => this.onClose(reason));\n  }\n\n  destroyAllChildren() {\n    this.eachChild((child) => child.destroy());\n  }\n\n  onLiveRedirect(redir) {\n    const { to, kind, flash } = redir;\n    const url = this.expandURL(to);\n    const e = new CustomEvent(\"phx:server-navigate\", {\n      detail: { to, kind, flash },\n    });\n    this.liveSocket.historyRedirect(e, url, kind, flash);\n  }\n\n  onLivePatch(redir) {\n    const { to, kind } = redir;\n    this.href = this.expandURL(to);\n    this.liveSocket.historyPatch(to, kind);\n  }\n\n  expandURL(to) {\n    return to.startsWith(\"/\")\n      ? `${window.location.protocol}//${window.location.host}${to}`\n      : to;\n  }\n\n  /**\n   * @param {{to: string, flash?: string, reloadToken?: string}} redirect\n   */\n  onRedirect({ to, flash, reloadToken }) {\n    this.liveSocket.redirect(to, flash, reloadToken);\n  }\n\n  isDestroyed() {\n    return this.destroyed;\n  }\n\n  joinDead() {\n    this.isDead = true;\n  }\n\n  joinPush() {\n    this.joinPush = this.joinPush || this.channel.join();\n    return this.joinPush;\n  }\n\n  join(callback) {\n    this.showLoader(this.liveSocket.loaderTimeout);\n    this.bindChannel();\n    if (this.isMain()) {\n      this.stopCallback = this.liveSocket.withPageLoading({\n        to: this.href,\n        kind: \"initial\",\n      });\n    }\n    this.joinCallback = (onDone) => {\n      onDone = onDone || function () {};\n      callback ? callback(this.joinCount, onDone) : onDone();\n    };\n\n    this.wrapPush(() => this.channel.join(), {\n      ok: (resp) => this.liveSocket.requestDOMUpdate(() => this.onJoin(resp)),\n      error: (error) => this.onJoinError(error),\n      timeout: () => this.onJoinError({ reason: \"timeout\" }),\n    });\n  }\n\n  onJoinError(resp) {\n    if (resp.reason === \"reload\") {\n      this.log(\"error\", () => [\n        `failed mount with ${resp.status}. Falling back to page reload`,\n        resp,\n      ]);\n      this.onRedirect({\n        to: this.liveSocket.main.href,\n        reloadToken: resp.token,\n      });\n      return;\n    } else if (resp.reason === \"unauthorized\" || resp.reason === \"stale\") {\n      this.log(\"error\", () => [\n        \"unauthorized live_redirect. Falling back to page request\",\n        resp,\n      ]);\n      this.onRedirect({ to: this.liveSocket.main.href, flash: this.flash });\n      return;\n    }\n    if (resp.redirect || resp.live_redirect) {\n      this.joinPending = false;\n      this.channel.leave();\n    }\n    if (resp.redirect) {\n      return this.onRedirect(resp.redirect);\n    }\n    if (resp.live_redirect) {\n      return this.onLiveRedirect(resp.live_redirect);\n    }\n    this.log(\"error\", () => [\"unable to join\", resp]);\n    if (this.isMain()) {\n      this.displayError(\n        [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS],\n        { unstructuredError: resp, errorKind: \"server\" },\n      );\n      if (this.liveSocket.isConnected()) {\n        this.liveSocket.reloadWithJitter(this);\n      }\n    } else {\n      if (this.joinAttempts >= MAX_CHILD_JOIN_ATTEMPTS) {\n        // put the root review into permanent error state, but don't destroy it as it can remain active\n        this.root.displayError(\n          [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS],\n          { unstructuredError: resp, errorKind: \"server\" },\n        );\n        this.log(\"error\", () => [\n          `giving up trying to mount after ${MAX_CHILD_JOIN_ATTEMPTS} tries`,\n          resp,\n        ]);\n        this.destroy();\n      }\n      const trueChildEl = DOM.byId(this.el.id);\n      if (trueChildEl) {\n        DOM.mergeAttrs(trueChildEl, this.el);\n        this.displayError(\n          [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS],\n          { unstructuredError: resp, errorKind: \"server\" },\n        );\n        this.el = trueChildEl;\n      } else {\n        this.destroy();\n      }\n    }\n  }\n\n  onClose(reason) {\n    if (this.isDestroyed()) {\n      return;\n    }\n    if (\n      this.isMain() &&\n      this.liveSocket.hasPendingLink() &&\n      reason !== \"leave\"\n    ) {\n      return this.liveSocket.reloadWithJitter(this);\n    }\n    this.destroyAllChildren();\n    this.liveSocket.dropActiveElement(this);\n    if (this.liveSocket.isUnloaded()) {\n      this.showLoader(BEFORE_UNLOAD_LOADER_TIMEOUT);\n    }\n  }\n\n  onError(reason) {\n    this.onClose(reason);\n    if (this.liveSocket.isConnected()) {\n      this.log(\"error\", () => [\"view crashed\", reason]);\n    }\n    if (!this.liveSocket.isUnloaded()) {\n      if (this.liveSocket.isConnected()) {\n        this.displayError(\n          [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS],\n          { unstructuredError: reason, errorKind: \"server\" },\n        );\n      } else {\n        this.displayError(\n          [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_CLIENT_ERROR_CLASS],\n          { unstructuredError: reason, errorKind: \"client\" },\n        );\n      }\n    }\n  }\n\n  displayError(classes, details = {}) {\n    if (this.isMain()) {\n      DOM.dispatchEvent(window, \"phx:page-loading-start\", {\n        detail: { to: this.href, kind: \"error\", ...details },\n      });\n    }\n    this.showLoader();\n    this.setContainerClasses(...classes);\n    this.delayedDisconnected();\n  }\n\n  delayedDisconnected() {\n    this.disconnectedTimer = setTimeout(() => {\n      this.execAll(this.binding(\"disconnected\"));\n    }, this.liveSocket.disconnectedTimeout);\n  }\n\n  wrapPush(callerPush, receives) {\n    const latency = this.liveSocket.getLatencySim();\n    const withLatency = latency\n      ? (cb) => setTimeout(() => !this.isDestroyed() && cb(), latency)\n      : (cb) => !this.isDestroyed() && cb();\n\n    withLatency(() => {\n      callerPush()\n        .receive(\"ok\", (resp) =>\n          withLatency(() => receives.ok && receives.ok(resp)),\n        )\n        .receive(\"error\", (reason) =>\n          withLatency(() => receives.error && receives.error(reason)),\n        )\n        .receive(\"timeout\", () =>\n          withLatency(() => receives.timeout && receives.timeout()),\n        );\n    });\n  }\n\n  pushWithReply(refGenerator, event, payload) {\n    if (!this.isConnected()) {\n      return Promise.reject(new Error(\"no connection\"));\n    }\n\n    const [ref, [el], opts] = refGenerator\n      ? refGenerator({ payload })\n      : [null, [], {}];\n    const oldJoinCount = this.joinCount;\n    let onLoadingDone = function () {};\n    if (opts.page_loading) {\n      onLoadingDone = this.liveSocket.withPageLoading({\n        kind: \"element\",\n        target: el,\n      });\n    }\n\n    if (typeof payload.cid !== \"number\") {\n      delete payload.cid;\n    }\n\n    return new Promise((resolve, reject) => {\n      this.wrapPush(() => this.channel.push(event, payload, PUSH_TIMEOUT), {\n        ok: (resp) => {\n          if (ref !== null) {\n            this.lastAckRef = ref;\n          }\n          const finish = (hookReply) => {\n            if (resp.redirect) {\n              this.onRedirect(resp.redirect);\n            }\n            if (resp.live_patch) {\n              this.onLivePatch(resp.live_patch);\n            }\n            if (resp.live_redirect) {\n              this.onLiveRedirect(resp.live_redirect);\n            }\n            onLoadingDone();\n            resolve({ resp: resp, reply: hookReply, ref });\n          };\n          if (resp.diff) {\n            this.liveSocket.requestDOMUpdate(() => {\n              this.applyDiff(\"update\", resp.diff, ({ diff, reply, events }) => {\n                if (ref !== null) {\n                  this.undoRefs(ref, payload.event);\n                }\n                this.update(diff, events);\n                finish(reply);\n              });\n            });\n          } else {\n            if (ref !== null) {\n              this.undoRefs(ref, payload.event);\n            }\n            finish(null);\n          }\n        },\n        error: (reason) =>\n          reject(new Error(`failed with reason: ${JSON.stringify(reason)}`)),\n        timeout: () => {\n          reject(new Error(\"timeout\"));\n          if (this.joinCount === oldJoinCount) {\n            this.liveSocket.reloadWithJitter(this, () => {\n              this.log(\"timeout\", () => [\n                \"received timeout while communicating with server. Falling back to hard refresh for recovery\",\n              ]);\n            });\n          }\n        },\n      });\n    });\n  }\n\n  undoRefs(ref, phxEvent, onlyEls) {\n    if (!this.isConnected()) {\n      return;\n    } // exit if external form triggered\n    const selector = `[${PHX_REF_SRC}=\"${this.refSrc()}\"]`;\n\n    if (onlyEls) {\n      onlyEls = new Set(onlyEls);\n      DOM.all(document, selector, (parent) => {\n        if (onlyEls && !onlyEls.has(parent)) {\n          return;\n        }\n        // undo any child refs within parent first\n        DOM.all(parent, selector, (child) =>\n          this.undoElRef(child, ref, phxEvent),\n        );\n        this.undoElRef(parent, ref, phxEvent);\n      });\n    } else {\n      DOM.all(document, selector, (el) => this.undoElRef(el, ref, phxEvent));\n    }\n  }\n\n  undoElRef(el, ref, phxEvent) {\n    const elRef = new ElementRef(el);\n\n    elRef.maybeUndo(ref, phxEvent, (clonedTree) => {\n      // we need to perform a full patch on unlocked elements\n      // to perform all the necessary logic (like calling updated for hooks, etc.)\n      const patch = new DOMPatch(this, el, this.id, clonedTree, [], null, {\n        undoRef: ref,\n      });\n      const phxChildrenAdded = this.performPatch(patch, true);\n      DOM.all(el, `[${PHX_REF_SRC}=\"${this.refSrc()}\"]`, (child) =>\n        this.undoElRef(child, ref, phxEvent),\n      );\n      if (phxChildrenAdded) {\n        this.joinNewChildren();\n      }\n    });\n  }\n\n  refSrc() {\n    return this.el.id;\n  }\n\n  putRef(elements, phxEvent, eventType, opts = {}) {\n    const newRef = this.ref++;\n    const disableWith = this.binding(PHX_DISABLE_WITH);\n    if (opts.loading) {\n      const loadingEls = DOM.all(document, opts.loading).map((el) => {\n        return { el, lock: true, loading: true };\n      });\n      elements = elements.concat(loadingEls);\n    }\n\n    for (const { el, lock, loading } of elements) {\n      if (!lock && !loading) {\n        throw new Error(\"putRef requires lock or loading\");\n      }\n      el.setAttribute(PHX_REF_SRC, this.refSrc());\n      if (loading) {\n        el.setAttribute(PHX_REF_LOADING, newRef);\n      }\n      if (lock) {\n        el.setAttribute(PHX_REF_LOCK, newRef);\n      }\n\n      if (\n        !loading ||\n        (opts.submitter && !(el === opts.submitter || el === opts.form))\n      ) {\n        continue;\n      }\n\n      const lockCompletePromise = new Promise((resolve) => {\n        el.addEventListener(`phx:undo-lock:${newRef}`, () => resolve(detail), {\n          once: true,\n        });\n      });\n\n      const loadingCompletePromise = new Promise((resolve) => {\n        el.addEventListener(\n          `phx:undo-loading:${newRef}`,\n          () => resolve(detail),\n          { once: true },\n        );\n      });\n\n      el.classList.add(`phx-${eventType}-loading`);\n      const disableText = el.getAttribute(disableWith);\n      if (disableText !== null) {\n        if (!el.getAttribute(PHX_DISABLE_WITH_RESTORE)) {\n          el.setAttribute(PHX_DISABLE_WITH_RESTORE, el.textContent);\n        }\n        if (disableText !== \"\") {\n          el.textContent = disableText;\n        }\n        // PHX_DISABLED could have already been set in disableForm\n        el.setAttribute(\n          PHX_DISABLED,\n          el.getAttribute(PHX_DISABLED) || el.disabled,\n        );\n        el.setAttribute(\"disabled\", \"\");\n      }\n\n      const detail = {\n        event: phxEvent,\n        eventType: eventType,\n        ref: newRef,\n        isLoading: loading,\n        isLocked: lock,\n        lockElements: elements.filter(({ lock }) => lock).map(({ el }) => el),\n        loadingElements: elements\n          .filter(({ loading }) => loading)\n          .map(({ el }) => el),\n        unlock: (els) => {\n          els = Array.isArray(els) ? els : [els];\n          this.undoRefs(newRef, phxEvent, els);\n        },\n        lockComplete: lockCompletePromise,\n        loadingComplete: loadingCompletePromise,\n        lock: (lockEl) => {\n          return new Promise((resolve) => {\n            if (this.isAcked(newRef)) {\n              return resolve(detail);\n            }\n            lockEl.setAttribute(PHX_REF_LOCK, newRef);\n            lockEl.setAttribute(PHX_REF_SRC, this.refSrc());\n            lockEl.addEventListener(\n              `phx:lock-stop:${newRef}`,\n              () => resolve(detail),\n              { once: true },\n            );\n          });\n        },\n      };\n      if (opts.payload) {\n        detail[\"payload\"] = opts.payload;\n      }\n      if (opts.target) {\n        detail[\"target\"] = opts.target;\n      }\n      if (opts.originalEvent) {\n        detail[\"originalEvent\"] = opts.originalEvent;\n      }\n      el.dispatchEvent(\n        new CustomEvent(\"phx:push\", {\n          detail: detail,\n          bubbles: true,\n          cancelable: false,\n        }),\n      );\n      if (phxEvent) {\n        el.dispatchEvent(\n          new CustomEvent(`phx:push:${phxEvent}`, {\n            detail: detail,\n            bubbles: true,\n            cancelable: false,\n          }),\n        );\n      }\n    }\n    return [newRef, elements.map(({ el }) => el), opts];\n  }\n\n  isAcked(ref) {\n    return this.lastAckRef !== null && this.lastAckRef >= ref;\n  }\n\n  componentID(el) {\n    const cid = el.getAttribute && el.getAttribute(PHX_COMPONENT);\n    return cid ? parseInt(cid) : null;\n  }\n\n  targetComponentID(target, targetCtx, opts = {}) {\n    if (isCid(targetCtx)) {\n      return targetCtx;\n    }\n\n    const cidOrSelector =\n      opts.target || target.getAttribute(this.binding(\"target\"));\n    if (isCid(cidOrSelector)) {\n      return parseInt(cidOrSelector);\n    } else if (targetCtx && (cidOrSelector !== null || opts.target)) {\n      return this.closestComponentID(targetCtx);\n    } else {\n      return null;\n    }\n  }\n\n  closestComponentID(targetCtx) {\n    if (isCid(targetCtx)) {\n      return targetCtx;\n    } else if (targetCtx) {\n      return maybe(\n        // We either use the closest data-phx-component binding, or -\n        // in case of portals - continue with the portal source.\n        // This is necessary if teleporting an element outside of its LiveComponent.\n        targetCtx.closest(`[${PHX_COMPONENT}],[${PHX_TELEPORTED_SRC}]`),\n        (el) => {\n          // Default case, direct component.\n          if (el.hasAttribute(PHX_COMPONENT)) {\n            return this.ownsElement(el) && this.componentID(el);\n          }\n          // Teleported, search for the closest live component starting\n          // at the portal source.\n          if (el.hasAttribute(PHX_TELEPORTED_SRC)) {\n            const portalParent = DOM.byId(el.getAttribute(PHX_TELEPORTED_SRC));\n            return this.closestComponentID(portalParent);\n          }\n        },\n      );\n    } else {\n      return null;\n    }\n  }\n\n  pushHookEvent(el, targetCtx, event, payload) {\n    if (!this.isConnected()) {\n      this.log(\"hook\", () => [\n        \"unable to push hook event. LiveView not connected\",\n        event,\n        payload,\n      ]);\n      return Promise.reject(\n        new Error(\"unable to push hook event. LiveView not connected\"),\n      );\n    }\n\n    const refGenerator = () =>\n      this.putRef([{ el, loading: true, lock: true }], event, \"hook\", {\n        payload,\n        target: targetCtx,\n      });\n\n    return this.pushWithReply(refGenerator, \"event\", {\n      type: \"hook\",\n      event: event,\n      value: payload,\n      cid: this.closestComponentID(targetCtx),\n    }).then(({ resp: _resp, reply, ref }) => ({ reply, ref }));\n  }\n\n  extractMeta(el, meta, value) {\n    const prefix = this.binding(\"value-\");\n    for (let i = 0; i < el.attributes.length; i++) {\n      if (!meta) {\n        meta = {};\n      }\n      const name = el.attributes[i].name;\n      if (name.startsWith(prefix)) {\n        meta[name.replace(prefix, \"\")] = el.getAttribute(name);\n      }\n    }\n    if (el.value !== undefined && !(el instanceof HTMLFormElement)) {\n      if (!meta) {\n        meta = {};\n      }\n      meta.value = el.value;\n\n      if (\n        el.tagName === \"INPUT\" &&\n        CHECKABLE_INPUTS.indexOf(el.type) >= 0 &&\n        !el.checked\n      ) {\n        delete meta.value;\n      }\n    }\n    if (value) {\n      if (!meta) {\n        meta = {};\n      }\n      for (const key in value) {\n        meta[key] = value[key];\n      }\n    }\n    return meta;\n  }\n\n  serializeForm(form, opts, onlyNames = []) {\n    const { submitter } = opts;\n\n    // We must inject the submitter in the order that it exists in the DOM\n    // relative to other inputs. For example, for checkbox groups, the order must be maintained.\n    let injectedElement;\n    if (submitter && submitter.name) {\n      const input = document.createElement(\"input\");\n      input.type = \"hidden\";\n      // set the form attribute if the submitter has one;\n      // this can happen if the element is outside the actual form element\n      const formId = submitter.getAttribute(\"form\");\n      if (formId) {\n        input.setAttribute(\"form\", formId);\n      }\n      input.name = submitter.name;\n      input.value = submitter.value;\n      submitter.parentElement.insertBefore(input, submitter);\n      injectedElement = input;\n    }\n\n    const formData = new FormData(form);\n    const toRemove = [];\n\n    formData.forEach((val, key, _index) => {\n      if (val instanceof File) {\n        toRemove.push(key);\n      }\n    });\n\n    // Cleanup after building fileData\n    toRemove.forEach((key) => formData.delete(key));\n\n    const params = new URLSearchParams();\n\n    const { inputsUnused, onlyHiddenInputs } = Array.from(form.elements).reduce(\n      (acc, input) => {\n        const { inputsUnused, onlyHiddenInputs } = acc;\n        const key = input.name;\n        if (!key) {\n          return acc;\n        }\n\n        if (inputsUnused[key] === undefined) {\n          inputsUnused[key] = true;\n        }\n        if (onlyHiddenInputs[key] === undefined) {\n          onlyHiddenInputs[key] = true;\n        }\n\n        const inputSkipUnusedField = input.hasAttribute(\n          this.binding(PHX_NO_UNUSED_FIELD),\n        );\n\n        const isUsed =\n          DOM.private(input, PHX_HAS_FOCUSED) ||\n          DOM.private(input, PHX_HAS_SUBMITTED) ||\n          inputSkipUnusedField;\n\n        const isHidden = input.type === \"hidden\";\n        inputsUnused[key] = inputsUnused[key] && !isUsed;\n        onlyHiddenInputs[key] = onlyHiddenInputs[key] && isHidden;\n\n        return acc;\n      },\n      { inputsUnused: {}, onlyHiddenInputs: {} },\n    );\n\n    const formSkipUnusedFields = form.hasAttribute(\n      this.binding(PHX_NO_UNUSED_FIELD),\n    );\n\n    for (const [key, val] of formData.entries()) {\n      if (onlyNames.length === 0 || onlyNames.indexOf(key) >= 0) {\n        const isUnused = inputsUnused[key];\n        const hidden = onlyHiddenInputs[key];\n        const skipUnusedCheck = formSkipUnusedFields;\n\n        if (\n          !skipUnusedCheck &&\n          isUnused &&\n          !(submitter && submitter.name == key) &&\n          !hidden\n        ) {\n          params.append(prependFormDataKey(key, \"_unused_\"), \"\");\n        }\n        if (typeof val === \"string\") {\n          params.append(key, val);\n        }\n      }\n    }\n\n    // remove the injected element again\n    // (it would be removed by the next dom patch anyway, but this is cleaner)\n    if (submitter && injectedElement) {\n      submitter.parentElement.removeChild(injectedElement);\n    }\n\n    return params.toString();\n  }\n\n  pushEvent(type, el, targetCtx, phxEvent, meta, opts = {}, onReply) {\n    this.pushWithReply(\n      (maybePayload) =>\n        this.putRef([{ el, loading: true, lock: true }], phxEvent, type, {\n          ...opts,\n          payload: maybePayload?.payload,\n        }),\n      \"event\",\n      {\n        type: type,\n        event: phxEvent,\n        value: this.extractMeta(el, meta, opts.value),\n        cid: this.targetComponentID(el, targetCtx, opts),\n      },\n    )\n      .then(({ reply }) => onReply && onReply(reply))\n      .catch((error) => logError(\"Failed to push event\", error));\n  }\n\n  pushFileProgress(fileEl, entryRef, progress, onReply = function () {}) {\n    this.liveSocket.withinOwners(fileEl.form, (view, targetCtx) => {\n      view\n        .pushWithReply(null, \"progress\", {\n          event: fileEl.getAttribute(view.binding(PHX_PROGRESS)),\n          ref: fileEl.getAttribute(PHX_UPLOAD_REF),\n          entry_ref: entryRef,\n          progress: progress,\n          cid: view.targetComponentID(fileEl.form, targetCtx),\n        })\n        .then(() => onReply())\n        .catch((error) => logError(\"Failed to push file progress\", error));\n    });\n  }\n\n  pushInput(inputEl, targetCtx, forceCid, phxEvent, opts, callback) {\n    if (!inputEl.form) {\n      throw new Error(\"form events require the input to be inside a form\");\n    }\n\n    let uploads;\n    const cid = isCid(forceCid)\n      ? forceCid\n      : this.targetComponentID(inputEl.form, targetCtx, opts);\n    const refGenerator = (maybePayload) => {\n      return this.putRef(\n        [\n          { el: inputEl, loading: true, lock: true },\n          { el: inputEl.form, loading: true, lock: true },\n        ],\n        phxEvent,\n        \"change\",\n        { ...opts, payload: maybePayload?.payload },\n      );\n    };\n    let formData;\n    const meta = this.extractMeta(inputEl.form, {}, opts.value);\n    const serializeOpts = {};\n    if (inputEl instanceof HTMLButtonElement) {\n      serializeOpts.submitter = inputEl;\n    }\n    if (inputEl.getAttribute(this.binding(\"change\"))) {\n      formData = this.serializeForm(inputEl.form, serializeOpts, [\n        inputEl.name,\n      ]);\n    } else {\n      formData = this.serializeForm(inputEl.form, serializeOpts);\n    }\n    if (\n      DOM.isUploadInput(inputEl) &&\n      inputEl.files &&\n      inputEl.files.length > 0\n    ) {\n      LiveUploader.trackFiles(inputEl, Array.from(inputEl.files));\n    }\n    uploads = LiveUploader.serializeUploads(inputEl);\n\n    const event = {\n      type: \"form\",\n      event: phxEvent,\n      value: formData,\n      meta: {\n        // no target was implicitly sent as \"undefined\" in LV <= 1.0.5, therefore\n        // we have to keep it. In 1.0.6 we switched from passing meta as URL encoded data\n        // to passing it directly in the event, but the JSON encode would drop keys with\n        // undefined values.\n        _target: opts._target || \"undefined\",\n        ...meta,\n      },\n      uploads: uploads,\n      cid: cid,\n    };\n    this.pushWithReply(refGenerator, \"event\", event)\n      .then(({ resp }) => {\n        if (DOM.isUploadInput(inputEl) && DOM.isAutoUpload(inputEl)) {\n          // the element could be inside a locked parent for other unrelated changes;\n          // we can only start uploads when the tree is unlocked and the\n          // necessary data attributes are set in the real DOM\n          ElementRef.onUnlock(inputEl, () => {\n            if (LiveUploader.filesAwaitingPreflight(inputEl).length > 0) {\n              const [ref, _els] = refGenerator();\n              this.undoRefs(ref, phxEvent, [inputEl.form]);\n              this.uploadFiles(\n                inputEl.form,\n                phxEvent,\n                targetCtx,\n                ref,\n                cid,\n                (_uploads) => {\n                  callback && callback(resp);\n                  this.triggerAwaitingSubmit(inputEl.form, phxEvent);\n                  this.undoRefs(ref, phxEvent);\n                },\n              );\n            }\n          });\n        } else {\n          callback && callback(resp);\n        }\n      })\n      .catch((error) => logError(\"Failed to push input event\", error));\n  }\n\n  triggerAwaitingSubmit(formEl, phxEvent) {\n    const awaitingSubmit = this.getScheduledSubmit(formEl);\n    if (awaitingSubmit) {\n      const [_el, _ref, _opts, callback] = awaitingSubmit;\n      this.cancelSubmit(formEl, phxEvent);\n      callback();\n    }\n  }\n\n  getScheduledSubmit(formEl) {\n    return this.formSubmits.find(([el, _ref, _opts, _callback]) =>\n      el.isSameNode(formEl),\n    );\n  }\n\n  scheduleSubmit(formEl, ref, opts, callback) {\n    if (this.getScheduledSubmit(formEl)) {\n      return true;\n    }\n    this.formSubmits.push([formEl, ref, opts, callback]);\n  }\n\n  cancelSubmit(formEl, phxEvent) {\n    this.formSubmits = this.formSubmits.filter(\n      ([el, ref, _opts, _callback]) => {\n        if (el.isSameNode(formEl)) {\n          this.undoRefs(ref, phxEvent);\n          return false;\n        } else {\n          return true;\n        }\n      },\n    );\n  }\n\n  disableForm(formEl, phxEvent, opts = {}) {\n    const filterIgnored = (el) => {\n      const userIgnored = closestPhxBinding(\n        el,\n        `${this.binding(PHX_UPDATE)}=ignore`,\n        el.form,\n      );\n      return !(\n        userIgnored || closestPhxBinding(el, \"data-phx-update=ignore\", el.form)\n      );\n    };\n    const filterDisables = (el) => {\n      return el.hasAttribute(this.binding(PHX_DISABLE_WITH));\n    };\n    const filterButton = (el) => el.tagName == \"BUTTON\";\n\n    const filterInput = (el) =>\n      [\"INPUT\", \"TEXTAREA\", \"SELECT\"].includes(el.tagName);\n\n    const formElements = Array.from(formEl.elements);\n    const disables = formElements.filter(filterDisables);\n    const buttons = formElements.filter(filterButton).filter(filterIgnored);\n    const inputs = formElements.filter(filterInput).filter(filterIgnored);\n\n    buttons.forEach((button) => {\n      button.setAttribute(PHX_DISABLED, button.disabled);\n      button.disabled = true;\n    });\n    inputs.forEach((input) => {\n      input.setAttribute(PHX_READONLY, input.readOnly);\n      input.readOnly = true;\n      if (input.files) {\n        input.setAttribute(PHX_DISABLED, input.disabled);\n        input.disabled = true;\n      }\n    });\n    const formEls = disables\n      .concat(buttons)\n      .concat(inputs)\n      .map((el) => {\n        return { el, loading: true, lock: true };\n      });\n\n    // we reverse the order so form children are already locked by the time\n    // the form is locked\n    const els = [{ el: formEl, loading: true, lock: false }]\n      .concat(formEls)\n      .reverse();\n    return this.putRef(els, phxEvent, \"submit\", opts);\n  }\n\n  pushFormSubmit(formEl, targetCtx, phxEvent, submitter, opts, onReply) {\n    const refGenerator = (maybePayload) =>\n      this.disableForm(formEl, phxEvent, {\n        ...opts,\n        form: formEl,\n        payload: maybePayload?.payload,\n        submitter: submitter,\n      });\n    // store the submitter in the form element in order to trigger it\n    // for phx-trigger-action\n    DOM.putPrivate(formEl, \"submitter\", submitter);\n    const cid = this.targetComponentID(formEl, targetCtx);\n    if (LiveUploader.hasUploadsInProgress(formEl)) {\n      const [ref, _els] = refGenerator();\n      const push = () =>\n        this.pushFormSubmit(\n          formEl,\n          targetCtx,\n          phxEvent,\n          submitter,\n          opts,\n          onReply,\n        );\n      return this.scheduleSubmit(formEl, ref, opts, push);\n    } else if (LiveUploader.inputsAwaitingPreflight(formEl).length > 0) {\n      const [ref, els] = refGenerator();\n      const proxyRefGen = () => [ref, els, opts];\n      this.uploadFiles(formEl, phxEvent, targetCtx, ref, cid, (_uploads) => {\n        // if we still having pending preflights it means we have invalid entries\n        // and the phx-submit cannot be completed\n        if (LiveUploader.inputsAwaitingPreflight(formEl).length > 0) {\n          return this.undoRefs(ref, phxEvent);\n        }\n        const meta = this.extractMeta(formEl, {}, opts.value);\n        const formData = this.serializeForm(formEl, { submitter });\n        this.pushWithReply(proxyRefGen, \"event\", {\n          type: \"form\",\n          event: phxEvent,\n          value: formData,\n          meta: meta,\n          cid: cid,\n        })\n          .then(({ resp }) => onReply(resp))\n          .catch((error) => logError(\"Failed to push form submit\", error));\n      });\n    } else if (\n      !(\n        formEl.hasAttribute(PHX_REF_SRC) &&\n        formEl.classList.contains(\"phx-submit-loading\")\n      )\n    ) {\n      const meta = this.extractMeta(formEl, {}, opts.value);\n      const formData = this.serializeForm(formEl, { submitter });\n      this.pushWithReply(refGenerator, \"event\", {\n        type: \"form\",\n        event: phxEvent,\n        value: formData,\n        meta: meta,\n        cid: cid,\n      })\n        .then(({ resp }) => onReply(resp))\n        .catch((error) => logError(\"Failed to push form submit\", error));\n    }\n  }\n\n  uploadFiles(formEl, phxEvent, targetCtx, ref, cid, onComplete) {\n    const joinCountAtUpload = this.joinCount;\n    const inputEls = LiveUploader.activeFileInputs(formEl);\n    let numFileInputsInProgress = inputEls.length;\n\n    // get each file input\n    inputEls.forEach((inputEl) => {\n      const uploader = new LiveUploader(inputEl, this, () => {\n        numFileInputsInProgress--;\n        if (numFileInputsInProgress === 0) {\n          onComplete();\n        }\n      });\n\n      const entries = uploader\n        .entries()\n        .map((entry) => entry.toPreflightPayload());\n\n      if (entries.length === 0) {\n        numFileInputsInProgress--;\n        return;\n      }\n\n      const payload = {\n        ref: inputEl.getAttribute(PHX_UPLOAD_REF),\n        entries: entries,\n        cid: this.targetComponentID(inputEl.form, targetCtx),\n      };\n\n      this.log(\"upload\", () => [\"sending preflight request\", payload]);\n\n      this.pushWithReply(null, \"allow_upload\", payload)\n        .then(({ resp }) => {\n          this.log(\"upload\", () => [\"got preflight response\", resp]);\n          // the preflight will reject entries beyond the max entries\n          // so we error and cancel entries on the client that are missing from the response\n          uploader.entries().forEach((entry) => {\n            if (resp.entries && !resp.entries[entry.ref]) {\n              this.handleFailedEntryPreflight(\n                entry.ref,\n                \"failed preflight\",\n                uploader,\n              );\n            }\n          });\n          // for auto uploads, we may have an empty entries response from the server\n          // for form submits that contain invalid entries\n          if (resp.error || Object.keys(resp.entries).length === 0) {\n            this.undoRefs(ref, phxEvent);\n            const errors = resp.error || [];\n            errors.map(([entry_ref, reason]) => {\n              this.handleFailedEntryPreflight(entry_ref, reason, uploader);\n            });\n          } else {\n            const onError = (callback) => {\n              this.channel.onError(() => {\n                if (this.joinCount === joinCountAtUpload) {\n                  callback();\n                }\n              });\n            };\n            uploader.initAdapterUpload(resp, onError, this.liveSocket);\n          }\n        })\n        .catch((error) => logError(\"Failed to push upload\", error));\n    });\n  }\n\n  handleFailedEntryPreflight(uploadRef, reason, uploader) {\n    if (uploader.isAutoUpload()) {\n      // uploadRef may be top level upload config ref or entry ref\n      const entry = uploader\n        .entries()\n        .find((entry) => entry.ref === uploadRef.toString());\n      if (entry) {\n        entry.cancel();\n      }\n    } else {\n      uploader.entries().map((entry) => entry.cancel());\n    }\n    this.log(\"upload\", () => [`error for entry ${uploadRef}`, reason]);\n  }\n\n  dispatchUploads(targetCtx, name, filesOrBlobs) {\n    const targetElement = this.targetCtxElement(targetCtx) || this.el;\n    const inputs = DOM.findUploadInputs(targetElement).filter(\n      (el) => el.name === name,\n    );\n    if (inputs.length === 0) {\n      logError(`no live file inputs found matching the name \"${name}\"`);\n    } else if (inputs.length > 1) {\n      logError(`duplicate live file inputs found matching the name \"${name}\"`);\n    } else {\n      DOM.dispatchEvent(inputs[0], PHX_TRACK_UPLOADS, {\n        detail: { files: filesOrBlobs },\n      });\n    }\n  }\n\n  targetCtxElement(targetCtx) {\n    if (isCid(targetCtx)) {\n      const [target] = DOM.findComponentNodeList(this.id, targetCtx);\n      return target;\n    } else if (targetCtx) {\n      return targetCtx;\n    } else {\n      return null;\n    }\n  }\n\n  pushFormRecovery(oldForm, newForm, templateDom, callback) {\n    // we are only recovering forms inside the current view, therefore it is safe to\n    // skip withinOwners here and always use this when referring to the view\n    const phxChange = this.binding(\"change\");\n    const phxTarget = newForm.getAttribute(this.binding(\"target\")) || newForm;\n    const phxEvent =\n      newForm.getAttribute(this.binding(PHX_AUTO_RECOVER)) ||\n      newForm.getAttribute(this.binding(\"change\"));\n    const inputs = Array.from(oldForm.elements).filter(\n      (el) => DOM.isFormInput(el) && el.name && !el.hasAttribute(phxChange),\n    );\n    if (inputs.length === 0) {\n      callback();\n      return;\n    }\n\n    // we must clear tracked uploads before recovery as they no longer have valid refs\n    inputs.forEach(\n      (input) =>\n        input.hasAttribute(PHX_UPLOAD_REF) && LiveUploader.clearFiles(input),\n    );\n    // pushInput assumes that there is a source element that initiated the change;\n    // because this is not the case when we recover forms, we provide the first input we find\n    const input = inputs.find((el) => el.type !== \"hidden\") || inputs[0];\n\n    // in the case that there are multiple targets, we count the number of pending recovery events\n    // and only call the callback once all events have been processed\n    let pending = 0;\n    // withinTargets(phxTarget, callback, dom, viewEl)\n    this.withinTargets(\n      phxTarget,\n      (targetView, targetCtx) => {\n        const cid = this.targetComponentID(newForm, targetCtx);\n        pending++;\n        let e = new CustomEvent(\"phx:form-recovery\", {\n          detail: { sourceElement: oldForm },\n        });\n        JS.exec(e, \"change\", phxEvent, this, input, [\n          \"push\",\n          {\n            _target: input.name,\n            targetView,\n            targetCtx,\n            newCid: cid,\n            callback: () => {\n              pending--;\n              if (pending === 0) {\n                callback();\n              }\n            },\n          },\n        ]);\n      },\n      templateDom,\n    );\n  }\n\n  pushLinkPatch(e, href, targetEl, callback) {\n    const linkRef = this.liveSocket.setPendingLink(href);\n    // only add loading states if event is trusted (it was triggered by user, such as click) and\n    // it's not a forward/back navigation from popstate\n    const loading = e.isTrusted && e.type !== \"popstate\";\n    const refGen = targetEl\n      ? () =>\n          this.putRef(\n            [{ el: targetEl, loading: loading, lock: true }],\n            null,\n            \"click\",\n          )\n      : null;\n    const fallback = () => this.liveSocket.redirect(window.location.href);\n    const url = href.startsWith(\"/\")\n      ? `${location.protocol}//${location.host}${href}`\n      : href;\n\n    this.pushWithReply(refGen, \"live_patch\", { url }).then(\n      ({ resp }) => {\n        this.liveSocket.requestDOMUpdate(() => {\n          if (resp.link_redirect) {\n            this.liveSocket.replaceMain(href, null, callback, linkRef);\n          } else if (resp.redirect) {\n            // handled by bindChannel\n            return;\n          } else {\n            if (this.liveSocket.commitPendingLink(linkRef)) {\n              this.href = href;\n            }\n            this.applyPendingUpdates();\n            callback && callback(linkRef);\n          }\n        });\n      },\n      ({ error: _error, timeout: _timeout }) => fallback(),\n    );\n  }\n\n  getFormsForRecovery() {\n    // Form recovery is complex in LiveView:\n    // We want to support nested LiveViews and also provide a good user experience.\n    // Therefore, when the channel rejoins, we copy all forms that are eligible for\n    // recovery to be able to access them later.\n    // Why do we need to copy them? Because when the main LiveView joins, any forms\n    // in nested LiveViews would be lost.\n    //\n    // We should rework this in the future to serialize the form payload here\n    // instead of cloning the DOM nodes, but making this work correctly is tedious,\n    // as sending the correct form payload relies on JS.push to extract values\n    // from JS commands (phx-change={JS.push(\"event\", value: ..., target: ...)}),\n    // as well as view.pushInput, which expects DOM elements.\n\n    if (this.joinCount === 0) {\n      return {};\n    }\n\n    const phxChange = this.binding(\"change\");\n\n    return DOM.all(\n      document,\n      `#${CSS.escape(this.id)} form[${phxChange}], [${PHX_TELEPORTED_REF}=\"${CSS.escape(this.id)}\"] form[${phxChange}]`,\n    )\n      .filter((form) => form.id)\n      .filter((form) => form.elements.length > 0)\n      .filter(\n        (form) =>\n          form.getAttribute(this.binding(PHX_AUTO_RECOVER)) !== \"ignore\",\n      )\n      .map((form) => {\n        // We need to clone the whole form, as relying on form.elements can lead to\n        // situations where we have\n        //\n        //   <form><fieldset disabled><input name=\"foo\" value=\"bar\"></fieldset></form>\n        //\n        // and form.elements returns both the fieldset and the input separately.\n        // Because the fieldset is disabled, the input should NOT be sent though.\n        // We can only reliably serialize the form by cloning it fully.\n        const clonedForm = form.cloneNode(true);\n        // we call morphdom to copy any special state\n        // like the selected option of a <select> element;\n        // any also copy over privates (which contain information about touched fields)\n        morphdom(clonedForm, form, {\n          onBeforeElUpdated: (fromEl, toEl) => {\n            DOM.copyPrivates(fromEl, toEl);\n            if (fromEl.getAttribute(\"form\") === form.id) {\n              // In case the form contains an element with form=\"id\" pointing\n              // to the form itself, firefox still associates the element with the\n              // original form element. This is not fixed by removing the parameter,\n              // instead we remove the element from the form and add it again without\n              // form parameter below.\n              // See: https://github.com/phoenixframework/phoenix_live_view/issues/4021\n              fromEl.parentNode.removeChild(fromEl);\n              return false;\n            }\n            return true;\n          },\n        });\n        // next up, we also need to clone any elements with form=\"id\" parameter\n        const externalElements = document.querySelectorAll(\n          `[form=\"${CSS.escape(form.id)}\"]`,\n        );\n        Array.from(externalElements).forEach((el) => {\n          const clonedEl = /** @type {HTMLElement} */ (el.cloneNode(true));\n          morphdom(clonedEl, el);\n          DOM.copyPrivates(clonedEl, el);\n          // See https://github.com/phoenixframework/phoenix_live_view/issues/4021\n          clonedEl.removeAttribute(\"form\");\n          clonedForm.appendChild(clonedEl);\n        });\n        return clonedForm;\n      })\n      .reduce((acc, form) => {\n        acc[form.id] = form;\n        return acc;\n      }, {});\n  }\n\n  maybePushComponentsDestroyed(destroyedCIDs) {\n    let willDestroyCIDs = destroyedCIDs.filter((cid) => {\n      return DOM.findComponentNodeList(this.id, cid).length === 0;\n    });\n\n    const onError = (error) => {\n      if (!this.isDestroyed()) {\n        logError(\"Failed to push components destroyed\", error);\n      }\n    };\n\n    if (willDestroyCIDs.length > 0) {\n      // we must reset the render change tracking for cids that\n      // could be added back from the server so we don't skip them\n      willDestroyCIDs.forEach((cid) => this.rendered.resetRender(cid));\n\n      this.pushWithReply(null, \"cids_will_destroy\", { cids: willDestroyCIDs })\n        .then(() => {\n          // we must wait for pending transitions to complete before determining\n          // if the cids were added back to the DOM in the meantime (#3139)\n          this.liveSocket.requestDOMUpdate(() => {\n            // See if any of the cids we wanted to destroy were added back,\n            // if they were added back, we don't actually destroy them.\n            let completelyDestroyCIDs = willDestroyCIDs.filter((cid) => {\n              return DOM.findComponentNodeList(this.id, cid).length === 0;\n            });\n\n            if (completelyDestroyCIDs.length > 0) {\n              this.pushWithReply(null, \"cids_destroyed\", {\n                cids: completelyDestroyCIDs,\n              })\n                .then(({ resp }) => {\n                  this.rendered.pruneCIDs(resp.cids);\n                })\n                .catch(onError);\n            }\n          });\n        })\n        .catch(onError);\n    }\n  }\n\n  ownsElement(el) {\n    let parentViewEl = DOM.closestViewEl(el);\n    return (\n      el.getAttribute(PHX_PARENT_ID) === this.id ||\n      (parentViewEl && parentViewEl.id === this.id) ||\n      (!parentViewEl && this.isDead)\n    );\n  }\n\n  submitForm(form, targetCtx, phxEvent, submitter, opts = {}) {\n    DOM.putPrivate(form, PHX_HAS_SUBMITTED, true);\n    const inputs = Array.from(form.elements);\n    inputs.forEach((input) => DOM.putPrivate(input, PHX_HAS_SUBMITTED, true));\n    this.liveSocket.blurActiveElement(this);\n    this.pushFormSubmit(form, targetCtx, phxEvent, submitter, opts, () => {\n      this.liveSocket.restorePreviouslyActiveFocus();\n    });\n  }\n\n  binding(kind) {\n    return this.liveSocket.binding(kind);\n  }\n\n  // phx-portal\n  pushPortalElementId(id) {\n    this.portalElementIds.add(id);\n  }\n\n  dropPortalElementId(id) {\n    this.portalElementIds.delete(id);\n  }\n\n  destroyPortalElements() {\n    // We only unload the socket if we navigate away\n    // (for example for a form submit or external navigation)\n    // so there's no need to remove portal elements.\n    // In fact, we actually need to keep the portal elements in\n    // case this is an external form submission from a teleported form.\n    if (!this.liveSocket.unloaded) {\n      this.portalElementIds.forEach((id) => {\n        const el = document.getElementById(id);\n        if (el) {\n          el.remove();\n        }\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "assets/js/phoenix_live_view/view_hook.ts",
    "content": "import jsCommands, { EncodedJS, HookJSCommands } from \"./js_commands\";\nimport DOM from \"./dom\";\nimport LiveSocket from \"./live_socket\";\nimport View from \"./view\";\n\nconst HOOK_ID = \"hookId\";\nconst DEAD_HOOK = \"deadHook\";\nlet viewHookID = 1;\n\nexport type OnReply = (reply: any, ref: number) => any;\nexport type CallbackRef = { event: string; callback: (payload: any) => any };\n\nexport type PhxTarget = string | number | HTMLElement;\n\nexport interface HookInterface<E extends HTMLElement = HTMLElement> {\n  /**\n   * The DOM element that the hook is attached to.\n   */\n  el: E;\n\n  /**\n   * The LiveSocket instance that the hook is attached to.\n   */\n  liveSocket: LiveSocket;\n\n  /**\n   * The mounted callback.\n   *\n   * Called when the element has been added to the DOM and its server LiveView has finished mounting.\n   */\n  mounted?: () => void;\n\n  /**\n   * The beforeUpdate callback.\n   *\n   * Called when the element is about to be updated in the DOM.\n   * Note: any call here must be synchronous as the operation cannot be deferred or cancelled.\n   */\n  beforeUpdate?: () => void;\n\n  /**\n   * The updated callback.\n   *\n   * Called when the element has been updated in the DOM by the server\n   */\n  updated?: () => void;\n\n  /**\n   * The destroyed callback.\n   *\n   * Called when the element has been removed from the page, either by a parent update, or by the parent being removed entirely\n   */\n  destroyed?: () => void;\n\n  /**\n   * The disconnected callback.\n   *\n   * Called when the element's parent LiveView has disconnected from the server.\n   */\n  disconnected?: () => void;\n\n  /**\n   * The reconnected callback.\n   *\n   * Called when the element's parent LiveView has reconnected to the server.\n   */\n  reconnected?: () => void;\n\n  /**\n   * Returns an object with methods to manipulate the DOM and execute JavaScript.\n   * The applied changes integrate with server DOM patching.\n   */\n  js(): HookJSCommands;\n\n  /**\n   * Pushes an event to the server.\n   *\n   * @param event - The event name.\n   * @param [payload] - The payload to send to the server. Defaults to an empty object.\n   * @param [onReply] - A callback to handle the server's reply.\n   *\n   * When onReply is not provided, the method returns a Promise that\n   * When onReply is provided, the method returns void.\n   */\n  pushEvent(event: string, payload: any, onReply: OnReply): void;\n  pushEvent(event: string, payload?: any): Promise<any>;\n\n  /**\n   * Pushed a targeted event to the server.\n   *\n   * It sends the event to the LiveComponent or LiveView the `selectorOrTarget` is defined in,\n   * where its value can be either a query selector, an actual DOM element, or a CID (component id)\n   * returned by the `@myself` assign.\n   *\n   * If the query selector returns more than one element it will send the event to all of them,\n   * even if all the elements are in the same LiveComponent or LiveView. Because of this,\n   * if no callback is passed, a promise is returned that matches the return value of\n   * [`Promise.allSettled()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled#return_value).\n   * Individual fulfilled values are of the format `{ reply, ref }`, where `reply` is the server's reply.\n   *\n   * @param selectorOrTarget - The selector, element, or CID to target.\n   * @param event - The event name.\n   * @param [payload] - The payload to send to the server. Defaults to an empty object.\n   * @param [onReply] - A callback to handle the server's reply.\n   *\n   * When onReply is not provided, the method returns a Promise.\n   * When onReply is provided, the method returns void.\n   */\n  pushEventTo(\n    selectorOrTarget: PhxTarget,\n    event: string,\n    payload: object,\n    onReply: OnReply,\n  ): void;\n  pushEventTo(\n    selectorOrTarget: PhxTarget,\n    event: string,\n    payload?: object,\n  ): Promise<PromiseSettledResult<{ reply: any; ref: number }>[]>;\n\n  /**\n   * Allows to register a callback to be called when an event is received from the server.\n   *\n   * This is used to handle `pushEvent` calls from the server. The callback is called with the payload from the server.\n   *\n   * @param event - The event name.\n   * @param callback - The callback to call when the event is received.\n   *\n   * @returns A reference to the callback, which can be used in `removeHandleEvent` to remove the callback.\n   */\n  handleEvent(event: string, callback: (payload: any) => any): CallbackRef;\n\n  /**\n   * Removes a callback registered with `handleEvent`.\n   *\n   * @param callbackRef - The reference to the callback to remove.\n   */\n  removeHandleEvent(ref: CallbackRef): void;\n\n  /**\n   * Allows to trigger a live file upload.\n   *\n   * @param name - The upload name corresponding to the `Phoenix.LiveView.allow_upload/3` call.\n   * @param files - The files to upload.\n   */\n  upload(name: any, files: any): any;\n\n  /**\n   * Allows to trigger a live file upload to a specific target.\n   *\n   * @param selectorOrTarget - The target to upload the files to.\n   * @param name - The upload name corresponding to the `Phoenix.LiveView.allow_upload/3` call.\n   * @param files - The files to upload.\n   */\n  uploadTo(selectorOrTarget: PhxTarget, name: any, files: any): any;\n\n  // allow unknown methods, as people can define them in their hooks\n  [key: PropertyKey]: any;\n}\n\n// based on https://github.com/DefinitelyTyped/DefinitelyTyped/blob/fac1aa75acdddbf4f1a95e98ee2297b54ce4b4c9/types/phoenix_live_view/hooks.d.ts#L26\n// licensed under MIT\nexport interface Hook<T = object, E extends HTMLElement = HTMLElement> {\n  /**\n   * The mounted callback.\n   *\n   * Called when the element has been added to the DOM and its server LiveView has finished mounting.\n   */\n  mounted?: (this: T & HookInterface<E>) => void;\n\n  /**\n   * The beforeUpdate callback.\n   *\n   * Called when the element is about to be updated in the DOM.\n   * Note: any call here must be synchronous as the operation cannot be deferred or cancelled.\n   */\n  beforeUpdate?: (this: T & HookInterface<E>) => void;\n\n  /**\n   * The updated callback.\n   *\n   * Called when the element has been updated in the DOM by the server\n   */\n  updated?: (this: T & HookInterface<E>) => void;\n\n  /**\n   * The destroyed callback.\n   *\n   * Called when the element has been removed from the page, either by a parent update, or by the parent being removed entirely\n   */\n  destroyed?: (this: T & HookInterface<E>) => void;\n\n  /**\n   * The disconnected callback.\n   *\n   * Called when the element's parent LiveView has disconnected from the server.\n   */\n  disconnected?: (this: T & HookInterface<E>) => void;\n\n  /**\n   * The reconnected callback.\n   *\n   * Called when the element's parent LiveView has reconnected to the server.\n   */\n  reconnected?: (this: T & HookInterface<E>) => void;\n\n  // Allow custom methods with any signature and custom properties\n  [key: PropertyKey]: any;\n}\n\n/**\n * Base class for LiveView hooks. Users extend this class to define their hooks.\n *\n * Example:\n * ```typescript\n * class MyCustomHook extends ViewHook {\n *   myState = \"initial\";\n *\n *   mounted() {\n *     console.log(\"Hook mounted on element:\", this.el);\n *     this.el.addEventListener(\"click\", () => {\n *       this.pushEvent(\"element-clicked\", { state: this.myState });\n *     });\n *   }\n *\n *   updated() {\n *     console.log(\"Hook updated\", this.el.id);\n *   }\n *\n *   myCustomMethod(someArg: string) {\n *     console.log(\"myCustomMethod called with:\", someArg, \"Current state:\", this.myState);\n *   }\n * }\n * ```\n *\n * The `this` context within the hook methods (mounted, updated, custom methods, etc.)\n * will refer to the hook instance, providing access to `this.el`, `this.liveSocket`,\n * `this.pushEvent()`, etc., as well as any properties or methods defined on the subclass.\n */\nexport class ViewHook<E extends HTMLElement = HTMLElement>\n  implements HookInterface<E>\n{\n  el: E;\n\n  private __listeners: Set<CallbackRef>;\n  private __isDisconnected: boolean;\n  private __view!: () => View;\n  private __liveSocket!: () => LiveSocket;\n\n  get liveSocket(): LiveSocket {\n    return this.__liveSocket();\n  }\n\n  static makeID() {\n    return viewHookID++;\n  }\n  static elementID(el: HTMLElement) {\n    return DOM.private(el, HOOK_ID);\n  }\n  static deadHook(el: HTMLElement) {\n    return DOM.private(el, DEAD_HOOK) === true;\n  }\n\n  constructor(view: View | null, el: E, callbacks?: Hook) {\n    this.el = el;\n    this.__attachView(view);\n    this.__listeners = new Set();\n    this.__isDisconnected = false;\n    DOM.putPrivate(this.el, HOOK_ID, ViewHook.makeID());\n    if (view && view.isDead) {\n      DOM.putPrivate(this.el, DEAD_HOOK, true);\n    }\n\n    if (callbacks) {\n      // This instance is for an object-literal hook. Copy methods/properties.\n      // These are properties that should NOT be overridden by the callbacks object.\n      const protectedProps = new Set([\n        \"el\",\n        \"liveSocket\",\n        \"__view\",\n        \"__listeners\",\n        \"__isDisconnected\",\n        \"constructor\", // Standard object properties\n        // Core ViewHook API methods\n        \"js\",\n        \"pushEvent\",\n        \"pushEventTo\",\n        \"handleEvent\",\n        \"removeHandleEvent\",\n        \"upload\",\n        \"uploadTo\",\n        // Internal lifecycle callers\n        \"__mounted\",\n        \"__updated\",\n        \"__beforeUpdate\",\n        \"__destroyed\",\n        \"__reconnected\",\n        \"__disconnected\",\n        \"__cleanup__\",\n      ]);\n\n      for (const key in callbacks) {\n        if (Object.prototype.hasOwnProperty.call(callbacks, key)) {\n          (this as any)[key] = callbacks[key];\n          // for backwards compatibility, we allow the overwrite, but we log a warning\n          if (protectedProps.has(key)) {\n            console.warn(\n              `Hook object for element #${el.id} overwrites core property '${key}'!`,\n            );\n          }\n        }\n      }\n\n      const lifecycleMethods: (keyof Hook)[] = [\n        \"mounted\",\n        \"beforeUpdate\",\n        \"updated\",\n        \"destroyed\",\n        \"disconnected\",\n        \"reconnected\",\n      ];\n      lifecycleMethods.forEach((methodName) => {\n        if (\n          callbacks[methodName] &&\n          typeof callbacks[methodName] === \"function\"\n        ) {\n          (this as any)[methodName] = callbacks[methodName];\n        }\n      });\n    }\n    // If 'callbacks' is not provided, this is an instance of a user-defined class (e.g., MyHook).\n    // Its methods (mounted, updated, custom) are already part of its prototype or instance,\n    // and will correctly override the defaults from ViewHook.prototype.\n  }\n\n  /** @internal */\n  __attachView(view: View | null) {\n    if (view) {\n      this.__view = () => view;\n      this.__liveSocket = () => view.liveSocket;\n    } else {\n      this.__view = () => {\n        throw new Error(\n          `hook not yet attached to a live view: ${this.el.outerHTML}`,\n        );\n      };\n      this.__liveSocket = () => {\n        throw new Error(\n          `hook not yet attached to a live view: ${this.el.outerHTML}`,\n        );\n      };\n    }\n  }\n\n  // Default lifecycle methods\n  mounted(): void {}\n  beforeUpdate(): void {}\n  updated(): void {}\n  destroyed(): void {}\n  disconnected(): void {}\n  reconnected(): void {}\n\n  // Internal lifecycle callers - called by the View\n\n  /** @internal */\n  __mounted() {\n    this.mounted();\n  }\n  /** @internal */\n  __updated() {\n    this.updated();\n  }\n  /** @internal */\n  __beforeUpdate() {\n    this.beforeUpdate();\n  }\n  /** @internal */\n  __destroyed() {\n    this.destroyed();\n    DOM.deletePrivate(this.el, HOOK_ID); // https://github.com/phoenixframework/phoenix_live_view/issues/3496\n  }\n  /** @internal */\n  __reconnected() {\n    if (this.__isDisconnected) {\n      this.__isDisconnected = false;\n      this.reconnected();\n    }\n  }\n  /** @internal */\n  __disconnected() {\n    this.__isDisconnected = true;\n    this.disconnected();\n  }\n\n  js(): HookJSCommands {\n    return {\n      ...jsCommands(this.__view().liveSocket, \"hook\"),\n      exec: (encodedJS: EncodedJS) => {\n        this.__view().liveSocket.execJS(this.el, encodedJS, \"hook\");\n      },\n    };\n  }\n\n  pushEvent(event: string, payload: any, onReply: OnReply): void;\n  pushEvent(event: string, payload?: any): Promise<any>;\n  pushEvent(\n    event: string,\n    payload?: any,\n    onReply?: OnReply,\n  ): Promise<any> | void {\n    const promise = this.__view().pushHookEvent(\n      this.el,\n      null,\n      event,\n      payload || {},\n    );\n    if (onReply === undefined) {\n      return promise.then(({ reply }: { reply: any }) => reply);\n    }\n    promise\n      .then(({ reply, ref }: { reply: any; ref: number }) =>\n        onReply(reply, ref),\n      )\n      .catch(() => {});\n  }\n\n  pushEventTo(\n    selectorOrTarget: PhxTarget,\n    event: string,\n    payload: object,\n    onReply: OnReply,\n  ): void;\n  pushEventTo(\n    selectorOrTarget: PhxTarget,\n    event: string,\n    payload?: object,\n  ): Promise<PromiseSettledResult<{ reply: any; ref: number }>[]>;\n  pushEventTo(\n    selectorOrTarget: PhxTarget,\n    event: string,\n    payload?: object,\n    onReply?: OnReply,\n  ): Promise<PromiseSettledResult<{ reply: any; ref: number }>[]> | void {\n    if (onReply === undefined) {\n      const targetPair: { view: View; targetCtx: any }[] = [];\n      this.__view().withinTargets(\n        selectorOrTarget,\n        (view: View, targetCtx: any) => {\n          targetPair.push({ view, targetCtx });\n        },\n      );\n      const promises = targetPair.map(({ view, targetCtx }) => {\n        return view.pushHookEvent(this.el, targetCtx, event, payload || {});\n      });\n      return Promise.allSettled(promises);\n    }\n    this.__view().withinTargets(\n      selectorOrTarget,\n      (view: View, targetCtx: any) => {\n        view\n          .pushHookEvent(this.el, targetCtx, event, payload || {})\n          .then(({ reply, ref }: { reply: any; ref: number }) =>\n            onReply(reply, ref),\n          )\n          .catch(() => {});\n      },\n    );\n  }\n\n  handleEvent(event: string, callback: (payload: any) => any): CallbackRef {\n    const callbackRef: CallbackRef = {\n      event,\n      callback: (customEvent: CustomEvent) => callback(customEvent.detail),\n    };\n    window.addEventListener(\n      `phx:${event}`,\n      callbackRef.callback as EventListener,\n    );\n    this.__listeners.add(callbackRef);\n    return callbackRef;\n  }\n\n  removeHandleEvent(ref: CallbackRef): void {\n    window.removeEventListener(\n      `phx:${ref.event}`,\n      ref.callback as EventListener,\n    );\n    this.__listeners.delete(ref);\n  }\n\n  upload(name: string, files: FileList): any {\n    return this.__view().dispatchUploads(null, name, files);\n  }\n\n  uploadTo(selectorOrTarget: PhxTarget, name: string, files: FileList): any {\n    return this.__view().withinTargets(\n      selectorOrTarget,\n      (view: View, targetCtx: any) => {\n        view.dispatchUploads(targetCtx, name, files);\n      },\n    );\n  }\n\n  /** @internal */\n  __cleanup__() {\n    this.__listeners.forEach((callbackRef) =>\n      this.removeHandleEvent(callbackRef),\n    );\n  }\n}\n\nexport type HooksOptions = Record<string, typeof ViewHook | Hook<any, any>>;\n\nexport default ViewHook;\n"
  },
  {
    "path": "assets/test/browser_test.ts",
    "content": "import Browser from \"phoenix_live_view/browser\";\n\ndescribe(\"Browser\", () => {\n  beforeEach(() => {\n    clearCookies();\n  });\n\n  describe(\"setCookie\", () => {\n    test(\"sets a cookie\", () => {\n      Browser.setCookie(\"apple\", 1234);\n      Browser.setCookie(\"orange\", \"5678\");\n      expect(document.cookie).toContain(\"apple\");\n      expect(document.cookie).toContain(\"1234\");\n      expect(document.cookie).toContain(\"orange\");\n      expect(document.cookie).toContain(\"5678\");\n    });\n  });\n\n  describe(\"getCookie\", () => {\n    test(\"returns the value for a cookie\", () => {\n      document.cookie = \"apple=1234\";\n      document.cookie = \"orange=5678\";\n      expect(Browser.getCookie(\"apple\")).toEqual(\"1234\");\n    });\n    test(\"returns an empty string for a non-existent cookie\", () => {\n      document.cookie = \"apple=1234\";\n      document.cookie = \"orange=5678\";\n      expect(Browser.getCookie(\"plum\")).toEqual(\"\");\n    });\n  });\n\n  describe(\"redirect\", () => {\n    test(\"redirects to a new URL\", () => {\n      const navigate = jest.fn();\n      const targetUrl = \"https://phoenixframework.com\";\n      Browser.redirect(targetUrl, null, navigate);\n      expect(navigate).toHaveBeenCalledWith(targetUrl);\n    });\n\n    test(\"sets a flash cookie before redirecting\", () => {\n      const navigate = jest.fn();\n      const targetUrl = \"https://phoenixframework.com\";\n      const flashMessage = \"mango\";\n      Browser.redirect(targetUrl, flashMessage, navigate);\n\n      expect(document.cookie).toContain(\"__phoenix_flash__\");\n      expect(document.cookie).toContain(flashMessage);\n      expect(navigate).toHaveBeenCalledWith(targetUrl);\n    });\n  });\n});\n\n// Adapted from https://stackoverflow.com/questions/179355/clearing-all-cookies-with-javascript/179514#179514\nfunction clearCookies() {\n  const cookies = document.cookie.split(\";\");\n\n  for (let i = 0; i < cookies.length; i++) {\n    const cookie = cookies[i];\n    const eqPos = cookie.indexOf(\"=\");\n    const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;\n    document.cookie = name + \"=;expires=Thu, 01 Jan 1970 00:00:00 GMT\";\n  }\n}\n"
  },
  {
    "path": "assets/test/debounce_test.ts",
    "content": "import DOM from \"phoenix_live_view/dom\";\n\nconst after = (time, func) => setTimeout(func, time);\n\nconst simulateInput = (input, val) => {\n  input.value = val;\n  DOM.dispatchEvent(input, \"input\");\n};\n\nconst simulateKeyDown = (input, val) => {\n  input.value = input.value + val;\n  DOM.dispatchEvent(input, \"input\");\n};\n\nconst container = () => {\n  const div = document.createElement(\"div\");\n  div.innerHTML = `\n  <form phx-change=\"validate\" phx-submit=\"submit\">\n    <input type=\"text\" name=\"blur\" phx-debounce=\"blur\" />\n    <input type=\"text\" name=\"debounce-200\" phx-debounce=\"200\" />\n    <input type=\"text\" name=\"throttle-200\" phx-throttle=\"200\" />\n    <button id=\"throttle-200\" phx-throttle=\"200\" />+</button>\n    <input\n      name=\"throttle-range-with-blur\"\n      type=\"range\"\n      min=\"100\"\n      max=\"1000\"\n      phx-throttle=\"200\"\n      phx-change=\"change-tick-frequency\"\n    />\n  </form>\n  <div id=\"throttle-keydown\" phx-keydown=\"keydown\" phx-throttle=\"200\"></div>\n  `;\n  return div;\n};\n\ndescribe(\"debounce\", function () {\n  test(\"triggers once on input blur\", async () => {\n    let calls = 0;\n    const el = container().querySelector(\"input[name=blur]\");\n\n    DOM.debounce(\n      el,\n      {},\n      \"phx-debounce\",\n      100,\n      \"phx-throttle\",\n      200,\n      () => true,\n      () => calls++,\n    );\n    DOM.dispatchEvent(el, \"blur\");\n    expect(calls).toBe(1);\n\n    DOM.dispatchEvent(el, \"blur\");\n    DOM.dispatchEvent(el, \"blur\");\n    DOM.dispatchEvent(el, \"blur\");\n    expect(calls).toBe(1);\n  });\n\n  test(\"triggers debounce on input blur\", async () => {\n    let calls = 0;\n    const el: HTMLInputElement = container().querySelector(\n      \"input[name=debounce-200]\",\n    )!;\n\n    el.addEventListener(\"input\", (e) => {\n      DOM.debounce(\n        el,\n        e,\n        \"phx-debounce\",\n        0,\n        \"phx-throttle\",\n        0,\n        () => true,\n        () => calls++,\n      );\n    });\n    simulateInput(el, \"one\");\n    simulateInput(el, \"two\");\n    simulateInput(el, \"three\");\n    DOM.dispatchEvent(el, \"blur\");\n    DOM.dispatchEvent(el, \"blur\");\n    DOM.dispatchEvent(el, \"blur\");\n    expect(calls).toBe(1);\n    expect(el.value).toBe(\"three\");\n  });\n\n  test(\"triggers debounce on input blur caused by tab\", async () => {\n    let calls = 0;\n    const el: HTMLInputElement = container().querySelector(\n      \"input[name=debounce-200]\",\n    )!;\n\n    el.addEventListener(\"input\", (e) => {\n      DOM.debounce(\n        el,\n        e,\n        \"phx-debounce\",\n        0,\n        \"phx-throttle\",\n        0,\n        () => true,\n        () => calls++,\n      );\n    });\n    simulateInput(el, \"one\");\n    simulateInput(el, \"two\");\n    el.dispatchEvent(\n      new KeyboardEvent(\"keydown\", {\n        bubbles: true,\n        cancelable: true,\n        key: \"Tab\",\n      }),\n    );\n    DOM.dispatchEvent(el, \"blur\");\n    expect(calls).toBe(1);\n    expect(el.value).toBe(\"two\");\n  });\n\n  test(\"triggers on timeout\", (done) => {\n    let calls = 0;\n    const el: HTMLInputElement = container().querySelector(\n      \"input[name=debounce-200]\",\n    )!;\n\n    el.addEventListener(\"input\", (e) => {\n      DOM.debounce(\n        el,\n        e,\n        \"phx-debounce\",\n        100,\n        \"phx-throttle\",\n        200,\n        () => true,\n        () => calls++,\n      );\n    });\n    simulateKeyDown(el, \"1\");\n    simulateKeyDown(el, \"2\");\n    simulateKeyDown(el, \"3\");\n    after(100, () => {\n      expect(calls).toBe(0);\n      simulateKeyDown(el, \"4\");\n      after(75, () => {\n        expect(calls).toBe(0);\n        after(250, () => {\n          expect(calls).toBe(1);\n          expect(el.value).toBe(\"1234\");\n          simulateKeyDown(el, \"5\");\n          simulateKeyDown(el, \"6\");\n          simulateKeyDown(el, \"7\");\n          after(250, () => {\n            expect(calls).toBe(2);\n            expect(el.value).toBe(\"1234567\");\n            done();\n          });\n        });\n      });\n    });\n  });\n\n  test(\"uses default when value is blank\", (done) => {\n    let calls = 0;\n    const el: HTMLInputElement = container().querySelector(\n      \"input[name=debounce-200]\",\n    )!;\n    el.setAttribute(\"phx-debounce\", \"\");\n\n    el.addEventListener(\"input\", (e) => {\n      DOM.debounce(\n        el,\n        e,\n        \"phx-debounce\",\n        500,\n        \"phx-throttle\",\n        200,\n        () => true,\n        () => calls++,\n      );\n    });\n    simulateInput(el, \"one\");\n    simulateInput(el, \"two\");\n    simulateInput(el, \"three\");\n    after(100, () => {\n      expect(calls).toBe(0);\n      expect(el.value).toBe(\"three\");\n      simulateInput(el, \"four\");\n      simulateInput(el, \"five\");\n      simulateInput(el, \"six\");\n      after(1200, () => {\n        expect(calls).toBe(1);\n        expect(el.value).toBe(\"six\");\n        done();\n      });\n    });\n  });\n\n  test(\"cancels trigger on submit\", (done) => {\n    let calls = 0;\n    const parent = container();\n    const el: HTMLInputElement = parent.querySelector(\n      \"input[name=debounce-200]\",\n    )!;\n\n    el.addEventListener(\"input\", (e) => {\n      DOM.debounce(\n        el,\n        e,\n        \"phx-debounce\",\n        100,\n        \"phx-throttle\",\n        200,\n        () => true,\n        () => calls++,\n      );\n    });\n    el.form!.addEventListener(\"submit\", () => {\n      el.value = \"submitted\";\n    });\n    simulateInput(el, \"changed\");\n    DOM.dispatchEvent(el.form, \"submit\");\n    after(100, () => {\n      expect(calls).toBe(0);\n      expect(el.value).toBe(\"submitted\");\n      simulateInput(el, \"changed again\");\n      after(250, () => {\n        expect(calls).toBe(1);\n        expect(el.value).toBe(\"changed again\");\n        done();\n      });\n    });\n  });\n});\n\ndescribe(\"throttle\", function () {\n  test(\"triggers immediately, then on timeout\", (done) => {\n    let calls = 0;\n    const el: HTMLButtonElement = container().querySelector(\"#throttle-200\")!;\n\n    el.addEventListener(\"click\", (e) => {\n      DOM.debounce(\n        el,\n        e,\n        \"phx-debounce\",\n        100,\n        \"phx-throttle\",\n        200,\n        () => true,\n        () => {\n          calls++;\n          el.innerText = `now:${calls}`;\n        },\n      );\n    });\n    DOM.dispatchEvent(el, \"click\");\n    DOM.dispatchEvent(el, \"click\");\n    DOM.dispatchEvent(el, \"click\");\n    expect(calls).toBe(1);\n    expect(el.innerText).toBe(\"now:1\");\n    after(250, () => {\n      expect(calls).toBe(1);\n      expect(el.innerText).toBe(\"now:1\");\n      DOM.dispatchEvent(el, \"click\");\n      DOM.dispatchEvent(el, \"click\");\n      DOM.dispatchEvent(el, \"click\");\n      after(250, () => {\n        expect(calls).toBe(2);\n        expect(el.innerText).toBe(\"now:2\");\n        done();\n      });\n    });\n  });\n\n  test(\"uses default when value is blank\", (done) => {\n    let calls = 0;\n    const el: HTMLButtonElement = container().querySelector(\"#throttle-200\")!;\n    el.setAttribute(\"phx-throttle\", \"\");\n\n    el.addEventListener(\"click\", (e) => {\n      DOM.debounce(\n        el,\n        e,\n        \"phx-debounce\",\n        100,\n        \"phx-throttle\",\n        500,\n        () => true,\n        () => {\n          calls++;\n          el.innerText = `now:${calls}`;\n        },\n      );\n    });\n    DOM.dispatchEvent(el, \"click\");\n    DOM.dispatchEvent(el, \"click\");\n    DOM.dispatchEvent(el, \"click\");\n    expect(calls).toBe(1);\n    expect(el.innerText).toBe(\"now:1\");\n    after(200, () => {\n      expect(calls).toBe(1);\n      expect(el.innerText).toBe(\"now:1\");\n      DOM.dispatchEvent(el, \"click\");\n      DOM.dispatchEvent(el, \"click\");\n      DOM.dispatchEvent(el, \"click\");\n      after(250, () => {\n        expect(calls).toBe(1);\n        expect(el.innerText).toBe(\"now:1\");\n        done();\n      });\n    });\n  });\n\n  test(\"cancels trigger on submit\", (done) => {\n    let calls = 0;\n    const el: HTMLInputElement = container().querySelector(\n      \"input[name=throttle-200]\",\n    )!;\n\n    el.addEventListener(\"input\", (e) => {\n      DOM.debounce(\n        el,\n        e,\n        \"phx-debounce\",\n        100,\n        \"phx-throttle\",\n        200,\n        () => true,\n        () => calls++,\n      );\n    });\n    el.form!.addEventListener(\"submit\", () => {\n      el.value = \"submitted\";\n    });\n    simulateInput(el, \"changed\");\n    simulateInput(el, \"changed2\");\n    DOM.dispatchEvent(el.form, \"submit\");\n    expect(calls).toBe(1);\n    expect(el.value).toBe(\"submitted\");\n    simulateInput(el, \"changed3\");\n    after(100, () => {\n      expect(calls).toBe(2);\n      expect(el.value).toBe(\"changed3\");\n      done();\n    });\n  });\n\n  test(\"triggers only once when there is only one event\", (done) => {\n    let calls = 0;\n    const el: HTMLButtonElement = container().querySelector(\"#throttle-200\")!;\n\n    el.addEventListener(\"click\", (e) => {\n      DOM.debounce(\n        el,\n        e,\n        \"phx-debounce\",\n        100,\n        \"phx-throttle\",\n        200,\n        () => true,\n        () => {\n          calls++;\n          el.innerText = `now:${calls}`;\n        },\n      );\n    });\n    DOM.dispatchEvent(el, \"click\");\n    expect(calls).toBe(1);\n    expect(el.innerText).toBe(\"now:1\");\n    after(250, () => {\n      expect(calls).toBe(1);\n      done();\n    });\n  });\n\n  test(\"sends value on blur when phx-blur dispatches change\", (done) => {\n    let calls = 0;\n    const el: HTMLInputElement = container().querySelector(\n      \"input[name=throttle-range-with-blur]\",\n    )!;\n\n    el.addEventListener(\"input\", (e) => {\n      DOM.debounce(\n        el,\n        e,\n        \"phx-debounce\",\n        100,\n        \"phx-throttle\",\n        200,\n        () => true,\n        () => {\n          calls++;\n          el.innerText = `now:${calls}`;\n        },\n      );\n    });\n    el.value = \"500\";\n    DOM.dispatchEvent(el, \"input\");\n    // these will be throttled\n    for (let i = 0; i < 100; i++) {\n      el.value = i.toString();\n      DOM.dispatchEvent(el, \"input\");\n    }\n    expect(calls).toBe(1);\n    expect(el.innerText).toBe(\"now:1\");\n    // when using phx-blur={JS.dispatch(\"change\")} we would trigger another\n    // input event immediately after the blur\n    // therefore starting a new throttle cycle\n    DOM.dispatchEvent(el, \"blur\");\n    // simulate phx-blur\n    DOM.dispatchEvent(el, \"input\");\n    expect(calls).toBe(2);\n    expect(el.innerText).toBe(\"now:2\");\n    after(250, () => {\n      expect(calls).toBe(2);\n      expect(el.innerText).toBe(\"now:2\");\n      done();\n    });\n  });\n});\n\ndescribe(\"throttle keydown\", function () {\n  test(\"when the same key is pressed triggers immediately, then on timeout\", (done) => {\n    const keyPresses = {};\n    const el: HTMLDivElement = container().querySelector(\"#throttle-keydown\")!;\n\n    el.addEventListener(\"keydown\", (e) => {\n      DOM.debounce(\n        el,\n        e,\n        \"phx-debounce\",\n        100,\n        \"phx-throttle\",\n        200,\n        () => true,\n        () => {\n          keyPresses[e.key] = (keyPresses[e.key] || 0) + 1;\n        },\n      );\n    });\n\n    const pressA = new KeyboardEvent(\"keydown\", { key: \"a\" });\n    el.dispatchEvent(pressA);\n    el.dispatchEvent(pressA);\n    el.dispatchEvent(pressA);\n\n    expect(keyPresses[\"a\"]).toBe(1);\n    after(250, () => {\n      expect(keyPresses[\"a\"]).toBe(1);\n      el.dispatchEvent(pressA);\n      el.dispatchEvent(pressA);\n      el.dispatchEvent(pressA);\n      expect(keyPresses[\"a\"]).toBe(2);\n      done();\n    });\n  });\n\n  test(\"when different key is pressed triggers immediately\", (done) => {\n    const keyPresses = {};\n    const el: HTMLDivElement = container().querySelector(\"#throttle-keydown\")!;\n\n    el.addEventListener(\"keydown\", (e) => {\n      DOM.debounce(\n        el,\n        e,\n        \"phx-debounce\",\n        100,\n        \"phx-throttle\",\n        200,\n        () => true,\n        () => {\n          keyPresses[e.key] = (keyPresses[e.key] || 0) + 1;\n        },\n      );\n    });\n\n    const pressA = new KeyboardEvent(\"keydown\", { key: \"a\" });\n    const pressB = new KeyboardEvent(\"keydown\", { key: \"b\" });\n\n    el.dispatchEvent(pressA);\n    el.dispatchEvent(pressB);\n    el.dispatchEvent(pressA);\n    el.dispatchEvent(pressB);\n\n    expect(keyPresses[\"a\"]).toBe(2);\n    expect(keyPresses[\"b\"]).toBe(2);\n    done();\n  });\n});\n"
  },
  {
    "path": "assets/test/dom_test.ts",
    "content": "import DOM from \"phoenix_live_view/dom\";\nimport { appendTitle, tag } from \"./test_helpers\";\n\nconst e = (href: string) => {\n  const anchor = document.createElement(\"a\");\n  anchor.setAttribute(\"href\", href);\n  const event = {\n    target: anchor,\n    defaultPrevented: false,\n  } as unknown as Event & { target: HTMLAnchorElement };\n  return event;\n};\n\ndescribe(\"DOM\", () => {\n  beforeEach(() => {\n    const curTitle = document.querySelector(\"title\");\n    curTitle && curTitle.remove();\n  });\n\n  describe(\"wantsNewTab\", () => {\n    test(\"case insensitive target\", () => {\n      const event = e(\"https://test.local\");\n      expect(DOM.wantsNewTab(event)).toBe(false);\n      // lowercase\n      event.target.setAttribute(\"target\", \"_blank\");\n      expect(DOM.wantsNewTab(event)).toBe(true);\n      // uppercase\n      event.target.setAttribute(\"target\", \"_BLANK\");\n      expect(DOM.wantsNewTab(event)).toBe(true);\n    });\n  });\n\n  describe(\"isNewPageClick\", () => {\n    test(\"identical locations\", () => {\n      let currentLoc;\n      currentLoc = new URL(\"https://test.local/foo\");\n      expect(DOM.isNewPageClick(e(\"/foo\"), currentLoc)).toBe(true);\n      expect(DOM.isNewPageClick(e(\"https://test.local/foo\"), currentLoc)).toBe(\n        true,\n      );\n      expect(DOM.isNewPageClick(e(\"//test.local/foo\"), currentLoc)).toBe(true);\n      // with hash\n      expect(DOM.isNewPageClick(e(\"/foo#hash\"), currentLoc)).toBe(false);\n      expect(\n        DOM.isNewPageClick(e(\"https://test.local/foo#hash\"), currentLoc),\n      ).toBe(false);\n      expect(DOM.isNewPageClick(e(\"//test.local/foo#hash\"), currentLoc)).toBe(\n        false,\n      );\n      // different paths\n      expect(DOM.isNewPageClick(e(\"/foo2#hash\"), currentLoc)).toBe(true);\n      expect(\n        DOM.isNewPageClick(e(\"https://test.local/foo2#hash\"), currentLoc),\n      ).toBe(true);\n      expect(DOM.isNewPageClick(e(\"//test.local/foo2#hash\"), currentLoc)).toBe(\n        true,\n      );\n    });\n\n    test(\"identical locations with query\", () => {\n      let currentLoc;\n      currentLoc = new URL(\"https://test.local/foo?query=1\");\n      expect(DOM.isNewPageClick(e(\"/foo\"), currentLoc)).toBe(true);\n      expect(\n        DOM.isNewPageClick(e(\"https://test.local/foo?query=1\"), currentLoc),\n      ).toBe(true);\n      expect(\n        DOM.isNewPageClick(e(\"//test.local/foo?query=1\"), currentLoc),\n      ).toBe(true);\n      // with hash\n      expect(DOM.isNewPageClick(e(\"/foo?query=1#hash\"), currentLoc)).toBe(\n        false,\n      );\n      expect(\n        DOM.isNewPageClick(\n          e(\"https://test.local/foo?query=1#hash\"),\n          currentLoc,\n        ),\n      ).toBe(false);\n      expect(\n        DOM.isNewPageClick(e(\"//test.local/foo?query=1#hash\"), currentLoc),\n      ).toBe(false);\n      // different query\n      expect(DOM.isNewPageClick(e(\"/foo?query=2#hash\"), currentLoc)).toBe(true);\n      expect(\n        DOM.isNewPageClick(\n          e(\"https://test.local/foo?query=2#hash\"),\n          currentLoc,\n        ),\n      ).toBe(true);\n      expect(\n        DOM.isNewPageClick(e(\"//test.local/foo?query=2#hash\"), currentLoc),\n      ).toBe(true);\n    });\n\n    test(\"empty hash href\", () => {\n      const currentLoc = new URL(\"https://test.local/foo\");\n      expect(DOM.isNewPageClick(e(\"#\"), currentLoc)).toBe(false);\n    });\n\n    test(\"local hash\", () => {\n      const currentLoc = new URL(\"https://test.local/foo\");\n      expect(DOM.isNewPageClick(e(\"#foo\"), currentLoc)).toBe(false);\n    });\n\n    test(\"with defaultPrevented return sfalse\", () => {\n      let currentLoc;\n      currentLoc = new URL(\"https://test.local/foo\");\n      const event = e(\"/foo\");\n      (event as any).defaultPrevented = true;\n      expect(DOM.isNewPageClick(event, currentLoc)).toBe(false);\n    });\n\n    test(\"ignores mailto and tel links\", () => {\n      expect(\n        DOM.isNewPageClick(e(\"mailto:foo\"), new URL(\"https://test.local/foo\")),\n      ).toBe(false);\n      expect(\n        DOM.isNewPageClick(e(\"tel:1234\"), new URL(\"https://test.local/foo\")),\n      ).toBe(false);\n    });\n\n    test(\"ignores contenteditable\", () => {\n      let currentLoc;\n      currentLoc = new URL(\"https://test.local/foo\");\n      const event = e(\"/bar\");\n      (event.target as any).isContentEditable = true;\n      expect(DOM.isNewPageClick(event, currentLoc)).toBe(false);\n    });\n  });\n\n  describe(\"putTitle\", () => {\n    test(\"with no attributes\", () => {\n      appendTitle({});\n      DOM.putTitle(\"My Title\");\n      expect(document.title).toBe(\"My Title\");\n    });\n\n    test(\"with prefix\", () => {\n      appendTitle({ prefix: \"PRE \" });\n      DOM.putTitle(\"My Title\");\n      expect(document.title).toBe(\"PRE My Title\");\n    });\n\n    test(\"with suffix\", () => {\n      appendTitle({ suffix: \" POST\" });\n      DOM.putTitle(\"My Title\");\n      expect(document.title).toBe(\"My Title POST\");\n    });\n\n    test(\"with prefix and suffix\", () => {\n      appendTitle({ prefix: \"PRE \", suffix: \" POST\" });\n      DOM.putTitle(\"My Title\");\n      expect(document.title).toBe(\"PRE My Title POST\");\n    });\n\n    test(\"with default\", () => {\n      appendTitle({ default: \"DEFAULT\", prefix: \"PRE \", suffix: \" POST\" });\n      DOM.putTitle(null);\n      expect(document.title).toBe(\"PRE DEFAULT POST\");\n\n      DOM.putTitle(undefined);\n      expect(document.title).toBe(\"PRE DEFAULT POST\");\n\n      DOM.putTitle(\"\");\n      expect(document.title).toBe(\"PRE DEFAULT POST\");\n    });\n  });\n\n  describe(\"findExistingParentCIDs\", () => {\n    test(\"returns only parent cids\", () => {\n      const view = tag(\n        \"div\",\n        {},\n        `\n        <div id=\"foo\" data-phx-main=\"true\"\n            data-phx-session=\"123\"\n            data-phx-static=\"456\"\n            class=\"phx-connected\"\n            data-phx-root-id=\"phx-FgFpFf-J8Gg-jEnh\">\n        </div>\n      `,\n      );\n      document.body.appendChild(view);\n\n      view.appendChild(\n        tag(\n          \"div\",\n          { \"data-phx-component\": 1, \"data-phx-view\": \"foo\" },\n          `\n        <div data-phx-component=\"2\" data-phx-view=\"foo\"></div>\n      `,\n        ),\n      );\n      expect(DOM.findExistingParentCIDs(\"foo\", [1, 2])).toEqual(new Set([1]));\n\n      view.appendChild(\n        tag(\n          \"div\",\n          { \"data-phx-component\": 1, \"data-phx-view\": \"foo\" },\n          `\n        <div data-phx-component=\"2\" data-phx-view=\"foo\">\n          <div data-phx-component=\"3\" data-phx-view=\"foo\"></div>\n        </div>\n      `,\n        ),\n      );\n      expect(DOM.findExistingParentCIDs(\"foo\", [1, 2, 3])).toEqual(\n        new Set([1]),\n      );\n    });\n\n    test(\"ignores elements in child LiveViews #3626\", () => {\n      const view = tag(\n        \"div\",\n        {},\n        `\n        <div data-phx-main=\"true\"\n            data-phx-session=\"123\"\n            data-phx-static=\"456\"\n            id=\"phx-123\"\n            class=\"phx-connected\"\n            data-phx-root-id=\"phx-FgFpFf-J8Gg-jEnh\">\n        </div>\n      `,\n      );\n      document.body.appendChild(view);\n\n      view.appendChild(\n        tag(\n          \"div\",\n          { \"data-phx-component\": 1, \"data-phx-view\": \"phx-123\" },\n          `\n        <div data-phx-session=\"123\" data-phx-static=\"456\" data-phx-parent=\"phx-123\" id=\"phx-child-view\">\n          <div data-phx-component=\"1\" data-phx-view=\"phx-child-view\"></div>\n        </div>\n      `,\n        ),\n      );\n      expect(DOM.findExistingParentCIDs(\"phx-123\", [1])).toEqual(new Set([1]));\n    });\n  });\n\n  describe(\"findComponentNodeList\", () => {\n    test(\"returns nodes with cid ID (except indirect children)\", () => {\n      const view = tag(\"div\", { id: \"foo\" }, \"\");\n      let component1 = tag(\n        \"div\",\n        { \"data-phx-component\": 0, \"data-phx-view\": \"foo\" },\n        \"Hello\",\n      );\n      let component2 = tag(\n        \"div\",\n        { \"data-phx-component\": 0, \"data-phx-view\": \"foo\" },\n        \"World\",\n      );\n      let component3 = tag(\n        \"div\",\n        { \"data-phx-session\": \"123\" },\n        `\n        <div data-phx-component=\"0\" data-phx-view=\"123\"></div>\n      `,\n      );\n      document.body.appendChild(view);\n      view.appendChild(component1);\n      view.appendChild(component2);\n      view.appendChild(component3);\n\n      expect(DOM.findComponentNodeList(\"foo\", 0, document)).toEqual([\n        component1,\n        component2,\n      ]);\n    });\n\n    test(\"returns empty list with no matching cid\", () => {\n      const view = tag(\"div\", { id: \"foo\" }, \"\");\n      let component1 = tag(\n        \"div\",\n        { \"data-phx-component\": 0, \"data-phx-view\": \"foo\" },\n        \"Hello\",\n      );\n      document.body.appendChild(view);\n      view.appendChild(component1);\n      expect(DOM.findComponentNodeList(\"bar\", 123)).toEqual([]);\n    });\n  });\n\n  test(\"isNowTriggerFormExternal\", () => {\n    let form;\n    form = tag(\"form\", { \"phx-trigger-external\": \"\" }, \"\");\n    document.body.appendChild(form);\n    expect(DOM.isNowTriggerFormExternal(form, \"phx-trigger-external\")).toBe(\n      true,\n    );\n\n    form = tag(\"form\", {}, \"\");\n    document.body.appendChild(form);\n    expect(DOM.isNowTriggerFormExternal(form, \"phx-trigger-external\")).toBe(\n      false,\n    );\n\n    // not in the DOM -> false\n    form = tag(\"form\", { \"phx-trigger-external\": \"\" }, \"\");\n    expect(DOM.isNowTriggerFormExternal(form, \"phx-trigger-external\")).toBe(\n      false,\n    );\n  });\n\n  describe(\"cleanChildNodes\", () => {\n    test(\"only cleans when phx-update is append or prepend\", () => {\n      const content = `\n      <div id=\"1\">1</div>\n      <div>no id</div>\n\n      some test\n      `.trim();\n\n      const div = tag(\"div\", {}, content);\n      DOM.cleanChildNodes(div, \"phx-update\");\n\n      expect(div.innerHTML).toBe(content);\n    });\n\n    test(\"silently removes empty text nodes\", () => {\n      const content = `\n      <div id=\"1\">1</div>\n\n\n      <div id=\"2\">2</div>\n      `.trim();\n\n      const div = tag(\"div\", { \"phx-update\": \"append\" }, content);\n      DOM.cleanChildNodes(div, \"phx-update\");\n\n      expect(div.innerHTML).toBe('<div id=\"1\">1</div><div id=\"2\">2</div>');\n    });\n\n    test(\"emits warning when removing elements without id\", () => {\n      const content = `\n      <div id=\"1\">1</div>\n      <div>no id</div>\n\n      some test\n      `.trim();\n\n      const div = tag(\"div\", { \"phx-update\": \"append\" }, content);\n\n      let errorCount = 0;\n      jest.spyOn(console, \"error\").mockImplementation(() => (errorCount += 1));\n      DOM.cleanChildNodes(div, \"phx-update\");\n\n      expect(div.innerHTML).toBe('<div id=\"1\">1</div>');\n      expect(errorCount).toBe(2);\n    });\n  });\n\n  describe(\"isFormInput\", () => {\n    test(\"identifies all inputs except for buttons as form inputs\", () => {\n      [\n        \"checkbox\",\n        \"color\",\n        \"date\",\n        \"datetime-local\",\n        \"email\",\n        \"file\",\n        \"hidden\",\n        \"image\",\n        \"month\",\n        \"number\",\n        \"password\",\n        \"radio\",\n        \"range\",\n        \"reset\",\n        \"search\",\n        \"submit\",\n        \"tel\",\n        \"text\",\n        \"time\",\n        \"url\",\n        \"week\",\n      ].forEach((inputType) => {\n        const input = tag(\"input\", { type: inputType }, \"\");\n        expect(DOM.isFormInput(input)).toBeTruthy();\n      });\n\n      const input = tag(\"input\", { type: \"button\" }, \"\");\n      expect(DOM.isFormInput(input)).toBeFalsy();\n    });\n\n    test(\"identifies selects as form inputs\", () => {\n      const select = tag(\"select\", {}, \"\");\n      expect(DOM.isFormInput(select)).toBeTruthy();\n    });\n\n    test(\"identifies textareas as form inputs\", () => {\n      const textarea = tag(\"textarea\", {}, \"\");\n      expect(DOM.isFormInput(textarea)).toBeTruthy();\n    });\n\n    test(\"identifies form associated custom elements as form inputs\", () => {\n      class CustomFormInput extends HTMLElement {\n        static formAssociated = true;\n\n        constructor() {\n          super();\n        }\n      }\n      customElements.define(\"custom-form-input\", CustomFormInput);\n      const customFormInput = tag(\"custom-form-input\", {}, \"\");\n      expect(DOM.isFormInput(customFormInput)).toBeTruthy();\n\n      class CustomNotFormInput extends HTMLElement {\n        constructor() {\n          super();\n        }\n      }\n\n      customElements.define(\"custom-not-form-input\", CustomNotFormInput);\n      const customNotFormInput = tag(\"custom-not-form-input\", {}, \"\");\n      expect(DOM.isFormInput(customNotFormInput)).toBeFalsy();\n    });\n  });\n});\n"
  },
  {
    "path": "assets/test/event_test.ts",
    "content": "import { Socket } from \"phoenix\";\nimport LiveSocket from \"phoenix_live_view/live_socket\";\nimport View from \"phoenix_live_view/view\";\n\nimport { version as liveview_version } from \"../../package.json\";\nimport { HooksOptions } from \"phoenix_live_viewview_hook\";\n\nlet containerId = 0;\n\nlet simulateView = (liveSocket, events, innerHTML) => {\n  let el = document.createElement(\"div\");\n  el.setAttribute(\"data-phx-session\", \"abc123\");\n  el.setAttribute(\"id\", `container${containerId++}`);\n  el.innerHTML = innerHTML;\n  document.body.appendChild(el);\n\n  let view = new View(el, liveSocket);\n  view.onJoin({ rendered: { e: events, s: [innerHTML] }, liveview_version });\n  view.isConnected = () => true;\n  return view;\n};\n\nlet stubNextChannelReply = (view, replyPayload) => {\n  let oldPush = view.channel.push;\n  view.channel.push = () => {\n    return {\n      receives: [],\n      receive(kind, cb) {\n        if (kind === \"ok\") {\n          cb({ diff: { r: replyPayload } });\n          view.channel.push = oldPush;\n        }\n        return this;\n      },\n    };\n  };\n};\n\nlet stubNextChannelReplyWithError = (view, reason) => {\n  let oldPush = view.channel.push;\n  view.channel.push = () => {\n    return {\n      receives: [],\n      receive(kind, cb) {\n        if (kind === \"error\") {\n          cb(reason);\n          view.channel.push = oldPush;\n        }\n        return this;\n      },\n    };\n  };\n};\n\ndescribe(\"events\", () => {\n  let processedEvents;\n  beforeEach(() => {\n    document.body.innerHTML = \"\";\n    processedEvents = [];\n  });\n\n  test(\"events on join\", () => {\n    let liveSocket = new LiveSocket(\"/live\", Socket, {\n      hooks: <HooksOptions>{\n        Map: {\n          mounted() {\n            this.handleEvent(\"points\", (data) =>\n              processedEvents.push({ event: \"points\", data: data }),\n            );\n          },\n        },\n      },\n    });\n    let _view = simulateView(\n      liveSocket,\n      [[\"points\", { values: [1, 2, 3] }]],\n      `\n      <div id=\"map\" phx-hook=\"Map\">\n      </div>\n    `,\n    );\n\n    expect(processedEvents).toEqual([\n      { event: \"points\", data: { values: [1, 2, 3] } },\n    ]);\n  });\n\n  test(\"events on update\", () => {\n    let liveSocket = new LiveSocket(\"/live\", Socket, {\n      hooks: <HooksOptions>{\n        Game: {\n          mounted() {\n            this.handleEvent(\"scores\", (data) =>\n              processedEvents.push({ event: \"scores\", data: data }),\n            );\n          },\n        },\n      },\n    });\n    let view = simulateView(\n      liveSocket,\n      [],\n      `\n      <div id=\"game\" phx-hook=\"Game\">\n      </div>\n    `,\n    );\n\n    expect(processedEvents).toEqual([]);\n\n    view.update({}, [[\"scores\", { values: [1, 2, 3] }]]);\n    expect(processedEvents).toEqual([\n      { event: \"scores\", data: { values: [1, 2, 3] } },\n    ]);\n  });\n\n  test(\"events handlers are cleaned up on destroy\", () => {\n    let destroyed: Array<string> = [];\n    let liveSocket = new LiveSocket(\"/live\", Socket, {\n      hooks: <HooksOptions>{\n        Handler: {\n          mounted() {\n            this.handleEvent(\"my-event\", (data) =>\n              processedEvents.push({\n                id: this.el.id,\n                event: \"my-event\",\n                data: data,\n              }),\n            );\n          },\n          destroyed() {\n            destroyed.push(this.el.id);\n          },\n        },\n      },\n    });\n    let view = simulateView(\n      liveSocket,\n      [],\n      `\n      <div id=\"handler1\" phx-hook=\"Handler\"></div>\n      <div id=\"handler2\" phx-hook=\"Handler\"></div>\n    `,\n    );\n\n    expect(processedEvents).toEqual([]);\n\n    view.update({}, [[\"my-event\", { val: 1 }]]);\n    expect(processedEvents).toEqual([\n      { id: \"handler1\", event: \"my-event\", data: { val: 1 } },\n      { id: \"handler2\", event: \"my-event\", data: { val: 1 } },\n    ]);\n\n    let newHTML = '<div id=\"handler1\" phx-hook=\"Handler\"></div>';\n    view.update({ s: [newHTML] }, [[\"my-event\", { val: 2 }]]);\n\n    expect(destroyed).toEqual([\"handler2\"]);\n\n    expect(processedEvents).toEqual([\n      { id: \"handler1\", event: \"my-event\", data: { val: 1 } },\n      { id: \"handler2\", event: \"my-event\", data: { val: 1 } },\n      { id: \"handler1\", event: \"my-event\", data: { val: 2 } },\n    ]);\n  });\n\n  test(\"removeHandleEvent\", () => {\n    let liveSocket = new LiveSocket(\"/live\", Socket, {\n      hooks: <HooksOptions>{\n        Remove: {\n          mounted() {\n            let ref = this.handleEvent(\"remove\", (data) => {\n              this.removeHandleEvent(ref);\n              processedEvents.push({ event: \"remove\", data: data });\n            });\n          },\n        },\n      },\n    });\n    let view = simulateView(\n      liveSocket,\n      [],\n      `\n      <div id=\"remove\" phx-hook=\"Remove\"></div>\n    `,\n    );\n\n    expect(processedEvents).toEqual([]);\n\n    view.update({}, [[\"remove\", { val: 1 }]]);\n    expect(processedEvents).toEqual([{ event: \"remove\", data: { val: 1 } }]);\n\n    view.update({}, [[\"remove\", { val: 1 }]]);\n    expect(processedEvents).toEqual([{ event: \"remove\", data: { val: 1 } }]);\n  });\n});\n\ndescribe(\"pushEvent replies\", () => {\n  let processedReplies;\n  beforeEach(() => {\n    processedReplies = [];\n  });\n\n  test(\"reply\", (done) => {\n    let view;\n    let liveSocket = new LiveSocket(\"/live\", Socket, {\n      hooks: <HooksOptions>{\n        Gateway: {\n          mounted() {\n            stubNextChannelReply(view, { transactionID: \"1001\" });\n            this.pushEvent(\"charge\", { amount: 123 }, (resp, ref) => {\n              processedReplies.push({ resp, ref });\n              view.el.dispatchEvent(\n                new CustomEvent(\"replied\", { detail: { resp, ref } }),\n              );\n            });\n          },\n        },\n      },\n    });\n    view = simulateView(liveSocket, [], \"\");\n    view.update(\n      {\n        s: [\n          `\n      <div id=\"gateway\" phx-hook=\"Gateway\">\n      </div>\n    `,\n        ],\n      },\n      [],\n    );\n\n    view.el.addEventListener(\"replied\", () => {\n      expect(processedReplies).toEqual([\n        { resp: { transactionID: \"1001\" }, ref: 0 },\n      ]);\n      done();\n    });\n  });\n\n  test(\"promise\", (done) => {\n    let view;\n    let liveSocket = new LiveSocket(\"/live\", Socket, {\n      hooks: <HooksOptions>{\n        Gateway: {\n          mounted() {\n            stubNextChannelReply(view, { transactionID: \"1001\" });\n            this.pushEvent(\"charge\", { amount: 123 }).then((reply) => {\n              processedReplies.push(reply);\n              view.el.dispatchEvent(\n                new CustomEvent(\"replied\", { detail: reply }),\n              );\n            });\n          },\n        },\n      },\n    });\n    view = simulateView(liveSocket, [], \"\");\n    view.update(\n      {\n        s: [\n          `\n      <div id=\"gateway\" phx-hook=\"Gateway\">\n      </div>\n    `,\n        ],\n      },\n      [],\n    );\n\n    view.el.addEventListener(\"replied\", () => {\n      expect(processedReplies).toEqual([{ transactionID: \"1001\" }]);\n      done();\n    });\n  });\n\n  test(\"rejects with error\", (done) => {\n    let view;\n    let liveSocket = new LiveSocket(\"/live\", Socket, {\n      hooks: <HooksOptions>{\n        Gateway: {\n          mounted() {\n            stubNextChannelReplyWithError(view, \"error\");\n            this.pushEvent(\"charge\", { amount: 123 }).catch((error) => {\n              expect(error).toEqual(expect.any(Error));\n              done();\n            });\n          },\n        },\n      },\n    });\n    view = simulateView(liveSocket, [], \"\");\n    view.update(\n      {\n        s: [\n          `\n      <div id=\"gateway\" phx-hook=\"Gateway\">\n      </div>\n    `,\n        ],\n      },\n      [],\n    );\n  });\n\n  test(\"pushEventTo - promise with multiple targets\", (done) => {\n    let view;\n    let liveSocket = new LiveSocket(\"/live\", Socket, {\n      hooks: <HooksOptions>{\n        Gateway: {\n          mounted() {\n            stubNextChannelReply(view, { transactionID: \"1001\" });\n            this.pushEventTo(\"[data-foo]\", \"charge\", { amount: 123 }).then(\n              (result) => {\n                expect(result).toEqual([\n                  {\n                    status: \"fulfilled\",\n                    value: { ref: 0, reply: { transactionID: \"1001\" } },\n                  },\n                  // we only stubbed one reply\n                  { status: \"rejected\", reason: expect.any(Error) },\n                ]);\n                done();\n              },\n            );\n          },\n        },\n      },\n    });\n    view = simulateView(liveSocket, [], \"\");\n    liveSocket.main = view;\n    liveSocket.roots[view.id] = view;\n    view.update(\n      {\n        s: [\n          `\n      <div id=\"${view.id}\" data-phx-session=\"abc123\" data-phx-root-id=\"${view.id}\">\n        <div id=\"gateway\" phx-hook=\"Gateway\">\n          <div data-foo=\"bar\"></div>\n          <div data-foo=\"baz\"></div>\n        </div>\n      </div>\n    `,\n        ],\n      },\n      [],\n    );\n  });\n\n  test(\"pushEvent without connection noops\", (done) => {\n    let view;\n    const spy = jest.fn();\n    let liveSocket = new LiveSocket(\"/live\", Socket, {\n      hooks: <HooksOptions>{\n        Gateway: {\n          mounted() {\n            stubNextChannelReply(view, { transactionID: \"1001\" });\n            this.pushEvent(\"charge\", { amount: 1233433 })\n              .then(spy)\n              .catch(() => {\n                view.el.dispatchEvent(new CustomEvent(\"pushed\"));\n              });\n          },\n        },\n      },\n    });\n    view = simulateView(liveSocket, [], \"\");\n    view.isConnected = () => false;\n    view.update(\n      {\n        s: [\n          `\n      <div id=\"gateway\" phx-hook=\"Gateway\">\n      </div>\n    `,\n        ],\n      },\n      [],\n    );\n\n    view.el.addEventListener(\"pushed\", () => {\n      expect(spy).not.toHaveBeenCalled();\n      done();\n    });\n  });\n});\n"
  },
  {
    "path": "assets/test/globals.d.ts",
    "content": "declare global {\n  let LV_VSN: string;\n}\n\nexport {};\n"
  },
  {
    "path": "assets/test/hook_types_test.ts",
    "content": "/**\n * Type tests for Hook and HooksOptions.\n *\n * These tests verify:\n * 1. Typed hooks with custom state and element types can be assigned to HooksOptions\n *    (requires Hook<any, any> in HooksOptions definition)\n * 2. Hook<SpecificType> is assignable to Hook<object> due to covariant `out T` annotation\n *    (see https://github.com/phoenixframework/phoenix_live_view/issues/3955)\n *\n * This file is checked by `npm run typecheck:tests`.\n */\n\nimport type { Hook, HooksOptions } from \"phoenix_live_view/view_hook\";\n\n// Hook with custom state\ninterface CounterState {\n  count: number;\n  increment(): void;\n}\n\nconst CounterHook: Hook<CounterState> = {\n  count: 0,\n  increment() {\n    this.count++;\n  },\n  mounted() {\n    this.el.addEventListener(\"click\", () => this.increment());\n  },\n};\n\n// Hook targeting a specific element type\ninterface CanvasState {\n  ctx: CanvasRenderingContext2D | null;\n}\n\nconst CanvasHook: Hook<CanvasState, HTMLCanvasElement> = {\n  ctx: null,\n  mounted() {\n    // this.el should be typed as HTMLCanvasElement\n    this.ctx = this.el.getContext(\"2d\");\n  },\n};\n\n// Hook with both custom state and specific element type\ninterface VideoState {\n  isPlaying: boolean;\n  toggle(): void;\n}\n\nconst VideoHook: Hook<VideoState, HTMLVideoElement> = {\n  isPlaying: false,\n  toggle() {\n    if (this.isPlaying) {\n      this.el.pause();\n    } else {\n      this.el.play();\n    }\n    this.isPlaying = !this.isPlaying;\n  },\n  mounted() {\n    this.el.addEventListener(\"click\", () => this.toggle());\n  },\n};\n\n// All typed hooks should be assignable to HooksOptions\nconst hooks: HooksOptions = {\n  Counter: CounterHook,\n  Canvas: CanvasHook,\n  Video: VideoHook,\n};\n\n// =============================================================================\n// Test for issue #3955: Hook<T> with required properties in HooksOptions\n// https://github.com/phoenixframework/phoenix_live_view/issues/3955\n//\n// The fix using Hook<any, any> in HooksOptions allows typed hooks to be\n// assigned regardless of their specific type parameters.\n// =============================================================================\n\ninterface LinksInTab {\n  tabName: string; // required property\n  links: string[];\n}\n\nconst LinksInTabHook: Hook<LinksInTab> = {\n  tabName: \"\",\n  links: [],\n  mounted() {\n    this.tabName = this.el.dataset.tab || \"default\";\n    console.log(`Tab ${this.tabName} mounted with ${this.links.length} links`);\n  },\n};\n\n// This tests that Hook<LinksInTab> (with required properties) can be\n// assigned to HooksOptions. This was the original issue #3955.\nconst hooksWithRequiredProps: HooksOptions = {\n  LinksInTab: LinksInTabHook,\n};\n\nexport { hooks, hooksWithRequiredProps };\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3913\n// Checks that custom methods and properties are allowed for a basic Hook.\nconst InfiniteScroll: Hook = {\n  page() {\n    return this.el.dataset.page;\n  },\n  mounted() {\n    this.pending = this.page();\n    window.addEventListener(\"scroll\", () => {\n      if (this.pending == this.page() && 80 > 90) {\n        this.pending = this.page() + 1;\n        this.pushEvent(\"load-more\", {});\n      }\n    });\n  },\n  updated() {\n    this.pending = this.page();\n  },\n};\n\nexport { InfiniteScroll };\n\n// This file is primarily for compile-time type checking via `npm run typecheck:tests`.\n// The dummy test below satisfies Jest's requirement for at least one test.\ntest(\"hook types compile correctly\", () => {\n  expect(hooks).toBeDefined();\n  expect(hooksWithRequiredProps).toBeDefined();\n});\n"
  },
  {
    "path": "assets/test/index_test.ts",
    "content": "import { LiveSocket, isUsedInput, ViewHook } from \"phoenix_live_view\";\nimport * as LiveSocket2 from \"phoenix_live_view/live_socket\";\nimport ViewHook2 from \"phoenix_live_view/view_hook\";\nimport DOM from \"phoenix_live_view/dom\";\n\ndescribe(\"Named Imports\", () => {\n  test(\"LiveSocket is equal to the actual LiveSocket\", () => {\n    expect(LiveSocket).toBe(LiveSocket2.default);\n  });\n\n  test(\"ViewHook is equal to the actual ViewHook\", () => {\n    expect(ViewHook).toBe(ViewHook2);\n  });\n});\n\ndescribe(\"isUsedInput\", () => {\n  test(\"returns true if the input is used\", () => {\n    const input = document.createElement(\"input\");\n    input.type = \"text\";\n    expect(isUsedInput(input)).toBeFalsy();\n    DOM.putPrivate(input, \"phx-has-focused\", true);\n    expect(isUsedInput(input)).toBe(true);\n  });\n});\n"
  },
  {
    "path": "assets/test/integration/event_test.ts",
    "content": "import { Socket } from \"phoenix\";\nimport LiveSocket from \"phoenix_live_view/live_socket\";\n\nconst stubViewPushInput = (view, callback) => {\n  view.pushInput = (\n    sourceEl,\n    targetCtx,\n    newCid,\n    event,\n    pushOpts,\n    originalCallback,\n  ) => {\n    return callback(\n      sourceEl,\n      targetCtx,\n      newCid,\n      event,\n      pushOpts,\n      originalCallback,\n    );\n  };\n};\n\nconst prepareLiveViewDOM = (document, rootId) => {\n  document.body.innerHTML = `\n    <div data-phx-session=\"abc123\"\n         data-phx-root-id=\"${rootId}\"\n         id=\"${rootId}\">\n      <div class=\"form-wrapper\" data-phx-component=\"2\" data-phx-view=\"root\">\n        <form id=\"form\" phx-change=\"validate\" phx-target=\"2\">\n          <label for=\"first_name\">First Name</label>\n          <input id=\"first_name\" value=\"\" name=\"user[first_name]\" />\n\n          <label for=\"last_name\">Last Name</label>\n          <input id=\"last_name\" value=\"\" name=\"user[last_name]\" />\n        </form>\n      </div>\n    </div>\n  `;\n};\n\ndescribe(\"events\", () => {\n  beforeEach(() => {\n    prepareLiveViewDOM(global.document, \"root\");\n  });\n\n  test(\"send change event to correct target\", () => {\n    const liveSocket = new LiveSocket(\"/live\", Socket);\n    liveSocket.connect();\n    const view = liveSocket.getViewByEl(document.getElementById(\"root\"));\n    view.isConnected = () => true;\n    const input = view.el.querySelector(\"#first_name\");\n    let meta = {\n      event: null,\n      target: null,\n      changed: null,\n    };\n\n    stubViewPushInput(\n      view,\n      (sourceEl, targetCtx, newCid, event, pushOpts, _callback) => {\n        meta = {\n          event,\n          target: targetCtx,\n          changed: pushOpts[\"_target\"],\n        };\n      },\n    );\n\n    input.value = \"John Doe\";\n    input.dispatchEvent(new Event(\"change\", { bubbles: true }));\n\n    expect(meta).toEqual({\n      event: \"validate\",\n      target: 2,\n      changed: \"user[first_name]\",\n    });\n  });\n});\n"
  },
  {
    "path": "assets/test/integration/metadata_test.ts",
    "content": "import { Socket } from \"phoenix\";\nimport LiveSocket from \"phoenix_live_view/live_socket\";\n\nconst stubViewPushEvent = (view, callback) => {\n  view.pushEvent = (type, el, targetCtx, phxEvent, meta, opts = {}) => {\n    return callback(type, el, targetCtx, phxEvent, meta, opts);\n  };\n};\n\nconst prepareLiveViewDOM = (document, rootId) => {\n  document.body.innerHTML = `\n    <div data-phx-session=\"abc123\"\n         data-phx-root-id=\"${rootId}\"\n         id=\"${rootId}\">\n      <label for=\"plus\">Plus</label>\n      <input id=\"plus\" value=\"1\" />\n      <button id=\"btn\" phx-click=\"inc_temperature\">Inc Temperature</button>\n    </div>\n  `;\n};\n\ndescribe(\"metadata\", () => {\n  beforeEach(() => {\n    prepareLiveViewDOM(global.document, \"root\");\n  });\n\n  test(\"is empty by default\", () => {\n    const liveSocket = new LiveSocket(\"/live\", Socket);\n    liveSocket.connect();\n    const view = liveSocket.getViewByEl(document.getElementById(\"root\"));\n    const btn = view.el.querySelector(\"button\");\n    let meta = {};\n    stubViewPushEvent(\n      view,\n      (type, el, target, targetCtx, phxEvent, metadata) => {\n        meta = metadata;\n      },\n    );\n    btn.dispatchEvent(new Event(\"click\", { bubbles: true }));\n\n    expect(meta).toEqual({});\n  });\n\n  test(\"can be user defined\", () => {\n    const liveSocket = new LiveSocket(\"/live\", Socket, {\n      metadata: {\n        click: (e, el) => {\n          return {\n            id: el.id,\n            altKey: e.altKey,\n          };\n        },\n      },\n    });\n    liveSocket.connect();\n    liveSocket.isConnected = () => true;\n    const view = liveSocket.getViewByEl(document.getElementById(\"root\"));\n    view.isConnected = () => true;\n    const btn = view.el.querySelector(\"button\");\n    let meta = {};\n    stubViewPushEvent(view, (type, el, target, phxEvent, metadata, _opts) => {\n      meta = metadata;\n    });\n    btn.dispatchEvent(new Event(\"click\", { bubbles: true }));\n\n    expect(meta).toEqual({\n      id: \"btn\",\n      altKey: undefined,\n    });\n  });\n});\n"
  },
  {
    "path": "assets/test/integration/portal_test.ts",
    "content": "import { Socket } from \"phoenix\";\nimport LiveSocket from \"phoenix_live_view/live_socket\";\nimport DOMPatch from \"phoenix_live_view/dom_patch\";\nimport { PHX_PORTAL, PHX_SKIP } from \"phoenix_live_view/constants\";\nimport { simulateJoinedView } from \"../test_helpers\";\n\n// Helper functions\nfunction createViewWithPortal(rootId = \"root\") {\n  document.body.innerHTML = `\n    <div data-phx-session=\"abc123\"\n         data-phx-root-id=\"${rootId}\"\n         data-phx-static=\"456\"\n         id=\"${rootId}\">\n      <div id=\"content\"></div>\n      <div id=\"portal-target\"></div>\n      <div id=\"portal-target-2\"></div>\n    </div>\n  `;\n\n  const rootEl = document.getElementById(rootId);\n  const liveSocket = new LiveSocket(\"/live\", Socket);\n  const view = simulateJoinedView(rootEl, liveSocket);\n\n  return { liveSocket, view };\n}\n\nfunction createHtmlWithPortal(id, targetId, content) {\n  const portalHtml = `\n    <template id=\"${id}\" ${PHX_PORTAL}=\"#${targetId}\">\n      <div id=\"portal-content-${id}\">\n        ${content}\n      </div>\n    </template>\n  `;\n  return portalHtml;\n}\n\nfunction performPatch(view, container, htmlString) {\n  const tempDiv = document.createElement(\"div\");\n  tempDiv.innerHTML = htmlString;\n\n  const domPatch = new DOMPatch(view, container, view.id, tempDiv, [], null);\n  domPatch.perform(false);\n}\n\ndescribe(\"Portal handling\", () => {\n  beforeEach(() => {\n    document.body.innerHTML = \"\";\n  });\n\n  test(\"basic portal teleporting\", () => {\n    const { view } = createViewWithPortal();\n    const content = document.getElementById(\"content\");\n    const portalTarget = document.getElementById(\"portal-target\")!;\n\n    // Create HTML with portal template\n    const html = `<div>${createHtmlWithPortal(\"portal1\", \"portal-target\", \"Hello from portal\")}</div>`;\n\n    // Perform the patch\n    performPatch(view, content, html);\n\n    // Verify portal content was teleported to target\n    expect(portalTarget.innerHTML).toContain(\"Hello from portal\");\n    expect(\n      portalTarget.querySelector(\"#portal-content-portal1\"),\n    ).not.toBeNull();\n\n    // Verify portal element ID was tracked\n    expect(view.portalElementIds.has(\"portal-content-portal1\")).toBe(true);\n  });\n\n  test(\"updating portal content\", () => {\n    const { view } = createViewWithPortal();\n    const content = document.getElementById(\"content\");\n    const portalTarget = document.getElementById(\"portal-target\")!;\n\n    // First patch to create portal\n    const html1 = `<div>${createHtmlWithPortal(\"portal1\", \"portal-target\", \"Initial content\")}</div>`;\n    performPatch(view, content, html1);\n\n    // Second patch to update portal content\n    const html2 = `<div>${createHtmlWithPortal(\"portal1\", \"portal-target\", \"Updated content\")}</div>`;\n    performPatch(view, content, html2);\n\n    // Verify content was updated\n    expect(portalTarget.innerHTML).toContain(\"Updated content\");\n    expect(portalTarget.innerHTML).not.toContain(\"Initial content\");\n  });\n\n  test(\"removing portal template removes teleported content\", () => {\n    const { view } = createViewWithPortal();\n    const content = document.getElementById(\"content\");\n    const portalTarget = document.getElementById(\"portal-target\")!;\n\n    // First patch to create portal\n    const html1 = `<div>${createHtmlWithPortal(\"portal1\", \"portal-target\", \"Portal content\")}</div>`;\n    performPatch(view, content, html1);\n\n    // Verify portal was created\n    expect(\n      portalTarget.querySelector(\"#portal-content-portal1\"),\n    ).not.toBeNull();\n\n    // Second patch to remove portal template\n    const html2 = \"<div></div>\";\n    performPatch(view, content, html2);\n\n    // Verify teleported content was removed\n    expect(portalTarget.querySelector(\"#portal-content-portal1\")).toBeNull();\n    expect(view.portalElementIds.size).toBe(0);\n  });\n\n  test(\"removing parent of portal template removes teleported content\", () => {\n    const { view } = createViewWithPortal();\n    const content = document.getElementById(\"content\");\n    const portalTarget = document.getElementById(\"portal-target\")!;\n\n    // First patch - create a parent container with portal inside\n    const html1 = `\n      <div>\n        <div id=\"parent-container\">\n          ${createHtmlWithPortal(\"portal1\", \"portal-target\", \"Nested portal content\")}\n        </div>\n      </div>\n    `;\n    performPatch(view, content, html1);\n\n    // Verify portal content was teleported\n    expect(\n      portalTarget.querySelector(\"#portal-content-portal1\"),\n    ).not.toBeNull();\n\n    // Second patch - remove the parent container\n    const html2 = \"<div></div>\";\n    performPatch(view, content, html2);\n\n    // Verify teleported content was removed\n    expect(portalTarget.querySelector(\"#portal-content-portal1\")).toBeNull();\n    expect(view.portalElementIds.size).toBe(0);\n  });\n\n  test(\"teleporting to non-existent target throws error\", () => {\n    const { view } = createViewWithPortal();\n    const content = document.getElementById(\"content\");\n\n    // Create template with invalid target\n    const html = `<div>${createHtmlWithPortal(\"portal1\", \"non-existent-target\", \"Content\")}</div>`;\n\n    // Expect error when teleporting\n    expect(() => {\n      performPatch(view, content, html);\n    }).toThrow(\"portal target with selector #non-existent-target not found\");\n  });\n\n  test(\"portal template without id throws error\", () => {\n    const { view } = createViewWithPortal();\n    const content = document.getElementById(\"content\");\n\n    // Create template with content that has no ID\n    const html = `\n      <div>\n        <template id=\"invalid-portal\" ${PHX_PORTAL}=\"#portal-target\">\n          <div>Content without ID</div>\n        </template>\n      </div>\n    `;\n\n    // Expect error when teleporting\n    expect(() => {\n      performPatch(view, content, html);\n    }).toThrow(\"phx-portal template must have a single root element with ID\");\n  });\n\n  test(\"cleans up teleported elements when view is destroyed\", () => {\n    const { view, liveSocket } = createViewWithPortal();\n    const content = document.getElementById(\"content\");\n    const portalTarget = document.getElementById(\"portal-target\")!;\n\n    // Create portal\n    const html = `<div>${createHtmlWithPortal(\"portal1\", \"portal-target\", \"Content\")}</div>`;\n    performPatch(view, content, html);\n\n    // Verify content was teleported\n    expect(\n      portalTarget.querySelector(\"#portal-content-portal1\"),\n    ).not.toBeNull();\n\n    // Destroy the view\n    liveSocket.destroyViewByEl(view.el);\n\n    // Verify teleported content was removed\n    expect(portalTarget.querySelector(\"#portal-content-portal1\")).toBeNull();\n  });\n\n  test(\"handles multiple portals to the same target\", () => {\n    const { view } = createViewWithPortal();\n    const content = document.getElementById(\"content\");\n    const portalTarget = document.getElementById(\"portal-target\")!;\n\n    // Create HTML with two portal templates\n    const html = `\n      <div>\n        ${createHtmlWithPortal(\"portal1\", \"portal-target\", \"First portal\")}\n        ${createHtmlWithPortal(\"portal2\", \"portal-target\", \"Second portal\")}\n      </div>\n    `;\n\n    // Perform the patch\n    performPatch(view, content, html);\n\n    // Verify both portals were teleported\n    expect(\n      portalTarget.querySelector(\"#portal-content-portal1\"),\n    ).not.toBeNull();\n    expect(\n      portalTarget.querySelector(\"#portal-content-portal2\"),\n    ).not.toBeNull();\n    expect(portalTarget.innerHTML).toContain(\"First portal\");\n    expect(portalTarget.innerHTML).toContain(\"Second portal\");\n  });\n\n  test(\"teleported elements are removed if source is removed\", () => {\n    const { view } = createViewWithPortal();\n    const content = document.getElementById(\"content\");\n    const portalTarget = document.getElementById(\"portal-target\")!;\n\n    // First patch to create portals\n    const html1 = `\n      <div>\n        ${createHtmlWithPortal(\"portal1\", \"portal-target\", \"First portal\")}\n        ${createHtmlWithPortal(\"portal2\", \"portal-target\", \"Second portal\")}\n      </div>\n    `;\n    performPatch(view, content, html1);\n\n    // Verify both portals were teleported\n    expect(portalTarget.querySelectorAll(\"div\").length).toBe(2);\n\n    // Second patch to remove one portal template\n    const html2 = `<div>${createHtmlWithPortal(\"portal1\", \"portal-target\", \"First portal\")}</div>`;\n    performPatch(view, content, html2);\n\n    // Verify only one portal remains\n    expect(portalTarget.querySelectorAll(\"div\").length).toBe(1);\n    expect(\n      portalTarget.querySelector(\"#portal-content-portal1\"),\n    ).not.toBeNull();\n    expect(portalTarget.querySelector(\"#portal-content-portal2\")).toBeNull();\n    expect(view.portalElementIds.size).toBe(1);\n    expect(view.portalElementIds.has(\"portal-content-portal1\")).toBe(true);\n    expect(view.portalElementIds.has(\"portal-content-portal2\")).toBe(false);\n  });\n\n  test(\"teleported elements with PHX_SKIP are ignored\", () => {\n    const { view } = createViewWithPortal();\n    const content = document.getElementById(\"content\");\n    const portalTarget = document.getElementById(\"portal-target\")!;\n\n    // Create template with skipped content\n    const html = `\n      <div>\n        <template id=\"skipped-portal\" ${PHX_PORTAL}=\"#portal-target\">\n          <div id=\"portal-content-skipped\">Hello World!</div>\n        </template>\n      </div>\n    `;\n\n    // Perform the patch\n    performPatch(view, content, html);\n\n    // Verify the portal was not teleported because of PHX_SKIP\n    expect(\n      portalTarget.querySelector(\"#portal-content-skipped\")!.innerHTML,\n    ).toBe(\"Hello World!\");\n\n    const html2 = `\n      <div>\n        <template id=\"skipped-portal\" ${PHX_PORTAL}=\"#portal-target\">\n          <div id=\"portal-content-skipped\" ${PHX_SKIP}></div>\n        </template>\n      </div>\n    `;\n\n    performPatch(view, content, html2);\n    // PHX_SKIP nodes are skipped\n    expect(\n      portalTarget.querySelector(\"#portal-content-skipped\")!.innerHTML,\n    ).toBe(\"Hello World!\");\n  });\n\n  test(\"changing target of a portal moves content to new target\", () => {\n    const { view } = createViewWithPortal();\n    const content = document.getElementById(\"content\");\n    const portalTarget1 = document.getElementById(\"portal-target\")!;\n    const portalTarget2 = document.getElementById(\"portal-target-2\")!;\n\n    // First patch to create portal with target1\n    const html1 = `<div>${createHtmlWithPortal(\"portal1\", \"portal-target\", \"Portal content\")}</div>`;\n    performPatch(view, content, html1);\n\n    // Verify content was teleported to first target\n    expect(\n      portalTarget1.querySelector(\"#portal-content-portal1\"),\n    ).not.toBeNull();\n    expect(portalTarget2.querySelector(\"#portal-content-portal1\")).toBeNull();\n\n    // Second patch to change target\n    const html2 = `<div>${createHtmlWithPortal(\"portal1\", \"portal-target-2\", \"Portal content\")}</div>`;\n    performPatch(view, content, html2);\n\n    // Verify content was moved to second target\n    expect(portalTarget1.querySelector(\"#portal-content-portal1\")).toBeNull();\n    expect(\n      portalTarget2.querySelector(\"#portal-content-portal1\"),\n    ).not.toBeNull();\n  });\n});\n"
  },
  {
    "path": "assets/test/js_test.ts",
    "content": "import { Socket } from \"phoenix\";\nimport LiveSocket from \"phoenix_live_view/live_socket\";\nimport JS from \"phoenix_live_view/js\";\nimport ViewHook from \"phoenix_live_view/view_hook\";\nimport {\n  simulateJoinedView,\n  simulateVisibility,\n  liveViewDOM,\n} from \"./test_helpers\";\nimport { HookJSCommands } from \"phoenix_live_view/js_commands\";\n\nconst setupView = (content) => {\n  const el = liveViewDOM(content);\n  global.document.body.appendChild(el);\n  const liveSocket = new LiveSocket(\"/live\", Socket);\n  return simulateJoinedView(el, liveSocket);\n};\n\nconst event = new CustomEvent(\"phx:exec\");\n\ndescribe(\"JS\", () => {\n  beforeEach(() => {\n    global.document.body.innerHTML = \"\";\n    jest.useFakeTimers();\n  });\n\n  afterEach(() => {\n    jest.useRealTimers();\n  });\n\n  describe(\"hook.js()\", () => {\n    let js: HookJSCommands;\n    let view, modal;\n    beforeEach(() => {\n      view = setupView('<div id=\"modal\">modal</div>');\n      modal = view.el.querySelector(\"#modal\");\n      const hook = new ViewHook(view, view.el, {});\n      js = hook.js();\n    });\n\n    test(\"exec\", () => {\n      simulateVisibility(modal);\n      expect(modal.style.display).toBe(\"\");\n      js.exec('[[\"toggle\", {\"to\": \"#modal\"}]]');\n      jest.runAllTimers();\n      expect(modal.style.display).toBe(\"none\");\n    });\n\n    test(\"exec with command array\", () => {\n      simulateVisibility(modal);\n      expect(modal.style.display).toBe(\"\");\n      js.exec([[\"toggle\", { to: \"#modal\" }]]);\n      jest.runAllTimers();\n      expect(modal.style.display).toBe(\"none\");\n    });\n\n    test(\"show and hide\", (done) => {\n      simulateVisibility(modal);\n      expect(modal.style.display).toBe(\"\");\n      js.hide(modal);\n      jest.advanceTimersByTime(100);\n      expect(modal.style.display).toBe(\"none\");\n      js.show(modal);\n      jest.advanceTimersByTime(100);\n      expect(modal.style.display).toBe(\"block\");\n      done();\n    });\n\n    test(\"toggle\", (done) => {\n      simulateVisibility(modal);\n      expect(modal.style.display).toBe(\"\");\n      js.toggle(modal);\n      jest.advanceTimersByTime(100);\n      expect(modal.style.display).toBe(\"none\");\n      js.toggle(modal);\n      jest.advanceTimersByTime(100);\n      expect(modal.style.display).toBe(\"block\");\n      done();\n    });\n\n    test(\"addClass and removeClass\", (done) => {\n      expect(Array.from(modal.classList)).toEqual([]);\n      js.addClass(modal, \"class1 class2\");\n      jest.advanceTimersByTime(100);\n      expect(Array.from(modal.classList)).toEqual([\"class1\", \"class2\"]);\n      jest.advanceTimersByTime(100);\n      js.removeClass(modal, \"class1\");\n      jest.advanceTimersByTime(100);\n      expect(Array.from(modal.classList)).toEqual([\"class2\"]);\n      js.addClass(modal, [\"class3\", \"class4\"]);\n      jest.advanceTimersByTime(100);\n      expect(Array.from(modal.classList)).toEqual([\n        \"class2\",\n        \"class3\",\n        \"class4\",\n      ]);\n      js.removeClass(modal, [\"class3\", \"class4\"]);\n      jest.advanceTimersByTime(100);\n      expect(Array.from(modal.classList)).toEqual([\"class2\"]);\n      done();\n    });\n\n    test(\"toggleClass\", (done) => {\n      expect(Array.from(modal.classList)).toEqual([]);\n      js.toggleClass(modal, \"class1 class2\");\n      jest.advanceTimersByTime(100);\n      expect(Array.from(modal.classList)).toEqual([\"class1\", \"class2\"]);\n      js.toggleClass(modal, [\"class1\"]);\n      jest.advanceTimersByTime(100);\n      expect(Array.from(modal.classList)).toEqual([\"class2\"]);\n      done();\n    });\n\n    test(\"transition\", (done) => {\n      js.transition(modal, \"shake\", { time: 150 });\n      jest.advanceTimersByTime(100);\n      expect(Array.from(modal.classList)).toEqual([\"shake\"]);\n      jest.advanceTimersByTime(100);\n      expect(Array.from(modal.classList)).toEqual([]);\n      js.transition(modal, [\"shake\", \"opacity-50\", \"opacity-100\"], {\n        time: 150,\n      });\n      jest.advanceTimersByTime(10);\n      expect(Array.from(modal.classList)).toEqual([\"opacity-50\"]);\n      jest.advanceTimersByTime(200);\n      expect(Array.from(modal.classList)).toEqual([\"opacity-100\"]);\n      done();\n    });\n\n    test(\"setAttribute and removeAttribute\", (done) => {\n      js.removeAttribute(modal, \"works\");\n      js.setAttribute(modal, \"works\", \"123\");\n      expect(modal.getAttribute(\"works\")).toBe(\"123\");\n      js.removeAttribute(modal, \"works\");\n      expect(modal.getAttribute(\"works\")).toBe(null);\n      done();\n    });\n\n    test(\"toggleAttr\", (done) => {\n      js.toggleAttribute(modal, \"works\", \"on\", \"off\");\n      expect(modal.getAttribute(\"works\")).toBe(\"on\");\n      js.toggleAttribute(modal, \"works\", \"on\", \"off\");\n      expect(modal.getAttribute(\"works\")).toBe(\"off\");\n      js.toggleAttribute(modal, \"works\", \"on\", \"off\");\n      expect(modal.getAttribute(\"works\")).toBe(\"on\");\n      done();\n    });\n\n    test(\"push\", (done) => {\n      const originalWithinOwners = view.liveSocket.withinOwners;\n      view.liveSocket.withinOwners = (el, callback) => {\n        callback(view);\n      };\n\n      const originalExec = JS.exec;\n      JS.exec = jest.fn();\n\n      js.push(modal, \"custom-event\", { value: { key: \"value\" } });\n\n      expect(JS.exec).toHaveBeenCalled();\n\n      view.liveSocket.withinOwners = originalWithinOwners;\n      JS.exec = originalExec;\n      done();\n    });\n\n    test(\"navigate\", (done) => {\n      const originalHistoryRedirect = view.liveSocket.historyRedirect;\n      view.liveSocket.historyRedirect = jest.fn();\n\n      js.navigate(\"/test-url\");\n      expect(view.liveSocket.historyRedirect).toHaveBeenCalledWith(\n        expect.any(CustomEvent),\n        \"/test-url\",\n        \"push\",\n        null,\n        null,\n      );\n\n      js.navigate(\"/test-url\", { replace: true });\n      expect(view.liveSocket.historyRedirect).toHaveBeenCalledWith(\n        expect.any(CustomEvent),\n        \"/test-url\",\n        \"replace\",\n        null,\n        null,\n      );\n\n      view.liveSocket.historyRedirect = originalHistoryRedirect;\n      done();\n    });\n\n    test(\"patch\", (done) => {\n      const originalPushHistoryPatch = view.liveSocket.pushHistoryPatch;\n      view.liveSocket.pushHistoryPatch = jest.fn();\n\n      js.patch(\"/test-url\");\n      expect(view.liveSocket.pushHistoryPatch).toHaveBeenCalledWith(\n        expect.any(CustomEvent),\n        \"/test-url\",\n        \"push\",\n        null,\n      );\n\n      js.patch(\"/test-url\", { replace: true });\n      expect(view.liveSocket.pushHistoryPatch).toHaveBeenCalledWith(\n        expect.any(CustomEvent),\n        \"/test-url\",\n        \"replace\",\n        null,\n      );\n\n      view.liveSocket.pushHistoryPatch = originalPushHistoryPatch;\n      done();\n    });\n  });\n\n  describe(\"exec_toggle\", () => {\n    test(\"with defaults\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\">modal</div>\n      <div id=\"click\" phx-click='[[\"toggle\", {\"to\": \"#modal\"}]]'></div>\n      `);\n      const modal = simulateVisibility(document.querySelector(\"#modal\"));\n      const click = document.querySelector(\"#click\")!;\n      let showEndCalled = false;\n      let hideEndCalled = false;\n      let showStartCalled = false;\n      let hideStartCalled = false;\n      modal.addEventListener(\"phx:show-end\", () => (showEndCalled = true));\n      modal.addEventListener(\"phx:hide-end\", () => (hideEndCalled = true));\n      modal.addEventListener(\"phx:show-start\", () => (showStartCalled = true));\n      modal.addEventListener(\"phx:hide-start\", () => (hideStartCalled = true));\n\n      expect(modal.style.display).toEqual(\"\");\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n      jest.runAllTimers();\n\n      expect(modal.style.display).toEqual(\"none\");\n\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n      jest.runAllTimers();\n\n      expect(modal.style.display).toEqual(\"block\");\n      expect(showEndCalled).toBe(true);\n      expect(hideEndCalled).toBe(true);\n      expect(showStartCalled).toBe(true);\n      expect(hideStartCalled).toBe(true);\n\n      done();\n    });\n\n    test(\"with display\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\">modal</div>\n      <div id=\"click\" phx-click='[[\"toggle\", {\"to\": \"#modal\", \"display\": \"inline-block\"}]]'></div>\n      `);\n      const modal = simulateVisibility(document.querySelector(\"#modal\"));\n      const click = document.querySelector(\"#click\")!;\n      let showEndCalled = false;\n      let hideEndCalled = false;\n      let showStartCalled = false;\n      let hideStartCalled = false;\n      modal.addEventListener(\"phx:show-end\", () => (showEndCalled = true));\n      modal.addEventListener(\"phx:hide-end\", () => (hideEndCalled = true));\n      modal.addEventListener(\"phx:show-start\", () => (showStartCalled = true));\n      modal.addEventListener(\"phx:hide-start\", () => (hideStartCalled = true));\n\n      expect(modal.style.display).toEqual(\"\");\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n      jest.runAllTimers();\n\n      expect(modal.style.display).toEqual(\"none\");\n\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n      jest.runAllTimers();\n\n      expect(modal.style.display).toEqual(\"inline-block\");\n      expect(showEndCalled).toBe(true);\n      expect(hideEndCalled).toBe(true);\n      expect(showStartCalled).toBe(true);\n      expect(hideStartCalled).toBe(true);\n      done();\n    });\n\n    test(\"with in and out classes\", async () => {\n      const view = setupView(`\n      <div id=\"modal\">modal</div>\n      <div id=\"click\" phx-click='[[\"toggle\", {\"to\": \"#modal\", \"ins\": [[\"fade-in\"],[\"fade-in-start\"],[\"fade-in-end\"]], \"outs\": [[\"fade-out\"],[\"fade-out-start\"],[\"fade-out-end\"]]}]]'></div>\n      `);\n      const modal = simulateVisibility(document.querySelector(\"#modal\"));\n      const click = document.querySelector(\"#click\")!;\n      let showEndCalled = false;\n      let hideEndCalled = false;\n      let showStartCalled = false;\n      let hideStartCalled = false;\n      modal.addEventListener(\"phx:show-end\", () => (showEndCalled = true));\n      modal.addEventListener(\"phx:hide-end\", () => (hideEndCalled = true));\n      modal.addEventListener(\"phx:show-start\", () => (showStartCalled = true));\n      modal.addEventListener(\"phx:hide-start\", () => (hideStartCalled = true));\n\n      expect(modal.style.display).toEqual(\"\");\n      expect(modal.classList.contains(\"fade-out\")).toBe(false);\n      expect(modal.classList.contains(\"fade-in\")).toBe(false);\n\n      // toggle out\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n      expect(hideStartCalled).toBe(true);\n      // first tick: waiting for start classes to be set\n      jest.advanceTimersToNextFrame();\n      expect(modal.classList.contains(\"fade-out-start\")).toBe(true);\n      expect(modal.classList.contains(\"fade-out\")).toBe(false);\n      // second tick: waiting for out classes to be set\n      jest.advanceTimersToNextFrame();\n      expect(modal.classList.contains(\"fade-out-start\")).toBe(true);\n      expect(modal.classList.contains(\"fade-out\")).toBe(true);\n      // third tick: waiting for outEndClasses\n      jest.advanceTimersToNextFrame();\n      expect(modal.classList.contains(\"fade-out-start\")).toBe(false);\n      expect(modal.classList.contains(\"fade-out\")).toBe(true);\n      expect(modal.classList.contains(\"fade-out-end\")).toBe(true);\n      // wait for onEnd\n      jest.runAllTimers();\n      jest.advanceTimersToNextFrame();\n      // fifth tick: display: none\n      jest.advanceTimersToNextFrame();\n      expect(hideEndCalled).toBe(true);\n      expect(modal.style.display).toEqual(\"none\");\n      // sixth tick, removed end classes\n      jest.advanceTimersToNextFrame();\n      expect(modal.classList.contains(\"fade-out-start\")).toBe(false);\n      expect(modal.classList.contains(\"fade-out\")).toBe(false);\n      expect(modal.classList.contains(\"fade-out-end\")).toBe(false);\n\n      // toggle in\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n      expect(showStartCalled).toBe(true);\n      // first tick: waiting for start classes to be set\n      jest.advanceTimersToNextFrame();\n      expect(modal.classList.contains(\"fade-in-start\")).toBe(true);\n      expect(modal.classList.contains(\"fade-in\")).toBe(false);\n      expect(modal.style.display).toEqual(\"none\");\n      // second tick: waiting for in classes to be set\n      jest.advanceTimersToNextFrame();\n      expect(modal.classList.contains(\"fade-in-start\")).toBe(true);\n      expect(modal.classList.contains(\"fade-in\")).toBe(true);\n      expect(modal.classList.contains(\"fade-in-end\")).toBe(false);\n      expect(modal.style.display).toEqual(\"block\");\n      // third tick: waiting for inEndClasses\n      jest.advanceTimersToNextFrame();\n      expect(modal.classList.contains(\"fade-in-start\")).toBe(false);\n      expect(modal.classList.contains(\"fade-in\")).toBe(true);\n      expect(modal.classList.contains(\"fade-in-end\")).toBe(true);\n      // wait for onEnd\n      jest.runAllTimers();\n      jest.advanceTimersToNextFrame();\n      expect(showEndCalled).toBe(true);\n      // sixth tick, removed end classes\n      expect(modal.classList.contains(\"fade-in-start\")).toBe(false);\n      expect(modal.classList.contains(\"fade-in\")).toBe(false);\n      expect(modal.classList.contains(\"fade-in-end\")).toBe(false);\n    });\n  });\n\n  describe(\"exec_transition\", () => {\n    test(\"with defaults\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\">modal</div>\n      <div id=\"click\" phx-click='[[\"transition\", {\"to\": \"#modal\", \"transition\": [[\"fade-out\"],[],[]]}]]'></div>\n      `);\n      const modal = simulateVisibility(document.querySelector(\"#modal\"));\n      const click = document.querySelector(\"#click\")!;\n\n      expect(Array.from(modal.classList)).toEqual([\"modal\"]);\n\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n      jest.advanceTimersByTime(100);\n\n      expect(Array.from(modal.classList)).toEqual([\"modal\", \"fade-out\"]);\n      jest.runAllTimers();\n\n      expect(Array.from(modal.classList)).toEqual([\"modal\"]);\n      done();\n    });\n\n    test(\"with multiple selector\", (done) => {\n      const view = setupView(`\n      <div id=\"modal1\" class=\"modal\">modal</div>\n      <div id=\"modal2\" class=\"modal\">modal</div>\n      <div id=\"click\" phx-click='[[\"transition\", {\"to\": \"#modal1, #modal2\", \"transition\": [[\"fade-out\"],[],[]]}]]'></div>\n      `);\n      const modal1 = document.querySelector(\"#modal1\")!;\n      const modal2 = document.querySelector(\"#modal2\")!;\n      const click = document.querySelector(\"#click\")!;\n\n      expect(Array.from(modal1.classList)).toEqual([\"modal\"]);\n      expect(Array.from(modal2.classList)).toEqual([\"modal\"]);\n\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n      jest.advanceTimersByTime(100);\n\n      expect(Array.from(modal1.classList)).toEqual([\"modal\", \"fade-out\"]);\n      expect(Array.from(modal2.classList)).toEqual([\"modal\", \"fade-out\"]);\n\n      jest.runAllTimers();\n\n      expect(Array.from(modal1.classList)).toEqual([\"modal\"]);\n      expect(Array.from(modal2.classList)).toEqual([\"modal\"]);\n\n      done();\n    });\n  });\n\n  describe(\"exec_dispatch\", () => {\n    test(\"with defaults\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\">modal</div>\n      <div id=\"click\" phx-click='[[\"dispatch\", {\"to\": \"#modal\", \"event\": \"click\"}]]'></div>\n      `);\n      const modal = simulateVisibility(document.querySelector(\"#modal\"));\n      const click = document.querySelector(\"#click\")!;\n\n      modal.addEventListener(\"click\", () => {\n        done();\n      });\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n    });\n\n    test(\"with to scope inner\", (done) => {\n      const view = setupView(`\n      <div id=\"click\" phx-click='[[\"dispatch\", {\"to\": {\"inner\": \".modal\"}, \"event\": \"click\"}]]'>\n        <div class=\"modal\">modal</div>\n      </div>\n      `);\n      const modal = simulateVisibility(document.querySelector(\".modal\"));\n      const click = document.querySelector(\"#click\")!;\n\n      modal.addEventListener(\"click\", () => done());\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n    });\n\n    test(\"with to scope closest\", (done) => {\n      const view = setupView(`\n      <div class=\"modal\">\n        <div>\n          <div id=\"click\" phx-click='[[\"dispatch\", {\"to\": {\"closest\": \".modal\"}, \"event\": \"click\"}]]'></div>\n        </div>\n      </div>\n      `);\n      const modal = simulateVisibility(document.querySelector(\".modal\"));\n      const click = document.querySelector(\"#click\")!;\n\n      modal.addEventListener(\"click\", () => done());\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n    });\n\n    test(\"with details\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\">modal</div>\n      <div id=\"click\" phx-click='[[\"dispatch\", {\"to\": \"#modal\", \"event\": \"click\"}]]'></div>\n      <div id=\"close\" phx-click='[[\"dispatch\", {\"to\": \"#modal\", \"event\": \"close\", \"detail\": {\"id\": 1}}]]'></div>\n      `);\n      const modal = simulateVisibility(document.querySelector(\"#modal\"));\n      const click = document.querySelector(\"#click\")!;\n      const close = document.querySelector(\"#close\")!;\n\n      modal.addEventListener(\"close\", (e) => {\n        expect(e.detail).toEqual({ id: 1, dispatcher: close });\n        modal.addEventListener(\"click\", (e) => {\n          expect(e.detail).toEqual(0);\n          done();\n        });\n        JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n      });\n      JS.exec(event, \"close\", close.getAttribute(\"phx-click\"), view, close);\n    });\n\n    test(\"with multiple selector\", (done) => {\n      const view = setupView(`\n      <div id=\"modal1\" class=\"modal\">modal1</div>\n      <div id=\"modal2\" class=\"modal\">modal2</div>\n      <div id=\"close\" phx-click='[[\"dispatch\", {\"to\": \".modal\", \"event\": \"close\", \"detail\": {\"id\": 123}}]]'></div>\n      `);\n      let modal1Clicked = false;\n      const modal1 = document.querySelector(\"#modal1\")!;\n      const modal2 = document.querySelector(\"#modal2\")!;\n      const close = document.querySelector(\"#close\")!;\n\n      modal1.addEventListener(\"close\", (e: CustomEventInit<object>) => {\n        modal1Clicked = true;\n        expect(e.detail).toEqual({ id: 123, dispatcher: close });\n      });\n\n      modal2.addEventListener(\"close\", (e: CustomEventInit<object>) => {\n        expect(modal1Clicked).toBe(true);\n        expect(e.detail).toEqual({ id: 123, dispatcher: close });\n        done();\n      });\n\n      JS.exec(event, \"close\", close.getAttribute(\"phx-click\"), view, close);\n    });\n\n    test(\"blocking blocks DOM updates until done\", (done) => {\n      let view = setupView(`\n      <div id=\"modal\">modal</div>\n      <div id=\"click\" phx-click='[[\"dispatch\", {\"to\": \"#modal\", \"event\": \"custom\", \"blocking\": true}]]'></div>\n      `);\n      let modal = simulateVisibility(document.querySelector(\"#modal\"));\n      let click = document.querySelector(\"#click\")!;\n      let doneCalled = false;\n\n      modal.addEventListener(\"custom\", (e) => {\n        expect(e.detail).toEqual({\n          done: expect.any(Function),\n          dispatcher: click,\n        });\n        expect(view.liveSocket.transitions.size()).toBe(1);\n        view.liveSocket.requestDOMUpdate(() => {\n          expect(doneCalled).toBe(true);\n          done();\n        });\n        // now we unblock the transition\n        e.detail.done();\n        doneCalled = true;\n      });\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n    });\n  });\n\n  describe(\"exec_add_class and exec_remove_class\", () => {\n    test(\"with defaults\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\">modal</div>\n      <div id=\"add\" phx-click='[[\"add_class\", {\"to\": \"#modal\", \"names\": [\"class1\"]}]]'></div>\n      <div id=\"remove\" phx-click='[[\"remove_class\", {\"to\": \"#modal\", \"names\": [\"class1\"]}]]'></div>\n      `);\n      const modal = simulateVisibility(document.querySelector(\"#modal\"));\n      const add = document.querySelector(\"#add\")!;\n      const remove = document.querySelector(\"#remove\")!;\n\n      JS.exec(event, \"click\", add.getAttribute(\"phx-click\"), view, add);\n      JS.exec(event, \"click\", add.getAttribute(\"phx-click\"), view, add);\n      JS.exec(event, \"click\", add.getAttribute(\"phx-click\"), view, add);\n      jest.runAllTimers();\n\n      expect(Array.from(modal.classList)).toEqual([\"modal\", \"class1\"]);\n\n      JS.exec(event, \"click\", remove.getAttribute(\"phx-click\"), view, remove);\n      jest.runAllTimers();\n\n      expect(Array.from(modal.classList)).toEqual([\"modal\"]);\n      done();\n    });\n\n    test(\"with multiple selector\", (done) => {\n      const view = setupView(`\n      <div id=\"modal1\" class=\"modal\">modal</div>\n      <div id=\"modal2\" class=\"modal\">modal</div>\n      <div id=\"add\" phx-click='[[\"add_class\", {\"to\": \"#modal1, #modal2\", \"names\": [\"class1\"]}]]'></div>\n      <div id=\"remove\" phx-click='[[\"remove_class\", {\"to\": \"#modal1, #modal2\", \"names\": [\"class1\"]}]]'></div>\n      `);\n      const modal1 = document.querySelector(\"#modal1\")!;\n      const modal2 = document.querySelector(\"#modal2\")!;\n      const add = document.querySelector(\"#add\")!;\n      const remove = document.querySelector(\"#remove\")!;\n\n      JS.exec(event, \"click\", add.getAttribute(\"phx-click\"), view, add);\n      jest.runAllTimers();\n\n      expect(Array.from(modal1.classList)).toEqual([\"modal\", \"class1\"]);\n      expect(Array.from(modal2.classList)).toEqual([\"modal\", \"class1\"]);\n\n      JS.exec(event, \"click\", remove.getAttribute(\"phx-click\"), view, remove);\n      jest.runAllTimers();\n\n      expect(Array.from(modal1.classList)).toEqual([\"modal\"]);\n      expect(Array.from(modal2.classList)).toEqual([\"modal\"]);\n      done();\n    });\n  });\n\n  describe(\"exec_toggle_class\", () => {\n    test(\"with defaults\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\">modal</div>\n      <div id=\"toggle\" phx-click='[[\"toggle_class\", {\"to\": \"#modal\", \"names\": [\"class1\"]}]]'></div>\n      `);\n      const modal = simulateVisibility(document.querySelector(\"#modal\"));\n      const toggle = document.querySelector(\"#toggle\")!;\n\n      JS.exec(event, \"click\", toggle.getAttribute(\"phx-click\"), view, toggle);\n      jest.runAllTimers();\n\n      expect(Array.from(modal.classList)).toEqual([\"modal\", \"class1\"]);\n\n      JS.exec(event, \"click\", toggle.getAttribute(\"phx-click\"), view, toggle);\n      jest.runAllTimers();\n\n      expect(Array.from(modal.classList)).toEqual([\"modal\"]);\n      done();\n    });\n\n    test(\"with multiple selector\", (done) => {\n      const view = setupView(`\n      <div id=\"modal1\" class=\"modal\">modal</div>\n      <div id=\"modal2\" class=\"modal\">modal</div>\n      <div id=\"toggle\" phx-click='[[\"toggle_class\", {\"to\": \"#modal1, #modal2\", \"names\": [\"class1\"]}]]'></div>\n      `);\n      const modal1 = document.querySelector(\"#modal1\")!;\n      const modal2 = document.querySelector(\"#modal2\")!;\n      const toggle = document.querySelector(\"#toggle\")!;\n\n      JS.exec(event, \"click\", toggle.getAttribute(\"phx-click\"), view, toggle);\n      jest.runAllTimers();\n\n      expect(Array.from(modal1.classList)).toEqual([\"modal\", \"class1\"]);\n      expect(Array.from(modal2.classList)).toEqual([\"modal\", \"class1\"]);\n      JS.exec(event, \"click\", toggle.getAttribute(\"phx-click\"), view, toggle);\n      jest.runAllTimers();\n\n      expect(Array.from(modal1.classList)).toEqual([\"modal\"]);\n      expect(Array.from(modal2.classList)).toEqual([\"modal\"]);\n      done();\n    });\n\n    test(\"with transition\", (done) => {\n      const view = setupView(`\n      <button phx-click='[[\"toggle_class\",{\"names\":[\"t\"],\"transition\":[[\"a\"],[\"b\"],[\"c\"]]}]]'></button>\n      `);\n      const button = document.querySelector(\"button\")!;\n\n      expect(Array.from(button.classList)).toEqual([]);\n\n      JS.exec(event, \"click\", button.getAttribute(\"phx-click\"), view, button);\n\n      jest.advanceTimersByTime(100);\n      expect(Array.from(button.classList)).toEqual([\"a\", \"c\"]);\n\n      jest.runAllTimers();\n      expect(Array.from(button.classList)).toEqual([\"c\", \"t\"]);\n\n      done();\n    });\n  });\n\n  describe(\"push\", () => {\n    test(\"regular event\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\">modal</div>\n      <div id=\"click\" phx-click='[[\"push\", {\"event\": \"clicked\"}]]'></div>\n      `);\n      const click = document.querySelector(\"#click\")!;\n      view.pushEvent = (eventType, sourceEl, targetCtx, event, meta) => {\n        expect(eventType).toBe(\"click\");\n        expect(event).toBe(\"clicked\");\n        expect(meta).toBeUndefined();\n        done();\n      };\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n    });\n\n    test(\"form change event with JS command\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\">modal</div>\n      <form id=\"my-form\" phx-change='[[\"push\", {\"event\": \"validate\", \"_target\": \"username\"}]]' phx-submit=\"submit\">\n        <input type=\"text\" name=\"username\" id=\"username\" phx-click=''></div>\n      </form>\n      `);\n      const form = document.querySelector(\"#my-form\")!;\n      const input: HTMLInputElement = document.querySelector(\"#username\")!;\n      view.pushInput = (\n        sourceEl,\n        _targetCtx,\n        _newCid,\n        phxEvent,\n        { _target },\n        _callback,\n      ) => {\n        expect(phxEvent).toBe(\"validate\");\n        expect(sourceEl.isSameNode(input)).toBe(true);\n        expect(_target).toBe(input.name);\n        done();\n      };\n      const args = [\"push\", { _target: input.name, dispatcher: input }];\n      JS.exec(\n        event,\n        \"change\",\n        form.getAttribute(\"phx-change\"),\n        view,\n        input,\n        args,\n      );\n    });\n\n    test(\"form change event with phx-value and JS command value\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\">modal</div>\n      <form id=\"my-form\"\n        phx-change='[[\"push\", {\"event\": \"validate\", \"_target\": \"username\", \"value\": {\"command_value\": \"command\",\"nested\":{\"array\":[1,2]}}}]]'\n        phx-submit=\"submit\"\n        phx-value-attribute_value=\"attribute\"\n      >\n        <input type=\"text\" name=\"username\" id=\"username\" phx-click=''></div>\n      </form>\n      `);\n      const form = document.querySelector(\"#my-form\")!;\n      const input: HTMLInputElement = document.querySelector(\"#username\")!;\n      view.pushWithReply = (refGen, event, payload) => {\n        expect(payload).toEqual({\n          cid: null,\n          event: \"validate\",\n          type: \"form\",\n          value: \"_unused_username=&username=\",\n          meta: {\n            _target: \"username\",\n            command_value: \"command\",\n            nested: {\n              array: [1, 2],\n            },\n            attribute_value: \"attribute\",\n          },\n          uploads: {},\n        });\n        return Promise.resolve({ resp: done() });\n      };\n      const args = [\"push\", { _target: input.name, dispatcher: input }];\n      JS.exec(\n        event,\n        \"change\",\n        form.getAttribute(\"phx-change\"),\n        view,\n        input,\n        args,\n      );\n    });\n\n    test(\"form change event prefers JS.push value over phx-value-* over input value\", (done) => {\n      const view = setupView(`\n        <form id=\"my-form\" phx-value-name=\"value from phx-value param\" phx-change=\"[[&quot;push&quot;,{&quot;value&quot;:{&quot;name&quot;:&quot;value from push opts&quot;},&quot;event&quot;:&quot;change&quot;}]]\">\n          <input id=\"textField\" name=\"name\" value=\"input value\" />\n        </form>\n      `);\n      const form: HTMLFormElement = document.querySelector(\"#my-form\")!;\n      const input: HTMLInputElement = document.querySelector(\"#textField\")!;\n      view.pushWithReply = (refGen, event, payload) => {\n        expect(payload).toEqual({\n          cid: null,\n          event: \"change\",\n          type: \"form\",\n          value: \"_unused_name=&name=input+value\",\n          meta: {\n            _target: \"name\",\n            name: \"value from push opts\",\n          },\n          uploads: {},\n        });\n        return Promise.resolve({ resp: done() });\n      };\n      const args = [\"push\", { _target: input.name, dispatcher: input }];\n      JS.exec(\n        event,\n        \"change\",\n        form.getAttribute(\"phx-change\"),\n        view,\n        input,\n        args,\n      );\n    });\n\n    test(\"form change event prefers phx-value-* over input value\", (done) => {\n      const view = setupView(`\n        <form id=\"my-form\" phx-value-name=\"value from phx-value param\" phx-change=\"change\">\n          <input id=\"textField\" name=\"name\" value=\"input value\" />\n        </form>\n      `);\n      const form: HTMLFormElement = document.querySelector(\"#my-form\")!;\n      const input: HTMLInputElement = document.querySelector(\"#textField\")!;\n      view.pushWithReply = (refGen, event, payload) => {\n        expect(payload).toEqual({\n          cid: null,\n          event: \"change\",\n          type: \"form\",\n          value: \"_unused_name=&name=input+value\",\n          meta: {\n            _target: \"name\",\n            name: \"value from phx-value param\",\n          },\n          uploads: {},\n        });\n        return Promise.resolve({ resp: done() });\n      };\n      const args = [\"push\", { _target: input.name, dispatcher: input }];\n      JS.exec(\n        event,\n        \"change\",\n        form.getAttribute(\"phx-change\"),\n        view,\n        input,\n        args,\n      );\n    });\n\n    test(\"form change event with string event\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\">modal</div>\n      <form id=\"my-form\" phx-change='validate' phx-submit=\"submit\">\n        <input type=\"text\" name=\"username\" id=\"username\" />\n        <input type=\"text\" name=\"other\" id=\"other\" phx-change=\"other_changed\" />\n      </form>\n      `);\n      const form: HTMLFormElement = document.querySelector(\"#my-form\")!;\n      const input: HTMLInputElement = document.querySelector(\"#username\")!;\n      const oldPush = view.pushInput.bind(view);\n      view.pushInput = (\n        sourceEl,\n        targetCtx,\n        newCid,\n        phxEvent,\n        opts,\n        callback,\n      ) => {\n        const { _target } = opts;\n        expect(phxEvent).toBe(\"validate\");\n        expect(sourceEl.isSameNode(input)).toBe(true);\n        expect(_target).toBe(input.name);\n        oldPush(sourceEl, targetCtx, newCid, phxEvent, opts, callback);\n      };\n      view.pushWithReply = (_refGen, _event, payload) => {\n        expect(payload).toEqual({\n          cid: null,\n          event: \"validate\",\n          type: \"form\",\n          uploads: {},\n          value: \"_unused_username=&username=&_unused_other=&other=\",\n          meta: { _target: \"username\" },\n        });\n        return Promise.resolve({ resp: done() });\n      };\n      const args = [\"push\", { _target: input.name, dispatcher: input }];\n      JS.exec(\n        event,\n        \"change\",\n        form.getAttribute(\"phx-change\"),\n        view,\n        input,\n        args,\n      );\n    });\n\n    test(\"input change event with JS command\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\">modal</div>\n      <form id=\"my-form\" phx-change='validate' phx-submit=\"submit\">\n        <input type=\"text\" name=\"username\" id=\"username1\" phx-change='[[\"push\", {\"event\": \"username_changed\", \"_target\": \"username\"}]]'/>\n        <input type=\"text\" name=\"other\" id=\"other\" />\n      </form>\n      `);\n      const input: HTMLInputElement = document.querySelector(\"#username1\")!;\n      const oldPush = view.pushInput.bind(view);\n      view.pushInput = (\n        sourceEl,\n        targetCtx,\n        newCid,\n        phxEvent,\n        opts,\n        callback,\n      ) => {\n        const { _target } = opts;\n        expect(phxEvent).toBe(\"username_changed\");\n        expect(sourceEl.isSameNode(input)).toBe(true);\n        expect(_target).toBe(input.name);\n        oldPush(sourceEl, targetCtx, newCid, phxEvent, opts, callback);\n      };\n      view.pushWithReply = (_refGen, _event, payload) => {\n        expect(payload).toEqual({\n          cid: null,\n          event: \"username_changed\",\n          type: \"form\",\n          uploads: {},\n          value: \"_unused_username=&username=\",\n          meta: { _target: \"username\" },\n        });\n        return Promise.resolve({ resp: done() });\n      };\n      const args = [\"push\", { _target: input.name, dispatcher: input }];\n      JS.exec(\n        event,\n        \"change\",\n        input.getAttribute(\"phx-change\"),\n        view,\n        input,\n        args,\n      );\n    });\n\n    test(\"input change event with string event\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\">modal</div>\n      <form id=\"my-form\" phx-change='validate' phx-submit=\"submit\">\n        <input type=\"text\" name=\"username\" id=\"username\" phx-change='username_changed' />\n        <input type=\"text\" name=\"other\" id=\"other\" />\n      </form>\n      `);\n      const input: HTMLInputElement = document.querySelector(\"#username\")!;\n      const oldPush = view.pushInput.bind(view);\n      view.pushInput = (\n        sourceEl,\n        targetCtx,\n        newCid,\n        phxEvent,\n        opts,\n        callback,\n      ) => {\n        const { _target } = opts;\n        expect(phxEvent).toBe(\"username_changed\");\n        expect(sourceEl.isSameNode(input)).toBe(true);\n        expect(_target).toBe(input.name);\n        oldPush(sourceEl, targetCtx, newCid, phxEvent, opts, callback);\n      };\n      view.pushWithReply = (refGen, event, payload) => {\n        expect(payload).toEqual({\n          cid: null,\n          event: \"username_changed\",\n          type: \"form\",\n          uploads: {},\n          value: \"_unused_username=&username=\",\n          meta: { _target: \"username\" },\n        });\n        return Promise.resolve({ resp: done() });\n      };\n      const args = [\"push\", { _target: input.name, dispatcher: input }];\n      JS.exec(\n        event,\n        \"change\",\n        input.getAttribute(\"phx-change\"),\n        view,\n        input,\n        args,\n      );\n    });\n\n    test(\"submit event\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\">modal</div>\n      <form id=\"my-form\" phx-change=\"validate\" phx-submit='[[\"push\", {\"event\": \"save\"}]]'>\n        <input type=\"text\" name=\"username\" id=\"username\" />\n        <input type=\"text\" name=\"desc\" id=\"desc\" phx-change=\"desc_changed\" />\n      </form>\n      `);\n      const form: HTMLFormElement = document.querySelector(\"#my-form\")!;\n\n      view.pushWithReply = (refGen, event, payload) => {\n        expect(payload).toEqual({\n          cid: null,\n          event: \"save\",\n          type: \"form\",\n          value: \"username=&desc=\",\n          meta: {},\n        });\n        return Promise.resolve({ resp: done() });\n      };\n      JS.exec(event, \"submit\", form.getAttribute(\"phx-submit\"), view, form, [\n        \"push\",\n        {},\n      ]);\n    });\n\n    test(\"submit event with phx-value and JS command value\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\">modal</div>\n      <form id=\"my-form\"\n            phx-change=\"validate\"\n            phx-submit='[[\"push\", {\"event\": \"save\", \"value\": {\"command_value\": \"command\",\"nested\":{\"array\":[1,2]}}}]]'\n            phx-value-attribute_value=\"attribute\"\n      >\n        <input type=\"text\" name=\"username\" id=\"username\" />\n        <input type=\"text\" name=\"desc\" id=\"desc\" phx-change=\"desc_changed\" />\n      </form>\n      `);\n      const form: HTMLFormElement = document.querySelector(\"#my-form\")!;\n\n      view.pushWithReply = (refGen, event, payload) => {\n        expect(payload).toEqual({\n          cid: null,\n          event: \"save\",\n          type: \"form\",\n          value: \"username=&desc=\",\n          meta: {\n            command_value: \"command\",\n            nested: {\n              array: [1, 2],\n            },\n            attribute_value: \"attribute\",\n          },\n        });\n        return Promise.resolve({ resp: done() });\n      };\n      JS.exec(event, \"submit\", form.getAttribute(\"phx-submit\"), view, form, [\n        \"push\",\n        {},\n      ]);\n    });\n\n    test(\"page_loading\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\">modal</div>\n      <div id=\"click\" phx-click='[[\"push\", {\"event\": \"clicked\", \"page_loading\": true}]]'></div>\n      `);\n      const click = document.querySelector(\"#click\")!;\n      view.pushEvent = (eventType, sourceEl, targetCtx, event, meta, opts) => {\n        expect(opts!.page_loading).toBe(true);\n        done();\n      };\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n    });\n\n    test(\"loading\", () => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\">modal</div>\n      <div id=\"click\" phx-click='[[\"push\", {\"event\": \"clicked\", \"loading\": \"#modal\"}]]'></div>\n      `);\n      const click = document.querySelector(\"#click\")!;\n      const modal = document.getElementById(\"modal\")!;\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n      expect(Array.from(modal.classList)).toEqual([\n        \"modal\",\n        \"phx-click-loading\",\n      ]);\n      expect(Array.from(click.classList)).toEqual([\"phx-click-loading\"]);\n    });\n\n    test(\"value\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\">modal</div>\n      <div id=\"click\" phx-value-three=\"3\" phx-click='[[\"push\", {\"event\": \"clicked\", \"value\": {\"one\": 1, \"two\": 2}}]]'></div>\n      `);\n      const click = document.querySelector(\"#click\")!;\n\n      view.pushWithReply = (refGenerator, event, payload) => {\n        expect(payload.value).toEqual({ one: 1, two: 2, three: \"3\" });\n        return Promise.resolve({ resp: done() });\n      };\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n    });\n  });\n\n  describe(\"multiple instructions\", () => {\n    test(\"push and toggle\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\">modal</div>\n      <div id=\"click\" phx-click='[[\"push\", {\"event\": \"clicked\"}], [\"toggle\", {\"to\": \"#modal\"}]]'></div>\n      `);\n      const modal = simulateVisibility(document.querySelector(\"#modal\"));\n      const click = document.querySelector(\"#click\")!;\n\n      view.pushEvent = (_eventType, _sourceEl, _targetCtx, event, _data) => {\n        expect(event).toEqual(\"clicked\");\n        done();\n      };\n\n      expect(modal.style.display).toEqual(\"\");\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n      jest.runAllTimers();\n\n      expect(modal.style.display).toEqual(\"none\");\n    });\n  });\n\n  describe(\"exec_set_attr and exec_remove_attr\", () => {\n    test(\"with defaults\", () => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\">modal</div>\n      <div id=\"set\" phx-click='[[\"set_attr\", {\"to\": \"#modal\", \"attr\": [\"aria-expanded\", \"true\"]}]]'></div>\n      <div id=\"remove\" phx-click='[[\"remove_attr\", {\"to\": \"#modal\", \"attr\": \"aria-expanded\"}]]'></div>\n      `);\n      const modal = document.querySelector(\"#modal\")!;\n      const set = document.querySelector(\"#set\")!;\n      const remove = document.querySelector(\"#remove\")!;\n\n      expect(modal.getAttribute(\"aria-expanded\")).toEqual(null);\n      JS.exec(event, \"click\", set.getAttribute(\"phx-click\"), view, set);\n      expect(modal.getAttribute(\"aria-expanded\")).toEqual(\"true\");\n\n      JS.exec(event, \"click\", remove.getAttribute(\"phx-click\"), view, remove);\n      expect(modal.getAttribute(\"aria-expanded\")).toEqual(null);\n    });\n\n    test(\"with no selector\", () => {\n      const view = setupView(`\n      <div id=\"set\" phx-click='[[\"set_attr\", {\"to\": null, \"attr\": [\"aria-expanded\", \"true\"]}]]'></div>\n      <div id=\"remove\" class=\"here\" phx-click='[[\"remove_attr\", {\"to\": null, \"attr\": \"class\"}]]'></div>\n      `);\n      const set = document.querySelector(\"#set\")!;\n      const remove = document.querySelector(\"#remove\")!;\n\n      expect(set.getAttribute(\"aria-expanded\")).toEqual(null);\n      JS.exec(event, \"click\", set.getAttribute(\"phx-click\"), view, set);\n      expect(set.getAttribute(\"aria-expanded\")).toEqual(\"true\");\n\n      expect(remove.getAttribute(\"class\")).toEqual(\"here\");\n      JS.exec(event, \"click\", remove.getAttribute(\"phx-click\"), view, remove);\n      expect(remove.getAttribute(\"class\")).toEqual(null);\n    });\n\n    test(\"setting a pre-existing attribute updates its value\", () => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\" aria-expanded=\"false\">modal</div>\n      <div id=\"set\" phx-click='[[\"set_attr\", {\"to\": \"#modal\", \"attr\": [\"aria-expanded\", \"true\"]}]]'></div>\n      `);\n      const set = document.querySelector(\"#set\")!;\n      const modal = document.querySelector(\"#modal\")!;\n\n      expect(modal.getAttribute(\"aria-expanded\")).toEqual(\"false\");\n      JS.exec(event, \"click\", set.getAttribute(\"phx-click\"), view, set);\n      expect(modal.getAttribute(\"aria-expanded\")).toEqual(\"true\");\n    });\n\n    test(\"setting a dynamically added attribute updates its value\", () => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\">modal</div>\n      <div id=\"set-false\" phx-click='[[\"set_attr\", {\"to\": \"#modal\", \"attr\": [\"aria-expanded\", \"false\"]}]]'></div>\n      <div id=\"set-true\" phx-click='[[\"set_attr\", {\"to\": \"#modal\", \"attr\": [\"aria-expanded\", \"true\"]}]]'></div>\n      `);\n      const setFalse = document.querySelector(\"#set-false\")!;\n      const setTrue = document.querySelector(\"#set-true\")!;\n      const modal = document.querySelector(\"#modal\")!;\n\n      expect(modal.getAttribute(\"aria-expanded\")).toEqual(null);\n      JS.exec(\n        event,\n        \"click\",\n        setFalse.getAttribute(\"phx-click\"),\n        view,\n        setFalse,\n      );\n      expect(modal.getAttribute(\"aria-expanded\")).toEqual(\"false\");\n      JS.exec(event, \"click\", setTrue.getAttribute(\"phx-click\"), view, setTrue);\n      expect(modal.getAttribute(\"aria-expanded\")).toEqual(\"true\");\n    });\n  });\n\n  describe(\"exec\", () => {\n    test(\"executes command\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\" phx-remove='[[\"push\", {\"event\": \"clicked\"}]]'>modal</div>\n      <div id=\"click\" phx-click='[[\"exec\",{\"attr\": \"phx-remove\", \"to\": \"#modal\"}]]'></div>\n      `);\n      const click = document.querySelector(\"#click\")!;\n      view.pushEvent = (eventType, sourceEl, targetCtx, event, _meta) => {\n        expect(eventType).toBe(\"exec\");\n        expect(event).toBe(\"clicked\");\n        done();\n      };\n      JS.exec(event, \"exec\", click.getAttribute(\"phx-click\"), view, click);\n    });\n\n    test(\"with command array\", (done) => {\n      const view = setupView(`\n      <div id=\"modal\" phx-remove='[[\"push\", {\"event\": \"clicked\"}]]'>modal</div>\n      `);\n      const modal = document.querySelector(\"#modal\")!;\n      view.pushEvent = (eventType, sourceEl, targetCtx, event, _meta) => {\n        expect(eventType).toBe(\"exec\");\n        expect(event).toBe(\"clicked\");\n        done();\n      };\n      JS.exec(event, \"exec\", [[\"exec\", { attr: \"phx-remove\" }]], view, modal);\n    });\n\n    test(\"with no selector\", () => {\n      const view = setupView(`\n      <div\n        id=\"click\"\n        phx-click='[[\"exec\", {\"attr\": \"data-toggle\"}]]''\n        data-toggle='[[\"toggle_attr\", {\"attr\": [\"open\", \"true\"]}]]'\n      ></div>\n      `);\n      const click = document.querySelector(\"#click\")!;\n\n      expect(click.getAttribute(\"open\")).toEqual(null);\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n      expect(click.getAttribute(\"open\")).toEqual(\"true\");\n    });\n\n    test(\"with to scope inner\", () => {\n      const view = setupView(`\n      <div id=\"click\" phx-click='[[\"exec\",{\"attr\": \"data-toggle\", \"to\": {\"inner\": \"#modal\"}}]]'>\n        <div id=\"modal\" data-toggle='[[\"toggle_attr\", {\"attr\": [\"open\", \"true\"]}]]'>modal</div>\n      </div>\n      `);\n      const modal = document.querySelector(\"#modal\")!;\n      const click = document.querySelector(\"#click\")!;\n\n      expect(modal.getAttribute(\"open\")).toEqual(null);\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n      expect(modal.getAttribute(\"open\")).toEqual(\"true\");\n    });\n\n    test(\"with to scope closest\", () => {\n      const view = setupView(`\n      <div id=\"modal\" data-toggle='[[\"toggle_attr\", {\"attr\": [\"open\", \"true\"]}]]'>\n        <div id=\"click\" phx-click='[[\"exec\",{\"attr\": \"data-toggle\", \"to\": {\"closest\": \"#modal\"}}]]'></div>\n      </div>\n      `);\n      const modal = document.querySelector(\"#modal\")!;\n      const click = document.querySelector(\"#click\")!;\n\n      expect(modal.getAttribute(\"open\")).toEqual(null);\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n      expect(modal.getAttribute(\"open\")).toEqual(\"true\");\n    });\n\n    test(\"with multiple selector\", () => {\n      const view = setupView(`\n      <div id=\"modal1\" data-toggle='[[\"toggle_attr\", {\"attr\": [\"open\", \"true\"]}]]'>modal</div>\n      <div id=\"modal2\" data-toggle='[[\"toggle_attr\", {\"attr\": [\"open\", \"true\"]}]]' open='true'>modal</div>\n      <div id=\"click\" phx-click='[[\"exec\", {\"attr\": \"data-toggle\", \"to\": \"#modal1, #modal2\"}]]'></div>\n      `);\n      const modal1 = document.querySelector(\"#modal1\")!;\n      const modal2 = document.querySelector(\"#modal2\")!;\n      const click = document.querySelector(\"#click\")!;\n\n      expect(modal1.getAttribute(\"open\")).toEqual(null);\n      expect(modal2.getAttribute(\"open\")).toEqual(\"true\");\n      JS.exec(event, \"click\", click.getAttribute(\"phx-click\"), view, click);\n      expect(modal1.getAttribute(\"open\")).toEqual(\"true\");\n      expect(modal2.getAttribute(\"open\")).toEqual(null);\n    });\n  });\n\n  describe(\"exec_toggle_attr\", () => {\n    test(\"with defaults\", () => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\">modal</div>\n      <div id=\"toggle\" phx-click='[[\"toggle_attr\", {\"to\": \"#modal\", \"attr\": [\"open\", \"true\"]}]]'></div>\n      `);\n      const modal = document.querySelector(\"#modal\")!;\n      const toggle = document.querySelector(\"#toggle\")!;\n\n      expect(modal.getAttribute(\"open\")).toEqual(null);\n      JS.exec(event, \"click\", toggle.getAttribute(\"phx-click\"), view, toggle);\n      expect(modal.getAttribute(\"open\")).toEqual(\"true\");\n\n      JS.exec(event, \"click\", toggle.getAttribute(\"phx-click\"), view, toggle);\n      expect(modal.getAttribute(\"open\")).toEqual(null);\n    });\n\n    test(\"with no selector\", () => {\n      const view = setupView(`\n      <div id=\"toggle\" phx-click='[[\"toggle_attr\", {\"to\": null, \"attr\": [\"open\", \"true\"]}]]'></div>\n      `);\n      const toggle = document.querySelector(\"#toggle\")!;\n\n      expect(toggle.getAttribute(\"open\")).toEqual(null);\n      JS.exec(event, \"click\", toggle.getAttribute(\"phx-click\"), view, toggle);\n      expect(toggle.getAttribute(\"open\")).toEqual(\"true\");\n    });\n\n    test(\"with multiple selector\", () => {\n      const view = setupView(`\n      <div id=\"modal1\">modal</div>\n      <div id=\"modal2\" open=\"true\">modal</div>\n      <div id=\"toggle\" phx-click='[[\"toggle_attr\", {\"to\": \"#modal1, #modal2\", \"attr\": [\"open\", \"true\"]}]]'></div>\n      `);\n      const modal1 = document.querySelector(\"#modal1\")!;\n      const modal2 = document.querySelector(\"#modal2\")!;\n      const toggle = document.querySelector(\"#toggle\")!;\n\n      expect(modal1.getAttribute(\"open\")).toEqual(null);\n      expect(modal2.getAttribute(\"open\")).toEqual(\"true\");\n      JS.exec(event, \"click\", toggle.getAttribute(\"phx-click\"), view, toggle);\n      expect(modal1.getAttribute(\"open\")).toEqual(\"true\");\n      expect(modal2.getAttribute(\"open\")).toEqual(null);\n    });\n\n    test(\"toggling a pre-existing attribute updates its value\", () => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\" open=\"true\">modal</div>\n      <div id=\"toggle\" phx-click='[[\"toggle_attr\", {\"to\": \"#modal\", \"attr\": [\"open\", \"true\"]}]]'></div>\n      `);\n      const toggle = document.querySelector(\"#toggle\")!;\n      const modal = document.querySelector(\"#modal\")!;\n\n      expect(modal.getAttribute(\"open\")).toEqual(\"true\");\n      JS.exec(event, \"click\", toggle.getAttribute(\"phx-click\"), view, toggle);\n      expect(modal.getAttribute(\"open\")).toEqual(null);\n    });\n\n    test(\"toggling a dynamically added attribute updates its value\", () => {\n      const view = setupView(`\n      <div id=\"modal\" class=\"modal\">modal</div>\n      <div id=\"toggle1\" phx-click='[[\"toggle_attr\", {\"to\": \"#modal\", \"attr\": [\"open\", \"true\"]}]]'></div>\n      <div id=\"toggle2\" phx-click='[[\"toggle_attr\", {\"to\": \"#modal\", \"attr\": [\"open\", \"true\"]}]]'></div>\n      `);\n      const toggle1 = document.querySelector(\"#toggle1\")!;\n      const toggle2 = document.querySelector(\"#toggle2\")!;\n      const modal = document.querySelector(\"#modal\")!;\n\n      expect(modal.getAttribute(\"open\")).toEqual(null);\n      JS.exec(event, \"click\", toggle1.getAttribute(\"phx-click\"), view, toggle1);\n      expect(modal.getAttribute(\"open\")).toEqual(\"true\");\n      JS.exec(event, \"click\", toggle2.getAttribute(\"phx-click\"), view, toggle2);\n      expect(modal.getAttribute(\"open\")).toEqual(null);\n    });\n\n    test(\"toggling between two values\", () => {\n      const view = setupView(`\n      <div id=\"toggle\" phx-click='[[\"toggle_attr\", {\"to\": null, \"attr\": [\"aria-expanded\", \"true\", \"false\"]}]]'></div>\n      `);\n      const toggle = document.querySelector(\"#toggle\")!;\n\n      JS.exec(event, \"click\", toggle.getAttribute(\"phx-click\"), view, toggle);\n      expect(toggle.getAttribute(\"aria-expanded\")).toEqual(\"true\");\n      JS.exec(event, \"click\", toggle.getAttribute(\"phx-click\"), view, toggle);\n      expect(toggle.getAttribute(\"aria-expanded\")).toEqual(\"false\");\n    });\n  });\n\n  describe(\"focus\", () => {\n    test(\"works like a stack\", () => {\n      const view = setupView(`\n      <div id=\"modal1\" tabindex=\"0\" class=\"modal\">modal 1</div>\n      <div id=\"modal2\" tabindex=\"0\" class=\"modal\">modal 2</div>\n      <div id=\"push1\" phx-click='[[\"push_focus\", {\"to\": \"#modal1\"}]]'></div>\n      <div id=\"push2\" phx-click='[[\"push_focus\", {\"to\": \"#modal2\"}]]'></div>\n      <div id=\"pop\" phx-click='[[\"pop_focus\", {}]]'></div>\n      `);\n      const modal1 = document.querySelector(\"#modal1\")!;\n      const modal2 = document.querySelector(\"#modal2\")!;\n      const push1 = document.querySelector(\"#push1\")!;\n      const push2 = document.querySelector(\"#push2\")!;\n      const pop = document.querySelector(\"#pop\")!;\n\n      JS.exec(event, \"click\", push1.getAttribute(\"phx-click\"), view, push1);\n      JS.exec(event, \"click\", push2.getAttribute(\"phx-click\"), view, push2);\n\n      JS.exec(event, \"click\", pop.getAttribute(\"phx-click\"), view, pop);\n      jest.runAllTimers();\n      expect(document.activeElement).toBe(modal2);\n\n      JS.exec(event, \"click\", pop.getAttribute(\"phx-click\"), view, pop);\n      jest.runAllTimers();\n      expect(document.activeElement).toBe(modal1);\n    });\n  });\n\n  describe(\"exec_focus_first\", () => {\n    test(\"focuses div with tabindex 0\", () => {\n      const view = setupView(`\n      <div id=\"parent\">\n        <div id=\"modal1\" class=\"modal\">modal 1</div>\n        <div id=\"modal2\" tabindex=\"0\" class=\"modal\">modal 2</div>\n        <div id=\"modal3\" tabindex=\"1\" class=\"modal\">modal 1</div>\n        <div id=\"push\" phx-click='[[\"focus_first\", {\"to\": \"#parent\"}]]'></div>\n      </div>\n      `);\n      const modal2 = document.querySelector(\"#modal2\")!;\n      const push = document.querySelector(\"#push\")!;\n\n      JS.exec(event, \"click\", push.getAttribute(\"phx-click\"), view, push);\n\n      jest.runAllTimers();\n      expect(document.activeElement).toBe(modal2);\n    });\n  });\n});\n"
  },
  {
    "path": "assets/test/live_socket_test.ts",
    "content": "import { Socket } from \"phoenix\";\nimport LiveSocket from \"phoenix_live_view/live_socket\";\nimport JS from \"phoenix_live_view/js\";\nimport { simulateJoinedView, simulateVisibility } from \"./test_helpers\";\n\nconst container = (num) => global.document.getElementById(`container${num}`);\n\nconst prepareLiveViewDOM = (document) => {\n  const div = document.createElement(\"div\");\n  div.setAttribute(\"data-phx-session\", \"abc123\");\n  div.setAttribute(\"data-phx-root-id\", \"container1\");\n  div.setAttribute(\"id\", \"container1\");\n  div.innerHTML = `\n    <label for=\"plus\">Plus</label>\n    <input id=\"plus\" value=\"1\" />\n    <button phx-click=\"inc_temperature\">Inc Temperature</button>\n  `;\n  const button = div.querySelector(\"button\");\n  const input = div.querySelector(\"input\");\n  button.addEventListener(\"click\", () => {\n    setTimeout(() => {\n      input.value += 1;\n    }, 200);\n  });\n  document.body.appendChild(div);\n};\n\ndescribe(\"LiveSocket\", () => {\n  let liveSocket;\n\n  beforeEach(() => {\n    prepareLiveViewDOM(global.document);\n  });\n\n  afterEach(() => {\n    liveSocket && liveSocket.destroyAllViews();\n    liveSocket = null;\n  });\n\n  afterAll(() => {\n    global.document.body.innerHTML = \"\";\n  });\n\n  test(\"sets defaults\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    expect(liveSocket.socket).toBeDefined();\n    expect(liveSocket.socket.onOpen).toBeDefined();\n    expect(liveSocket.viewLogger).toBeUndefined();\n    expect(liveSocket.unloaded).toBe(false);\n    expect(liveSocket.bindingPrefix).toBe(\"phx-\");\n    expect(liveSocket.prevActive).toBe(null);\n  });\n\n  test(\"sets defaults with socket\", async () => {\n    liveSocket = new LiveSocket(new Socket(\"//example.org/chat\"), Socket);\n    expect(liveSocket.socket).toBeDefined();\n    expect(liveSocket.socket.onOpen).toBeDefined();\n    expect(liveSocket.unloaded).toBe(false);\n    expect(liveSocket.bindingPrefix).toBe(\"phx-\");\n    expect(liveSocket.prevActive).toBe(null);\n  });\n\n  test(\"viewLogger\", async () => {\n    const viewLogger = jest.fn();\n    liveSocket = new LiveSocket(\"/live\", Socket, { viewLogger });\n    expect(liveSocket.viewLogger).toBe(viewLogger);\n    liveSocket.connect();\n    const view = liveSocket.getViewByEl(container(1));\n    liveSocket.log(view, \"updated\", () => [\"\", JSON.stringify(\"<div>\")]);\n    expect(viewLogger).toHaveBeenCalledWith(\n      view,\n      \"updated\",\n      \"\",\n      JSON.stringify(\"<div>\"),\n    );\n  });\n\n  test(\"connect\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const _socket = liveSocket.connect();\n    expect(liveSocket.getViewByEl(container(1))).toBeDefined();\n  });\n\n  test(\"disconnect\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n\n    liveSocket.connect();\n    liveSocket.disconnect();\n\n    expect(liveSocket.getViewByEl(container(1)).destroy).toBeDefined();\n  });\n\n  test(\"channel\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n\n    liveSocket.connect();\n    const channel = liveSocket.channel(\n      \"lv:def456\",\n      function (this: { getSession(): string }) {\n        return { session: this.getSession() };\n      },\n    );\n\n    expect(channel).toBeDefined();\n  });\n\n  test(\"getViewByEl\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n\n    liveSocket.connect();\n\n    expect(liveSocket.getViewByEl(container(1)).destroy).toBeDefined();\n  });\n\n  test(\"destroyAllViews\", async () => {\n    const secondLiveView = document.createElement(\"div\");\n    secondLiveView.setAttribute(\"data-phx-session\", \"def456\");\n    secondLiveView.setAttribute(\"data-phx-root-id\", \"container1\");\n    secondLiveView.setAttribute(\"id\", \"container2\");\n    secondLiveView.innerHTML = `\n      <label for=\"plus\">Plus</label>\n      <input id=\"plus\" value=\"1\" />\n      <button phx-click=\"inc_temperature\">Inc Temperature</button>\n    `;\n    document.body.appendChild(secondLiveView);\n\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    liveSocket.connect();\n\n    const el = container(1);\n    expect(liveSocket.getViewByEl(el)).toBeDefined();\n\n    liveSocket.destroyAllViews();\n    expect(liveSocket.roots).toEqual({});\n\n    // Simulate a race condition which may attempt to\n    // destroy an element that no longer exists\n    liveSocket.destroyViewByEl(el);\n    expect(liveSocket.roots).toEqual({});\n  });\n\n  test(\"binding\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n\n    expect(liveSocket.binding(\"value\")).toBe(\"phx-value\");\n  });\n\n  test(\"getBindingPrefix\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n\n    expect(liveSocket.getBindingPrefix()).toEqual(\"phx-\");\n  });\n\n  test(\"getBindingPrefix custom\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket, {\n      bindingPrefix: \"company-\",\n    });\n\n    expect(liveSocket.getBindingPrefix()).toEqual(\"company-\");\n  });\n\n  test(\"owner\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    liveSocket.connect();\n\n    const _view = liveSocket.getViewByEl(container(1));\n    const btn = document.querySelector(\"button\");\n    const _callback = (view) => {\n      expect(view.id).toBe(view.id);\n    };\n    liveSocket.owner(btn, (view) => view.id);\n  });\n\n  test(\"getActiveElement default before LiveSocket activeElement is set\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n\n    const input = document.querySelector(\"input\")!;\n    input.focus();\n\n    expect(liveSocket.getActiveElement()).toEqual(input);\n  });\n\n  test(\"blurActiveElement\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n\n    const input = document.querySelector(\"input\")!;\n    input.focus();\n\n    expect(liveSocket.prevActive).toBeNull();\n\n    liveSocket.blurActiveElement();\n    // sets prevActive\n    expect(liveSocket.prevActive).toEqual(input);\n    expect(liveSocket.getActiveElement()).not.toEqual(input);\n  });\n\n  test(\"restorePreviouslyActiveFocus\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n\n    const input = document.querySelector(\"input\")!;\n    input.focus();\n\n    liveSocket.blurActiveElement();\n    expect(liveSocket.prevActive).toEqual(input);\n    expect(liveSocket.getActiveElement()).not.toEqual(input);\n\n    // focus()\n    liveSocket.restorePreviouslyActiveFocus();\n    expect(liveSocket.prevActive).toEqual(input);\n    expect(liveSocket.getActiveElement()).toEqual(input);\n    expect(document.activeElement).toEqual(input);\n  });\n\n  test(\"dropActiveElement unsets prevActive\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n\n    liveSocket.connect();\n\n    const input = document.querySelector(\"input\")!;\n    input.focus();\n    liveSocket.blurActiveElement();\n    expect(liveSocket.prevActive).toEqual(input);\n\n    const view = liveSocket.getViewByEl(container(1));\n    liveSocket.dropActiveElement(view);\n    expect(liveSocket.prevActive).toBeNull();\n    // this fails.  Is this correct?\n    // expect(liveSocket.getActiveElement()).not.toEqual(input)\n  });\n\n  test(\"storage can be overridden\", async () => {\n    let getItemCalls = 0;\n    const override = {\n      getItem: function (_keyName) {\n        getItemCalls = getItemCalls + 1;\n      },\n    };\n\n    liveSocket = new LiveSocket(\"/live\", Socket, {\n      sessionStorage: override,\n    });\n    liveSocket.getLatencySim();\n\n    // liveSocket constructor reads nav history position from sessionStorage\n    expect(getItemCalls).toEqual(2);\n  });\n\n  describe(\"execJS\", () => {\n    let view, liveSocket;\n\n    beforeEach(() => {\n      global.document.body.innerHTML = \"\";\n      prepareLiveViewDOM(global.document);\n      jest.useFakeTimers();\n\n      liveSocket = new LiveSocket(\"/live\", Socket);\n      view = simulateJoinedView(\n        document.getElementById(\"container1\"),\n        liveSocket,\n      );\n    });\n\n    afterEach(() => {\n      liveSocket && liveSocket.destroyAllViews();\n      liveSocket = null;\n      jest.useRealTimers();\n    });\n\n    afterAll(() => {\n      global.document.body.innerHTML = \"\";\n    });\n\n    test(\"accepts JSON-encoded command string\", () => {\n      const el = document.createElement(\"div\");\n      el.setAttribute(\"id\", \"test-exec\");\n      el.setAttribute(\n        \"data-test\",\n        '[[\"toggle_attr\", {\"attr\": [\"open\", \"true\"]}]]',\n      );\n      view.el.appendChild(el);\n\n      expect(el.getAttribute(\"open\")).toBeNull();\n      liveSocket.execJS(el, el.getAttribute(\"data-test\"));\n      jest.runAllTimers();\n      expect(el.getAttribute(\"open\")).toEqual(\"true\");\n    });\n\n    test(\"accepts command array\", () => {\n      const el = document.createElement(\"div\");\n      el.setAttribute(\"id\", \"test-exec-array\");\n      view.el.appendChild(el);\n\n      expect(el.getAttribute(\"open\")).toBeNull();\n      liveSocket.execJS(el, [[\"toggle_attr\", { attr: [\"open\", \"true\"] }]]);\n      jest.runAllTimers();\n      expect(el.getAttribute(\"open\")).toEqual(\"true\");\n    });\n  });\n});\n\ndescribe(\"liveSocket.js()\", () => {\n  let view, liveSocket, js;\n\n  beforeEach(() => {\n    global.document.body.innerHTML = \"\";\n    prepareLiveViewDOM(global.document);\n    jest.useFakeTimers();\n\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    view = simulateJoinedView(\n      document.getElementById(\"container1\"),\n      liveSocket,\n    );\n    js = liveSocket.js();\n  });\n\n  afterEach(() => {\n    liveSocket && liveSocket.destroyAllViews();\n    liveSocket = null;\n    jest.useRealTimers();\n  });\n\n  afterAll(() => {\n    global.document.body.innerHTML = \"\";\n  });\n\n  test(\"exec\", () => {\n    const el = document.createElement(\"div\");\n    el.setAttribute(\"id\", \"test-exec\");\n    el.setAttribute(\n      \"data-test\",\n      '[[\"toggle_attr\", {\"attr\": [\"open\", \"true\"]}]]',\n    );\n    view.el.appendChild(el);\n\n    expect(el.getAttribute(\"open\")).toBeNull();\n    js.exec(el, el.getAttribute(\"data-test\"));\n    jest.runAllTimers();\n    expect(el.getAttribute(\"open\")).toEqual(\"true\");\n  });\n\n  test(\"exec with command array\", () => {\n    const el = document.createElement(\"div\");\n    el.setAttribute(\"id\", \"test-exec-array\");\n    view.el.appendChild(el);\n\n    expect(el.getAttribute(\"open\")).toBeNull();\n    js.exec(el, [[\"toggle_attr\", { attr: [\"open\", \"true\"] }]]);\n    jest.runAllTimers();\n    expect(el.getAttribute(\"open\")).toEqual(\"true\");\n  });\n\n  test(\"show and hide\", (done) => {\n    const el = document.createElement(\"div\");\n    el.setAttribute(\"id\", \"test-visibility\");\n    view.el.appendChild(el);\n    simulateVisibility(el);\n\n    expect(el.style.display).toBe(\"\");\n    js.hide(el);\n    jest.runAllTimers();\n    expect(el.style.display).toBe(\"none\");\n\n    js.show(el);\n    jest.runAllTimers();\n    expect(el.style.display).toBe(\"block\");\n    done();\n  });\n\n  test(\"toggle\", (done) => {\n    const el = document.createElement(\"div\");\n    el.setAttribute(\"id\", \"test-toggle\");\n    view.el.appendChild(el);\n    simulateVisibility(el);\n\n    expect(el.style.display).toBe(\"\");\n    js.toggle(el);\n    jest.runAllTimers();\n    expect(el.style.display).toBe(\"none\");\n\n    js.toggle(el);\n    jest.runAllTimers();\n    expect(el.style.display).toBe(\"block\");\n    done();\n  });\n\n  test(\"addClass, removeClass and toggleClass\", (done) => {\n    const el = document.createElement(\"div\");\n    el.setAttribute(\"id\", \"test-classes\");\n    el.className = \"initial-class\";\n    view.el.appendChild(el);\n\n    js.addClass(el, \"test-class\");\n    jest.runAllTimers();\n    expect(el.classList.contains(\"test-class\")).toBe(true);\n    expect(el.classList.contains(\"initial-class\")).toBe(true);\n\n    js.addClass(el, [\"multiple\", \"classes\"]);\n    jest.runAllTimers();\n    expect(el.classList.contains(\"multiple\")).toBe(true);\n    expect(el.classList.contains(\"classes\")).toBe(true);\n\n    js.removeClass(el, \"test-class\");\n    jest.runAllTimers();\n    expect(el.classList.contains(\"test-class\")).toBe(false);\n    expect(el.classList.contains(\"initial-class\")).toBe(true);\n\n    js.removeClass(el, [\"multiple\", \"classes\"]);\n    jest.runAllTimers();\n    expect(el.classList.contains(\"multiple\")).toBe(false);\n    expect(el.classList.contains(\"classes\")).toBe(false);\n\n    js.toggleClass(el, \"toggle-class\");\n    jest.runAllTimers();\n    expect(el.classList.contains(\"toggle-class\")).toBe(true);\n\n    js.toggleClass(el, \"toggle-class\");\n    jest.runAllTimers();\n    expect(el.classList.contains(\"toggle-class\")).toBe(false);\n    done();\n  });\n\n  test(\"transition\", (done) => {\n    const el = document.createElement(\"div\");\n    el.setAttribute(\"id\", \"test-transition\");\n    view.el.appendChild(el);\n\n    js.transition(el, \"fade-in\");\n    jest.advanceTimersByTime(100);\n    expect(el.classList.contains(\"fade-in\")).toBe(true);\n\n    js.transition(el, [\"ease-out duration-300\", \"opacity-0\", \"opacity-100\"]);\n    jest.advanceTimersByTime(100);\n    expect(el.classList.contains(\"ease-out\")).toBe(true);\n    expect(el.classList.contains(\"duration-300\")).toBe(true);\n    expect(el.classList.contains(\"opacity-100\")).toBe(true);\n    done();\n  });\n\n  test(\"setAttribute, removeAttribute and toggleAttribute\", () => {\n    const el = document.createElement(\"div\");\n    el.setAttribute(\"id\", \"test-attributes\");\n    view.el.appendChild(el);\n\n    js.setAttribute(el, \"data-test\", \"value\");\n    expect(el.getAttribute(\"data-test\")).toBe(\"value\");\n\n    js.removeAttribute(el, \"data-test\");\n    expect(el.getAttribute(\"data-test\")).toBeNull();\n\n    js.toggleAttribute(el, \"aria-expanded\", \"true\", \"false\");\n    expect(el.getAttribute(\"aria-expanded\")).toBe(\"true\");\n\n    js.toggleAttribute(el, \"aria-expanded\", \"true\", \"false\");\n    expect(el.getAttribute(\"aria-expanded\")).toBe(\"false\");\n  });\n\n  test(\"push\", () => {\n    const el = document.createElement(\"div\");\n    el.setAttribute(\"id\", \"test-push\");\n    view.el.appendChild(el);\n\n    const originalWithinOwners = liveSocket.withinOwners;\n    liveSocket.withinOwners = (el, callback) => {\n      callback(view);\n    };\n\n    const originalExec = JS.exec;\n    JS.exec = jest.fn();\n\n    js.push(el, \"custom-event\", { value: { key: \"value\" } });\n\n    expect(JS.exec).toHaveBeenCalled();\n\n    liveSocket.withinOwners = originalWithinOwners;\n    JS.exec = originalExec;\n  });\n\n  test(\"navigate\", () => {\n    const originalHistoryRedirect = liveSocket.historyRedirect;\n    liveSocket.historyRedirect = jest.fn();\n\n    js.navigate(\"/test-url\");\n    expect(liveSocket.historyRedirect).toHaveBeenCalledWith(\n      expect.any(CustomEvent),\n      \"/test-url\",\n      \"push\",\n      null,\n      null,\n    );\n\n    js.navigate(\"/test-url\", { replace: true });\n    expect(liveSocket.historyRedirect).toHaveBeenCalledWith(\n      expect.any(CustomEvent),\n      \"/test-url\",\n      \"replace\",\n      null,\n      null,\n    );\n\n    liveSocket.historyRedirect = originalHistoryRedirect;\n  });\n\n  test(\"patch\", () => {\n    const originalPushHistoryPatch = liveSocket.pushHistoryPatch;\n    liveSocket.pushHistoryPatch = jest.fn();\n\n    js.patch(\"/test-url\");\n    expect(liveSocket.pushHistoryPatch).toHaveBeenCalledWith(\n      expect.any(CustomEvent),\n      \"/test-url\",\n      \"push\",\n      null,\n    );\n\n    js.patch(\"/test-url\", { replace: true });\n    expect(liveSocket.pushHistoryPatch).toHaveBeenCalledWith(\n      expect.any(CustomEvent),\n      \"/test-url\",\n      \"replace\",\n      null,\n    );\n\n    liveSocket.pushHistoryPatch = originalPushHistoryPatch;\n  });\n});\n"
  },
  {
    "path": "assets/test/modify_root_test.ts",
    "content": "import { modifyRoot } from \"phoenix_live_view/rendered\";\n\ndescribe(\"modifyRoot stripping comments\", () => {\n  test(\"starting comments\", () => {\n    // starting comments\n    const html = `\n    <!-- start -->\n    <!-- start2 -->\n    <div class=\"px-51\"><!-- MENU --><div id=\"menu\">MENU</div></div>\n    `;\n    const [strippedHTML, commentBefore, commentAfter] = modifyRoot(html, {});\n    expect(strippedHTML).toEqual(\n      '<div class=\"px-51\"><!-- MENU --><div id=\"menu\">MENU</div></div>',\n    );\n    expect(commentBefore).toEqual(`\n    <!-- start -->\n    <!-- start2 -->\n    `);\n    expect(commentAfter).toEqual(`\n    `);\n  });\n\n  test(\"ending comments\", () => {\n    const html = `\n    <div class=\"px-52\"><!-- MENU --><div id=\"menu\">MENU</div></div>\n    <!-- ending -->\n    `;\n    const [strippedHTML, commentBefore, commentAfter] = modifyRoot(html, {});\n    expect(strippedHTML).toEqual(\n      '<div class=\"px-52\"><!-- MENU --><div id=\"menu\">MENU</div></div>',\n    );\n    expect(commentBefore).toEqual(`\n    `);\n    expect(commentAfter).toEqual(`\n    <!-- ending -->\n    `);\n  });\n\n  test(\"starting and ending comments\", () => {\n    const html = `\n    <!-- starting -->\n    <div class=\"px-53\"><!-- MENU --><div id=\"menu\">MENU</div></div>\n    <!-- ending -->\n    `;\n    const [strippedHTML, commentBefore, commentAfter] = modifyRoot(html, {});\n    expect(strippedHTML).toEqual(\n      '<div class=\"px-53\"><!-- MENU --><div id=\"menu\">MENU</div></div>',\n    );\n    expect(commentBefore).toEqual(`\n    <!-- starting -->\n    `);\n    expect(commentAfter).toEqual(`\n    <!-- ending -->\n    `);\n  });\n\n  test(\"merges new attrs\", () => {\n    const html = `\n    <div class=\"px-5\"><div id=\"menu\">MENU</div></div>\n    `;\n    expect(modifyRoot(html, { id: 123 })[0]).toEqual(\n      '<div id=\"123\" class=\"px-5\"><div id=\"menu\">MENU</div></div>',\n    );\n    expect(modifyRoot(html, { id: 123, another: \"\" })[0]).toEqual(\n      '<div id=\"123\" another=\"\" class=\"px-5\"><div id=\"menu\">MENU</div></div>',\n    );\n    // clearing innerHTML\n    expect(modifyRoot(html, { id: 123, another: \"\" }, true)[0]).toEqual(\n      '<div id=\"123\" another=\"\"></div>',\n    );\n    // self closing\n    const selfClose = `\n    <input class=\"px-5\"/>\n    `;\n    expect(modifyRoot(selfClose, { id: 123, another: \"\" })[0]).toEqual(\n      '<input id=\"123\" another=\"\" class=\"px-5\"/>',\n    );\n  });\n\n  test(\"mixed whitespace\", () => {\n    const html = `\n    <div\n${\"\\t\"}class=\"px-5\"><div id=\"menu\">MENU</div></div>\n    `;\n    expect(modifyRoot(html, { id: 123 })[0]).toEqual(`<div id=\"123\"\n${\"\\t\"}class=\"px-5\"><div id=\"menu\">MENU</div></div>`);\n    expect(modifyRoot(html, { id: 123, another: \"\" })[0])\n      .toEqual(`<div id=\"123\" another=\"\"\n${\"\\t\"}class=\"px-5\"><div id=\"menu\">MENU</div></div>`);\n    // clearing innerHTML\n    expect(modifyRoot(html, { id: 123, another: \"\" }, true)[0]).toEqual(\n      '<div id=\"123\" another=\"\"></div>',\n    );\n  });\n\n  test(\"self closed\", () => {\n    let html = `<input${\"\\t\\r\\n\"}class=\"px-5\"/>`;\n    expect(modifyRoot(html, { id: 123, another: \"\" })[0]).toEqual(\n      `<input id=\"123\" another=\"\"${\"\\t\\r\\n\"}class=\"px-5\"/>`,\n    );\n\n    html = '<input class=\"text-sm\"/>';\n    expect(modifyRoot(html, { id: 123 })[0]).toEqual(\n      '<input id=\"123\" class=\"text-sm\"/>',\n    );\n\n    html = \"<img/>\";\n    expect(modifyRoot(html, { id: 123 })[0]).toEqual('<img id=\"123\"/>');\n\n    html = \"<img>\";\n    expect(modifyRoot(html, { id: 123 })[0]).toEqual('<img id=\"123\">');\n\n    html = '<!-- before --><!-- <> --><input class=\"text-sm\"/><!-- after -->';\n    let result = modifyRoot(html, { id: 123 });\n    expect(result[0]).toEqual('<input id=\"123\" class=\"text-sm\"/>');\n    expect(result[1]).toEqual(\"<!-- before --><!-- <> -->\");\n    expect(result[2]).toEqual(\"<!-- after -->\");\n\n    // unclosed self closed\n    html = '<img class=\"px-5\">';\n    expect(modifyRoot(html, { id: 123 })[0]).toEqual(\n      '<img id=\"123\" class=\"px-5\">',\n    );\n\n    html =\n      '<!-- <before> --><img class=\"px-5\"><!-- <after> --><!-- <after2> -->';\n    result = modifyRoot(html, { id: 123 });\n    expect(result[0]).toEqual('<img id=\"123\" class=\"px-5\">');\n    expect(result[1]).toEqual(\"<!-- <before> -->\");\n    expect(result[2]).toEqual(\"<!-- <after> --><!-- <after2> -->\");\n  });\n\n  test(\"does not extract id from inner element\", () => {\n    const html =\n      '<div>\\n  <div id=\"verify-payment-data-component\" data-phx-id=\"phx-F6AZf4FwSR4R50pB-39\" data-phx-skip></div>\\n</div>';\n    const attrs = {\n      \"data-phx-id\": \"c3-phx-F6AZf4FwSR4R50pB\",\n      \"data-phx-component\": 3,\n      \"data-phx-skip\": true,\n    };\n\n    const [strippedHTML, _commentBefore, _commentAfter] = modifyRoot(\n      html,\n      attrs,\n      true,\n    );\n\n    expect(strippedHTML).toEqual(\n      '<div data-phx-id=\"c3-phx-F6AZf4FwSR4R50pB\" data-phx-component=\"3\" data-phx-skip></div>',\n    );\n  });\n});\n"
  },
  {
    "path": "assets/test/rendered_test.ts",
    "content": "import Rendered from \"phoenix_live_view/rendered\";\nimport {\n  STATIC,\n  COMPONENTS,\n  KEYED,\n  KEYED_COUNT,\n  TEMPLATES,\n} from \"phoenix_live_view/constants\";\n\ndescribe(\"Rendered\", () => {\n  describe(\"mergeDiff\", () => {\n    test(\"recursively merges two diffs\", () => {\n      const simple = new Rendered(\"123\", simpleDiff1);\n      simple.mergeDiff(simpleDiff2);\n      expect(simple.get()).toEqual({\n        ...simpleDiffResult,\n        [COMPONENTS]: {},\n        newRender: true,\n      });\n\n      const deep = new Rendered(\"123\", deepDiff1);\n      deep.mergeDiff(deepDiff2);\n      expect(deep.get()).toEqual({ ...deepDiffResult, [COMPONENTS]: {} });\n    });\n\n    test(\"merges the latter diff if it contains a `static` key\", () => {\n      const diff1 = { 0: [\"a\"], 1: [\"b\"] };\n      const diff2 = { 0: [\"c\"], [STATIC]: [\"c\"] };\n      const rendered = new Rendered(\"123\", diff1);\n      rendered.mergeDiff(diff2);\n      expect(rendered.get()).toEqual({ ...diff2, [COMPONENTS]: {} });\n    });\n\n    test(\"merges the latter diff if it contains a `static` key even when nested\", () => {\n      const diff1 = { 0: { 0: [\"a\"], 1: [\"b\"] } };\n      const diff2 = { 0: { 0: [\"c\"], [STATIC]: [\"c\"] } };\n      const rendered = new Rendered(\"123\", diff1);\n      rendered.mergeDiff(diff2);\n      expect(rendered.get()).toEqual({ ...diff2, [COMPONENTS]: {} });\n    });\n\n    test(\"merges components considering links\", () => {\n      const diff1 = {};\n      const diff2 = {\n        [COMPONENTS]: { 1: { [STATIC]: [\"c\"] }, 2: { [STATIC]: 1 } },\n      };\n      const rendered = new Rendered(\"123\", diff1);\n      rendered.mergeDiff(diff2);\n      expect(rendered.get()).toEqual({\n        [COMPONENTS]: { 1: { [STATIC]: [\"c\"] }, 2: { [STATIC]: [\"c\"] } },\n      });\n    });\n\n    test(\"merges components considering old and new links\", () => {\n      const diff1 = { [COMPONENTS]: { 1: { [STATIC]: [\"old\"] } } };\n      const diff2 = {\n        [COMPONENTS]: {\n          1: { [STATIC]: [\"new\"] },\n          2: { newRender: true, [STATIC]: -1 },\n          3: { newRender: true, [STATIC]: 1 },\n        },\n      };\n      const rendered = new Rendered(\"123\", diff1);\n      rendered.mergeDiff(diff2);\n      expect(rendered.get()).toEqual({\n        [COMPONENTS]: {\n          1: { [STATIC]: [\"new\"] },\n          2: { [STATIC]: [\"old\"] },\n          3: { [STATIC]: [\"new\"] },\n        },\n      });\n    });\n\n    test(\"merges components whole tree considering old and new links\", () => {\n      const diff1 = {\n        [COMPONENTS]: { 1: { 0: { [STATIC]: [\"nested\"] }, [STATIC]: [\"old\"] } },\n      };\n\n      const diff2 = {\n        [COMPONENTS]: {\n          1: { 0: { [STATIC]: [\"nested\"] }, [STATIC]: [\"new\"] },\n          2: { 0: { [STATIC]: [\"replaced\"] }, [STATIC]: -1 },\n          3: { 0: { [STATIC]: [\"replaced\"] }, [STATIC]: 1 },\n          4: { [STATIC]: -1 },\n          5: { [STATIC]: 1 },\n        },\n      };\n\n      const rendered1 = new Rendered(\"123\", diff1);\n      rendered1.mergeDiff(diff2);\n      expect(rendered1.get()).toEqual({\n        [COMPONENTS]: {\n          1: { 0: { [STATIC]: [\"nested\"] }, [STATIC]: [\"new\"] },\n          2: { 0: { [STATIC]: [\"replaced\"] }, [STATIC]: [\"old\"] },\n          3: { 0: { [STATIC]: [\"replaced\"] }, [STATIC]: [\"new\"] },\n          4: { 0: { [STATIC]: [\"nested\"] }, [STATIC]: [\"old\"] },\n          5: { 0: { [STATIC]: [\"nested\"] }, [STATIC]: [\"new\"] },\n        },\n      });\n\n      const diff3 = {\n        [COMPONENTS]: {\n          1: { 0: { [STATIC]: [\"newRender\"] }, [STATIC]: [\"new\"] },\n          2: { 0: { [STATIC]: [\"replaced\"] }, [STATIC]: -1 },\n          3: { 0: { [STATIC]: [\"replaced\"] }, [STATIC]: 1 },\n          4: { [STATIC]: -1 },\n          5: { [STATIC]: 1 },\n        },\n      };\n\n      const rendered2 = new Rendered(\"123\", diff1);\n      rendered2.mergeDiff(diff3);\n      expect(rendered2.get()).toEqual({\n        [COMPONENTS]: {\n          1: { 0: { [STATIC]: [\"newRender\"] }, [STATIC]: [\"new\"] },\n          2: { 0: { [STATIC]: [\"replaced\"] }, [STATIC]: [\"old\"] },\n          3: { 0: { [STATIC]: [\"replaced\"] }, [STATIC]: [\"new\"] },\n          4: { 0: { [STATIC]: [\"nested\"] }, [STATIC]: [\"old\"] },\n          5: { 0: { [STATIC]: [\"newRender\"] }, [STATIC]: [\"new\"] },\n        },\n      });\n    });\n\n    test(\"replaces a string when a map is returned\", () => {\n      const diff1 = { 0: { 0: \"<button>Press Me</button>\", [STATIC]: \"\" } };\n      const diff2 = { 0: { 0: { 0: \"val\", [STATIC]: \"\" }, [STATIC]: \"\" } };\n      const rendered = new Rendered(\"123\", diff1);\n      rendered.mergeDiff(diff2);\n      expect(rendered.get()).toEqual({ ...diff2, [COMPONENTS]: {} });\n    });\n\n    test(\"replaces a map when a string is returned\", () => {\n      const diff1 = { 0: { 0: { 0: \"val\", [STATIC]: \"\" }, [STATIC]: \"\" } };\n      const diff2 = { 0: { 0: \"<button>Press Me</button>\", [STATIC]: \"\" } };\n      const rendered = new Rendered(\"123\", diff1);\n      rendered.mergeDiff(diff2);\n      expect(rendered.get()).toEqual({ ...diff2, [COMPONENTS]: {} });\n    });\n\n    test(\"expands shared static from cids\", () => {\n      const mountDiff = {\n        \"0\": \"\",\n        \"1\": \"\",\n        \"2\": {\n          \"0\": \"new post\",\n          \"1\": \"\",\n          \"2\": {\n            d: [[1], [2]],\n            s: [\"\", \"\"],\n          },\n          s: [\"h1\", \"h2\", \"h3\", \"h4\"],\n        },\n        c: {\n          \"1\": {\n            \"0\": \"1008\",\n            \"1\": \"chris_mccord\",\n            \"2\": \"My post\",\n            \"3\": \"1\",\n            \"4\": \"0\",\n            \"5\": \"1\",\n            \"6\": \"0\",\n            \"7\": \"edit\",\n            \"8\": \"delete\",\n            s: [\"s0\", \"s1\", \"s2\", \"s3\", \"s4\", \"s5\", \"s6\", \"s7\", \"s8\", \"s9\"],\n          },\n          \"2\": {\n            \"0\": \"1007\",\n            \"1\": \"chris_mccord\",\n            \"2\": \"My post\",\n            \"3\": \"2\",\n            \"4\": \"0\",\n            \"5\": \"2\",\n            \"6\": \"0\",\n            \"7\": \"edit\",\n            \"8\": \"delete\",\n            s: 1,\n          },\n        },\n        s: [\"f1\", \"f2\", \"f3\", \"f4\"],\n        title: \"Listing Posts\",\n      };\n\n      const updateDiff = {\n        \"2\": {\n          \"2\": {\n            d: [[3]],\n          },\n        },\n        c: {\n          \"3\": {\n            \"0\": \"1009\",\n            \"1\": \"chris_mccord\",\n            \"2\": \"newnewnewnewnewnewnewnew\",\n            \"3\": \"3\",\n            \"4\": \"0\",\n            \"5\": \"3\",\n            \"6\": \"0\",\n            \"7\": \"edit\",\n            \"8\": \"delete\",\n            s: -2,\n          },\n        },\n      };\n\n      const rendered = new Rendered(\"123\", mountDiff);\n      expect(rendered.getComponent(rendered.get(), 1)[STATIC]).toEqual(\n        rendered.getComponent(rendered.get(), 2)[STATIC],\n      );\n      rendered.mergeDiff(updateDiff);\n      const sharedStatic = rendered.getComponent(rendered.get(), 1)[STATIC];\n\n      expect(sharedStatic).toBeTruthy();\n      expect(sharedStatic).toEqual(\n        rendered.getComponent(rendered.get(), 2)[STATIC],\n      );\n      expect(sharedStatic).toEqual(\n        rendered.getComponent(rendered.get(), 3)[STATIC],\n      );\n    });\n  });\n\n  describe(\"isNewFingerprint\", () => {\n    test(\"returns true if `diff.static` is truthy\", () => {\n      const diff = { [STATIC]: [\"<h2>\"] };\n      const rendered = new Rendered(\"123\", {});\n      expect(rendered.isNewFingerprint(diff)).toEqual(true);\n    });\n\n    test(\"returns false if `diff.static` is falsy\", () => {\n      const diff = { [STATIC]: undefined };\n      const rendered = new Rendered(\"123\", {});\n      expect(rendered.isNewFingerprint(diff)).toEqual(false);\n    });\n\n    test(\"returns false if `diff` is undefined\", () => {\n      const rendered = new Rendered(\"123\", {});\n      expect(rendered.isNewFingerprint()).toEqual(false);\n    });\n  });\n\n  describe(\"toString\", () => {\n    test(\"stringifies a diff\", () => {\n      const rendered = new Rendered(\"123\", simpleDiffResult);\n      const { buffer: str } = rendered.toString();\n      expect(str.trim()).toEqual(\n        `<div data-phx-id=\"m1-123\" class=\"thermostat\">\n  <div class=\"bar cooling\">\n    <a href=\"#\" phx-click=\"toggle-mode\">cooling</a>\n    <span>07:15:04 PM</span>\n  </div>\n</div>`.trim(),\n      );\n    });\n\n    test(\"reuses static in components and comprehensions\", () => {\n      const rendered = new Rendered(\"123\", staticReuseDiff);\n      const { buffer: str } = rendered.toString();\n      expect(str.trim()).toEqual(\n        `<div data-phx-id=\"m1-123\">\n  <p>\n    foo\n    <span>0: <b data-phx-id=\"c1-123\" data-phx-component=\"1\" data-phx-view=\"123\">FROM index_1 world</b></span><span>1: <b data-phx-id=\"c2-123\" data-phx-component=\"2\" data-phx-view=\"123\">FROM index_2 world</b></span>\n  </p>\n\n  <p>\n    bar\n    <span>0: <b data-phx-id=\"c3-123\" data-phx-component=\"3\" data-phx-view=\"123\">FROM index_1 world</b></span><span>1: <b data-phx-id=\"c4-123\" data-phx-component=\"4\" data-phx-view=\"123\">FROM index_2 world</b></span>\n  </p>\n</div>`.trim(),\n      );\n    });\n  });\n});\n\nconst simpleDiff1 = {\n  \"0\": \"cooling\",\n  \"1\": \"cooling\",\n  \"2\": \"07:15:03 PM\",\n  [STATIC]: [\n    '<div class=\"thermostat\">\\n  <div class=\"bar ',\n    '\">\\n    <a href=\"#\" phx-click=\"toggle-mode\">',\n    \"</a>\\n    <span>\",\n    \"</span>\\n  </div>\\n</div>\\n\",\n  ],\n  r: 1,\n};\n\nconst simpleDiff2 = {\n  \"2\": \"07:15:04 PM\",\n};\n\nconst simpleDiffResult = {\n  \"0\": \"cooling\",\n  \"1\": \"cooling\",\n  \"2\": \"07:15:04 PM\",\n  [STATIC]: [\n    '<div class=\"thermostat\">\\n  <div class=\"bar ',\n    '\">\\n    <a href=\"#\" phx-click=\"toggle-mode\">',\n    \"</a>\\n    <span>\",\n    \"</span>\\n  </div>\\n</div>\\n\",\n  ],\n  r: 1,\n};\n\nconst deepDiff1 = {\n  \"0\": {\n    \"0\": {\n      [KEYED]: {\n        0: { 0: \"user1058\", 1: \"1\" },\n        1: { 0: \"user99\", 1: \"1\" },\n        [KEYED_COUNT]: 2,\n      },\n      [STATIC]: [\n        \"        <tr>\\n          <td>\",\n        \" (\",\n        \")</td>\\n        </tr>\\n\",\n      ],\n      r: 1,\n    },\n    [STATIC]: [\n      \"  <table>\\n    <thead>\\n      <tr>\\n        <th>Username</th>\\n        <th></th>\\n      </tr>\\n    </thead>\\n    <tbody>\\n\",\n      \"    </tbody>\\n  </table>\\n\",\n    ],\n    r: 1,\n  },\n  \"1\": {\n    [KEYED]: {\n      0: {\n        0: \"asdf_asdf\",\n        1: \"asdf@asdf.com\",\n        2: \"123-456-7890\",\n        3: '<a href=\"/users/1\">Show</a>',\n        4: '<a href=\"/users/1/edit\">Edit</a>',\n        5: '<a href=\"#\" phx-click=\"delete_user\" phx-value=\"1\">Delete</a>',\n      },\n      [KEYED_COUNT]: 1,\n    },\n    [STATIC]: [\n      \"    <tr>\\n      <td>\",\n      \"</td>\\n      <td>\",\n      \"</td>\\n      <td>\",\n      \"</td>\\n\\n      <td>\\n\",\n      \"        \",\n      \"\\n\",\n      \"      </td>\\n    </tr>\\n\",\n    ],\n    r: 1,\n  },\n};\n\nconst deepDiff2 = {\n  \"0\": {\n    \"0\": {\n      [KEYED]: { 0: { 0: \"user1058\", 1: \"2\" }, [KEYED_COUNT]: 1 },\n    },\n  },\n};\n\nconst deepDiffResult = {\n  \"0\": {\n    \"0\": {\n      newRender: true,\n      [KEYED]: {\n        0: { 0: \"user1058\", 1: \"2\" },\n        [KEYED_COUNT]: 1,\n      },\n      [STATIC]: [\n        \"        <tr>\\n          <td>\",\n        \" (\",\n        \")</td>\\n        </tr>\\n\",\n      ],\n      r: 1,\n    },\n    [STATIC]: [\n      \"  <table>\\n    <thead>\\n      <tr>\\n        <th>Username</th>\\n        <th></th>\\n      </tr>\\n    </thead>\\n    <tbody>\\n\",\n      \"    </tbody>\\n  </table>\\n\",\n    ],\n    newRender: true,\n    r: 1,\n  },\n  \"1\": {\n    [KEYED]: {\n      0: {\n        0: \"asdf_asdf\",\n        1: \"asdf@asdf.com\",\n        2: \"123-456-7890\",\n        3: '<a href=\"/users/1\">Show</a>',\n        4: '<a href=\"/users/1/edit\">Edit</a>',\n        5: '<a href=\"#\" phx-click=\"delete_user\" phx-value=\"1\">Delete</a>',\n      },\n      [KEYED_COUNT]: 1,\n    },\n    [STATIC]: [\n      \"    <tr>\\n      <td>\",\n      \"</td>\\n      <td>\",\n      \"</td>\\n      <td>\",\n      \"</td>\\n\\n      <td>\\n\",\n      \"        \",\n      \"\\n\",\n      \"      </td>\\n    </tr>\\n\",\n    ],\n    r: 1,\n  },\n};\n\nconst staticReuseDiff = {\n  \"0\": {\n    [KEYED]: {\n      [KEYED_COUNT]: 2,\n      0: {\n        0: \"foo\",\n        1: {\n          [KEYED]: {\n            [KEYED_COUNT]: 2,\n            0: { 0: \"0\", 1: 1 },\n            1: { 0: \"1\", 1: 2 },\n          },\n          [STATIC]: 0,\n        },\n      },\n      1: {\n        0: \"bar\",\n        1: {\n          [KEYED]: {\n            [KEYED_COUNT]: 2,\n            0: { 0: \"0\", 1: 3 },\n            1: { 0: \"1\", 1: 4 },\n          },\n          [STATIC]: 0,\n        },\n      },\n    },\n    [STATIC]: [\"\\n  <p>\\n    \", \"\\n    \", \"\\n  </p>\\n\"],\n    r: 1,\n    [TEMPLATES]: { \"0\": [\"<span>\", \": \", \"</span>\"] },\n  },\n  [COMPONENTS]: {\n    \"1\": {\n      \"0\": \"index_1\",\n      \"1\": \"world\",\n      [STATIC]: [\"<b>FROM \", \" \", \"</b>\"],\n      r: 1,\n    },\n    \"2\": { \"0\": \"index_2\", \"1\": \"world\", [STATIC]: 1, r: 1 },\n    \"3\": { \"0\": \"index_1\", \"1\": \"world\", [STATIC]: 1, r: 1 },\n    \"4\": { \"0\": \"index_2\", \"1\": \"world\", [STATIC]: 3, r: 1 },\n  },\n  [STATIC]: [\"<div>\", \"</div>\"],\n  r: 1,\n};\n"
  },
  {
    "path": "assets/test/test_helpers.ts",
    "content": "import View from \"phoenix_live_view/view\";\nimport { version as liveview_version } from \"../../package.json\";\n\nexport const appendTitle = (opts, innerHTML?: string) => {\n  Array.from(document.head.querySelectorAll(\"title\")).forEach((el) =>\n    el.remove(),\n  );\n  const title = document.createElement(\"title\");\n  const { prefix, suffix, default: defaultTitle } = opts;\n  if (prefix) {\n    title.setAttribute(\"data-prefix\", prefix);\n  }\n  if (suffix) {\n    title.setAttribute(\"data-suffix\", suffix);\n  }\n  if (defaultTitle) {\n    title.setAttribute(\"data-default\", defaultTitle);\n  } else {\n    title.removeAttribute(\"data-default\");\n  }\n  if (innerHTML) {\n    title.innerHTML = innerHTML;\n  }\n  document.head.appendChild(title);\n};\n\nexport const rootContainer = (content) => {\n  const div = tag(\"div\", { id: \"root\" }, content);\n  document.body.appendChild(div);\n  return div;\n};\n\nexport const tag = (tagName, attrs, innerHTML) => {\n  const el = document.createElement(tagName);\n  el.innerHTML = innerHTML;\n  for (const key in attrs) {\n    el.setAttribute(key, attrs[key]);\n  }\n  return el;\n};\n\nexport const simulateJoinedView = (el, liveSocket) => {\n  const view = new View(el, liveSocket);\n  stubChannel(view);\n  liveSocket.roots[view.id] = view;\n  view.isConnected = () => true;\n  view.onJoin({ rendered: { s: [el.innerHTML] }, liveview_version });\n  return view;\n};\n\nexport const simulateVisibility = (el) => {\n  el.getClientRects = () => {\n    const style = window.getComputedStyle(el);\n    const visible = !(style.opacity === \"0\" || style.display === \"none\");\n    return visible ? { length: 1 } : { length: 0 };\n  };\n  return el;\n};\n\nexport const stubChannel = (view: View) => {\n  const fakePush = {\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n    receives: [] as [string, Function][],\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n    receive(kind: string, cb: Function) {\n      this.receives.push([kind, cb]);\n      return this;\n    },\n  };\n  view.channel.push = () => fakePush;\n  view.channel.leave = () => ({\n    receive(kind, cb) {\n      if (kind === \"ok\") {\n        cb();\n      }\n      return this;\n    },\n  });\n};\n\nexport function liveViewDOM(content?: string) {\n  const div = document.createElement(\"div\");\n  div.setAttribute(\"data-phx-view\", \"User.Form\");\n  div.setAttribute(\"data-phx-session\", \"abc123\");\n  div.setAttribute(\"data-phx-main\", \"\");\n  div.setAttribute(\"id\", \"container\");\n  div.setAttribute(\"class\", \"user-implemented-class\");\n  div.innerHTML =\n    content ||\n    `\n    <form id=\"my-form\">\n      <label for=\"plus\">Plus</label>\n      <input id=\"plus\" value=\"1\" name=\"increment\" />\n      <textarea id=\"note\" name=\"note\">2</textarea>\n      <input type=\"checkbox\" phx-click=\"toggle_me\" />\n      <button phx-click=\"inc_temperature\">Inc Temperature</button>\n      <div\n        id=\"status\"\n        phx-disconnected='[[\"show\",{\"display\":null,\"time\":200,\"to\":null,\"transition\":[[],[],[]]}]]'\n        phx-connected='[[\"hide\",{\"time\":200,\"to\":null,\"transition\":[[],[],[]]}]]'\n        style=\"display:  none;\"\n      >\n        disconnected!\n      </div>\n    </form>\n  `;\n  document.body.innerHTML = \"\";\n  document.body.appendChild(div);\n  return div;\n}\n"
  },
  {
    "path": "assets/test/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"noEmit\": true,\n    \"allowJs\": true,\n    \"checkJs\": false,\n    \"resolveJsonModule\": true,\n    \"baseUrl\": \".\",\n    \"strict\": true,\n    \"noImplicitAny\": false,\n    \"paths\": {\n      \"phoenix_live_view\": [\"../js/phoenix_live_view/index.ts\"],\n      \"phoenix_live_view*\": [\"../js/phoenix_live_view/*\"]\n    }\n  },\n  \"include\": [\"./**/*\"],\n  \"exclude\": []\n}\n"
  },
  {
    "path": "assets/test/utils_test.ts",
    "content": "import { Socket } from \"phoenix\";\nimport { closestPhxBinding } from \"phoenix_live_view/utils\";\nimport LiveSocket from \"phoenix_live_view/live_socket\";\nimport { simulateJoinedView, liveViewDOM } from \"./test_helpers\";\n\nconst setupView = (content) => {\n  const el = liveViewDOM(content);\n  global.document.body.appendChild(el);\n  const liveSocket = new LiveSocket(\"/live\", Socket);\n  return simulateJoinedView(el, liveSocket);\n};\n\ndescribe(\"utils\", () => {\n  describe(\"closestPhxBinding\", () => {\n    test(\"if an element's parent has a phx-click binding and is not disabled, return the parent\", () => {\n      const _view = setupView(`\n      <button id=\"button\" phx-click=\"toggle\">\n        <span id=\"innerContent\">This is a button</span>\n      </button>\n      `);\n      const element = global.document.querySelector(\"#innerContent\");\n      const parent = global.document.querySelector(\"#button\");\n      expect(closestPhxBinding(element, \"phx-click\")).toBe(parent);\n    });\n\n    test(\"if an element's parent is disabled, return null\", () => {\n      const _view = setupView(`\n      <button id=\"button\" phx-click=\"toggle\" disabled>\n        <span id=\"innerContent\">This is a button</span>\n      </button>\n      `);\n      const element = global.document.querySelector(\"#innerContent\");\n      expect(closestPhxBinding(element, \"phx-click\")).toBe(null);\n    });\n  });\n});\n"
  },
  {
    "path": "assets/test/view_test.ts",
    "content": "import { Socket } from \"phoenix\";\nimport { createHook } from \"phoenix_live_view/index\";\nimport LiveSocket from \"phoenix_live_view/live_socket\";\nimport DOM from \"phoenix_live_view/dom\";\nimport View from \"phoenix_live_view/view\";\nimport ViewHook, { HooksOptions } from \"phoenix_live_view/view_hook\";\n\nimport { version as liveview_version } from \"../../package.json\";\n\nimport {\n  PHX_LOADING_CLASS,\n  PHX_ERROR_CLASS,\n  PHX_SERVER_ERROR_CLASS,\n  PHX_HAS_FOCUSED,\n} from \"phoenix_live_view/constants\";\n\nimport {\n  tag,\n  simulateJoinedView,\n  stubChannel,\n  rootContainer,\n  liveViewDOM,\n  simulateVisibility,\n  appendTitle,\n} from \"./test_helpers\";\n\nconst simulateUsedInput = (input) => {\n  DOM.putPrivate(input, PHX_HAS_FOCUSED, true);\n};\n\ndescribe(\"View + DOM\", function () {\n  let liveSocket;\n\n  beforeEach(() => {\n    submitBefore = HTMLFormElement.prototype.submit;\n    global.Phoenix = { Socket };\n    global.document.body.innerHTML = liveViewDOM().outerHTML;\n  });\n\n  afterEach(() => {\n    liveSocket && liveSocket.destroyAllViews();\n    liveSocket = null;\n  });\n\n  afterAll(() => {\n    global.document.body.innerHTML = \"\";\n  });\n\n  test(\"update\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n    const updateDiff = {\n      s: [\"<h2>\", \"</h2>\"],\n      fingerprint: 123,\n    };\n\n    const view = simulateJoinedView(el, liveSocket);\n    view.update(updateDiff, []);\n\n    expect(view.el.firstChild.tagName).toBe(\"H2\");\n    expect(view.rendered!.get()).toEqual(updateDiff);\n  });\n\n  test(\"applyDiff with empty title uses default if present\", async () => {\n    appendTitle({}, \"Foo\");\n\n    const titleEl = document.querySelector(\"title\")!;\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n    const updateDiff = {\n      s: [\"<h2>\", \"</h2>\"],\n      fingerprint: 123,\n      t: \"\",\n    };\n\n    const view = simulateJoinedView(el, liveSocket);\n    view.applyDiff(\"mount\", updateDiff, ({ diff, events }) =>\n      view.update(diff, events),\n    );\n\n    expect(view.el.firstChild.tagName).toBe(\"H2\");\n    expect(view.rendered!.get()).toEqual(updateDiff);\n\n    await new Promise(requestAnimationFrame);\n    expect(document.title).toBe(\"Foo\");\n    titleEl.setAttribute(\"data-default\", \"DEFAULT\");\n    view.applyDiff(\"mount\", updateDiff, ({ diff, events }) =>\n      view.update(diff, events),\n    );\n    await new Promise(requestAnimationFrame);\n    expect(document.title).toBe(\"DEFAULT\");\n  });\n\n  test(\"applyDiff with empty title does not use default for non-main views\", async () => {\n    appendTitle({}, \"Foo\");\n\n    const titleEl = document.querySelector(\"title\")!;\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n    const updateDiff = {\n      s: [\"<h2>\", \"</h2>\"],\n      fingerprint: 123,\n      t: \"\",\n    };\n\n    const view = simulateJoinedView(el, liveSocket);\n    view.el.removeAttribute(\"data-phx-main\");\n    view.applyDiff(\"mount\", updateDiff, ({ diff, events }) =>\n      view.update(diff, events),\n    );\n\n    expect(view.el.firstChild.tagName).toBe(\"H2\");\n    expect(view.rendered!.get()).toEqual(updateDiff);\n\n    await new Promise(requestAnimationFrame);\n    expect(document.title).toBe(\"Foo\");\n    titleEl.setAttribute(\"data-default\", \"DEFAULT\");\n    view.applyDiff(\"mount\", updateDiff, ({ diff, events }) =>\n      view.update(diff, events),\n    );\n    await new Promise(requestAnimationFrame);\n    expect(document.title).toBe(\"Foo\");\n  });\n\n  test(\"pushWithReply\", function () {\n    expect.assertions(1);\n\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n\n    const view = simulateJoinedView(el, liveSocket);\n    const channelStub = {\n      leave() {\n        return {\n          receive(_status, _cb) {\n            return this;\n          },\n        };\n      },\n      push(_evt, payload, _timeout) {\n        expect(payload.value).toBe(\"increment=1\");\n        return {\n          receive() {\n            return this;\n          },\n        };\n      },\n    };\n    view.channel = channelStub;\n\n    view.pushWithReply(\n      null,\n      { target: el.querySelector(\"form\") },\n      { value: \"increment=1\" },\n    );\n  });\n\n  test(\"pushWithReply with update\", function () {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n\n    const view = simulateJoinedView(el, liveSocket);\n    const channelStub = {\n      leave() {\n        return {\n          receive(_status, _cb) {\n            return this;\n          },\n        };\n      },\n      push(_evt, payload, _timeout) {\n        expect(payload.value).toBe(\"increment=1\");\n        return {\n          receive(_status, cb) {\n            const diff = {\n              s: [\"<h2>\", \"</h2>\"],\n              fingerprint: 123,\n            };\n            cb(diff);\n            return this;\n          },\n        };\n      },\n    };\n    view.channel = channelStub;\n\n    view.pushWithReply(\n      null,\n      { target: el.querySelector(\"form\") },\n      { value: \"increment=1\" },\n    );\n\n    expect(view.el.querySelector(\"form\")).toBeTruthy();\n  });\n\n  test(\"pushEvent\", function () {\n    expect.assertions(3);\n\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n    const input = el.querySelector(\"input\");\n\n    const view = simulateJoinedView(el, liveSocket);\n    const channelStub = {\n      leave() {\n        return {\n          receive(_status, _cb) {\n            return this;\n          },\n        };\n      },\n      push(_evt, payload, _timeout) {\n        expect(payload.type).toBe(\"keyup\");\n        expect(payload.event).toBeDefined();\n        expect(payload.value).toEqual({ value: \"1\" });\n        return {\n          receive() {\n            return this;\n          },\n        };\n      },\n    };\n    view.channel = channelStub;\n\n    view.pushEvent(\"keyup\", input, el, \"click\", {});\n  });\n\n  test(\"pushEvent as checkbox not checked\", function () {\n    expect.assertions(1);\n\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n    const input = el.querySelector('input[type=\"checkbox\"]');\n\n    const view = simulateJoinedView(el, liveSocket);\n    const channelStub = {\n      leave() {\n        return {\n          receive(_status, _cb) {\n            return this;\n          },\n        };\n      },\n      push(_evt, payload, _timeout) {\n        expect(payload.value).toEqual({});\n        return {\n          receive() {\n            return this;\n          },\n        };\n      },\n    };\n    view.channel = channelStub;\n\n    view.pushEvent(\"click\", input, el, \"toggle_me\", {});\n  });\n\n  test(\"pushEvent as checkbox when checked\", function () {\n    expect.assertions(1);\n\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n    const input: HTMLInputElement = el.querySelector('input[type=\"checkbox\"]')!;\n    const view = simulateJoinedView(el, liveSocket);\n\n    input.checked = true;\n\n    const channelStub = {\n      leave() {\n        return {\n          receive(_status, _cb) {\n            return this;\n          },\n        };\n      },\n      push(_evt, payload, _timeout) {\n        expect(payload.value).toEqual({ value: \"on\" });\n        return {\n          receive() {\n            return this;\n          },\n        };\n      },\n    };\n    view.channel = channelStub;\n\n    view.pushEvent(\"click\", input, el, \"toggle_me\", {});\n  });\n\n  test(\"pushEvent as checkbox with value\", function () {\n    expect.assertions(1);\n\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n    const input: HTMLInputElement = el.querySelector('input[type=\"checkbox\"]')!;\n    const view = simulateJoinedView(el, liveSocket);\n\n    input.value = \"1\";\n    input.checked = true;\n\n    const channelStub = {\n      leave() {\n        return {\n          receive(_status, _cb) {\n            return this;\n          },\n        };\n      },\n      push(_evt, payload, _timeout) {\n        expect(payload.value).toEqual({ value: \"1\" });\n        return {\n          receive() {\n            return this;\n          },\n        };\n      },\n    };\n    view.channel = channelStub;\n\n    view.pushEvent(\"click\", input, el, \"toggle_me\", {});\n  });\n\n  test(\"pushInput\", function () {\n    expect.assertions(4);\n\n    const liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n    const input = el.querySelector(\"input\")!;\n    simulateUsedInput(input);\n    const view = simulateJoinedView(el, liveSocket);\n    const channelStub = {\n      leave() {\n        return {\n          receive(_status, _cb) {\n            return this;\n          },\n        };\n      },\n      push(_evt, payload, _timeout) {\n        expect(payload.type).toBe(\"form\");\n        expect(payload.event).toBeDefined();\n        expect(payload.value).toBe(\"increment=1&_unused_note=&note=2\");\n        expect(payload.meta).toEqual({ _target: \"increment\" });\n        return {\n          receive() {\n            return this;\n          },\n        };\n      },\n    };\n    view.channel = channelStub;\n\n    view.pushInput(input, el, null, \"validate\", { _target: input.name });\n  });\n\n  test(\"pushInput with with phx-value and JS command value\", function () {\n    expect.assertions(4);\n\n    const liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM(`\n      <form id=\"my-form\" phx-value-attribute_value=\"attribute\">\n        <label for=\"plus\">Plus</label>\n        <input id=\"plus\" value=\"1\" name=\"increment\" />\n        <textarea id=\"note\" name=\"note\">2</textarea>\n        <input type=\"checkbox\" phx-click=\"toggle_me\" />\n        <button phx-click=\"inc_temperature\">Inc Temperature</button>\n      </form>\n    `);\n    const input = el.querySelector(\"input\")!;\n    simulateUsedInput(input);\n    const view = simulateJoinedView(el, liveSocket);\n    const channelStub = {\n      push(_evt, payload, _timeout) {\n        expect(payload.type).toBe(\"form\");\n        expect(payload.event).toBeDefined();\n        expect(payload.value).toBe(\"increment=1&_unused_note=&note=2\");\n        expect(payload.meta).toEqual({\n          _target: \"increment\",\n          attribute_value: \"attribute\",\n          nested: {\n            command_value: \"command\",\n            array: [1, 2],\n          },\n        });\n        return {\n          receive() {\n            return this;\n          },\n        };\n      },\n    };\n    view.channel = channelStub;\n    const optValue = { nested: { command_value: \"command\", array: [1, 2] } };\n    view.pushInput(input, el, null, \"validate\", {\n      _target: input.name,\n      value: optValue,\n    });\n  });\n\n  test(\"pushInput with nameless input\", function () {\n    expect.assertions(4);\n\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n    const input = el.querySelector(\"input\")!;\n    input.removeAttribute(\"name\");\n    simulateUsedInput(input);\n    const view = simulateJoinedView(el, liveSocket);\n    const channelStub = {\n      leave() {\n        return {\n          receive(_status, _cb) {\n            return this;\n          },\n        };\n      },\n      push(_evt, payload, _timeout) {\n        expect(payload.type).toBe(\"form\");\n        expect(payload.event).toBeDefined();\n        expect(payload.value).toBe(\"_unused_note=&note=2\");\n        expect(payload.meta).toEqual({ _target: \"undefined\" });\n        return {\n          receive() {\n            return this;\n          },\n        };\n      },\n    };\n    view.channel = channelStub;\n\n    view.pushInput(input, el, null, \"validate\", { _target: input.name });\n  });\n\n  test(\"getFormsForRecovery\", function () {\n    let view, html;\n    liveSocket = new LiveSocket(\"/live\", Socket);\n\n    html = '<form id=\"my-form\" phx-change=\"cg\"><input name=\"foo\"></form>';\n    view = new View(liveViewDOM(html), liveSocket);\n    expect(view.joinCount).toBe(0);\n    expect(Object.keys(view.getFormsForRecovery()).length).toBe(0);\n\n    view.joinCount++;\n    expect(Object.keys(view.getFormsForRecovery()).length).toBe(1);\n\n    view.joinCount++;\n    expect(Object.keys(view.getFormsForRecovery()).length).toBe(1);\n\n    html =\n      '<form phx-change=\"cg\" phx-auto-recover=\"ignore\"><input name=\"foo\"></form>';\n    view = new View(liveViewDOM(html), liveSocket);\n    view.joinCount = 2;\n    expect(Object.keys(view.getFormsForRecovery()).length).toBe(0);\n\n    html = '<form><input name=\"foo\"></form>';\n    view = new View(liveViewDOM(), liveSocket);\n    view.joinCount = 2;\n    expect(Object.keys(view.getFormsForRecovery()).length).toBe(0);\n\n    html = '<form phx-change=\"cg\"></form>';\n    view = new View(liveViewDOM(html), liveSocket);\n    view.joinCount = 2;\n    expect(Object.keys(view.getFormsForRecovery()).length).toBe(0);\n\n    html =\n      '<form id=\\'my-form\\' phx-change=\\'[[\"push\",{\"event\":\"update\",\"target\":1}]]\\'><input name=\"foo\" /></form>';\n    view = new View(liveViewDOM(html), liveSocket);\n    view.joinCount = 1;\n    const newForms = view.getFormsForRecovery();\n    expect(Object.keys(newForms).length).toBe(1);\n    expect(newForms[\"my-form\"].getAttribute(\"phx-change\")).toBe(\n      '[[\"push\",{\"event\":\"update\",\"target\":1}]]',\n    );\n  });\n\n  describe(\"submitForm\", function () {\n    test(\"submits payload\", function () {\n      expect.assertions(3);\n\n      const liveSocket = new LiveSocket(\"/live\", Socket);\n      const el = liveViewDOM();\n      const form = el.querySelector(\"form\");\n\n      const view = simulateJoinedView(el, liveSocket);\n      const channelStub = {\n        push(_evt, payload, _timeout) {\n          expect(payload.type).toBe(\"form\");\n          expect(payload.event).toBeDefined();\n          expect(payload.value).toBe(\"increment=1&note=2\");\n          return {\n            receive() {\n              return this;\n            },\n          };\n        },\n      };\n      view.channel = channelStub;\n      view.submitForm(form, form, { target: form });\n    });\n\n    test(\"payload includes phx-value and JS command value\", function () {\n      expect.assertions(4);\n\n      const liveSocket = new LiveSocket(\"/live\", Socket);\n      const el = liveViewDOM(`\n        <form id=\"my-form\" phx-value-attribute_value=\"attribute\">\n          <label for=\"plus\">Plus</label>\n          <input id=\"plus\" value=\"1\" name=\"increment\" />\n          <textarea id=\"note\" name=\"note\">2</textarea>\n          <input type=\"checkbox\" phx-click=\"toggle_me\" />\n          <button phx-click=\"inc_temperature\">Inc Temperature</button>\n        </form>\n      `);\n      const form = el.querySelector(\"form\");\n\n      const view = simulateJoinedView(el, liveSocket);\n      const channelStub = {\n        push(_evt, payload, _timeout) {\n          expect(payload.type).toBe(\"form\");\n          expect(payload.event).toBeDefined();\n          expect(payload.value).toBe(\"increment=1&note=2\");\n          expect(payload.meta).toEqual({\n            attribute_value: \"attribute\",\n            nested: {\n              command_value: \"command\",\n              array: [1, 2],\n            },\n          });\n          return {\n            receive() {\n              return this;\n            },\n          };\n        },\n      };\n      view.channel = channelStub;\n      const opts = {\n        value: { nested: { command_value: \"command\", array: [1, 2] } },\n      };\n      view.submitForm(form, form, { target: form }, undefined, opts);\n    });\n\n    test(\"payload includes submitter when name is provided\", function () {\n      const btn = document.createElement(\"button\");\n      btn.setAttribute(\"type\", \"submit\");\n      btn.setAttribute(\"name\", \"btnName\");\n      btn.setAttribute(\"value\", \"btnValue\");\n      submitWithButton(btn, \"increment=1&note=2&btnName=btnValue\");\n    });\n\n    test(\"payload includes submitter when name is provided (submitter outside form)\", function () {\n      const btn = document.createElement(\"button\");\n      btn.setAttribute(\"form\", \"my-form\");\n      btn.setAttribute(\"type\", \"submit\");\n      btn.setAttribute(\"name\", \"btnName\");\n      btn.setAttribute(\"value\", \"btnValue\");\n      submitWithButton(\n        btn,\n        \"increment=1&note=2&btnName=btnValue\",\n        document.body,\n      );\n    });\n\n    test(\"payload does not include submitter when name is not provided\", function () {\n      const btn = document.createElement(\"button\");\n      btn.setAttribute(\"type\", \"submit\");\n      btn.setAttribute(\"value\", \"btnValue\");\n      submitWithButton(btn, \"increment=1&note=2\");\n    });\n\n    function submitWithButton(\n      btn,\n      queryString,\n      appendTo?: HTMLElement,\n      opts = {},\n    ) {\n      const liveSocket = new LiveSocket(\"/live\", Socket);\n      const el = liveViewDOM();\n      const form = el.querySelector(\"form\")!;\n      if (appendTo) {\n        appendTo.appendChild(btn);\n      } else {\n        form.appendChild(btn);\n      }\n\n      const view = simulateJoinedView(el, liveSocket);\n      const channelStub = {\n        push(_evt, payload, _timeout) {\n          expect(payload.type).toBe(\"form\");\n          expect(payload.event).toBeDefined();\n          expect(payload.value).toBe(queryString);\n          return {\n            receive() {\n              return this;\n            },\n          };\n        },\n      };\n\n      view.channel = channelStub;\n      view.submitForm(form, form, { target: form }, btn, opts);\n    }\n\n    test(\"disables elements after submission\", function () {\n      const liveSocket = new LiveSocket(\"/live\", Socket);\n      const el = liveViewDOM();\n      const form = el.querySelector(\"form\")!;\n\n      const view = simulateJoinedView(el, liveSocket);\n      stubChannel(view);\n\n      view.submitForm(form, form, { target: form });\n      expect(DOM.private(form, \"phx-has-submitted\")).toBeTruthy();\n      Array.from(form.elements).forEach((input) => {\n        expect(DOM.private(input, \"phx-has-submitted\")).toBeTruthy();\n      });\n      expect(form.classList.contains(\"phx-submit-loading\")).toBeTruthy();\n      expect(form.querySelector(\"button\")!.dataset.phxDisabled).toBeTruthy();\n      expect(form.querySelector(\"input\")!.dataset.phxReadonly).toBeTruthy();\n      expect(form.querySelector(\"textarea\")!.dataset.phxReadonly).toBeTruthy();\n    });\n\n    test(\"disables elements outside form\", function () {\n      const liveSocket = new LiveSocket(\"/live\", Socket);\n      const el = liveViewDOM(`\n      <form id=\"my-form\">\n      </form>\n      <label for=\"plus\">Plus</label>\n      <input id=\"plus\" value=\"1\" name=\"increment\" form=\"my-form\"/>\n      <textarea id=\"note\" name=\"note\" form=\"my-form\">2</textarea>\n      <input type=\"checkbox\" phx-click=\"toggle_me\" form=\"my-form\"/>\n      <button phx-click=\"inc_temperature\" form=\"my-form\">Inc Temperature</button>\n      `);\n      const form = el.querySelector(\"form\")!;\n\n      const view = simulateJoinedView(el, liveSocket);\n      stubChannel(view);\n\n      view.submitForm(form, form, { target: form });\n      expect(DOM.private(form, \"phx-has-submitted\")).toBeTruthy();\n      expect(form.classList.contains(\"phx-submit-loading\")).toBeTruthy();\n      expect(el.querySelector(\"button\")!.dataset.phxDisabled).toBeTruthy();\n      expect(el.querySelector(\"input\")!.dataset.phxReadonly).toBeTruthy();\n      expect(el.querySelector(\"textarea\")!.dataset.phxReadonly).toBeTruthy();\n    });\n\n    test(\"disables elements\", function () {\n      const liveSocket = new LiveSocket(\"/live\", Socket);\n      const el = liveViewDOM(`\n      <button phx-click=\"inc\" phx-disable-with>+</button>\n      `);\n      const button = el.querySelector(\"button\")!;\n\n      const view = simulateJoinedView(el, liveSocket);\n      stubChannel(view);\n\n      expect(button.disabled).toEqual(false);\n      view.pushEvent(\"click\", button, el, \"inc\", {});\n      expect(button.disabled).toEqual(true);\n    });\n  });\n\n  describe(\"phx-trigger-action\", () => {\n    test(\"triggers external submit on updated DOM el\", (done) => {\n      const liveSocket = new LiveSocket(\"/live\", Socket);\n      const el = liveViewDOM();\n      const view = simulateJoinedView(el, liveSocket);\n      const html =\n        '<form id=\"form\" phx-submit=\"submit\"><input type=\"text\"></form>';\n\n      stubChannel(view);\n      view.onJoin({\n        rendered: { s: [html], fingerprint: 123 },\n        liveview_version,\n      });\n      expect(view.el.innerHTML).toBe(html);\n\n      const formEl = document.getElementById(\"form\");\n      Object.getPrototypeOf(formEl).submit = done;\n      const updatedHtml =\n        '<form id=\"form\" phx-submit=\"submit\" phx-trigger-action><input type=\"text\"></form>';\n      view.update({ s: [updatedHtml] }, []);\n\n      expect(liveSocket.socket.closeWasClean).toBe(true);\n      expect(view.el.innerHTML).toBe(\n        '<form id=\"form\" phx-submit=\"submit\" phx-trigger-action=\"\"><input type=\"text\"></form>',\n      );\n    });\n\n    test(\"triggers external submit on added DOM el\", (done) => {\n      const liveSocket = new LiveSocket(\"/live\", Socket);\n      const el = liveViewDOM();\n      const view = simulateJoinedView(el, liveSocket);\n      const html = \"<div>not a form</div>\";\n      HTMLFormElement.prototype.submit = done;\n\n      stubChannel(view);\n      view.onJoin({\n        rendered: { s: [html], fingerprint: 123 },\n        liveview_version,\n      });\n      expect(view.el.innerHTML).toBe(html);\n\n      const updatedHtml =\n        '<form id=\"form\" phx-submit=\"submit\" phx-trigger-action><input type=\"text\"></form>';\n      view.update({ s: [updatedHtml] }, []);\n\n      expect(liveSocket.socket.closeWasClean).toBe(true);\n      expect(view.el.innerHTML).toBe(\n        '<form id=\"form\" phx-submit=\"submit\" phx-trigger-action=\"\"><input type=\"text\"></form>',\n      );\n    });\n  });\n\n  describe(\"phx-update\", function () {\n    const childIds = () =>\n      Array.from(document.getElementById(\"list\")!.children).map((child) =>\n        parseInt(child.id),\n      );\n    const countChildNodes = () =>\n      document.getElementById(\"list\")!.childNodes.length;\n\n    const createView = (updateType, initialEntries) => {\n      const liveSocket = new LiveSocket(\"/live\", Socket);\n      const el = liveViewDOM();\n      const view = simulateJoinedView(el, liveSocket);\n\n      stubChannel(view);\n\n      const joinDiff = {\n        \"0\": { k: initialEntries, s: ['\\n<div id=\"', '\">', \"</div>\\n\"] },\n        s: [`<div id=\"list\" phx-update=\"${updateType}\">`, \"</div>\"],\n      };\n\n      view.onJoin({ rendered: joinDiff, liveview_version });\n\n      return view;\n    };\n\n    const updateEntries = (view, entries) => {\n      const updateDiff = {\n        \"0\": {\n          k: entries,\n        },\n      };\n\n      view.update(updateDiff, []);\n    };\n\n    test(\"replace\", async () => {\n      const view = createView(\"replace\", { 0: { 0: \"1\", 1: \"1\" }, kc: 1 });\n      expect(childIds()).toEqual([1]);\n\n      updateEntries(view, {\n        0: { 0: \"2\", 1: \"2\" },\n        1: { 0: \"3\", 1: \"3\" },\n        kc: 2,\n      });\n      expect(childIds()).toEqual([2, 3]);\n    });\n\n    test(\"append\", async () => {\n      const view = createView(\"append\", { 0: { 0: \"1\", 1: \"1\" }, kc: 1 });\n      expect(childIds()).toEqual([1]);\n\n      // Append two elements\n      updateEntries(view, {\n        0: { 0: \"2\", 1: \"2\" },\n        1: { 0: \"3\", 1: \"3\" },\n        kc: 2,\n      });\n      expect(childIds()).toEqual([1, 2, 3]);\n\n      // Update the last element\n      updateEntries(view, {\n        0: { 0: \"3\", 1: \"3\" },\n        kc: 1,\n      });\n      expect(childIds()).toEqual([1, 2, 3]);\n\n      // Update the first element\n      updateEntries(view, {\n        0: { 0: \"1\", 1: \"1\" },\n        kc: 1,\n      });\n      expect(childIds()).toEqual([1, 2, 3]);\n\n      // Update before new elements\n      updateEntries(view, {\n        0: { 0: \"4\", 1: \"4\" },\n        1: { 0: \"5\", 1: \"5\" },\n        kc: 2,\n      });\n      expect(childIds()).toEqual([1, 2, 3, 4, 5]);\n\n      // Update after new elements\n      updateEntries(view, {\n        0: { 0: \"6\", 1: \"6\" },\n        1: { 0: \"7\", 1: \"7\" },\n        2: { 0: \"5\", 1: \"modified\" },\n        kc: 3,\n      });\n      expect(childIds()).toEqual([1, 2, 3, 4, 5, 6, 7]);\n\n      // Sandwich an update between two new elements\n      updateEntries(view, {\n        0: { 0: \"8\", 1: \"8\" },\n        1: { 0: \"7\", 1: \"modified\" },\n        2: { 0: \"9\", 1: \"9\" },\n        kc: 3,\n      });\n      expect(childIds()).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);\n\n      // Update all elements in reverse order\n      updateEntries(view, {\n        0: { 0: \"9\", 1: \"9\" },\n        1: { 0: \"8\", 1: \"8\" },\n        2: { 0: \"7\", 1: \"7\" },\n        3: { 0: \"6\", 1: \"6\" },\n        4: { 0: \"5\", 1: \"5\" },\n        5: { 0: \"4\", 1: \"4\" },\n        6: { 0: \"3\", 1: \"3\" },\n        7: { 0: \"2\", 1: \"2\" },\n        8: { 0: \"1\", 1: \"1\" },\n        kc: 9,\n      });\n      expect(childIds()).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);\n\n      // Make sure we don't have a memory leak when doing updates\n      const initialCount = countChildNodes();\n      updateEntries(view, {\n        0: { 0: \"1\", 1: \"1\" },\n        1: { 0: \"2\", 1: \"2\" },\n        2: { 0: \"3\", 1: \"3\" },\n        kc: 3,\n      });\n      updateEntries(view, {\n        0: { 0: \"1\", 1: \"1\" },\n        1: { 0: \"2\", 1: \"2\" },\n        2: { 0: \"3\", 1: \"3\" },\n        kc: 3,\n      });\n      updateEntries(view, {\n        0: { 0: \"1\", 1: \"1\" },\n        1: { 0: \"2\", 1: \"2\" },\n        2: { 0: \"3\", 1: \"3\" },\n        kc: 3,\n      });\n\n      expect(countChildNodes()).toBe(initialCount);\n    });\n\n    test(\"prepend\", async () => {\n      const view = createView(\"prepend\", { 0: { 0: \"1\", 1: \"1\" }, kc: 1 });\n      expect(childIds()).toEqual([1]);\n\n      // Append two elements\n      updateEntries(view, {\n        0: { 0: \"2\", 1: \"2\" },\n        1: { 0: \"3\", 1: \"3\" },\n        kc: 2,\n      });\n      expect(childIds()).toEqual([2, 3, 1]);\n\n      // Update the last element\n      updateEntries(view, {\n        0: { 0: \"3\", 1: \"3\" },\n        kc: 1,\n      });\n      expect(childIds()).toEqual([2, 3, 1]);\n\n      // Update the first element\n      updateEntries(view, {\n        0: { 0: \"1\", 1: \"1\" },\n        kc: 1,\n      });\n      expect(childIds()).toEqual([2, 3, 1]);\n\n      // Update before new elements\n      updateEntries(view, {\n        0: { 0: \"4\", 1: \"4\" },\n        1: { 0: \"5\", 1: \"5\" },\n        kc: 2,\n      });\n      expect(childIds()).toEqual([4, 5, 2, 3, 1]);\n\n      // Update after new elements\n      updateEntries(view, {\n        0: { 0: \"6\", 1: \"6\" },\n        1: { 0: \"7\", 1: \"7\" },\n        2: { 0: \"5\", 1: \"modified\" },\n        kc: 3,\n      });\n      expect(childIds()).toEqual([6, 7, 4, 5, 2, 3, 1]);\n\n      // Sandwich an update between two new elements\n      updateEntries(view, {\n        0: { 0: \"8\", 1: \"8\" },\n        1: { 0: \"7\", 1: \"modified\" },\n        2: { 0: \"9\", 1: \"9\" },\n        kc: 3,\n      });\n      expect(childIds()).toEqual([8, 9, 6, 7, 4, 5, 2, 3, 1]);\n\n      // Update all elements in reverse order\n      updateEntries(view, {\n        0: { 0: \"9\", 1: \"9\" },\n        1: { 0: \"8\", 1: \"8\" },\n        2: { 0: \"7\", 1: \"7\" },\n        3: { 0: \"6\", 1: \"6\" },\n        4: { 0: \"5\", 1: \"5\" },\n        5: { 0: \"4\", 1: \"4\" },\n        6: { 0: \"3\", 1: \"3\" },\n        7: { 0: \"2\", 1: \"2\" },\n        8: { 0: \"1\", 1: \"1\" },\n        kc: 9,\n      });\n      expect(childIds()).toEqual([8, 9, 6, 7, 4, 5, 2, 3, 1]);\n\n      // Make sure we don't have a memory leak when doing updates\n      const initialCount = countChildNodes();\n      updateEntries(view, {\n        0: { 0: \"1\", 1: \"1\" },\n        1: { 0: \"2\", 1: \"2\" },\n        2: { 0: \"3\", 1: \"3\" },\n        kc: 3,\n      });\n      updateEntries(view, {\n        0: { 0: \"1\", 1: \"1\" },\n        1: { 0: \"2\", 1: \"2\" },\n        2: { 0: \"3\", 1: \"3\" },\n        kc: 3,\n      });\n      updateEntries(view, {\n        0: { 0: \"1\", 1: \"1\" },\n        1: { 0: \"2\", 1: \"2\" },\n        2: { 0: \"3\", 1: \"3\" },\n        kc: 3,\n      });\n      updateEntries(view, {\n        0: { 0: \"1\", 1: \"1\" },\n        1: { 0: \"2\", 1: \"2\" },\n        2: { 0: \"3\", 1: \"3\" },\n        kc: 3,\n      });\n\n      expect(countChildNodes()).toBe(initialCount);\n    });\n\n    test(\"ignore\", async () => {\n      const view = createView(\"ignore\", { 0: { 0: \"1\", 1: \"1\" }, kc: 1 });\n      expect(childIds()).toEqual([1]);\n\n      // Append two elements\n      updateEntries(view, {\n        0: { 0: \"2\", 1: \"2\" },\n        1: { 0: \"3\", 1: \"3\" },\n        kc: 2,\n      });\n      expect(childIds()).toEqual([1]);\n    });\n  });\n\n  describe(\"JS integration\", () => {\n    test(\"ignore_attributes skips attributes on update\", () => {\n      let liveSocket = new LiveSocket(\"/live\", Socket);\n      let el = liveViewDOM();\n      let updateDiff = {\n        \"0\": ' phx-mounted=\"[[&quot;ignore_attrs&quot;,{&quot;attrs&quot;:[&quot;open&quot;]}]]\"',\n        \"1\": \"0\",\n        s: [\n          \"<details\",\n          \">\\n    <summary>A</summary>\\n    <span>\",\n          \"</span></details>\",\n        ],\n      };\n\n      let view = simulateJoinedView(el, liveSocket);\n      view.applyDiff(\"update\", updateDiff, ({ diff, events }) =>\n        view.update(diff, events),\n      );\n\n      expect(view.el.firstChild.tagName).toBe(\"DETAILS\");\n      expect(view.el.firstChild.open).toBe(false);\n      view.el.firstChild.open = true;\n      view.el.firstChild.setAttribute(\"data-foo\", \"bar\");\n\n      // now update, the HTML patch would normally reset the open attribute\n      view.applyDiff(\"update\", { \"1\": \"1\" }, ({ diff, events }) =>\n        view.update(diff, events),\n      );\n      // open is ignored, so it is kept as is\n      expect(view.el.firstChild.open).toBe(true);\n      // foo is not ignored, so it is reset\n      expect(view.el.firstChild.getAttribute(\"data-foo\")).toBe(null);\n      expect(view.el.firstChild.textContent.replace(/\\s+/g, \"\")).toEqual(\"A1\");\n    });\n\n    test(\"ignore_attributes skips boolean attributes on update when not set\", () => {\n      let liveSocket = new LiveSocket(\"/live\", Socket);\n      let el = liveViewDOM();\n      let updateDiff = {\n        \"0\": ' phx-mounted=\"[[&quot;ignore_attrs&quot;,{&quot;attrs&quot;:[&quot;open&quot;]}]]\"',\n        \"1\": \"0\",\n        s: [\n          \"<details open\",\n          \">\\n    <summary>A</summary>\\n    <span>\",\n          \"</span></details>\",\n        ],\n      };\n\n      let view = simulateJoinedView(el, liveSocket);\n      view.applyDiff(\"update\", updateDiff, ({ diff, events }) =>\n        view.update(diff, events),\n      );\n\n      expect(view.el.firstChild.tagName).toBe(\"DETAILS\");\n      expect(view.el.firstChild.open).toBe(true);\n      view.el.firstChild.open = false;\n      view.el.firstChild.setAttribute(\"data-foo\", \"bar\");\n\n      // now update, the HTML patch would normally reset the open attribute\n      view.applyDiff(\"update\", { \"1\": \"1\" }, ({ diff, events }) =>\n        view.update(diff, events),\n      );\n      // open is ignored, so it is kept as is\n      expect(view.el.firstChild.open).toBe(false);\n      // foo is not ignored, so it is reset\n      expect(view.el.firstChild.getAttribute(\"data-foo\")).toBe(null);\n      expect(view.el.firstChild.textContent.replace(/\\s+/g, \"\")).toEqual(\"A1\");\n    });\n\n    test(\"ignore_attributes wildcard\", () => {\n      let liveSocket = new LiveSocket(\"/live\", Socket);\n      let el = liveViewDOM();\n      let updateDiff = {\n        \"0\": ' phx-mounted=\"[[&quot;ignore_attrs&quot;,{&quot;attrs&quot;:[&quot;open&quot;,&quot;data-*&quot;]}]]\"',\n        \"1\": ' data-foo=\"foo\" data-bar=\"bar\"',\n        \"2\": \"0\",\n        s: [\n          \"<details\",\n          \"\",\n          \">\\n    <summary>A</summary>\\n    <span>\",\n          \"</span></details>\",\n        ],\n      };\n\n      let view = simulateJoinedView(el, liveSocket);\n      view.applyDiff(\"update\", updateDiff, ({ diff, events }) =>\n        view.update(diff, events),\n      );\n\n      expect(view.el.firstChild.tagName).toBe(\"DETAILS\");\n      expect(view.el.firstChild.open).toBe(false);\n      view.el.firstChild.open = true;\n      view.el.firstChild.setAttribute(\"data-foo\", \"bar\");\n      view.el.firstChild.setAttribute(\"data-other\", \"also kept\");\n      // apply diff\n      view.applyDiff(\n        \"update\",\n        { \"1\": 'data-foo=\"foo\" data-bar=\"bar\" data-new=\"new\"', \"2\": \"1\" },\n        ({ diff, events }) => view.update(diff, events),\n      );\n      expect(view.el.firstChild.open).toBe(true);\n      expect(view.el.firstChild.getAttribute(\"data-foo\")).toBe(\"bar\");\n      expect(view.el.firstChild.getAttribute(\"data-bar\")).toBe(\"bar\");\n      expect(view.el.firstChild.getAttribute(\"data-other\")).toBe(\"also kept\");\n      expect(view.el.firstChild.textContent.replace(/\\s+/g, \"\")).toEqual(\"A1\");\n\n      // Not added for being ignored\n      expect(view.el.firstChild.getAttribute(\"data-new\")).toBe(null);\n    });\n\n    test(\"ignore_attributes *\", () => {\n      let liveSocket = new LiveSocket(\"/live\", Socket);\n      let el = liveViewDOM();\n      let updateDiff = {\n        \"0\": ' phx-mounted=\"[[&quot;ignore_attrs&quot;,{&quot;attrs&quot;:[&quot;open&quot;,&quot;*&quot;]}]]\"',\n        \"1\": ' data-foo=\"foo\" data-bar=\"bar\"',\n        \"2\": \"0\",\n        s: [\n          \"<details\",\n          \"\",\n          \">\\n    <summary>A</summary>\\n    <span>\",\n          \"</span></details>\",\n        ],\n      };\n\n      let view = simulateJoinedView(el, liveSocket);\n      view.applyDiff(\"update\", updateDiff, ({ diff, events }) =>\n        view.update(diff, events),\n      );\n\n      expect(view.el.firstChild.tagName).toBe(\"DETAILS\");\n      expect(view.el.firstChild.open).toBe(false);\n      view.el.firstChild.open = true;\n      view.el.firstChild.setAttribute(\"data-foo\", \"bar\");\n      view.el.firstChild.setAttribute(\"data-other\", \"also kept\");\n      view.el.firstChild.setAttribute(\"something\", \"else\");\n      // apply diff\n      view.applyDiff(\n        \"update\",\n        { \"1\": 'data-foo=\"foo\" data-bar=\"bar\" data-new=\"new\"', \"2\": \"1\" },\n        ({ diff, events }) => view.update(diff, events),\n      );\n      expect(view.el.firstChild.open).toBe(true);\n      expect(view.el.firstChild.getAttribute(\"data-foo\")).toBe(\"bar\");\n      expect(view.el.firstChild.getAttribute(\"data-bar\")).toBe(\"bar\");\n      expect(view.el.firstChild.getAttribute(\"something\")).toBe(\"else\");\n      expect(view.el.firstChild.getAttribute(\"data-other\")).toBe(\"also kept\");\n      expect(view.el.firstChild.textContent.replace(/\\s+/g, \"\")).toEqual(\"A1\");\n\n      // Not added for being ignored\n      expect(view.el.firstChild.getAttribute(\"data-new\")).toBe(null);\n    });\n  });\n});\n\nlet submitBefore;\ndescribe(\"View\", function () {\n  let liveSocket;\n\n  beforeEach(() => {\n    submitBefore = HTMLFormElement.prototype.submit;\n    global.Phoenix = { Socket };\n    global.document.body.innerHTML = liveViewDOM().outerHTML;\n  });\n\n  afterEach(() => {\n    liveSocket && liveSocket.destroyAllViews();\n    liveSocket = null;\n    HTMLFormElement.prototype.submit = submitBefore;\n    jest.useRealTimers();\n  });\n\n  afterAll(() => {\n    global.document.body.innerHTML = \"\";\n  });\n\n  test(\"sets defaults\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n    const view = simulateJoinedView(el, liveSocket);\n    expect(view.liveSocket).toBe(liveSocket);\n    expect(view.parent).toBeUndefined();\n    expect(view.el).toBe(el);\n    expect(view.id).toEqual(\"container\");\n    expect(view.getSession).toBeDefined();\n    expect(view.channel).toBeDefined();\n    expect(view.loaderTimer).toBeDefined();\n  });\n\n  test(\"binding\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n    const view = simulateJoinedView(el, liveSocket);\n    expect(view.binding(\"submit\")).toEqual(\"phx-submit\");\n  });\n\n  test(\"getSession\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n    const view = simulateJoinedView(el, liveSocket);\n    expect(view.getSession()).toEqual(\"abc123\");\n  });\n\n  test(\"getStatic\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n    let view = simulateJoinedView(el, liveSocket);\n    expect(view.getStatic()).toEqual(null);\n    view.destroy();\n\n    el.setAttribute(\"data-phx-static\", \"foo\");\n    view = simulateJoinedView(el, liveSocket);\n    expect(view.getStatic()).toEqual(\"foo\");\n  });\n\n  test(\"showLoader and hideLoader\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = document.querySelector(\"[data-phx-session]\")!;\n\n    const view = simulateJoinedView(el, liveSocket);\n    view.showLoader();\n    expect(el.classList.contains(\"phx-loading\")).toBeTruthy();\n    expect(el.classList.contains(\"phx-connected\")).toBeFalsy();\n    expect(el.classList.contains(\"user-implemented-class\")).toBeTruthy();\n\n    view.hideLoader();\n    expect(el.classList.contains(\"phx-loading\")).toBeFalsy();\n    expect(el.classList.contains(\"phx-connected\")).toBeTruthy();\n  });\n\n  test(\"displayError and hideLoader\", (done) => {\n    jest.useFakeTimers();\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const loader = document.createElement(\"span\");\n    const phxView = document.querySelector(\"[data-phx-session]\")!;\n    phxView.parentNode!.insertBefore(loader, phxView.nextSibling);\n    const el = document.querySelector(\"[data-phx-session]\")!;\n    const status: HTMLElement = el.querySelector(\"#status\")!;\n\n    const view = simulateJoinedView(el, liveSocket);\n\n    expect(status.style.display).toBe(\"none\");\n    view.displayError([\n      PHX_LOADING_CLASS,\n      PHX_ERROR_CLASS,\n      PHX_SERVER_ERROR_CLASS,\n    ]);\n    expect(el.classList.contains(\"phx-loading\")).toBeTruthy();\n    expect(el.classList.contains(\"phx-error\")).toBeTruthy();\n    expect(el.classList.contains(\"phx-connected\")).toBeFalsy();\n    expect(el.classList.contains(\"user-implemented-class\")).toBeTruthy();\n    jest.runAllTimers();\n    expect(status.style.display).toBe(\"block\");\n    simulateVisibility(status);\n    view.hideLoader();\n    jest.runAllTimers();\n    expect(status.style.display).toBe(\"none\");\n    done();\n  });\n\n  test(\"join\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n    const _view = simulateJoinedView(el, liveSocket);\n\n    // view.join()\n    // still need a few tests\n  });\n\n  test(\"sends _track_static and _mounts on params\", () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n    const view = new View(el, liveSocket);\n    stubChannel(view);\n\n    expect(view.channel.params()).toEqual({\n      flash: undefined,\n      params: { _mounts: 0, _mount_attempts: 0, _live_referer: undefined },\n      session: \"abc123\",\n      static: null,\n      url: undefined,\n      redirect: undefined,\n      sticky: false,\n    });\n\n    el.innerHTML +=\n      '<link rel=\"stylesheet\" href=\"/css/app-123.css?vsn=d\" phx-track-static=\"\">';\n    el.innerHTML += '<link rel=\"stylesheet\" href=\"/css/nontracked.css\">';\n    el.innerHTML += '<img src=\"/img/tracked.png\" phx-track-static>';\n    el.innerHTML += '<img src=\"/img/untracked.png\">';\n\n    expect(view.channel.params()).toEqual({\n      flash: undefined,\n      session: \"abc123\",\n      static: null,\n      url: undefined,\n      redirect: undefined,\n      params: {\n        _mounts: 0,\n        _mount_attempts: 1,\n        _live_referer: undefined,\n        _track_static: [\n          \"http://localhost/css/app-123.css?vsn=d\",\n          \"http://localhost/img/tracked.png\",\n        ],\n      },\n      sticky: false,\n    });\n  });\n});\n\ndescribe(\"View Hooks\", function () {\n  let liveSocket;\n\n  beforeEach(() => {\n    global.document.body.innerHTML = liveViewDOM().outerHTML;\n  });\n\n  afterEach(() => {\n    liveSocket && liveSocket.destroyAllViews();\n    liveSocket = null;\n  });\n\n  afterAll(() => {\n    global.document.body.innerHTML = \"\";\n  });\n\n  test(\"phx-mounted\", (done) => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n\n    const html =\n      '<h2 id=\"test\" phx-mounted=\"[[&quot;add_class&quot;,{&quot;names&quot;:[&quot;new-class&quot;],&quot;time&quot;:200,&quot;to&quot;:null,&quot;transition&quot;:[[],[],[]]}]]\">test mounted</h2>';\n    el.innerHTML = html;\n\n    const view = simulateJoinedView(el, liveSocket);\n\n    view.onJoin({\n      rendered: {\n        s: [html],\n        fingerprint: 123,\n      },\n      liveview_version,\n    });\n    window.requestAnimationFrame(() => {\n      expect(document.getElementById(\"test\")!.getAttribute(\"class\")).toBe(\n        \"new-class\",\n      );\n      view.update(\n        {\n          s: [\n            html +\n              '<h2 id=\"test2\" phx-mounted=\"[[&quot;add_class&quot;,{&quot;names&quot;:[&quot;new-class2&quot;],&quot;time&quot;:200,&quot;to&quot;:null,&quot;transition&quot;:[[],[],[]]}]]\">test mounted</h2>',\n          ],\n          fingerprint: 123,\n        },\n        [],\n      );\n      window.requestAnimationFrame(() => {\n        expect(document.getElementById(\"test\")!.getAttribute(\"class\")).toBe(\n          \"new-class\",\n        );\n        expect(document.getElementById(\"test2\")!.getAttribute(\"class\")).toBe(\n          \"new-class2\",\n        );\n        done();\n      });\n    });\n  });\n\n  test(\"hooks\", async () => {\n    let upcaseWasDestroyed = false;\n    let upcaseBeforeUpdate = false;\n    let hookLiveSocket;\n    const Hooks = <HooksOptions>{\n      Upcase: {\n        mounted() {\n          hookLiveSocket = this.liveSocket;\n          this.el.innerHTML = this.el.innerHTML.toUpperCase();\n        },\n        beforeUpdate() {\n          upcaseBeforeUpdate = true;\n        },\n        updated() {\n          this.el.innerHTML = this.el.innerHTML + \" updated\";\n        },\n        disconnected() {\n          this.el.innerHTML = \"disconnected\";\n        },\n        reconnected() {\n          this.el.innerHTML = \"connected\";\n        },\n        destroyed() {\n          upcaseWasDestroyed = true;\n        },\n      },\n    };\n    liveSocket = new LiveSocket(\"/live\", Socket, { hooks: Hooks });\n    const el = liveViewDOM();\n\n    const view = simulateJoinedView(el, liveSocket);\n\n    view.onJoin({\n      rendered: {\n        s: ['<h2 id=\"up\" phx-hook=\"Upcase\">test mount</h2>'],\n        fingerprint: 123,\n      },\n      liveview_version,\n    });\n    expect(view.el.firstChild.innerHTML).toBe(\"TEST MOUNT\");\n    expect(Object.keys(view.viewHooks)).toHaveLength(1);\n\n    view.update(\n      {\n        s: ['<h2 id=\"up\" phx-hook=\"Upcase\">test update</h2>'],\n        fingerprint: 123,\n      },\n      [],\n    );\n    expect(upcaseBeforeUpdate).toBe(true);\n    expect(view.el.firstChild.innerHTML).toBe(\"test update updated\");\n\n    view.showLoader();\n    expect(view.el.firstChild.innerHTML).toBe(\"disconnected\");\n\n    view.triggerReconnected();\n    expect(view.el.firstChild.innerHTML).toBe(\"connected\");\n\n    view.update({ s: [\"<div></div>\"], fingerprint: 123 }, []);\n    expect(upcaseWasDestroyed).toBe(true);\n    expect(hookLiveSocket).toBeDefined();\n    expect(Object.keys(view.viewHooks)).toEqual([]);\n  });\n\n  test(\"class based hook\", async () => {\n    let upcaseWasDestroyed = false;\n    let upcaseBeforeUpdate = false;\n    let hookLiveSocket;\n    const Hooks = {\n      Upcase: class extends ViewHook {\n        mounted() {\n          hookLiveSocket = this.liveSocket;\n          this.el.innerHTML = this.el.innerHTML.toUpperCase();\n        }\n        beforeUpdate() {\n          upcaseBeforeUpdate = true;\n        }\n        updated() {\n          this.el.innerHTML = this.el.innerHTML + \" updated\";\n        }\n        disconnected() {\n          this.el.innerHTML = \"disconnected\";\n        }\n        reconnected() {\n          this.el.innerHTML = \"connected\";\n        }\n        destroyed() {\n          upcaseWasDestroyed = true;\n        }\n      },\n    };\n    const liveSocket = new LiveSocket(\"/live\", Socket, { hooks: Hooks });\n    const el = liveViewDOM();\n\n    const view = simulateJoinedView(el, liveSocket);\n\n    view.onJoin({\n      rendered: {\n        s: ['<h2 id=\"up\" phx-hook=\"Upcase\">test mount</h2>'],\n        fingerprint: 123,\n      },\n      liveview_version,\n    });\n    expect(view.el.firstChild.innerHTML).toBe(\"TEST MOUNT\");\n    expect(Object.keys(view.viewHooks)).toHaveLength(1);\n\n    view.update(\n      {\n        s: ['<h2 id=\"up\" phx-hook=\"Upcase\">test update</h2>'],\n        fingerprint: 123,\n      },\n      [],\n    );\n    expect(upcaseBeforeUpdate).toBe(true);\n    expect(view.el.firstChild.innerHTML).toBe(\"test update updated\");\n\n    view.showLoader();\n    expect(view.el.firstChild.innerHTML).toBe(\"disconnected\");\n\n    view.triggerReconnected();\n    expect(view.el.firstChild.innerHTML).toBe(\"connected\");\n\n    view.update({ s: [\"<div></div>\"], fingerprint: 123 }, []);\n    expect(upcaseWasDestroyed).toBe(true);\n    expect(hookLiveSocket).toBeDefined();\n    expect(Object.keys(view.viewHooks)).toEqual([]);\n  });\n\n  test(\"createHook\", (done) => {\n    const liveSocket = new LiveSocket(\"/live\", Socket, {});\n    const el = liveViewDOM();\n    customElements.define(\n      \"custom-el\",\n      class extends HTMLElement {\n        hook!: ViewHook;\n        connectedCallback() {\n          this.hook = createHook(this, {\n            mounted: () => {\n              expect(this.hook.liveSocket).toBeTruthy();\n              done();\n            },\n          });\n          // Before mounting, accessing liveSocket throws (hook not yet attached to a live view)\n          expect(() => this.hook.liveSocket).toThrow();\n        }\n      },\n    );\n    const customEl = document.createElement(\"custom-el\");\n    customEl.id = \"foo\";\n    el.appendChild(customEl);\n    simulateJoinedView(el, liveSocket);\n  });\n\n  test(\"view destroyed\", async () => {\n    const values: Array<string> = [];\n    const Hooks = <HooksOptions>{\n      Check: {\n        destroyed() {\n          values.push(\"destroyed\");\n        },\n      },\n    };\n    const liveSocket = new LiveSocket(\"/live\", Socket, { hooks: Hooks });\n    const el = liveViewDOM();\n\n    const view = simulateJoinedView(el, liveSocket);\n\n    view.onJoin({\n      rendered: {\n        s: ['<h2 id=\"check\" phx-hook=\"Check\">test mount</h2>'],\n        fingerprint: 123,\n      },\n      liveview_version,\n    });\n    expect(view.el.firstChild.innerHTML).toBe(\"test mount\");\n\n    view.destroy();\n\n    expect(values).toEqual([\"destroyed\"]);\n  });\n\n  test(\"view reconnected\", async () => {\n    const values: Array<string> = [];\n    const Hooks = {\n      Check: <HooksOptions>{\n        mounted() {\n          values.push(\"mounted\");\n        },\n        disconnected() {\n          values.push(\"disconnected\");\n        },\n        reconnected() {\n          values.push(\"reconnected\");\n        },\n      },\n    };\n    const liveSocket = new LiveSocket(\"/live\", Socket, { hooks: Hooks });\n    const el = liveViewDOM();\n\n    const view = simulateJoinedView(el, liveSocket);\n\n    view.onJoin({\n      rendered: {\n        s: ['<h2 id=\"check\" phx-hook=\"Check\"></h2>'],\n        fingerprint: 123,\n      },\n      liveview_version,\n    });\n    expect(values).toEqual([\"mounted\"]);\n\n    view.triggerReconnected();\n    // The hook hasn't disconnected, so it shouldn't receive \"reconnected\" message\n    expect(values).toEqual([\"mounted\"]);\n\n    view.showLoader();\n    expect(values).toEqual([\"mounted\", \"disconnected\"]);\n\n    view.triggerReconnected();\n    expect(values).toEqual([\"mounted\", \"disconnected\", \"reconnected\"]);\n  });\n\n  test(\"dispatches uploads\", async () => {\n    const hooks = { Recorder: {} };\n    const liveSocket = new LiveSocket(\"/live\", Socket, { hooks });\n    const el = liveViewDOM();\n    const view = simulateJoinedView(el, liveSocket);\n\n    const template = `\n    <form id=\"rec\" phx-hook=\"Recorder\" phx-change=\"change\">\n    <input accept=\"*\" data-phx-active-refs=\"\" data-phx-done-refs=\"\" data-phx-preflighted-refs=\"\" data-phx-update=\"ignore\" data-phx-upload-ref=\"0\" id=\"uploads0\" name=\"doc\" phx-hook=\"Phoenix.LiveFileUpload\" type=\"file\">\n    </form>\n    `;\n    view.onJoin({\n      rendered: {\n        s: [template],\n        fingerprint: 123,\n      },\n      liveview_version,\n    });\n\n    const recorderHook = view.getHook(view.el.querySelector(\"#rec\"));\n    const fileEl = view.el.querySelector(\"#uploads0\");\n    const dispatchEventSpy = jest.spyOn(fileEl, \"dispatchEvent\");\n\n    const contents = { hello: \"world\" };\n    const blob = new Blob([JSON.stringify(contents, null, 2)], {\n      type: \"application/json\",\n    });\n    recorderHook.upload(\"doc\", [blob]);\n\n    expect(dispatchEventSpy).toHaveBeenCalledWith(\n      new CustomEvent(\"track-uploads\", {\n        bubbles: true,\n        cancelable: true,\n        detail: { files: [blob] },\n      }),\n    );\n  });\n\n  test(\"dom hooks\", async () => {\n    let fromHTML,\n      toHTML = null;\n    const liveSocket = new LiveSocket(\"/live\", Socket, {\n      dom: {\n        onBeforeElUpdated(from, to) {\n          fromHTML = from.innerHTML;\n          toHTML = to.innerHTML;\n        },\n      },\n    });\n    const el = liveViewDOM();\n    const view = simulateJoinedView(el, liveSocket);\n\n    view.onJoin({\n      rendered: { s: [\"<div>initial</div>\"], fingerprint: 123 },\n      liveview_version,\n    });\n    expect(view.el.firstChild.innerHTML).toBe(\"initial\");\n\n    view.update({ s: [\"<div>updated</div>\"], fingerprint: 123 }, []);\n    expect(fromHTML).toBe(\"initial\");\n    expect(toHTML).toBe(\"updated\");\n    expect(view.el.firstChild.innerHTML).toBe(\"updated\");\n  });\n\n  test(\"can overwrite property\", async () => {\n    let customHandleEventCalled = false;\n    const Hooks = <HooksOptions>{\n      Upcase: {\n        mounted() {\n          this.handleEvent = jest\n            .fn()\n            .mockImplementation(() => (customHandleEventCalled = true));\n          this.el.innerHTML = this.el.innerHTML.toUpperCase();\n        },\n        updated() {\n          this.handleEvent(\"foo\", () => {});\n        },\n      },\n    };\n    liveSocket = new LiveSocket(\"/live\", Socket, { hooks: Hooks });\n    const el = liveViewDOM();\n\n    const view = simulateJoinedView(el, liveSocket);\n\n    view.onJoin({\n      rendered: {\n        s: ['<h2 id=\"up\" phx-hook=\"Upcase\">test mount</h2>'],\n        fingerprint: 123,\n      },\n      liveview_version,\n    });\n    expect(view.el.firstChild.innerHTML).toBe(\"TEST MOUNT\");\n    expect(Object.keys(view.viewHooks)).toHaveLength(1);\n\n    expect(customHandleEventCalled).toBe(false);\n    view.update(\n      {\n        s: ['<h2 id=\"up\" phx-hook=\"Upcase\">test update</h2>'],\n        fingerprint: 123,\n      },\n      [],\n    );\n    expect(customHandleEventCalled).toBe(true);\n  });\n});\n\nfunction liveViewComponent() {\n  const div = document.createElement(\"div\");\n  div.setAttribute(\"data-phx-session\", \"abc123\");\n  div.setAttribute(\"id\", \"container\");\n  div.setAttribute(\"class\", \"user-implemented-class\");\n  div.innerHTML = `\n    <article class=\"form-wrapper\" data-phx-component=\"0\">\n      <form>\n        <label for=\"plus\">Plus</label>\n        <input id=\"plus\" value=\"1\" name=\"increment\" phx-target=\".form-wrapper\" />\n        <input type=\"checkbox\" phx-click=\"toggle_me\" phx-target=\".form-wrapper\" />\n        <button phx-click=\"inc_temperature\">Inc Temperature</button>\n      </form>\n    </article>\n  `;\n  return div;\n}\n\ndescribe(\"View + Component\", function () {\n  let liveSocket;\n\n  beforeEach(() => {\n    global.Phoenix = { Socket };\n    global.document.body.innerHTML = liveViewComponent().outerHTML;\n  });\n\n  afterEach(() => {\n    liveSocket && liveSocket.destroyAllViews();\n    liveSocket = null;\n  });\n\n  afterAll(() => {\n    global.document.body.innerHTML = \"\";\n  });\n\n  test(\"targetComponentID\", async () => {\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewComponent();\n    const view = simulateJoinedView(el, liveSocket);\n    const form = el.querySelector('input[type=\"checkbox\"]');\n    const targetCtx = el.querySelector(\".form-wrapper\");\n    expect(view.targetComponentID(el, targetCtx)).toBe(null);\n    expect(view.targetComponentID(form, targetCtx)).toBe(0);\n  });\n\n  test(\"pushEvent\", (done) => {\n    expect.assertions(17);\n\n    liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewComponent();\n    const targetCtx = el.querySelector(\".form-wrapper\");\n\n    const view = simulateJoinedView(el, liveSocket);\n    const input = view.el.querySelector(\"input[id=plus]\");\n    const channelStub = {\n      leave() {\n        return {\n          receive(_status, _cb) {\n            return this;\n          },\n        };\n      },\n      push(_evt, payload, _timeout) {\n        expect(payload.type).toBe(\"keyup\");\n        expect(payload.event).toBeDefined();\n        expect(payload.value).toEqual({ value: \"1\" });\n        expect(payload.cid).toEqual(0);\n        return {\n          receive(_status, cb) {\n            cb({ ref: payload.ref });\n            return this;\n          },\n        };\n      },\n    };\n    view.channel = channelStub;\n\n    input.addEventListener(\"phx:push:myevent\", (e) => {\n      const { ref, lockComplete, loadingComplete } = e.detail;\n      expect(ref).toBe(0);\n      expect(e.target).toBe(input);\n      loadingComplete.then((detail) => {\n        expect(detail.event).toBe(\"myevent\");\n        expect(detail.ref).toBe(0);\n        lockComplete.then((detail) => {\n          expect(detail.event).toBe(\"myevent\");\n          expect(detail.ref).toBe(0);\n          done();\n        });\n      });\n    });\n    input.addEventListener(\"phx:push\", (e) => {\n      const { lock, unlock, lockComplete } = e.detail;\n      expect(typeof lock).toBe(\"function\");\n      expect(view.el.getAttribute(\"data-phx-ref-lock\")).toBe(null);\n      // lock accepts unlock function to fire, which will done() the test\n      lockComplete.then((detail) => {\n        expect(detail.event).toBe(\"myevent\");\n      });\n      lock(view.el).then((detail) => {\n        expect(detail.event).toBe(\"myevent\");\n      });\n      expect(e.target).toBe(input);\n      expect(input.getAttribute(\"data-phx-ref-lock\")).toBe(\"0\");\n      expect(view.el.getAttribute(\"data-phx-ref-lock\")).toBe(\"0\");\n      unlock(view.el);\n      expect(view.el.getAttribute(\"data-phx-ref-lock\")).toBe(null);\n    });\n\n    view.pushEvent(\"keyup\", input, targetCtx, \"myevent\", {});\n  });\n\n  test(\"pushInput\", function (done) {\n    const html = `<form id=\"form\" phx-change=\"validate\">\n      <label for=\"first_name\">First Name</label>\n      <input id=\"first_name\" value=\"\" name=\"user[first_name]\" />\n\n      <label for=\"last_name\">Last Name</label>\n      <input id=\"last_name\" value=\"\" name=\"user[last_name]\" />\n    </form>`;\n    const liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM(html);\n    const view = simulateJoinedView(el, liveSocket);\n    Array.from(view.el.querySelectorAll(\"input\")).forEach((input) =>\n      simulateUsedInput(input),\n    );\n    const channelStub = {\n      validate: \"\",\n      meta: null,\n      nextValidate(payload, meta) {\n        this.meta = meta;\n        this.validate = Object.entries(payload)\n          .map(\n            ([key, value]) =>\n              `${encodeURIComponent(key)}=${value ? encodeURIComponent(value as string) : \"\"}`,\n          )\n          .join(\"&\");\n      },\n      push(_evt, payload, _timeout) {\n        expect(payload.value).toBe(this.validate);\n        expect(payload.meta).toEqual(this.meta);\n        return {\n          receive(status, cb) {\n            if (status === \"ok\") {\n              const diff = {\n                s: [\n                  `\n                <form id=\"form\" phx-change=\"validate\">\n                  <label for=\"first_name\">First Name</label>\n                  <input id=\"first_name\" value=\"\" name=\"user[first_name]\" />\n                  <span class=\"feedback\">can't be blank</span>\n\n                  <label for=\"last_name\">Last Name</label>\n                  <input id=\"last_name\" value=\"\" name=\"user[last_name]\" />\n                  <span class=\"feedback\">can't be blank</span>\n                </form>\n                `,\n                ],\n                fingerprint: 345,\n              };\n              cb({ diff: diff });\n              return this;\n            } else {\n              return this;\n            }\n          },\n        };\n      },\n    };\n    view.channel = channelStub;\n\n    const first_name = view.el.querySelector(\"#first_name\");\n    const last_name = view.el.querySelector(\"#last_name\");\n    view.channel.nextValidate(\n      { \"user[first_name]\": null, \"user[last_name]\": null },\n      { _target: \"user[first_name]\" },\n    );\n    // we have to set this manually since it's set by a change event that would require more plumbing with the liveSocket in the test to hook up\n    DOM.putPrivate(first_name, \"phx-has-focused\", true);\n    view.pushInput(first_name, el, null, \"validate\", {\n      _target: first_name.name,\n    });\n    window.requestAnimationFrame(() => {\n      view.channel.nextValidate(\n        { \"user[first_name]\": null, \"user[last_name]\": null },\n        { _target: \"user[last_name]\" },\n      );\n      view.pushInput(last_name, el, null, \"validate\", {\n        _target: last_name.name,\n      });\n      window.requestAnimationFrame(() => {\n        done();\n      });\n    });\n  });\n\n  test(\"adds auto ID to prevent teardown/re-add\", () => {\n    const liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n    const view = simulateJoinedView(el, liveSocket);\n\n    stubChannel(view);\n\n    const joinDiff = {\n      \"0\": { \"0\": \"\", \"1\": 0, s: [\"\", \"\", \"<h2>2</h2>\\n\"] },\n      c: {\n        \"0\": { s: ['<div phx-click=\"show-rect\">Menu</div>\\n'], r: 1 },\n      },\n      s: [\"\", \"\"],\n    };\n\n    const updateDiff = {\n      \"0\": {\n        \"0\": { s: [\"  <h1>1</h1>\\n\"], r: 1 },\n      },\n    };\n\n    view.onJoin({ rendered: joinDiff, liveview_version });\n    expect(view.el.innerHTML.trim()).toBe(\n      '<div data-phx-id=\"c0-container\" data-phx-component=\"0\" data-phx-view=\"container\" phx-click=\"show-rect\">Menu</div>\\n<h2>2</h2>',\n    );\n\n    view.update(updateDiff, []);\n    expect(view.el.innerHTML.trim().replace(\"\\n\", \"\")).toBe(\n      '<h1 data-phx-id=\"m1-container\">1</h1><div data-phx-id=\"c0-container\" data-phx-component=\"0\" data-phx-view=\"container\" phx-click=\"show-rect\">Menu</div>\\n<h2>2</h2>',\n    );\n  });\n\n  test(\"respects nested components\", () => {\n    const liveSocket = new LiveSocket(\"/live\", Socket);\n    const el = liveViewDOM();\n    const view = simulateJoinedView(el, liveSocket);\n\n    stubChannel(view);\n\n    const joinDiff = {\n      \"0\": 0,\n      c: {\n        \"0\": { \"0\": 1, s: [\"<div>Hello</div>\", \"\"], r: 1 },\n        \"1\": { s: [\"<div>World</div>\"], r: 1 },\n      },\n      s: [\"\", \"\"],\n    };\n\n    view.onJoin({ rendered: joinDiff, liveview_version });\n    expect(view.el.innerHTML.trim()).toBe(\n      '<div data-phx-id=\"c0-container\" data-phx-component=\"0\" data-phx-view=\"container\">Hello</div><div data-phx-id=\"c1-container\" data-phx-component=\"1\" data-phx-view=\"container\">World</div>',\n    );\n  });\n\n  test(\"destroys children when they are removed by an update\", () => {\n    const id = \"root\";\n    const childHTML = `<div data-phx-parent-id=\"${id}\" data-phx-session=\"\" data-phx-static=\"\" id=\"bar\" data-phx-root-id=\"${id}\"></div>`;\n    const newChildHTML = `<div data-phx-parent-id=\"${id}\" data-phx-session=\"\" data-phx-static=\"\" id=\"baz\" data-phx-root-id=\"${id}\"></div>`;\n    const el = document.createElement(\"div\");\n    el.setAttribute(\"data-phx-session\", \"abc123\");\n    el.setAttribute(\"id\", id);\n    document.body.appendChild(el);\n\n    const liveSocket = new LiveSocket(\"/live\", Socket);\n\n    const view = simulateJoinedView(el, liveSocket);\n\n    const joinDiff = { s: [childHTML] };\n\n    const updateDiff = { s: [newChildHTML] };\n\n    view.onJoin({ rendered: joinDiff, liveview_version });\n    expect(view.el.innerHTML.trim()).toEqual(childHTML);\n    expect(view.getChildById(\"bar\")).toBeDefined();\n\n    view.update(updateDiff, []);\n    expect(view.el.innerHTML.trim()).toEqual(newChildHTML);\n    expect(view.getChildById(\"baz\")).toBeDefined();\n    expect(view.getChildById(\"bar\")).toBeUndefined();\n  });\n\n  describe(\"undoRefs\", () => {\n    test(\"restores phx specific attributes awaiting a ref\", () => {\n      const content = `\n        <span data-phx-ref-loading=\"1\" data-phx-ref-src=\"root\"></span>\n        <form phx-change=\"suggest\" phx-submit=\"search\" class=\"phx-submit-loading\" data-phx-ref-loading=\"38\" data-phx-ref-src=\"root\">\n          <input type=\"text\" name=\"q\" value=\"ddsdsd\" placeholder=\"Live dependency search\" list=\"results\" autocomplete=\"off\" data-phx-readonly=\"false\" readonly=\"\" class=\"phx-submit-loading\" data-phx-ref-loading=\"38\" data-phx-ref-src=\"root\">\n          <datalist id=\"results\">\n          </datalist>\n          <button type=\"submit\" phx-disable-with=\"Searching...\" data-phx-disabled=\"false\" disabled=\"\" class=\"phx-submit-loading\" data-phx-ref-loading=\"38\" data-phx-ref-src=\"root\" data-phx-disable-with-restore=\"GO TO HEXDOCS\">Searching...</button>\n        </form>\n      `.trim();\n      const liveSocket = new LiveSocket(\"/live\", Socket);\n      const el = rootContainer(content);\n      const view = simulateJoinedView(el, liveSocket);\n\n      view.undoRefs(1);\n      expect(el.innerHTML).toBe(\n        `\n        <span></span>\n        <form phx-change=\"suggest\" phx-submit=\"search\" class=\"phx-submit-loading\" data-phx-ref-src=\"root\" data-phx-ref-loading=\"38\">\n          <input type=\"text\" name=\"q\" value=\"ddsdsd\" placeholder=\"Live dependency search\" list=\"results\" autocomplete=\"off\" data-phx-readonly=\"false\" readonly=\"\" class=\"phx-submit-loading\" data-phx-ref-src=\"root\" data-phx-ref-loading=\"38\">\n          <datalist id=\"results\">\n          </datalist>\n          <button type=\"submit\" phx-disable-with=\"Searching...\" data-phx-disabled=\"false\" disabled=\"\" class=\"phx-submit-loading\" data-phx-disable-with-restore=\"GO TO HEXDOCS\" data-phx-ref-src=\"root\" data-phx-ref-loading=\"38\">Searching...</button>\n        </form>\n      `.trim(),\n      );\n\n      view.undoRefs(38);\n      expect(el.innerHTML).toBe(\n        `\n        <span></span>\n        <form phx-change=\"suggest\" phx-submit=\"search\">\n          <input type=\"text\" name=\"q\" value=\"ddsdsd\" placeholder=\"Live dependency search\" list=\"results\" autocomplete=\"off\">\n          <datalist id=\"results\">\n          </datalist>\n          <button type=\"submit\" phx-disable-with=\"Searching...\">GO TO HEXDOCS</button>\n        </form>\n      `.trim(),\n      );\n    });\n\n    test(\"replaces any previous applied component\", () => {\n      const liveSocket = new LiveSocket(\"/live\", Socket);\n      const el = rootContainer(\"\");\n\n      const fromEl = tag(\n        \"span\",\n        { \"data-phx-ref-src\": el.id, \"data-phx-ref-lock\": \"1\" },\n        \"hello\",\n      );\n      const toEl = tag(\"span\", { class: \"new\" }, \"world\");\n\n      DOM.putPrivate(fromEl, \"data-phx-ref-lock\", toEl);\n\n      el.appendChild(fromEl);\n      const view = simulateJoinedView(el, liveSocket);\n\n      view.undoRefs(1);\n      expect(el.innerHTML).toBe('<span class=\"new\">world</span>');\n    });\n\n    test(\"triggers beforeUpdate and updated hooks\", () => {\n      global.document.body.innerHTML = \"\";\n      let beforeUpdate = false;\n      let updated = false;\n      const Hooks = {\n        MyHook: {\n          beforeUpdate() {\n            beforeUpdate = true;\n          },\n          updated() {\n            updated = true;\n          },\n        },\n      };\n      const liveSocket = new LiveSocket(\"/live\", Socket, { hooks: Hooks });\n      const el = liveViewDOM();\n      const view = simulateJoinedView(el, liveSocket);\n      stubChannel(view);\n      view.onJoin({\n        rendered: { s: ['<span id=\"myhook\" phx-hook=\"MyHook\">Hello</span>'] },\n        liveview_version,\n      });\n\n      view.update(\n        {\n          s: [\n            '<span id=\"myhook\" data-phx-ref-loading=\"1\" data-phx-ref-lock=\"2\" data-phx-ref-src=\"container\" phx-hook=\"MyHook\" class=\"phx-change-loading\">Hello</span>',\n          ],\n        },\n        [],\n      );\n\n      const toEl = tag(\"span\", { id: \"myhook\", \"phx-hook\": \"MyHook\" }, \"world\");\n      DOM.putPrivate(el.querySelector(\"#myhook\"), \"data-phx-ref-lock\", toEl);\n\n      view.undoRefs(1);\n\n      expect(el.querySelector(\"#myhook\")!.outerHTML).toBe(\n        '<span id=\"myhook\" phx-hook=\"MyHook\" data-phx-ref-src=\"container\" data-phx-ref-lock=\"2\" data-phx-ref-loading=\"1\">Hello</span>',\n      );\n      view.undoRefs(2);\n      expect(el.querySelector(\"#myhook\")!.outerHTML).toBe(\n        '<span id=\"myhook\" phx-hook=\"MyHook\">world</span>',\n      );\n      expect(beforeUpdate).toBe(true);\n      expect(updated).toBe(true);\n    });\n  });\n});\n\ndescribe(\"DOM\", function () {\n  it(\"mergeAttrs attributes\", function () {\n    const target = document.createElement(\"input\");\n    target.type = \"checkbox\";\n    target.id = \"foo\";\n    target.setAttribute(\"checked\", \"true\");\n\n    const source = document.createElement(\"input\");\n    source.type = \"checkbox\";\n    source.id = \"bar\";\n\n    expect(target.getAttribute(\"checked\")).toEqual(\"true\");\n    expect(target.id).toEqual(\"foo\");\n\n    DOM.mergeAttrs(target, source);\n\n    expect(target.getAttribute(\"checked\")).toEqual(null);\n    expect(target.id).toEqual(\"bar\");\n  });\n\n  it(\"mergeAttrs with properties\", function () {\n    const target = document.createElement(\"input\");\n    target.type = \"checkbox\";\n    target.id = \"foo\";\n    target.checked = true;\n\n    const source = document.createElement(\"input\");\n    source.type = \"checkbox\";\n    source.id = \"bar\";\n\n    expect(target.checked).toEqual(true);\n    expect(target.id).toEqual(\"foo\");\n\n    DOM.mergeAttrs(target, source);\n\n    expect(target.checked).toEqual(true);\n    expect(target.id).toEqual(\"bar\");\n  });\n});\n"
  },
  {
    "path": "babel.config.json",
    "content": "{\n  \"presets\": [\n    \"@babel/preset-env\",\n    \"@babel/preset-typescript\"\n  ]\n}\n"
  },
  {
    "path": "config/config.exs",
    "content": "import Config\n\nconfig :phoenix, :json_library, Jason\nconfig :phoenix, :trim_on_html_eex_engine, false\n\nif Mix.env() == :dev do\n  esbuild = fn args ->\n    [\n      args: ~w(./js/phoenix_live_view --bundle) ++ args,\n      cd: Path.expand(\"../assets\", __DIR__),\n      env: %{\"NODE_PATH\" => Path.expand(\"../deps\", __DIR__)}\n    ]\n  end\n\n  lv_vsn = Mix.Project.config()[:version]\n\n  config :esbuild,\n    version: \"0.20.2\",\n    module:\n      esbuild.(\n        ~w(--format=esm --sourcemap --define:LV_VSN=\"#{lv_vsn}\" --outfile=../priv/static/phoenix_live_view.esm.js)\n      ),\n    main:\n      esbuild.(\n        ~w(--format=cjs --sourcemap --define:LV_VSN=\"#{lv_vsn}\" --outfile=../priv/static/phoenix_live_view.cjs.js)\n      ),\n    cdn:\n      esbuild.(\n        ~w(--format=iife --target=es2016 --global-name=LiveView --define:LV_VSN=\"#{lv_vsn}\" --outfile=../priv/static/phoenix_live_view.js)\n      ),\n    cdn_min:\n      esbuild.(\n        ~w(--format=iife --target=es2016 --global-name=LiveView --minify --define:LV_VSN=\"#{lv_vsn}\" --outfile=../priv/static/phoenix_live_view.min.js)\n      )\nend\n\nimport_config \"#{config_env()}.exs\"\n"
  },
  {
    "path": "config/dev.exs",
    "content": "import Config\n"
  },
  {
    "path": "config/docs.exs",
    "content": "import Config\n"
  },
  {
    "path": "config/e2e.exs",
    "content": "import Config\n\nconfig :logger, :level, :error\n"
  },
  {
    "path": "config/test.exs",
    "content": "import Config\n\nconfig :logger, :level, :debug\nconfig :logger, :default_handler, false\n\nconfig :phoenix_live_view, enable_expensive_runtime_checks: true\n\nconfig :phoenix_live_view, :test_warnings, missing_form_id: :ignore\n"
  },
  {
    "path": "eslint.config.js",
    "content": "import playwright from \"eslint-plugin-playwright\"\nimport jest from \"eslint-plugin-jest\"\nimport globals from \"globals\"\nimport js from \"@eslint/js\"\nimport tseslint from \"typescript-eslint\"\n\nconst sharedRules = {\n  \"@typescript-eslint/no-unused-vars\": [\"error\", {\n    argsIgnorePattern: \"^_\",\n    varsIgnorePattern: \"^_\",\n  }],\n\n  \"@typescript-eslint/no-unused-expressions\": \"off\",\n  \"@typescript-eslint/no-explicit-any\": \"off\",\n\n  \"no-useless-escape\": \"off\",\n  \"no-cond-assign\": \"off\",\n  \"no-case-declarations\": \"off\",\n  \"prefer-const\": \"off\"\n}\n\nexport default tseslint.config([\n  {\n    ignores: [\n      \"_build/\",\n      \"assets/js/types/\",\n      \"test/e2e/test-results/\",\n      \"coverage/\",\n      \"cover/\",\n      \"priv/\",\n      \"deps/\",\n      \"doc/\"\n    ]\n  },\n  {\n    extends: [js.configs.recommended, ...tseslint.configs.recommended],\n    files: [\"*.js\", \"*.ts\", \"test/e2e/**\"],\n    ignores: [\"assets/**\"],\n    \n    plugins: {\n      ...playwright.configs[\"flat/recommended\"].plugins,\n    },\n\n    rules: {\n      ...playwright.configs[\"flat/recommended\"].rules,\n      ...sharedRules\n    },\n  },\n  {\n    extends: [js.configs.recommended, ...tseslint.configs.recommended],\n    files: [\"assets/**/*.{js,ts}\"],\n    ignores: [\"test/e2e/**\"],\n\n    plugins: {\n      jest,\n    },\n\n    languageOptions: {\n      globals: {\n        ...globals.browser,\n        ...jest.environments.globals.globals,\n        global: \"writable\",\n      },\n\n      ecmaVersion: 12,\n      sourceType: \"module\",\n    },\n\n    rules: {\n      ...sharedRules,\n    },\n  }])\n"
  },
  {
    "path": "guides/cheatsheets/html-attrs.cheatmd",
    "content": "# phx-* HTML attributes\n\nA summary of special HTML attributes used in Phoenix LiveView templates.\nEach attribute is linked to its documentation for more details.\n\n## Event Handlers\n{: .col-2}\n\nAttribute values can be:\n\n* An event name for the [`handle_event`](`c:Phoenix.LiveView.handle_event/3`) server callback\n* [JS commands](../client/bindings.md#js-commands) to be executed directly on the client\n\n> Use [`phx-value-*`](../client/bindings.md#click-events) attributes to pass params to the server.\n\n> Use [`phx-debounce` and `phx-throttle`](../client/bindings.md#rate-limiting-events-with-debounce-and-throttle) to control the frequency of events.\n\n### Click\n\n| Attributes                                                                                               |\n|----------------------------------------------------------------------------------------------------------|\n| [`phx-click`](../client/bindings.md#click-events) [`phx-click-away`](../client/bindings.md#click-events) |\n\n### Focus\n\n| Attributes                                                                                                                         |\n|------------------------------------------------------------------------------------------------------------------------------------|\n| [`phx-blur`](../client/bindings.md#focus-and-blur-events) [`phx-focus`](../client/bindings.md#focus-and-blur-events)               |\n| [`phx-window-blur`](../client/bindings.md#focus-and-blur-events) [`phx-window-focus`](../client/bindings.md#focus-and-blur-events) |\n\n### Keyboard\n\n| Attributes                                                                                                      |\n|-----------------------------------------------------------------------------------------------------------------|\n| [`phx-keydown`](../client/bindings.md#key-events) [`phx-keyup`](../client/bindings.md#key-events)               |\n| [`phx-window-keydown`](../client/bindings.md#key-events) [`phx-window-keyup`](../client/bindings.md#key-events) |\n\n> Use the `phx-key` attribute to listen to specific keys.\n\n### Scroll\n\n| Attributes                                                                                                                                                                           |\n|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| [`phx-viewport-top`](../client/bindings.md#scroll-events-and-infinite-stream-pagination) [`phx-viewport-bottom`](../client/bindings.md#scroll-events-and-infinite-stream-pagination) |\n\n### Example\n\n#### lib/hello_web/live/hello_live.html.heex\n\n```heex\n<button type=\"button\" phx-click=\"click\" phx-value-user={@current_user.id}>Click Me</button>\n<button type=\"button\" phx-click={JS.toggle(to: \"#example\")}>Toggle</button>\n```\n{: .wrap}\n\n## Form Event Handlers\n{: .col-2}\n\n### On `<form>` elements\n\n| Attribute                                                                                  | Value                                                                      |\n|--------------------------------------------------------------------------------------------|----------------------------------------------------------------------------|\n| [`phx-change`](../client/form-bindings.md#form-events)                                     | Event name or [JS commands](../client/bindings.md#js-commands)             |\n| [`phx-submit`](../client/form-bindings.md#form-events)                                     | Event name or [JS commands](../client/bindings.md#js-commands)             |\n| [`phx-auto-recover`](../client/form-bindings.md#recovery-following-crashes-or-disconnects) | Event name, [JS commands](../client/bindings.md#js-commands) or `\"ignore\"` |\n| [`phx-trigger-action`](../client/form-bindings.md#submitting-the-form-action-over-http)    | Presence (or `true`) disconnects the LiveView and submits the form         |\n| [`phx-no-unused-field`](../client/form-bindings.md#error-feedback)                         | Presence (or `true`) opts out of reporting unused form fields              |\n\n### On `<button>` elements\n\n| Attribute                                                                    | Value                                |\n|------------------------------------------------------------------------------|--------------------------------------|\n| [`phx-disable-with`](../client/form-bindings.md#javascript-client-specifics) | Text to show during event submission |\n\n### Form Example\n\n#### lib/hello_web/live/hello_live.html.heex\n\n```heex\n<form id=\"my-form\" phx-change=\"validate\" phx-submit=\"save\">\n  <input type=\"text\" name=\"name\" phx-debounce=\"500\" phx-throttle=\"500\" />\n  <button type=\"submit\" phx-disable-with=\"Saving...\">Save</button>\n</form>\n```\n{: .wrap}\n\n## Socket Connection Lifecycle\n\n| Attribute                                                    | Value                                                                                                                   |\n|--------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|\n| [`phx-connected`](../client/bindings.md#lifecycle-events)    | [JS commands](../client/bindings.md#js-commands) executed after the [`LiveSocket`](../client/js-interop.md) connects    |\n| [`phx-disconnected`](../client/bindings.md#lifecycle-events) | [JS commands](../client/bindings.md#js-commands) executed after the [`LiveSocket`](../client/js-interop.md) disconnects |\n\n#### lib/hello_web/live/hello_live.html.heex\n\n```heex\n<div id=\"status\" class=\"hidden\" phx-disconnected={JS.show()} phx-connected={JS.hide()}>\n  Attempting to reconnect...\n</div>\n```\n\n## DOM Element Lifecycle\n\n| Attribute                                           | Value                                                                                  |\n|-----------------------------------------------------|----------------------------------------------------------------------------------------|\n| [`phx-mounted`](../client/bindings.md#dom-patching) | [JS commands](../client/bindings.md#js-commands) executed after the element is mounted |\n| [`phx-remove`](../client/bindings.md#dom-patching)  | [JS commands](../client/bindings.md#js-commands) executed during the element removal   |\n| [`phx-update`](../client/bindings.md#dom-patching)  | `\"replace\"` (default), `\"stream\"` or `\"ignore\"`, configures DOM patching behavior      |\n\n#### lib/hello_web/live/hello_live.html.heex\n\n```heex\n<div\n  id=\"iframe-container\"\n  phx-mounted={JS.transition(\"animate-bounce\", time: 2000)}\n  phx-remove={JS.hide(transition: {\"transition-all transform ease-in duration-200\", \"opacity-100\", \"opacity-0\"})}\n>\n  <button type=\"button\" phx-click={JS.exec(\"phx-remove\", to: \"#iframe-container\")}>Hide</button>\n  <iframe id=\"iframe\" src=\"https://example.com\" phx-update=\"ignore\"></iframe>\n</div>\n```\n\n## Client Hooks\n\n| Attribute                                                       | Value                                                                                           |\n|-----------------------------------------------------------------|-------------------------------------------------------------------------------------------------|\n| [`phx-hook`](../client/js-interop.md#client-hooks-via-phx-hook) | The name of a previously defined JavaScript hook in the [`LiveSocket`](../client/js-interop.md) |\n\nClient hooks provide bidirectional communication between client and server using\n`this.pushEvent` and `this.handleEvent` to send and receive events.\n\n#### lib/hello_web/live/hello_live.html.heex\n\n```heex\n<div id=\"example\" phx-hook=\"Example\">\n  <h1>Events</h1>\n  <ul id=\"example-events\"></ul>\n</div>\n```\n\n#### assets/js/app.js\n\n```javascript\nlet Hooks = {}\nHooks.Example = {\n  // Callbacks\n  mounted()      { this.appendEvent(\"Mounted\") },\n  beforeUpdate() { this.appendEvent(\"Before Update\") },\n  updated()      { this.appendEvent(\"Updated\") },\n  destroyed()    { this.appendEvent(\"Destroyed\") },\n  disconnected() { this.appendEvent(\"Disconnected\") },\n  reconnected()  { this.appendEvent(\"Reconnected\") },\n\n  // Custom Helper\n  appendEvent(name) {\n    console.log(name)\n    let li = document.createElement(\"li\")\n    li.innerText = name\n    this.el.querySelector(\"#example-events\").appendChild(li)\n  }\n}\n\nlet liveSocket = new LiveSocket(\"/live\", Socket, {hooks: Hooks})\n```\n\n## Tracking Static Assets\n\n| Attribute                                                  | Value                               |\n|------------------------------------------------------------|-------------------------------------|\n| [`phx-track-static`](`Phoenix.LiveView.static_changed?/1`) | None, used to annotate static files |\n\n#### lib/hello_web/components/layouts/root.html.heex\n\n```heex\n<link phx-track-static rel=\"stylesheet\" href={~p\"/assets/app.css\"} />\n<script defer phx-track-static type=\"text/javascript\" src={~p\"/assets/app.js\"}></script>\n```\n\n## Formatting & Syntax\n\n| Attribute                                                                             | Value                                                                                                 |\n|---------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|\n| [`phx-no-format`](Phoenix.LiveView.HTMLFormatter.html#module-skip-formatting)         | Presence (or `true`) skips automatic HEEx formatting for the element block                            |\n| [`phx-no-curly-interpolation`](Phoenix.Component.html#sigil_H/2-interpolating-blocks) | Presence (or `true`) disables `{...}` interpolation inside the element, requiring `<%= ... %>` syntax |\n"
  },
  {
    "path": "guides/client/bindings.md",
    "content": "# Bindings\n\nPhoenix supports DOM element bindings for client-server interaction. For\nexample, to react to a click on a button, you would render the element:\n\n```heex\n<button phx-click=\"inc_temperature\">+</button>\n```\n\nThen on the server, all LiveView bindings are handled with the `handle_event`\ncallback, for example:\n\n    def handle_event(\"inc_temperature\", _value, socket) do\n      {:ok, new_temp} = Thermostat.inc_temperature(socket.assigns.id)\n      {:noreply, assign(socket, :temperature, new_temp)}\n    end\n\n| Binding                | Attributes |\n|------------------------|------------|\n| [Params](#click-events) | `phx-value-*` |\n| [Click Events](#click-events) | `phx-click`, `phx-click-away` |\n| [Form Events](form-bindings.md) | `phx-change`, `phx-submit`, `phx-disable-with`, `phx-trigger-action`, `phx-auto-recover` |\n| [Focus Events](#focus-and-blur-events) | `phx-blur`, `phx-focus`, `phx-window-blur`, `phx-window-focus` |\n| [Key Events](#key-events) | `phx-keydown`, `phx-keyup`, `phx-window-keydown`, `phx-window-keyup`, `phx-key` |\n| [Scroll Events](#scroll-events-and-infinite-pagination) | `phx-viewport-top`, `phx-viewport-bottom` |\n| [DOM Patching](#dom-patching) | `phx-update`, `phx-mounted`, `phx-remove` |\n| [JS Interop](js-interop.md#client-hooks-via-phx-hook) | `phx-hook` |\n| [Lifecycle Events](#lifecycle-events) | `phx-connected`, `phx-disconnected` |\n| [Rate Limiting](#rate-limiting-events-with-debounce-and-throttle) | `phx-debounce`, `phx-throttle` |\n| [Static tracking](`Phoenix.LiveView.static_changed?/1`) | `phx-track-static` |\n\nIf you need to trigger commands actions via JavaScript, see [JavaScript interoperability](js-interop.md#js-commands).\n\n## Click Events\n\nThe `phx-click` binding is used to send click events to the server.\nWhen any client event, such as a `phx-click` click is pushed, the value\nsent to the server will be chosen with the following priority:\n\n  * The `:value` specified in `Phoenix.LiveView.JS.push/3`, such as:\n\n    ```heex\n    <div phx-click={JS.push(\"inc\", value: %{myvar1: @val1})}>\n    ```\n\n  * Any number of optional `phx-value-` prefixed attributes, such as:\n\n    ```heex\n    <div phx-click=\"inc\" phx-value-myvar1=\"val1\" phx-value-myvar2=\"val2\">\n    ```\n\n    will send the following map of params to the server:\n\n        def handle_event(\"inc\", %{\"myvar1\" => \"val1\", \"myvar2\" => \"val2\"}, socket) do\n\n    If the `phx-value-` prefix is used, the server payload will also contain a `\"value\"`\n    if the element's value attribute exists. Note that if the element has a `value` property,\n    like the `<li>` element, it takes precedence over `phx-value-value`. If you need to overwrite\n    the `\"value\"` key in the payload, you can use `Phoenix.LiveView.JS.push/3` with its `value` option.\n\n  * The payload will also include any additional user defined metadata of the client event.\n    For example, the following `LiveSocket` client option would send the coordinates and\n    `altKey` information for all clicks:\n\n    ```javascript\n    let liveSocket = new LiveSocket(\"/live\", Socket, {\n      params: {_csrf_token: csrfToken},\n      metadata: {\n        click: (e, el) => {\n          return {\n            altKey: e.altKey,\n            clientX: e.clientX,\n            clientY: e.clientY\n          }\n        }\n      }\n    })\n    ```\n\nThe `phx-click-away` event is fired when a click event happens outside of the element.\nThis is useful for hiding toggled containers like drop-downs.\n\n## Focus and Blur Events\n\nFocus and blur events may be bound to DOM elements that emit\nsuch events, using the `phx-blur`, and `phx-focus` bindings, for example:\n\n```heex\n<input name=\"email\" phx-focus=\"myfocus\" phx-blur=\"myblur\"/>\n```\n\nTo detect when the page itself has received focus or blur,\n`phx-window-focus` and `phx-window-blur` may be specified. These window\nlevel events may also be necessary if the element in consideration\n(most often a `div` with no tabindex) cannot receive focus. Like other\nbindings, `phx-value-*` can be provided on the bound element, and those\nvalues will be sent as part of the payload. For example:\n\n```heex\n<div class=\"container\"\n    phx-window-focus=\"page-active\"\n    phx-window-blur=\"page-inactive\"\n    phx-value-page=\"123\">\n  ...\n</div>\n```\n\n## Key Events\n\nThe `onkeydown`, and `onkeyup` events are supported via the `phx-keydown`,\nand `phx-keyup` bindings. Each binding supports a `phx-key` attribute, which triggers\nthe event for the specific key press. If no `phx-key` is provided, the event is triggered\nfor any key press. When pushed, the value sent to the server will contain the `\"key\"`\nthat was pressed, plus any user-defined metadata. For example, pressing the\nEscape key looks like this:\n\n    %{\"key\" => \"Escape\"}\n\nTo capture additional user-defined metadata, the `metadata` option for keydown events\nmay be provided to the `LiveSocket` constructor. For example:\n\n```javascript\nlet liveSocket = new LiveSocket(\"/live\", Socket, {\n  params: {_csrf_token: csrfToken},\n  metadata: {\n    keydown: (e, el) => {\n      return {\n        key: e.key,\n        metaKey: e.metaKey,\n        repeat: e.repeat\n      }\n    }\n  }\n})\n```\n\nTo determine which key has been pressed you should use `key` value. The\navailable options can be found on\n[MDN](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values)\nor via the [Key Event Viewer](https://w3c.github.io/uievents/tools/key-event-viewer.html).\n\n*Note*: `phx-keyup` and `phx-keydown` are not supported on inputs.\nInstead use form bindings, such as `phx-change`, `phx-submit`, etc.\n\n*Note*: it is possible for certain browser features like autofill to trigger key events\nwith no `\"key\"` field present in the value map sent to the server. For this reason, we\nrecommend always having a fallback catch-all event handler for LiveView key bindings.\nBy default, the bound element will be the event listener, but a\nwindow-level binding may be provided via `phx-window-keydown` or `phx-window-keyup`,\nfor example:\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div id=\"thermostat\" phx-window-keyup=\"update_temp\">\n        Current temperature: {@temperature}\n      </div>\n      \"\"\"\n    end\n\n    def handle_event(\"update_temp\", %{\"key\" => \"ArrowUp\"}, socket) do\n      {:ok, new_temp} = Thermostat.inc_temperature(socket.assigns.id)\n      {:noreply, assign(socket, :temperature, new_temp)}\n    end\n\n    def handle_event(\"update_temp\", %{\"key\" => \"ArrowDown\"}, socket) do\n      {:ok, new_temp} = Thermostat.dec_temperature(socket.assigns.id)\n      {:noreply, assign(socket, :temperature, new_temp)}\n    end\n\n    def handle_event(\"update_temp\", _, socket) do\n      {:noreply, socket}\n    end\n\n## Rate limiting events with Debounce and Throttle\n\nAll events can be rate-limited on the client by using the\n`phx-debounce` and `phx-throttle` bindings, with the exception of the `phx-blur`\nbinding, which is fired immediately.\n\nRate limited and debounced events have the following behavior:\n\n  * `phx-debounce` - Accepts either an integer timeout value (in milliseconds),\n    or `\"blur\"`. When an integer is provided, emitting the event is delayed by\n    the specified milliseconds. When `\"blur\"` is provided, emitting the event is\n    delayed until the field is blurred by the user. When the value is omitted\n    a default of 300ms is used. Debouncing is typically used for input elements.\n\n  * `phx-throttle` - Accepts an integer timeout value to throttle the event in milliseconds.\n    Unlike debounce, throttle will immediately emit the event, then rate limit it at once\n    per provided timeout. When the value is omitted a default of 300ms is used.\n    Throttling is typically used to rate limit clicks, mouse and keyboard actions.\n\nFor example, to avoid validating an email until the field is blurred, while validating\nthe username at most every 2 seconds after a user changes the field:\n\n```heex\n<form id=\"my-form\" phx-change=\"validate\" phx-submit=\"save\">\n  <input type=\"text\" name=\"user[email]\" phx-debounce=\"blur\"/>\n  <input type=\"text\" name=\"user[username]\" phx-debounce=\"2000\"/>\n</form>\n```\n\nAnd to rate limit a volume up click to once every second:\n\n```heex\n<button phx-click=\"volume_up\" phx-throttle=\"1000\">+</button>\n```\n\nLikewise, you may throttle held-down keydown:\n\n```heex\n<div phx-window-keydown=\"keydown\" phx-throttle=\"500\">\n  ...\n</div>\n```\n\nUnless held-down keys are required, a better approach is generally to use\n`phx-keyup` bindings which only trigger on key up, thereby being self-limiting.\nHowever, `phx-keydown` is useful for games and other use cases where a constant\npress on a key is desired. In such cases, throttle should always be used.\n\n### Debounce and Throttle special behavior\n\nThe following specialized behavior is performed for forms and keydown bindings:\n\n  * When a `phx-submit`, or a `phx-change` for a different input is triggered,\n    any current debounce or throttle timers are reset for existing inputs.\n\n  * A `phx-keydown` binding is only throttled for key repeats. Unique keypresses\n    back-to-back will dispatch the pressed key events.\n\n## JS commands\n\nLiveView bindings support a JavaScript command interface via the `Phoenix.LiveView.JS` module, which allows you to specify utility operations that execute on the client when firing `phx-` binding events, such as `phx-click`, `phx-change`, etc. Commands compose together to allow you to push events, add classes to elements, transition elements in and out, and more.\nSee the `Phoenix.LiveView.JS` documentation for full usage.\n\nFor a small example of what's possible, imagine you want to show and hide a modal on the page without needing to make the round trip to the server to render the content:\n\n```heex\n<div id=\"modal\" class=\"modal\">\n  My Modal\n</div>\n\n<button phx-click={JS.show(to: \"#modal\", transition: \"fade-in\")}>\n  show modal\n</button>\n\n<button phx-click={JS.hide(to: \"#modal\", transition: \"fade-out\")}>\n  hide modal\n</button>\n\n<button phx-click={JS.toggle(to: \"#modal\", in: \"fade-in\", out: \"fade-out\")}>\n  toggle modal\n</button>\n```\n\nOr if your UI library relies on classes to perform the showing or hiding:\n\n```heex\n<div id=\"modal\" class=\"modal\">\n  My Modal\n</div>\n\n<button phx-click={JS.add_class(\"show\", to: \"#modal\", transition: \"fade-in\")}>\n  show modal\n</button>\n\n<button phx-click={JS.remove_class(\"show\", to: \"#modal\", transition: \"fade-out\")}>\n  hide modal\n</button>\n```\n\nCommands compose together. For example, you can push an event to the server and\nimmediately hide the modal on the client:\n\n```heex\n<div id=\"modal\" class=\"modal\">\n  My Modal\n</div>\n\n<button phx-click={JS.push(\"modal-closed\") |> JS.remove_class(\"show\", to: \"#modal\", transition: \"fade-out\")}>\n  hide modal\n</button>\n```\n\nIt is also useful to extract commands into their own functions:\n\n```elixir\nalias Phoenix.LiveView.JS\n\ndef hide_modal(js \\\\ %JS{}, selector) do\n  js\n  |> JS.push(\"modal-closed\")\n  |> JS.remove_class(\"show\", to: selector, transition: \"fade-out\")\nend\n```\n\n```heex\n<button phx-click={hide_modal(\"#modal\")}>hide modal</button>\n```\n\nThe `Phoenix.LiveView.JS.push/3` command is particularly powerful in allowing you to customize the event being pushed to the server. For example, imagine you start with a familiar `phx-click` which pushes a message to the server when clicked:\n\n```heex\n<button phx-click=\"clicked\">click</button>\n```\n\nNow imagine you want to customize what happens when the `\"clicked\"` event is pushed, such as which component should be targeted, which element should receive CSS loading state classes, etc. This can be accomplished with options on the JS push command. For example:\n\n```heex\n<button phx-click={JS.push(\"clicked\", target: @myself, loading: \".container\")}>click</button>\n```\n\nSee `Phoenix.LiveView.JS.push/3` for all supported options.\n\n## DOM patching\n\nA container can be marked with `phx-update` to configure how the DOM\nis updated. The following values are supported:\n\n  * `replace` - the default operation. Replaces the element with the contents\n\n  * `stream` - supports stream operations. Streams are used to manage large\n    collections in the UI without having to store the collection on the server\n\n  * `ignore` - ignores updates to the DOM regardless of new content changes.\n    This is useful for client-side interop with existing libraries that do\n    their own DOM operations\n\nWhen using `phx-update`, a unique DOM ID must always be set in the\ncontainer. If using \"stream\", a DOM ID must also be set\nfor each child. When inserting stream elements containing an\nID already present in the container, LiveView will replace the existing\nelement with the new content. See `Phoenix.LiveView.stream/3` for more\ninformation.\n\nThe \"ignore\" behaviour is frequently used when you need to integrate\nwith another JS library. Updates from the server to the element's content\nand attributes are ignored, *except for data attributes*. Changes, additions,\nand removals from the server to data attributes are merged with the ignored\nelement which can be used to pass data to the JS handler.\n\nTo react to elements being mounted to the DOM, the `phx-mounted` binding\ncan be used. For example, to animate an element on mount:\n\n```heex\n<div phx-mounted={JS.transition(\"animate-ping\", time: 500)}>\n```\n\nIf `phx-mounted` is used on the initial page render, it will run at the earliest\nopportunity. For elements outside of a LiveView, this is as soon as `liveSocket.connect()`\nis executed. For elements inside of a LiveView, this is only after the initial socket\nconnection is established and the LiveView is mounted.\n\nTo react to elements being removed from the DOM, the `phx-remove` binding\nmay be specified, which can contain a `Phoenix.LiveView.JS` command to execute.\nThe `phx-remove` command is only executed for the removed parent element.\nIt does not cascade to children.\n\nTo react to elements being updated in the DOM, you'll need to use a\n[hook](js-interop.md#client-hooks-via-phx-hook), which gives you full access\nto the element life-cycle.\n\n## Lifecycle events\n\nLiveView supports the `phx-connected` and `phx-disconnected` bindings to react\nto connection lifecycle events with JS commands. For example, to show an element\nwhen the LiveView has lost its connection and hide it when the connection\nrecovers:\n\n```heex\n<div id=\"status\" class=\"hidden\" phx-disconnected={JS.show()} phx-connected={JS.hide()}>\n  Attempting to reconnect...\n</div>\n```\n\n`phx-connected` and `phx-disconnected` are only executed when operating\ninside a LiveView container. For static templates, they will have no effect.\n\n## LiveView events prefix\n\nThe `lv:` event prefix supports LiveView specific features that are handled\nby LiveView without calling the user's `handle_event/3` callbacks. Today,\nthe following events are supported:\n\n  - `lv:clear-flash` – clears the flash when sent to the server. If a\n    `phx-value-key` is provided, the specific key will be removed from the flash.\n\nFor example:\n\n```heex\n<p class=\"alert\" phx-click=\"lv:clear-flash\" phx-value-key=\"info\">\n  {Phoenix.Flash.get(@flash, :info)}\n</p>\n```\n\n## Scroll events and infinite pagination\n\nThe `phx-viewport-top` and `phx-viewport-bottom` bindings allow you to detect when a container's\nfirst child reaches the top of the viewport, or the last child reaches the bottom of the viewport.\nThis is useful for infinite scrolling where you want to send paging events for the next results set or previous results set as the user is scrolling up and down and reaches the top or bottom of the viewport.\n\nGenerally, applications will add padding above and below a container when performing infinite scrolling to allow smooth scrolling as results are loaded. Combined with `Phoenix.LiveView.stream/3`, the `phx-viewport-top` and `phx-viewport-bottom` allow for infinite virtualized list that only keeps a small set of actual elements in the DOM. For example:\n\n```elixir\ndef mount(_, _, socket) do\n  {:ok,\n    socket\n    |> assign(page: 1, per_page: 20)\n    |> paginate_posts(1)}\nend\n\ndefp paginate_posts(socket, new_page) when new_page >= 1 do\n  %{per_page: per_page, page: cur_page} = socket.assigns\n  posts = Blog.list_posts(offset: (new_page - 1) * per_page, limit: per_page)\n\n  {posts, at, limit} =\n    if new_page >= cur_page do\n      {posts, -1, per_page * 3 * -1}\n    else\n      {Enum.reverse(posts), 0, per_page * 3}\n    end\n\n  case posts do\n    [] ->\n      assign(socket, end_of_timeline?: at == -1)\n\n    [_ | _] = posts ->\n      socket\n      |> assign(end_of_timeline?: false)\n      |> assign(:page, new_page)\n      |> stream(:posts, posts, at: at, limit: limit)\n  end\nend\n```\n\nOur `paginate_posts` function fetches a page of posts, and determines if the user is paging to a previous page or next page. Based on the direction of paging, the stream is either prepended to, or appended to with `at` of `0` or `-1` respectively. We also set the `limit` of the stream to three times the `per_page` to allow enough posts in the UI to appear as an infinite list, but small enough to maintain UI performance. We also set an `@end_of_timeline?` assign to track whether the user is at the end of results or not. Finally, we update the `@page` assign and posts stream. We can then wire up our container to support the viewport events:\n\n```heex\n<ul\n  id=\"posts\"\n  phx-update=\"stream\"\n  phx-viewport-top={@page > 1 && JS.push(\"prev-page\", page_loading: true)}\n  phx-viewport-bottom={!@end_of_timeline? && JS.push(\"next-page\", page_loading: true)}\n  class={[\n    if(@end_of_timeline?, do: \"pb-10\", else: \"pb-[calc(200vh)]\"),\n    if(@page == 1, do: \"pt-10\", else: \"pt-[calc(200vh)]\")\n  ]}\n>\n  <li :for={{id, post} <- @streams.posts} id={id}>\n    <.post_card post={post} />\n  </li>\n</ul>\n<div :if={@end_of_timeline?} class=\"mt-5 text-[50px] text-center\">\n  🎉 You made it to the beginning of time 🎉\n</div>\n```\n\nThere's not much here, but that's the point! This little snippet of UI is driving a fully virtualized list with bidirectional infinite scrolling. We use the `phx-viewport-top` binding to send the `\"prev-page\"` event to the LiveView, but only if the user is beyond the first page. It doesn't make sense to load negative page results, so we remove the binding entirely in those cases. Next, we wire up `phx-viewport-bottom` to send the `\"next-page\"` event, but only if we've yet to reach the end of the timeline. Finally, we conditionally apply some CSS classes which sets a large top and bottom padding to twice the viewport height based on the current pagination for smooth scrolling.\n\nTo complete our solution, we only need to handle the `\"prev-page\"` and `\"next-page\"` events in the LiveView:\n\n```elixir\ndef handle_event(\"next-page\", _, socket) do\n  {:noreply, paginate_posts(socket, socket.assigns.page + 1)}\nend\n\ndef handle_event(\"prev-page\", %{\"_overran\" => true}, socket) do\n  {:noreply, paginate_posts(socket, 1)}\nend\n\ndef handle_event(\"prev-page\", _, socket) do\n  if socket.assigns.page > 1 do\n    {:noreply, paginate_posts(socket, socket.assigns.page - 1)}\n  else\n    {:noreply, socket}\n  end\nend\n```\n\nThis code simply calls the `paginate_posts` function we defined as our first step, using the current or next page to drive the results. Notice that we match on a special `\"_overran\" => true` parameter in our `\"prev-page\"` event. The viewport events send this parameter when the user has \"overran\" the viewport top or bottom. Imagine the case where the user is scrolling back up through many pages of results, but grabs the scrollbar and returns immediately to the top of the page. This means our `<ul id=\"posts\">` container was overrun by the top of the viewport, and we need to reset the the UI to page the first page.\n\nWhen testing, you can use `Phoenix.LiveViewTest.render_hook/3` to test the viewport events:\n\n```elixir\nview\n|> element(\"#posts\")\n|> render_hook(\"next-page\")\n```\n"
  },
  {
    "path": "guides/client/external-uploads.md",
    "content": "# External uploads\n\n> This guide continues from the configuration started in the\n> server [Uploads guide](uploads.html).\n\nUploads to external cloud providers, such as Amazon S3,\nGoogle Cloud, etc., can be achieved by using the\n`:external` option in [`allow_upload/3`](`Phoenix.LiveView.allow_upload/3`).\n\nYou provide a 2-arity function to allow the server to\ngenerate metadata for each upload entry, which is passed to\na user-specified JavaScript function on the client.\n\nTypically when your function is invoked, you will generate a\npre-signed URL, specific to your cloud storage provider, that\nwill provide temporary access for the end-user to upload data\ndirectly to your cloud storage.\n\n## Chunked HTTP Uploads\n\nFor any service that supports large file\nuploads via chunked HTTP requests with `Content-Range`\nheaders, you can use the UpChunk JS library by Mux to do all\nthe hard work of uploading the file. For small file uploads\nor to get started quickly, consider [uploading directly to S3](#direct-to-s3)\ninstead.\n\nYou only need to wire the UpChunk instance to the LiveView\nUploadEntry callbacks, and LiveView will take care of the rest.\n\nInstall [UpChunk](https://github.com/muxinc/upchunk) by\nsaving [its contents](https://unpkg.com/@mux/upchunk@2)\nto `assets/vendor/upchunk.js` or by installing it with `npm`:\n\n```shell\n$ npm install --prefix assets --save @mux/upchunk\n```\n\nConfigure your uploader on `c:Phoenix.LiveView.mount/3`:\n\n    def mount(_params, _session, socket) do\n      {:ok,\n       socket\n       |> assign(:uploaded_files, [])\n       |> allow_upload(:avatar, accept: :any, max_entries: 3, external: &presign_upload/2)}\n    end\n\nSupply the `:external` option to\n`Phoenix.LiveView.allow_upload/3`. It requires a 2-arity\nfunction that generates a signed URL where the client will\npush the bytes for the upload entry. This function must\nreturn either `{:ok, meta, socket}` or `{:error, meta, socket}`,\nwhere `meta` must be a map.\n\nFor example, if you were using a context that provided a\n[`start_session`](https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol##Start_Resumable_Session)\nfunction, you might write something like this:\n\n    defp presign_upload(entry, socket) do\n      {:ok, %{\"Location\" => link}} =\n        SomeTube.start_session(%{\n          \"uploadType\" => \"resumable\",\n          \"x-upload-content-length\" => entry.client_size\n        })\n\n      {:ok, %{uploader: \"UpChunk\", entrypoint: link}, socket}\n    end\n\nFinally, on the client-side, we use UpChunk to create an\nupload from the temporary URL generated on the server and\nattach listeners for its events to the entry's callbacks:\n\n```javascript\nimport * as UpChunk from \"@mux/upchunk\"\n\nlet Uploaders = {}\n\nUploaders.UpChunk = function(entries, onViewError){\n  entries.forEach(entry => {\n    // create the upload session with UpChunk\n    let { file, meta: { entrypoint } } = entry\n    let upload = UpChunk.createUpload({ endpoint: entrypoint, file })\n\n    // stop uploading in the event of a view error\n    onViewError(() => upload.pause())\n\n    // upload error triggers LiveView error\n    upload.on(\"error\", (e) => entry.error(e.detail.message))\n\n    // notify progress events to LiveView\n    upload.on(\"progress\", (e) => {\n      if(e.detail < 100){ entry.progress(e.detail) }\n    })\n\n    // success completes the UploadEntry\n    upload.on(\"success\", () => entry.progress(100))\n  })\n}\n\n// Don't forget to assign Uploaders to the liveSocket\nlet liveSocket = new LiveSocket(\"/live\", Socket, {\n  uploaders: Uploaders,\n  params: {_csrf_token: csrfToken}\n})\n```\n\n## Direct to S3\n\nThe largest object that can be uploaded to S3 in a single PUT is 5 GB\naccording to [S3 FAQ](https://aws.amazon.com/s3/faqs/). For larger file\nuploads, consider using chunking as shown above.\n\nThis guide assumes an existing S3 bucket is set up with the correct CORS configuration\nwhich allows uploading directly to the bucket.\n\nAn example CORS config is:\n\n```json\n[\n    {\n        \"AllowedHeaders\": [ \"*\" ],\n        \"AllowedMethods\": [ \"PUT\", \"POST\" ],\n        \"AllowedOrigins\": [ \"*\" ],\n        \"ExposeHeaders\": []\n    }\n]\n```\n\nYou may put your domain in the \"allowedOrigins\" instead. More information on configuring CORS for\nS3 buckets is [available on AWS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ManageCorsUsing.html).\n\nIn order to enforce all of your file constraints when uploading to S3,\nit is necessary to perform a multipart form POST with your file data.\nYou should have the following S3 information ready before proceeding:\n\n1. aws_access_key_id\n2. aws_secret_access_key\n3. bucket_name\n4. region\n\nWe will first implement the LiveView portion:\n\n```elixir\ndef mount(_params, _session, socket) do\n  {:ok,\n    socket\n    |> assign(:uploaded_files, [])\n    |> allow_upload(:avatar, accept: :any, max_entries: 3, external: &presign_upload/2)}\nend\n\ndefp presign_upload(entry, socket) do\n  uploads = socket.assigns.uploads\n  bucket = \"phx-upload-example\"\n  key = \"public/#{entry.client_name}\"\n\n  config = %{\n    region: \"us-east-1\",\n    access_key_id: System.fetch_env!(\"AWS_ACCESS_KEY_ID\"),\n    secret_access_key: System.fetch_env!(\"AWS_SECRET_ACCESS_KEY\")\n  }\n\n  {:ok, fields} =\n    SimpleS3Upload.sign_form_upload(config, bucket,\n      key: key,\n      content_type: entry.client_type,\n      max_file_size: uploads[entry.upload_config].max_file_size,\n      expires_in: :timer.hours(1)\n    )\n\n  meta = %{uploader: \"S3\", key: key, url: \"http://#{bucket}.s3-#{config.region}.amazonaws.com\", fields: fields}\n  {:ok, meta, socket}\nend\n```\n\nHere, we implemented a `presign_upload/2` function, which we passed as a\ncaptured anonymous function to `:external`. It generates a pre-signed URL\nfor the upload and returns our `:ok` result, with a payload of metadata\nfor the client, along with our unchanged socket. \n\nNext, we add a missing module `SimpleS3Upload` to generate pre-signed URLs\nfor S3. Create a file called `simple_s3_upload.ex`. Get the file's content\nfrom this zero-dependency module called [`SimpleS3Upload`](https://gist.github.com/chrismccord/37862f1f8b1f5148644b75d20d1cb073)\nwritten by Chris McCord.\n\n> Tip: if you encounter errors with the `:crypto` module or with S3 blocking ACLs, \n> please read the comments in the gist above for solutions.\n\nNext, we add our JavaScript client-side uploader. The metadata *must* contain the\n`:uploader` key, specifying the name of the JavaScript client-side uploader.\nIn this case, it's `\"S3\"`, as shown above.\n\nAdd a new file `uploaders.js` in the following directory `assets/js/` next to `app.js`.\nThe content for this `S3` client uploader:\n\n```javascript\nlet Uploaders = {}\n\nUploaders.S3 = function(entries, onViewError){\n  entries.forEach(entry => {\n    let formData = new FormData()\n    let {url, fields} = entry.meta\n    Object.entries(fields).forEach(([key, val]) => formData.append(key, val))\n    formData.append(\"file\", entry.file)\n    let xhr = new XMLHttpRequest()\n    onViewError(() => xhr.abort())\n    xhr.onload = () => xhr.status === 204 ? entry.progress(100) : entry.error()\n    xhr.onerror = () => entry.error()\n    xhr.upload.addEventListener(\"progress\", (event) => {\n      if(event.lengthComputable){\n        let percent = Math.round((event.loaded / event.total) * 100)\n        if(percent < 100){ entry.progress(percent) }\n      }\n    })\n\n    xhr.open(\"POST\", url, true)\n    xhr.send(formData)\n  })\n}\n\nexport default Uploaders;\n```\n\nWe define an `Uploaders.S3` function, which receives our entries. It then\nperforms an AJAX request for each entry, using the `entry.progress()` and\n`entry.error()` functions to report upload events back to the LiveView.\nThe name of the uploader must match the one we return on the `:uploader`\nmetadata in LiveView.\n\nFinally, head over to `app.js` and add the `uploaders: Uploaders` key to\nthe `LiveSocket` constructor to tell phoenix where to find the uploaders returned \nwithin the external metadata.\n\n```javascript\n// for uploading to S3\nimport Uploaders from \"./uploaders\"\n\nlet liveSocket = new LiveSocket(\"/live\",\n   Socket, {\n     params: {_csrf_token: csrfToken},\n     uploaders: Uploaders\n  }\n)\n```\n\nNow \"S3\" returned from the server will match the one in the client.\nTo debug client-side JavaScript when trying to upload, you can inspect your\nbrowser and look at the console or networks tab to view the error logs.\n\n### Direct to S3-Compatible\n\n> This section assumes that you installed and configured [ExAws](https://hexdocs.pm/ex_aws/readme.html)\n> and [ExAws.S3](https://hexdocs.pm/ex_aws_s3/ExAws.S3.html) correctly in your project and can execute\n> the examples in the page without errors.\n\nMost S3 compatible platforms like Cloudflare R2 don't support `POST` when\nuploading files so we need to use `PUT` with a signed URL instead of the\nsigned `POST`and send the file straight to the service, to do so we need to\nchange the `presign_upload/2` function and the `Uploaders.S3` that does the upload.\n\nThe new `presign_upload/2`:\n\n```elixir\ndef presign_upload(entry, socket) do\n  config = ExAws.Config.new(:s3)\n  bucket = \"bucket\"\n  key = \"public/#{entry.client_name}\"\n\n  {:ok, url} =\n    ExAws.S3.presigned_url(config, :put, bucket, key,\n      expires_in: 3600,\n      query_params: [{\"Content-Type\", entry.client_type}]\n    )\n   {:ok, %{uploader: \"S3\", key: key, url: url}, socket}\nend\n```\n\nThe new `Uploaders.S3`:\n\n```javascript\nUploaders.S3 = function (entries, onViewError) {\n  entries.forEach(entry => {\n    let xhr = new XMLHttpRequest()\n    onViewError(() => xhr.abort())\n    xhr.onload = () => xhr.status === 200 ? entry.progress(100) : entry.error()\n    xhr.onerror = () => entry.error()\n\n    xhr.upload.addEventListener(\"progress\", (event) => {\n      if(event.lengthComputable){\n        let percent = Math.round((event.loaded / event.total) * 100)\n        if(percent < 100){ entry.progress(percent) }\n      }\n    })\n\n    let url = entry.meta.url\n    xhr.open(\"PUT\", url, true)\n    xhr.send(entry.file)\n  })\n}\n```\n"
  },
  {
    "path": "guides/client/form-bindings.md",
    "content": "# Form bindings\n\n## Form events\n\nTo handle form changes and submissions, use the `phx-change` and `phx-submit`\nevents. In general, it is preferred to handle input changes at the form level,\nwhere all form fields are passed to the LiveView's callback given any\nsingle input change. For example, to handle real-time form validation and\nsaving, your form would use both `phx-change` and `phx-submit` bindings.\nLet's get started with an example:\n\n```heex\n<.form for={@form} id=\"my-form\" phx-change=\"validate\" phx-submit=\"save\">\n  <.input type=\"text\" field={@form[:username]} />\n  <.input type=\"email\" field={@form[:email]} />\n  <button>Save</button>\n</.form>\n```\n\n`.form` is the function component defined in `Phoenix.Component.form/1`,\nwe recommend reading its documentation for more details on how it works\nand all supported options. `.form` expects a `@form` assign, which can\nbe created from a changeset or user parameters via `Phoenix.Component.to_form/1`.\n\n`input/1` is a function component for rendering inputs, most often\ndefined in your own application, often encapsulating labelling,\nerror handling, and more. Here is a simple version to get started with:\n\n    attr :field, Phoenix.HTML.FormField\n    attr :rest, :global, include: ~w(type)\n    def input(assigns) do\n      ~H\"\"\"\n      <input id={@field.id} name={@field.name} value={@field.value} {@rest} />\n      \"\"\"\n    end\n\n> ### The `CoreComponents` module {: .info}\n>\n> If your application was generated with Phoenix v1.7, then `mix phx.new`\n> automatically imports many ready-to-use function components, such as\n> `.input` component with built-in features and styles.\n\nWith the form rendered, your LiveView picks up the events in `handle_event`\ncallbacks, to validate and attempt to save the parameter accordingly:\n\n    def render(assigns) ...\n\n    def mount(_params, _session, socket) do\n      {:ok, assign(socket, form: to_form(Accounts.change_user(%User{})))}\n    end\n\n    def handle_event(\"validate\", %{\"user\" => params}, socket) do\n      form =\n        %User{}\n        |> Accounts.change_user(params)\n        |> to_form(action: :validate)\n\n      {:noreply, assign(socket, form: form)}\n    end\n\n    def handle_event(\"save\", %{\"user\" => user_params}, socket) do\n      case Accounts.create_user(user_params) do\n        {:ok, user} ->\n          {:noreply,\n           socket\n           |> put_flash(:info, \"user created\")\n           |> redirect(to: ~p\"/users/#{user}\")}\n\n        {:error, %Ecto.Changeset{} = changeset} ->\n          {:noreply, assign(socket, form: to_form(changeset))}\n      end\n    end\n\nThe validate callback simply updates the changeset based on all form input\nvalues, then convert the changeset to a form and assign it to the socket.\nIf the form changes, such as generating new errors, [`render/1`](`c:Phoenix.LiveView.render/1`)\nis invoked and the form is re-rendered.\n\nLikewise for `phx-submit` bindings, the same callback is invoked and\npersistence is attempted. On success, a `:noreply` tuple is returned and the\nsocket is annotated for redirect with `Phoenix.LiveView.redirect/2` to\nthe new user page, otherwise the socket assigns are updated with the errored\nchangeset to be re-rendered for the client.\n\nYou may wish for an individual input to use its own change event or to target\na different component. This can be accomplished by annotating the input itself\nwith `phx-change`, for example:\n\n```heex\n<.form for={@form} id=\"my-form\" phx-change=\"validate\" phx-submit=\"save\">\n  ...\n  <.input field={@form[:email]} phx-change=\"email_changed\" phx-target={@myself} />\n</.form>\n```\n\nThen your LiveView or LiveComponent would handle the event:\n\n```elixir\ndef handle_event(\"email_changed\", %{\"user\" => %{\"email\" => email}}, socket) do\n  ...\nend\n```\n\n> #### Note {: .warning}\n> 1. Only the individual input is sent as params for an input marked with `phx-change`.\n> 2. While it is possible to use `phx-change` on individual inputs, those inputs\n>    must still be within a form.\n\n## Error feedback\n\nFor proper error feedback on form updates, LiveView sends special parameters on form events\nstarting with `_unused_` to indicate that the input for the specific field has not been interacted with yet.\n\nWhen creating a form from these parameters through `Phoenix.Component.to_form/2` or `Phoenix.Component.form/1`,\n`Phoenix.Component.used_input?/1` can be used to filter error messages.\n\nFor example, your `MyAppWeb.CoreComponents` may use this function:\n\n```elixir\ndef input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do\n  errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []\n\n  assigns\n  |> assign(field: nil, id: assigns.id || field.id)\n  |> assign(:errors, Enum.map(errors, &translate_error(&1)))\n```\n\nNow only errors for fields that were interacted with are shown.\n\nTo disable sending of `_unused` parameters, you can annotate individual inputs or the whole form with\n`phx-no-unused-field`.\n\n## Number inputs\n\nNumber inputs are a special case in LiveView forms. On programmatic updates,\nsome browsers will clear invalid inputs. So LiveView will not send change events\nfrom the client when an input is invalid, instead allowing the browser's native\nvalidation UI to drive user interaction. Once the input becomes valid, change and\nsubmit events will be sent normally.\n\n```heex\n<input type=\"number\">\n```\n\nThis is known to have a plethora of problems including accessibility, large numbers\nare converted to exponential notation, and scrolling can accidentally increase or\ndecrease the number.\n\nOne alternative is the `inputmode` attribute, which may serve your application's needs\nand users much better. According to [Can I Use?](https://caniuse.com/#search=inputmode),\nthe following is supported by 94% of the global market (as of Nov 2024):\n\n```heex\n<input type=\"text\" inputmode=\"numeric\" pattern=\"[0-9]*\">\n```\n\n## Password inputs\n\nPassword inputs are also special cased in `Phoenix.HTML`. For security reasons,\npassword field values are not reused when rendering a password input tag. This\nrequires explicitly setting the `:value` in your markup, for example:\n\n```heex\n<.input field={f[:password]} value={input_value(f[:password].value)} />\n<.input field={f[:password_confirmation]} value={input_value(f[:password_confirmation].value)} />\n```\n\n## Nested inputs\n\nNested inputs are handled using `.inputs_for` function component. By default\nit will add the necessary hidden input fields for tracking ids of Ecto associations.\n\n```heex\n<.inputs_for :let={fp} field={f[:friends]}>\n  <.input field={fp[:name]} type=\"text\" />\n</.inputs_for>\n```\n\n## File inputs\n\nLiveView forms support [reactive file inputs](uploads.md),\nincluding drag and drop support via the `phx-drop-target`\nattribute:\n\n```heex\n<div class=\"container\" phx-drop-target={@uploads.avatar.ref}>\n  ...\n  <.live_file_input upload={@uploads.avatar} />\n</div>\n```\n\nSee `Phoenix.Component.live_file_input/1` for more.\n\n## Submitting the form action over HTTP\n\nThe `phx-trigger-action` attribute can be added to a form to trigger a standard\nform submit on DOM patch to the URL specified in the form's standard `action`\nattribute. This is useful to perform pre-final validation of a LiveView form\nsubmit before posting to a controller route for operations that require\nPlug session mutation. For example, in your LiveView template you can\nannotate the `phx-trigger-action` with a boolean assign:\n\n```heex\n<.form :let={f} for={@changeset}\n  action={~p\"/users/reset_password\"}\n  phx-submit=\"save\"\n  phx-trigger-action={@trigger_submit}>\n```\n\nThen in your LiveView, you can toggle the assign to trigger the form with the current\nfields on next render:\n\n    def handle_event(\"save\", params, socket) do\n      case validate_change_password(socket.assigns.user, params) do\n        {:ok, changeset} ->\n          {:noreply, assign(socket, changeset: changeset, trigger_submit: true)}\n\n        {:error, changeset} ->\n          {:noreply, assign(socket, changeset: changeset)}\n      end\n    end\n\nOnce `phx-trigger-action` is true, LiveView disconnects and then submits the form.\n\n## Recovery following crashes or disconnects\n\nBy default, all forms marked with `phx-change` and having `id`\nattribute will recover input values automatically after the user has\nreconnected or the LiveView has remounted after a crash. This is\nachieved by the client triggering the same `phx-change` to the server\nas soon as the mount has been completed.\n\n**Note:** if you want to see form recovery working in development, please\nmake sure to disable live reloading in development by commenting out the\nLiveReload plug in your `endpoint.ex` file or by setting `code_reloader: false`\nin your `config/dev.exs`. Otherwise live reloading may cause the current page\nto be reloaded whenever you restart the server, which will discard all form\nstate.\n\nFor most use cases, this is all you need and form recovery will happen\nwithout consideration. In some cases, where forms are built step-by-step in a\nstateful fashion, it may require extra recovery handling on the server outside\nof your existing `phx-change` callback code. To enable specialized recovery,\nprovide a `phx-auto-recover` binding on the form to specify a different event\nto trigger for recovery, which will receive the form params as usual. For example,\nimagine a LiveView wizard form where the form is stateful and built based on what\nstep the user is on and by prior selections:\n\n```heex\n<form id=\"wizard\" phx-change=\"validate_wizard_step\" phx-auto-recover=\"recover_wizard\">\n```\n\nOn the server, the `\"validate_wizard_step\"` event is only concerned with the\ncurrent client form data, but the server maintains the entire state of the wizard.\nTo recover in this scenario, you can specify a recovery event, such as `\"recover_wizard\"`\nabove, which would wire up to the following server callbacks in your LiveView:\n\n    def handle_event(\"validate_wizard_step\", params, socket) do\n      # regular validations for current step\n      {:noreply, socket}\n    end\n\n    def handle_event(\"recover_wizard\", params, socket) do\n      # rebuild state based on client input data up to the current step\n      {:noreply, socket}\n    end\n\nTo forgo automatic form recovery, set `phx-auto-recover=\"ignore\"`.\n\n## Resetting forms\n\nTo reset a LiveView form, you can use the standard `type=\"reset\"` on a\nform button or input. When clicked, the form inputs will be reset to their\noriginal values.\nAfter the form is reset, a `phx-change` event is emitted with the `_target` param\ncontaining the reset `name`. For example, the following element:\n\n```heex\n<form id=\"my-form\" phx-change=\"changed\">\n  ...\n  <button type=\"reset\" name=\"reset\">Reset</button>\n</form>\n```\n\nCan be handled on the server differently from your regular change function:\n\n    def handle_event(\"changed\", %{\"_target\" => [\"reset\"]} = params, socket) do\n      # handle form reset\n    end\n\n    def handle_event(\"changed\", params, socket) do\n      # handle regular form change\n    end\n\n## JavaScript client specifics\n\nThe JavaScript client is always the source of truth for current input values.\nFor any given input with focus, LiveView will never overwrite the input's current\nvalue, even if it deviates from the server's rendered updates. This works well\nfor updates where major side effects are not expected, such as form validation\nerrors, or additive UX around the user's input values as they fill out a form.\n\nFor these use cases, the `phx-change` input does not concern itself with disabling\ninput editing while an event to the server is in flight. When a `phx-change` event\nis sent to the server, the input tag and parent form tag receive the\n`phx-change-loading` CSS class, then the payload is pushed to the server with a\n`\"_target\"` param in the root payload containing the keyspace of the input name\nwhich triggered the change event.\n\nFor example, if the following input triggered a change event:\n\n```heex\n<input name=\"user[username]\"/>\n```\n\nThe server's `handle_event/3` would receive a payload:\n\n    %{\"_target\" => [\"user\", \"username\"], \"user\" => %{\"username\" => \"Name\"}}\n\nThe `phx-submit` event is used for form submissions where major side effects\ntypically happen, such as rendering new containers, calling an external\nservice, or redirecting to a new page.\n\nOn submission of a form bound with a `phx-submit` event:\n\n1. The form's inputs are set to `readonly`\n2. Any submit button on the form is disabled\n3. The form receives the `\"phx-submit-loading\"` class\n\nOn completion of server processing of the `phx-submit` event:\n\n1. The submitted form is reactivated and loses the `\"phx-submit-loading\"` class\n2. The last input with focus is restored (unless another input has received focus)\n3. Updates are patched to the DOM as usual\n\nTo handle latent events, the `<button>` tag of a form can be annotated with\n`phx-disable-with`, which swaps the element's `innerText` with the provided\nvalue during event submission. For example, the following code would change\nthe \"Save\" button to \"Saving...\", and restore it to \"Save\" on acknowledgment:\n\n```heex\n<button type=\"submit\" phx-disable-with=\"Saving...\">Save</button>\n```\n\n> #### A note on disabled buttons {: .info}\n>\n> By default, LiveView only disables submit buttons and inputs within forms\n> while waiting for a server acknowledgement. If you want a button outside of\n> a form to be disabled without changing its text, you can add `phx-disable-with`\n> without a value:\n>\n> ```heex\n>  <button type=\"button\" phx-disable-with>...</button>\n> ```\n>\n> Note also that LiveView ignores clicks on elements that are currently awaiting\n> an acknowledgement from the server. This means that although a regular button\n> without `phx-disable-with` is not semantically disabled while waiting for a\n> server response, it will not trigger duplicate events.\n>\n> Finally, `phx-disable-with` works with an element‘s `innerText`,\n> therefore nested DOM elements, like `svg` or icons, won't be preserved.\n> See \"CSS loading states\" for alternative approaches to this.\n\nYou may also take advantage of LiveView's CSS loading state classes to\nswap out your form content while the form is submitting. For example,\nwith the following rules in your `app.css`:\n\n```css\n.while-submitting { display: none; }\n.inputs { display: block; }\n\n.phx-submit-loading .while-submitting { display: block; }\n.phx-submit-loading .inputs { display: none; }\n```\n\nYou can show and hide content with the following markup:\n\n```heex\n<form id=\"my-form\" phx-change=\"update\">\n  <div class=\"while-submitting\">Please wait while we save our content...</div>\n  <div class=\"inputs\">\n    <input type=\"text\" name=\"text\" value={@text}>\n  </div>\n</form>\n```\n\nAdditionally, we strongly recommend including a unique HTML \"id\" attribute on the form.\nWhen DOM siblings change, elements without an ID will be replaced rather than moved,\nwhich can cause issues such as form fields losing focus.\n\n## Triggering `phx-` form events with JavaScript\n\nOften it is desirable to trigger an event on a DOM element without explicit\nuser interaction on the element. For example, a custom form element such as a\ndate picker or custom select input which utilizes a hidden input element to\nstore the selected state.\n\nIn these cases, the event functions on the DOM API can be used, for example\nto trigger a `phx-change` event:\n\n```javascript\ndocument.getElementById(\"my-select\").dispatchEvent(\n  new Event(\"input\", {bubbles: true})\n)\n```\n\nWhen using a client hook, `this.el` can be used to determine the element as\noutlined in the \"Client hooks\" documentation.\n\nIt is also possible to trigger a `phx-submit` using a \"submit\" event:\n\n```javascript\ndocument.getElementById(\"my-form\").dispatchEvent(\n  new Event(\"submit\", {bubbles: true, cancelable: true})\n)\n```\n\n## Preventing form submission with JavaScript\n\nIn some cases, you may want to conditionally prevent form submission based on client-side validation or other business logic before allowing a `phx-submit` to be processed by the server.\n\nJavaScript can be used to prevent the default form submission behavior, for example with a [hook](js-interop.md#client-hooks-via-phx-hook):\n\n```javascript\n/**\n * @type {import(\"phoenix_live_view\").HooksOptions}\n */\nlet Hooks = {}\nHooks.CustomFormSubmission = {\n  mounted() {\n    this.el.addEventListener(\"submit\", (event) => {\n      if (!this.shouldSubmit()) {\n        // prevent the event from bubbling to the default LiveView handler\n        event.stopPropagation()\n        // prevent the default browser behavior (submitting the form over HTTP)\n        event.preventDefault()\n      }\n    })\n  },\n  shouldSubmit() {\n    // Check if we should submit the form\n    ...\n  }\n}\n```\n\nThis hook can be set on your form as such:\n\n```heex\n<form id=\"my-form\" phx-hook=\"CustomFormSubmission\">\n  <input type=\"text\" name=\"text\" value={@text}>\n</form>\n```\n"
  },
  {
    "path": "guides/client/js-interop.md",
    "content": "# JavaScript interoperability\n\nTo enable LiveView client/server interaction, we instantiate a LiveSocket. For example:\n\n```javascript\nimport {Socket} from \"phoenix\"\nimport {LiveSocket} from \"phoenix_live_view\"\n\nlet csrfToken = document.querySelector(\"meta[name='csrf-token']\").getAttribute(\"content\")\nlet liveSocket = new LiveSocket(\"/live\", Socket, {params: {_csrf_token: csrfToken}})\nliveSocket.connect()\n```\n\nAll options are passed directly to the `Phoenix.Socket` constructor,\nexcept for the following LiveView specific options:\n\n  * `bindingPrefix` - the prefix to use for phoenix bindings. Defaults `\"phx-\"`\n  * `params` - the `connect_params` to pass to the view's mount callback. May be\n    a literal object or closure returning an object. When a closure is provided,\n    the function receives the view's element.\n  * `hooks` - a reference to a user-defined hooks namespace, containing client\n    callbacks for server/client interop. See the [Client hooks](#client-hooks-via-phx-hook)\n    section below for details.\n  * `uploaders` - a reference to a user-defined uploaders namespace, containing\n    client callbacks for client-side direct-to-cloud uploads. See the\n    [External uploads guide](external-uploads.md) for details.\n  * `metadata` - additional user-defined metadata that is sent along events to the server.\n    See the [Key events](bindings.html#key-events) section in the bindings guide\n    for an example.\n\nThe `liveSocket` instance exposes the following methods:\n- `connect()` - call this once after creation to connect to the server\n- `enableDebug()` -  turns on debug logging, see [Debugging client events](#debugging-client-events)\n- `disableDebug()` -  turns off debug logging\n- `enableLatencySim(milliseconds)` - turns on latency simulation, see [Simulating latency](#simulating-latency)\n- `disableLatencySim()` - turns off latency simulation\n- `execJS(el, encodedJS)` - executes encoded JS command in the context of the element\n- `js()` - returns an object with methods to manipulate the DOM and execute JS commands. The applied changes integrate with server DOM patching. See [JS commands](#js-commands).\n\n## Debugging client events\n\nTo aid debugging on the client when troubleshooting issues, the `enableDebug()`\nand `disableDebug()` functions are exposed on the `LiveSocket` JavaScript instance.\nCalling `enableDebug()` turns on debug logging which includes LiveView life-cycle and\npayload events as they come and go from client to server. In practice, you can expose\nyour instance on `window` for quick access in the browser's web console, for example:\n\n```javascript\n// app.js\nlet liveSocket = new LiveSocket(...)\nliveSocket.connect()\nwindow.liveSocket = liveSocket\n\n// in the browser's web console\n>> liveSocket.enableDebug()\n```\n\nThe debug state uses the browser's built-in `sessionStorage`, so it will remain in effect\nfor as long as your browser session lasts.\n\n## Simulating Latency\n\nProper handling of latency is critical for good UX. LiveView's CSS loading states allow\nthe client to provide user feedback while awaiting a server response. In development,\nnear zero latency on localhost does not allow latency to be easily represented or tested,\nso LiveView includes a latency simulator with the JavaScript client to ensure your\napplication provides a pleasant experience. Like the `enableDebug()` function above,\nthe `LiveSocket` instance includes `enableLatencySim(milliseconds)` and `disableLatencySim()`\nfunctions which apply throughout the current browser session. The `enableLatencySim` function\naccepts an integer in milliseconds for the one-way latency to and from the server. For example:\n\n```javascript\n// app.js\nlet liveSocket = new LiveSocket(...)\nliveSocket.connect()\nwindow.liveSocket = liveSocket\n\n// in the browser's web console\n>> liveSocket.enableLatencySim(1000)\n[Log] latency simulator enabled for the duration of this browser session.\n      Call disableLatencySim() to disable\n```\n\n## Handling server-pushed events\n\nWhen the server uses `Phoenix.LiveView.push_event/3`, the event name\nwill be dispatched in the browser with the `phx:` prefix. For example,\nimagine the following template where you want to highlight an existing\nelement from the server to draw the user's attention:\n\n```heex\n<div id={\"item-#{item.id}\"} class=\"item\">\n  {item.title}\n</div>\n```\n\nNext, the server can issue a highlight using the standard `push_event`:\n\n```elixir\ndef handle_info({:item_updated, item}, socket) do\n  {:noreply, push_event(socket, \"highlight\", %{id: \"item-#{item.id}\"})}\nend\n```\n\nFinally, a window event listener can listen for the event and conditionally\nexecute the highlight command if the element matches:\n\n```javascript\nlet liveSocket = new LiveSocket(...)\nwindow.addEventListener(\"phx:highlight\", (e) => {\n  let el = document.getElementById(e.detail.id)\n  if(el) {\n    // logic for highlighting\n  }\n})\n```\n\nIf you desire, you can also integrate this functionality with Phoenix'\nJS commands, executing JS commands for the given element whenever highlight\nis triggered. First, update the element to embed the JS command into a data\nattribute:\n\n```heex\n<div id={\"item-#{item.id}\"} class=\"item\" data-highlight={JS.transition(\"highlight\")}>\n  {item.title}\n</div>\n```\n\nNow, in the event listener, use `LiveSocket.execJS` to trigger all JS\ncommands in the new attribute:\n\n```javascript\nlet liveSocket = new LiveSocket(...)\nwindow.addEventListener(\"phx:highlight\", (e) => {\n  document.querySelectorAll(`[data-highlight]`).forEach(el => {\n    if(el.id == e.detail.id){\n      liveSocket.execJS(el, el.getAttribute(\"data-highlight\"))\n    }\n  })\n})\n```\n\n## Client hooks via `phx-hook`\n\nTo handle custom client-side JavaScript when an element is added, updated,\nor removed by the server, a hook object may be provided via `phx-hook`.\n`phx-hook` must point to an object with the following life-cycle callbacks:\n\n  * `mounted` - the element has been added to the DOM and its server\n    LiveView has finished mounting\n  * `beforeUpdate` - the element is about to be updated in the DOM.\n    *Note*: any call here must be synchronous as the operation cannot\n    be deferred or cancelled.\n  * `updated` - the element has been updated in the DOM by the server.\n    *Note*: `window.location` may not reflect the current URL during this callback.\n    For navigation-aware logic, use the `phx:navigate` event instead.\n  * `destroyed` - the element has been removed from the page, either\n    by a parent update, or by the parent being removed entirely\n  * `disconnected` - the element's parent LiveView has disconnected from the server\n  * `reconnected` - the element's parent LiveView has reconnected to the server\n\n*Note:* When using hooks outside the context of a LiveView, `mounted` is the only\ncallback invoked, and only those elements on the page at DOM ready will be tracked.\nFor dynamic tracking of the DOM as elements are added, removed, and updated, a LiveView\nshould be used.\n\nThe above life-cycle callbacks have in-scope access to the following attributes:\n\n  * `el` - attribute referencing the bound DOM node\n  * `liveSocket` - the reference to the underlying `LiveSocket` instance\n  * `pushEvent(event, payload, (reply, ref) => ...)` - method to push an event from the client to the LiveView server.\n    If no callback function is passed, a promise that resolves to the `reply` is returned.\n  * `pushEventTo(selectorOrTarget, event, payload, (reply, ref) => ...)` - method to push targeted events from the client\n    to LiveViews and LiveComponents. It sends the event to the LiveComponent or LiveView the `selectorOrTarget` is\n    defined in, where its value can be either a query selector or an actual DOM element. If the query selector returns\n    more than one element it will send the event to all of them, even if all the elements are in the same LiveComponent\n    or LiveView. `pushEventTo` supports passing the node element e.g. `this.el` instead of selector e.g. `\"#\" + this.el.id`\n    as the first parameter for target.\n    As there can be multiple targets, if no callback is passed, a promise is returned that matches the return value of\n    [`Promise.allSettled()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled#return_value). Individual fulfilled values are of the format `{ reply, ref }`.\n  * `handleEvent(event, (payload) => ...)` - method to handle an event pushed from the server. Returns a value that can be passed to `removeHandleEvent` to remove the event handler.\n  * `removeHandleEvent(ref)` - method to remove an event handler added via `handleEvent`\n  * `upload(name, files)` - method to inject a list of file-like objects into an uploader.\n  * `uploadTo(selectorOrTarget, name, files)` - method to inject a list of file-like objects into an uploader.\n    The hook will send the files to the uploader with `name` defined by [`allow_upload/3`](`Phoenix.LiveView.allow_upload/3`)\n    on the server-side. Dispatching new uploads triggers an input change event which will be sent to the\n    LiveComponent or LiveView the `selectorOrTarget` is defined in, where its value can be either a query selector or an\n    actual DOM element. If the query selector returns more than one live file input, an error will be logged.\n  * `js()` - returns an object with methods to manipulate the DOM and execute JavaScript. The applied changes integrate with server DOM patching. See [JS commands](#js-commands).\n\nFor example, the markup for a controlled input for phone-number formatting could be written\nlike this:\n\n```heex\n<input type=\"text\" name=\"user[phone_number]\" id=\"user-phone-number\" phx-hook=\"PhoneNumber\" />\n```\n\nThen a hook callback object could be defined and passed to the socket:\n\n```javascript\n/**\n * @type {import(\"phoenix_live_view\").HooksOptions}\n */\nlet Hooks = {}\nHooks.PhoneNumber = {\n  mounted() {\n    this.el.addEventListener(\"input\", e => {\n      let match = this.el.value.replace(/\\D/g, \"\").match(/^(\\d{3})(\\d{3})(\\d{4})$/)\n      if(match) {\n        this.el.value = `${match[1]}-${match[2]}-${match[3]}`\n      }\n    })\n  }\n}\n\nlet liveSocket = new LiveSocket(\"/live\", Socket, {hooks: Hooks, ...})\n...\n```\n\n*Note*: when using `phx-hook`, a unique DOM ID must always be set.\n\nFor integration with client-side libraries which require a broader access to full\nDOM management, the `LiveSocket` constructor accepts a `dom` option with an\n`onBeforeElUpdated` callback. The `fromEl` and `toEl` DOM nodes are passed to the\nfunction just before the DOM patch operations occurs in LiveView. This allows external\nlibraries to (re)initialize DOM elements or copy attributes as necessary as LiveView\nperforms its own patch operations. The update operation cannot be cancelled or deferred,\nand the return value is ignored.\n\nFor example, the following option could be used to guarantee that some attributes set on the client-side are kept intact:\n\n```javascript\n...\nlet liveSocket = new LiveSocket(\"/live\", Socket, {\n  params: {_csrf_token: csrfToken},\n  hooks: Hooks,\n  dom: {\n    onBeforeElUpdated(from, to) {\n      for (const attr of from.attributes) {\n        if (attr.name.startsWith(\"data-js-\")) {\n          to.setAttribute(attr.name, attr.value);\n        }\n      }\n    }\n  }\n})\n```\n\nIn the example above, all attributes starting with `data-js-` won't be replaced when the DOM is patched by LiveView.\n\nA hook can also be defined as a subclass of `ViewHook`:\n\n```javascript\nimport { ViewHook } from \"phoenix_live_view\"\n\nclass MyHook extends ViewHook {\n  mounted() {\n    ...\n  }\n}\n\nlet liveSocket = new LiveSocket(..., {\n  hooks: {\n    MyHook\n  }\n})\n```\n\n### Colocated Hooks / Colocated JavaScript\n\nWhen writing components that require some more control over the DOM, it often feels inconvenient to\nhave to write a hook in a separate file. Instead, one wants to have the hook logic right next to the component\ncode. For such cases, HEEx supports `Phoenix.LiveView.ColocatedHook` and `Phoenix.LiveView.ColocatedJS`.\n\nLet's see an example:\n\n```elixir\ndef phone_number_input(assigns) do\n  ~H\"\"\"\n  <input type=\"text\" name=\"user[phone_number]\" id=\"user-phone-number\" phx-hook=\".PhoneNumber\" />\n  <script :type={Phoenix.LiveView.ColocatedHook} name=\".PhoneNumber\">\n    export default {\n      mounted() {\n        this.el.addEventListener(\"input\", e => {\n          let match = this.el.value.replace(/\\D/g, \"\").match(/^(\\d{3})(\\d{3})(\\d{4})$/)\n          if(match) {\n            this.el.value = `${match[1]}-${match[2]}-${match[3]}`\n          }\n        })\n      }\n    }\n  </script>\n  \"\"\"\nend\n```\n\nWhen LiveView finds a `<script>` element with `:type={ColocatedHook}`, it will extract the\nhook code at compile time and write it into a special folder inside the `_build/` directory.\nTo use the hooks, all that needs to be done is to import the manifest into your JS bundle,\nwhich is automatically done in the `app.js` file generated by `mix phx.new` for new Phoenix 1.8 apps:\n\n```diff\n...\n  import {Socket} from \"phoenix\"\n  import {LiveSocket} from \"phoenix_live_view\"\n  import topbar from \"../vendor/topbar\"\n+ import {hooks as colocatedHooks} from \"phoenix-colocated/my_app\"\n\n  let csrfToken = document.querySelector(\"meta[name='csrf-token']\").getAttribute(\"content\")\n  let liveSocket = new LiveSocket(\"/live\", Socket, {\n    longPollFallbackMs: 2500,\n    params: {_csrf_token: csrfToken},\n+   hooks: {...colocatedHooks}\n })\n```\n\nThe `\"phoenix-colocated\"` package is a folder inside the `Mix.Project.build_path()`,\nwhich is included by default in the [`esbuild`](https://hexdocs.pm/esbuild) configuration of new\nPhoenix projects (requires `{:esbuild, \"~> 0.10\"}` or later):\n\n```elixir\nconfig :esbuild,\n  ...\n  my_app: [\n    args:\n      ~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.),\n    cd: Path.expand(\"../assets\", __DIR__),\n    env: %{\n      \"NODE_PATH\" => [Path.expand(\"../deps\", __DIR__), Mix.Project.build_path()]\n    }\n  ]\n```\n\nWhen rendering a component that includes a colocated hook, the `<script>` tag is omitted\nfrom the rendered output. Furthermore, to prevent conflicts with other components, colocated hooks\nrequire you to use the special dot syntax when naming the hook, as well as in the `phx-hook` attribute.\nLiveView will prefix the hook name by the current module name at compile time. This also means\nthat in cases where a hook is meant to be used in multiple components across a project, the hook\nshould be defined as a regular, non-colocated hook instead.\n\nYou can read more about colocated hooks [in the module documentation for `ColocatedHook`](`Phoenix.LiveView.ColocatedHook`).\nLiveView also supports colocating other JavaScript code, for more information, see `Phoenix.LiveView.ColocatedJS`.\n\n### Client-server communication\n\nA hook can push events to the LiveView by using the `pushEvent` function and receive a\nreply from the server via a `{:reply, map, socket}` return value. The reply payload will be\npassed to the optional `pushEvent` response callback.\n\nCommunication with the hook from the server can be done by reading data attributes on the\nhook element or by using `Phoenix.LiveView.push_event/3` on the server and `handleEvent` on the client.\n\nAn example of responding with `:reply` might look like this.\n\n```heex\n<div phx-hook=\"ClickMeHook\" id=\"click-me\">\n  Click me for a message!\n</div>\n```\n\n```javascript\nHooks.ClickMeHook = {\n  mounted() {\n    this.el.addEventListener(\"click\", () => {\n      // Push event to LiveView with callback for reply\n      this.pushEvent(\"get_message\", {}, (reply) => {\n        console.debug(reply.message);\n      });\n    });\n  }\n}\n```\n\nThen in your callback you respond with `{:reply, map, socket}`\n\n```elixir\ndef handle_event(\"get_message\", _params, socket) do\n  # Use :reply to respond to the pushEvent\n  {:reply, %{message: \"Hello from LiveView!\"}, socket}\nend\n```\n\nAnother example, to implement infinite scrolling, one can pass the current page using data attributes:\n\n```heex\n<div id=\"infinite-scroll\" phx-hook=\"InfiniteScroll\" data-page={@page}>\n```\n\nAnd then in the client:\n\n```javascript\n/**\n * @type {import(\"phoenix_live_view\").Hook}\n */\nHooks.InfiniteScroll = {\n  page() { return this.el.dataset.page },\n  mounted(){\n    this.pending = this.page()\n    window.addEventListener(\"scroll\", e => {\n      if(this.pending == this.page() && scrollAt() > 90){\n        this.pending = this.page() + 1\n        this.pushEvent(\"load-more\", {})\n      }\n    })\n  },\n  updated(){ this.pending = this.page() }\n}\n```\n\nHowever, the data attribute approach is not a good approach if you need to frequently push data to the client. To push out-of-band events to the client, for example to render charting points, one could do:\n\n```heex\n<div id=\"chart\" phx-hook=\"Chart\">\n```\n\nAnd then on the client:\n\n```javascript\n/**\n * @type {import(\"phoenix_live_view\").Hook}\n */\nHooks.Chart = {\n  mounted(){\n    this.handleEvent(\"points\", ({points}) => MyChartLib.addPoints(points))\n  }\n}\n```\n\nAnd then you can push events as:\n\n    {:noreply, push_event(socket, \"points\", %{points: new_points})}\n\nEvents pushed from the server via `push_event` are global and will be dispatched\nto all active hooks on the client who are handling that event. If you need to scope events\n(for example when pushing from a live component that has siblings on the current live view),\nthen this must be done by namespacing them:\n\n    def update(%{id: id, points: points} = assigns, socket) do\n      socket =\n        socket\n        |> assign(assigns)\n        |> push_event(\"points-#{id}\", points)\n\n      {:ok, socket}\n    end\n\nAnd then on the client:\n\n```javascript\nHooks.Chart = {\n  mounted(){\n    this.handleEvent(`points-${this.el.id}`, (points) => MyChartLib.addPoints(points));\n  }\n}\n```\n\n*Note*: In case a LiveView pushes events and renders content, `handleEvent` callbacks are invoked after the page is updated. Therefore, if the LiveView redirects at the same time it pushes events, callbacks won't be invoked on the old page's elements. Callbacks would be invoked on the redirected page's newly mounted hook elements.\n\n## JS commands\n\n*Note*: If possible, construct commands via Elixir using `Phoenix.LiveView.JS` and trigger them via Phoenix DOM [Bindings](bindings.md).\n\nWhile `Phoenix.LiveView.JS` allows you to construct a declarative representation of a command, it may not cover all use cases.\nIn addition, you can execute commands that integrate with server DOM patching via JavaScript using:\n- Client hooks: `this.js()` or the\n- LiveSocket instance: `liveSocket.js()`.\n\nThe command interface returned by `js()` above offers the following functions:\n- `show(el, opts = {})` - shows an element. Options: `display`, `transition`, `time`, `blocking`. For more details, see `Phoenix.LiveView.JS.show/1`.\n- `hide(el, opts = {})` - hides an element. Options: `transition`, `time`, `blocking`. For more details, see `Phoenix.LiveView.JS.hide/1`.\n- `toggle(el, opts = {})` - toggles the visibility of an element. Options: `display`, `in`, `out`, `time`, `blocking`. For more details, see `Phoenix.LiveView.JS.toggle/1`.\n- `addClass(el, names, opts = {})` - adds CSS class(es) to an element. Options: `transition`, `time`, `blocking`. For more details, see `Phoenix.LiveView.JS.add_class/1`.\n- `removeClass(el, names, opts = {})` - removes CSS class(es) to an element. Options: `transition`, `time`, `blocking`. For more details, see `Phoenix.LiveView.JS.remove_class/1`.\n- `toggleClass(el, names, opts = {})` - toggles CSS class(es) to an element. Options: `transition`, `time`, `blocking`. For more details, see `Phoenix.LiveView.JS.toggle_class/1`.\n- `transition(el, transition, opts = {})` - applies a CSS transition to an element. Options: `time`, `blocking`. For more details, see `Phoenix.LiveView.JS.transition/1`.\n- `setAttribute(el, attr, val)` - sets an attribute on an element\n- `removeAttribute(el, attr)` - removes an attribute from an element\n- `toggleAttribute(el, attr, val1, val2)` - toggles an attribute on an element between two values\n- `push(el, type, opts = {})` - pushes an event to the server. To target a LiveComponent by its ID, pass a separate `target` in the options. Options: `target`, `loading`, `page_loading`, `value`. For more details, see `Phoenix.LiveView.JS.push/1`.\n- `navigate(href, opts = {})` - sends a navigation event to the server and updates the browser's pushState history. Options: `replace`. For more details, see `Phoenix.LiveView.JS.navigate/1`.\n- `patch(href, opts = {})` - sends a patch event to the server and updates the browser's pushState history. Options: `replace`. For more details, see `Phoenix.LiveView.JS.patch/1`.\n- `exec(encodedJS)` - *only via Client hook `this.js()`*: executes encoded JS command in the context of the hook's root node. The encoded JS command should be constructed via `Phoenix.LiveView.JS` and is usually stored as an HTML attribute. Example: `this.js().exec(this.el.getAttribute('phx-remove'))`.\n- `exec(el, encodedJS)` - *only via `liveSocket.js()`*: executes encoded JS command in the context of any element.\n"
  },
  {
    "path": "guides/client/syncing-changes.md",
    "content": "# Syncing changes and optimistic UIs\n\nWhen using LiveView, whenever you change the state in your LiveView process, changes are automatically sent and applied in the client.\n\nHowever, in many occasions, the client may have its own state: inputs, buttons, focused UI elements, and more. In order to avoid server updates from destroying state on the client, LiveView provides several features and out-of-the-box conveniences.\n\nLet's start by discussing which problems may arise from client-server integration, which may apply to any web application, and explore how LiveView solves it automatically. If you want to focus on the more practical aspects, you can jump to later sections or watch the video below:\n\n<iframe width=\"560\" height=\"315\" src=\"https://www.youtube-nocookie.com/embed/fCdi7SEPrTs?si=ai_gcKZALmzc1Gy8\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen></iframe>\n\n## The problem in a nutshell\n\nImagine your web application has a form. The form has a single email input and a button. We have to validate that the email is unique in our database and render a tiny “✗” or “✓“ accordingly close to the input. Because we are using server-side rendering, we are debouncing/throttling form changes to the server. And, to avoid double-submissions, we want to disable the button as soon as it is clicked.\n\nHere is what could happen. The user has typed “hello@example.” and debounce kicks in, causing the client to send an event to the server. Here is how the client looks like at this moment:\n\n```plain\n[ hello@example.    ]\n\n    ------------\n       SUBMIT\n    ------------\n```\n\nWhile the server is processing this information, the user finishes typing the email and presses submit. The client sends the submit event to the server, then proceeds to disable the button, and change its value to “SUBMITTING”:\n\n```plain\n[ hello@example.com ]\n\n    ------------\n     SUBMITTING\n    ------------\n```\n\nImmediately after pressing submit, the client receives an update from the server, but this is an update from the debounce event! If the client were to simply render this server update, the client would effectively roll back the form to the previous state shown below, which would be a disaster:\n\n```plain\n[ hello@example.    ] ✓\n\n    ------------\n       SUBMIT\n    ------------\n```\n\nThis is a simple example of how client and server state can evolve and differ for periods of times, due to the latency (distance) between them, in any web application, not only LiveView.\n\nLiveView solves this in two ways:\n\n* The JavaScript client is always the source of truth for current input values\n\n* LiveView tracks how many events are currently in flight in a given input/button/form. The changes to the form are applied behind the scenes as they arrive, but LiveView only shows them once all in-flight events have been resolved\n\nIn other words, for the most common cases, **LiveView will automatically sync client and server state for you**. This is a huge benefit of LiveView, as many other stacks would require developers to tackle these problems themselves. For complete detail in how LiveView handles forms, see [the JavaScript client specifics in the Form Bindings page](form-bindings.md#javascript-client-specifics).\n\n## Optimistic UIs via loading classes\n\nWhenever an HTML element pushes an event to the server, LiveView will attach a `-loading` class to it. For example the following markup:\n\n```heex\n<button phx-click=\"clicked\" phx-window-keydown=\"key\">...</button>\n```\n\nOn click, would receive the `phx-click-loading` class, and on keydown would receive the `phx-keydown-loading` class. The CSS loading classes are maintained until an acknowledgement is received on the client for the pushed event. If the element is triggered several times, the loading state is removed only when all events are resolved.\n\nThis means the most trivial optimistic UI enhancements can be done in LiveView by simply adding a CSS rule. For example, imagine you want to fade the text of an element when it is clicked, while it waits for a response:\n\n```css\n.phx-click-loading.opaque-on-click {\n  opacity: 50%;\n}\n```\n\nNow, by adding the class `opaque-on-click` to any element, the elements give an immediate feedback on click.\n\nThe following events receive CSS loading classes:\n\n  - `phx-click` - `phx-click-loading`\n  - `phx-change` - `phx-change-loading`\n  - `phx-submit` - `phx-submit-loading`\n  - `phx-focus` - `phx-focus-loading`\n  - `phx-blur` - `phx-blur-loading`\n  - `phx-window-keydown` - `phx-keydown-loading`\n  - `phx-window-keyup` - `phx-keyup-loading`\n\nEvents that happen inside a form have their state applied to both the element and the form. When an input changes, `phx-change-loading` applies to both input and form. On submit, both button and form get the `phx-submit-loading` classes. Buttons, in particular, also support a `phx-disabled-with` attribute, which allows you to customize the text of the button on click:\n\n```heex\n<button phx-disable-with=\"Submitting...\">Submit</button>\n```\n\n### Tailwind integration\n\nIf you are using Tailwind, you may want to use [the `addVariant` plugin](https://tailwindcss.com/docs/plugins#adding-variants) to make it even easier to customize your elements loading state.\n\n```javascript\nplugins: [\n  plugin(({ addVariant }) => {\n    addVariant(\"phx-click-loading\", [\".phx-click-loading&\", \".phx-click-loading &\",]);\n    addVariant(\"phx-submit-loading\", [\".phx-submit-loading&\", \".phx-submit-loading &\",]);\n    addVariant(\"phx-change-loading\", [\".phx-change-loading&\", \".phx-change-loading &\",]);\n  }),\n],\n```\n\nNow to fade one element on click, you simply need to add:\n\n```heex\n<button phx-click=\"clicked\" class=\"phx-click-loading:opacity-50\">...</button>\n```\n\n## Optimistic UIs via JS commands\n\nWhile loading classes are extremely handy, they only apply to the element currently clicked. Sometimes, you may to click a \"Delete\" button but mark the whole row that holds the button as loading (for example, to fade it out).\n\nBy using JS commands, you can tell LiveView which elements get the loading state:\n\n```heex\n<button phx-click={JS.push(\"delete\", loading: \"#post-row-13\")}>Delete</button>\n```\n\nBesides custom loading elements, you can use [JS commands](`Phoenix.LiveView.JS`) for a huge variety of operations, such as adding/removing classes, toggling attributes, hiding elements, transitions, and more.\n\nFor example, imagine that you want to immediately remove an element from the page on click, you can do this:\n\n```heex\n<button phx-click={JS.push(\"delete\") |> JS.hide()}>Delete</button>\n```\n\nIf the element you want to delete is not the clicked button, but its parent (or other element), you can pass a selector to hide:\n\n```heex\n<button phx-click={JS.push(\"delete\") |> JS.hide(\"#post-row-13\")}>Delete</button>\n```\n\nOr if you'd rather add a class instead:\n\n```heex\n<button phx-click={JS.push(\"delete\") |> JS.add_class(\"opacity-50\")}>Delete</button>\n```\n\nOne key property of JS commands, such as `hide` and `add_class`, is that they are DOM-patch aware, so operations applied by the JS APIs will stick to elements across patches from the server.\n\nJS commands also include a `dispatch` function, which dispatches an event to the DOM element to trigger client-specific functionality. For example, to trigger copying to a clipboard, you may implement this event listener:\n\n```javascript\nwindow.addEventListener(\"app:clipcopy\", (event) => {\n  if (\"clipboard\" in navigator) {\n    if (event.target.tagName === \"INPUT\") {\n      navigator.clipboard.writeText(event.target.value);\n    } else {\n      navigator.clipboard.writeText(event.target.textContent);\n    }\n  } else {\n    alert(\n      \"Sorry, your browser does not support clipboard copy.\\nThis generally requires a secure origin — either HTTPS or localhost.\",\n    );\n  }\n});\n```\n\nAnd then trigger it as follows:\n\n```heex\n<button phx-click={JS.dispatch(\"app:clipcopy\", to: \"#printed-output\")}>Copy</button>\n```\n\nTransitions are also only a few characters away:\n\n```heex\n<div id=\"item\">My Item</div>\n<button phx-click={JS.transition(\"shake\", to: \"#item\")}>Shake!</button>\n```\n\nSee `Phoenix.LiveView.JS` for more examples and documentation.\n\n## Optimistic UIs via JS hooks\n\nOn the most complex cases, you can assume control of a DOM element, and control exactly how and when server updates apply to the element on the page. See [the Client hooks via `phx-hook` section in the JavaScript interoperability page](js-interop.md#client-hooks-via-phx-hook) to learn more.\n\n## Live navigation\n\nLiveView also provides mechanisms to customize and interact with navigation events.\n\n### Navigation classes\n\nThe following classes are applied to the LiveView's parent container:\n\n  - `\"phx-connected\"` - applied when the view has connected to the server\n  - `\"phx-loading\"` - applied when the view is not connected to the server\n  - `\"phx-error\"` - applied when an error occurs on the server. Note, this\n    class will be applied in conjunction with `\"phx-loading\"` if connection\n    to the server is lost.\n\n### Navigation events\n\nFor live page navigation via `<.link navigate={...}>` and `<.link patch={...}>`, their server-side equivalents `push_navigate` and `push_patch`, as well as form submits via `phx-submit`, the JavaScript events `\"phx:page-loading-start\"` and `\"phx:page-loading-stop\"` are dispatched on window. This is useful for showing main page loading status, for example:\n\n```javascript\n// app.js\nimport topbar from \"topbar\"\nwindow.addEventListener(\"phx:page-loading-start\", info => topbar.show(500))\nwindow.addEventListener(\"phx:page-loading-stop\", info => topbar.hide())\n```\n\nWithin the callback, `info.detail` will be an object that contains a `kind`\nkey, with a value that depends on the triggering event:\n\n  - `\"redirect\"` - the event was triggered by a redirect\n  - `\"patch\"` - the event was triggered by a patch\n  - `\"initial\"` - the event was triggered by initial page load\n  - `\"element\"` - the event was triggered by a `phx-` bound element, such as `phx-click`\n  - `\"error\"` - the event was triggered by an error, such as a view crash or socket disconnection\n\nAdditionally, `Phoenix.LiveView.JS.push/3` may dispatch page loading events by passing `page_loading: true` option.\n\nFor all kinds of page loading events, all but `\"element\"` will receive an additional `to` key in the info metadata pointing to the href associated with the page load. In the case of an `\"element\"` page loading event, the info will contain a `\"target\"` key containing the DOM element which triggered the page loading state.\n\nA lower level `phx:navigate` event is also triggered any time the browser's URL bar is programmatically changed by Phoenix or the user navigation forward or back. The `info.detail` will contain the following information:\n\n  - `\"href\"` - the location the URL bar was navigated to.\n  - `\"patch\"` - the boolean flag indicating this was a patch navigation.\n  - `\"pop\"` - the boolean flag indication this was a navigation via `popstate`\n    from a user navigation forward or back in history.\n\nFor navigation-aware logic, prefer `phx:navigate` over hook callbacks like `updated()`,\nas hooks may fire before `window.location` is updated.\n"
  },
  {
    "path": "guides/introduction/welcome.md",
    "content": "# Welcome\n\nWelcome to Phoenix LiveView documentation. Phoenix LiveView enables\nrich, real-time user experiences with server-rendered HTML. A general\noverview of LiveView and its benefits is [available in our README](https://github.com/phoenixframework/phoenix_live_view).\n\n## What is a LiveView?\n\nLiveViews are processes that receive events, update their state,\nand render updates to a page as diffs.\n\nThe LiveView programming model is declarative: instead of saying\n\"once event X happens, change Y on the page\", events in LiveView\nare regular messages which may cause changes to the state. Once\nthe state changes, the LiveView will re-render the relevant parts of\nits HTML template and push it to the browser, which updates the page\nin the most efficient manner.\n\nLiveView state is nothing more than functional and immutable\nElixir data structures. The events are either internal application messages\n(usually emitted by `Phoenix.PubSub`) or sent by the client/browser.\n\nEvery LiveView is first rendered statically as part of a regular\nHTTP request, which provides quick times for \"First Meaningful\nPaint\", in addition to helping search and indexing engines.\nA persistent connection is then established between the client and\nserver. This allows LiveView applications to react faster to user\nevents as there is less work to be done and less data to be sent\ncompared to stateless requests that have to authenticate, decode, load,\nand encode data on every request.\n\n## Example\n\nLiveView is included by default in Phoenix applications.\nTherefore, to use LiveView, you must have already installed Phoenix\nand created your first application. If you haven't done so,\ncheck [Phoenix' installation guide](https://hexdocs.pm/phoenix/installation.html)\nto get started.\n\nThe behaviour of a LiveView is outlined by a module which implements\na series of functions as callbacks. Let's see an example. Write the\nfile below to `lib/my_app_web/live/thermostat_live.ex`. Remember to replace the\ndirectory `my_app_web` and the module `MyAppWeb` with your app's name:\n\n```elixir\ndefmodule MyAppWeb.ThermostatLive do\n  use MyAppWeb, :live_view\n\n  def render(assigns) do\n    ~H\"\"\"\n    Current temperature: {@temperature}°F\n    <button phx-click=\"inc_temperature\">+</button>\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    temperature = 70 # Let's assume a fixed temperature for now\n    {:ok, assign(socket, :temperature, temperature)}\n  end\n\n  def handle_event(\"inc_temperature\", _params, socket) do\n    {:noreply, update(socket, :temperature, &(&1 + 1))}\n  end\nend\n```\n\nThe module above defines three functions (they are callbacks\nrequired by LiveView). The first one is `render/1`,\nwhich receives the socket `assigns` and is responsible for returning\nthe content to be rendered on the page. We use the `~H` sigil to define\na HEEx template, which stands for HTML+EEx. They are an extension of\nElixir's builtin EEx templates, with support for HTML validation, syntax-based\ncomponents, smart change tracking, and more. You can learn more about\nthe template syntax in `Phoenix.Component.sigil_H/2` (note\n`Phoenix.Component` is automatically imported when you use `Phoenix.LiveView`).\n\nThe data used on rendering comes from the `mount` callback. The\n`mount` callback is invoked when the LiveView starts. In it, you\ncan access the request parameters, read information stored in the\nsession (typically information which identifies who is the current\nuser), and a socket. The socket is where we keep all state, including\nassigns. `mount` proceeds to assign a default temperature to the socket.\nBecause Elixir data structures are immutable, LiveView APIs often\nreceive the socket and return an updated socket. Then we return\n`{:ok, socket}` to signal that we were able to mount the LiveView\nsuccessfully. After `mount`, LiveView will render the page with the\nvalues from `assigns` and send it to the client.\n\nIf you look at the HTML rendered, you will notice there is a button\nwith a `phx-click` attribute. When the button is clicked, a\n`\"inc_temperature\"` event is sent to the server, which is matched and\nhandled by the `handle_event` callback. This callback updates the socket\nand returns `{:noreply, socket}` with the updated socket.\n`handle_*` callbacks in LiveView (and in Elixir in general) are\ninvoked based on some action, in this case, the user clicking a button.\nThe `{:noreply, socket}` return means there is no additional replies\nsent to the browser, only that a new version of the page is rendered.\nLiveView then computes diffs and sends them to the client.\n\nNow we are ready to render our LiveView. You can serve the LiveView\ndirectly from your router:\n\n```elixir\ndefmodule MyAppWeb.Router do\n  use MyAppWeb, :router\n\n  pipeline :browser do\n    ...\n  end\n\n  scope \"/\", MyAppWeb do\n    pipe_through :browser\n    ...\n\n    live \"/thermostat\", ThermostatLive\n  end\nend\n```\n\nOnce the LiveView is rendered, a regular HTML response is sent. When you\ngenerate your Phoenix app with `mix phx.new`, the installer also creates an `./assets/js/app.js` file with the\nfollowing code:\n\n```javascript\nimport {Socket} from \"phoenix\"\nimport {LiveSocket} from \"phoenix_live_view\"\n\nlet csrfToken = document.querySelector(\"meta[name='csrf-token']\").getAttribute(\"content\")\nlet liveSocket = new LiveSocket(\"/live\", Socket, {params: {_csrf_token: csrfToken}})\nliveSocket.connect()\n```\n\nBecause of this, the JavaScript client will connect over WebSockets and `mount/3`\nwill be invoked inside a spawned LiveView process.\n\n## Parameters and session\n\nThe mount callback receives three arguments: the request parameters, the session, and the socket.\n\nThe parameters can be used to read information from the URL. For example, assuming you have a `Thermostat` module defined somewhere that can read this information based on the house name, you could write this:\n\n```elixir\ndef mount(%{\"house\" => house}, _session, socket) do\n  temperature = Thermostat.get_house_reading(house)\n  {:ok, assign(socket, :temperature, temperature)}\nend\n```\n\nAnd then in your router:\n\n```elixir\nlive \"/thermostat/:house\", ThermostatLive\n```\n\nThe session retrieves information from a signed (or encrypted) cookie. This is where you can store authentication information, such as `current_user_id`:\n\n```elixir\ndef mount(_params, %{\"current_user_id\" => user_id}, socket) do\n  temperature = Thermostat.get_user_reading(user_id)\n  {:ok, assign(socket, :temperature, temperature)}\nend\n```\n\n> Phoenix comes with built-in authentication generators. See `mix phx.gen.auth`.\n\nMost times, in practice, you will use both:\n\n```elixir\ndef mount(%{\"house\" => house}, %{\"current_user_id\" => user_id}, socket) do\n  temperature = Thermostat.get_house_reading(user_id, house)\n  {:ok, assign(socket, :temperature, temperature)}\nend\n```\n\nIn other words, you want to read the information about a given house, as long as the user has access to it.\n\n## Bindings\n\nPhoenix supports DOM element bindings for client-server interaction. For\nexample, to react to a click on a button, you would render the element:\n\n```heex\n<button phx-click=\"inc_temperature\">+</button>\n```\n\nThen on the server, all LiveView bindings are handled with the `handle_event/3`\ncallback, for example:\n\n    def handle_event(\"inc_temperature\", _value, socket) do\n      {:noreply, update(socket, :temperature, &(&1 + 1))}\n    end\n\nTo update UI state, for example, to open and close dropdowns, switch tabs,\netc, LiveView also supports JS commands (`Phoenix.LiveView.JS`), which\nexecute directly on the client without reaching the server. To learn more,\nsee [our bindings page](bindings.md) for a complete list of all LiveView\nbindings as well as our [JavaScript interoperability guide](js-interop.md).\n\nLiveView has built-in support for forms, including uploads and association\nmanagement. See `Phoenix.Component.form/1` as a starting point and\n`Phoenix.Component.inputs_for/1` for working with associations.\nThe [Uploads](uploads.md) and [Form bindings](form-bindings.md) guides provide\nmore information about advanced features.\n\n## Navigation\n\nLiveView provides functionality to allow page navigation using the\n[browser's pushState API](https://developer.mozilla.org/en-US/docs/Web/API/History_API).\nWith live navigation, the page is updated without a full page reload.\n\nYou can either *patch* the current LiveView, updating its URL, or\n*navigate* to a new LiveView. You can learn more about them in the\n[Live Navigation](live-navigation.md) guide.\n\n## Generators\n\nPhoenix v1.6 and later includes code generators for LiveView. If you want to see\nan example of how to structure your application, from the database all the way up\nto LiveViews, run the following within a LiveView project:\n\n```shell\n$ mix phx.gen.live Blog Post posts title:string body:text\n```\n\nFor more information, run `mix help phx.gen.live`.\n\nFor authentication, with built-in LiveView support, run `mix phx.gen.auth Account User users`.\n\n## Compartmentalize state, markup, and events in LiveView\n\nLiveView supports two extension mechanisms: function components, provided by\n`HEEx` templates, and stateful components, known as LiveComponents.\n\n### Function components to organize markup and event handling\n\nSimilar to `render(assigns)` in our LiveView, a function component is any\nfunction that receives an assigns map and returns a `~H` template. For example:\n\n    def weather_greeting(assigns) do\n      ~H\"\"\"\n      <div title=\"My div\" class={@class}>\n        <p>Hello {@name}</p>\n        <MyApp.Weather.city name=\"Kraków\"/>\n      </div>\n      \"\"\"\n    end\n\nYou can learn more about function components in the `Phoenix.Component`\nmodule. At the end of the day, they are a useful mechanism for code organization\nand to reuse markup in your LiveViews.\n\nSometimes you need to share more than just markup across LiveViews. When you also\nwant to move events to a separate module, or use the same event handler in multiple\nplaces, function components can be paired with\n[`Phoenix.LiveView.attach_hook/4`](`Phoenix.LiveView.attach_hook/4#sharing-event-handling-logic`).\n\n### Live components to encapsulate additional state\n\nA component will occasionally need control over not only its own events,\nbut also its own separate state. For these cases, LiveView\nprovides `Phoenix.LiveComponent`, which are rendered using\n[`live_component/1`](`Phoenix.Component.live_component/1`):\n\n```heex\n<.live_component module={UserComponent} id={user.id} user={user} />\n```\n\nLiveComponents have their own `mount/1` and `handle_event/3` callbacks, as well\nas their own state with change tracking support, similar to LiveViews. They are\nlightweight since they \"run\" in the same process as the parent LiveView, but\nare more complex than function components themselves. Given they all run in the\nsame process, errors in components cause the whole view to fail to render.\nFor a complete rundown, see `Phoenix.LiveComponent`.\n\nWhen in doubt over [Functional components or live components?](`Phoenix.LiveComponent#functional-components-or-live-components`), default to the former.\nRely on the latter only when you need the additional state.\n\n### live_render/3 to encapsulate state (with error isolation)\n\nFinally, if you want complete isolation between parts of a LiveView, you can\nalways render a LiveView inside another LiveView by calling\n[`live_render/3`](`Phoenix.Component.live_render/3`). This child LiveView\nruns in a separate process than the parent, with its own callbacks. If a child\nLiveView crashes, it won't affect the parent. If the parent crashes, all children\nare terminated.\n\nWhen rendering a child LiveView, the `:id` option is required to uniquely\nidentify the child. A child LiveView will only ever be rendered and mounted\na single time, provided its ID remains unchanged. To force a child to re-mount\nwith new session data, a new ID must be provided.\n\nGiven that it runs in its own process, a nested LiveView is an excellent tool\nfor creating completely isolated UI elements, but it is a slightly expensive\nabstraction if all you want is to compartmentalize markup or events (or both).\n\n### Summary\n  * use `Phoenix.Component` for code organization and reusing markup (optionally with [`attach_hook/4`](`Phoenix.LiveView.attach_hook/4#sharing-event-handling-logic`) for event handling reuse)\n  * use `Phoenix.LiveComponent` for sharing state, markup, and events between LiveViews\n  * use nested `Phoenix.LiveView` to compartmentalize state, markup, and events (with error isolation)\n\n## Guides\n\nThis documentation is split into two categories. We have the API\nreference for all LiveView modules, that's where you will learn\nmore about `Phoenix.Component`, `Phoenix.LiveView`, and so on.\n\nLiveView also has many guides to help you on your journey,\nsplit on server-side and client-side:\n\n### Server-side\n\nThese guides focus on server-side functionality:\n\n* [Assigns and HEEx templates](assigns-eex.md)\n* [Deployments and recovery](deployments.md)\n* [Error and exception handling](error-handling.md)\n* [Gettext for internationalization](gettext.md)\n* [Live layouts](live-layouts.md)\n* [Live navigation](live-navigation.md)\n* [Security considerations](security-model.md)\n* [Telemetry](telemetry.md)\n* [Uploads](uploads.md)\n\n### Client-side\n\nThese guides focus on LiveView bindings and client-side integration:\n\n* [Bindings](bindings.md)\n* [External uploads](external-uploads.md)\n* [Form bindings](form-bindings.md)\n* [JavaScript interoperability](js-interop.md)\n* [Syncing changes and optimistic UIs](syncing-changes.md)\n"
  },
  {
    "path": "guides/server/assigns-eex.md",
    "content": "# Assigns and HEEx templates\n\nAll of the data in a LiveView is stored in the socket, which is a server\nside struct called `Phoenix.LiveView.Socket`. Your own data is stored\nunder the `assigns` key of said struct. The server data is never shared\nwith the client beyond what your template renders.\n\nPhoenix template language is called HEEx (HTML+EEx). EEx is Embedded\nElixir, an Elixir string template engine. Those templates\nare either files with the `.heex` extension or they are created\ndirectly in source files via the `~H` sigil. You can learn more about\nthe HEEx syntax by checking the docs for [the `~H` sigil](`Phoenix.Component.sigil_H/2`).\n\nThe `Phoenix.Component.assign/2` and `Phoenix.Component.assign/3`\nfunctions help store those values. Those values can be accessed\nin the LiveView as `socket.assigns.name` but they are accessed\ninside HEEx templates as `@name`.\n\nIn this section, we are going to cover how LiveView minimizes\nthe payload over the wire by understanding the interplay between\nassigns and templates.\n\n## Change tracking\n\nWhen you first render a `.heex` template, it will send all of the\nstatic and dynamic parts of the template to the client. Imagine the\nfollowing template:\n\n```heex\n<h1>{expand_title(@title)}</h1>\n```\n\nIt has two static parts, `<h1>` and `</h1>` and one dynamic part\nmade of `expand_title(@title)`. Further rendering of this template\nwon't resend the static parts and it will only resend the dynamic\npart if it changes.\n\nThe tracking of changes is done via assigns. If the `@title` assign\nchanges, then LiveView will execute the dynamic parts of the template,\n`expand_title(@title)`, and send the new content. If `@title` is the same,\nnothing is executed and nothing is sent.\n\nChange tracking also works when accessing map/struct fields.\nTake this template:\n\n```heex\n<div id={\"user_#{@user.id}\"}>\n  {@user.name}\n</div>\n```\n\nIf the `@user.name` changes but `@user.id` doesn't, then LiveView\nwill re-render only `@user.name` and it will not execute or resend `@user.id`\nat all.\n\nThe change tracking also works when rendering other templates as\nlong as they are also `.heex` templates:\n\n```heex\n{render(\"child_template.html\", assigns)}\n```\n\nOr when using function components:\n\n```heex\n<.show_name name={@user.name} />\n```\n\nThe assign tracking feature also implies that you MUST avoid performing\ndirect operations in the template. For example, if you perform a database\nquery in your template:\n\n```heex\n<%= for user <- Repo.all(User) do %>\n  {user.name}\n<% end %>\n```\n\nThen Phoenix will never re-render the section above, even if the number of\nusers in the database changes. Instead, you need to store the users as\nassigns in your LiveView before it renders the template:\n\n    assign(socket, :users, Repo.all(User))\n\nGenerally speaking, **data loading should never happen inside the template**,\nregardless if you are using LiveView or not. The difference is that LiveView\nenforces this best practice.\n\n## Common pitfalls\n\nThere are some common pitfalls to keep in mind when using the `~H` sigil\nor `.heex` templates inside LiveViews.\n\n### Variables\n\nDue to the scope of variables, LiveView has to disable change tracking\nwhenever variables are used in the template, with the exception of\nvariables introduced by Elixir block constructs such as `case`,\n`for`, `if`, and others. Therefore, you **must avoid** code like\nthis in your HEEx templates:\n\n```heex\n<% some_var = @x + @y %>\n{some_var}\n```\n\nInstead, use a function:\n\n```heex\n{sum(@x, @y)}\n```\n\nSimilarly, **do not** define variables at the top of your `render` function\nfor LiveViews or LiveComponents. Since LiveView cannot track `sum` or `title`,\nif either value changes, both must be re-rendered by LiveView.\n\n    def render(assigns) do\n      sum = assigns.x + assigns.y\n      title = assigns.title\n\n      ~H\"\"\"\n      <h1>{title}</h1>\n\n      {sum}\n      \"\"\"\n    end\n\nInstead use the `assign/2`, `assign/3`, `assign_new/3`, and `update/3`\nfunctions to compute it. Any assign defined or updated this way will be marked as\nchanged, while other assigns like `@title` will still be tracked by LiveView.\n\n    assign(assigns, sum: assigns.x + assigns.y)\n\nThe same functions can be used inside function components too:\n\n    attr :x, :integer, required: true\n    attr :y, :integer, required: true\n    attr :title, :string, required: true\n    def sum_component(assigns) do\n      assigns = assign(assigns, sum: assigns.x + assigns.y)\n\n      ~H\"\"\"\n      <h1>{@title}</h1>\n\n      {@sum}\n      \"\"\"\n    end\n\nGenerally speaking, avoid accessing variables inside `HEEx` templates, as code that\naccess variables is always executed on every render. The exception are variables\nintroduced by Elixir's block constructs, such as `if` and `for` comprehensions.\nFor example, accessing the `post` variable defined by the comprehension below\nworks as expected:\n\n```heex\n<%= for post <- @posts do %>\n  ...\n<% end %>\n```\n\n### The `assigns` variable\n\nWhen talking about variables, it is also worth discussing the `assigns`\nspecial variable. Every time you use the `~H` sigil, you must define an\n`assigns` variable, which is also available on every `.heex` template.\nHowever, we must avoid accessing this variable directly inside templates\nand instead use `@` for accessing specific keys. This also applies to\nfunction components. Let's see some examples.\n\nSometimes you might want to pass all assigns from one function component to\nanother. For example, imagine you have a complex `card` component with\nheader, content and footer section. You might refactor your component\ninto three smaller components internally:\n\n```elixir\ndef card(assigns) do\n  ~H\"\"\"\n  <div class=\"card\">\n    <.card_header {assigns} />\n    <.card_body {assigns} />\n    <.card_footer {assigns} />\n  </div>\n  \"\"\"\nend\n\ndefp card_header(assigns) do\n  ...\nend\n\ndefp card_body(assigns) do\n  ...\nend\n\ndefp card_footer(assigns) do\n  ...\nend\n```\n\nBecause of the way function components handle attributes, the above code will\nnot perform change tracking and it will always re-render all three components\non every change.\n\nGenerally, you should avoid passing all assigns and instead be explicit about\nwhich assigns the child components need:\n\n```elixir\ndef card(assigns) do\n  ~H\"\"\"\n  <div class=\"card\">\n    <.card_header title={@title} class={@title_class} />\n    <.card_body>\n      {render_slot(@inner_block)}\n    </.card_body>\n    <.card_footer on_close={@on_close} />\n  </div>\n  \"\"\"\nend\n```\n\nIf you really need to pass all assigns you should instead use the regular\nfunction call syntax. This is the only case where accessing `assigns` inside\ntemplates is acceptable:\n\n```elixir\ndef card(assigns) do\n  ~H\"\"\"\n  <div class=\"card\">\n    {card_header(assigns)}\n    {card_body(assigns)}\n    {card_footer(assigns)}\n  </div>\n  \"\"\"\nend\n```\n\nThis ensures that the change tracking information from the parent component\nis passed to each child component, only re-rendering what is necessary.\nHowever, generally speaking, it is best to avoid passing `assigns` altogether\nand instead let LiveView figure out the best way to track changes.\n\n### Modifying the `assigns` variable\n\nNever modify the `assigns` variable in a function component through generic functions like `Map.put/3` or `Map.merge/2`. Instead, use `Phoenix.Component.assign/2`, `Phoenix.Component.assign/3`, `Phoenix.Component.assign_new/3`, or `Phoenix.Component.update/3`. If you modify the `assigns` variable with e.g. `Map.put/3`, those assigns inside your `HEEx` template will not update after the initial render. Using the LiveView specific assign functions is required for change tracking to work.\n\nSo, **never do this**:\n\n```elixir\ndef card(assigns) do\n  assigns = Map.put(assigns, :sum, Enum.sum(assigns.values))\n\n  ~H\"\"\"\n  <p>{@sum}</p>\n  \"\"\"\nend\n```\n\nBut instead do this:\n\n```elixir\ndef card(assigns) do\n  assigns = assign(assigns, :sum, Enum.sum(assigns.values))\n\n  ~H\"\"\"\n  <p>{@sum}</p>\n  \"\"\"\nend\n```\n\nIf you use `Map.put/3` instead of `assign/3` here, the `sum` assign will not update if you change the `values` after the initial render of the `HEEx` template.\n\n### Comprehensions\n\nHEEx supports comprehensions in templates, which is a way to traverse lists\nand collections. For example:\n\n```heex\n<%= for post <- @posts do %>\n  <section>\n    <h1>{expand_title(post.title)}</h1>\n  </section>\n<% end %>\n```\n\nOr using the special `:for` attribute:\n\n```heex\n<section :for={post <- @posts}>\n  <h1>{expand_title(post.title)}</h1>\n</section>\n```\n\nComprehensions in templates are optimized so the static parts of\na comprehension are only sent once, regardless of the number of items.\nFurthermore, LiveView tracks changes within the collection given to the\ncomprehension. In the ideal case, if only a single entry in `@posts`\nchanges, only this entry is sent again. By default, the index is used\nto track changes. This means that if an entry is inserted, any entries after\nthat index will be considered changed and sent again. To optimize this, you can\nalso pass a `:key` on tags in HEEx:\n\n```heex\n<section :for={post <- @posts} :key={post.id}>\n  <h1>{expand_title(post.title)}</h1>\n</section>\n```\n\nYou can read more about `:key` in the [documentation for `sigil_H/2`](Phoenix.Component.html#sigil_H/2-special-attributes).\n\nTo track changes in comprehensions, LiveView needs to perform additional\nbookkeeping, which requires extra memory on the server. If memory usage is a\nconcern, you should also consider to use `Phoenix.LiveView.stream/4`, which\nallows you to manage collections without keeping them in memory.\n\n### Summary\n\nTo sum up:\n\n  1. Avoid defining local variables inside HEEx templates, except within Elixir's constructs\n\n  2. Avoid passing or accessing the `assigns` variable inside HEEx templates\n\n  3. Only use LiveView specific assign functions to modify the `assigns` variable\n"
  },
  {
    "path": "guides/server/deployments.md",
    "content": "# Deployments and recovery\n\nOne of the questions that arise from LiveView stateful model is what considerations are necessary when deploying a new version of LiveView (or when recovering from an error).\n\nFirst off, whenever LiveView disconnects, it will automatically attempt to reconnect to the server using exponential back-off. This means it will try immediately, then wait 2s and try again, then 5s and so on. If you are deploying, this typically means the next reconnection will immediately succeed and your load balancer will automatically redirect to the new servers.\n\nHowever, your LiveView _may_ still have state that will be lost in this transition. How to deal with it? The good news is that there are a series of practices you can follow that will not only help with deployments but it will improve your application in general.\n\n1. Keep state in the query parameters when appropriate. For example, if your application has tabs and the user clicked a tab, instead of using `phx-click` and `c:Phoenix.LiveView.handle_event/3` to manage it, you should implement it using `<.link patch={...}>` passing the tab name as parameter. You will then receive the new tab name `c:Phoenix.LiveView.handle_params/3` which will set the relevant assign to choose which tab to display. You can even define specific URLs for each tab in your application router. By doing this, you will reduce the amount of server state, make tab navigation shareable via links, improving search engine indexing, and more.\n\n2. Consider storing other relevant state in the database. For example, if you are building a chat app and you want to store which messages have been read, you can store so in the database. Once the page is loaded, you retrieve the index of the last read message. This makes the application more robust, allow data to be synchronized across devices, etc.\n\n3. If your application uses forms (which is most likely the case), keep in mind that Phoenix performs automatic form recovery: in case of disconnections, Phoenix will collect the form data and resubmit it on reconnection. This mechanism works out of the box for most forms but you may want to customize it or test it for your most complex forms. See the relevant section [in the \"Form bindings\" document](../client/form-bindings.md) to learn more.\n\nThe idea is that: if you follow the practices above, most of your state is already handled within your app and therefore deployments should not bring additional concerns. Not only that, it will bring overall benefits to your app such as indexing, link sharing, device sharing, and so on.\n\nIf you really have complex state that cannot be immediately handled, then you may need to resort to special strategies. This may be persisting \"old\" state to Redis/S3/Database and loading the new state on the new connections. Or you may take special care when migrating connections (for example, if you are building a game, you may want to wait for on-going sessions to finish before turning down the old server while routing new sessions to the new ones). Such cases will depend on your requirements (and they would likely exist regardless of which application stack you are using).\n"
  },
  {
    "path": "guides/server/error-handling.md",
    "content": "# Error and exception handling\n\nAs with any other Elixir code, exceptions may happen during the LiveView\nlife-cycle. This page describes how LiveView handles errors at different\nstages.\n\n## Expected scenarios\n\nIn this section, we will talk about error cases that you expect to happen\nwithin your application. For example, a user filling in a form with invalid\ndata is expected. In a LiveView, we typically handle those cases by storing\nthe form state in LiveView assigns and rendering any relevant error message\nback to the client.\n\nWe may also use `flash` messages for this. For example, imagine you have a\npage to manage all \"Team members\" in an organization. However, if there is\nonly one member left in the organization, they should not be allowed to\nleave. You may want to handle this by using flash messages:\n\n    if MyApp.Org.leave(socket.assigns.current_org, member) do\n      {:noreply, socket}\n    else\n      {:noreply, put_flash(socket, :error, \"last member cannot leave organization\")}\n    end\n\nHowever, one may argue that, if the last member of an organization cannot\nleave it, it may be better to not even show the \"Leave\" button in the UI\nwhen the organization has only one member.\n\nGiven the button does not appear in the UI, triggering the \"leave\" action when\nthe organization has only one member is an unexpected scenario. This means we\ncan rewrite the code above to:\n\n    true = MyApp.Org.leave(socket.assigns.current_org, member)\n    {:noreply, socket}\n\nIf `leave` does not return `true`, Elixir will raise a `MatchError`\nexception. Or you could provide a `leave!` function that raises a specific\nexception:\n\n    MyApp.Org.leave!(socket.assigns.current_org, member)\n    {:noreply, socket}\n\nHowever, what will happen with a LiveView in case of exceptions?\nLet's talk about unexpected scenarios.\n\n## Unexpected scenarios\n\nElixir developers tend to write assertive code. This means that, if we\nexpect `leave` to always return true, we can explicitly match on its\nresult, as we did above:\n\n    true = MyApp.Org.leave(socket.assigns.current_org, member)\n    {:noreply, socket}\n\nIf `leave` fails and returns `false`, an exception is raised. It is common\nfor Elixir developers to use exceptions for unexpected scenarios in their\nPhoenix applications.\n\nFor example, if you are building an application where a user may belong to\none or more organizations, when accessing the organization page, you may want to\ncheck that the user has access to it like this:\n\n    organizations_query = Ecto.assoc(socket.assigns.current_user, :organizations)\n    Repo.get!(organizations_query, params[\"org_id\"])\n\nThe code above builds a query that returns all organizations that belongs to\nthe current user and then validates that the given `org_id` belongs to the\nuser. If there is no such `org_id` or if the user has no access to it,\n`Repo.get!` will raise an `Ecto.NoResultsError` exception.\n\nDuring a regular controller request, this exception will be converted to a\n404 exception and rendered as a custom error page, as\n[detailed here](https://hexdocs.pm/phoenix/custom_error_pages.html).\nLiveView will react to exceptions in three different ways, depending on\nwhere it is in its life-cycle.\n\n### Exceptions during HTTP mount\n\nWhen you first access a LiveView, a regular HTTP request is sent to the server\nand processed by the LiveView. The `mount` callback is invoked and then a page\nis rendered. Any exception here is caught, logged, and converted to an exception\npage by Phoenix error views - exactly how it works with controllers too.\n\n### Exceptions during connected mount\n\nIf the initial HTTP request succeeds, LiveView will connect to the server\nusing a stateful connection, typically a WebSocket. This spawns a long-running\nlightweight Elixir process on the server, which invokes the `mount` callback\nand renders an updated version of the page.\n\nAn exception during this stage will crash the LiveView process, which will be logged.\nOnce the client notices the crash, it fully reloads the page. This will cause `mount`\nto be invoked again during a regular HTTP request (the exact scenario of the previous\nsubsection).\n\nIn other words, LiveView will reload the page in case of errors, making it\nfail as if LiveView was not involved in the rendering in the first place.\n\n### Exceptions after connected mount\n\nOnce your LiveView is mounted and connected, any error will cause the LiveView process\nto crash and be logged. Once the client notices the error, it will remount the LiveView\nover the stateful connection, without reloading the page (the exact scenario of the\nprevious subsection). If remounting succeeds, the LiveView goes back to a working\nstate, updating the page and showing the user the latest information.\n\nFor example, let's say two users try to leave the organization at the same time.\nIn this case, both of them see the \"Leave\" button, but our `leave` function call\nwill succeed only for one of them:\n\n    true = MyApp.Org.leave(socket.assigns.current_org, member)\n    {:noreply, socket}\n\nWhen the exception raises, the client will remount the LiveView. Once you remount,\nyour code will now notice that there is only one user in the organization and\ntherefore no longer show the \"Leave\" button. In other words, by remounting,\nwe often update the state of the page, allowing exceptions to be automatically\nhandled.\n\nNote that the choice between conditionally checking on the result of the `leave`\nfunction with an `if`, or simply asserting it returns `true`, is completely\nup to you. If the likelihood of everyone leaving the organization at the same\ntime is low, then you may as well treat it as an unexpected scenario. Although\nother developers will be more comfortable by explicitly handling those cases.\nIn both scenarios, LiveView has you covered.\n\nFinally, if your LiveView crashes, its current state will be lost. Luckily,\nLiveView has a series of mechanisms and best practices you can follow to ensure\nthe user is shown the same page as before during reconnections. See the\n[\"Deployments and recovery\"](deployments.md) guide for more information."
  },
  {
    "path": "guides/server/gettext.md",
    "content": "# Gettext for internationalization\n\nFor internationalization with [gettext](https://hexdocs.pm/gettext/Gettext.html),\nyou must call `Gettext.put_locale/2` on the LiveView mount callback to instruct\nthe LiveView which locale should be used for rendering the page.\n\nHowever, one question that has to be answered is how to retrieve the locale in\nthe first place. There are many approaches to solve this problem:\n\n1. The locale could be stored in the URL as a parameter\n2. The locale could be stored in the session\n3. The locale could be stored in the database\n\nWe will briefly cover these approaches to provide some direction.\n\n## Locale from parameters\n\nYou can say all URLs have a locale parameter. In your router:\n\n    scope \"/:locale\" do\n      live ...\n      get ...\n    end\n\nAccessing a page without a locale should automatically redirect\nto a URL with locale (the best locale could be fetched from\nHTTP headers, which is outside of the scope of this guide).\n\nThen, assuming all URLs have a locale, you can set the Gettext\nlocale accordingly:\n\n    def mount(%{\"locale\" => locale}, _session, socket) do\n      Gettext.put_locale(MyApp.Gettext, locale)\n      {:ok, socket}\n    end\n\n\nYou can also use the [`on_mount`](`Phoenix.LiveView.on_mount/1`) hook to\nautomatically restore the locale for every LiveView in your application:\n\n    defmodule MyAppWeb.RestoreLocale do\n      def on_mount(:default, %{\"locale\" => locale}, _session, socket) do\n        Gettext.put_locale(MyApp.Gettext, locale)\n        {:cont, socket}\n      end\n\n      # catch-all case\n      def on_mount(:default, _params, _session, socket), do: {:cont, socket}\n    end\n\nThen, add this hook to `def live_view` under `MyAppWeb`, to run it on all\nLiveViews by default:\n\n    def live_view do\n      quote do\n        use Phoenix.LiveView\n\n        on_mount MyAppWeb.RestoreLocale\n        unquote(view_helpers())\n      end\n    end\n\nNote that, because the Gettext locale is not stored in the assigns, if you\nwant to change the locale, you must use `<.link navigate={...} />`, instead\nof simply patching the page.\n\n## Locale from session\n\nYou may also store the locale in the Plug session. For example, in a controller\nyou might do:\n\n    def put_user_session(conn, current_user) do\n      Gettext.put_locale(MyApp.Gettext, current_user.locale)\n\n      conn\n      |> put_session(:user_id, current_user.id)\n      |> put_session(:locale, current_user.locale)\n    end\n\nand then restore the locale from session within your LiveView mount:\n\n    def mount(_params, %{\"locale\" => locale}, socket) do\n      Gettext.put_locale(MyApp.Gettext, locale)\n      {:ok, socket}\n    end\n\nYou can also encapsulate this in a hook, as done in the previous section.\n\nHowever, if the locale is stored in the session, you can only change it\nby using regular controller requests. Therefore you should always use\n`<.link to={...} />` to point to a controller that change the session\naccordingly, reloading any LiveView.\n\n## Locale from database\n\nYou may also allow users to store their locale configuration in the database.\nThen, on `mount/3`, you can retrieve the user id from the session and load\nthe locale:\n\n    def mount(_params, %{\"user_id\" => user_id}, socket) do\n      user = Users.get_user!(user_id)\n      Gettext.put_locale(MyApp.Gettext, user.locale)\n      {:ok, socket}\n    end\n\nIn practice, you may end-up mixing more than one approach listed here.\nFor example, reading from the database is great once the user is logged in\nbut, before that happens, you may need to store the locale in the session\nor in the URL.\n\nSimilarly, you can keep the locale in the URL, but change the URL accordingly\nto the user preferred locale once they sign in. Hopefully this guide gives\nsome suggestions on how to move forward and explore the best approach for your\napplication.\n"
  },
  {
    "path": "guides/server/live-layouts.md",
    "content": "# Live layouts\n\nYour LiveView applications can be made of two layouts:\n\n  * the root layout - this layout typically contains the `<html>`\n    definition alongside the head and body tags. Any content defined\n    in the root layout will remain the same, even as you live navigate\n    across LiveViews. The root layout is typically declared on the\n    router with `put_root_layout` and defined as \"root.html.heex\"\n    in your layouts folder. It calls `{@inner_content}` to inject the\n    content rendered by the layout\n\n  * the app layout - this is the dynamic layout part of your application,\n    it often includes the menu, sidebar, flash messages, and more.\n    From Phoenix v1.8, this layout is explicitly rendered in your templates\n    by calling the `<Layouts.app />` component. In Phoenix v1.7 and earlier,\n    the layout was typically configured as part of the `lib/my_app_web.ex`\n    file, such as `use Phoenix.LiveView, layout: ...`\n\nOverall, those layouts are found in `components/layouts` and are\nembedded within `MyAppWeb.Layouts`.\n\n## Root layout\n\nThe \"root\" layout is rendered only on the initial request and\ntherefore it has access to the `@conn` assign. The root layout\nis typically defined in your router:\n\n    plug :put_root_layout, html: {MyAppWeb.Layouts, :root}\n\nThe root layout can also be set via the `:root_layout` option\nin your router via `Phoenix.LiveView.Router.live_session/2`.\n\n## Updating document title\n\nBecause the root layout from the Plug pipeline is rendered outside of\nLiveView, the contents cannot be dynamically changed. The one exception\nis the `<title>` of the HTML document. Phoenix LiveView special cases\nthe `@page_title` assign to allow dynamically updating the title of the\npage, which is useful when using live navigation, or annotating the browser\ntab with a notification. For example, to update the user's notification\ncount in the browser's title bar, first set the `page_title` assign on\nmount:\n\n    def mount(_params, _session, socket) do\n      socket = assign(socket, page_title: \"Latest Posts\")\n      {:ok, socket}\n    end\n\nThen access `@page_title` in the root layout:\n\n```heex\n<title>{@page_title}</title>\n```\n\nYou can also use the `Phoenix.Component.live_title/1` component to support\nadding automatic prefix and suffix to the page title when rendered and\non subsequent updates:\n\n```heex\n<Phoenix.Component.live_title default=\"Welcome\" prefix=\"MyApp – \">\n  {assigns[:page_title]}\n</Phoenix.Component.live_title>\n```\n\nAlthough the root layout is not updated by LiveView, by simply assigning\nto `page_title`, LiveView knows you want the title to be updated:\n\n    def handle_info({:new_messages, count}, socket) do\n      {:noreply, assign(socket, page_title: \"Latest Posts (#{count} new)\")}\n    end\n\n*Note*: If you find yourself needing to dynamically patch other parts of the\nbase layout, such as injecting new scripts or styles into the `<head>` during\nlive navigation, *then a regular, non-live, page navigation should be used\ninstead*. Assigning the `@page_title` updates the `document.title` directly,\nand therefore cannot be used to update any other part of the base layout.\n"
  },
  {
    "path": "guides/server/live-navigation.md",
    "content": "# Live navigation\n\nLiveView provides functionality to allow page navigation using the\n[browser's pushState API](https://developer.mozilla.org/en-US/docs/Web/API/History_API).\nWith live navigation, the page is updated without a full page reload.\n\nYou can trigger live navigation in two ways:\n\n  * From the client - this is done by passing either `patch={url}` or `navigate={url}`\n    to the `Phoenix.Component.link/1` component.\n\n  * From the server - this is done by `Phoenix.LiveView.push_patch/2` or `Phoenix.LiveView.push_navigate/2`.\n\nFor example, instead of writing the following in a template:\n\n```heex\n<.link href={~p\"/pages/#{@page + 1}\"}>Next</.link>\n```\n\nYou would write:\n\n```heex\n<.link patch={~p\"/pages/#{@page + 1}\"}>Next</.link>\n```\n\nOr in a LiveView:\n\n```elixir\n{:noreply, push_patch(socket, to: ~p\"/pages/#{@page + 1}\")}\n```\n\nThe \"patch\" operations must be used when you want to navigate to the\ncurrent LiveView, simply updating the URL and the current parameters,\nwithout mounting a new LiveView. When patch is used, the\n[`handle_params/3`](`c:Phoenix.LiveView.handle_params/3`) callback is\ninvoked and the minimal set of changes are sent to the client.\nSee the next section for more information.\n\nThe \"navigate\" operations must be used when you want to dismount the\ncurrent LiveView and mount a new one. You can only \"navigate\" between\nLiveViews in the same session. While redirecting, a `phx-loading` class\nis added to the LiveView, which can be used to indicate to the user a\nnew page is being loaded.\n\nIf you attempt to patch to another LiveView or navigate across live sessions,\na full page reload is triggered. This means your application continues to work,\nin case your application structure changes and that's not reflected in the navigation.\n\nHere is a quick breakdown:\n\n  * `<.link href={...}>` and [`redirect/2`](`Phoenix.Controller.redirect/2`)\n    are HTTP-based, work everywhere, and perform full page reloads\n\n  * `<.link navigate={...}>` and [`push_navigate/2`](`Phoenix.LiveView.push_navigate/2`)\n    work across LiveViews in the same session. They mount a new LiveView\n    while keeping the current layout\n\n  * `<.link patch={...}>` and [`push_patch/2`](`Phoenix.LiveView.push_patch/2`)\n    updates the current LiveView and sends only the minimal diff while also\n    maintaining the scroll position\n\n## `handle_params/3`\n\nThe [`handle_params/3`](`c:Phoenix.LiveView.handle_params/3`) callback is invoked\nafter [`mount/3`](`c:Phoenix.LiveView.mount/3`) and before the initial render.\nIt is also invoked every time `<.link patch={...}>`\nor [`push_patch/2`](`Phoenix.LiveView.push_patch/2`) are used.\nIt receives the request parameters as first argument, the url as second,\nand the socket as third.\n\nFor example, imagine you have a `UserTable` LiveView to show all users in\nthe system and you define it in the router as:\n\n    live \"/users\", UserTable\n\nNow to add live sorting, you could do:\n\n```heex\n<.link patch={~p\"/users?sort_by=name\"}>Sort by name</.link>\n```\n\nWhen clicked, since we are navigating to the current LiveView,\n[`handle_params/3`](`c:Phoenix.LiveView.handle_params/3`) will be invoked.\nRemember you should never trust the received params, so you must use the callback to\nvalidate the user input and change the state accordingly:\n\n    def handle_params(params, _uri, socket) do\n      socket =\n        case params[\"sort_by\"] do\n          sort_by when sort_by in ~w(name company) -> assign(socket, sort_by: sort_by)\n          _ -> socket\n        end\n\n      {:noreply, load_users(socket)}\n    end\n\nNote we returned `{:noreply, socket}`, where `:noreply` means no\nadditional information is sent to the client. As with other `handle_*`\ncallbacks, changes to the state inside\n[`handle_params/3`](`c:Phoenix.LiveView.handle_params/3`) will trigger\na new server render.\n\nNote the parameters given to [`handle_params/3`](`c:Phoenix.LiveView.handle_params/3`)\nare the same as the ones given to [`mount/3`](`c:Phoenix.LiveView.mount/3`).\nSo how do you decide which callback to use to load data?\nGenerally speaking, data should always be loaded on [`mount/3`](`c:Phoenix.LiveView.mount/3`),\nsince [`mount/3`](`c:Phoenix.LiveView.mount/3`) is invoked once per LiveView life-cycle.\nOnly the params you expect to be changed via\n`<.link patch={...}>` or\n[`push_patch/2`](`Phoenix.LiveView.push_patch/2`) must be loaded on\n[`handle_params/3`](`c:Phoenix.LiveView.handle_params/3`).\n\nFor example, imagine you have a blog. The URL for a single post is:\n\"/blog/posts/:post_id\". In the post page, you have comments and they are paginated.\nYou use `<.link patch={...}>` to update the shown\ncomments every time the user paginates, updating the URL to \"/blog/posts/:post_id?page=X\".\nIn this example, you will access `\"post_id\"` on [`mount/3`](`c:Phoenix.LiveView.mount/3`) and\nthe page of comments on [`handle_params/3`](`c:Phoenix.LiveView.handle_params/3`).\n\n## Replace page address\n\nLiveView also allows the current browser URL to be replaced. This is useful when you\nwant certain events to change the URL but without polluting the browser's history.\nThis can be done by passing the `<.link replace>` option to any of the navigation helpers.\n\n## Multiple LiveViews in the same page\n\nLiveView allows you to have multiple LiveViews in the same page by calling\n`Phoenix.Component.live_render/3` in your templates. However, only\nthe LiveViews defined directly in your router can use the \"Live Navigation\"\nfunctionality described here. This is important because LiveViews work\nclosely with your router, guaranteeing you can only navigate to known\nroutes.\n"
  },
  {
    "path": "guides/server/security-model.md",
    "content": "# Security considerations\n\nLiveView begins its life-cycle as a regular HTTP request. Then a stateful\nconnection is established. Both the HTTP request and the stateful connection\nreceive the client data via parameters and session.\n\nThis means that any session validation must happen both in the HTTP request\n(plug pipeline) and the stateful connection (LiveView mount).\n\n## Authentication vs authorization\n\nWhen speaking about security, there are two terms commonly used:\nauthentication and authorization. Authentication is about identifying\na user. Authorization is about telling if a user has access to a certain\nresource or feature in the system.\n\nIn a regular web application, once a user is authenticated, for example by\nentering their email and password, or by using a third-party service such as\nGoogle, Twitter, or Facebook, a token identifying the user is stored in the\nsession, which is a cookie (a key-value pair) stored in the user's browser.\n\nEvery time there is a request, we read the value from the session, and, if\nvalid, we fetch the user stored in the session from the database. The session\nis automatically validated by Phoenix and tools like `mix phx.gen.auth` can\ngenerate the building blocks of an authentication system for you.\n\nOnce the user is authenticated, they may perform many actions on the page,\nand some of those actions require specific permissions. This is called\nauthorization and the specific rules often change per application.\n\nIn a regular web application, we perform authentication and authorization\nchecks on every request. Given LiveViews start as a regular HTTP request,\nthey share the authentication logic with regular requests through plugs.\nThe request starts in your endpoint, which then invokes the router.\nPlugs are used to ensure the user is authenticated and stores the\nrelevant information in the session.\n\nOnce the user is authenticated, we typically validate the sessions on\nthe `mount` callback. Authorization rules generally happen on `mount`\n(for instance, is the user allowed to see this page?) and also on\n`handle_event` (is the user allowed to delete this item?).\n\n## `live_session`\n\nThe primary mechanism for grouping LiveViews is via the\n`Phoenix.LiveView.Router.live_session/2`. LiveView will then ensure\nthat navigation events within the same `live_session` skip the regular\nHTTP requests without going through the plug pipeline. Events across\nlive sessions will go through the router.\n\nFor example, imagine you need to authenticate two distinct types of users.\nYour regular users login via email and password, and you have an admin\ndashboard that uses HTTP auth. You can specify different `live_session`s\nfor each authentication flow:\n\n    scope \"/\" do\n      pipe_through [:authenticate_user]\n      get ...\n\n      live_session :default do\n        live ...\n      end\n    end\n\n    scope \"/admin\" do\n      pipe_through [:http_auth_admin]\n      get ...\n\n      live_session :admin do\n        live ...\n      end\n    end\n\nNow every time you try to navigate to an admin panel, and out of it,\na regular page navigation will happen and a brand new live connection\nwill be established.\n\nIt is worth remembering that LiveViews require their own security checks,\nso we use `pipe_through` above to protect the regular routes (get, post, etc.)\nand the LiveViews should run their own checks on the `mount` callback\n(or using `Phoenix.LiveView.on_mount/1` hooks).\n\nFor this purpose, you can combine `live_session` with `on_mount`, as well\nas other options, such as the `:root_layout`. Instead of declaring `on_mount`\non every LiveView, you can declare it at the router level and it will enforce\nit on all LiveViews under it:\n\n    scope \"/\" do\n      pipe_through [:authenticate_user]\n\n      live_session :default, on_mount: MyAppWeb.UserLiveAuth do\n        live ...\n      end\n    end\n\n    scope \"/admin\" do\n      pipe_through [:authenticate_admin]\n\n      live_session :admin, on_mount: MyAppWeb.AdminLiveAuth do\n        live ...\n      end\n    end\n\nEach live route under the `:default` `live_session` will invoke\nthe `MyAppWeb.UserLiveAuth` hook on mount. This module was defined\nearlier in this guide. We will also pipe regular web requests through\n`:authenticate_user`, which must execute the same checks as\n`MyAppWeb.UserLiveAuth`, but tailored to plug.\n\nSimilarly, the `:admin` `live_session` has its own authentication\nflow, powered by `MyAppWeb.AdminLiveAuth`. It also defines a plug\nequivalent named `:authenticate_admin`, which will be used by any\nregular request. If there are no regular web requests defined under\na live session, then the `pipe_through` checks are not necessary.\n\nDeclaring the `on_mount` on `live_session` is exactly the same as\ndeclaring it in each LiveView. Let's talk about which logic we typically\nexecute on mount.\n\n## Mounting considerations\n\nThe [`mount/3`](`c:Phoenix.LiveView.mount/3`) callback is invoked both on\nthe initial HTTP mount and when LiveView is connected. Therefore, any\nauthorization performed during mount will cover all scenarios.\n\nOnce the user is authenticated and stored in the session, the logic to fetch the user and further authorize its account needs to happen inside LiveView. For example, if you have the following plugs:\n\n    plug :ensure_user_authenticated\n    plug :ensure_user_confirmed\n\nThen the [`mount/3`](`c:Phoenix.LiveView.mount/3`) callback of your LiveView\nshould execute those same verifications:\n\n    def mount(_params, %{\"user_id\" => user_id} = _session, socket) do\n      socket = assign(socket, current_user: Accounts.get_user!(user_id))\n\n      socket =\n        if socket.assigns.current_user.confirmed_at do\n          socket\n        else\n          redirect(socket, to: \"/login\")\n        end\n\n      {:ok, socket}\n    end\n\nThe `on_mount` hook allows you to encapsulate this logic and execute it on every mount:\n\n    defmodule MyAppWeb.UserLiveAuth do\n      import Phoenix.Component\n      import Phoenix.LiveView\n      alias MyAppWeb.Accounts # from `mix phx.gen.auth`\n\n      def on_mount(:default, _params, %{\"user_token\" => user_token} = _session, socket) do\n        socket =\n          assign_new(socket, :current_user, fn ->\n            Accounts.get_user_by_session_token(user_token)\n          end)\n\n        if socket.assigns.current_user.confirmed_at do\n          {:cont, socket}\n        else\n          {:halt, redirect(socket, to: \"/login\")}\n        end\n      end\n    end\n\nWe use [`assign_new/3`](`Phoenix.Component.assign_new/3`). This is a\nconvenience to avoid fetching the `current_user` multiple times across\nparent-child LiveViews.\n\nNow we can use the hook whenever relevant. One option is to specify\nthe hook in your router under `live_session`:\n\n    live_session :default, on_mount: MyAppWeb.UserLiveAuth do\n      # Your routes\n    end\n\nAlternatively, you can either specify the hook directly in the LiveView:\n\n    defmodule MyAppWeb.PageLive do\n      use MyAppWeb, :live_view\n      on_mount MyAppWeb.UserLiveAuth\n\n      ...\n    end\n\nIf you prefer, you can add the hook to `def live_view` under `MyAppWeb`,\nto run it on all LiveViews by default:\n\n    def live_view do\n      quote do\n        use Phoenix.LiveView\n\n        on_mount MyAppWeb.UserLiveAuth\n        unquote(html_helpers())\n      end\n    end\n\n## Events considerations\n\nEvery time the user performs an action on your system, you should verify if the user\nis authorized to do so, regardless if you are using LiveViews or not. For example,\nimagine a user can see all projects in a web application, but they may not have\npermission to delete any of them. At the UI level, you handle this accordingly\nby not showing the delete button in the projects listing, but a savvy user can\ndirectly talk to the server and request a deletion anyway. For this reason, **you\nmust always verify permissions on the server**.\n\nIn LiveView, most actions are handled by the `handle_event` callback. Therefore,\nyou typically authorize the user within those callbacks. In the scenario just\ndescribed, one might implement this:\n\n    on_mount MyAppWeb.UserLiveAuth\n\n    def mount(_params, _session, socket) do\n      {:ok, load_projects(socket)}\n    end\n\n    def handle_event(\"delete_project\", %{\"project_id\" => project_id}, socket) do\n      Project.delete!(socket.assigns.current_user, project_id)\n      {:noreply, update(socket, :projects, &Enum.reject(&1, fn p -> p.id == project_id end)}\n    end\n\n    defp load_projects(socket) do\n      projects = Project.all_projects(socket.assigns.current_user)\n      assign(socket, projects: projects)\n    end\n\nFirst, we used `on_mount` to authenticate the user based on the data stored in\nthe session. Then we load all projects based on the authenticated user. Now,\nwhenever there is a request to delete a project, we still pass the current user\nas argument to the `Project` context, so it verifies if the user is allowed to\ndelete it or not. In case it cannot delete, it is fine to just raise an exception.\nAfter all, users are not meant to trigger this code path anyway (unless they are\nfiddling with something they are not supposed to!).\n\n## Disconnecting all instances of a live user\n\nSo far, the security model between LiveView and regular web applications have\nbeen remarkably similar. After all, we must always authenticate and authorize\nevery user. The main difference between them happens on logout or when revoking\naccess.\n\nBecause LiveView is a permanent connection between client and server, if a user\nis logged out, or removed from the system, this change won't reflect on the\nLiveView part unless the user reloads the page.\n\nLuckily, it is possible to address this by setting a `live_socket_id` in the\nsession. For example, when logging in a user, you could do:\n\n    conn\n    |> put_session(:current_user_id, user.id)\n    |> put_session(:live_socket_id, \"users_socket:#{user.id}\")\n\nNow all LiveView sockets will be identified and listen to the given `live_socket_id`.\nYou can then disconnect all live users identified by said ID by broadcasting on\nthe topic:\n\n    MyAppWeb.Endpoint.broadcast(\"users_socket:#{user.id}\", \"disconnect\", %{})\n\n> Note: If you use `mix phx.gen.auth` to generate your authentication system,\n> lines to that effect are already present in the generated code. The generated\n> code uses a `user_token` instead of referring to the `user_id`.\n\nOnce a LiveView is disconnected, the client will attempt to reestablish\nthe connection and re-execute the [`mount/3`](`c:Phoenix.LiveView.mount/3`)\ncallback. In this case, if the user is no longer logged in or it no longer has\naccess to the current resource, `mount/3` will fail and the user will be\nredirected.\n\nThis is the same mechanism provided by `Phoenix.Channel`s. Therefore, if\nyour application uses both channels and LiveViews, you can use the same\ntechnique to disconnect any stateful connection.\n\n## Summing up\n\nThe important concepts to keep in mind are:\n\n  * `live_session` can be used to draw boundaries between groups of\n    LiveViews. While you could use `live_session` to draw lines between\n    different authorization rules, doing so would lead to frequent page\n    reloads. For this reason, we typically use `live_session` to enforce\n    different *authentication* requirements or whenever you need to\n    change root layouts\n\n  * Your authentication logic (logging the user in) is typically part of\n    your regular web request pipeline and it is shared by both controllers\n    and LiveViews. Authentication then stores the user information in the\n    session. Regular web requests use `plug` to read the user from a session,\n    LiveViews read it inside an `on_mount` callback. This is typically a\n    single database lookup on both cases. Running `mix phx.gen.auth` sets\n    up all that is necessary\n\n  * Once authenticated, your authorization logic in LiveViews will happen\n    both during `mount` (such as \"can the user see this page?\") and during\n    events (like \"can the user delete this item?\"). Those rules are often\n    domain/business specific, and typically happen in your context modules.\n    This is also a requirement for regular requests and responses\n"
  },
  {
    "path": "guides/server/telemetry.md",
    "content": "# Telemetry\n\nLiveView currently exposes the following [`telemetry`](https://hexdocs.pm/telemetry) events:\n\n  * `[:phoenix, :live_view, :mount, :start]` - Dispatched by a `Phoenix.LiveView`\n    immediately before [`mount/3`](`c:Phoenix.LiveView.mount/3`) is invoked.\n\n    * Measurement:\n\n          %{system_time: System.monotonic_time}\n\n    * Metadata:\n\n          %{\n            socket: Phoenix.LiveView.Socket.t,\n            params: unsigned_params | :not_mounted_at_router,\n            session: map,\n            uri: String.t() | nil\n          }\n\n  * `[:phoenix, :live_view, :mount, :stop]` - Dispatched by a `Phoenix.LiveView`\n    when the [`mount/3`](`c:Phoenix.LiveView.mount/3`) callback completes successfully.\n\n    * Measurement:\n\n          %{duration: native_time}\n\n    * Metadata:\n\n          %{\n            socket: Phoenix.LiveView.Socket.t,\n            params: unsigned_params | :not_mounted_at_router,\n            session: map,\n            uri: String.t() | nil\n          }\n\n  * `[:phoenix, :live_view, :mount, :exception]` - Dispatched by a `Phoenix.LiveView`\n    when an exception is raised in the [`mount/3`](`c:Phoenix.LiveView.mount/3`) callback.\n\n    * Measurement: `%{duration: native_time}`\n\n    * Metadata:\n\n          %{\n            socket: Phoenix.LiveView.Socket.t,\n            kind: atom,\n            reason: term,\n            params: unsigned_params | :not_mounted_at_router,\n            session: map,\n            uri: String.t() | nil\n          }\n\n  * `[:phoenix, :live_view, :handle_params, :start]` - Dispatched by a `Phoenix.LiveView`\n    immediately before [`handle_params/3`](`c:Phoenix.LiveView.handle_params/3`) is invoked.\n\n    * Measurement:\n\n          %{system_time: System.monotonic_time}\n\n    * Metadata:\n\n          %{\n            socket: Phoenix.LiveView.Socket.t,\n            params: unsigned_params,\n            uri: String.t()\n          }\n\n  * `[:phoenix, :live_view, :handle_params, :stop]` - Dispatched by a `Phoenix.LiveView`\n    when the [`handle_params/3`](`c:Phoenix.LiveView.handle_params/3`) callback completes successfully.\n\n    * Measurement:\n\n          %{duration: native_time}\n\n    * Metadata:\n\n          %{\n            socket: Phoenix.LiveView.Socket.t,\n            params: unsigned_params,\n            uri: String.t()\n          }\n\n  * `[:phoenix, :live_view, :handle_params, :exception]` - Dispatched by a `Phoenix.LiveView`\n    when an exception is raised in the [`handle_params/3`](`c:Phoenix.LiveView.handle_params/3`) callback.\n\n    * Measurement:\n\n          %{duration: native_time}\n\n    * Metadata:\n\n          %{\n            socket: Phoenix.LiveView.Socket.t,\n            kind: atom,\n            reason: term,\n            params: unsigned_params,\n            uri: String.t()\n          }\n\n  * `[:phoenix, :live_view, :handle_event, :start]` - Dispatched by a `Phoenix.LiveView`\n    immediately before [`handle_event/3`](`c:Phoenix.LiveView.handle_event/3`) is invoked.\n\n    * Measurement:\n\n          %{system_time: System.monotonic_time}\n\n    * Metadata:\n\n          %{\n            socket: Phoenix.LiveView.Socket.t,\n            event: String.t(),\n            params: unsigned_params\n          }\n\n  * `[:phoenix, :live_view, :handle_event, :stop]` - Dispatched by a `Phoenix.LiveView`\n    when the [`handle_event/3`](`c:Phoenix.LiveView.handle_event/3`) callback completes successfully.\n\n    * Measurement:\n\n          %{duration: native_time}\n\n    * Metadata:\n\n          %{\n            socket: Phoenix.LiveView.Socket.t,\n            event: String.t(),\n            params: unsigned_params\n          }\n\n  * `[:phoenix, :live_view, :handle_event, :exception]` - Dispatched by a `Phoenix.LiveView`\n    when an exception is raised in the [`handle_event/3`](`c:Phoenix.LiveView.handle_event/3`) callback.\n\n    * Measurement:\n\n          %{duration: native_time}\n\n    * Metadata:\n\n          %{\n            socket: Phoenix.LiveView.Socket.t,\n            kind: atom,\n            reason: term,\n            event: String.t(),\n            params: unsigned_params\n          }\n\n  * `[:phoenix, :live_view, :render, :start]` - Dispatched by a `Phoenix.LiveView`\n    immediately before [`render/1`](`c:Phoenix.LiveComponent.render/1`) is invoked.\n\n    * Measurement:\n\n          %{system_time: System.monotonic_time}\n\n    * Metadata:\n\n          %{\n            socket: Phoenix.LiveView.Socket.t,\n            force?: boolean,\n            changed?: boolean\n          }\n\n  * `[:phoenix, :live_view, :render, :stop]` - Dispatched by a `Phoenix.LiveView`\n    when the [`render/1`](`c:Phoenix.LiveView.render/1`) callback completes successfully.\n\n    * Measurement:\n\n          %{duration: native_time}\n\n    * Metadata:\n\n          %{\n            socket: Phoenix.LiveView.Socket.t,\n            force?: boolean,\n            changed?: boolean\n          }\n\n  * `[:phoenix, :live_view, :render, :exception]` - Dispatched by a `Phoenix.LiveView`\n    when an exception is raised in the [`render/1`](`c:Phoenix.LiveView.render/1`) callback.\n\n    * Measurement:\n\n          %{duration: native_time}\n\n    * Metadata:\n\n          %{\n            socket: Phoenix.LiveView.Socket.t,\n            kind: atom,\n            reason: term,\n            force?: boolean,\n            changed?: boolean\n          }\n\n  * `[:phoenix, :live_component, :update, :start]` - Dispatched by a `Phoenix.LiveComponent`\n    immediately before [`update/2`](`c:Phoenix.LiveComponent.update/2`) or a\n    [`update_many/1`](`c:Phoenix.LiveComponent.update_many/1`) is invoked.\n\n    In the case of[`update/2`](`c:Phoenix.LiveComponent.update/2`) it might dispatch one event\n    for multiple calls.\n\n    * Measurement:\n\n          %{system_time: System.monotonic_time}\n\n    * Metadata:\n\n          %{\n            socket: Phoenix.LiveView.Socket.t,\n            component: atom,\n            assigns_sockets: [{map(), Phoenix.LiveView.Socket.t}]\n          }\n\n  * `[:phoenix, :live_component, :update, :stop]` - Dispatched by a `Phoenix.LiveComponent`\n    when the [`update/2`](`c:Phoenix.LiveComponent.update/2`) or a\n    [`update_many/1`](`c:Phoenix.LiveComponent.update_many/1`) callback completes successfully.\n\n    In the case of[`update/2`](`c:Phoenix.LiveComponent.update/2`) it might dispatch one event\n    for multiple calls. The `sockets` metadata contain the updated sockets.\n\n    * Measurement:\n\n          %{duration: native_time}\n\n    * Metadata:\n\n          %{\n            socket: Phoenix.LiveView.Socket.t,\n            component: atom,\n            assigns_sockets: [{map(), Phoenix.LiveView.Socket.t}],\n            sockets: [Phoenix.LiveView.Socket.t]\n          }\n\n  * `[:phoenix, :live_component, :update, :exception]` - Dispatched by a `Phoenix.LiveComponent`\n    when an exception is raised in the [`update/2`](`c:Phoenix.LiveComponent.update/2`) or a\n    [`update_many/1`](`c:Phoenix.LiveComponent.update_many/1`) callback.\n\n    In the case of[`update/2`](`c:Phoenix.LiveComponent.update/2`) it might dispatch one event\n    for multiple calls.\n\n    * Measurement:\n\n          %{duration: native_time}\n\n    * Metadata:\n\n          %{\n            socket: Phoenix.LiveView.Socket.t,\n            kind: atom,\n            reason: term,\n            component: atom,\n            assigns_sockets: [{map(), Phoenix.LiveView.Socket.t}]\n          }\n\n  * `[:phoenix, :live_component, :handle_event, :start]` - Dispatched by a `Phoenix.LiveComponent`\n    immediately before [`handle_event/3`](`c:Phoenix.LiveComponent.handle_event/3`) is invoked.\n\n    * Measurement:\n\n          %{system_time: System.monotonic_time}\n\n    * Metadata:\n\n          %{\n            socket: Phoenix.LiveView.Socket.t,\n            component: atom,\n            event: String.t(),\n            params: unsigned_params\n          }\n\n  * `[:phoenix, :live_component, :handle_event, :stop]` - Dispatched by a `Phoenix.LiveComponent`\n    when the [`handle_event/3`](`c:Phoenix.LiveComponent.handle_event/3`) callback completes successfully.\n\n    * Measurement:\n\n          %{duration: native_time}\n\n    * Metadata:\n\n          %{\n            socket: Phoenix.LiveView.Socket.t,\n            component: atom,\n            event: String.t(),\n            params: unsigned_params\n          }\n\n  * `[:phoenix, :live_component, :handle_event, :exception]` - Dispatched by a `Phoenix.LiveComponent`\n    when an exception is raised in the [`handle_event/3`](`c:Phoenix.LiveComponent.handle_event/3`) callback.\n\n    * Measurement:\n\n          %{duration: native_time}\n\n    * Metadata:\n\n          %{\n            socket: Phoenix.LiveView.Socket.t,\n            kind: atom,\n            reason: term,\n            component: atom,\n            event: String.t(),\n            params: unsigned_params\n          }\n\n  * `[:phoenix, :live_component, :destroyed]` - Dispatched by a `Phoenix.LiveComponent`\n    after it is destroyed. No measurement.\n\n    * Metadata:\n\n        %{\n          socket: Phoenix.LiveView.Socket.t,\n          component: atom,\n          cid: integer(),\n          live_view_socket: Phoenix.LiveView.Socket.t\n        }\n"
  },
  {
    "path": "guides/server/uploads.md",
    "content": "# Uploads\n\nLiveView supports interactive file uploads with progress for\nboth direct to server uploads as well as direct-to-cloud\n[external uploads](external-uploads.html) on the client.\n\n## Built-in Features\n\n  * Accept specification - Define accepted file types, max\n    number of entries, max file size, etc. When the client\n    selects file(s), the file metadata is automatically\n    validated against the specification. See\n    `Phoenix.LiveView.allow_upload/3`.\n\n  * Reactive entries - Uploads are populated in an\n    `@uploads` assign in the socket. Entries automatically\n    respond to progress, errors, cancellation, etc.\n\n  * Drag and drop - Use the `phx-drop-target` attribute to\n    enable. See `Phoenix.Component.live_file_input/1`.\n\n## Allow uploads\n\nYou enable an upload, typically on mount, via [`allow_upload/3`].\n\nFor this example, we will also keep a list of uploaded files in\na new assign named `uploaded_files`, but you could name it\nsomething else if you wanted.\n\n```elixir\n@impl Phoenix.LiveView\ndef mount(_params, _session, socket) do\n  {:ok,\n   socket\n   |> assign(:uploaded_files, [])\n   |> allow_upload(:avatar, accept: ~w(.jpg .jpeg), max_entries: 2)}\nend\n```\n\nThat's it for now! We will come back to the LiveView to\nimplement some form- and upload-related callbacks later, but\nmost of the functionality around uploads takes place in the\ntemplate.\n\nIt is worth to check out the documentation for [`allow_upload/3`] and\nlook at the available options, which also include `auto_upload` for\nautomatic uploads, among others.\n\n## Render reactive elements\n\nUse the `Phoenix.Component.live_file_input/1` component\nto render a file input for the upload:\n\n```heex\n<%!-- lib/my_app_web/live/upload_live.html.heex --%>\n\n<form id=\"upload-form\" phx-change=\"validate\" phx-submit=\"save\">\n  <.live_file_input upload={@uploads.avatar} />\n  <button type=\"submit\">Upload</button>\n</form>\n```\n\n> **Important:** You must bind `phx-submit` and `phx-change` on the form.\n\nNote that while [`live_file_input/1`]\nallows you to set additional attributes on the file input,\nmany attributes such as `id`, `accept`, and `multiple` will\nbe set automatically based on the [`allow_upload/3`] spec.\n\nReactive updates to the template will occur as the end-user\ninteracts with the file input.\n\n### Upload entries\n\nUploads are populated in an `@uploads` assign in the socket.\nEach allowed upload contains a _list_ of entries,\nirrespective of the `:max_entries` value in the\n[`allow_upload/3`] spec. These entry structs contain all the\ninformation about an upload, including progress, client file\ninfo, errors, etc.\n\nLet's look at an annotated example:\n\n```heex\n<%!-- lib/my_app_web/live/upload_live.html.heex --%>\n\n<%!-- use phx-drop-target with the upload ref to enable file drag and drop --%>\n<section phx-drop-target={@uploads.avatar.ref}>\n  <%!-- render each avatar entry --%>\n  <article :for={entry <- @uploads.avatar.entries} class=\"upload-entry\">\n    <figure>\n      <.live_img_preview entry={entry} />\n      <figcaption>{entry.client_name}</figcaption>\n    </figure>\n\n    <%!-- entry.progress will update automatically for in-flight entries --%>\n    <progress value={entry.progress} max=\"100\"> {entry.progress}% </progress>\n\n    <%!-- a regular click event whose handler will invoke Phoenix.LiveView.cancel_upload/3 --%>\n    <button type=\"button\" phx-click=\"cancel-upload\" phx-value-ref={entry.ref} aria-label=\"cancel\">&times;</button>\n\n    <%!-- Phoenix.Component.upload_errors/2 returns a list of error atoms --%>\n    <p :for={err <- upload_errors(@uploads.avatar, entry)} class=\"alert alert-danger\">{error_to_string(err)}</p>\n  </article>\n\n  <%!-- Phoenix.Component.upload_errors/1 returns a list of error atoms --%>\n  <p :for={err <- upload_errors(@uploads.avatar)} class=\"alert alert-danger\">\n    {error_to_string(err)}\n  </p>\n</section>\n```\n\nThe `section` element in the example acts as the\n`phx-drop-target` for the `:avatar` upload. Users can interact\nwith the file input or they can drop files over the element\nto add new entries.\n\nUpload entries are created when a file is added to the form\ninput and each will exist until it has been consumed,\nfollowing a successfully completed upload.\n\n### Styling the drop target\n\nPhoenix LiveView listens for drag events in the browser,\nand will annotate the drop target element with the `phx-drop-target-active` class\nwhen a user is dragging an element over the drop target.\n\nWhen using TailwindCSS, one may create a custom variant that can be used in conjunction\nwith this class to allow styling things specifically when the user is dragging something over the drop target.\n\n```css\n<%!-- assets/app.css --%>\n\n@custom-variant phx-drop-target-active (.phx-drop-target-active&, .phx-drop-target-active &);\n```\n\nThis variant can be used in HeEx templates like so:\n\n```heex\n<%!-- lib/my_app_web/live/upload_live.html.heex --%>\n\n<section phx-drop-target={@uploads.avatar.ref} class=\"phx-drop-target-active:scale-105\">\n<!-- ... -->\n</section>\n```\n\nIn this example, when a file is dragged over the dropzone element, the element grows in size.\n\nThis variant can also be used alongside [Tailwind's arbitrary state selectors](https://tailwindcss.com/docs/hover-focus-and-other-states),\nwhich can allow one to not only style the element itself, but the entire page, sibling elements, parent elements, and more.\n\n### Entry validation\n\nValidation occurs automatically based on any conditions\nthat were specified in [`allow_upload/3`] however, as\nmentioned previously you are required to bind `phx-change`\non the form in order for the validation to be performed.\nTherefore you must implement at least a minimal callback:\n\n```elixir\n@impl Phoenix.LiveView\ndef handle_event(\"validate\", _params, socket) do\n  {:noreply, socket}\nend\n```\n\nEntries for files that do not match the [`allow_upload/3`]\nspec will contain errors. Use\n`Phoenix.Component.upload_errors/2` and your own\nhelper function to render a friendly error message:\n\n```elixir\ndefp error_to_string(:too_large), do: \"Too large\"\ndefp error_to_string(:not_accepted), do: \"You have selected an unacceptable file type\"\n```\n\nFor error messages that affect all entries, use\n`Phoenix.Component.upload_errors/1`, and your own\nhelper function to render a friendly error message:\n\n```elixir\ndefp error_to_string(:too_many_files), do: \"You have selected too many files\"\n```\n\n### Cancel an entry\n\nUpload entries may also be canceled, either programmatically\nor as a result of a user action. For instance, to handle the\nclick event in the template above, you could do the following:\n\n```elixir\n@impl Phoenix.LiveView\ndef handle_event(\"cancel-upload\", %{\"ref\" => ref}, socket) do\n  {:noreply, cancel_upload(socket, :avatar, ref)}\nend\n```\n\n## Consume uploaded entries\n\nWhen the end-user submits a form containing a [`live_file_input/1`],\nthe JavaScript client first uploads the file(s) before\ninvoking the callback for the form's `phx-submit` event.\n\nWithin the callback for the `phx-submit` event, you invoke\nthe `Phoenix.LiveView.consume_uploaded_entries/3` function\nto process the completed uploads, persisting the relevant\nupload data alongside the form data:\n\n```elixir\n@impl Phoenix.LiveView\ndef handle_event(\"save\", _params, socket) do\n  uploaded_files =\n    consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->\n      dest = Path.join(Application.app_dir(:my_app, \"priv/static/uploads\"), Path.basename(path))\n      # You will need to create `priv/static/uploads` for `File.cp!/2` to work.\n      File.cp!(path, dest)\n      {:ok, ~p\"/uploads/#{Path.basename(dest)}\"}\n    end)\n\n  {:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}\nend\n```\n\n> **Note**: While client metadata cannot be trusted, max file size validations\n> are enforced as each chunk is received when performing direct to server uploads.\n\nThis example writes the file directly to disk, under the `priv` folder.\nIn order to access your upload, for example in an `<img />` tag, you need\nto add the `uploads` directory to `static_paths/0`.  In a vanilla Phoenix\nproject, this is found in `lib/my_app_web.ex`.\n\nAnother thing to be aware of is that in development, changes to\n`priv/static/uploads` will be picked up by `live_reload`.  This means that as\nsoon as your upload succeeds, your app will be reloaded in the browser.  This\ncan be temporarily disabled by setting `code_reloader: false` in `config/dev.exs`.\n\nBesides the above, this approach also has limitations in production. If you are\nrunning multiple instances of your application, the uploaded file will be stored\nonly in one of the instances. Any request routed to the other machine will\nultimately fail.\n\nFor these reasons, it is best if uploads are stored elsewhere, such as the\ndatabase (depending on the size and contents) or a separate storage service.\nFor more information on implementing client-side, direct-to-cloud uploads,\nsee the [External uploads guide](external-uploads.md) for details.\n\n## Appendix A: UploadLive\n\nA complete example of the LiveView from this guide:\n\n```elixir\n# lib/my_app_web/live/upload_live.ex\ndefmodule MyAppWeb.UploadLive do\n  use MyAppWeb, :live_view\n\n  @impl Phoenix.LiveView\n  def mount(_params, _session, socket) do\n    {:ok,\n    socket\n    |> assign(:uploaded_files, [])\n    |> allow_upload(:avatar, accept: ~w(.jpg .jpeg), max_entries: 2)}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"validate\", _params, socket) do\n    {:noreply, socket}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"cancel-upload\", %{\"ref\" => ref}, socket) do\n    {:noreply, cancel_upload(socket, :avatar, ref)}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"save\", _params, socket) do\n    uploaded_files =\n      consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->\n        dest = Path.join([:code.priv_dir(:my_app), \"static\", \"uploads\", Path.basename(path)])\n        # You will need to create `priv/static/uploads` for `File.cp!/2` to work.\n        File.cp!(path, dest)\n        {:ok, ~p\"/uploads/#{Path.basename(dest)}\"}\n      end)\n\n    {:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}\n  end\n\n  defp error_to_string(:too_large), do: \"Too large\"\n  defp error_to_string(:too_many_files), do: \"You have selected too many files\"\n  defp error_to_string(:not_accepted), do: \"You have selected an unacceptable file type\"\nend\n```\n\nTo access your uploads via your app, make sure to add `uploads` to\n`MyAppWeb.static_paths/0`.\n\n[`allow_upload/3`]: `Phoenix.LiveView.allow_upload/3`\n[`live_file_input/1`]: `Phoenix.Component.live_file_input/1`\n"
  },
  {
    "path": "jest.config.js",
    "content": "/*\n * For a detailed explanation regarding each configuration property, visit:\n * https://jestjs.io/docs/configuration\n */\n\nimport { default as packageJson } from \"./package.json\" with { type: \"json\" };\n\nexport default {\n  // All imported modules in your tests should be mocked automatically\n  // automock: false,\n\n  // Stop running tests after `n` failures\n  // bail: 0,\n\n  // The directory where Jest should store its cached dependency information\n  // cacheDirectory: \"/private/var/folders/7x/46yctrps77bd8hch96h9dkz80000gn/T/jest_dx\",\n\n  // Automatically clear mock calls and instances between every test\n  clearMocks: true,\n\n  // Indicates whether the coverage information should be collected while executing the test\n  // collectCoverage: false,\n\n  // An array of glob patterns indicating a set of files for which coverage information should be collected\n  // collectCoverageFrom: undefined,\n\n  // The directory where Jest should output its coverage files\n  // coverageDirectory: undefined,\n\n  // An array of regexp pattern strings used to skip coverage collection\n  coveragePathIgnorePatterns: [\"/node_modules/\", \"/assets/test/\"],\n\n  collectCoverage: true,\n\n  // Indicates which provider should be used to instrument code for coverage\n  coverageProvider: \"v8\",\n\n  // A list of reporter names that Jest uses when writing coverage reports\n  coverageReporters: [\"none\"],\n\n  // An object that configures minimum threshold enforcement for coverage results\n  // coverageThreshold: undefined,\n\n  // A path to a custom dependency extractor\n  // dependencyExtractor: undefined,\n\n  // Make calling deprecated APIs throw helpful error messages\n  // errorOnDeprecated: false,\n\n  // Force coverage collection from ignored files using an array of glob patterns\n  // forceCoverageMatch: [],\n\n  // A path to a module which exports an async function that is triggered once before all test suites\n  // globalSetup: undefined,\n\n  // A path to a module which exports an async function that is triggered once after all test suites\n  // globalTeardown: undefined,\n\n  // A set of global variables that need to be available in all test environments\n  globals: {\n    LV_VSN: packageJson.version,\n  },\n\n  // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.\n  // maxWorkers: \"50%\",\n\n  // An array of directory names to be searched recursively up from the requiring module's location\n  // moduleDirectories: [\n  //   \"node_modules\"\n  // ],\n\n  // An array of file extensions your modules use\n  // moduleFileExtensions: [\n  //   \"js\",\n  //   \"jsx\",\n  //   \"ts\",\n  //   \"tsx\",\n  //   \"json\",\n  //   \"node\"\n  // ],\n\n  // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module\n  moduleNameMapper: {\n    \"^phoenix_live_view$\": \"<rootDir>/assets/js/phoenix_live_view/index.ts\",\n    \"^phoenix_live_view/(.*)$\": \"<rootDir>/assets/js/phoenix_live_view/$1\",\n  },\n\n  // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader\n  // modulePathIgnorePatterns: [],\n\n  // Activates notifications for test results\n  // notify: false,\n\n  // An enum that specifies notification mode. Requires { notify: true }\n  // notifyMode: \"failure-change\",\n\n  // A preset that is used as a base for Jest's configuration\n  // preset: undefined,\n\n  // Run tests from one or more projects\n  // projects: undefined,\n\n  // Use this configuration option to add custom reporters to Jest\n  reporters: [\n    \"default\",\n    [\n      \"jest-monocart-coverage\",\n      {\n        name: \"Phoenix LiveView JS Unit Coverage\",\n        reports: [[\"raw\", { outputDir: \"./raw\" }], [\"v8\"], [\"console-summary\"]],\n        outputDir: \"./coverage\",\n      },\n    ],\n  ],\n\n  // Automatically reset mock state between every test\n  // resetMocks: false,\n\n  // Reset the module registry before running each individual test\n  // resetModules: false,\n\n  // A path to a custom resolver\n  // resolver: undefined,\n\n  // Automatically restore mock state between every test\n  // restoreMocks: false,\n\n  // The root directory that Jest should scan for tests and modules within\n  // rootDir: undefined,\n\n  // A list of paths to directories that Jest should use to search for files in\n  roots: [\n    \"<rootDir>/assets/test\"\n  ],\n\n  // Allows you to use a custom runner instead of Jest's default test runner\n  // runner: \"jest-runner\",\n\n  // The paths to modules that run some code to configure or set up the testing environment before each test\n  setupFiles: [\"<rootDir>/setupTests.js\"],\n\n  // A list of paths to modules that run some code to configure or set up the testing framework before each test\n  // setupFilesAfterEnv: [\n  //   \"<rootDir>/setupTestsAfterEnv.js\"\n  // ],\n\n  // The number of seconds after which a test is considered as slow and reported as such in the results.\n  // slowTestThreshold: 5,\n\n  // A list of paths to snapshot serializer modules Jest should use for snapshot testing\n  // snapshotSerializers: [],\n\n  // The test environment that will be used for testing\n  testEnvironment: \"jsdom\",\n\n  // Options that will be passed to the testEnvironment\n  // testEnvironmentOptions: {},\n\n  // Adds a location field to test results\n  // testLocationInResults: false,\n\n  // The glob patterns Jest uses to detect test files\n  // testMatch: [\n  //   \"**/__tests__/**/*.[jt]s?(x)\",\n  //   \"**/?(*.)+(spec|test).[tj]s?(x)\"\n  // ],\n\n  // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped\n  // testPathIgnorePatterns: [\n  //   \"/node_modules/\"\n  // ],\n\n  // The regexp pattern or array of patterns that Jest uses to detect test files\n  testRegex: \"/assets/test/.*_test\\\\.(js|ts)$\",\n\n  // This option allows the use of a custom results processor\n  // testResultsProcessor: undefined,\n\n  // This option allows use of a custom test runner\n  // testRunner: \"jest-circus/runner\",\n\n  // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href\n  // testURL: \"http://localhost\",\n\n  // Setting this value to \"fake\" allows the use of fake timers for functions such as \"setTimeout\"\n  // timers: \"real\",\n\n  // A map from regular expressions to paths to transformers\n  // transform: {},\n\n  // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation\n  // transformIgnorePatterns: [\n  //   \"/node_modules/\",\n  //   \"\\\\.pnp\\\\.[^\\\\/]+$\"\n  // ],\n\n  // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them\n  // unmockedModulePathPatterns: undefined,\n\n  // Indicates whether each individual test should be reported during the run\n  // verbose: undefined,\n\n  // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode\n  // watchPathIgnorePatterns: [],\n\n  // Whether to use watchman for file crawling\n  // watchman: true,\n};\n"
  },
  {
    "path": "lib/mix/tasks/compile/phoenix_live_view.ex",
    "content": "defmodule Mix.Tasks.Compile.PhoenixLiveView do\n  @moduledoc \"\"\"\n  A LiveView compiler for HEEx macro components.\n\n  Right now, only `Phoenix.LiveView.ColocatedHook` and `Phoenix.LiveView.ColocatedJS`\n  are handled.\n\n  You must add it to your `mix.exs` as:\n\n      compilers: [:phoenix_live_view] ++ Mix.compilers()\n\n  \"\"\"\n  use Mix.Task.Compiler\n\n  @recursive true\n\n  @doc false\n  def run(_args) do\n    Mix.Task.Compiler.after_compiler(:elixir, fn\n      {:noop, diagnostics} ->\n        {:noop, diagnostics}\n\n      {status, dignostics} ->\n        compile()\n        {status, dignostics}\n    end)\n\n    :noop\n  end\n\n  defp compile do\n    Phoenix.LiveView.ColocatedJS.compile()\n  end\nend\n"
  },
  {
    "path": "lib/mix/tasks/phoenix_live_view.upgrade.ex",
    "content": "if Code.ensure_loaded?(Igniter) do\n  defmodule Mix.Tasks.PhoenixLiveView.Upgrade do\n    @moduledoc false\n\n    use Igniter.Mix.Task\n\n    @impl Igniter.Mix.Task\n    def info(_argv, _composing_task) do\n      %Igniter.Mix.Task.Info{\n        # Groups allow for overlapping arguments for tasks by the same author\n        # See the generators guide for more.\n        group: :phoenix_live_view,\n        # *other* dependencies to add\n        # i.e `{:foo, \"~> 2.0\"}`\n        adds_deps: [],\n        # *other* dependencies to add and call their associated installers, if they exist\n        # i.e `{:foo, \"~> 2.0\"}`\n        installs: [],\n        # a list of positional arguments, i.e `[:file]`\n        positional: [:from, :to],\n        # Other tasks your task composes using `Igniter.compose_task`, passing in the CLI argv\n        # This ensures your option schema includes options from nested tasks\n        composes: [],\n        # `OptionParser` schema\n        schema: [\n          yes: :boolean\n        ],\n        # Default values for the options in the `schema`\n        defaults: [],\n        # CLI aliases\n        aliases: [],\n        # A list of options in the schema that are required\n        required: []\n      }\n    end\n\n    @impl Igniter.Mix.Task\n    def igniter(igniter) do\n      positional = igniter.args.positional\n      options = igniter.args.options\n\n      upgrades =\n        %{\n          \"1.1.0\" => [&Phoenix.LiveView.Igniter.UpgradeTo1_1.run/2]\n        }\n\n      # For each version that requires a change, add it to this map\n      # Each key is a version that points at a list of functions that take an\n      # igniter and options (i.e. flags or other custom options).\n      # See the upgrades guide for more.\n      Igniter.Upgrades.run(igniter, positional.from, positional.to, upgrades,\n        custom_opts: options\n      )\n    end\n  end\nelse\n  defmodule Mix.Tasks.PhoenixLiveView.Upgrade do\n    @moduledoc false\n\n    use Mix.Task\n\n    @impl Mix.Task\n    def run(_argv) do\n      Mix.shell().error(\"\"\"\n      The task 'phoenix_live_view.upgrade' requires igniter. Please install igniter and try again.\n\n      For more information, see: https://hexdocs.pm/igniter/readme.html#installation\n      \"\"\")\n\n      exit({:shutdown, 1})\n    end\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_component/declarative.ex",
    "content": "defmodule Phoenix.Component.Declarative do\n  @moduledoc false\n\n  ## Reserved assigns\n\n  # This list should only contain attributes that are given to components by engines\n  # @socket, @myself, etc. should not be listed here, as they shouldn't be given to\n  # function components in the first place\n  @reserved_assigns [:__changed__, :__slot__, :__given__, :inner_block]\n\n  @doc false\n  def __reserved__, do: @reserved_assigns\n\n  ## Global\n\n  @global_prefixes ~w(\n    phx-\n    aria-\n    data-\n  )\n  @globals ~w(\n    accesskey\n    alt\n    autocapitalize\n    autofocus\n    class\n    contenteditable\n    contextmenu\n    dir\n    draggable\n    enterkeyhint\n    exportparts\n    height\n    hidden\n    id\n    inert\n    inputmode\n    is\n    itemid\n    itemprop\n    itemref\n    itemscope\n    itemtype\n    lang\n    nonce\n    onabort\n    onautocomplete\n    onautocompleteerror\n    onblur\n    oncancel\n    oncanplay\n    oncanplaythrough\n    onchange\n    onclick\n    onclose\n    oncontextmenu\n    oncuechange\n    ondblclick\n    ondrag\n    ondragend\n    ondragenter\n    ondragleave\n    ondragover\n    ondragstart\n    ondrop\n    ondurationchange\n    onemptied\n    onended\n    onerror\n    onfocus\n    oninput\n    oninvalid\n    onkeydown\n    onkeypress\n    onkeyup\n    onload\n    onloadeddata\n    onloadedmetadata\n    onloadstart\n    onmousedown\n    onmouseenter\n    onmouseleave\n    onmousemove\n    onmouseout\n    onmouseover\n    onmouseup\n    onmousewheel\n    onpause\n    onplay\n    onplaying\n    onprogress\n    onratechange\n    onreset\n    onresize\n    onscroll\n    onseeked\n    onseeking\n    onselect\n    onshow\n    onsort\n    onstalled\n    onsubmit\n    onsuspend\n    ontimeupdate\n    ontoggle\n    onvolumechange\n    onwaiting\n    part\n    placeholder\n    popover\n    rel\n    role\n    slot\n    spellcheck\n    style\n    tabindex\n    target\n    title\n    translate\n    type\n    width\n    xml:base\n    xml:lang\n  )\n\n  @doc false\n  def __global__?(module, name, global_attr) when is_atom(module) and is_binary(name) do\n    includes = Keyword.get(global_attr.opts, :include, [])\n\n    if function_exported?(module, :__global__?, 1) do\n      module.__global__?(name) or __global__?(name) or name in includes\n    else\n      __global__?(name) or name in includes\n    end\n  end\n\n  for prefix <- @global_prefixes do\n    def __global__?(unquote(prefix) <> _), do: true\n  end\n\n  for name <- @globals do\n    def __global__?(unquote(name)), do: true\n  end\n\n  def __global__?(_), do: false\n\n  ## Def overrides\n\n  @doc false\n  defmacro def(expr, body) do\n    quote do\n      Kernel.def(unquote(annotate_def(:def, expr)), unquote(body))\n    end\n  end\n\n  @doc false\n  defmacro defp(expr, body) do\n    quote do\n      Kernel.defp(unquote(annotate_def(:defp, expr)), unquote(body))\n    end\n  end\n\n  defp annotate_def(kind, expr) do\n    case expr do\n      {:when, meta, [left, right]} -> {:when, meta, [annotate_call(kind, left), right]}\n      left -> annotate_call(kind, left)\n    end\n  end\n\n  defp annotate_call(kind, {name, meta, [{:\\\\, default_meta, [left, right]}]}),\n    do: {name, meta, [{:\\\\, default_meta, [annotate_arg(kind, left), right]}]}\n\n  defp annotate_call(kind, {name, meta, [arg]}),\n    do: {name, meta, [annotate_arg(kind, arg)]}\n\n  defp annotate_call(_kind, left),\n    do: left\n\n  defp annotate_arg(kind, {:=, meta, [{name, _, ctx} = var, arg]})\n       when is_atom(name) and is_atom(ctx) do\n    {:=, meta, [var, quote(do: unquote(__MODULE__).__pattern__!(unquote(kind), unquote(arg)))]}\n  end\n\n  defp annotate_arg(kind, {:=, meta, [arg, {name, _, ctx} = var]})\n       when is_atom(name) and is_atom(ctx) do\n    {:=, meta, [quote(do: unquote(__MODULE__).__pattern__!(unquote(kind), unquote(arg))), var]}\n  end\n\n  defp annotate_arg(kind, {name, meta, ctx} = var) when is_atom(name) and is_atom(ctx) do\n    {:=, meta, [quote(do: unquote(__MODULE__).__pattern__!(unquote(kind), _)), var]}\n  end\n\n  defp annotate_arg(kind, arg) do\n    quote(do: unquote(__MODULE__).__pattern__!(unquote(kind), unquote(arg)))\n  end\n\n  ## Attrs/slots\n\n  @doc false\n  @valid_opts [:global_prefixes]\n  def __setup__(module, opts) do\n    {prefixes, invalid_opts} = Keyword.pop(opts, :global_prefixes, [])\n\n    prefix_matches =\n      for prefix <- prefixes do\n        if not String.ends_with?(prefix, \"-\") do\n          raise ArgumentError,\n                \"global prefixes for #{inspect(module)} must end with a dash, got: #{inspect(prefix)}\"\n        end\n\n        quote(do: {unquote(prefix) <> _, true})\n      end\n\n    if invalid_opts != [] do\n      raise ArgumentError, \"\"\"\n      invalid options passed to #{inspect(__MODULE__)}.\n\n      The following options are supported: #{inspect(@valid_opts)}, got: #{inspect(invalid_opts)}\n      \"\"\"\n    end\n\n    Module.register_attribute(module, :__attrs__, accumulate: true)\n    Module.register_attribute(module, :__slot_attrs__, accumulate: true)\n    Module.register_attribute(module, :__slots__, accumulate: true)\n    Module.register_attribute(module, :__slot__, accumulate: false)\n    Module.register_attribute(module, :__components_calls__, accumulate: true)\n    Module.register_attribute(module, :__macro_components__, accumulate: true)\n    Module.put_attribute(module, :__components__, %{})\n    Module.put_attribute(module, :on_definition, __MODULE__)\n    Module.put_attribute(module, :before_compile, __MODULE__)\n\n    if prefix_matches == [] do\n      []\n    else\n      prefix_matches ++ [quote(do: {_, false})]\n    end\n  end\n\n  @doc false\n  def __slot__!(module, name, opts, line, file, block_fun) do\n    ensure_used!(module, line, file)\n    {doc, opts} = Keyword.pop(opts, :doc, nil)\n\n    if not (is_binary(doc) or is_nil(doc) or doc == false) do\n      compile_error!(line, file, \":doc must be a string or false, got: #{inspect(doc)}\")\n    end\n\n    {required, opts} = Keyword.pop(opts, :required, false)\n    {validate_attrs, opts} = Keyword.pop(opts, :validate_attrs, true)\n\n    if not is_boolean(required) do\n      compile_error!(line, file, \":required must be a boolean, got: #{inspect(required)}\")\n    end\n\n    Module.put_attribute(module, :__slot__, name)\n\n    slot_attrs =\n      try do\n        block_fun.()\n        module |> Module.get_attribute(:__slot_attrs__) |> Enum.reverse()\n      after\n        Module.put_attribute(module, :__slot__, nil)\n        Module.delete_attribute(module, :__slot_attrs__)\n      end\n\n    slot = %{\n      name: name,\n      required: required,\n      opts: opts,\n      doc: doc,\n      line: line,\n      attrs: slot_attrs,\n      validate_attrs: validate_attrs\n    }\n\n    validate_slot!(module, slot, line, file)\n    Module.put_attribute(module, :__slots__, slot)\n    :ok\n  end\n\n  defp validate_slot!(module, slot, line, file) do\n    slots = Module.get_attribute(module, :__slots__) || []\n\n    if Enum.find(slots, &(&1.name == slot.name)) do\n      compile_error!(line, file, \"\"\"\n      a duplicate slot with name #{inspect(slot.name)} already exists\\\n      \"\"\")\n    end\n\n    if slot.name == :inner_block and slot.attrs != [] do\n      compile_error!(line, file, \"\"\"\n      cannot define attributes in a slot with name #{inspect(slot.name)}\n      \"\"\")\n    end\n\n    if slot.opts != [] do\n      compile_error!(\n        line,\n        file,\n        \"invalid options #{inspect(slot.opts)} for slot #{inspect(slot.name)}. The supported options are: [:required, :doc, :validate_attrs]\"\n      )\n    end\n  end\n\n  @doc false\n  def __attr__!(module, name, type, opts, line, file) when is_atom(name) and is_list(opts) do\n    ensure_used!(module, line, file)\n    slot = Module.get_attribute(module, :__slot__)\n\n    if name == :inner_block do\n      compile_error!(\n        line,\n        file,\n        \"cannot define attribute called :inner_block. Maybe you wanted to use `slot` instead?\"\n      )\n    end\n\n    if type == :global && slot do\n      compile_error!(line, file, \"cannot define :global slot attributes\")\n    end\n\n    if type == :global and Keyword.has_key?(opts, :required) do\n      compile_error!(line, file, \"global attributes do not support the :required option\")\n    end\n\n    if type == :global and Keyword.has_key?(opts, :values) do\n      compile_error!(line, file, \"global attributes do not support the :values option\")\n    end\n\n    if type == :global and Keyword.has_key?(opts, :examples) do\n      compile_error!(line, file, \"global attributes do not support the :examples option\")\n    end\n\n    if type != :global and Keyword.has_key?(opts, :include) do\n      compile_error!(line, file, \":include is only supported for :global attributes\")\n    end\n\n    {doc, opts} = Keyword.pop(opts, :doc, nil)\n\n    if not (is_binary(doc) or is_nil(doc) or doc == false) do\n      compile_error!(line, file, \":doc must be a string or false, got: #{inspect(doc)}\")\n    end\n\n    {required, opts} = Keyword.pop(opts, :required, false)\n\n    if not is_boolean(required) do\n      compile_error!(line, file, \":required must be a boolean, got: #{inspect(required)}\")\n    end\n\n    if required and Keyword.has_key?(opts, :default) do\n      compile_error!(line, file, \"only one of :required or :default must be given\")\n    end\n\n    key = if slot, do: :__slot_attrs__, else: :__attrs__\n    type = validate_attr_type!(module, key, slot, name, type, line, file)\n    validate_attr_opts!(slot, name, opts, line, file)\n\n    if Keyword.has_key?(opts, :values) and Keyword.has_key?(opts, :examples) do\n      compile_error!(line, file, \"only one of :values or :examples must be given\")\n    end\n\n    if Keyword.has_key?(opts, :values) do\n      validate_attr_values!(slot, name, type, opts[:values], line, file)\n    end\n\n    if Keyword.has_key?(opts, :examples) do\n      validate_attr_examples!(slot, name, type, opts[:examples], line, file)\n    end\n\n    if Keyword.has_key?(opts, :default) do\n      validate_attr_default!(slot, name, type, opts, line, file)\n    end\n\n    attr = %{\n      slot: slot,\n      name: name,\n      type: type,\n      required: required,\n      opts: opts,\n      doc: doc,\n      line: line\n    }\n\n    Module.put_attribute(module, key, attr)\n    :ok\n  end\n\n  @builtin_types [:boolean, :integer, :float, :string, :atom, :list, :map, :fun, :global]\n  @valid_types [:any] ++ @builtin_types\n\n  defp validate_attr_type!(module, key, slot, name, type, line, file)\n       when is_atom(type) or is_tuple(type) do\n    attrs = Module.get_attribute(module, key) || []\n\n    cond do\n      Enum.find(attrs, fn attr -> attr.name == name end) ->\n        compile_error!(line, file, \"\"\"\n        a duplicate attribute with name #{attr_slot(name, slot)} already exists\\\n        \"\"\")\n\n      existing = type == :global && Enum.find(attrs, fn attr -> attr.type == :global end) ->\n        compile_error!(line, file, \"\"\"\n        cannot define :global attribute #{inspect(name)} because one \\\n        is already defined as #{attr_slot(existing.name, slot)}. \\\n        Only a single :global attribute may be defined\\\n        \"\"\")\n\n      true ->\n        :ok\n    end\n\n    cond do\n      type in @valid_types -> type\n      is_tuple(type) -> validate_tuple_attr_type!(slot, name, type, line, file)\n      type |> Atom.to_string() |> String.starts_with?(\"Elixir.\") -> {:struct, type}\n      true -> bad_type!(slot, name, type, line, file)\n    end\n  end\n\n  defp validate_attr_type!(_module, _key, slot, name, type, line, file) do\n    bad_type!(slot, name, type, line, file)\n  end\n\n  defp validate_tuple_attr_type!(_slot, _name, {:fun, arity} = type, _line, _file)\n       when is_integer(arity) do\n    type\n  end\n\n  defp validate_tuple_attr_type!(slot, name, type, line, file) do\n    bad_type!(slot, name, type, line, file)\n  end\n\n  defp bad_type!(slot, name, type, line, file) do\n    compile_error!(line, file, \"\"\"\n    invalid type #{inspect(type)} for attr #{attr_slot(name, slot)}. \\\n    The following types are supported:\n\n      * any Elixir struct, such as URI, MyApp.User, etc\n      * one of #{Enum.map_join(@builtin_types, \", \", &inspect/1)}\n      * a function written as:\n          * without arity, ex: :fun\n          * with a specific arity, ex: {:fun, 2}\n      * :any for all other types\n    \"\"\")\n  end\n\n  defp attr_slot(name, nil), do: \"#{inspect(name)}\"\n  defp attr_slot(name, slot), do: \"#{inspect(name)} in slot #{inspect(slot)}\"\n\n  defp validate_attr_default!(slot, name, type, opts, line, file) do\n    case {opts[:default], opts[:values]} do\n      {default, nil} ->\n        if not valid_value?(type, default) do\n          bad_default!(slot, name, type, default, line, file)\n        end\n\n      {default, values} ->\n        if default not in values do\n          compile_error!(line, file, \"\"\"\n          expected the default value for attr #{attr_slot(name, slot)} to be one of #{inspect(values)}, \\\n          got: #{inspect(default)}\n          \"\"\")\n        end\n    end\n  end\n\n  defp bad_default!(slot, name, type, default, line, file) do\n    compile_error!(line, file, \"\"\"\n    expected the default value for attr #{attr_slot(name, slot)} to be #{type_with_article(type)}, \\\n    got: #{inspect(default)}\n    \"\"\")\n  end\n\n  defp validate_attr_values!(slot, name, type, values, line, file) do\n    if not is_enumerable(values) or Enum.empty?(values) do\n      compile_error!(line, file, \"\"\"\n      :values must be a non-empty enumerable, got: #{inspect(values)}\n      \"\"\")\n    end\n\n    for value <- values,\n        not valid_value?(type, value),\n        do: bad_value!(slot, name, type, value, line, file)\n  end\n\n  defp is_enumerable(values) do\n    Enumerable.impl_for(values) != nil\n  end\n\n  defp bad_value!(slot, name, type, value, line, file) do\n    compile_error!(line, file, \"\"\"\n    expected the values for attr #{attr_slot(name, slot)} to be #{type_with_article(type)}, \\\n    got: #{inspect(value)}\n    \"\"\")\n  end\n\n  defp validate_attr_examples!(slot, name, type, examples, line, file) do\n    if not is_list(examples) or Enum.empty?(examples) do\n      compile_error!(line, file, \"\"\"\n      :examples must be a non-empty list, got: #{inspect(examples)}\n      \"\"\")\n    end\n\n    for example <- examples,\n        not valid_value?(type, example),\n        do: bad_example!(slot, name, type, example, line, file)\n  end\n\n  defp bad_example!(slot, name, type, example, line, file) do\n    compile_error!(line, file, \"\"\"\n    expected the examples for attr #{attr_slot(name, slot)} to be #{type_with_article(type)}, \\\n    got: #{inspect(example)}\n    \"\"\")\n  end\n\n  defp valid_value?(_type, nil), do: true\n  defp valid_value?(:any, _value), do: true\n  defp valid_value?(:string, value), do: is_binary(value)\n  defp valid_value?(:atom, value), do: is_atom(value)\n  defp valid_value?(:boolean, value), do: is_boolean(value)\n  defp valid_value?(:integer, value), do: is_integer(value)\n  defp valid_value?(:float, value), do: is_float(value)\n  defp valid_value?(:list, value), do: is_list(value)\n  defp valid_value?({:struct, mod}, value), do: is_struct(value, mod)\n  defp valid_value?(_type, _value), do: true\n\n  defp validate_attr_opts!(slot, name, opts, line, file) do\n    for {key, _} <- opts, message = invalid_attr_message(key, slot) do\n      compile_error!(line, file, \"\"\"\n      invalid option #{inspect(key)} for attr #{attr_slot(name, slot)}. #{message}\\\n      \"\"\")\n    end\n  end\n\n  defp invalid_attr_message(:include, inc) when is_list(inc) or is_nil(inc), do: nil\n\n  defp invalid_attr_message(:include, other),\n    do: \"include only supports a list of attributes, got: #{inspect(other)}\"\n\n  defp invalid_attr_message(:default, nil), do: nil\n\n  defp invalid_attr_message(:default, _),\n    do:\n      \":default is not supported inside slot attributes, \" <>\n        \"instead use Map.get/3 with a default value when accessing a slot attribute\"\n\n  defp invalid_attr_message(:required, _), do: nil\n  defp invalid_attr_message(:values, _), do: nil\n  defp invalid_attr_message(:examples, _), do: nil\n\n  defp invalid_attr_message(_key, nil),\n    do: \"The supported options are: [:required, :default, :values, :examples, :include]\"\n\n  defp invalid_attr_message(_key, _slot),\n    do: \"The supported options inside slots are: [:required]\"\n\n  defp compile_error!(line, file, msg) do\n    raise CompileError, line: line, file: file, description: msg\n  end\n\n  defmacro __pattern__!(kind, arg) do\n    {name, 1} = __CALLER__.function\n    {_slots, attrs} = register_component!(kind, __CALLER__, name, true)\n\n    fields =\n      for %{name: name, required: true, type: {:struct, struct}} <- attrs do\n        {name, quote(do: %unquote(struct){})}\n      end\n\n    if fields == [] do\n      arg\n    else\n      quote(do: %{unquote_splicing(fields)} = unquote(arg))\n    end\n  end\n\n  @doc false\n  def __on_definition__(env, kind, name, args, _guards, body) do\n    check? = not String.starts_with?(to_string(name), \"__\")\n\n    cond do\n      check? and length(args) == 1 and body == nil ->\n        register_component!(kind, env, name, false)\n\n      check? ->\n        attrs = pop_attrs(env)\n\n        validate_misplaced_attrs!(attrs, env.file, fn ->\n          case length(args) do\n            1 ->\n              \"could not define attributes for function #{name}/1. \" <>\n                \"Please make sure that you have `use Phoenix.Component` and that the function has no default arguments\"\n\n            arity ->\n              \"cannot declare attributes for function #{name}/#{arity}. Components must be functions with arity 1\"\n          end\n        end)\n\n        slots = pop_slots(env)\n\n        validate_misplaced_slots!(slots, env.file, fn ->\n          case length(args) do\n            1 ->\n              \"could not define slots for function #{name}/1. \" <>\n                \"Components cannot be dynamically defined or have default arguments\"\n\n            arity ->\n              \"cannot declare slots for function #{name}/#{arity}. Components must be functions with arity 1\"\n          end\n        end)\n\n      true ->\n        :ok\n    end\n  end\n\n  @doc false\n  defmacro __before_compile__(env) do\n    attrs = pop_attrs(env)\n\n    validate_misplaced_attrs!(attrs, env.file, fn ->\n      \"cannot define attributes without a related function component\"\n    end)\n\n    slots = pop_slots(env)\n\n    validate_misplaced_slots!(slots, env.file, fn ->\n      \"cannot define slots without a related function component\"\n    end)\n\n    components = Module.get_attribute(env.module, :__components__)\n    components_calls = Module.get_attribute(env.module, :__components_calls__) |> Enum.reverse()\n    macro_components = Module.get_attribute(env.module, :__macro_components__)\n\n    names_and_defs =\n      for {name, %{kind: kind, attrs: attrs, slots: slots, line: line}} <- components do\n        attr_defaults =\n          for %{name: name, required: false, opts: opts} <- attrs,\n              Keyword.has_key?(opts, :default),\n              do: {name, Macro.escape(opts[:default])}\n\n        slot_defaults =\n          for %{name: name, required: false} <- slots do\n            {name, []}\n          end\n\n        defaults = attr_defaults ++ slot_defaults\n\n        {global_name, global_default} =\n          case Enum.find(attrs, fn attr -> attr.type == :global end) do\n            %{name: name, opts: opts} -> {name, Macro.escape(Keyword.get(opts, :default, %{}))}\n            nil -> {nil, nil}\n          end\n\n        attr_names = for(attr <- attrs, do: attr.name)\n        slot_names = for(slot <- slots, do: slot.name)\n        known_keys = attr_names ++ slot_names ++ @reserved_assigns\n\n        def_body =\n          if global_name do\n            quote do\n              {assigns, caller_globals} = Map.split(assigns, unquote(known_keys))\n\n              globals =\n                case assigns do\n                  %{unquote(global_name) => explicit_global_assign} -> explicit_global_assign\n                  %{} -> Map.merge(unquote(global_default), caller_globals)\n                end\n\n              merged =\n                %{unquote_splicing(defaults)}\n                |> Map.merge(assigns)\n                |> Map.put(:__given__, assigns)\n\n              super(Phoenix.Component.assign(merged, unquote(global_name), globals))\n            end\n          else\n            quote do\n              merged =\n                %{unquote_splicing(defaults)}\n                |> Map.merge(assigns)\n                |> Map.put(:__given__, assigns)\n\n              super(merged)\n            end\n          end\n\n        # Generated function definitions do not emit unused warnings,\n        # but in this case, as we are simply overriding the user function,\n        # so we delete the context so it still warns.\n        {remote, meta, [call, args]} =\n          quote line: line do\n            Kernel.unquote(kind)(unquote(name)(assigns)) do\n              unquote(def_body)\n            end\n          end\n\n        {{name, 1}, {remote, meta, [delete_context(call), args]}}\n      end\n\n    {names, defs} = Enum.unzip(names_and_defs)\n\n    overridable =\n      if names != [] do\n        quote do\n          defoverridable unquote(names)\n        end\n      end\n\n    def_components_ast =\n      quote do\n        def __components__() do\n          unquote(Macro.escape(components))\n        end\n      end\n\n    def_components_calls_ast =\n      if components_calls != [] do\n        quote do\n          @after_verify {__MODULE__, :__phoenix_component_verify__}\n\n          @doc false\n          def __phoenix_component_verify__(module) do\n            unquote(__MODULE__).__verify__(module, unquote(Macro.escape(components_calls)))\n          end\n        end\n      end\n\n    macro_components_ast =\n      if macro_components != [] do\n        grouped =\n          Enum.group_by(macro_components, fn {module, _data} -> module end, fn {_module, data} ->\n            data\n          end)\n\n        quote do\n          @doc false\n          def __phoenix_macro_components__ do\n            unquote(Macro.escape(grouped))\n          end\n        end\n      end\n\n    {:__block__, [],\n     [def_components_ast, def_components_calls_ast, macro_components_ast, overridable | defs]}\n  end\n\n  defp delete_context(node) do\n    Macro.update_meta(node, &Keyword.delete(&1, :context))\n  end\n\n  defp register_component!(kind, env, name, check_if_defined?) do\n    slots = pop_slots(env)\n    attrs = pop_attrs(env)\n\n    cond do\n      slots != [] or attrs != [] ->\n        check_if_defined? and raise_if_function_already_defined!(env, name, slots, attrs)\n        register_component_doc(env, kind, slots, attrs)\n\n        for %{name: slot_name, line: line} <- slots,\n            Enum.find(attrs, &(&1.name == slot_name)) do\n          compile_error!(line, env.file, \"\"\"\n          cannot define a slot with name #{inspect(slot_name)}, as an attribute with that name already exists\\\n          \"\"\")\n        end\n\n        components =\n          env.module\n          |> Module.get_attribute(:__components__)\n          # Sort by name as this is used when they are validated\n          |> Map.put(name, %{\n            kind: kind,\n            attrs: Enum.sort_by(attrs, & &1.name),\n            slots: Enum.sort_by(slots, & &1.name),\n            line: env.line\n          })\n\n        Module.put_attribute(env.module, :__components__, components)\n        Module.put_attribute(env.module, :__last_component__, name)\n        {slots, attrs}\n\n      Module.get_attribute(env.module, :__last_component__) == name ->\n        %{slots: slots, attrs: attrs} = Module.get_attribute(env.module, :__components__)[name]\n        {slots, attrs}\n\n      true ->\n        {[], []}\n    end\n  end\n\n  # Documentation handling\n\n  defp register_component_doc(env, :def, slots, attrs) do\n    case Module.get_attribute(env.module, :doc) do\n      {_line, false} ->\n        :ok\n\n      {line, doc} ->\n        Module.put_attribute(env.module, :doc, {line, build_component_doc(doc, slots, attrs)})\n\n      nil ->\n        Module.put_attribute(env.module, :doc, {env.line, build_component_doc(slots, attrs)})\n    end\n  end\n\n  defp register_component_doc(_env, :defp, _slots, _attrs) do\n    :ok\n  end\n\n  defp build_component_doc(doc \\\\ \"\", slots, attrs) do\n    [left | right] = String.split(doc, \"[INSERT LVATTRDOCS]\")\n\n    IO.iodata_to_binary([\n      build_left_doc(left),\n      build_component_docs(slots, attrs),\n      build_right_doc(right)\n    ])\n  end\n\n  defp build_left_doc(\"\") do\n    [\"\"]\n  end\n\n  defp build_left_doc(left) do\n    [left, ?\\n]\n  end\n\n  defp build_component_docs(slots, attrs) do\n    case {slots, attrs} do\n      {[], []} ->\n        []\n\n      {slots, [] = _attrs} ->\n        [build_slots_docs(slots)]\n\n      {[] = _slots, attrs} ->\n        [build_attrs_docs(attrs)]\n\n      {slots, attrs} ->\n        [build_attrs_docs(attrs), ?\\n, build_slots_docs(slots)]\n    end\n  end\n\n  defp build_slots_docs(slots) do\n    [\n      \"## Slots\\n\",\n      for slot <- slots, slot.doc != false, into: [] do\n        slot_attrs =\n          for slot_attr <- slot.attrs,\n              slot_attr.doc != false,\n              slot_attr.slot == slot.name,\n              do: slot_attr\n\n        [\n          \"\\n* \",\n          build_slot_name(slot),\n          build_slot_required(slot),\n          build_slot_doc(slot, slot_attrs)\n        ]\n      end\n    ]\n  end\n\n  defp build_attrs_docs(attrs) do\n    [\n      \"## Attributes\\n\",\n      for attr <- attrs, attr.doc != false and attr.type != :global do\n        [\n          \"\\n* \",\n          build_attr_name(attr),\n          build_attr_type(attr),\n          build_attr_required(attr),\n          build_hyphen(attr),\n          build_attr_doc_and_default(attr, \"  \"),\n          build_attr_values_or_examples(attr)\n        ]\n      end,\n      # global always goes at the end\n      case Enum.find(attrs, &(&1.type === :global)) do\n        nil -> []\n        attr -> build_attr_doc_and_default(attr, \"  \")\n      end\n    ]\n  end\n\n  defp build_slot_name(%{name: name}) do\n    [\"`\", Atom.to_string(name), \"`\"]\n  end\n\n  defp build_slot_doc(%{doc: nil}, []) do\n    []\n  end\n\n  defp build_slot_doc(%{doc: doc}, []) do\n    [\" - \", build_doc(doc, \"  \", false)]\n  end\n\n  defp build_slot_doc(%{doc: nil}, slot_attrs) do\n    [\" - Accepts attributes:\\n\", build_slot_attrs_docs(slot_attrs)]\n  end\n\n  defp build_slot_doc(%{doc: doc}, slot_attrs) do\n    [\n      \" - \",\n      build_doc(doc, \"  \", true),\n      \"Accepts attributes:\\n\",\n      build_slot_attrs_docs(slot_attrs)\n    ]\n  end\n\n  defp build_slot_attrs_docs(slot_attrs) do\n    for slot_attr <- slot_attrs do\n      [\n        \"\\n  * \",\n        build_attr_name(slot_attr),\n        build_attr_type(slot_attr),\n        build_attr_required(slot_attr),\n        build_hyphen(slot_attr),\n        build_attr_doc_and_default(slot_attr, \"    \"),\n        build_attr_values_or_examples(slot_attr)\n      ]\n    end\n  end\n\n  defp build_slot_required(%{required: true}) do\n    [\" (required)\"]\n  end\n\n  defp build_slot_required(_slot) do\n    []\n  end\n\n  defp build_attr_name(%{name: name}) do\n    [\"`\", Atom.to_string(name), \"` \"]\n  end\n\n  defp build_attr_type(%{type: {:struct, type}}) do\n    [\"(`\", inspect(type), \"`)\"]\n  end\n\n  defp build_attr_type(%{type: type}) do\n    [\"(`\", inspect(type), \"`)\"]\n  end\n\n  defp build_attr_required(%{required: true}) do\n    [\" (required)\"]\n  end\n\n  defp build_attr_required(_attr) do\n    []\n  end\n\n  defp build_attr_doc_and_default(%{doc: doc, type: :global, opts: opts}, indent) do\n    [\n      \"\\n* Global attributes are accepted.\",\n      if(doc, do: [\" \", build_doc(doc, indent, false)], else: []),\n      case Keyword.get(opts, :include) do\n        inc when is_list(inc) and inc != [] ->\n          [\" \", \"Supports all globals plus:\", \" \", build_literal(inc), \".\"]\n\n        _ ->\n          []\n      end\n    ]\n  end\n\n  defp build_attr_doc_and_default(%{doc: doc, opts: opts}, indent) do\n    case Keyword.fetch(opts, :default) do\n      {:ok, default} ->\n        if doc do\n          [build_doc(doc, indent, true), \"Defaults to \", build_literal(default), \".\"]\n        else\n          [\"Defaults to \", build_literal(default), \".\"]\n        end\n\n      :error ->\n        if doc, do: [build_doc(doc, indent, false)], else: []\n    end\n  end\n\n  defp build_doc(doc, indent, text_after?) do\n    doc = String.trim(doc)\n    [head | tail] = String.split(doc, [\"\\r\\n\", \"\\n\"])\n    dot = if String.ends_with?(doc, \".\"), do: [], else: [?.]\n\n    tail =\n      Enum.map(tail, fn\n        \"\" -> \"\\n\"\n        other -> [?\\n, indent | other]\n      end)\n\n    case tail do\n      # Single line\n      [] when text_after? ->\n        [[head | tail], dot, ?\\s]\n\n      [] ->\n        [[head | tail], dot]\n\n      # Multi-line\n      _ when text_after? ->\n        [[head | tail], \"\\n\\n\", indent]\n\n      _ ->\n        [[head | tail], \"\\n\"]\n    end\n  end\n\n  defp build_attr_values_or_examples(%{opts: opts} = attr) do\n    space_before = if attr[:doc] || opts[:default], do: ?\\s, else: []\n\n    cond do\n      Keyword.get(opts, :values) ->\n        [space_before, \"Must be one of \", build_literals_list(opts[:values], \"or\"), ?.]\n\n      Keyword.get(opts, :examples) ->\n        [space_before, \"Examples include \", build_literals_list(opts[:examples], \"and\"), ?.]\n\n      true ->\n        []\n    end\n  end\n\n  defp build_literals_list([literal], _condition) do\n    [build_literal(literal)]\n  end\n\n  defp build_literals_list(literals, condition) do\n    literals\n    |> Enum.map_intersperse(\", \", &build_literal/1)\n    |> List.insert_at(-2, [condition, \" \"])\n  end\n\n  defp build_literal(literal) do\n    [?`, inspect(literal, charlists: :as_list), ?`]\n  end\n\n  defp build_hyphen(%{doc: doc}) when is_binary(doc) do\n    [\" - \"]\n  end\n\n  defp build_hyphen(%{opts: []}) do\n    []\n  end\n\n  defp build_hyphen(%{opts: _opts}) do\n    [\" - \"]\n  end\n\n  defp build_right_doc(\"\") do\n    []\n  end\n\n  defp build_right_doc(right) do\n    [?\\n, right]\n  end\n\n  defp validate_misplaced_attrs!(attrs, file, message_fun) do\n    with [%{line: first_attr_line} | _] <- attrs do\n      compile_error!(first_attr_line, file, message_fun.())\n    end\n  end\n\n  defp validate_misplaced_slots!(slots, file, message_fun) do\n    with [%{line: first_slot_line} | _] <- slots do\n      compile_error!(first_slot_line, file, message_fun.())\n    end\n  end\n\n  defp pop_attrs(env) do\n    slots = Module.delete_attribute(env.module, :__attrs__) || []\n    Enum.reverse(slots)\n  end\n\n  defp pop_slots(env) do\n    slots = Module.delete_attribute(env.module, :__slots__) || []\n    Enum.reverse(slots)\n  end\n\n  defp raise_if_function_already_defined!(env, name, slots, attrs) do\n    if Module.defines?(env.module, {name, 1}) do\n      {:v1, _, meta, _} = Module.get_definition(env.module, {name, 1})\n\n      with [%{line: first_attr_line} | _] <- attrs do\n        compile_error!(first_attr_line, env.file, \"\"\"\n        attributes must be defined before the first function clause at line #{meta[:line]}\n        \"\"\")\n      end\n\n      with [%{line: first_slot_line} | _] <- slots do\n        compile_error!(first_slot_line, env.file, \"\"\"\n        slots must be defined before the first function clause at line #{meta[:line]}\n        \"\"\")\n      end\n    end\n  end\n\n  # Verification\n\n  @doc false\n  def __verify__(module, component_calls) do\n    for %{component: {submod, fun}} = call <- component_calls,\n        Code.ensure_loaded?(submod) and function_exported?(submod, :__components__, 0),\n        component = submod.__components__()[fun],\n        do: verify(module, call, component)\n\n    :ok\n  end\n\n  defp verify(\n         caller_module,\n         %{slots: slots, attrs: attrs, root: root} = call,\n         %{slots: slots_defs, attrs: attrs_defs} = _component\n       ) do\n    {attrs, global_attr} =\n      Enum.reduce(attrs_defs, {attrs, nil}, fn attr_def, {attrs, global_attr} ->\n        %{name: name, required: required, type: type, opts: opts} = attr_def\n        attr_values = Keyword.get(opts, :values, nil)\n        {value, attrs} = Map.pop(attrs, name)\n\n        case {type, value} do\n          # missing required attr\n          {_type, nil} when not root and required ->\n            message = \"missing required attribute \\\"#{name}\\\" for component #{component_fa(call)}\"\n            warn(message, call.file, call.line)\n\n          # missing optional attr, or dynamic attr\n          {_type, nil} when root or not required ->\n            :ok\n\n          # global attrs cannot be directly used\n          {:global, {line, _column, _type_value}} ->\n            message =\n              \"global attribute \\\"#{name}\\\" in component #{component_fa(call)} may not be provided directly\"\n\n            warn(message, call.file, line)\n\n          # attrs must be one of values\n          {type, {line, _column, {_, type_value} = actual_type}} when not is_nil(attr_values) ->\n            if type_value not in attr_values do\n              value_ast_to_string = type_mismatch(type, actual_type) || inspect(type_value)\n\n              message =\n                \"attribute \\\"#{name}\\\" in component #{component_fa(call)} must be one of #{inspect(attr_values)}, got: \" <>\n                  value_ast_to_string\n\n              warn(message, call.file, line)\n            end\n\n          # attrs must be of the declared type\n          {type, {line, _column, type_value}} ->\n            if value_ast_to_string = type_mismatch(type, type_value) do\n              message =\n                \"attribute \\\"#{name}\\\" in component #{component_fa(call)} must be #{type_with_article(type)}, got: \" <>\n                  value_ast_to_string\n\n              [warn(message, call.file, line)]\n            end\n        end\n\n        {attrs, global_attr || (type == :global and attr_def)}\n      end)\n\n    for {name, {line, _column, _type_value}} <- attrs,\n        !(global_attr && __global__?(caller_module, Atom.to_string(name), global_attr)) do\n      message = \"undefined attribute \\\"#{name}\\\" for component #{component_fa(call)}\"\n      warn(message, call.file, line)\n    end\n\n    undefined_slots =\n      Enum.reduce(slots_defs, slots, fn slot_def, slots ->\n        %{name: slot_name, required: required, attrs: attrs, validate_attrs: validate_attrs} =\n          slot_def\n\n        {slot_values, slots} = Map.pop(slots, slot_name)\n\n        case slot_values do\n          # missing required slot\n          nil when required ->\n            message = \"missing required slot \\\"#{slot_name}\\\" for component #{component_fa(call)}\"\n            warn(message, call.file, call.line)\n\n          # missing optional slot\n          nil ->\n            :ok\n\n          # slot with attributes\n          _ ->\n            slot_attr_defs = Enum.into(attrs, %{}, &{&1.name, &1})\n            required_attrs = for {attr_name, %{required: true}} <- slot_attr_defs, do: attr_name\n\n            for %{attrs: slot_attrs, line: slot_line, root: false} <- slot_values,\n                attr_name <- required_attrs,\n                not Map.has_key?(slot_attrs, attr_name) do\n              message =\n                \"missing required attribute \\\"#{attr_name}\\\" in slot \\\"#{slot_name}\\\" \" <>\n                  \"for component #{component_fa(call)}\"\n\n              warn(message, call.file, slot_line)\n            end\n\n            for %{attrs: slot_attrs} <- slot_values,\n                {attr_name, {line, _column, type_value}} <- slot_attrs do\n              case slot_attr_defs do\n                # slots cannot accept global attributes\n                %{^attr_name => %{type: :global}} ->\n                  message =\n                    \"global attribute \\\"#{attr_name}\\\" in slot \\\"#{slot_name}\\\" \" <>\n                      \"for component #{component_fa(call)} may not be provided directly\"\n\n                  warn(message, call.file, line)\n\n                # slot attrs must be one of values\n                %{^attr_name => %{type: _type, opts: [values: attr_values]}}\n                when is_tuple(type_value) and tuple_size(type_value) == 2 ->\n                  {_, attr_value} = type_value\n\n                  if attr_value not in attr_values do\n                    message =\n                      \"attribute \\\"#{attr_name}\\\" in slot \\\"#{slot_name}\\\" \" <>\n                        \"for component #{component_fa(call)} must be one of #{inspect(attr_values)}, got: \" <>\n                        inspect(attr_value)\n\n                    warn(message, call.file, line)\n                  end\n\n                # slot attrs must be of the declared type\n                %{^attr_name => %{type: type}} ->\n                  if value_ast_to_string = type_mismatch(type, type_value) do\n                    message =\n                      \"attribute \\\"#{attr_name}\\\" in slot \\\"#{slot_name}\\\" \" <>\n                        \"for component #{component_fa(call)} must be #{type_with_article(type)}, got: \" <>\n                        value_ast_to_string\n\n                    warn(message, call.file, line)\n                  end\n\n                # undefined slot attr\n                %{} ->\n                  cond do\n                    attr_name == :inner_block ->\n                      :ok\n\n                    attrs == [] and not validate_attrs ->\n                      :ok\n\n                    true ->\n                      message =\n                        \"undefined attribute \\\"#{attr_name}\\\" in slot \\\"#{slot_name}\\\" \" <>\n                          \"for component #{component_fa(call)}\"\n\n                      warn(message, call.file, line)\n                  end\n              end\n            end\n        end\n\n        slots\n      end)\n\n    for {slot_name, slot_values} <- undefined_slots,\n        %{line: line} <- slot_values,\n        not implicit_inner_block?(slot_name, slots_defs) do\n      message = \"undefined slot \\\"#{slot_name}\\\" for component #{component_fa(call)}\"\n      warn(message, call.file, line)\n    end\n\n    :ok\n  end\n\n  defp implicit_inner_block?(slot_name, slots_defs) do\n    slot_name == :inner_block and length(slots_defs) > 0\n  end\n\n  defp type_mismatch(:any, _type_value), do: nil\n  defp type_mismatch(_type, :any), do: nil\n  defp type_mismatch(type, {type, _value}), do: nil\n  defp type_mismatch(:atom, {:boolean, _value}), do: nil\n  defp type_mismatch({:struct, _}, {:map, {:%{}, _, [{:|, _, [_, _]}]}}), do: nil\n  defp type_mismatch(:fun, {:fun, _}), do: nil\n  defp type_mismatch({:fun, arity}, {:fun, arity}), do: nil\n  defp type_mismatch({:fun, _arity}, {:fun, arity}), do: type_with_article({:fun, arity})\n  defp type_mismatch(_type, {:fun, arity}), do: type_with_article({:fun, arity})\n  defp type_mismatch(_type, {_, value}), do: Macro.to_string(value)\n\n  defp component_fa(%{component: {mod, fun}}) do\n    \"#{inspect(mod)}.#{fun}/1\"\n  end\n\n  ## Shared helpers\n\n  defp type_with_article({:struct, struct}), do: \"a #{inspect(struct)} struct\"\n  defp type_with_article(:fun), do: \"a function\"\n  defp type_with_article({:fun, arity}), do: \"a function of arity #{arity}\"\n  defp type_with_article(type) when type in [:atom, :integer], do: \"an #{inspect(type)}\"\n  defp type_with_article(type), do: \"a #{inspect(type)}\"\n\n  # TODO: Provide column information in error messages\n  defp warn(message, file, line) do\n    IO.warn(message, file: file, line: line)\n  end\n\n  defp ensure_used!(module, line, file) do\n    if !Module.get_attribute(module, :__attrs__) do\n      compile_error!(\n        line,\n        file,\n        \"you must `use Phoenix.Component` to declare attributes. It is currently only imported.\"\n      )\n    end\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_component/macro_component.ex",
    "content": "defmodule Phoenix.Component.MacroComponent do\n  @moduledoc false\n\n  #   A macro component is a special type of component that can modify its content\n  #   at compile time.\n  #\n  #   Instead of introducing a special tag syntax like `<#macro-component>`, LiveView\n  #   implements them using a special `:type` attribute as the most useful macro\n  #   components take their content and extract it to somewhere else, for example\n  #   to a file in the local file system. A good example for this is `Phoenix.LiveView.ColocatedHook`\n  #   and `Phoenix.LiveView.ColocatedJS`.\n  #\n  #   ## AST\n  #\n  #   Macro components work by defining a callback module that implements the\n  #   `Phoenix.LiveView.MacroComponent` behaviour. The module's `c:transform/2` callback\n  #   is called for each macro component used while LiveView compiles a HEEx component:\n  #\n  #   ```heex\n  #   <div id=\"hey\" phx-hook=\".foo\">\n  #     <!-- content -->\n  #   </div>\n  #\n  #   <script :type={ColocatedHook} name=\".foo\">\n  #     export default {\n  #       mounted() {\n  #         this.el.firstElementChild.textContent = \"Hello from JS!\"\n  #       }\n  #     }\n  #   </script>\n  #   ```\n  #\n  #   In this example, the `ColocatedHook`'s `c:transform/2` callback will be invoked\n  #   with the AST of the `<script>` tag:\n  #\n  #   ```elixir\n  #   {\"script\",\n  #     [{\"name\", \".foo\"}],\n  #     [\n  #       \"\\\\n  export default {\\\\n    mounted() {\\\\n      this.el.firstElementChild.textContent = \\\\\"Hello from JS!\\\\\"\\\\n    }\\\\n  }\\\\n\"\n  #     ]}\n  #   ```\n  #\n  #   This module provides some utilities to work with the AST, which uses\n  #   standard Elixir data structures:\n  #\n  #   1. A HTML tag is represented as `{tag, attributes, children, meta}`\n  #   2. Text is represented as a plain binary\n  #   3. Attributes are represented as a list of `{key, value}` tuples where\n  #      the value is an Elixir AST (which can be a plain binary for simple attributes)\n  #\n  #   > #### Limitations {: .warning}\n  #   > The AST is not whitespace preserving. When using macro components,\n  #   > the original whitespace between attributes is lost.\n  #   >\n  #   > Also, macro components can currently only contain simple HTML. Any interpolation\n  #   > like `<%= @foo %>` or components inside are not supported.\n  #\n  #   ## Example: a compile-time markdown renderer\n  #\n  #   Let's say we want to create a macro component that renders markdown as HTML at\n  #   compile time. First, we need some library that actually converts the markdown to\n  #   HTML. For this example, we use [`mdex`](https://hex.pm/packages/mdex).\n  #\n  #   We start by defining the module for the macro component:\n  #\n  #   ```elixir\n  #   defmodule MyAppWeb.MarkdownComponent do\n  #     @behaviour Phoenix.Component.MacroComponent\n  #\n  #     @impl true\n  #     def transform({\"pre\", attrs, children, _tag_meta}, _meta) do\n  #       markdown = Phoenix.Component.MacroComponent.ast_to_string(children)\n  #       html_doc = MDEx.to_html!(markdown)\n  #\n  #       {:ok, {\"div\", attrs, [html_doc], %{}}}\n  #     end\n  #   end\n  #   ```\n  #\n  #   That's it. Since the div could contain nested elements, for example when using\n  #   an HTML code block, we need to convert the children to a string first, using the\n  #   `Phoenix.Component.MacroComponent.ast_to_string/1` function.\n  #\n  #   Then, we can simply replace the element's contents with the returned HTML string from\n  #   MDEx.\n  #\n  #   We can now use the macro component inside our HEEx templates:\n  #\n  #       defmodule MyAppWeb.ExampleLiveView do\n  #         use MyAppWeb, :live_view\n  #\n  #         def render(assigns) do\n  #           ~H\\\"\\\"\\\"\n  #           <pre :type={MyAppWeb.MarkdownComponent} class=\"prose mt-8\">\n  #           ## Hello World\n  #\n  #           This is some markdown!\n  #\n  #           ```elixir\n  #           defmodule Hello do\n  #             def world do\n  #               IO.puts \"Hello, world!\"\n  #             end\n  #           end\n  #           ```\n  #           </pre>\n  #           \\\"\\\"\\\"\n  #         end\n  #       end\n  #\n  #   Note: this example uses the `prose` class from TailwindCSS for styling.\n  #\n  #   One trick to prevent issues with extra whitespace is that we use a `<pre>` tag in the LiveView\n  #   template, which prevents the `Phoenix.LiveView.HTMLFormatter` from indenting the contents, which\n  #   would mess with the markdown parsing. When rendering, we replace it with a `<div>` tag in the\n  #   macro component.\n  #\n  #   Another example for a macro component that transforms its content is available in\n  #   LiveView's end to end tests: a macro component that performs\n  #   [syntax highlighting at compile time](https://github.com/phoenixframework/phoenix_live_view/blob/38851d943f3280c5982d75679291dccb8c442534/test/e2e/support/colocated_live.ex#L4-L35)\n  #   using the [Makeup](https://hexdocs.pm/makeup/Makeup.html) library.\n  #\n  #   ## Directives\n  #\n  #   Macro components may return directives from `transform/2` which can be used to influence\n  #   other elements in the template outside of the macro component at compile-time. For example:\n  #\n  #   ```elixir\n  #   defmodule MyAppWeb.TagRootSampleComponent do\n  #     @behaviour Phoenix.Component.MacroComponent\n  #\n  #     @impl true\n  #     def transform(_ast, _meta) do\n  #       {:ok, \"\", %{}, [root_tag_attribute: {\"phx-sample-one\", \"test\"}, root_tag_attribute: {\"phx-sample-two\", true}]}\n  #     end\n  #   end\n  #   ```\n  #\n  #   The following directives are currently supported:\n  #\n  #   * `:root_tag_attribute` - A `{name, value}` tuple to apply as an attribute to all root tags during template compilation.\n  #     Requires that a global `:root_tag_attribute` is configured for the application. The attribute name must be a string and the attribute value must be a string or `true`.\n  #     May be provided multiple times to apply multiple attributes.\n  #\n\n  @type tag :: binary()\n  @type attribute :: {binary(), Macro.t()}\n  @type attributes :: [attribute()]\n  @type children :: [heex_ast()]\n  @type tag_meta :: %{closing: :self | :void}\n  @type heex_ast :: {tag(), attributes(), children(), tag_meta()} | binary()\n  @type transform_meta :: %{env: Macro.Env.t()}\n  @type directive :: {:root_tag_attribute, {name :: String.t(), value :: String.t() | true}}\n  @type directives :: [directive]\n\n  @callback transform(heex_ast :: heex_ast(), meta :: transform_meta()) ::\n              {:ok, heex_ast()}\n              | {:ok, heex_ast(), data :: term()}\n              | {:ok, heex_ast(), data :: term(), directives :: directives()}\n\n  @doc \"\"\"\n  Returns the stored data from macro components that returned `{:ok, ast, data}`.\n\n  As one macro component can be used multiple times in one module, the result is a list of all data values.\n\n  If the component module does not have any macro components defined, an empty list is returned.\n  \"\"\"\n  @spec get_data(module(), module()) :: [term()] | nil\n  def get_data(component_module, macro_component) do\n    if Code.ensure_loaded?(component_module) and\n         function_exported?(component_module, :__phoenix_macro_components__, 0) do\n      component_module.__phoenix_macro_components__()\n      |> Map.get(macro_component, [])\n    else\n      []\n    end\n  end\n\n  @doc false\n  def build_ast(node, env) when is_tuple(node) do\n    case node do\n      {:self_close, :tag, name, attrs, meta} ->\n        closing_meta = Map.take(meta, [:closing])\n        {:ok, {name, attrs_to_ast(attrs, env), [], closing_meta}}\n\n      {:block, :tag, name, attrs, children, _meta, _close_meta} ->\n        children_ast = build_ast(children, env)\n        {:ok, {name, attrs_to_ast(attrs, env), children_ast, %{}}}\n    end\n  catch\n    {:ast_error, message, error_meta} ->\n      {:error, message, error_meta}\n  end\n\n  def build_ast(children, env) when is_list(children) do\n    Enum.map(children, fn\n      {:text, text, _meta} ->\n        text\n\n      {:self_close, :tag, name, attrs, meta} ->\n        closing_meta = Map.take(meta, [:closing])\n        {name, attrs_to_ast(attrs, env), [], closing_meta}\n\n      {:block, :tag, name, attrs, nested_children, _meta, _close_meta} ->\n        {name, attrs_to_ast(attrs, env), build_ast(nested_children, env), %{}}\n\n      {:self_close, type, _name, _attrs, meta}\n      when type in [:local_component, :remote_component] ->\n        throw({:ast_error, \"function components cannot be nested inside a macro component\", meta})\n\n      {:block, type, _name, _attrs, _children, meta, _close_meta}\n      when type in [:local_component, :remote_component] ->\n        throw({:ast_error, \"function components cannot be nested inside a macro component\", meta})\n\n      {:self_close, :slot, _name, _attrs, meta} ->\n        throw({:ast_error, \"slots cannot be nested inside a macro component\", meta})\n\n      {:block, :slot, _name, _attrs, _children, meta, _close_meta} ->\n        throw({:ast_error, \"slots cannot be nested inside a macro component\", meta})\n\n      {:body_expr, _expr, meta} ->\n        throw({:ast_error, \"interpolation is not currently supported in macro components\", meta})\n\n      {:eex, _expr, meta} ->\n        throw({:ast_error, \"EEx is not currently supported in macro components\", meta})\n\n      {:eex_block, _expr, _blocks, meta} ->\n        throw({:ast_error, \"EEx is not currently supported in macro components\", meta})\n    end)\n  end\n\n  defp attrs_to_ast(attrs, env) do\n    Enum.map(attrs, fn\n      # for now, we don't support root expressions (<div {@foo}>)\n      {:root, value, attr_meta} ->\n        format_attr = fn\n          {:string, binary, _meta} -> binary\n          {:expr, code, _meta} -> code\n          nil -> \"nil\"\n        end\n\n        throw(\n          {:ast_error,\n           \"dynamic attributes are not supported in macro components, got: #{format_attr.(value)}\",\n           attr_meta}\n        )\n\n      {name, {:string, binary, _meta}, _attr_meta} ->\n        {name, binary}\n\n      {name, {:expr, code, expr_meta}, _attr_meta} ->\n        ast =\n          Code.string_to_quoted!(code,\n            line: expr_meta.line,\n            column: expr_meta.column,\n            file: env.file\n          )\n\n        {name, ast}\n\n      {name, nil, _attr_meta} ->\n        {name, nil}\n    end)\n  end\n\n  @doc false\n  # Convert macro AST back to tree nodes (parser format)\n  # We keep reuse the original line + column metadata from the original tag\n  def ast_to_tree({tag, attrs, [], %{closing: _closing} = meta}, original_meta) do\n    tree_attrs = attrs_to_tree(attrs, original_meta)\n    {:self_close, :tag, tag, tree_attrs, Map.merge(original_meta, meta)}\n  end\n\n  def ast_to_tree({tag, attrs, children, _meta}, original_meta) do\n    tree_attrs = attrs_to_tree(attrs, original_meta)\n    tree_children = Enum.map(children, &ast_to_tree(&1, original_meta))\n    {:block, :tag, tag, tree_attrs, tree_children, original_meta, %{}}\n  end\n\n  def ast_to_tree(text, _original_meta) when is_binary(text) do\n    {:text, text, %{}}\n  end\n\n  defp attrs_to_tree(attrs, meta) do\n    Enum.map(attrs, fn\n      {name, nil} ->\n        {name, nil, meta}\n\n      {name, value} when is_binary(value) ->\n        delimiter = attr_quotes(name, value)\n        {name, {:string, value, Map.put(meta, :delimiter, delimiter)}, meta}\n\n      {name, ast} ->\n        # Convert quoted AST back to string for the tree node format\n        code = Macro.to_string(ast)\n        {name, {:expr, code, meta}, meta}\n    end)\n  end\n\n  @doc \"\"\"\n  Turns an AST into a string.\n\n  ## Options\n\n    * `attributes_encoder` - a custom function to encode attributes to iodata.\n       Defaults to an HTML-safe encoder.\n\n  \"\"\"\n  @spec ast_to_string(heex_ast(), keyword()) :: binary()\n  def ast_to_string(ast, opts \\\\ []) do\n    opts = Keyword.put_new(opts, :attributes_encoder, &ast_attributes_to_iodata/1)\n\n    ast\n    |> ast_to_iodata(opts)\n    |> IO.iodata_to_binary()\n  end\n\n  defp ast_to_iodata(list, opts) when is_list(list) do\n    Enum.map(list, &ast_to_iodata(&1, opts))\n  end\n\n  # self closing / void tags cannot have children\n  defp ast_to_iodata({name, attrs, [], %{closing: closing}}, opts) do\n    suffix =\n      case closing do\n        :void -> \">\"\n        :self -> \"/>\"\n      end\n\n    [\n      \"<\",\n      name,\n      opts[:attributes_encoder].(attrs),\n      suffix\n    ]\n  end\n\n  defp ast_to_iodata({name, attrs, children, _meta}, opts) do\n    [\n      \"<\",\n      name,\n      opts[:attributes_encoder].(attrs),\n      \">\",\n      Enum.map(children, &ast_to_iodata(&1, opts)),\n      \"</\",\n      name,\n      \">\"\n    ]\n  end\n\n  defp ast_to_iodata(binary, _opts) when is_binary(binary) do\n    binary\n  end\n\n  defp ast_attributes_to_iodata(attrs) do\n    Enum.map(attrs, fn\n      {key, value} when is_binary(value) ->\n        encode_binary_attribute(key, value)\n\n      {key, nil} ->\n        ~s( #{key})\n\n      {key, value} ->\n        raise ArgumentError,\n              \"cannot convert AST with non-string attribute \\\"#{key}\\\" to string. Got: #{Macro.to_string(value)}\"\n    end)\n  end\n\n  defp encode_binary_attribute(key, value) when is_binary(key) and is_binary(value) do\n    case attr_quotes(key, value) do\n      ?\" ->\n        ~s( #{key}=\"#{value}\")\n\n      ?' ->\n        ~s( #{key}='#{value}')\n    end\n  end\n\n  defp attr_quotes(key, value) do\n    case {:binary.match(value, ~s[\"]), :binary.match(value, \"'\")} do\n      {:nomatch, _} ->\n        ?\"\n\n      {_, :nomatch} ->\n        ?'\n\n      _ ->\n        raise ArgumentError, \"\"\"\n        invalid attribute value for \\\"#{key}\\\".\n        Attribute values must not contain single and double quotes at the same time.\n\n        You need to escape your attribute before using it in the MacroComponent AST. You can use `Phoenix.HTML.attributes_escape/1` to do so.\n        \"\"\"\n    end\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_component.ex",
    "content": "defmodule Phoenix.Component do\n  @moduledoc ~S'''\n  Define reusable function components with HEEx templates.\n\n  A function component is any function that receives an assigns\n  map as an argument and returns a rendered struct built with\n  [the `~H` sigil](`sigil_H/2`):\n\n      defmodule MyComponent do\n        # In Phoenix apps, the line is typically: use MyAppWeb, :html\n        use Phoenix.Component\n\n        def greet(assigns) do\n          ~H\"\"\"\n          <p>Hello, {@name}!</p>\n          \"\"\"\n        end\n      end\n\n  This function uses the `~H` sigil to return a rendered template.\n  `~H` stands for HEEx (HTML + EEx). HEEx is a template language for\n  writing HTML mixed with Elixir interpolation. We can write Elixir\n  code inside `{...}` for HTML-aware interpolation inside tag attributes\n  and the body. We can also interpolate arbitrary HEEx blocks using `<%= ... %>`\n  We use `@name` to access the key `name` defined inside `assigns`.\n\n  When invoked within a `~H` sigil or HEEx template file:\n\n  ```heex\n  <MyComponent.greet name=\"Jane\" />\n  ```\n\n  The following HTML is rendered:\n\n  ```html\n  <p>Hello, Jane!</p>\n  ```\n\n  If the function component is defined locally, or its module is imported,\n  then the caller can invoke the function directly without specifying the module:\n\n  ```heex\n  <.greet name=\"Jane\" />\n  ```\n\n  For dynamic values, you can interpolate Elixir expressions into a function component:\n\n  ```heex\n  <.greet name={@user.name} />\n  ```\n\n  Function components can also accept blocks of HEEx content (more on this later):\n\n  ```heex\n  <.card>\n    <p>This is the body of my card!</p>\n  </.card>\n  ```\n\n  In this module we will learn how to build rich and composable components to\n  use in our applications.\n\n  ## Attributes\n\n  `Phoenix.Component` provides the `attr/3` macro to declare what attributes the proceeding function\n  component expects to receive when invoked:\n\n      attr :name, :string, required: true\n\n      def greet(assigns) do\n        ~H\"\"\"\n        <p>Hello, {@name}!</p>\n        \"\"\"\n      end\n\n  By calling `attr/3`, it is now clear that `greet/1` requires a string attribute called `name`\n  present in its assigns map to properly render. Failing to do so will result in a compilation\n  warning:\n\n  ```heex\n  <MyComponent.greet />\n    <!-- warning: missing required attribute \"name\" for component MyAppWeb.MyComponent.greet/1\n             lib/app_web/my_component.ex:15 -->\n  ```\n\n  Attributes can provide default values that are automatically merged into the assigns map:\n\n      attr :name, :string, default: \"Bob\"\n\n  Now you can invoke the function component without providing a value for `name`:\n\n  ```heex\n  <.greet />\n  ```\n\n  Rendering the following HTML:\n\n  ```html\n  <p>Hello, Bob!</p>\n  ```\n\n  Accessing an attribute which is required and does not have a default value will fail.\n  You must explicitly declare `default: nil` or assign a value programmatically with the\n  `assign_new/3` function.\n\n  Multiple attributes can be declared for the same function component:\n\n      attr :name, :string, required: true\n      attr :age, :integer, required: true\n\n      def celebrate(assigns) do\n        ~H\"\"\"\n        <p>\n          Happy birthday {@name}!\n          You are {@age} years old.\n        </p>\n        \"\"\"\n      end\n\n  Allowing the caller to pass multiple values:\n\n  ```heex\n  <.celebrate name={\"Genevieve\"} age={34} />\n  ```\n\n  Rendering the following HTML:\n\n  ```html\n  <p>\n    Happy birthday Genevieve!\n    You are 34 years old.\n  </p>\n  ```\n\n  Multiple function components can be defined in the same module, with different attributes. In the\n  following example, `<Components.greet/>` requires a `name`, but *does not* require a `title`, and\n  `<Components.heading>` requires a `title`, but *does not* require a `name`.\n\n      defmodule Components do\n        # In Phoenix apps, the line is typically: use MyAppWeb, :html\n        use Phoenix.Component\n\n        attr :title, :string, required: true\n\n        def heading(assigns) do\n          ~H\"\"\"\n          <h1>{@title}</h1>\n          \"\"\"\n        end\n\n        attr :name, :string, required: true\n\n        def greet(assigns) do\n          ~H\"\"\"\n          <p>Hello {@name}</p>\n          \"\"\"\n        end\n      end\n\n  With the `attr/3` macro you have the core ingredients to create reusable function components.\n  But what if you need your function components to support dynamic attributes, such as common HTML\n  attributes to mix into a component's container?\n\n  ## Global attributes\n\n  Global attributes are a set of attributes that a function component can accept when it\n  declares an attribute of type `:global`. By default, the set of attributes accepted are those\n  attributes common to all standard HTML tags.\n  See [Global attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes)\n  for a complete list of attributes.\n\n  Once a global attribute is declared, any number of attributes in the set can be passed by\n  the caller without having to modify the function component itself.\n\n  Below is an example of a function component that accepts a dynamic number of global attributes:\n\n      attr :message, :string, required: true\n      attr :rest, :global\n\n      def notification(assigns) do\n        ~H\"\"\"\n        <span {@rest}>{@message}</span>\n        \"\"\"\n      end\n\n  The caller can pass multiple global attributes (such as `phx-*` bindings or the `class` attribute):\n\n  ```heex\n  <.notification message=\"You've got mail!\" class=\"bg-green-200\" phx-click=\"close\" />\n  ```\n\n  Rendering the following HTML:\n\n  ```html\n  <span class=\"bg-green-200\" phx-click=\"close\">You've got mail!</span>\n  ```\n\n  Note that the function component did not have to explicitly declare a `class` or `phx-click`\n  attribute in order to render.\n\n  Global attributes can define defaults which are merged with attributes provided by the caller.\n  For example, you may declare a default `class` if the caller does not provide one:\n\n      attr :rest, :global, default: %{class: \"bg-blue-200\"}\n\n  Now you can call the function component without a `class` attribute:\n\n  ```heex\n  <.notification message=\"You've got mail!\" phx-click=\"close\" />\n  ```\n\n  Rendering the following HTML:\n\n  ```html\n  <span class=\"bg-blue-200\" phx-click=\"close\">You've got mail!</span>\n  ```\n\n  Note that the global attribute cannot be provided directly and doing so will emit\n  a warning. In other words, this is invalid:\n\n  ```heex\n  <.notification message=\"You've got mail!\" rest={%{\"phx-click\" => \"close\"}} />\n  ```\n\n  ### Included globals\n\n  You may also specify which attributes are included in addition to the known globals\n  with the `:include` option. For example to support the `form` attribute on a button\n  component:\n\n  ```elixir\n  # <.button form=\"my-form\"/>\n  attr :rest, :global, include: ~w(form)\n  slot :inner_block\n  def button(assigns) do\n    ~H\"\"\"\n    <button {@rest}>{render_slot(@inner_block)}</button>\n    \"\"\"\n  end\n  ```\n\n  The `:include` option is useful to apply global additions on a case-by-case basis,\n  but sometimes you want to extend existing components with new global attributes,\n  such as Alpine.js' `x-` prefixes, which we'll outline next.\n\n  ### Custom global attribute prefixes\n\n  You can extend the set of global attributes by providing a list of attribute prefixes to\n  `use Phoenix.Component`. Like the default attributes common to all HTML elements,\n  any number of attributes that start with a global prefix will be accepted by function\n  components invoked by the current module. By default, the following prefixes are supported:\n  `phx-`, `aria-`, and `data-`. For example, to support the `x-` prefix used by\n  [Alpine.js](https://alpinejs.dev/), you can pass the `:global_prefixes` option to\n  `use Phoenix.Component`:\n\n      use Phoenix.Component, global_prefixes: ~w(x-)\n\n  In your Phoenix application, this is typically done in your\n  `lib/my_app_web.ex` file, inside the `def html` definition:\n\n      def html do\n        quote do\n          use Phoenix.Component, global_prefixes: ~w(x-)\n          # ...\n        end\n      end\n\n  Now all function components invoked by this module will accept any number of attributes\n  prefixed with `x-`, in addition to the default global prefixes.\n\n  You can learn more about attributes by reading the documentation for `attr/3`.\n\n  ## Slots\n\n  In addition to attributes, function components can accept blocks of HEEx content, referred to\n  as slots. Slots enable further customization of the rendered HTML, as the caller can pass the\n  function component HEEx content they want the component to render. `Phoenix.Component` provides\n  the `slot/3` macro used to declare slots for function components:\n\n      slot :inner_block, required: true\n\n      def button(assigns) do\n        ~H\"\"\"\n        <button>\n          {render_slot(@inner_block)}\n        </button>\n        \"\"\"\n      end\n\n  The expression `render_slot(@inner_block)` renders the HEEx content. You can invoke this function\n  component like so:\n\n  ```heex\n  <.button>\n    This renders <strong>inside</strong> the button!\n  </.button>\n  ```\n\n  Which renders the following HTML:\n\n  ```html\n  <button>\n    This renders <strong>inside</strong> the button!\n  </button>\n  ```\n\n  Like the `attr/3` macro, using the `slot/3` macro will provide compile-time validations.\n  For example, invoking `button/1` without a slot of HEEx content will result in a compilation\n  warning being emitted:\n\n  ```heex\n  <.button />\n    <!-- warning: missing required slot \"inner_block\" for component MyAppWeb.MyComponent.button/1\n             lib/app_web/my_component.ex:15 -->\n  ```\n\n  ### The default slot\n\n  The example above uses the default slot, accessible as an assign named `@inner_block`, to render\n  HEEx content via the `render_slot/1` function.\n\n  If the values rendered in the slot need to be dynamic, you can pass a second value back to the\n  HEEx content by calling `render_slot/2`:\n\n      slot :inner_block, required: true\n\n      attr :entries, :list, default: []\n\n      def unordered_list(assigns) do\n        ~H\"\"\"\n        <ul>\n          <li :for={entry <- @entries}>{render_slot(@inner_block, entry)}</li>\n        </ul>\n        \"\"\"\n      end\n\n  When invoking the function component, you can use the special attribute `:let` to take the value\n  that the function component passes back and bind it to a variable:\n\n  ```heex\n  <.unordered_list :let={fruit} entries={~w(apples bananas cherries)}>\n    I like <b>{fruit}</b>!\n  </.unordered_list>\n  ```\n\n  Rendering the following HTML:\n\n  ```html\n  <ul>\n    <li>I like <b>apples</b>!</li>\n    <li>I like <b>bananas</b>!</li>\n    <li>I like <b>cherries</b>!</li>\n  </ul>\n  ```\n\n  Now the separation of concerns is maintained: the caller can specify multiple values in a list\n  attribute without having to specify the HEEx content that surrounds and separates them.\n\n  ### Named slots\n\n  In addition to the default slot, function components can accept multiple, named slots of HEEx\n  content. For example, imagine you want to create a modal that has a header, body, and footer:\n\n      slot :header\n      slot :inner_block, required: true\n      slot :footer, required: true\n\n      def modal(assigns) do\n        ~H\"\"\"\n        <div class=\"modal\">\n          <div class=\"modal-header\">\n            {render_slot(@header) || \"Modal\"}\n          </div>\n          <div class=\"modal-body\">\n            {render_slot(@inner_block)}\n          </div>\n          <div class=\"modal-footer\">\n            {render_slot(@footer)}\n          </div>\n        </div>\n        \"\"\"\n      end\n\n  You can invoke this function component using the named slot HEEx syntax:\n\n  ```heex\n  <.modal>\n    This is the body, everything not in a named slot is rendered in the default slot.\n    <:footer>\n      This is the bottom of the modal.\n    </:footer>\n  </.modal>\n  ```\n\n  Rendering the following HTML:\n\n  ```html\n  <div class=\"modal\">\n    <div class=\"modal-header\">\n      Modal.\n    </div>\n    <div class=\"modal-body\">\n      This is the body, everything not in a named slot is rendered in the default slot.\n    </div>\n    <div class=\"modal-footer\">\n      This is the bottom of the modal.\n    </div>\n  </div>\n  ```\n\n  As shown in the example above, `render_slot/1` returns `nil` when an optional slot\n  is declared and none is given. This can be used to attach default behaviour.\n\n  ### Slot attributes\n\n  Unlike the default slot, it is possible to pass a named slot multiple pieces of HEEx content.\n  Named slots can also accept attributes, defined by passing a block to the `slot/3` macro.\n  If multiple pieces of content are passed, `render_slot/2` will merge and render all the values.\n\n  Below is a table component illustrating multiple named slots with attributes:\n\n      slot :column, doc: \"Columns with column labels\" do\n        attr :label, :string, required: true, doc: \"Column label\"\n      end\n\n      attr :rows, :list, default: []\n\n      def table(assigns) do\n        ~H\"\"\"\n        <table>\n          <tr>\n            <th :for={col <- @column}>{col.label}</th>\n          </tr>\n          <tr :for={row <- @rows}>\n            <td :for={col <- @column}>{render_slot(col, row)}</td>\n          </tr>\n        </table>\n        \"\"\"\n      end\n\n  You can invoke this function component like so:\n\n  ```heex\n  <.table rows={[%{name: \"Jane\", age: \"34\"}, %{name: \"Bob\", age: \"51\"}]}>\n    <:column :let={user} label=\"Name\">\n      {user.name}\n    </:column>\n    <:column :let={user} label=\"Age\">\n      {user.age}\n    </:column>\n  </.table>\n  ```\n\n  Rendering the following HTML:\n\n  ```html\n  <table>\n    <tr>\n      <th>Name</th>\n      <th>Age</th>\n    </tr>\n    <tr>\n      <td>Jane</td>\n      <td>34</td>\n    </tr>\n    <tr>\n      <td>Bob</td>\n      <td>51</td>\n    </tr>\n  </table>\n  ```\n\n  You can learn more about slots and the `slot/3` macro [in its documentation](`slot/3`).\n\n  ## Embedding external template files\n\n  The `embed_templates/1` macro can be used to embed `.html.heex` files\n  as function components. The directory path is based on the current\n  module (`__DIR__`), and a wildcard pattern may be used to select all\n  files within a directory tree. For example, imagine a directory listing:\n\n  ```plain\n  ├── components.ex\n  ├── cards\n  │   ├── pricing_card.html.heex\n  │   └── features_card.html.heex\n  ```\n\n  Then you can embed the page templates in your `components.ex` module\n  and call them like any other function component:\n\n      defmodule MyAppWeb.Components do\n        use Phoenix.Component\n\n        embed_templates \"cards/*\"\n\n        def landing_hero(assigns) do\n          ~H\"\"\"\n          <.pricing_card />\n          <.features_card />\n          \"\"\"\n        end\n      end\n\n  See `embed_templates/1` for more information, including declarative\n  assigns support for embedded templates.\n\n  ## Debug information\n\n  HEEx templates support adding annotations and locations to the rendered\n  page, which are special HTML comments and attributes that help you identify\n  where markup in your HTML document is rendered within your function component\n  tree.\n\n  For example, imagine the following HEEx template:\n\n  ```heex\n  <.header>\n    <.button>Click</.button>\n  </.header>\n  ```\n\n  By turning on `debug_heex_annotations`, the HTML document would receive the\n  following comments when debug annotations are enabled:\n\n  ```html\n  <!-- @caller lib/app_web/home_live.ex:20 -->\n  <!-- <AppWeb.CoreComponents.header> lib/app_web/core_components.ex:123 -->\n  <header class=\"p-5\">\n    <!-- @caller lib/app_web/home_live.ex:48 -->\n    <!-- <AppWeb.CoreComponents.button> lib/app_web/core_components.ex:456 -->\n    <button class=\"px-2 bg-indigo-500 text-white\">Click</button>\n    <!-- </AppWeb.CoreComponents.button> -->\n  </header>\n  <!-- </AppWeb.CoreComponents.header> -->\n  ```\n\n  Similarly, you can also turn on `:debug_attributes`, which adds a\n  `data-phx-loc` attribute with the line of where each HTML tag is defined\n  (as well as `data-phx-pid` to the LiveView container):\n\n  ```html\n  <header data-phx-loc=\"125\" class=\"p-5\">\n    <button data-phx-loc=\"458\" class=\"px-2 bg-indigo-500 text-white\">Click</button>\n  </header>\n  ```\n\n  These features work on any `~H` or `.html.heex` template. They can be enabled\n  globally with the following configuration in your `config/dev.exs` file:\n\n      config :phoenix_live_view,\n        debug_heex_annotations: true,\n        debug_attributes: true\n\n  Changing this configuration will require `mix clean` and a full recompile.\n\n  ## Dynamic Component Rendering\n\n  Sometimes you might need to decide at runtime which component to render.\n  Because function components are just regular functions, we can leverage\n  Elixir's `apply/3` function to dynamically call a module and/or function passed\n  in as an assign.\n\n  For example, using the following function component definition:\n\n  ```elixir\n  attr :module, :atom, required: true\n  attr :function, :atom, required: true\n  # any shared attributes\n  attr :shared, :string, required: true\n\n  # any shared slots\n  slot :named_slot, required: true\n  slot :inner_block, required: true\n\n  def dynamic_component(assigns) do\n    {mod, assigns} = Map.pop(assigns, :module)\n    {func, assigns} = Map.pop(assigns, :function)\n\n    apply(mod, func, [assigns])\n  end\n  ```\n\n  Then you can use the `dynamic_component` function like so:\n\n  ```heex\n  <.dynamic_component\n    module={MyAppWeb.MyModule}\n    function={:my_function}\n    shared=\"Yay Elixir!\"\n  >\n    <p>Howdy from the inner block!</p>\n    <:named_slot>\n      <p>Howdy from the named slot!</p>\n    </:named_slot>\n  </.dynamic_component>\n  ```\n\n  This will call the `MyAppWeb.MyModule.my_function/1` function passing in the remaining assigns.\n\n  ```elixir\n  defmodule MyAppWeb.MyModule do\n    attr :shared, :string, required: true\n\n    slot :named_slot, required: true\n    slot :inner_block, required: true\n\n    def my_function(assigns) do\n      ~H\"\"\"\n      <p>Dynamic component with shared assigns: {@shared}</p>\n      {render_slot(@inner_block)}\n      {render_slot(@named_slot)}\n      \"\"\"\n    end\n  end\n  ```\n\n  Resulting in the following HTML:\n\n  ```html\n  <p>Dynamic component with shared assigns: Yay Elixir!</p>\n  <p>Howdy from the inner block!</p>\n  <p>Howdy from the named slot!</p>\n  ```\n\n  Note that to get the most out of `Phoenix.Component`'s compile-time validations, it is beneficial to\n  define such a `dynamic_component` for a specific set of components sharing the same API, instead of\n  defining it for the general case.\n  In this example, we defined our `dynamic_component` to expect an assign called `shared`, as well as\n  two slots that all components we want to use with it must implement.\n  The called `my_function` component's attribute and slot definitions cannot be validated through the apply call.\n  '''\n\n  ## Functions\n\n  alias Phoenix.LiveView.{Static, Socket, AsyncResult}\n  @reserved_assigns Phoenix.Component.Declarative.__reserved__()\n  # Note we allow live_action as it may be passed down to a component, so it is not listed\n  @non_assignables [:uploads, :streams, :socket, :myself]\n\n  @doc ~S'''\n  The `~H` sigil for writing HEEx templates inside source files.\n\n  `HEEx` is a HTML-aware and component-friendly extension of Elixir Embedded\n  language (`EEx`) that provides:\n\n    * Built-in handling of HTML attributes\n\n    * An HTML-like notation for injecting function components\n\n    * Compile-time validation of the structure of the template\n\n    * The ability to minimize the amount of data sent over the wire\n\n    * Out-of-the-box code formatting via `mix format`\n\n  ## Example\n\n      ~H\"\"\"\n      <div title=\"My div\" class={@class}>\n        <p>Hello {@name}</p>\n        <MyApp.Weather.city name=\"Kraków\"/>\n      </div>\n      \"\"\"\n\n  ## Syntax\n\n  `HEEx` is built on top of Embedded Elixir (`EEx`). In this section, we are going to\n  cover the basic constructs in `HEEx` templates as well as its syntax extensions.\n\n  ### Interpolation\n\n  `HEEx` allows using `{...}` for HTML-aware interpolation, inside tag attributes\n  as well as the body:\n\n  ```heex\n  <p>Hello, {@name}</p>\n  ```\n\n  If you want to interpolate an attribute, you write:\n\n  ```heex\n  <div class={@class}>\n    ...\n  </div>\n  ```\n\n  You can put any Elixir expression between `{ ... }`. For example, if you want\n  to set classes, where some are static and others are dynamic, you can using\n  string interpolation:\n\n  ```heex\n  <div class={\"btn btn-#{@type}\"}>\n    ...\n  </div>\n  ```\n\n  The following attribute values have special meaning on HTML tags:\n\n  * `true` - if a value is `true`, the attribute is rendered with no value at all.\n    For example, `<input required={true}>` is the same as `<input required>`;\n\n  * `false` or `nil` - if a value is `false` or `nil`, the attribute is omitted.\n    Note the `class` and `style` attributes will be rendered as empty strings,\n    instead of ommitted, which has the same effect as not rendering them, but\n    allows for rendering optimizations.\n\n  * `list` (only for the `class` attribute) - each element of the list is processed\n    as a different class. `nil` and `false` elements are discarded. Nested lists\n    are supported and flattened.\n\n  For multiple dynamic attributes, you can use the same notation but without\n  assigning the expression to any specific attribute:\n\n  ```heex\n  <div {@dynamic_attrs}>\n    ...\n  </div>\n  ```\n\n  In this case, the expression inside `{...}` must be either a keyword list or\n  a map containing the key-value pairs representing the dynamic attributes.\n  If using a map, ensure your keys are atoms.\n\n  ### Interpolating blocks\n\n  The curly braces syntax is the default mechanism for interpolating code.\n  However, it cannot be used in all scenarios, in particular:\n\n    * Curly braces cannot be used inside `<script>` and `<style>` tags,\n      as that would make writing JS and CSS quite tedious. You can also\n      fully disable curly braces interpolation in a given tag and\n      its children by adding the `phx-no-curly-interpolation` attribute\n\n    * it does not support multiline block constructs, such as `if`,\n      `case`, and similar\n\n  For example, if you need to interpolate a string inside a script tag,\n  you could do:\n\n  ```heex\n  <script>\n    window.URL = \"<%= @my_url %>\"\n  </script>\n  ```\n\n  Similarly, for block constructs in Elixir, you can write:\n\n  ```heex\n  <%= if @show_greeting? do %>\n    <p>Hello, {@name}</p>\n  <% end %>\n  ```\n\n  However, for conditionals and for-comprehensions, there are built-in constructs\n  in HEEx too, which we will explore next.\n\n  > #### Curly braces in text within tag bodies {: .tip}\n  >\n  > If you have text in your tag bodies, which includes curly braces you can use\n  > `&lbrace;` or `<%= \"{\" %>` to prevent them from being considered the start of\n  > interpolation.\n\n  ### Special attributes\n\n  Apart from normal HTML attributes, HEEx also supports some special attributes\n  such as `:let` and `:for`.\n\n  #### :let\n\n  This is used by components and slots that want to yield a value back to the\n  caller. For an example, see how `form/1` works:\n\n  ```heex\n  <.form :let={f} for={@form} id=\"my-form\" phx-change=\"validate\" phx-submit=\"save\">\n    <.input field={f[:username]} type=\"text\" />\n    ...\n  </.form>\n  ```\n\n  Notice how the variable `f`, defined by `.form` is used by your `input` component.\n  The `Phoenix.Component` module has detailed documentation on how to use and\n  implement such functionality.\n\n  #### :if and :for\n\n  It is a syntax sugar for `<%= if .. do %>` and `<%= for .. do %>` that can be\n  used in regular HTML, function components, and slots.\n\n  For example in an HTML tag:\n\n  ```heex\n  <table id=\"admin-table\" :if={@admin?}>\n    <tr :for={user <- @users}>\n      <td>{user.name}</td>\n    </tr>\n  <table>\n  ```\n\n  The snippet above will only render the table if `@admin?` is true,\n  and generate a `tr` per user as you would expect from the collection.\n\n  `:for` can be used similarly in function components:\n\n  ```heex\n  <.error :for={msg <- @errors} message={msg}/>\n  ```\n\n  Which is equivalent to writing:\n\n  ```heex\n  <%= for msg <- @errors do %>\n    <.error message={msg} />\n  <% end %>\n  ```\n\n  And `:for` in slots behaves the same way:\n\n  ```heex\n  <.table id=\"my-table\" rows={@users}>\n    <:col :for={header <- @headers} :let={user}>\n      <td>{user[header]}</td>\n    </:col>\n  <.table>\n  ```\n\n  You can also combine `:for` and `:if` for tags, components, and slot to act as a filter:\n\n  ```heex\n  <.error :for={msg <- @errors} :if={msg != nil} message={msg} />\n  ```\n\n  Note that unlike Elixir's regular `for`, HEEx' `:for` does not support multiple\n  generators in one expression. In such cases, you must use `EEx`'s blocks.\n\n  > #### Change tracking `:for` on slots {: .warning}\n  >\n  > Compared to regular HTML tags and components, LiveView does not\n  > optimize comprehensions on slots.\n  > This means that if `@headers` changes in the example above, all\n  > headers are sent over the wire again.\n  >\n  > Furthermore, `:key` (see below) is also not supported on slots\n  > right now.\n\n  #### `:key`ed comprehensions\n\n  When using `:for`, you can optionally provide a `:key` expression to perform\n  better change tracking inside the comprehension:\n\n  ```heex\n  <ul>\n    <li :for={%{id: id, name: name} <- @items} :key={id}>\n      Count: <span>{@count}</span>,\n      item: {name}\n    </li>\n  </ul>\n  ```\n\n  By default, the index is used as a key, which means that appending an entry leads to\n  all items being considered changed. Therefore, we recommend to use a `:key` whenever possible.\n\n  Note that the `:key` has no effect when using [streams](`Phoenix.LiveView.stream/4`).\n\n  ### Function components\n\n  Function components are stateless components implemented as pure functions\n  with the help of the `Phoenix.Component` module. They can be either local\n  (same module) or remote (external module).\n\n  `HEEx` allows invoking these function components directly in the template\n  using an HTML-like notation. For example, a remote function:\n\n  ```heex\n  <MyApp.Weather.city name=\"Kraków\"/>\n  ```\n\n  A local function can be invoked with a leading dot:\n\n  ```heex\n  <.city name=\"Kraków\"/>\n  ```\n\n  where the component could be defined as follows:\n\n      defmodule MyApp.Weather do\n        use Phoenix.Component\n\n        def city(assigns) do\n          ~H\"\"\"\n          The chosen city is: {@name}.\n          \"\"\"\n        end\n\n        def country(assigns) do\n          ~H\"\"\"\n          The chosen country is: {@name}.\n          \"\"\"\n        end\n      end\n\n  It is typically best to group related functions into a single module, as\n  opposed to having many modules with a single `render/1` function. Function\n  components support other important features, such as slots. You can learn\n  more about components in `Phoenix.Component`.\n\n  ## Code formatting\n\n  You can automatically format HEEx templates (.heex) and `~H` sigils\n  using `Phoenix.LiveView.HTMLFormatter`. Please check that module\n  for more information.\n  '''\n  @doc type: :macro\n  defmacro sigil_H({:<<>>, meta, [expr]}, modifiers)\n           when modifiers == [] or modifiers == ~c\"noformat\" do\n    if not Macro.Env.has_var?(__CALLER__, {:assigns, nil}) do\n      raise \"~H requires a variable named \\\"assigns\\\" to exist and be set to a map\"\n    end\n\n    Phoenix.LiveView.TagEngine.compile(expr,\n      file: __CALLER__.file,\n      line: __CALLER__.line + 1,\n      caller: __CALLER__,\n      indentation: meta[:indentation] || 0,\n      tag_handler: Phoenix.LiveView.HTMLEngine\n    )\n  end\n\n  @doc ~S'''\n  Filters the assigns as a list of keywords for use in dynamic tag attributes.\n\n  One should prefer to use declarative assigns and `:global` attributes\n  over this function.\n\n  ## Examples\n\n  Imagine the following `my_link` component which allows a caller\n  to pass a `new_window` assign, along with any other attributes they\n  would like to add to the element, such as class, data attributes, etc:\n\n  ```heex\n  <.my_link to=\"/\" id={@id} new_window={true} class=\"my-class\">Home</.my_link>\n  ```\n\n  We could support the dynamic attributes with the following component:\n\n      def my_link(assigns) do\n        target = if assigns[:new_window], do: \"_blank\", else: false\n        extra = assigns_to_attributes(assigns, [:new_window, :to])\n\n        assigns =\n          assigns\n          |> assign(:target, target)\n          |> assign(:extra, extra)\n\n        ~H\"\"\"\n        <a href={@to} target={@target} {@extra}>\n          {render_slot(@inner_block)}\n        </a>\n        \"\"\"\n      end\n\n  The above would result in the following rendered HTML:\n\n  ```heex\n  <a href=\"/\" target=\"_blank\" id=\"1\" class=\"my-class\">Home</a>\n  ```\n\n  The second argument (optional) to `assigns_to_attributes` is a list of keys to\n  exclude. It typically includes reserved keys by the component itself, which either\n  do not belong in the markup, or are already handled explicitly by the component.\n  '''\n  def assigns_to_attributes(assigns, exclude \\\\ []) do\n    excluded_keys = @reserved_assigns ++ exclude\n    for {key, val} <- assigns, key not in excluded_keys, into: [], do: {key, val}\n  end\n\n  @doc \"\"\"\n  Renders a LiveView within a template.\n\n  This is useful in two situations:\n\n  * When rendering a child LiveView inside a LiveView.\n\n  * When rendering a LiveView inside a regular (non-live) controller/view.\n\n  Most other cases for shared functionality, including state management and user interactions, can be\n  [achieved with function components or LiveComponents](welcome.html#compartmentalize-state-markup-and-events-in-liveview)\n\n  ## Options\n\n  * `:session` - a map of binary keys with extra session data to be serialized and sent\n  to the client. All session data currently in the connection is automatically available\n  in LiveViews. You can use this option to provide extra data. Remember all session data is\n  serialized and sent to the client, so you should always keep the data in the session\n  to a minimum. For example, instead of storing a User struct, you should store the \"user_id\"\n  and load the User when the LiveView mounts.\n\n  * `:container` - an optional tuple for the HTML tag and DOM attributes to be used for the\n  LiveView container. For example: `{:li, style: \"color: blue;\"}`. By default it uses the module\n  definition container. See the \"Containers\" section below for more information.\n\n  * `:id` - both the DOM ID and the ID to uniquely identify a LiveView. An `:id` is\n  automatically generated when rendering root LiveViews but it is a required option when\n  rendering a child LiveView.\n\n  * `:sticky` - an optional flag to maintain the LiveView across live redirects, even if it is\n  nested within another LiveView. Note that this only works for LiveViews that are in the same\n  [live_session](`Phoenix.LiveView.Router.live_session/3`).\n  If you are rendering the sticky view within another LiveView, make sure that the sticky view\n  itself does not use the same layout. You can do so by returning `{:ok, socket, layout: false}`\n  from mount.\n\n  ## Examples\n\n  When rendering from a controller/view, you can call:\n\n  ```heex\n  {live_render(@conn, MyApp.ThermostatLive)}\n  ```\n\n  Or:\n\n  ```heex\n  {live_render(@conn, MyApp.ThermostatLive, session: %{\"home_id\" => @home.id})}\n  ```\n\n  Within another LiveView, you must pass the `:id` option:\n\n  ```heex\n  {live_render(@socket, MyApp.ThermostatLive, id: \"thermostat\")}\n  ```\n\n  ## Containers\n\n  When a LiveView is rendered, its contents are wrapped in a container. By default,\n  the container is a `div` tag with a handful of LiveView-specific attributes.\n\n  The container can be customized in different ways:\n\n  * You can change the default `container` on `use Phoenix.LiveView`:\n\n        use Phoenix.LiveView, container: {:tr, id: \"foo-bar\"}\n\n  * You can override the container tag and pass extra attributes when calling `live_render`\n  (as well as on your `live` call in your router):\n\n        live_render socket, MyLiveView, container: {:tr, class: \"highlight\"}\n\n  If you don't want the container to affect layout, you can use the CSS property\n  `display: contents` or a class that applies it, like Tailwind's `.contents`.\n\n  Beware if you set this to `:body`, as any content injected inside the body\n  (such as `Phoenix.LiveReload` features) will be discarded once the LiveView\n  connects\n\n  ## Testing\n\n  Note that `render_click/1` and other testing functions will send events to the root LiveView, and you will want to\n  `find_live_child/2` to interact with nested LiveViews in your live tests.\n  \"\"\"\n  def live_render(conn_or_socket, view, opts \\\\ [])\n\n  def live_render(%Plug.Conn{} = conn, view, opts) do\n    case Static.render(conn, view, opts) do\n      {:ok, content, _assigns} ->\n        content\n\n      {:stop, _} ->\n        raise RuntimeError, \"cannot redirect from a child LiveView\"\n    end\n  end\n\n  def live_render(%Socket{} = parent, view, opts) do\n    Static.nested_render(parent, view, opts)\n  end\n\n  @doc ~S'''\n  Renders a slot entry with the given optional `argument`.\n\n  ```heex\n  {render_slot(@inner_block, @form)}\n  ```\n\n  If the slot has no entries, nil is returned.\n\n  If multiple slot entries are defined for the same slot,`render_slot/2` will automatically render\n  all entries, merging their contents. In case you want to use the entries' attributes, you need\n  to iterate over the list to access each slot individually.\n\n  For example, imagine a table component:\n\n  ```heex\n  <.table rows={@users}>\n    <:col :let={user} label=\"Name\">\n      {user.name}\n    </:col>\n\n    <:col :let={user} label=\"Address\">\n      {user.address}\n    </:col>\n  </.table>\n  ```\n\n  At the top level, we pass the rows as an assign and we define a `:col` slot for each column we\n  want in the table. Each column also has a `label`, which we are going to use in the table header.\n\n  Inside the component, you can render the table with headers, rows, and columns:\n\n      def table(assigns) do\n        ~H\"\"\"\n        <table>\n          <tr>\n            <th :for={col <- @col}>{col.label}</th>\n          </tr>\n          <tr :for={row <- @rows}>\n            <td :for={col <- @col}>{render_slot(col, row)}</td>\n          </tr>\n        </table>\n        \"\"\"\n      end\n\n  '''\n  defmacro render_slot(slot, argument \\\\ nil) do\n    quote do\n      unquote(__MODULE__).__render_slot__(\n        var!(changed, Phoenix.LiveView.Engine),\n        unquote(slot),\n        unquote(argument)\n      )\n    end\n  end\n\n  @doc false\n  def __render_slot__(_, [], _), do: nil\n\n  def __render_slot__(changed, [entry], argument) do\n    call_inner_block!(entry, changed, argument)\n  end\n\n  def __render_slot__(changed, entries, argument) when is_list(entries) do\n    assigns = %{entries: entries, changed: changed, argument: argument}\n\n    ~H\"\"\"\n    <%= for entry <- @entries do %>{call_inner_block!(entry, @changed, @argument)}<% end %>\n    \"\"\"noformat\n  end\n\n  def __render_slot__(changed, entry, argument) when is_map(entry) do\n    case entry.inner_block do\n      %Phoenix.LiveView.Rendered{} = rendered -> rendered\n      fun -> fun.(changed, argument)\n    end\n  end\n\n  defp call_inner_block!(entry, changed, argument) do\n    if !entry.inner_block do\n      message = \"attempted to render slot <:#{entry.__slot__}> but the slot has no inner content\"\n      raise RuntimeError, message\n    end\n\n    case entry.inner_block do\n      %Phoenix.LiveView.Rendered{} = rendered -> rendered\n      fun -> fun.(changed, argument)\n    end\n  end\n\n  @doc \"\"\"\n  Returns the flash message from the LiveView flash assign.\n\n  ## Examples\n\n  ```heex\n  <p class=\"alert alert-info\">{live_flash(@flash, :info)}</p>\n  <p class=\"alert alert-danger\">{live_flash(@flash, :error)}</p>\n  ```\n  \"\"\"\n  @deprecated \"Use Phoenix.Flash.get/2 in Phoenix v1.7+\"\n  def live_flash(%_struct{} = other, _key) do\n    raise ArgumentError, \"live_flash/2 expects a @flash assign, got: #{inspect(other)}\"\n  end\n\n  def live_flash(%{} = flash, key), do: Map.get(flash, to_string(key))\n\n  @doc \"\"\"\n  Returns errors for the upload as a whole.\n\n  For errors that apply to a specific upload entry, use `upload_errors/2`.\n\n  The output is a list. The following error may be returned:\n\n  * `:too_many_files` - The number of selected files exceeds the `:max_entries` constraint\n\n  ## Examples\n\n      def upload_error_to_string(:too_many_files), do: \"You have selected too many files\"\n\n  ```heex\n  <div :for={err <- upload_errors(@uploads.avatar)} class=\"alert alert-danger\">\n    {upload_error_to_string(err)}\n  </div>\n  ```\n  \"\"\"\n  def upload_errors(%Phoenix.LiveView.UploadConfig{} = conf) do\n    for {ref, error} <- conf.errors, ref == conf.ref, do: error\n  end\n\n  @doc \"\"\"\n  Returns errors for the upload entry.\n\n  For errors that apply to the upload as a whole, use `upload_errors/1`.\n\n  The output is a list. The following errors may be returned:\n\n  * `:too_large` - The entry exceeds the `:max_file_size` constraint\n  * `:not_accepted` - The entry does not match the `:accept` MIME types\n  * `:external_client_failure` - When external upload fails\n  * `{:writer_failure, reason}` - When the custom writer fails with `reason`\n\n  ## Examples\n\n  ```elixir\n  defp upload_error_to_string(:too_large), do: \"The file is too large\"\n  defp upload_error_to_string(:not_accepted), do: \"You have selected an unacceptable file type\"\n  defp upload_error_to_string(:external_client_failure), do: \"Something went terribly wrong\"\n  ```\n\n  ```heex\n  <%= for entry <- @uploads.avatar.entries do %>\n    <div :for={err <- upload_errors(@uploads.avatar, entry)} class=\"alert alert-danger\">\n      {upload_error_to_string(err)}\n    </div>\n  <% end %>\n  ```\n  \"\"\"\n  def upload_errors(\n        %Phoenix.LiveView.UploadConfig{} = conf,\n        %Phoenix.LiveView.UploadEntry{} = entry\n      ) do\n    for {ref, error} <- conf.errors, ref == entry.ref, do: error\n  end\n\n  @doc ~S'''\n  Assigns the given `key` with value from `fun` into `socket_or_assigns` if one does not yet exist.\n\n  The first argument is either a LiveView `socket` or an `assigns` map from function components.\n\n  This function is useful for lazily assigning values and sharing assigns.\n  We will cover both use cases next.\n\n  ## Lazy assigns\n\n  Imagine you have a function component that accepts a color:\n\n  ```heex\n  <.my_component bg_color=\"red\" />\n  ```\n\n  The color is also optional, so you can skip it:\n\n  ```heex\n  <.my_component />\n  ```\n\n  In such cases, the implementation can use `assign_new` to lazily\n  assign a color if none is given. Let's make it so it picks a random one\n  when none is given:\n\n      def my_component(assigns) do\n        assigns = assign_new(assigns, :bg_color, fn -> Enum.random(~w(bg-red-200 bg-green-200 bg-blue-200)) end)\n\n        ~H\"\"\"\n        <div class={@bg_color}>\n          Example\n        </div>\n        \"\"\"\n      end\n\n  ## Sharing assigns\n\n  It is possible to share assigns between the Plug pipeline and LiveView on disconnected render\n  and between parent-child LiveViews when connected.\n\n  ### When disconnected\n\n  When a user first accesses an application using LiveView, the LiveView is first rendered in its\n  disconnected state, as part of a regular HTML response. By using `assign_new` in the mount\n  callback of your LiveView, you can instruct LiveView to re-use any assigns already set in `conn`\n  during disconnected state.\n\n  Imagine you have a Plug that does:\n\n      # A plug\n      def authenticate(conn, _opts) do\n        if user_id = get_session(conn, :user_id) do\n          assign(conn, :current_user, Accounts.get_user!(user_id))\n        else\n          send_resp(conn, :forbidden)\n        end\n      end\n\n  You can re-use the `:current_user` assign in your LiveView during the initial render:\n\n      def mount(_params, %{\"user_id\" => user_id}, socket) do\n        {:ok, assign_new(socket, :current_user, fn -> Accounts.get_user!(user_id) end)}\n      end\n\n  In such case `conn.assigns.current_user` will be used if present. If there is no such\n  `:current_user` assign or the LiveView was mounted as part of the live navigation, where no Plug\n  pipelines are invoked, then the anonymous function is invoked to execute the query instead.\n\n  ### When connected\n\n  LiveView is also able to share assigns via `assign_new` with children LiveViews,\n  as long as the child LiveView is also mounted when the parent LiveView is mounted\n  and the child LiveView is not rendered with `sticky: true`. Let's see an example.\n\n  If the parent LiveView defines a `:current_user` assign and the child LiveView also\n  uses `assign_new/3` to fetch the `:current_user` in its `mount/3` callback, as in\n  the previous subsection, the assign will be fetched from the parent LiveView, once\n  again avoiding additional database queries.\n\n  Note that `fun` also provides access to the previously assigned values:\n\n      assigns =\n        assigns\n        |> assign_new(:foo, fn -> \"foo\" end)\n        |> assign_new(:bar, fn %{foo: foo} -> foo <> \"bar\" end)\n\n  Assigns sharing is performed when possible but not guaranteed. Therefore, you must\n  ensure the result of the function given to `assign_new/3` is the same as if the value\n  was fetched from the parent. Otherwise consider passing values to the child LiveView\n  as part of its session.\n  '''\n  def assign_new(socket_or_assigns, key, fun)\n\n  def assign_new(%Socket{} = socket, key, fun) do\n    validate_assign_key!(key)\n    Phoenix.LiveView.Utils.assign_new(socket, key, fun)\n  end\n\n  def assign_new(%{__changed__: changed} = assigns, key, fun) when is_function(fun, 1) do\n    case assigns do\n      %{^key => _} -> assigns\n      %{} -> Phoenix.LiveView.Utils.force_assign(assigns, changed, key, fun.(assigns))\n    end\n  end\n\n  def assign_new(%{__changed__: changed} = assigns, key, fun) when is_function(fun, 0) do\n    case assigns do\n      %{^key => _} -> assigns\n      %{} -> Phoenix.LiveView.Utils.force_assign(assigns, changed, key, fun.())\n    end\n  end\n\n  def assign_new(assigns, _key, fun) when is_function(fun, 0) or is_function(fun, 1) do\n    raise_bad_socket_or_assign!(\"assign_new/3\", assigns)\n  end\n\n  defp raise_bad_socket_or_assign!(name, assigns) do\n    extra =\n      case assigns do\n        %_{} ->\n          \"\"\n\n        %{} ->\n          \"\"\"\n          You passed an assigns map that does not have the relevant change tracking \\\n          information. This typically means you are calling a function component by \\\n          hand instead of using the HEEx template syntax. If you are using HEEx, make \\\n          sure you are calling a component using:\n\n              <.component attribute={value} />\n\n          If you are outside of HEEx and you want to test a component, use \\\n          Phoenix.LiveViewTest.render_component/2:\n\n              Phoenix.LiveViewTest.render_component(&component/1, attribute: \"value\")\n\n          \"\"\"\n\n        _ ->\n          \"\"\n      end\n\n    raise ArgumentError,\n          \"#{name} expects a socket from Phoenix.LiveView/Phoenix.LiveComponent \" <>\n            \" or an assigns map from Phoenix.Component as first argument, got: \" <>\n            inspect(assigns) <> extra\n  end\n\n  @doc \"\"\"\n  Adds a `key`-`value` pair to `socket_or_assigns`.\n\n  The first argument is either a LiveView `socket` or an `assigns` map from function components.\n\n  ## Examples\n\n      iex> assign(socket, :name, \"Elixir\")\n\n  \"\"\"\n  def assign(socket_or_assigns, key, value)\n\n  def assign(%Socket{} = socket, key, value) do\n    validate_assign_key!(key)\n    Phoenix.LiveView.Utils.assign(socket, key, value)\n  end\n\n  def assign(%{__changed__: changed} = assigns, key, value) do\n    case assigns do\n      # force assign the key if the attribute was given with matching value\n      %{^key => ^value, __given__: given} when not is_map_key(given, key) ->\n        Phoenix.LiveView.Utils.force_assign(assigns, changed, key, value)\n\n      %{^key => ^value} ->\n        assigns\n\n      %{} ->\n        Phoenix.LiveView.Utils.force_assign(assigns, changed, key, value)\n    end\n  end\n\n  def assign(assigns, _key, _val) do\n    raise_bad_socket_or_assign!(\"assign/3\", assigns)\n  end\n\n  @doc \"\"\"\n  Adds key-value pairs to assigns.\n\n  The first argument is either a LiveView `socket` or an `assigns` map from function components.\n\n  When a keyword list or map is provided as the second argument, it will be merged into the existing assigns.\n  If a function is given, it takes the current assigns as an argument and its return\n  value will be merged into the current assigns.\n\n  ## Examples\n\n      iex> assign(socket, name: \"Elixir\", logo: \"💧\")\n      iex> assign(socket, %{name: \"Elixir\"})\n      iex> assign(socket, fn %{name: name, logo: logo} -> %{title: Enum.join([name, logo], \" | \")} end)\n\n  \"\"\"\n  def assign(socket_or_assigns, keyword_or_map)\n      when is_map(keyword_or_map) or is_list(keyword_or_map) do\n    Enum.reduce(keyword_or_map, socket_or_assigns, fn {key, value}, acc ->\n      assign(acc, key, value)\n    end)\n  end\n\n  def assign(socket_or_assigns, fun) when is_function(fun, 1) do\n    assigns =\n      case socket_or_assigns do\n        %Socket{assigns: assigns} -> assigns\n        assigns -> assigns\n      end\n\n    assign(socket_or_assigns, fun.(assigns))\n  end\n\n  defp validate_assign_key!(:flash) do\n    raise ArgumentError,\n          \":flash is a reserved assign by LiveView and it cannot be set directly. \" <>\n            \"Use the appropriate flash functions instead\"\n  end\n\n  defp validate_assign_key!(assign) when assign in @non_assignables do\n    raise ArgumentError,\n          \"#{inspect(assign)} is a reserved assign by LiveView and it cannot be set directly\"\n  end\n\n  defp validate_assign_key!(key) when is_atom(key), do: :ok\n\n  defp validate_assign_key!(key) do\n    raise ArgumentError, \"assigns in LiveView must be atoms, got: #{inspect(key)}\"\n  end\n\n  @doc \"\"\"\n  Updates an existing `key` with `fun` in the given `socket_or_assigns`.\n\n  The first argument is either a LiveView `socket` or an `assigns` map from function components.\n\n  The update function receives the current key's value and returns the updated value.\n  Raises if the key does not exist.\n\n  The update function may also be of arity 2, in which case it receives the current key's value\n  as the first argument and the current assigns as the second argument.\n  Raises if the key does not exist.\n\n  ## Examples\n\n      iex> update(socket, :count, fn count -> count + 1 end)\n      iex> update(socket, :count, &(&1 + 1))\n      iex> update(socket, :max_users_this_session, fn current_max, %{users: users} ->\n      ...>   max(current_max, length(users))\n      ...> end)\n  \"\"\"\n  def update(socket_or_assigns, key, fun)\n\n  def update(%Socket{assigns: assigns} = socket, key, fun) when is_function(fun, 2) do\n    update(socket, key, &fun.(&1, assigns))\n  end\n\n  def update(%Socket{assigns: assigns} = socket, key, fun) when is_function(fun, 1) do\n    case assigns do\n      %{^key => val} -> Phoenix.LiveView.Utils.assign(socket, key, fun.(val))\n      %{} -> raise KeyError, key: key, term: assigns\n    end\n  end\n\n  def update(assigns, key, fun) when is_function(fun, 2) do\n    update(assigns, key, &fun.(&1, assigns))\n  end\n\n  def update(assigns, key, fun) when is_function(fun, 1) do\n    case assigns do\n      %{^key => val} -> assign(assigns, key, fun.(val))\n      %{} -> raise KeyError, key: key, term: assigns\n    end\n  end\n\n  @doc \"\"\"\n  Checks if the given key changed in `socket_or_assigns`.\n\n  The first argument is either a LiveView `socket` or an `assigns` map from function components.\n\n  ## Examples\n\n      iex> changed?(socket, :count)\n\n  \"\"\"\n  def changed?(socket_or_assigns, key)\n\n  def changed?(%Socket{assigns: assigns}, key) do\n    Phoenix.LiveView.Utils.changed?(assigns, key)\n  end\n\n  def changed?(%{__changed__: _} = assigns, key) do\n    Phoenix.LiveView.Utils.changed?(assigns, key)\n  end\n\n  def changed?(assigns, _key) do\n    raise_bad_socket_or_assign!(\"changed?/2\", assigns)\n  end\n\n  @doc \"\"\"\n  Converts a given data structure to a `Phoenix.HTML.Form`.\n\n  This is commonly used to convert a map or an Ecto changeset\n  into a form to be given to the `form/1` component.\n\n  ## Creating a form from params\n\n  If you want to create a form based on `handle_event` parameters,\n  you could do:\n\n      def handle_event(\"submitted\", params, socket) do\n        {:noreply, assign(socket, form: to_form(params))}\n      end\n\n  When you pass a map to `to_form/1`, it assumes said map contains\n  the form parameters, which are expected to have string keys.\n\n  You can also specify a name to nest the parameters:\n\n      def handle_event(\"submitted\", %{\"user\" => user_params}, socket) do\n        {:noreply, assign(socket, form: to_form(user_params, as: :user))}\n      end\n\n  ## Creating a form from changesets\n\n  When using changesets, the underlying data, form parameters, and\n  errors are retrieved from it. The `:as` option is automatically\n  computed too. For example, if you have a user schema:\n\n      defmodule MyApp.Users.User do\n        use Ecto.Schema\n\n        schema \"...\" do\n          ...\n        end\n      end\n\n  And then you create a changeset that you pass to `to_form`:\n\n      %MyApp.Users.User{}\n      |> Ecto.Changeset.change()\n      |> to_form()\n\n  In this case, once the form is submitted, the parameters will\n  be available under `%{\"user\" => user_params}`.\n\n  ## Options\n\n    * `:as` - the `name` prefix to be used in form inputs\n    * `:id` - the `id` prefix to be used in form inputs\n    * `:errors` - keyword list of errors (used by maps exclusively)\n    * `:action` - The action that was taken against the form. This value can be\n      used to distinguish between different operations such as the user typing\n      into a form for validation, or submitting a form for a database insert.\n      For example: `to_form(changeset, action: :validate)`,\n      or `to_form(changeset, action: :save)`. The provided action is passed\n      to the underlying `Phoenix.HTML.FormData` implementation options.\n\n  The underlying data may accept additional options when\n  converted to forms. For example, a map accepts `:errors`\n  to list errors, but such option is not accepted by\n  changesets. `:errors` is a keyword of tuples in the shape\n  of `{error_message, options_list}`. Here is an example:\n\n      to_form(%{\"search\" => nil}, errors: [search: {\"Can't be blank\", []}])\n\n  If an existing `Phoenix.HTML.Form` struct is given, the\n  options above will override its existing values if given.\n  Then the remaining options are merged with the existing\n  form options.\n\n  Errors in a form are only displayed if the changeset's `action`\n  field is set (and it is not set to `:ignore`) and can be filtered\n  by whether the fields have been used on the client or not. Refer to\n  [a note on :errors for more information](#form/1-a-note-on-errors).\n  \"\"\"\n  def to_form(data_or_params, options \\\\ [])\n\n  def to_form(%Phoenix.HTML.Form{} = data, []) do\n    data\n  end\n\n  def to_form(%Phoenix.HTML.Form{} = data, options) do\n    data =\n      case Keyword.fetch(options, :as) do\n        {:ok, as} ->\n          name = if as == nil, do: as, else: to_string(as)\n          %{data | name: name, id: Keyword.get(options, :id) || name}\n\n        :error ->\n          case Keyword.fetch(options, :id) do\n            {:ok, id} -> %{data | id: id}\n            :error -> data\n          end\n      end\n\n    {options, data} =\n      Enum.reduce(options, {data.options, data}, fn\n        {:as, _as}, {options, data} -> {options, data}\n        {:action, action}, {options, data} -> {options, %{data | action: action}}\n        {:errors, errors}, {options, data} -> {options, %{data | errors: errors}}\n        {key, value}, {options, data} -> {[{key, value} | Keyword.delete(options, key)], data}\n      end)\n\n    %{data | options: options}\n  end\n\n  def to_form(data, options) do\n    if is_atom(data) do\n      IO.warn(\"\"\"\n      Passing an atom to \"for\" in the form component is deprecated.\n      Instead of:\n\n          <.form :let={f} for={#{inspect(data)}} ...>\n\n      You might do:\n\n          <.form :let={f} for={%{}} as={#{inspect(data)}} ...>\n\n      Or, if you prefer, use to_form to create a form in your LiveView:\n\n          assign(socket, form: to_form(%{}, as: #{inspect(data)}))\n\n      and then use it in your templates (no :let required):\n\n          <.form for={@form}>\n      \"\"\")\n    end\n\n    Phoenix.HTML.FormData.to_form(data, options)\n  end\n\n  @doc \"\"\"\n  Checks if the input field was used by the client.\n\n  Used inputs are only those inputs that have been focused, interacted with, or\n  submitted by the client. For LiveView, this is used to filter errors from the\n  `Phoenix.HTML.FormData` implementation to avoid showing \"field can't be blank\"\n  in scenarios where the client hasn't yet interacted with specific fields.\n\n  Used inputs are tracked internally by the client sending a sibling key\n  derived from each input name, which indicates the inputs that remain  unused\n  on the client. For example, a form with email and title fields where only the\n  title has been modified so far on the client, would send the following payload:\n\n      %{\n        \"title\" => \"new title\",\n        \"email\" => \"\",\n        \"_unused_email\" => \"\"\n      }\n\n  The `_unused_email` key indicates that the email field has not been used by the\n  client, which is used to filter errors from the UI.\n\n  Nested fields are also supported. For example, a form with a nested datetime field\n  is considered used if any of the nested parameters are used.\n\n      %{\n        \"bday\" => %{\n          \"year\" => \"\",\n          \"month\" => \"\",\n          \"day\" => \"\",\n          \"_unused_day\" => \"\"\n        }\n      }\n\n  The `_unused_day` key indicates that the day field has not been used by the client,\n  but the year and month fields have been used, meaning the birthday field as a whole\n  was used.\n\n  ## Examples\n\n  For example, imagine in your template you render a title and email input.\n  On initial load the end-user begins typing the title field. The client will send\n  the entire form payload to the server with the typed title and an empty email.\n\n  The `Phoenix.HTML.FormData` implementation will consider an empty email in\n  this scenario as invalid, but the user shouldn't see the error because they\n  haven't yet used the email input. To handle this, `used_input?/1` can be used to\n  filter errors from the client by referencing param metadata to distinguish between\n  used and unused input fields. For non-LiveViews, all inputs are considered used.\n\n  ```heex\n  <input type=\"text\" name={@form[:title].name} value={@form[:title].value} />\n\n  <div :if={used_input?(@form[:title])}>\n    <p :for={error <- @form[:title].errors}>{error}</p>\n  </div>\n\n  <input type=\"text\" name={@form[:email].name} value={@form[:email].value} />\n\n  <div :if={used_input?(@form[:email])}>\n    <p :for={error <- @form[:email].errors}>{error}</p>\n  </div>\n  ```\n  \"\"\"\n  def used_input?(%Phoenix.HTML.FormField{field: field, form: form}) do\n    used_param?(form.params, field)\n  end\n\n  defp used_param?(_params, \"_unused_\" <> _), do: false\n\n  defp used_param?(params, field) do\n    field_str = \"#{field}\"\n    unused_field_str = \"_unused_#{field}\"\n\n    case params do\n      %{^field_str => _, ^unused_field_str => _} ->\n        false\n\n      %{^field_str => %{} = nested} when not is_struct(nested) ->\n        Enum.any?(Map.keys(nested), &used_param?(nested, &1))\n\n      %{^field_str => _val} ->\n        true\n\n      %{} ->\n        false\n    end\n  end\n\n  @doc \"\"\"\n  Embeds external template files into the module as function components.\n\n  ## Options\n\n    * `:root` - The root directory to embed files. Defaults to the current\n      module's directory (`__DIR__`)\n    * `:suffix` - A string value to append to embedded function names. By\n      default, function names will be the name of the template file excluding\n      the format and engine.\n\n  A wildcard pattern may be used to select all files within a directory tree.\n  For example, imagine a directory listing:\n\n  ```plain\n  ├── components.ex\n  ├── pages\n  │   ├── about_page.html.heex\n  │   └── welcome_page.html.heex\n  ```\n\n  Then to embed the page templates in your `components.ex` module:\n\n      defmodule MyAppWeb.Components do\n        use Phoenix.Component\n\n        embed_templates \"pages/*\"\n      end\n\n  Now, your module will have an `about_page/1` and `welcome_page/1` function\n  component defined. Embedded templates also support declarative assigns\n  via bodyless function definitions, for example:\n\n      defmodule MyAppWeb.Components do\n        use Phoenix.Component\n\n        embed_templates \"pages/*\"\n\n        attr :name, :string, required: true\n        def welcome_page(assigns)\n\n        slot :header\n        def about_page(assigns)\n      end\n\n  Multiple invocations of `embed_templates` is also supported, which can be\n  useful if you have more than one template format. For example:\n\n      defmodule MyAppWeb.Emails do\n        use Phoenix.Component\n\n        embed_templates \"emails/*.html\", suffix: \"_html\"\n        embed_templates \"emails/*.text\", suffix: \"_text\"\n      end\n\n  Note: this function is the same as `Phoenix.Template.embed_templates/2`.\n  It is also provided here for convenience and documentation purposes.\n  Therefore, if you want to embed templates for other formats, which are\n  not related to `Phoenix.Component`, prefer to\n  `import Phoenix.Template, only: [embed_templates: 1]` than this module.\n  \"\"\"\n  @doc type: :macro\n  defmacro embed_templates(pattern, opts \\\\ []) do\n    quote bind_quoted: [pattern: pattern, opts: opts] do\n      Phoenix.Template.compile_all(\n        &Phoenix.Component.__embed__(&1, opts[:suffix]),\n        Path.expand(opts[:root] || __DIR__, __DIR__),\n        pattern\n      )\n    end\n  end\n\n  @doc false\n  def __embed__(path, suffix),\n    do:\n      path\n      |> Path.basename()\n      |> Path.rootname()\n      |> Path.rootname()\n      |> Kernel.<>(suffix || \"\")\n\n  ## Declarative assigns API\n\n  @doc false\n  defmacro __using__(opts \\\\ []) do\n    conditional =\n      if __CALLER__.module != Phoenix.LiveView.Helpers do\n        quote do: import(Phoenix.LiveView.Helpers)\n      end\n\n    imports =\n      quote bind_quoted: [opts: opts] do\n        import Kernel, except: [def: 2, defp: 2]\n        import Phoenix.Component\n        import Phoenix.Component.Declarative\n        require Phoenix.Template\n\n        for {prefix_match, value} <- Phoenix.Component.Declarative.__setup__(__MODULE__, opts) do\n          @doc false\n          def __global__?(unquote(prefix_match)), do: unquote(value)\n        end\n      end\n\n    [conditional, imports]\n  end\n\n  @doc ~S'''\n  Declares a function component slot.\n\n  ## Arguments\n\n  * `name` - an atom defining the name of the slot. Note that slots cannot define the same name\n  as any other slots or attributes declared for the same component.\n\n  * `opts` - a keyword list of options. Defaults to `[]`.\n\n  * `block` - a code block containing calls to `attr/3`. Defaults to `nil`.\n\n  ### Options\n\n  * `:required` - marks a slot as required. If a caller does not pass a value for a required slot,\n  a compilation warning is emitted. Otherwise, an omitted slot will default to `[]`.\n\n  * `:validate_attrs` - when set to `false`, no warning is emitted when a caller passes attributes\n  to a slot defined without a do block. If not set, defaults to `true`.\n\n  * `:doc` - documentation for the slot. Any slot attributes declared\n  will have their documentation listed alongside the slot.\n\n  ### Slot Attributes\n\n  A named slot may declare attributes by passing a block with calls to `attr/3`.\n\n  Unlike attributes, slot attributes cannot accept the `:default` option. Passing one\n  will result in a compile warning being issued.\n\n  ### The Default Slot\n\n  The default slot can be declared by passing `:inner_block` as the `name` of the slot.\n\n  Note that the `:inner_block` slot declaration cannot accept a block. Passing one will\n  result in a compilation error.\n\n  ## Compile-Time Validations\n\n  LiveView performs some validation of slots via the `:phoenix_live_view` compiler.\n  When slots are defined, LiveView will warn at compilation time on the caller if:\n\n  * A required slot of a component is missing.\n\n  * An unknown slot is given.\n\n  * An unknown slot attribute is given.\n\n  On the side of the function component itself, defining attributes provides the following\n  quality of life improvements:\n\n  * Slot documentation is generated for the component.\n\n  * Calls made to the component are tracked for reflection and validation purposes.\n\n  ## Documentation Generation\n\n  Public function components that define slots will have their docs injected into the function's\n  documentation, depending on the value of the `@doc` module attribute:\n\n  * if `@doc` is a string, the slot docs are injected into that string. The optional placeholder\n  `[INSERT LVATTRDOCS]` can be used to specify where in the string the docs are injected.\n  Otherwise, the docs are appended to the end of the `@doc` string.\n\n  * if `@doc` is unspecified, the slot docs are used as the default `@doc` string.\n\n  * if `@doc` is `false`, the slot docs are omitted entirely.\n\n  The injected slot docs are formatted as a markdown list:\n\n    * `name` (required) - slot docs. Accepts attributes:\n      * `name` (`:type`) (required) - attr docs. Defaults to `:default`.\n\n  By default, all slots will have their docs injected into the function `@doc` string.\n  To hide a specific slot, you can set the value of `:doc` to `false`.\n\n  ## Example\n\n      slot :header\n      slot :inner_block, required: true\n      slot :footer\n\n      def modal(assigns) do\n        ~H\"\"\"\n        <div class=\"modal\">\n          <div class=\"modal-header\">\n            {render_slot(@header) || \"Modal\"}\n          </div>\n          <div class=\"modal-body\">\n            {render_slot(@inner_block)}\n          </div>\n          <div class=\"modal-footer\">\n            {render_slot(@footer) || submit_button()}\n          </div>\n        </div>\n        \"\"\"\n      end\n\n  As shown in the example above, `render_slot/1` returns `nil` when an optional slot is declared\n  and none is given. This can be used to attach default behaviour.\n  '''\n  @doc type: :macro\n  defmacro slot(name, opts, block)\n\n  defmacro slot(name, opts, do: block) when is_atom(name) and is_list(opts) do\n    quote do\n      Phoenix.Component.Declarative.__slot__!(\n        __MODULE__,\n        unquote(name),\n        unquote(opts),\n        __ENV__.line,\n        __ENV__.file,\n        fn -> unquote(block) end\n      )\n    end\n  end\n\n  @doc \"\"\"\n  Declares a slot. See `slot/3` for more information.\n  \"\"\"\n  @doc type: :macro\n  defmacro slot(name, opts \\\\ []) when is_atom(name) and is_list(opts) do\n    {block, opts} = Keyword.pop(opts, :do, nil)\n\n    quote do\n      Phoenix.Component.Declarative.__slot__!(\n        __MODULE__,\n        unquote(name),\n        unquote(opts),\n        __ENV__.line,\n        __ENV__.file,\n        fn -> unquote(block) end\n      )\n    end\n  end\n\n  @doc ~S'''\n  Declares attributes for a HEEx function components.\n\n  ## Arguments\n\n  * `name` - an atom defining the name of the attribute. Note that attributes cannot define the\n  same name as any other attributes or slots declared for the same component.\n\n  * `type` - an atom defining the type of the attribute.\n\n  * `opts` - a keyword list of options. Defaults to `[]`.\n\n  ### Types\n\n  An attribute is declared by its name, type, and options. The following types are supported:\n\n  | Name            | Description                                                          |\n  |-----------------|----------------------------------------------------------------------|\n  | `:any`          | any term (including `nil`)                                           |\n  | `:string`       | any binary string                                                    |\n  | `:atom`         | any atom (including `true`, `false`, and `nil`)                      |\n  | `:boolean`      | any boolean                                                          |\n  | `:integer`      | any integer                                                          |\n  | `:float`        | any float                                                            |\n  | `:list`         | any list of any arbitrary types                                      |\n  | `:map`          | any map of any arbitrary types                                       |\n  | `:fun`          | any function                                                         |\n  | `{:fun, arity}` | any function of arity                                                |\n  | `:global`       | any common HTML attributes, plus those defined by `:global_prefixes` |\n  | A struct module | any module that defines a struct with `defstruct/1`                  |\n\n  Note only `:any` and `:atom` expect the value to be set to `nil`.\n\n  ### Options\n\n  * `:required` - marks an attribute as required. If a caller does not pass the given attribute,\n  a compile warning is issued.\n\n  * `:default` - the default value for the attribute if not provided. If this option is\n    not set and the attribute is not given, accessing the attribute will fail unless a\n    value is explicitly set with `assign_new/3`.\n\n  * `:examples` - a non-exhaustive list of values accepted by the attribute, used for documentation\n    purposes.\n\n  * `:values` - an exhaustive list of values accepted by the attributes. If a caller passes a literal\n    not contained in this list, a compile warning is issued.\n\n  * `:doc` - documentation for the attribute.\n\n  ## Compile-Time Validations\n\n  LiveView performs some validation of attributes via the `:phoenix_live_view` compiler.\n  When attributes are defined, LiveView will warn at compilation time on the caller if:\n\n  * A required attribute of a component is missing.\n\n  * An unknown attribute is given.\n\n  * You specify a literal attribute (such as `value=\"string\"` or `value`, but not `value={expr}`)\n  and the type does not match. The following types currently support literal validation:\n  `:string`, `:atom`, `:boolean`, `:integer`, `:float`, `:map` and `:list`.\n\n  * You specify a literal attribute and it is not a member of the `:values` list.\n\n  LiveView does not perform any validation at runtime. This means the type information is mostly\n  used for documentation and reflection purposes.\n\n  On the side of the LiveView component itself, defining attributes provides the following quality\n  of life improvements:\n\n  * The default value of all attributes will be added to the `assigns` map upfront.\n\n  * Attribute documentation is generated for the component.\n\n  * Required struct types are annotated and emit compilation warnings. For example, if you specify\n  `attr :user, User, required: true` and then you write `@user.non_valid_field` in your template,\n  a warning will be emitted.\n\n  * Calls made to the component are tracked for reflection and validation purposes.\n\n  ## Documentation Generation\n\n  Public function components that define attributes will have their attribute\n  types and docs injected into the function's documentation, depending on the\n  value of the `@doc` module attribute:\n\n  * if `@doc` is a string, the attribute docs are injected into that string. The optional\n  placeholder `[INSERT LVATTRDOCS]` can be used to specify where in the string the docs are\n  injected. Otherwise, the docs are appended to the end of the `@doc` string.\n\n  * if `@doc` is unspecified, the attribute docs are used as the default `@doc` string.\n\n  * if `@doc` is `false`, the attribute docs are omitted entirely.\n\n  The injected attribute docs are formatted as a markdown list:\n\n    * `name` (`:type`) (required) - attr docs. Defaults to `:default`.\n\n  By default, all attributes will have their types and docs injected into the function `@doc`\n  string. To hide a specific attribute, you can set the value of `:doc` to `false`.\n\n  ## Example\n\n      attr :name, :string, required: true\n      attr :age, :integer, required: true\n\n      def celebrate(assigns) do\n        ~H\"\"\"\n        <p>\n          Happy birthday {@name}!\n          You are {@age} years old.\n        </p>\n        \"\"\"\n      end\n  '''\n  @doc type: :macro\n  defmacro attr(name, type, opts \\\\ []) do\n    type = Macro.expand_literals(type, %{__CALLER__ | function: {:attr, 3}})\n    opts = Macro.expand_literals(opts, %{__CALLER__ | function: {:attr, 3}})\n\n    quote bind_quoted: [name: name, type: type, opts: opts] do\n      Phoenix.Component.Declarative.__attr__!(\n        __MODULE__,\n        name,\n        type,\n        opts,\n        __ENV__.line,\n        __ENV__.file\n      )\n    end\n  end\n\n  ## Components\n\n  import Kernel, except: [def: 2, defp: 2]\n  import Phoenix.Component.Declarative\n  alias Phoenix.Component.Declarative\n\n  # We need to bootstrap by hand to avoid conflicts.\n  [] = Declarative.__setup__(__MODULE__, [])\n\n  attr = fn name, type, opts ->\n    Declarative.__attr__!(__MODULE__, name, type, opts, __ENV__.line, __ENV__.file)\n  end\n\n  slot = fn name, opts ->\n    Declarative.__slot__!(__MODULE__, name, opts, __ENV__.line, __ENV__.file, fn -> nil end)\n  end\n\n  @doc \"\"\"\n  A function component for rendering `Phoenix.LiveComponent` within a parent LiveView.\n\n  While LiveViews can be nested, each LiveView starts its own process. A LiveComponent provides\n  similar functionality to LiveView, except they run in the same process as the LiveView,\n  with its own encapsulated state. That's why they are called stateful components.\n\n  ## Attributes\n\n  * `id` (`:string`) (required) - A unique identifier for the LiveComponent. Note the `id` won't\n  necessarily be used as the DOM `id`. That is up to the component to decide.\n\n  * `module` (`:atom`) (required) - The LiveComponent module to render.\n\n  Any additional attributes provided will be passed to the LiveComponent as a map of assigns.\n  See `Phoenix.LiveComponent` for more information.\n\n  ## Examples\n\n  ```heex\n  <.live_component module={MyApp.WeatherComponent} id=\"thermostat\" city=\"Kraków\" />\n  ```\n  \"\"\"\n  @doc type: :component\n  def live_component(assigns)\n\n  def live_component(assigns) when is_map(assigns) do\n    id = assigns[:id]\n\n    {module, assigns} =\n      assigns\n      |> Map.delete(:__changed__)\n      |> Map.pop(:module)\n\n    if module == nil or not is_atom(module) do\n      raise ArgumentError,\n            \".live_component expects module={...} to be given and to be an atom, \" <>\n              \"got: #{inspect(module)}\"\n    end\n\n    if id == nil do\n      raise ArgumentError, \".live_component expects id={...} to be given, got: nil\"\n    end\n\n    case module.__live__() do\n      %{kind: :component} ->\n        %Phoenix.LiveView.Component{id: id, assigns: assigns, component: module}\n\n      %{kind: kind} ->\n        raise ArgumentError, \"expected #{inspect(module)} to be a component, but it is a #{kind}\"\n    end\n  end\n\n  @doc \"\"\"\n  Renders a title with automatic prefix/suffix on `@page_title` updates.\n\n  [INSERT LVATTRDOCS]\n\n  ## Examples\n\n  ```heex\n  <.live_title default=\"Welcome\" prefix=\"MyApp · \">\n    {assigns[:page_title]}\n  </.live_title>\n  ```\n\n  ```heex\n  <.live_title default=\"Welcome\" suffix=\" · MyApp\">\n    {assigns[:page_title]}\n  </.live_title>\n  ```\n  \"\"\"\n  @doc type: :component\n  attr.(:prefix, :string,\n    default: nil,\n    doc: \"A prefix added before the content of `inner_block`.\"\n  )\n\n  attr.(:default, :string,\n    default: nil,\n    doc:\n      \"The default title to use if the inner block is empty on regular or connected mounts.\" <>\n        \" *Note*: empty titles, such as `nil` or an empty string, fall back to the default value.\"\n  )\n\n  attr.(:suffix, :string, default: nil, doc: \"A suffix added after the content of `inner_block`.\")\n  slot.(:inner_block, required: true, doc: \"Content rendered inside the `title` tag.\")\n\n  def live_title(assigns) do\n    ~H\"\"\"\n    <title data-prefix={@prefix} data-default={@default} data-suffix={@suffix} phx-no-format>{@prefix}{render_present(render_slot(@inner_block), @default)}{@suffix}</title>\n    \"\"\"\n  end\n\n  defp render_present(rendered_block, default) do\n    block_str =\n      rendered_block\n      |> Phoenix.HTML.html_escape()\n      |> Phoenix.HTML.safe_to_string()\n\n    if String.trim(block_str) == \"\" do\n      default\n    else\n      rendered_block\n    end\n  end\n\n  @doc ~S'''\n  Renders a form.\n\n  This function receives a `Phoenix.HTML.Form` struct, generally created with\n  `to_form/2`, and generates the relevant form tags. It can be used either\n  inside LiveView or outside.\n\n  > To see how forms work in practice, you can run\n  > `mix phx.gen.live Blog Post posts title body:text` inside your Phoenix\n  > application, which will setup the necessary database tables and LiveViews\n  > to manage your data.\n\n  ## Examples: inside LiveView\n\n  Inside LiveViews, this function component is typically called with\n  as `for={@form}`, where `@form` is the result of the `to_form/1` function.\n  `to_form/1` expects either a map or an [`Ecto.Changeset`](https://hexdocs.pm/ecto/Ecto.Changeset.html)\n  as the source of data and normalizes it into `Phoenix.HTML.Form` structure.\n\n  For example, you may use the parameters received in a\n  `c:Phoenix.LiveView.handle_event/3` callback to create an Ecto changeset\n  and then use `to_form/1` to convert it to a form. Then, in your templates,\n  you pass the `@form` as argument to `:for`:\n\n  ```heex\n  <.form\n    for={@form}\n    id=\"my-form\"\n    phx-change=\"change_name\"\n  >\n    <.input field={@form[:email]} />\n  </.form>\n  ```\n\n  The `.input` component is generally defined as part of your own application\n  and adds all styling necessary:\n\n  ```heex\n  def input(assigns) do\n    ~H\"\"\"\n    <input type=\"text\" name={@field.name} id={@field.id} value={@field.value} class=\"...\" />\n    \"\"\"\n  end\n  ```\n\n  A form accepts multiple options. For example, if you are doing file uploads\n  and you want to capture submissions, you might write instead:\n\n  ```heex\n  <.form\n    for={@form}\n    id=\"my-form\"\n    phx-change=\"change_user\"\n    phx-submit=\"save_user\"\n    multipart\n  >\n    ...\n    <input type=\"submit\" value=\"Save\" />\n  </.form>\n  ```\n\n  Notice how both examples use `phx-change`. The LiveView must implement the\n  `phx-change` event and store the input values as they arrive on change.\n  This is important because, if an unrelated change happens on the page,\n  LiveView should re-render the inputs with their updated values. Without `phx-change`,\n  the inputs would otherwise be cleared. Alternatively, you can use `phx-update=\"ignore\"`\n  on the form to discard any updates.\n\n  ### Using the `for` attribute\n\n  The `for` attribute can also be a map or an Ecto.Changeset. In such cases,\n  a form will be created on the fly, and you can capture it using `:let`:\n\n  ```heex\n  <.form\n    :let={form}\n    for={@changeset}\n    id=\"my-form\"\n    phx-change=\"change_user\"\n  >\n  ```\n\n  However, such approach is discouraged in LiveView for two reasons:\n\n    * LiveView can better optimize your code if you access the form fields\n      using `@form[:field]` rather than through the let-variable `form`\n\n    * Ecto changesets are meant to be single use. By never storing the changeset\n      in the assign, you will be less tempted to use it across operations\n\n  ### A note on `:errors`\n\n  Even if `changeset.errors` is non-empty, errors will not be displayed in a\n  form if [the changeset\n  `:action`](https://hexdocs.pm/ecto/Ecto.Changeset.html#module-changeset-actions)\n  is `nil` or `:ignore`.\n\n  This is useful for things like validation hints on form fields, e.g. an empty\n  changeset for a new form. That changeset isn't valid, but we don't want to\n  show errors until an actual user action has been performed.\n\n  For example, if the user submits and a `Repo.insert/1` is called and fails on\n  changeset validation, the action will be set to `:insert` to show that an\n  insert was attempted, and the presence of that action will cause errors to be\n  displayed. The same is true for Repo.update/delete.\n\n  Error visibility is handled by providing the action to `to_form/2`, which will\n  set the underlying changeset action. You can also set the action manually by\n  directly updating on the `Ecto.Changeset` struct field, or by using\n  `Ecto.Changeset.apply_action/2`. Since the action can be arbitrary, you can\n  set it to `:validate` or anything else to avoid giving the impression that a\n  database operation has actually been attempted.\n\n  ### Displaying errors on used and unused input fields\n\n  Used inputs are only those inputs that have been focused, interacted with, or\n  submitted by the client. In most cases, a user shouldn't receive error feedback\n  for forms they haven't yet interacted with, until they submit the form. Filtering\n  the errors based on used input fields can be done with `used_input?/1`.\n\n  ## Example: outside LiveView (regular HTTP requests)\n\n  The `form` component can still be used to submit forms outside of LiveView.\n  In such cases, the standard HTML `action` attribute MUST be given.\n  Without said attribute, the `form` method and csrf token are discarded.\n\n  ```heex\n  <.form :let={f} for={@changeset} action={~p\"/comments/#{@comment}\"}>\n    <.input field={f[:body]} />\n  </.form>\n  ```\n\n  In the example above, we passed a changeset to `for` and captured\n  the value using `:let={f}`. This approach is ok outside of LiveViews,\n  as there are no change tracking optimizations to consider.\n\n  ### CSRF protection\n\n  CSRF protection is a mechanism to ensure that the user who rendered\n  the form is the one actually submitting it. This module generates a\n  CSRF token by default. Your application should check this token on\n  the server to avoid attackers from making requests on your server on\n  behalf of other users. Phoenix by default checks this token.\n\n  When posting a form with a host in its address, such as \"//host.com/path\"\n  instead of only \"/path\", Phoenix will include the host signature in the\n  token and validate the token only if the accessed host is the same as\n  the host in the token. This is to avoid tokens from leaking to third\n  party applications. If this behaviour is problematic, you can generate\n  a non-host specific token with `Plug.CSRFProtection.get_csrf_token/0` and\n  pass it to the form generator via the `:csrf_token` option.\n\n  [INSERT LVATTRDOCS]\n  '''\n  @doc type: :component\n  attr.(:for, :any, required: true, doc: \"An existing form or the form source data.\")\n\n  attr.(:action, :string,\n    doc: \"\"\"\n    The action to submit the form on.\n    This attribute must be given if you intend to submit the form to a URL without LiveView.\n    \"\"\"\n  )\n\n  attr.(:as, :atom,\n    doc: \"\"\"\n    The prefix to be used in names and IDs generated by the form.\n    For example, setting `as: :user_params` means the parameters\n    will be nested \"user_params\" in your `handle_event` or\n    `conn.params[\"user_params\"]` for regular HTTP requests.\n    If you set this option, you must capture the form with `:let`.\n    \"\"\"\n  )\n\n  attr.(:csrf_token, :any,\n    doc: \"\"\"\n    A token to authenticate the validity of requests.\n    One is automatically generated when an action is given and the method is not `get`.\n    When set to `false`, no token is generated.\n    \"\"\"\n  )\n\n  attr.(:errors, :list,\n    doc: \"\"\"\n    Use this to manually pass a keyword list of errors to the form.\n    This option is useful when a regular map is given as the form\n    source and it will make the errors available under `f.errors`.\n    If you set this option, you must capture the form with `:let`.\n    \"\"\"\n  )\n\n  attr.(:method, :string,\n    doc: \"\"\"\n    The HTTP method.\n    It is only used if an `:action` is given. If the method is not `get` nor `post`,\n    an input tag with name `_method` is generated alongside the form tag.\n    If an `:action` is given with no method, the method will default to the return value\n    of `Phoenix.HTML.FormData.to_form/2` (usually `post`).\n    \"\"\"\n  )\n\n  attr.(:multipart, :boolean,\n    default: false,\n    doc: \"\"\"\n    Sets `enctype` to `multipart/form-data`.\n    Required when uploading files.\n    \"\"\"\n  )\n\n  attr.(:rest, :global,\n    include: ~w(autocomplete name rel enctype novalidate target),\n    doc: \"Additional HTML attributes to add to the form tag.\"\n  )\n\n  slot.(:inner_block, required: true, doc: \"The content rendered inside of the form tag.\")\n\n  def form(assigns) do\n    action = assigns[:action]\n\n    # We require for={...} to be given but we automatically handle nils for convenience\n    form_for =\n      case assigns[:for] do\n        nil -> %{}\n        other -> other\n      end\n\n    form_options =\n      assigns\n      |> Map.take([:as, :csrf_token, :errors, :method, :multipart])\n      |> Map.merge(assigns.rest)\n      |> Map.to_list()\n\n    # Since FormData may add options, read the actual options from form\n    %{options: opts} = form = to_form(form_for, form_options)\n\n    # By default, we will ignore action, method, and csrf token\n    # unless the action is given.\n    {attrs, hidden_method, csrf_token} =\n      if action do\n        {method, opts} = Keyword.pop(opts, :method)\n        {method, hidden_method} = form_method(method)\n\n        {csrf_token, opts} =\n          Keyword.pop_lazy(opts, :csrf_token, fn ->\n            if method == \"post\" do\n              Plug.CSRFProtection.get_csrf_token_for(action)\n            end\n          end)\n\n        {[action: action, method: method] ++ opts, hidden_method, csrf_token}\n      else\n        {opts, nil, nil}\n      end\n\n    attrs =\n      case Keyword.pop(attrs, :multipart, false) do\n        {false, attrs} -> attrs\n        {true, attrs} -> Keyword.put(attrs, :enctype, \"multipart/form-data\")\n      end\n\n    assigns =\n      assign(assigns,\n        form: form,\n        csrf_token: csrf_token,\n        hidden_method: hidden_method,\n        attrs: attrs\n      )\n\n    ~H\"\"\"\n    <form {@attrs}>\n      <input\n        :if={@hidden_method && @hidden_method not in ~w(get post)}\n        name=\"_method\"\n        type=\"hidden\"\n        hidden\n        value={@hidden_method}\n      />\n      <input :if={@csrf_token} name=\"_csrf_token\" type=\"hidden\" hidden value={@csrf_token} />\n      {render_slot(@inner_block, @form)}\n    </form>\n    \"\"\"\n  end\n\n  defp form_method(nil), do: {\"post\", nil}\n\n  defp form_method(method) when is_binary(method) do\n    case String.downcase(method) do\n      method when method in ~w(get post) -> {method, nil}\n      _ -> {\"post\", method}\n    end\n  end\n\n  @doc \"\"\"\n  Renders nested form inputs for associations or embeds.\n\n  [INSERT LVATTRDOCS]\n\n  ## Examples\n\n  ```heex\n  <.form\n    for={@form}\n    id=\"my-form\"\n    phx-change=\"change_name\"\n  >\n    <.inputs_for :let={f_nested} field={@form[:nested]}>\n      <.input type=\"text\" field={f_nested[:name]} />\n    </.inputs_for>\n  </.form>\n  ```\n\n  ## Dynamically adding and removing inputs\n\n  Dynamically adding and removing inputs is supported by rendering named buttons for\n  inserts and removals. Like inputs, buttons with name/value pairs are serialized with\n  form data on change and submit events. Libraries such as Ecto, or custom param\n  filtering can then inspect the parameters and handle the added or removed fields.\n  This can be combined with `Ecto.Changeset.cast_assoc/3`'s `:sort_param` and `:drop_param`\n  options. For example, imagine a parent with an `:emails` `has_many` or `embeds_many`\n  association. To cast the user input from a nested form, one simply needs to configure\n  the options:\n\n      schema \"mailing_lists\" do\n        field :title, :string\n\n        embeds_many :emails, EmailNotification, on_replace: :delete do\n          field :email, :string\n          field :name, :string\n        end\n      end\n\n      def changeset(list, attrs) do\n        list\n        |> cast(attrs, [:title])\n        |> cast_embed(:emails,\n          with: &email_changeset/2,\n          sort_param: :emails_sort,\n          drop_param: :emails_drop\n        )\n      end\n\n  Here we see the `:sort_param` and `:drop_param` options in action.\n\n  > Note: `on_replace: :delete` on the `has_many` and `embeds_many` is required\n  > when using these options.\n\n  When Ecto sees the specified sort or drop parameter from the form, it will sort\n  the children based on the order they appear in the form, add new children it hasn't\n  seen, or drop children if the parameter instructs it to do so.\n\n  The markup for such a schema and association would look like this:\n\n  ```heex\n  <.inputs_for :let={ef} field={@form[:emails]}>\n    <input type=\"hidden\" name=\"mailing_list[emails_sort][]\" value={ef.index} />\n    <.input type=\"text\" field={ef[:email]} placeholder=\"email\" />\n    <.input type=\"text\" field={ef[:name]} placeholder=\"name\" />\n    <button\n      type=\"button\"\n      name=\"mailing_list[emails_drop][]\"\n      value={ef.index}\n      phx-click={JS.dispatch(\"change\")}\n    >\n      <.icon name=\"hero-x-mark\" class=\"w-6 h-6 relative top-2\" />\n    </button>\n  </.inputs_for>\n\n  <input type=\"hidden\" name=\"mailing_list[emails_drop][]\" />\n\n  <button type=\"button\" name=\"mailing_list[emails_sort][]\" value=\"new\" phx-click={JS.dispatch(\"change\")}>\n    add more\n  </button>\n  ```\n\n  We used `inputs_for` to render inputs for the `:emails` association, which\n  contains an email address and name input for each child. Within the nested inputs,\n  we render a hidden `mailing_list[emails_sort][]` input, which is set to the index of the\n  given child. This tells Ecto's cast operation how to sort existing children, or\n  where to insert new children. Next, we render the email and name inputs as usual.\n  Then we render a button containing the \"delete\" text with the name `mailing_list[emails_drop][]`,\n  containing the index of the child as its value.\n\n  Like before, this tells Ecto to delete the child at this index when the button is\n  clicked. We use `phx-click={JS.dispatch(\"change\")}` on the button to tell LiveView\n  to treat this button click as a change event, rather than a submit event on the form,\n  which invokes our form's `phx-change` binding.\n\n  Outside the `inputs_for`, we render an empty `mailing_list[emails_drop][]` input,\n  to ensure that all children are deleted when saving a form where the user\n  dropped all entries. This hidden input is required whenever dropping associations.\n\n  Finally, we also render another button with the sort param name `mailing_list[emails_sort][]`\n  and `value=\"new\"` name with accompanied \"add more\" text. Please note that this button must\n  have `type=\"button\"` to prevent it from submitting the form.\n  Ecto will treat unknown sort params as new children and build a new child.\n  This button is optional and only necessary if you want to dynamically add entries.\n  You can optionally add a similar button before the `<.inputs_for>`, in the case you want\n  to prepend entries.\n\n  > ### A note on accessing a field's `value` {: .warning}\n  >\n  > You may be tempted to access `form[:field].value` or attempt to manipulate\n  > the form metadata in your templates. However, bear in mind that the `form[:field]`\n  > value reflects the most recent changes. For example, an `:integer` field may\n  > either contain integer values, but it may also hold a string, if the form has\n  > been submitted.\n  >\n  > This is particularly noticeable when using `inputs_for`. Accessing the `.value`\n  > of a nested field may either return a struct, a changeset, or raw parameters\n  > sent by the client (when using `drop_param`). This makes the `form[:field].value`\n  > impractical for deriving or computing other properties.\n  >\n  > The correct way to approach this problem is by computing any property either in\n  > your LiveViews, by traversing the relevant changesets and data structures, or by\n  > moving the logic to the `Ecto.Changeset` itself.\n  >\n  > As an example, imagine you are building a time tracking application where:\n  >\n  > - users enter the total work time for a day\n  > - individual activities are tracked as embeds\n  > - the sum of all activities should match the total time\n  > - the form should display the remaining time\n  >\n  > Instead of trying to calculate the remaining time in your template by\n  > doing something like `calculate_remaining(@form)` and accessing\n  > `form[:activities].value`, calculate the remaining time based\n  > on the changeset in your `handle_event` instead:\n  >\n  > ```elixir\n  > def handle_event(\"validate\", %{\"tracked_day\" => params}, socket) do\n  >   changeset = TrackedDay.changeset(socket.assigns.tracked_day, params)\n  >   remaining = calculate_remaining(changeset)\n  >   {:noreply, assign(socket, form: to_form(changeset, action: :validate), remaining: remaining)}\n  > end\n  >\n  > # Helper function to calculate remaining time\n  > defp calculate_remaining(changeset) do\n  >   total = Ecto.Changeset.get_field(changeset, :total)\n  >   activities = Ecto.Changeset.get_embed(changeset, :activities)\n  >\n  >   Enum.reduce(activities, total, fn activity, acc ->\n  >     duration =\n  >       case activity do\n  >         %{valid?: true} = changeset -> Ecto.Changeset.get_field(changeset, :duration)\n  >         # if the activity is invalid, we don't include its duration in the calculation\n  >         _ -> 0\n  >       end\n  >\n  >     acc - length\n  >   end)\n  > end\n  > ```\n  >\n  > This logic might also be implemented directly in your schema module and, if you\n  > often need the `:remaining` value, you could also add it as a `:virtual` field to\n  > your schema and run the calculation when validating the changeset:\n  >\n  > ```elixir\n  > def changeset(tracked_day, attrs) do\n  >   tracked_day\n  >   |> cast(attrs, [:total_duration])\n  >   |> cast_embed(:activities)\n  >   |> validate_required([:total_duration])\n  >   |> validate_number(:total_duration, greater_than: 0)\n  >   |> validate_and_put_remaining_time()\n  > end\n  >\n  > defp validate_and_put_remaining_time(changeset) do\n  >   remaining = calculate_remaining(changeset)\n  >   put_change(changeset, :remaining, remaining)\n  > end\n  > ```\n  >\n  > By using this approach, you can safely render the remaining time in your template\n  > using `@form[:remaining].value`, avoiding the pitfalls of directly accessing complex field values.\n  \"\"\"\n  @doc type: :component\n  attr.(:field, Phoenix.HTML.FormField,\n    required: true,\n    doc: \"A %Phoenix.HTML.Form{}/field name tuple, for example: {@form[:email]}.\"\n  )\n\n  attr.(:id, :string,\n    doc: \"\"\"\n    The id base to be used in the form inputs. Defaults to the parent form id. The computed\n    id will be the concatenation of the base id with the field name, along with a book keeping\n    index for each input in the list.\n    \"\"\"\n  )\n\n  attr.(:as, :atom,\n    doc: \"\"\"\n    The name to be used in the form, defaults to the concatenation of the given\n    field to the parent form name.\n    \"\"\"\n  )\n\n  attr.(:default, :any, doc: \"The value to use if none is available.\")\n\n  attr.(:prepend, :list,\n    doc: \"\"\"\n    The values to prepend when rendering. This only applies if the field value\n    is a list and no parameters were sent through the form.\n    \"\"\"\n  )\n\n  attr.(:append, :list,\n    doc: \"\"\"\n    The values to append when rendering. This only applies if the field value\n    is a list and no parameters were sent through the form.\n    \"\"\"\n  )\n\n  attr.(:skip_hidden, :boolean,\n    default: false,\n    doc: \"\"\"\n    Skip the automatic rendering of hidden fields to allow for more tight control\n    over the generated markup.\n    \"\"\"\n  )\n\n  attr.(:skip_persistent_id, :boolean,\n    default: false,\n    doc: \"\"\"\n    Skip the automatic rendering of hidden _persistent_id fields used for reordering\n    inputs.\n    \"\"\"\n  )\n\n  attr.(:options, :list,\n    default: [],\n    doc: \"\"\"\n    Any additional options for the `Phoenix.HTML.FormData` protocol\n    implementation.\n    \"\"\"\n  )\n\n  slot.(:inner_block, required: true, doc: \"The content rendered for each nested form.\")\n\n  @persistent_id \"_persistent_id\"\n  def inputs_for(assigns) do\n    %Phoenix.HTML.FormField{field: field_name, form: parent_form} = assigns.field\n    options = assigns |> Map.take([:id, :as, :default, :append, :prepend]) |> Keyword.new()\n\n    options =\n      parent_form.options\n      |> Keyword.take([:multipart])\n      |> Keyword.merge(options)\n      |> Keyword.merge(assigns.options)\n\n    forms = parent_form.impl.to_form(parent_form.source, parent_form, field_name, options)\n\n    forms =\n      case assigns do\n        %{skip_persistent_id: true} ->\n          forms\n\n        _ ->\n          apply_persistent_id(\n            parent_form,\n            forms,\n            field_name,\n            options\n          )\n      end\n\n    assigns = assign(assigns, :forms, forms)\n\n    ~H\"\"\"\n    <%= for finner <- @forms do %>\n      <%= if !@skip_hidden do %>\n        <%= for {name, value_or_values} <- finner.hidden,\n                name = name_for_value_or_values(finner, name, value_or_values),\n                value <- List.wrap(value_or_values) do %>\n          <input type=\"hidden\" name={name} value={value} />\n        <% end %>\n      <% end %>\n      {render_slot(@inner_block, finner)}\n    <% end %>\n    \"\"\"\n  end\n\n  defp apply_persistent_id(parent_form, forms, field_name, options) do\n    seen_ids = for f <- forms, vid = f.params[@persistent_id], into: %{}, do: {vid, true}\n\n    {forms, _} =\n      Enum.map_reduce(forms, {seen_ids, 0}, fn\n        %Phoenix.HTML.Form{params: params} = form, {seen_ids, index} ->\n          id =\n            case params do\n              %{@persistent_id => id} -> id\n              %{} -> next_id(map_size(seen_ids), seen_ids)\n            end\n\n          form_id =\n            if inputs_for_id = options[:id] do\n              \"#{inputs_for_id}_#{field_name}_#{id}\"\n            else\n              \"#{parent_form.id}_#{field_name}_#{id}\"\n            end\n\n          new_params = Map.put(params, @persistent_id, id)\n          new_hidden = [{@persistent_id, id} | form.hidden]\n\n          new_form = %{\n            form\n            | id: form_id,\n              params: new_params,\n              hidden: new_hidden,\n              index: index\n          }\n\n          {new_form, {Map.put(seen_ids, id, true), index + 1}}\n      end)\n\n    forms\n  end\n\n  defp next_id(idx, %{} = seen_ids) do\n    id_str = to_string(idx)\n\n    if Map.has_key?(seen_ids, id_str) do\n      next_id(idx + 1, seen_ids)\n    else\n      id_str\n    end\n  end\n\n  defp name_for_value_or_values(form, field, values) when is_list(values) do\n    Phoenix.HTML.Form.input_name(form, field) <> \"[]\"\n  end\n\n  defp name_for_value_or_values(form, field, _value) do\n    Phoenix.HTML.Form.input_name(form, field)\n  end\n\n  @doc \"\"\"\n  Generates a link to a given route.\n\n  It is typically used with one of the three attributes:\n\n    * `patch` - on click, it patches the current LiveView with the given path\n    * `navigate` - on click, it navigates to a new LiveView at the given path\n    * `href` - on click, it performs traditional browser navigation (as any `<a>` tag)\n\n  [INSERT LVATTRDOCS]\n\n  ## Examples\n\n  ```heex\n  <.link href=\"/\">Regular anchor link</.link>\n  ```\n\n  ```heex\n  <.link navigate={~p\"/\"} class=\"underline\">home</.link>\n  ```\n\n  ```heex\n  <.link navigate={~p\"/?sort=asc\"} replace={false}>\n    Sort By Price\n  </.link>\n  ```\n\n  ```heex\n  <.link patch={~p\"/details\"}>view details</.link>\n  ```\n\n  ```heex\n  <.link href={URI.parse(\"https://elixir-lang.org\")}>hello</.link>\n  ```\n\n  ```heex\n  <.link href=\"/the_world\" method=\"delete\" data-confirm=\"Really?\">delete</.link>\n  ```\n\n  ## JavaScript dependency\n\n  In order to support links where `:method` is not `\"get\"` or use the above data attributes,\n  `Phoenix.HTML` relies on JavaScript. You can load `priv/static/phoenix_html.js` into your\n  build tool.\n\n  ### Data attributes\n\n  Data attributes are added as a keyword list passed to the `data` key. The following data\n  attributes are supported:\n\n  * `data-confirm` - shows a confirmation prompt before generating and submitting the form when\n  `:method` is not `\"get\"`.\n\n  ### Overriding the default confirm behaviour\n\n  `phoenix_html.js` does trigger a custom event `phoenix.link.click` on the clicked DOM element\n  when a click happened. This allows you to intercept the event on its way bubbling up\n  to `window` and do your own custom logic to enhance or replace how the `data-confirm`\n  attribute is handled. You could for example replace the browsers `confirm()` behavior with\n  a custom javascript implementation:\n\n  ```javascript\n  // Compared to a javascript window.confirm, the custom dialog does not block\n  // javascript execution. Therefore to make this work as expected we store\n  // the successful confirmation as an attribute and re-trigger the click event.\n  // On the second click, the `data-confirm-resolved` attribute is set and we proceed.\n  const RESOLVED_ATTRIBUTE = \"data-confirm-resolved\";\n  // listen on document.body, so it's executed before the default of\n  // phoenix_html, which is listening on the window object\n  document.body.addEventListener('phoenix.link.click', function (e) {\n    // Prevent default implementation\n    e.stopPropagation();\n    // Introduce alternative implementation\n    var message = e.target.getAttribute(\"data-confirm\");\n    if(!message){ return; }\n\n    // Confirm is resolved execute the click event\n    if (e.target?.hasAttribute(RESOLVED_ATTRIBUTE)) {\n      e.target.removeAttribute(RESOLVED_ATTRIBUTE);\n      return;\n    }\n\n    // Confirm is needed, preventDefault and show your modal\n    e.preventDefault();\n    e.target?.setAttribute(RESOLVED_ATTRIBUTE, \"\");\n\n    vex.dialog.confirm({\n      message: message,\n      callback: function (value) {\n        if (value == true) {\n          // Customer confirmed, re-trigger the click event.\n          e.target?.click();\n        } else {\n          // Customer canceled\n          e.target?.removeAttribute(RESOLVED_ATTRIBUTE);\n        }\n      }\n    })\n  }, false);\n  ```\n\n  Or you could attach your own custom behavior.\n\n  ```javascript\n  window.addEventListener('phoenix.link.click', function (e) {\n    // Introduce custom behaviour\n    var message = e.target.getAttribute(\"data-prompt\");\n    var answer = e.target.getAttribute(\"data-prompt-answer\");\n    if(message && answer && (answer != window.prompt(message))) {\n      e.preventDefault();\n    }\n  }, false);\n  ```\n\n  The latter could also be bound to any `click` event, but this way you can be sure your custom\n  code is only executed when the code of `phoenix_html.js` is run.\n\n  ## CSRF Protection\n\n  By default, CSRF tokens are generated through `Plug.CSRFProtection`.\n  \"\"\"\n  @doc type: :component\n  attr.(:navigate, :string,\n    doc: \"\"\"\n    Navigates to a LiveView.\n    When redirecting across LiveViews, the browser page is kept, but a new LiveView process\n    is mounted and its contents is loaded on the page. It is only possible to navigate\n    between LiveViews declared under the same router\n    [`live_session`](`Phoenix.LiveView.Router.live_session/3`).\n    When used outside of a LiveView or across live sessions, it behaves like a regular\n    browser redirect.\n    \"\"\"\n  )\n\n  attr.(:patch, :string,\n    doc: \"\"\"\n    Patches the current LiveView.\n    The `handle_params` callback of the current LiveView will be invoked and the minimum content\n    will be sent over the wire, as any other LiveView diff.\n    \"\"\"\n  )\n\n  attr.(:href, :any,\n    doc: \"\"\"\n    Uses traditional browser navigation to the new location.\n    This means the whole page is reloaded on the browser.\n    \"\"\"\n  )\n\n  attr.(:replace, :boolean,\n    default: false,\n    doc: \"\"\"\n    When using `:patch` or `:navigate`,\n    should the browser's history be replaced with `pushState`?\n    \"\"\"\n  )\n\n  attr.(:method, :string,\n    default: \"get\",\n    doc: \"\"\"\n    The HTTP method to use with the link. This is intended for usage outside of LiveView\n    and therefore only works with the `href={...}` attribute. It has no effect on `patch`\n    and `navigate` instructions.\n\n    In case the method is not `get`, the link is generated inside the form which sets the proper\n    information. In order to submit the form, JavaScript must be enabled in the browser.\n    \"\"\"\n  )\n\n  attr.(:csrf_token, :any,\n    default: true,\n    doc: \"\"\"\n    A boolean or custom token to use for links with an HTTP method other than `get`.\n    \"\"\"\n  )\n\n  attr.(:rest, :global,\n    include: ~w(download hreflang referrerpolicy rel target type),\n    doc: \"\"\"\n    Additional HTML attributes added to the `a` tag.\n    \"\"\"\n  )\n\n  slot.(:inner_block,\n    required: true,\n    doc: \"\"\"\n    The content rendered inside of the `a` tag.\n    \"\"\"\n  )\n\n  def link(%{navigate: to} = assigns) when is_binary(to) do\n    ~H\"\"\"\n    <a\n      href={@navigate}\n      data-phx-link=\"redirect\"\n      data-phx-link-state={if @replace, do: \"replace\", else: \"push\"}\n      phx-no-format\n      {@rest}\n    >{render_slot(@inner_block)}</a>\n    \"\"\"\n  end\n\n  def link(%{patch: to} = assigns) when is_binary(to) do\n    ~H\"\"\"\n    <a\n      href={@patch}\n      data-phx-link=\"patch\"\n      data-phx-link-state={if @replace, do: \"replace\", else: \"push\"}\n      phx-no-format\n      {@rest}\n    >{render_slot(@inner_block)}</a>\n    \"\"\"\n  end\n\n  def link(%{href: href} = assigns) when href != \"#\" and not is_nil(href) do\n    href = Phoenix.LiveView.Utils.valid_destination!(href, \"<.link>\")\n    assigns = assign(assigns, :href, href)\n\n    ~H\"\"\"\n    <a\n      href={@href}\n      data-method={if @method != \"get\", do: @method}\n      data-csrf={if @method != \"get\", do: csrf_token(@csrf_token, @href)}\n      data-to={if @method != \"get\", do: @href}\n      phx-no-format\n      {@rest}\n    >{render_slot(@inner_block)}</a>\n    \"\"\"\n  end\n\n  def link(%{} = assigns) do\n    ~H\"\"\"\n    <a href=\"#\" {@rest}>{render_slot(@inner_block)}</a>\n    \"\"\"\n  end\n\n  defp csrf_token(true, href), do: Plug.CSRFProtection.get_csrf_token_for(href)\n  defp csrf_token(false, _href), do: nil\n  defp csrf_token(csrf, _href) when is_binary(csrf), do: csrf\n\n  @doc \"\"\"\n  Wraps tab focus around a container for accessibility.\n\n  This is an essential accessibility feature for interfaces such as modals, dialogs, and menus.\n\n  [INSERT LVATTRDOCS]\n\n  ## Examples\n\n  Simply render your inner content within this component and focus will be wrapped around the\n  container as the user tabs through the containers content:\n\n  ```heex\n  <.focus_wrap id=\"my-modal\" class=\"bg-white\">\n    <div id=\"modal-content\">\n      Are you sure?\n      <button phx-click=\"cancel\">Cancel</button>\n      <button phx-click=\"confirm\">OK</button>\n    </div>\n  </.focus_wrap>\n  ```\n  \"\"\"\n  @doc type: :component\n  attr.(:id, :string, required: true, doc: \"The DOM identifier of the container tag.\")\n\n  attr.(:rest, :global, doc: \"Additional HTML attributes to add to the container tag.\")\n\n  slot.(:inner_block, required: true, doc: \"The content rendered inside of the container tag.\")\n\n  def focus_wrap(assigns) do\n    ~H\"\"\"\n    <div id={@id} phx-hook=\"Phoenix.FocusWrap\" {@rest}>\n      <div id={\"#{@id}-start\"} tabindex=\"0\" aria-hidden=\"true\"></div>\n      {render_slot(@inner_block)}\n      <div id={\"#{@id}-end\"} tabindex=\"0\" aria-hidden=\"true\"></div>\n    </div>\n    \"\"\"\n  end\n\n  @doc \"\"\"\n  Generates a dynamically named HTML tag.\n\n  Raises an `ArgumentError` if the tag name is found to be unsafe HTML.\n\n  [INSERT LVATTRDOCS]\n\n  ## Examples\n\n  ```heex\n  <.dynamic_tag tag_name=\"input\" name=\"my-input\" type=\"text\"/>\n  ```\n\n  ```html\n  <input name=\"my-input\" type=\"text\"/>\n  ```\n\n  ```heex\n  <.dynamic_tag tag_name=\"p\">content</.dynamic_tag>\n  ```\n\n  ```html\n  <p>content</p>\n  ```\n  \"\"\"\n  @doc type: :component\n  attr.(:tag_name, :string, required: true, doc: \"The name of the tag, such as `div`.\")\n\n  attr.(:name, :string,\n    required: false,\n    doc:\n      \"Deprecated: use tag_name instead. If tag_name is used, passed to the tag. Otherwise the name of the tag, such as `div`.\"\n  )\n\n  attr.(:rest, :global,\n    doc: \"\"\"\n    Additional HTML attributes to add to the tag, ensuring proper escaping.\n    \"\"\"\n  )\n\n  slot.(:inner_block, [])\n\n  def dynamic_tag(%{rest: rest} = assigns) do\n    {tag_name, rest} =\n      case assigns do\n        %{tag_name: tag_name, name: name} ->\n          {tag_name, Map.put(rest, :name, name)}\n\n        %{tag_name: tag_name} ->\n          {tag_name, rest}\n\n        %{name: name} ->\n          IO.warn(\"\"\"\n          Passing the tag name to `Phoenix.Component.dynamic_tag/1` using the `name` attribute is deprecated.\n\n          Instead of:\n\n              <.dynamic_tag name=\"p\" ...>\n\n          use `tag_name` instead:\n\n              <.dynamic_tag tag_name=\"p\" ...>\n          \"\"\")\n\n          {name, Map.delete(rest, :name)}\n      end\n\n    tag =\n      case Phoenix.HTML.html_escape(tag_name) do\n        {:safe, ^tag_name} ->\n          tag_name\n\n        {:safe, _escaped} ->\n          raise ArgumentError,\n                \"expected dynamic_tag name to be safe HTML, got: #{inspect(tag_name)}\"\n      end\n\n    assigns =\n      assigns\n      |> assign(:tag, tag)\n      |> assign(:escaped_attrs, Phoenix.LiveView.HTMLEngine.attributes_escape(rest))\n\n    if assigns.inner_block != [] do\n      ~H\"\"\"\n      {{:safe, [?<, @tag]}}{@escaped_attrs}{{:safe, [?>]}}{render_slot(@inner_block)}{{:safe,\n       [?<, ?/, @tag, ?>]}}\n      \"\"\"\n    else\n      ~H\"\"\"\n      {{:safe, [?<, @tag]}}{@escaped_attrs}{{:safe, [?/, ?>]}}\n      \"\"\"\n    end\n  end\n\n  @doc \"\"\"\n  Builds a file input tag for a LiveView upload.\n\n  [INSERT LVATTRDOCS]\n\n  ## Customizing the Label\n\n  The `id` attribute cannot be overwritten, but you can create a label with a `for` attribute\n  pointing to the UploadConfig `ref`:\n\n  ```heex\n  <label for={@uploads.avatar.ref}>\n    <.live_file_input upload={@uploads.avatar} />\n  </label>\n  ```\n\n  ## Drag and Drop\n\n  Drag and drop is supported by annotating the droppable container with a `phx-drop-target`\n  attribute pointing to the UploadConfig `ref`, so the following markup is all that is required\n  for drag and drop support:\n\n  ```heex\n  <label for={@uploads.avatar.ref} phx-drop-target={@uploads.avatar.ref}>\n    <.live_file_input upload={@uploads.avatar} />\n  </label>\n  ```\n\n  The drop target receives the `phx-drop-target-active` class when it is active. For more information, see the [uploads guide](guides/server/uploads.md).\n  ## Examples\n\n  Rendering a file input:\n\n  ```heex\n  <.live_file_input upload={@uploads.avatar} />\n  ```\n\n  Rendering a file input with a label:\n\n  ```heex\n  <label for={@uploads.avatar.ref}>Avatar</label>\n  <.live_file_input upload={@uploads.avatar} />\n  ```\n  \"\"\"\n  @doc type: :component\n\n  attr.(:upload, Phoenix.LiveView.UploadConfig,\n    required: true,\n    doc: \"The `Phoenix.LiveView.UploadConfig` struct\"\n  )\n\n  attr.(:accept, :string,\n    doc:\n      \"the optional override for the accept attribute. Defaults to :accept specified by allow_upload\"\n  )\n\n  attr.(:rest, :global, include: ~w(webkitdirectory required disabled capture form))\n\n  def live_file_input(%{upload: upload} = assigns) do\n    assigns = assign_new(assigns, :accept, fn -> upload.accept != :any && upload.accept end)\n\n    ~H\"\"\"\n    <input\n      id={@upload.ref}\n      type=\"file\"\n      name={@upload.name}\n      accept={@accept}\n      data-phx-hook=\"Phoenix.LiveFileUpload\"\n      data-phx-upload-ref={@upload.ref}\n      data-phx-active-refs={join_refs(for(entry <- @upload.entries, do: entry.ref))}\n      data-phx-done-refs={join_refs(for(entry <- @upload.entries, entry.done?, do: entry.ref))}\n      data-phx-preflighted-refs={\n        join_refs(for(entry <- @upload.entries, entry.preflighted?, do: entry.ref))\n      }\n      data-phx-auto-upload={@upload.auto_upload?}\n      {if @upload.max_entries > 1, do: Map.put(@rest, :multiple, true), else: @rest}\n    />\n    \"\"\"\n  end\n\n  defp join_refs(entries), do: Enum.join(entries, \",\")\n\n  @doc ~S\"\"\"\n  Generates an image preview on the client for a selected file.\n\n  [INSERT LVATTRDOCS]\n\n  ## Examples\n\n  ```heex\n  <.live_img_preview :for={entry <- @uploads.avatar.entries} entry={entry} width=\"75\" />\n  ```\n\n  When you need to use it multiple times, make sure that they have distinct ids\n\n  ```heex\n  <.live_img_preview :for={entry <- @uploads.avatar.entries} entry={entry} width=\"75\" />\n\n  <.live_img_preview :for={entry <- @uploads.avatar.entries} id={\"modal-#{entry.ref}\"} entry={entry} width=\"500\" />\n  ```\n  \"\"\"\n  @doc type: :component\n\n  attr.(:entry, Phoenix.LiveView.UploadEntry,\n    required: true,\n    doc: \"The `Phoenix.LiveView.UploadEntry` struct\"\n  )\n\n  attr.(:id, :string,\n    default: nil,\n    doc:\n      \"the id of the img tag. Derived by default from the entry ref, but can be overridden as needed if you need to render a preview of the same entry multiple times on the same page\"\n  )\n\n  attr.(:rest, :global, [])\n\n  def live_img_preview(assigns) do\n    ~H\"\"\"\n    <img\n      id={@id || \"phx-preview-#{@entry.ref}\"}\n      data-phx-upload-ref={@entry.upload_ref}\n      data-phx-entry-ref={@entry.ref}\n      data-phx-hook=\"Phoenix.LiveImgPreview\"\n      data-phx-update=\"ignore\"\n      phx-no-format\n      {@rest}\n    />\n    \"\"\"\n  end\n\n  @doc \"\"\"\n  Intersperses separator slot between an enumerable.\n\n  Useful when you need to add a separator between items such as when\n  rendering breadcrumbs for navigation. Provides each item to the\n  inner block.\n\n  ## Examples\n\n  ```heex\n  <.intersperse :let={item} enum={[\"home\", \"profile\", \"settings\"]}>\n    <:separator>\n      <span class=\"sep\">|</span>\n    </:separator>\n    {item}\n  </.intersperse>\n  ```\n\n  Renders the following markup:\n\n  ```html\n  home <span class=\"sep\">|</span> profile <span class=\"sep\">|</span> settings\n  ```\n  \"\"\"\n  @doc type: :component\n  attr.(:enum, :any, required: true, doc: \"the enumerable to intersperse with separators\")\n  slot.(:inner_block, required: true, doc: \"the inner_block to render for each item\")\n  slot.(:separator, required: true, doc: \"the slot for the separator\")\n\n  def intersperse(assigns) do\n    ~H\"\"\"\n    <%= for item <- Enum.intersperse(@enum, :separator) do %><%=\n      if item == :separator do\n        render_slot(@separator)\n      else\n        render_slot(@inner_block, item)\n      end\n    %><% end %>\n    \"\"\"noformat\n  end\n\n  @doc \"\"\"\n  Renders a `Phoenix.LiveView.AsyncResult` struct (e.g. from `Phoenix.LiveView.assign_async/4`)\n  with slots for the different loading states.\n  The result state takes precedence over subsequent loading and failed\n  states.\n\n  > #### Note {: .info}\n  >\n  > The inner block receives the result of the async assign as a `:let`.\n  > The let is only accessible to the inner block and is not in scope to the\n  > other slots.\n\n  ## Examples\n\n  ```elixir\n  def mount(%{\"slug\" => slug}, _, socket) do\n    {:ok,\n      socket\n      |> assign_async(:org, fn -> {:ok, %{org: fetch_org!(slug)}} end)}\n  end\n  ```\n\n  ```heex\n  <.async_result :let={org} assign={@org}>\n    <:loading>Loading organization...</:loading>\n    <:failed :let={_failure}>there was an error loading the organization</:failed>\n    <%= if org do %>\n      {org.name}\n    <% else %>\n      You don't have an organization yet.\n    <% end %>\n  </.async_result>\n  ```\n\n  See [Async Operations](`m:Phoenix.LiveView#module-async-operations`) for more information.\n\n  To display loading and failed states again on subsequent `assign_async` calls,\n  reset the assign to a result-free `%AsyncResult{}`:\n\n  ```elixir\n  {:noreply,\n    socket\n    |> assign_async(:page, :data, &reload_data/0)\n    |> assign(:page, AsyncResult.loading())}\n  ```\n  \"\"\"\n  @doc type: :component\n  attr.(:assign, AsyncResult, required: true)\n  slot.(:loading, doc: \"rendered while the assign is loading for the first time\")\n\n  slot.(:failed,\n    doc:\n      \"rendered when an error or exit is caught or assign_async returns `{:error, reason}` for the first time. Receives the error as a `:let`\"\n  )\n\n  slot.(:inner_block,\n    doc:\n      \"rendered when the assign is loaded successfully via `AsyncResult.ok/2`. Receives the result as a `:let`\"\n  )\n\n  def async_result(%{assign: async_assign} = assigns) do\n    cond do\n      async_assign.ok? ->\n        ~H|{render_slot(@inner_block, @assign.result)}|\n\n      async_assign.loading ->\n        ~H|{render_slot(@loading, @assign.loading)}|\n\n      async_assign.failed ->\n        ~H|{render_slot(@failed, @assign.failed)}|\n    end\n  end\n\n  @doc \"\"\"\n  Renders a portal.\n\n  A portal is a component that teleports its content to another place in the DOM.\n  It is useful in cases where you need to render some content in another place, for\n  example due to overflow or [stacking context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_context).\n\n  A portal consists of two parts:\n\n  1. The portal source: the component that should be teleported.\n  2. The portal target: the DOM element that will render the content of the portal source.\n\n  Any element can be a portal target. In most cases, the target would be rendered inside\n  the layout of your application. Portal sources must be defined with the `.portal` component.\n\n  > #### A note on testing {: .info}\n  >\n  > Because portals use `<template>` elements under the hood, you cannot query for elements\n  > inside of a portal when using `Phoenix.LiveViewTest.element/3` and other LiveViewTest functions.\n  >\n  > Instead, `Phoenix.LiveViewTest.render/1` the portal element itself to an HTML string and do\n  > assertions on those:\n  >\n  > ```heex\n  > <.portal id=\"my-portal\" target=\"body\">\n  >   <div id=\"something-inside\">...</div>\n  > </.portal>\n  > ```\n  >\n  > ```elixir\n  > # in your test, instead of\n  > # assert has_element?(view, \"#something-inside\")  <-- this won't work\n  > html = view |> element(\"#my-portal\") |> render()\n  > assert html =~ \"something-inside\"\n  > ```\n\n  ## Examples\n\n  ```heex\n  <.portal id=\"modal\" target=\"body\">\n    ...\n  </.portal>\n  ```\n  \"\"\"\n\n  attr.(:id, :string, required: true)\n\n  attr.(:target, :string,\n    required: true,\n    doc: \"A CSS selector that identifies the target. The target must be unique.\"\n  )\n\n  attr.(:class, :any, default: nil, doc: \"The class to apply to the portal wrapper.\")\n  attr.(:container, :string, default: \"div\", doc: \"The HTML tag to use as the portal wrapper.\")\n  slot.(:inner_block, required: true)\n\n  def portal(assigns) do\n    ~H\"\"\"\n    <template id={@id} data-phx-portal={@target}>\n      <%!--\n        For correct DOM patching, each portal source (template) must have a single root element,\n        which we enforce by wrapping the slot in a div. In the generated CSS for\n        new projects, we include a display: contents rule for data-phx-teleported-src,\n        which is set by the LiveView JS when an element is teleported.\n      --%>\n      <.dynamic_tag tag_name={@container} id={\"_lv_portal_wrap_\" <> @id} class={@class}>\n        {render_slot(@inner_block)}\n      </.dynamic_tag>\n    </template>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_component.ex",
    "content": "defmodule Phoenix.LiveComponent do\n  @moduledoc ~S'''\n  LiveComponents are a mechanism to compartmentalize state, markup, and\n  events for sharing across LiveViews.\n\n  LiveComponents are defined by using `Phoenix.LiveComponent` and are used\n  by calling `Phoenix.Component.live_component/1` in a parent LiveView.\n  They run inside the LiveView process but have their own state and\n  life-cycle. For this reason, they are also often called \"stateful components\".\n  This is a contrast to `Phoenix.Component`, also known as \"function components\",\n  which are stateless and do not have a life-cycle.\n\n  The simplest LiveComponent only needs to define a `c:render/1` function:\n\n      defmodule HeroComponent do\n        # In Phoenix apps, the line is typically: use MyAppWeb, :live_component\n        use Phoenix.LiveComponent\n\n        def render(assigns) do\n          ~H\"\"\"\n          <div class=\"hero\">{@content}</div>\n          \"\"\"\n        end\n      end\n\n  A LiveComponent is rendered with:\n\n  ```heex\n  <.live_component module={HeroComponent} id=\"hero\" content={@content} />\n  ```\n\n  You must always pass the `module` and `id` attributes. The `id` will be\n  available as an assign and it must be used to uniquely identify the\n  component. All other attributes will be available as assigns inside the\n  LiveComponent.\n\n  > #### Functional components or live components? {: .tip}\n  >\n  > Generally speaking, prefer function components over live components as they\n  > are a simpler abstraction with a smaller surface area. The use case for\n  > live components only arises when there is a need to encapsulate both\n  > event handling and additional state.\n  >\n  > Avoid using LiveComponents merely for code organization purposes.\n\n  ## Life-cycle\n\n  LiveComponents have a similar life-cycle to LiveViews:\n\n  * LiveViews: `mount/3` -> `handle_params/3` -> `render/1` \n  * LiveComponents: `mount/1` -> `update/2` -> `render/1` \n\n  Similarly, both may define a `handle_event/3` callback for\n  client events and use `handle_async/3` to deal with async\n  assigns. Over the next sections, we break down the life-cycle\n  of LiveComponents with examples and diagrams.\n\n  ### Mount and update\n\n  Live components are identified by the component module and their ID.\n  We often tie the component ID to some application based ID:\n\n  ```heex\n  <.live_component module={UserComponent} id={@user.id} user={@user} />\n  ```\n\n  When [`live_component/1`](`Phoenix.Component.live_component/1`) is called,\n  `c:mount/1` is called once, when the component is first added to the page.\n  `c:mount/1` receives a `socket` as its argument. Note that this is *not* the\n  same `socket` struct from the parent LiveView. It doesn't contain the parent\n  LiveView's `assigns`, and updating it won't affect the parent LiveView's\n  `socket`.\n\n  Then `c:update/2` is invoked with all of the assigns passed to\n  [`live_component/1`](`Phoenix.Component.live_component/1`). The assigns\n  received as the first argument to `c:update/2` will only include those\n  assigns given to [`live_component/1`](`Phoenix.Component.live_component/1`)\n  and the `socket`as second argument.\n\n  If `c:update/2` is not defined then all assigns given to\n  [`live_component/1`](`Phoenix.Component.live_component/1`) will simply be\n  merged into `socket.assigns`.\n\n  Both `c:mount/1` and `c:update/2` must return a tuple whose first element is\n  `:ok` and whose second element is the updated `socket`.\n\n  After the component is updated, `c:render/1` is called with all assigns.\n  On first render, we get:\n\n      mount(socket) -> update(assigns, socket) -> render(assigns)\n\n  Now, whenever the parent changes, `mount/1` is no longer called, only update and\n  then render:\n\n      update(assigns, socket) -> render(assigns)\n\n  Two live components with the same module and ID are treated as the same component,\n  regardless of where they are in the page. Therefore, if you change the location\n  of where a component is rendered within its parent LiveView, it won't be remounted.\n  This means you can use live components to implement cards and other elements that\n  can be moved around without losing state. A component is only discarded when the\n  client observes it is removed from the page.\n\n  Finally, the given `id` is not automatically used as the DOM ID. If you want to set\n  a DOM ID, it is your responsibility to do so when rendering:\n\n      defmodule UserComponent do\n        # In Phoenix apps, the line is typically: use MyAppWeb, :live_component\n        use Phoenix.LiveComponent\n\n        def render(assigns) do\n          ~H\"\"\"\n          <div id={\"user-#{@id}\"} class=\"user\">\n            {@user.name}\n          </div>\n          \"\"\"\n        end\n      end\n\n  ### Events\n\n  LiveComponents can also implement the `c:handle_event/3` callback\n  that works exactly the same as in LiveView. For a client event to\n  reach a component, the tag must be annotated with a `phx-target`.\n  If you want to send the event to yourself, you can simply use the\n  `@myself` assign, which is an *internal unique reference* to the\n  component instance:\n\n  ```heex\n  <a href=\"#\" phx-click=\"say_hello\" phx-target={@myself}>\n    Say hello!\n  </a>\n  ```\n\n  Note that `@myself` is not set for stateless components, as they cannot\n  receive events.\n\n  If you want to target another component, you can also pass an ID\n  or a class selector to any element inside the targeted component.\n  For example, if there is a `UserComponent` with the DOM ID of `\"user-13\"`,\n  using a query selector, we can send an event to it with:\n\n  ```heex\n  <a href=\"#\" phx-click=\"say_hello\" phx-target=\"#user-13\">\n    Say hello!\n  </a>\n  ```\n\n  In both cases, `c:handle_event/3` will be called with the\n  \"say_hello\" event. When `c:handle_event/3` is called for a component,\n  only the diff of the component is sent to the client, making them\n  extremely efficient.\n\n  Any valid query selector for `phx-target` is supported, provided that the\n  matched nodes are children of a LiveView or LiveComponent, for example\n  to send the `close` event to multiple components:\n\n  ```heex\n  <a href=\"#\" phx-click=\"close\" phx-target=\"#modal, #sidebar\">\n    Dismiss\n  </a>\n  ```\n\n  ### Update many\n\n  Live components support an optional `c:update_many/1` callback\n  as an alternative to `c:update/2`. While `c:update/2` is called for\n  each component individually, `c:update_many/1` is called with all\n  LiveComponents of the same module being currently rendered/updated.\n  The advantage is that you can preload data from the database using\n  a single query for all components, instead of running one query per\n  component.\n\n  To provide a more complete understanding of why both callbacks are necessary,\n  let's see an example. Imagine you are implementing a component and the component\n  needs to load some state from the database. For example:\n\n  ```heex\n  <.live_component module={UserComponent} id={user_id} />\n  ```\n\n  A possible implementation would be to load the user on the `c:update/2`\n  callback:\n\n      def update(assigns, socket) do\n        user = Repo.get!(User, assigns.id)\n        {:ok, assign(socket, :user, user)}\n      end\n\n  However, the issue with said approach is that, if you are rendering\n  multiple user components in the same page, you have a N+1 query problem.\n  By using `c:update_many/1` instead of `c:update/2` , we receive a list\n  of all assigns and sockets, allowing us to update many at once:\n\n      def update_many(assigns_sockets) do\n        list_of_ids = Enum.map(assigns_sockets, fn {assigns, _socket} -> assigns.id end)\n\n        users =\n          from(u in User, where: u.id in ^list_of_ids, select: {u.id, u})\n          |> Repo.all()\n          |> Map.new()\n\n        Enum.map(assigns_sockets, fn {assigns, socket} ->\n          assign(socket, :user, users[assigns.id])\n        end)\n      end\n\n  Now only a single query to the database will be made. In fact, the\n  `update_many/1` algorithm is a breadth-first tree traversal, which means\n  that even for nested components, the amount of queries are kept to\n  a minimum.\n\n  Finally, note that `c:update_many/1` must return an updated list of\n  sockets in the same order as they are given. If `c:update_many/1` is\n  defined, `c:update/2` is not invoked.\n\n  ### Summary\n\n  All of the life-cycle events are summarized in the diagram below.\n  The bubble events in white are triggers that invoke the component.\n  In purple are component callbacks; the underlined names\n  represent required callbacks.\n\n  ```mermaid\n  flowchart LR\n      *((start)):::event-.->M\n      WE([wait for<br>parent changes]):::event-.->M\n      W([wait for<br>events]):::event-.->H\n\n      subgraph j__transparent[\" \"]\n\n        subgraph i[\" \"]\n          direction TB\n          M(mount/1<br><em>only once</em>):::callback\n          M-->U\n          M-->UM\n        end\n\n        U(update/2):::callback-->A\n        UM(update_many/1):::callback-->A\n\n        subgraph j[\" \"]\n          direction TB\n          A --> |yes| R\n          H(handle_event/3):::callback-->A{any<br>changes?}:::diamond\n        end\n\n        A --> |no| W\n\n      end\n\n      R(render/1):::callback_req-->W\n\n      classDef event fill:#fff,color:#000,stroke:#000\n      classDef diamond fill:#FFC28C,color:#000,stroke:#000\n      classDef callback fill:#B7ADFF,color:#000,stroke-width:0\n      classDef callback_req fill:#B7ADFF,color:#000,stroke-width:0,text-decoration:underline\n  ```\n\n  ## Managing state\n\n  Now that we have learned how to define and use components, as well as\n  how to use `c:update_many/1` as a data loading optimization, it is important\n  to talk about how to manage state in components.\n\n  Generally speaking, you want to avoid both the parent LiveView and the\n  LiveComponent working on two different copies of the state. Instead, you\n  should assume only one of them to be the source of truth. Let's discuss\n  the two different approaches in detail.\n\n  Imagine a scenario where a LiveView represents a board with each card\n  in it as a separate LiveComponent. Each card has a form to\n  allow update of the card title directly in the component, as follows:\n\n      defmodule CardComponent do\n        use Phoenix.LiveComponent\n\n        def render(assigns) do\n          ~H\"\"\"\n          <form phx-submit=\"...\" phx-target={@myself}>\n            <input name=\"title\">{@card.title}</input>\n            ...\n          </form>\n          \"\"\"\n        end\n\n        ...\n      end\n\n  We will see how to organize the data flow to keep either the board LiveView or\n  the card LiveComponents as the source of truth.\n\n  ### LiveView as the source of truth\n\n  If the board LiveView is the source of truth, it will be responsible\n  for fetching all of the cards in a board. Then it will call\n  [`live_component/1`](`Phoenix.Component.live_component/1`)\n  for each card, passing the card struct as argument to `CardComponent`:\n\n  ```heex\n  <.live_component\n    :for={card <- @cards}\n    module={CardComponent}\n    card={card}\n    id={card.id}\n    board_id={@id}\n  />\n  ```\n\n  Now, when the user submits the form, `CardComponent.handle_event/3`\n  will be triggered. However, if the update succeeds, you must not\n  change the card struct inside the component. If you do so, the card\n  struct in the component will get out of sync with the LiveView.  Since\n  the LiveView is the source of truth, you should instead tell the\n  LiveView that the card was updated.\n\n  Luckily, because the component and the view run in the same process,\n  sending a message from the LiveComponent to the parent LiveView is as\n  simple as sending a message to `self()`:\n\n      defmodule CardComponent do\n        ...\n        def handle_event(\"update_title\", %{\"title\" => title}, socket) do\n          send self(), {:updated_card, %{socket.assigns.card | title: title}}\n          {:noreply, socket}\n        end\n      end\n\n  The LiveView then receives this event using `c:Phoenix.LiveView.handle_info/2`:\n\n      defmodule BoardView do\n        ...\n        def handle_info({:updated_card, card}, socket) do\n          # update the list of cards in the socket\n          {:noreply, updated_socket}\n        end\n      end\n\n  Because the list of cards in the parent socket was updated, the parent\n  LiveView will be re-rendered, sending the updated card to the component.\n  So in the end, the component does get the updated card, but always\n  driven from the parent.\n\n  Alternatively, instead of having the component send a message directly to the\n  parent view, the component could broadcast the update using `Phoenix.PubSub`.\n  Such as:\n\n      defmodule CardComponent do\n        ...\n        def handle_event(\"update_title\", %{\"title\" => title}, socket) do\n          message = {:updated_card, %{socket.assigns.card | title: title}}\n          Phoenix.PubSub.broadcast(MyApp.PubSub, board_topic(socket), message)\n          {:noreply, socket}\n        end\n\n        defp board_topic(socket) do\n          \"board:\" <> socket.assigns.board_id\n        end\n      end\n\n  As long as the parent LiveView subscribes to the `board:<ID>` topic,\n  it will receive updates. The advantage of using PubSub is that we get\n  distributed updates out of the box. Now, if any user connected to the\n  board changes a card, all other users will see the change.\n\n  ### LiveComponent as the source of truth\n\n  If each card LiveComponent is the source of truth, then the board LiveView\n  must no longer fetch the card structs from the database. Instead, the board\n  LiveView must only fetch the card ids, then render each component only by\n  passing an ID:\n\n  ```heex\n  <.live_component\n    :for={card_id <- @card_ids}\n    module={CardComponent}\n    id={card_id}\n    board_id={@id}\n  />\n  ```\n\n  Now, each CardComponent will load its own card. Of course, doing so\n  per card could be expensive and lead to N queries, where N is the\n  number of cards, so we can use the `c:update_many/1` callback to make it\n  efficient.\n\n  Once the card components are started, they can each manage their own\n  card, without concerning themselves with the parent LiveView.\n\n  However, note that components do not have a `c:Phoenix.LiveView.handle_info/2`\n  callback. Therefore, if you want to track distributed changes on a card,\n  you must have the parent LiveView receive those events and redirect them\n  to the appropriate card. For example, assuming card updates are sent\n  to the \"board:ID\" topic, and that the board LiveView is subscribed to\n  said topic, one could do:\n\n      def handle_info({:updated_card, card}, socket) do\n        send_update CardComponent, id: card.id, board_id: socket.assigns.id\n        {:noreply, socket}\n      end\n\n  With `Phoenix.LiveView.send_update/3`, the `CardComponent` given by `id`\n  will be invoked, triggering the update or update_many callback, which will\n  load the most up to date data from the database.\n\n  ### Unifying LiveView and LiveComponent communication\n\n  In the examples above, we have used `send/2` to communicate with LiveView\n  and `send_update/2` to communicate with components. This introduces a problem:\n  what if you have a component that may be mounted both inside a LiveView\n  or another component? Given each uses a different API for exchanging data,\n  this may seem tricky at first, but an elegant solution is to use anonymous\n  functions as callbacks. Let's see an example.\n\n  In the sections above, we wrote the following code in our `CardComponent`:\n\n  ```elixir\n  def handle_event(\"update_title\", %{\"title\" => title}, socket) do\n    send(self(), {:updated_card, %{socket.assigns.card | title: title}})\n    {:noreply, socket}\n  end\n  ```\n\n  The issue with this code is that, if CardComponent is mounted inside another\n  component, it will still message the LiveView. Not only that, this code may\n  be hard to maintain because the message sent by the component is defined far\n  away from the LiveView that will receive it.\n\n  Instead let's define a callback that will be invoked by CardComponent:\n\n  ```elixir\n  def handle_event(\"update_title\", %{\"title\" => title}, socket) do\n    socket.assigns.on_card_update.(%{socket.assigns.card | title: title})\n    {:noreply, socket}\n  end\n  ```\n\n  And now when initializing the CardComponent from a LiveView, we may write:\n\n  ```heex\n  <.live_component\n    module={CardComponent}\n    card={card}\n    id={card.id}\n    board_id={@id}\n    on_card_update={fn card -> send(self(), {:updated_card, card}) end} />\n  ```\n\n  If initializing it inside another component, one may write:\n\n  ```heex\n  <.live_component\n    module={CardComponent}\n    card={card}\n    id={card.id}\n    board_id={@id}\n    on_card_update={fn card -> send_update(@myself, card: card) end} />\n  ```\n\n  The major benefit in both cases is that the parent has explicit control\n  over the messages it will receive.\n\n  ## Cost of live components\n\n  The internal infrastructure LiveView uses to keep track of live\n  components is very lightweight. However, be aware that in order to\n  provide change tracking and to send diffs over the wire, all of the\n  components' assigns are kept in memory - exactly as it is done in\n  LiveViews themselves.\n\n  Therefore it is your responsibility to keep only the assigns necessary\n  in each component. For example, avoid passing all of LiveView's assigns\n  when rendering a component:\n\n  ```heex\n  <.live_component module={MyComponent} {assigns} />\n  ```\n\n  Instead pass only the keys that you need:\n\n  ```heex\n  <.live_component module={MyComponent} user={@user} org={@org} />\n  ```\n\n  Luckily, because LiveViews and LiveComponents are in the same process,\n  they share the data structure representations in memory. For example,\n  in the code above, the view and the component will share the same copies\n  of the `@user` and `@org` assigns.\n\n  You should also avoid using live components to provide abstract DOM\n  components. As a guideline, a good LiveComponent encapsulates\n  application concerns and not DOM functionality. For example, if you\n  have a page that shows products for sale, you can encapsulate the\n  rendering of each of those products in a component. This component\n  may have many buttons and events within it. On the opposite side,\n  do not write a component that is simply encapsulating generic DOM\n  components. For instance, do not do this:\n\n      defmodule MyButton do\n        use Phoenix.LiveComponent\n\n        def render(assigns) do\n          ~H\"\"\"\n          <button class=\"css-framework-class\" phx-click=\"click\">\n            {@text}\n          </button>\n          \"\"\"\n        end\n\n        def handle_event(\"click\", _, socket) do\n          _ = socket.assigns.on_click.()\n          {:noreply, socket}\n        end\n      end\n\n  Instead, it is much simpler to create a function component:\n\n      def my_button(%{text: _, click: _} = assigns) do\n        ~H\"\"\"\n        <button class=\"css-framework-class\" phx-click={@click}>\n          {@text}\n        </button>\n        \"\"\"\n      end\n\n  If you keep components mostly as an application concern with\n  only the necessary assigns, it is unlikely you will run into\n  issues related to live components.\n\n  ## Live patches and live redirects\n\n  A template rendered inside a component can use `<.link patch={...}>` and\n  `<.link navigate={...}>`. Patches are always handled by the parent LiveView,\n  as components do not provide `handle_params`.\n\n  ## Slots\n\n  LiveComponent can also receive slots, in the same way as a `Phoenix.Component`:\n\n  ```heex\n  <.live_component module={MyComponent} id={@data.id} >\n    <div>Inner content here</div>\n  </.live_component>\n  ```\n\n  If the LiveComponent defines an `c:update/2`, be sure that the socket it returns\n  includes the `:inner_block` assign it received.\n\n  See [the docs](Phoenix.Component.html#module-slots.md) for `Phoenix.Component` for more information.\n\n  ## Limitations\n\n  Live Components require a single HTML tag at the root. It is not possible\n  to have components that render only text or multiple tags.\n  '''\n\n  defmodule CID do\n    @moduledoc \"\"\"\n    The struct representing an internal unique reference to the component instance,\n    available as the `@myself` assign in live components.\n\n    Read more about the uses of `@myself` in the `Phoenix.LiveComponent` docs.\n    \"\"\"\n\n    defstruct [:cid]\n\n    defimpl Phoenix.HTML.Safe do\n      def to_iodata(%{cid: cid}), do: Integer.to_string(cid)\n    end\n\n    defimpl String.Chars do\n      def to_string(%{cid: cid}), do: Integer.to_string(cid)\n    end\n  end\n\n  alias Phoenix.LiveView.Socket\n\n  @doc \"\"\"\n  Uses LiveComponent in the current module.\n\n      use Phoenix.LiveComponent\n\n  ## Options\n\n    * `:global_prefixes` - the global prefixes to use for components. See\n      `Global Attributes` in `Phoenix.Component` for more information.\n  \"\"\"\n  defmacro __using__(opts \\\\ []) do\n    quote do\n      import Phoenix.LiveView\n      @behaviour Phoenix.LiveComponent\n      @before_compile Phoenix.LiveView.Renderer\n\n      # Phoenix.Component must come last so its @before_compile runs last\n      use Phoenix.Component, Keyword.take(unquote(opts), [:global_prefixes])\n\n      @doc false\n      def __live__, do: %{kind: :component, layout: false}\n    end\n  end\n\n  @callback mount(socket :: Socket.t()) ::\n              {:ok, Socket.t()} | {:ok, Socket.t(), keyword()}\n\n  @callback update(assigns :: Socket.assigns(), socket :: Socket.t()) :: {:ok, Socket.t()}\n\n  @callback update_many([{Socket.assigns(), Socket.t()}]) :: [Socket.t()]\n\n  @callback render(assigns :: Socket.assigns()) :: Phoenix.LiveView.Rendered.t()\n\n  @callback handle_event(\n              event :: binary,\n              unsigned_params :: Phoenix.LiveView.unsigned_params(),\n              socket :: Socket.t()\n            ) ::\n              {:noreply, Socket.t()} | {:reply, map, Socket.t()}\n\n  @callback handle_async(\n              name :: term,\n              async_fun_result :: {:ok, term} | {:exit, term},\n              socket :: Socket.t()\n            ) ::\n              {:noreply, Socket.t()}\n\n  @optional_callbacks mount: 1,\n                      update_many: 1,\n                      update: 2,\n                      render: 1,\n                      handle_event: 3,\n                      handle_async: 3\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/application.ex",
    "content": "defmodule Phoenix.LiveView.Application do\n  @moduledoc false\n\n  use Application\n\n  @impl true\n  def start(_type, _args) do\n    Phoenix.LiveView.Logger.install()\n    Supervisor.start_link([], strategy: :one_for_one, name: Phoenix.LiveView.Supervisor)\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/async.ex",
    "content": "defmodule Phoenix.LiveView.Async do\n  @moduledoc false\n\n  alias Phoenix.LiveView.{AsyncResult, Socket, Channel}\n\n  defp warn_socket_access(op, warn) do\n    warn.(\"\"\"\n    you are accessing the LiveView Socket inside a function given to #{op}.\n\n    This is an expensive operation because the whole socket is copied to the new process.\n\n    Instead of:\n\n        #{op}(socket, :key, fn ->\n          do_something(socket.assigns.my_assign)\n        end)\n\n    You should do:\n\n        my_assign = socket.assigns.my_assign\n\n        #{op}(socket, :key, fn ->\n          do_something(my_assign)\n        end)\n\n    For more information, see https://hexdocs.pm/elixir/1.16.1/process-anti-patterns.html#sending-unnecessary-data.\n    \"\"\")\n  end\n\n  # this is not private to prevent the unused function warning as we only\n  # call this function when enable_expensive_runtime_checks is set\n  def warn_assigns_access(op, warn) do\n    warn.(\"\"\"\n    you are accessing an assigns map inside a function given to #{op}.\n\n    This is an expensive operation because the whole map is copied to the new process.\n\n    Instead of:\n\n        #{op}(socket, :key, fn ->\n          do_something(assigns.my_assign)\n        end)\n\n    You should do:\n\n        my_assign = assigns.my_assign\n\n        #{op}(socket, :key, fn ->\n          do_something(my_assign)\n        end)\n\n    For more information, see https://hexdocs.pm/elixir/1.16.1/process-anti-patterns.html#sending-unnecessary-data.\n    \"\"\")\n  end\n\n  defp validate_function_env(func, op, env) do\n    # prevent false positives, for example\n    # start_async(socket, :foo, function_that_returns_the_anonymous_function(socket))\n    if match?({:&, _, _}, func) or match?({:fn, _, _}, func) do\n      Macro.prewalk(func, fn\n        {:socket, meta, _} ->\n          warn_socket_access(op, fn msg ->\n            meta = Keyword.take(meta, [:line, :column]) ++ [line: env.line, file: env.file]\n            IO.warn(msg, meta)\n          end)\n\n        other ->\n          other\n      end)\n    end\n\n    :ok\n  end\n\n  if Application.compile_env(:phoenix_live_view, :enable_expensive_runtime_checks, false) do\n    defp validate_function_env(func, op) do\n      {:env, variables} = Function.info(func, :env)\n\n      cond do\n        Enum.any?(variables, &match?(%Phoenix.LiveView.Socket{}, &1)) ->\n          warn_socket_access(op, fn msg -> IO.warn(msg) end)\n\n        Enum.any?(variables, &match?(%{__changed__: _}, &1)) ->\n          warn_assigns_access(op, fn msg -> IO.warn(msg) end)\n\n        true ->\n          :ok\n      end\n    end\n  else\n    defp validate_function_env(_func, _op), do: :ok\n  end\n\n  def start_async(socket, key, func, opts, env) do\n    validate_function_env(func, :start_async, env)\n\n    quote do\n      Phoenix.LiveView.Async.start_async(\n        unquote(socket),\n        unquote(key),\n        unquote(func),\n        unquote(opts)\n      )\n    end\n  end\n\n  def start_async(%Socket{} = socket, key, func, opts) when is_function(func, 0) do\n    # runtime check\n    if Phoenix.LiveView.connected?(socket) do\n      validate_function_env(func, :start_async)\n    end\n\n    run_async_task(socket, key, func, :start, opts)\n  end\n\n  def assign_async(socket, key_or_keys, func, opts, env) do\n    validate_function_env(func, :assign_async, env)\n\n    quote do\n      Phoenix.LiveView.Async.assign_async(\n        unquote(socket),\n        unquote(key_or_keys),\n        unquote(func),\n        unquote(opts)\n      )\n    end\n  end\n\n  def assign_async(%Socket{} = socket, key_or_keys, func, opts)\n      when (is_atom(key_or_keys) or is_list(key_or_keys)) and\n             is_function(func, 0) do\n    # runtime check\n    if Phoenix.LiveView.connected?(socket) do\n      validate_function_env(func, :assign_async)\n    end\n\n    keys = List.wrap(key_or_keys)\n\n    # verifies result inside task\n    wrapped_func = fn ->\n      case func.() do\n        {:ok, %{} = assigns} ->\n          if Enum.find(keys, &(not is_map_key(assigns, &1))) do\n            raise ArgumentError, \"\"\"\n            expected assign_async to return map of assigns for all keys\n            in #{inspect(keys)}, but got: #{inspect(assigns)}\n            \"\"\"\n          else\n            {:ok, assigns}\n          end\n\n        {:error, reason} ->\n          {:error, reason}\n\n        other ->\n          raise ArgumentError, \"\"\"\n          expected assign_async to return {:ok, map} of\n          assigns for #{inspect(keys)} or {:error, reason}, got: #{inspect(other)}\n          \"\"\"\n      end\n    end\n\n    reset = Keyword.get(opts, :reset, false)\n\n    new_assigns =\n      Enum.map(keys, fn key ->\n        reset = if is_list(reset), do: key in reset, else: reset\n\n        case {reset, socket.assigns} do\n          {false, %{^key => %AsyncResult{ok?: true} = existing}} ->\n            {key, AsyncResult.loading(existing, keys)}\n\n          _ ->\n            {key, AsyncResult.loading(keys)}\n        end\n      end)\n\n    socket\n    |> Phoenix.Component.assign(new_assigns)\n    |> run_async_task(keys, wrapped_func, :assign, opts)\n  end\n\n  def stream_async(socket, key, func, opts, env) do\n    validate_function_env(func, :stream_async, env)\n\n    quote do\n      Phoenix.LiveView.Async.stream_async(\n        unquote(socket),\n        unquote(key),\n        unquote(func),\n        unquote(opts)\n      )\n    end\n  end\n\n  def stream_async(%Socket{} = socket, key, func, opts)\n      when is_atom(key) and is_function(func, 0) do\n    # runtime check\n    if Phoenix.LiveView.connected?(socket) do\n      validate_function_env(func, :stream_async)\n    end\n\n    reset = Keyword.get(opts, :reset, false)\n\n    check_enumerable! = fn result ->\n      if Enumerable.impl_for(result) do\n        :ok\n      else\n        raise ArgumentError, \"\"\"\n        expected stream_async to return {:ok, Enumerable.t()}, {:ok, Enumerable.t(), options} or {:error, reason} but the result\n        is does not implement the Enumerable protocol\n        \"\"\"\n      end\n    end\n\n    # store opts for streaming\n    wrapped_func = fn ->\n      case func.() do\n        {:ok, result} ->\n          check_enumerable!.(result)\n          {:ok, result, []}\n\n        {:ok, result, opts} when is_list(opts) ->\n          check_enumerable!.(result)\n          {:ok, result, opts}\n\n        {:error, reason} ->\n          {:error, reason}\n\n        other ->\n          raise ArgumentError, \"\"\"\n          expected stream_async to return {:ok, Enumerable.t()} or {:error, reason}, got: #{inspect(other)}\n          \"\"\"\n      end\n    end\n\n    value =\n      case {reset, socket.assigns} do\n        {false, %{^key => %AsyncResult{ok?: true} = existing}} ->\n          AsyncResult.loading(existing)\n\n        _ ->\n          AsyncResult.loading()\n      end\n\n    socket\n    |> Phoenix.Component.assign(key, value)\n    |> maybe_init_stream(key)\n    |> run_async_task(key, wrapped_func, :stream, opts)\n  end\n\n  defp maybe_init_stream(socket, key) do\n    case socket.assigns do\n      %{streams: %{^key => _}} -> socket\n      _ -> Phoenix.LiveView.stream(socket, key, [])\n    end\n  end\n\n  def run_async_task(%Socket{} = socket, key, func, kind, opts) when is_function(func, 0) do\n    if Phoenix.LiveView.connected?(socket) do\n      lv_pid = self()\n      cid = cid(socket)\n\n      {:ok, pid} =\n        if supervisor = Keyword.get(opts, :supervisor) do\n          Task.Supervisor.start_child(supervisor, fn ->\n            Process.link(lv_pid)\n            do_async(lv_pid, cid, key, func, kind)\n          end)\n        else\n          Task.start_link(fn -> do_async(lv_pid, cid, key, func, kind) end)\n        end\n\n      ref =\n        :erlang.monitor(:process, pid, alias: :reply_demonitor, tag: {__MODULE__, key, cid, kind})\n\n      send(pid, {:context, ref})\n\n      update_private_async(socket, &Map.put(&1, key, {ref, pid, kind}))\n    else\n      socket\n    end\n  end\n\n  defp do_async(lv_pid, cid, key, func, async_kind) do\n    receive do\n      {:context, ref} ->\n        try do\n          result = func.()\n          Channel.report_async_result(ref, async_kind, ref, cid, key, {:ok, result})\n        catch\n          catch_kind, reason ->\n            caught_result = to_exit(catch_kind, reason, __STACKTRACE__)\n            Channel.report_async_result(ref, async_kind, ref, cid, key, caught_result)\n            :erlang.raise(catch_kind, reason, __STACKTRACE__)\n        after\n          Process.unlink(lv_pid)\n        end\n    end\n  end\n\n  def cancel_async(%Socket{} = socket, %AsyncResult{} = result, reason) do\n    case result do\n      %AsyncResult{loading: keys} when is_list(keys) ->\n        new_assigns = for key <- keys, do: {key, AsyncResult.failed(result, {:exit, reason})}\n\n        socket\n        |> Phoenix.Component.assign(new_assigns)\n        |> cancel_async(keys, reason)\n\n      %AsyncResult{} ->\n        socket\n    end\n  end\n\n  def cancel_async(%Socket{} = socket, key, reason) do\n    case get_private_async(socket, key) do\n      {_ref, pid, _kind} when is_pid(pid) ->\n        Process.unlink(pid)\n        Process.exit(pid, reason)\n        socket\n\n      nil ->\n        socket\n    end\n  end\n\n  def handle_async(socket, maybe_component, kind, key, ref, result) do\n    case prune_current_async(socket, key, ref) do\n      {:ok, pruned_socket} ->\n        handle_kind(pruned_socket, maybe_component, kind, key, result)\n\n      :error ->\n        socket\n    end\n  end\n\n  def handle_trap_exit(socket, maybe_component, kind, key, ref, reason) do\n    handle_async(socket, maybe_component, kind, key, ref, {:exit, reason})\n  end\n\n  defp handle_kind(socket, maybe_component, :start, key, result) do\n    callback_mod = maybe_component || socket.view\n\n    case Phoenix.LiveView.Lifecycle.handle_async(key, result, socket) do\n      {:cont, %Socket{} = socket} ->\n        case callback_mod.handle_async(key, result, socket) do\n          {:noreply, %Socket{} = new_socket} ->\n            new_socket\n\n          other ->\n            raise ArgumentError, \"\"\"\n            expected #{inspect(callback_mod)}.handle_async/3 to return {:noreply, socket}, got:\n\n                #{inspect(other)}\n            \"\"\"\n        end\n\n      {:halt, %Socket{} = socket} ->\n        socket\n    end\n  end\n\n  defp handle_kind(socket, _maybe_component, :assign, keys, result) do\n    case result do\n      {:ok, {:ok, assigns}} when is_map(assigns) or is_list(assigns) ->\n        new_assigns =\n          for {key, val} <- assigns do\n            {key, AsyncResult.ok(get_current_async!(socket, key), val)}\n          end\n\n        Phoenix.Component.assign(socket, new_assigns)\n\n      {:ok, {:error, reason}} ->\n        new_assigns =\n          for key <- keys do\n            {key, AsyncResult.failed(get_current_async!(socket, key), {:error, reason})}\n          end\n\n        Phoenix.Component.assign(socket, new_assigns)\n\n      {:exit, _reason} = normalized_exit ->\n        new_assigns =\n          for key <- keys do\n            {key, AsyncResult.failed(get_current_async!(socket, key), normalized_exit)}\n          end\n\n        Phoenix.Component.assign(socket, new_assigns)\n    end\n  end\n\n  defp handle_kind(socket, _maybe_component, :stream, key, result) do\n    case result do\n      {:ok, {:ok, stream, opts}} ->\n        socket\n        |> Phoenix.Component.assign(key, AsyncResult.ok(get_current_async!(socket, key), true))\n        |> Phoenix.LiveView.stream(key, stream, opts)\n\n      {:ok, {:error, reason}} ->\n        Phoenix.Component.assign(\n          socket,\n          key,\n          AsyncResult.failed(get_current_async!(socket, key), {:error, reason})\n        )\n\n      {:exit, _reason} = normalized_exit ->\n        Phoenix.Component.assign(\n          socket,\n          key,\n          AsyncResult.failed(get_current_async!(socket, key), normalized_exit)\n        )\n    end\n  end\n\n  # handle race of async being canceled and then reassigned\n  defp prune_current_async(socket, key, ref) do\n    case get_private_async(socket, key) do\n      {^ref, _pid, _kind} -> {:ok, update_private_async(socket, &Map.delete(&1, key))}\n      {_ref, _pid, _kind} -> :error\n      nil -> :error\n    end\n  end\n\n  defp update_private_async(%{private: private} = socket, func) do\n    existing = Map.get(private, :live_async, %{})\n    %{socket | private: Map.put(private, :live_async, func.(existing))}\n  end\n\n  defp get_private_async(%Socket{} = socket, key) do\n    socket.private[:live_async][key]\n  end\n\n  defp get_current_async!(socket, key) do\n    # handle case where assign is temporary and needs to be rebuilt\n    case socket.assigns do\n      %{^key => %AsyncResult{} = current_async} -> current_async\n      %{^key => _other} -> AsyncResult.loading(key)\n      %{} -> raise ArgumentError, \"missing async assign #{inspect(key)}\"\n    end\n  end\n\n  defp to_exit(:throw, reason, stack), do: {:exit, {{:nocatch, reason}, stack}}\n  defp to_exit(:error, reason, stack), do: {:exit, {reason, stack}}\n  defp to_exit(:exit, reason, _stack), do: {:exit, reason}\n\n  defp cid(%Socket{} = socket) do\n    if myself = socket.assigns[:myself], do: myself.cid\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/async_result.ex",
    "content": "defmodule Phoenix.LiveView.AsyncResult do\n  @moduledoc ~S'''\n  Provides a data structure for tracking the state of an async assign.\n\n  See the `Async Operations` section of the `Phoenix.LiveView` docs for more information.\n\n  ## Fields\n\n    * `:ok?` - When true, indicates the `:result` has been set successfully at least once.\n    * `:loading` - The current loading state\n    * `:failed` - The current failed state\n    * `:result` - The successful result of the async task\n  '''\n\n  defstruct ok?: false,\n            loading: nil,\n            failed: nil,\n            result: nil\n\n  alias Phoenix.LiveView.AsyncResult\n\n  @doc \"\"\"\n  Creates an async result in loading state.\n\n  ## Examples\n\n      iex> result = AsyncResult.loading()\n      iex> result.loading\n      true\n      iex> result.ok?\n      false\n\n  \"\"\"\n  def loading do\n    %AsyncResult{loading: true}\n  end\n\n  @doc \"\"\"\n  Updates the loading state.\n\n  When loading, the failed state will be reset to `nil`.\n\n  ## Examples\n\n      iex> result = AsyncResult.loading(%{my: :loading_state})\n      iex> result.loading\n      %{my: :loading_state}\n      iex> result = AsyncResult.loading(result)\n      iex> result.loading\n      true\n\n  \"\"\"\n  def loading(%AsyncResult{} = result) do\n    %{result | loading: true, failed: nil}\n  end\n\n  def loading(loading_state) do\n    %AsyncResult{loading: loading_state, failed: nil}\n  end\n\n  @doc \"\"\"\n  Updates the loading state of an existing `async_result`.\n\n  When loading, the failed state will be reset to `nil`.\n  If the result was previously `ok?`, both `result` and\n  `loading` will be set.\n\n  ## Examples\n\n      iex> result = AsyncResult.loading()\n      iex> result = AsyncResult.loading(result, %{my: :other_state})\n      iex> result.loading\n      %{my: :other_state}\n\n  \"\"\"\n  def loading(%AsyncResult{} = result, loading_state) do\n    %{result | loading: loading_state, failed: nil}\n  end\n\n  @doc \"\"\"\n  Updates the failed state.\n\n  When failed, the loading state will be reset to `nil`.\n  If the result was previously `ok?`, both `result` and\n  `failed` will be set.\n\n  ## Examples\n\n      iex> result = AsyncResult.loading()\n      iex> result = AsyncResult.failed(result, {:exit, :boom})\n      iex> result.failed\n      {:exit, :boom}\n      iex> result.loading\n      nil\n\n  \"\"\"\n  def failed(%AsyncResult{} = result, reason) do\n    %{result | failed: reason, loading: nil}\n  end\n\n  @doc \"\"\"\n  Creates a successful result.\n\n  The `:ok?` field will also be set to `true` to indicate this result has\n  completed successfully at least once, regardless of future state changes.\n\n  ### Examples\n\n      iex> result = AsyncResult.ok(\"initial result\")\n      iex> result.ok?\n      true\n      iex> result.result\n      \"initial result\"\n\n  \"\"\"\n  def ok(value) do\n    %AsyncResult{failed: nil, loading: nil, ok?: true, result: value}\n  end\n\n  @doc \"\"\"\n  Updates the successful result.\n\n  The `:ok?` field will also be set to `true` to indicate this result has\n  completed successfully at least once, regardless of future state changes.\n\n  When ok'd, the loading and failed state will be reset to `nil`.\n\n  ## Examples\n\n      iex> result = AsyncResult.loading()\n      iex> result = AsyncResult.ok(result, \"completed\")\n      iex> result.ok?\n      true\n      iex> result.result\n      \"completed\"\n      iex> result.loading\n      nil\n\n  \"\"\"\n  def ok(%AsyncResult{} = result, value) do\n    %{result | failed: nil, loading: nil, ok?: true, result: value}\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/channel.ex",
    "content": "defmodule Phoenix.LiveView.Channel do\n  @moduledoc false\n  use GenServer, restart: :temporary\n\n  require Logger\n\n  alias Phoenix.LiveView.{\n    Socket,\n    Utils,\n    Diff,\n    Upload,\n    UploadConfig,\n    Route,\n    Session,\n    Lifecycle,\n    Async\n  }\n\n  alias Phoenix.Socket.{Broadcast, Message}\n\n  @prefix :phoenix\n  @not_mounted_at_router :not_mounted_at_router\n  @max_host_size 253\n\n  def start_link({endpoint, from}) do\n    hibernate_after = endpoint.config(:live_view)[:hibernate_after] || 15000\n    opts = [hibernate_after: hibernate_after]\n    GenServer.start_link(__MODULE__, from, opts)\n  end\n\n  def send_update(pid, ref, assigns) do\n    send(pid, {@prefix, :send_update, {ref, assigns}})\n  end\n\n  def send_update_after(pid, ref, assigns, time_in_milliseconds)\n      when is_integer(time_in_milliseconds) do\n    Process.send_after(\n      pid,\n      {@prefix, :send_update, {ref, assigns}},\n      time_in_milliseconds\n    )\n  end\n\n  def report_async_result(monitor_ref, kind, ref, cid, keys, result)\n      when is_reference(monitor_ref) and kind in [:assign, :start, :stream] and is_reference(ref) do\n    send(monitor_ref, {@prefix, :async_result, {kind, {ref, cid, keys, result}}})\n  end\n\n  def async_pids(lv_pid) do\n    GenServer.call(lv_pid, {@prefix, :async_pids})\n  end\n\n  def ping(pid) do\n    GenServer.call(pid, {@prefix, :ping}, :infinity)\n  end\n\n  def register_upload(pid, {upload_config_ref, entry_ref} = _ref, cid) do\n    info = %{channel_pid: self(), ref: upload_config_ref, entry_ref: entry_ref, cid: cid}\n    GenServer.call(pid, {@prefix, :register_entry_upload, info})\n  end\n\n  def fetch_upload_config(pid, name, cid) do\n    GenServer.call(pid, {@prefix, :fetch_upload_config, name, cid})\n  end\n\n  def drop_upload_entries(%UploadConfig{} = conf, entry_refs) do\n    info = %{ref: conf.ref, entry_refs: entry_refs, cid: conf.cid}\n    send(self(), {@prefix, :drop_upload_entries, info})\n  end\n\n  def report_writer_error(pid, reason) do\n    channel_pid = self()\n    send(pid, {@prefix, :report_writer_error, channel_pid, reason})\n  end\n\n  @impl true\n  def init({pid, _ref}) do\n    {:ok, Process.monitor(pid)}\n  end\n\n  @impl true\n  def handle_info({Phoenix.Channel, auth_payload, from, phx_socket}, ref) do\n    Process.demonitor(ref)\n    mount(auth_payload, from, phx_socket)\n  rescue\n    # Normalize exceptions for better client debugging\n    e -> reraise(e, __STACKTRACE__)\n  end\n\n  def handle_info({:DOWN, ref, _, _, _reason}, ref) do\n    {:stop, {:shutdown, :closed}, ref}\n  end\n\n  def handle_info(\n        {:DOWN, _, _, transport_pid, _reason},\n        %{socket: %{transport_pid: transport_pid}} = state\n      ) do\n    {:stop, {:shutdown, :closed}, state}\n  end\n\n  def handle_info({:DOWN, _, _, parent, reason}, %{socket: %{parent_pid: parent}} = state) do\n    send(state.socket.transport_pid, {:socket_close, self(), reason})\n    {:stop, {:shutdown, :parent_exited}, state}\n  end\n\n  def handle_info({:DOWN, _, :process, pid, reason} = msg, %{socket: socket} = state) do\n    case Map.fetch(state.upload_pids, pid) do\n      {:ok, {ref, entry_ref, cid}} ->\n        if reason in [:normal, {:shutdown, :closed}] do\n          new_state =\n            state\n            |> drop_upload_pid(pid)\n            |> unregister_upload(ref, entry_ref, cid)\n\n          {:noreply, new_state}\n        else\n          {:stop, {:shutdown, {:channel_upload_exit, reason}}, state}\n        end\n\n      :error ->\n        msg\n        |> view_handle_info(socket)\n        |> handle_result({:handle_info, 2, nil}, state)\n    end\n  end\n\n  def handle_info(%Broadcast{event: \"phx_drain\"}, state) do\n    send(state.socket.transport_pid, :socket_drain)\n    {:stop, {:shutdown, :draining}, state}\n  end\n\n  def handle_info(%Message{topic: topic, event: \"phx_leave\"} = msg, %{topic: topic} = state) do\n    send(state.socket.transport_pid, {:socket_close, self(), {:shutdown, :left}})\n    reply(state, msg.ref, :ok, %{})\n    {:stop, {:shutdown, :left}, state}\n  end\n\n  def handle_info(%Message{topic: topic, event: \"live_patch\"} = msg, %{topic: topic} = state) do\n    %{socket: socket} = state\n    %{view: view} = socket\n    %{\"url\" => url} = msg.payload\n\n    case Route.live_link_info!(socket, view, url) do\n      {:internal, %Route{params: params, action: action}} ->\n        socket = socket |> assign_action(action) |> Utils.clear_flash()\n\n        socket\n        |> Utils.call_handle_params!(view, params, url)\n        |> handle_result({:handle_params, 3, msg.ref}, state)\n\n      {:external, _uri} ->\n        {:noreply, reply(state, msg.ref, :ok, %{link_redirect: true})}\n    end\n  end\n\n  def handle_info(\n        %Message{topic: topic, event: \"cids_will_destroy\"} = msg,\n        %{topic: topic} = state\n      ) do\n    %{\"cids\" => cids} = msg.payload\n\n    new_components =\n      Enum.reduce(cids, state.components, fn cid, acc ->\n        Diff.mark_for_deletion_component(cid, acc)\n      end)\n\n    {:noreply, reply(%{state | components: new_components}, msg.ref, :ok, %{})}\n  end\n\n  def handle_info(%Message{topic: topic, event: \"progress\"} = msg, %{topic: topic} = state) do\n    cid = msg.payload[\"cid\"]\n\n    new_state =\n      write_socket(state, cid, msg.ref, fn socket, _ ->\n        %{\"ref\" => ref, \"entry_ref\" => entry_ref, \"progress\" => progress} = msg.payload\n        new_socket = Upload.update_progress(socket, ref, entry_ref, progress)\n        upload_conf = Upload.get_upload_by_ref!(new_socket, ref)\n        entry = UploadConfig.get_entry_by_ref(upload_conf, entry_ref)\n\n        if event = entry && upload_conf.progress_event do\n          case event.(upload_conf.name, entry, new_socket) do\n            {:noreply, %Socket{} = new_socket} ->\n              new_socket =\n                if new_socket.redirected do\n                  flash = Utils.changed_flash(new_socket)\n                  send(new_socket.root_pid, {@prefix, :redirect, new_socket.redirected, flash})\n                  %{new_socket | redirected: nil}\n                else\n                  new_socket\n                end\n\n              {new_socket, {:ok, {msg.ref, %{}}, state}}\n\n            other ->\n              raise ArgumentError, \"\"\"\n              expected #{inspect(upload_conf.name)} upload progress #{inspect(event)} to return {:noreply, Socket.t()} got:\n\n                  #{inspect(other)}\n              \"\"\"\n          end\n        else\n          {new_socket, {:ok, {msg.ref, %{}}, state}}\n        end\n      end)\n\n    {:noreply, new_state}\n  end\n\n  def handle_info(%Message{topic: topic, event: \"allow_upload\"} = msg, %{topic: topic} = state) do\n    %{\"ref\" => upload_ref, \"entries\" => entries} = payload = msg.payload\n    cid = payload[\"cid\"]\n\n    new_state =\n      write_socket(state, cid, msg.ref, fn socket, _ ->\n        socket = Upload.register_cid(socket, upload_ref, cid)\n        conf = Upload.get_upload_by_ref!(socket, upload_ref)\n        ensure_unique_upload_name!(state, conf)\n\n        {ok_or_error, reply, %Socket{} = new_socket} =\n          with {:ok, new_socket} <- Upload.put_entries(socket, conf, entries, cid) do\n            refs = Enum.map(entries, fn %{\"ref\" => ref} -> ref end)\n            Upload.generate_preflight_response(new_socket, conf.name, cid, refs)\n          end\n\n        new_upload_names =\n          case ok_or_error do\n            :ok -> Map.put(state.upload_names, conf.name, {upload_ref, cid})\n            _ -> state.upload_names\n          end\n\n        {new_socket, {:ok, {msg.ref, reply}, %{state | upload_names: new_upload_names}}}\n      end)\n\n    {:noreply, new_state}\n  end\n\n  def handle_info(\n        %Message{topic: topic, event: \"cids_destroyed\"} = msg,\n        %{topic: topic} = state\n      ) do\n    %{\"cids\" => cids} = msg.payload\n    {deleted_cids, new_state} = delete_components(state, cids)\n    {:noreply, reply(new_state, msg.ref, :ok, %{cids: deleted_cids})}\n  end\n\n  def handle_info(%Message{topic: topic, event: \"event\"} = msg, %{topic: topic} = state) do\n    %{\"value\" => raw_val, \"event\" => event, \"type\" => type} = payload = msg.payload\n    val = decode_event_type(type, raw_val, msg.payload)\n\n    if cid = msg.payload[\"cid\"] do\n      component_handle(state, cid, msg.ref, fn component_socket, component ->\n        component_socket\n        |> maybe_update_uploads(payload)\n        |> inner_component_handle_event(component, event, val)\n      end)\n    else\n      new_state = %{state | socket: maybe_update_uploads(state.socket, msg.payload)}\n\n      new_state.socket\n      |> view_handle_event(event, val)\n      |> handle_result({:handle_event, 3, msg.ref}, new_state)\n    end\n  end\n\n  def handle_info({@prefix, :async_result, {kind, info}}, state) do\n    {ref, cid, keys, result} = info\n\n    if cid do\n      component_handle(state, cid, nil, fn component_socket, component ->\n        component_socket =\n          %Socket{redirected: redirected, assigns: assigns} =\n          Async.handle_async(component_socket, component, kind, keys, ref, result)\n\n        {component_socket, {redirected, assigns.flash}}\n      end)\n    else\n      new_socket = Async.handle_async(state.socket, nil, kind, keys, ref, result)\n\n      handle_result({:noreply, new_socket}, {:handle_async, 3, nil}, state)\n    end\n  end\n\n  def handle_info({@prefix, :drop_upload_entries, info}, state) do\n    %{ref: ref, cid: cid, entry_refs: entry_refs} = info\n\n    new_state =\n      write_socket(state, cid, nil, fn socket, _ ->\n        upload_config = Upload.get_upload_by_ref!(socket, ref)\n        {Upload.drop_upload_entries(socket, upload_config, entry_refs), {:ok, nil, state}}\n      end)\n\n    {:noreply, new_state}\n  end\n\n  def handle_info({@prefix, :report_writer_error, channel_pid, reason}, state) do\n    case state.upload_pids do\n      %{^channel_pid => {ref, entry_ref, cid}} ->\n        new_state =\n          write_socket(state, cid, nil, fn socket, _ ->\n            upload_config = Upload.get_upload_by_ref!(socket, ref)\n\n            new_socket =\n              Upload.put_upload_error(\n                socket,\n                upload_config.name,\n                entry_ref,\n                {:writer_failure, reason}\n              )\n\n            {new_socket, {:ok, nil, state}}\n          end)\n\n        {:noreply, new_state}\n\n      _ ->\n        {:noreply, state}\n    end\n  end\n\n  def handle_info({@prefix, :send_update, update}, state) do\n    case Diff.update_component(state.socket, state.components, update) do\n      {diff, new_components} ->\n        {:noreply, push_diff(%{state | components: new_components}, diff, nil)}\n\n      :noop ->\n        handle_noop(update)\n\n        {:noreply, state}\n    end\n  end\n\n  def handle_info({@prefix, :redirect, command, flash}, state) do\n    handle_redirect(state, command, flash, nil)\n  end\n\n  def handle_info({{Phoenix.LiveView.Async, keys, cid, kind}, ref, :process, _pid, reason}, state) do\n    new_state =\n      write_socket(state, cid, nil, fn socket, component ->\n        new_socket = Async.handle_trap_exit(socket, component, kind, keys, ref, reason)\n        {new_socket, {:ok, nil, state}}\n      end)\n\n    {:noreply, new_state}\n  end\n\n  def handle_info({:phoenix_live_reload, _topic, _changed_file}, %{socket: socket} = state) do\n    case socket.private[:phoenix_reloader] do\n      {mod, fun, args} -> apply(mod, fun, [socket.endpoint | args])\n      nil -> :noop\n    end\n\n    new_socket =\n      Enum.reduce(socket.assigns, socket, fn {key, val}, socket ->\n        Utils.force_assign(socket, key, val)\n      end)\n\n    handle_changed(state, new_socket, nil)\n  end\n\n  def handle_info(msg, %{socket: socket} = state) do\n    msg\n    |> view_handle_info(socket)\n    |> handle_result({:handle_info, 2, nil}, state)\n  end\n\n  defp handle_noop({%Phoenix.LiveComponent.CID{cid: cid}, _}) do\n    # Only a warning, because there can be race conditions where a component is removed before a `send_update` happens.\n    Logger.debug(\n      \"send_update failed because component with CID #{inspect(cid)} does not exist or it has been removed\"\n    )\n  end\n\n  defp handle_noop({{module, id}, _}) do\n    if exported?(module, :__info__, 1) do\n      # Only a warning, because there can be race conditions where a component is removed before a `send_update` happens.\n      Logger.debug(\n        \"send_update failed because component #{inspect(module)} with ID #{inspect(id)} does not exist or it has been removed\"\n      )\n    else\n      raise ArgumentError, \"send_update failed (module #{inspect(module)} is not available)\"\n    end\n  end\n\n  @impl true\n  def handle_call({@prefix, :ping}, _from, state) do\n    {:reply, :ok, state}\n  end\n\n  def handle_call({@prefix, :async_pids}, _from, state) do\n    pids = state |> all_asyncs() |> Map.keys()\n    {:reply, {:ok, pids}, state}\n  end\n\n  def handle_call({@prefix, :fetch_upload_config, name, cid}, _from, state) do\n    read_socket(state, cid, fn socket, _ ->\n      result =\n        with {:ok, uploads} <- Map.fetch(socket.assigns, :uploads),\n             do: Map.fetch(uploads, name)\n\n      {:reply, result, state}\n    end)\n  end\n\n  def handle_call({@prefix, :child_mount, _child_pid, assign_new}, _from, state) do\n    assigns = Map.take(state.socket.assigns, assign_new)\n    {:reply, {:ok, assigns}, state}\n  end\n\n  def handle_call({@prefix, :register_entry_upload, info}, from, state) do\n    {:noreply, register_entry_upload(state, from, info)}\n  end\n\n  # Phoenix.LiveView.Debug.socket/1\n  def handle_call({@prefix, :debug_get_socket}, _from, state) do\n    {:reply, {:ok, state.socket}, state}\n  end\n\n  # Phoenix.LiveView.Debug.live_components/1\n  def handle_call(\n        {@prefix, :debug_live_components},\n        _from,\n        %{components: {components, _, _}} = state\n      ) do\n    component_info =\n      Enum.map(components, fn {cid, {mod, id, assigns, private, _prints}} ->\n        %{id: id, cid: cid, module: mod, assigns: assigns, children_cids: private.children_cids}\n      end)\n\n    {:reply, {:ok, component_info}, state}\n  end\n\n  def handle_call(msg, from, %{socket: socket} = state) do\n    case socket.view.handle_call(msg, from, socket) do\n      {:reply, reply, %Socket{} = new_socket} ->\n        case handle_changed(state, new_socket, nil) do\n          {:noreply, new_state} -> {:reply, reply, new_state}\n          {:stop, reason, new_state} -> {:stop, reason, reply, new_state}\n        end\n\n      other ->\n        handle_result(other, {:handle_call, 3, nil}, state)\n    end\n  end\n\n  @impl true\n  def handle_cast(msg, %{socket: socket} = state) do\n    msg\n    |> socket.view.handle_cast(socket)\n    |> handle_result({:handle_cast, 2, nil}, state)\n  end\n\n  @impl true\n  def format_status(:terminate, [_pdict, state]) do\n    state\n  end\n\n  def format_status(:normal, [_pdict, %{} = state]) do\n    %{topic: topic, socket: socket, components: {cid_to_component, _, _}} = state\n    %Socket{view: view, parent_pid: parent_pid, transport_pid: transport_pid} = socket\n\n    [\n      data: [\n        {~c\"LiveView\", view},\n        {~c\"Parent pid\", parent_pid},\n        {~c\"Transport pid\", transport_pid},\n        {~c\"Topic\", topic},\n        {~c\"Components count\", map_size(cid_to_component)}\n      ]\n    ]\n  end\n\n  def format_status(_, [_pdict, state]) do\n    [data: [{~c\"State\", state}]]\n  end\n\n  @impl true\n  def terminate(reason, %{socket: socket}) do\n    %{view: view} = socket\n\n    if exported?(view, :terminate, 2) do\n      view.terminate(reason, socket)\n    else\n      :ok\n    end\n  end\n\n  def terminate(_reason, _state) do\n    :ok\n  end\n\n  @impl true\n  def code_change(old, %{socket: socket} = state, extra) do\n    %{view: view} = socket\n\n    if exported?(view, :code_change, 3) do\n      {:ok, socket} = view.code_change(old, socket, extra)\n      {:ok, %{state | socket: socket}}\n    else\n      {:ok, state}\n    end\n  end\n\n  defp view_handle_event(%Socket{} = socket, \"lv:clear-flash\", val) do\n    case val do\n      %{\"key\" => key} -> {:noreply, Utils.clear_flash(socket, key)}\n      _ -> {:noreply, Utils.clear_flash(socket)}\n    end\n  end\n\n  defp view_handle_event(%Socket{}, \"lv:\" <> _ = bad_event, _val) do\n    raise ArgumentError, \"\"\"\n    received unknown LiveView event #{inspect(bad_event)}.\n    The following LiveView events are supported: lv:clear-flash.\n    \"\"\"\n  end\n\n  defp view_handle_event(%Socket{} = socket, event, val) do\n    :telemetry.span(\n      [:phoenix, :live_view, :handle_event],\n      %{socket: socket, event: event, params: val},\n      fn ->\n        case Lifecycle.handle_event(event, val, socket) do\n          {:halt, %Socket{} = socket} ->\n            {{:noreply, socket}, %{socket: socket, event: event, params: val}}\n\n          {:halt, reply, %Socket{} = socket} ->\n            {{:reply, reply, socket}, %{socket: socket, event: event, params: val}}\n\n          {:cont, %Socket{} = socket} ->\n            case socket.view.handle_event(event, val, socket) do\n              {:noreply, %Socket{} = socket} ->\n                {{:noreply, socket}, %{socket: socket, event: event, params: val}}\n\n              {:reply, reply, %Socket{} = socket} ->\n                {{:reply, reply, socket}, %{socket: socket, event: event, params: val}}\n\n              other ->\n                raise_bad_callback_response!(other, socket.view, :handle_event, 3)\n            end\n        end\n      end\n    )\n  end\n\n  defp view_handle_info(msg, %{view: view} = socket) do\n    exported? = exported?(view, :handle_info, 2)\n\n    case Lifecycle.handle_info(msg, socket) do\n      {:cont, %Socket{} = socket} when exported? ->\n        view.handle_info(msg, socket)\n\n      {:cont, %Socket{} = socket} when not exported? ->\n        Logger.debug(\n          \"warning: undefined handle_info in #{inspect(view)}. Unhandled message: #{inspect(msg)}\"\n        )\n\n        {:noreply, socket}\n\n      {_, %Socket{} = socket} ->\n        {:noreply, socket}\n    end\n  end\n\n  defp exported?(m, f, a) do\n    function_exported?(m, f, a) or (Code.ensure_loaded?(m) and function_exported?(m, f, a))\n  end\n\n  defp maybe_call_mount_handle_params(%{socket: socket} = state, router, url, params) do\n    %{view: view, redirected: mount_redirect} = socket\n    lifecycle = Lifecycle.stage_info(socket, view, :handle_params, 3)\n\n    cond do\n      mount_redirect ->\n        mount_handle_params_result({:noreply, socket}, state, :mount)\n\n      not lifecycle.any? ->\n        {:diff, diff, new_state} = render_diff(state, socket, true)\n        {:ok, diff, :mount, new_state}\n\n      socket.root_pid != self() or is_nil(router) ->\n        # Let the callback fail for the usual reasons\n        Route.live_link_info!(%{socket | router: nil}, view, url)\n\n      params == @not_mounted_at_router ->\n        raise \"cannot invoke handle_params/3 for #{inspect(view)} because #{inspect(view)}\" <>\n                \" was not mounted at the router with the live/3 macro under URL #{inspect(url)}\"\n\n      true ->\n        socket\n        |> Utils.call_handle_params!(view, lifecycle.exported?, params, url)\n        |> mount_handle_params_result(state, :mount)\n    end\n  end\n\n  defp mount_handle_params_result({:noreply, %Socket{} = new_socket}, state, redir) do\n    new_state = %{state | socket: new_socket}\n\n    case maybe_diff(new_state, true) do\n      {:diff, diff, new_state} ->\n        {:ok, diff, redir, new_state}\n\n      {:redirect, %{to: _to} = opts} ->\n        {:redirect, copy_flash(new_state, Utils.get_flash(new_socket), opts), new_state}\n\n      {:redirect, %{external: url}} ->\n        {:redirect, copy_flash(new_state, Utils.get_flash(new_socket), %{to: url}), new_state}\n\n      {:live, :redirect, %{to: _to} = opts} ->\n        {:live_redirect, copy_flash(new_state, Utils.get_flash(new_socket), opts), new_state}\n\n      {:live, :patch, %{to: to} = opts} ->\n        {params, action} = patch_params_and_action!(new_socket, opts)\n\n        %{socket: new_socket} = new_state = drop_redirect(new_state)\n        uri = build_uri(new_state, to)\n\n        new_socket\n        |> assign_action(action)\n        |> Utils.call_handle_params!(new_socket.view, params, uri)\n        |> mount_handle_params_result(new_state, {:live_patch, opts})\n    end\n  end\n\n  defp handle_result(\n         {:reply, %{} = reply, %Socket{} = new_socket},\n         {:handle_event, 3, ref},\n         state\n       ) do\n    handle_changed(state, Utils.put_reply(new_socket, reply), ref)\n  end\n\n  defp handle_result({:noreply, %Socket{} = new_socket}, {_from, _arity, ref}, state) do\n    handle_changed(state, new_socket, ref)\n  end\n\n  defp handle_result(result, {name, arity, _ref}, state) do\n    raise_bad_callback_response!(result, state.socket.view, name, arity)\n  end\n\n  defp raise_bad_callback_response!(result, view, :handle_call, 3) do\n    raise ArgumentError, \"\"\"\n    invalid noreply from #{inspect(view)}.handle_call/3 callback.\n\n    Expected one of:\n\n        {:noreply, %Socket{}}\n        {:reply, map, %Socket{}}\n\n    Got: #{inspect(result)}\n    \"\"\"\n  end\n\n  defp raise_bad_callback_response!(result, view, :handle_event, arity) do\n    raise ArgumentError, \"\"\"\n    invalid return from #{inspect(view)}.handle_event/#{arity} callback.\n\n    Expected one of:\n\n        {:noreply, %Socket{}}\n        {:reply, map, %Socket{}}\n\n    Got: #{inspect(result)}\n    \"\"\"\n  end\n\n  defp raise_bad_callback_response!(result, view, name, arity) do\n    raise ArgumentError, \"\"\"\n    invalid noreply from #{inspect(view)}.#{name}/#{arity} callback.\n\n    Expected one of:\n\n        {:noreply, %Socket{}}\n\n    Got: #{inspect(result)}\n    \"\"\"\n  end\n\n  defp component_handle(state, cid, ref, fun) do\n    %{socket: socket, components: components} = state\n\n    # Due to race conditions, the browser can send a request for a\n    # component ID that no longer exists. So we need to check for\n    # the :error case accordingly.\n    case Diff.write_component(socket, cid, components, fun) do\n      {diff, new_components, {redirected, flash}} ->\n        new_state = %{state | components: new_components}\n\n        # If there is a redirect, we don't send the ack (the ref) with the\n        # component diff, because otherwise the user may see transient\n        # state (such as the component unlocking refs just to be\n        # removed). The ref is sent with the redirect.\n        if redirected do\n          new_state\n          |> push_diff(diff, nil)\n          |> handle_redirect(redirected, flash, ref)\n        else\n          {:noreply, push_diff(new_state, diff, ref)}\n        end\n\n      :error ->\n        {:noreply, push_noop(state, ref)}\n    end\n  end\n\n  defp unregister_upload(state, ref, entry_ref, cid) do\n    write_socket(state, cid, nil, fn socket, _ ->\n      conf = Upload.get_upload_by_ref!(socket, ref)\n\n      new_state =\n        case conf.entries do\n          [_] -> drop_upload_name(state, conf.name)\n          _ -> state\n        end\n\n      {Upload.unregister_completed_entry_upload(socket, conf, entry_ref), {:ok, nil, new_state}}\n    end)\n  end\n\n  defp put_upload_pid(state, pid, ref, entry_ref, cid) when is_pid(pid) do\n    Process.monitor(pid)\n    %{state | upload_pids: Map.put(state.upload_pids, pid, {ref, entry_ref, cid})}\n  end\n\n  defp drop_upload_pid(state, pid) when is_pid(pid) do\n    %{state | upload_pids: Map.delete(state.upload_pids, pid)}\n  end\n\n  defp drop_upload_name(state, name) do\n    {_, new_state} = pop_in(state.upload_names[name])\n    new_state\n  end\n\n  defp inner_component_handle_event(component_socket, _component, \"lv:clear-flash\", val) do\n    component_socket =\n      case val do\n        %{\"key\" => key} -> Utils.clear_flash(component_socket, key)\n        _ -> Utils.clear_flash(component_socket)\n      end\n\n    {component_socket, {nil, %{}}}\n  end\n\n  defp inner_component_handle_event(_component_socket, _component, \"lv:\" <> _ = bad_event, _val) do\n    raise ArgumentError, \"\"\"\n    received unknown LiveView event #{inspect(bad_event)}.\n    The following LiveView events are supported: lv:clear-flash.\n    \"\"\"\n  end\n\n  defp inner_component_handle_event(component_socket, component, event, val) do\n    :telemetry.span(\n      [:phoenix, :live_component, :handle_event],\n      %{socket: component_socket, component: component, event: event, params: val},\n      fn ->\n        component_socket =\n          %Socket{redirected: redirected, assigns: assigns} =\n          case Lifecycle.handle_event(event, val, component_socket) do\n            {:halt, %Socket{} = component_socket} ->\n              component_socket\n\n            {:halt, %{} = reply, %Socket{} = component_socket} ->\n              Utils.put_reply(component_socket, reply)\n\n            {:cont, %Socket{} = component_socket} ->\n              case component.handle_event(event, val, component_socket) do\n                {:noreply, component_socket} ->\n                  component_socket\n\n                {:reply, %{} = reply, component_socket} ->\n                  Utils.put_reply(component_socket, reply)\n\n                other ->\n                  raise ArgumentError, \"\"\"\n                  invalid return from #{inspect(component)}.handle_event/3 callback.\n\n                  Expected one of:\n\n                      {:noreply, %Socket{}}\n                      {:reply, map, %Socket}\n\n                  Got: #{inspect(other)}\n                  \"\"\"\n              end\n\n            other ->\n              raise_bad_callback_response!(other, component_socket.view, :handle_event, 3)\n          end\n\n        new_component_socket =\n          if redirected do\n            Utils.clear_flash(component_socket)\n          else\n            component_socket\n          end\n\n        {\n          {new_component_socket, {redirected, assigns.flash}},\n          %{socket: new_component_socket, component: component, event: event, params: val}\n        }\n      end\n    )\n  end\n\n  defp decode_event_type(\"form\", url_encoded, raw_payload) do\n    url_encoded\n    |> Plug.Conn.Query.decode()\n    |> maybe_merge_meta(raw_payload)\n    |> decode_merge_target()\n  end\n\n  defp decode_event_type(_, value, _raw_payload), do: value\n\n  defp decode_merge_target(%{\"_target\" => target} = params) when is_list(target), do: params\n\n  defp decode_merge_target(%{\"_target\" => target} = params) when is_binary(target) do\n    keyspace = target |> Plug.Conn.Query.decode() |> gather_keys([])\n    Map.put(params, \"_target\", Enum.reverse(keyspace))\n  end\n\n  defp decode_merge_target(%{} = params), do: params\n\n  defp maybe_merge_meta(value, %{\"meta\" => meta}) when is_map(value) do\n    Map.merge(value, meta)\n  end\n\n  defp maybe_merge_meta(value, _raw_payload), do: value\n\n  defp gather_keys(%{} = map, acc) do\n    case Enum.at(map, 0) do\n      {key, val} -> gather_keys(val, [key | acc])\n      nil -> acc\n    end\n  end\n\n  defp gather_keys([], acc), do: acc\n  defp gather_keys([%{} = map], acc), do: gather_keys(map, acc)\n  defp gather_keys(_, acc), do: acc\n\n  defp handle_changed(state, %Socket{} = new_socket, ref, pending_live_patch \\\\ nil) do\n    new_state = %{state | socket: new_socket}\n\n    case maybe_diff(new_state, false) do\n      {:diff, diff, new_state} ->\n        {:noreply,\n         new_state\n         |> clear_live_patch_counter()\n         |> push_live_patch(pending_live_patch)\n         |> push_diff(diff, ref)}\n\n      result ->\n        handle_redirect(new_state, result, Utils.changed_flash(new_socket), ref)\n    end\n  end\n\n  defp check_patch_redirect_limit!(state) do\n    current = state.redirect_count\n\n    if current == 20 do\n      raise RuntimeError, \"\"\"\n      too many redirects for #{inspect(state.socket.view)} on action #{inspect(state.socket.assigns.live_action)}\n\n      Check the `handle_params/3` callback for an infinite patch redirect loop\n      \"\"\"\n    else\n      %{state | redirect_count: current + 1}\n    end\n  end\n\n  defp clear_live_patch_counter(state) do\n    %{state | redirect_count: 0}\n  end\n\n  defp handle_redirect(new_state, result, flash, ref) do\n    %{socket: new_socket} = new_state\n    root_pid = new_socket.root_pid\n\n    case result do\n      {:redirect, %{external: to} = opts} ->\n        opts =\n          copy_flash(new_state, flash, opts)\n          |> Map.delete(:external)\n          |> Map.put(:to, to)\n\n        new_state\n        |> push_pending_events_on_redirect(new_socket)\n        |> push_redirect(opts, ref)\n        |> stop_shutdown_redirect(:redirect, opts)\n\n      {:redirect, %{to: _to} = opts} ->\n        opts = copy_flash(new_state, flash, opts)\n\n        new_state\n        |> push_pending_events_on_redirect(new_socket)\n        |> push_redirect(opts, ref)\n        |> stop_shutdown_redirect(:redirect, opts)\n\n      {:live, :redirect, %{to: _to} = opts} ->\n        opts = copy_flash(new_state, flash, opts)\n\n        new_state\n        |> push_pending_events_on_redirect(new_socket)\n        |> push_live_redirect(opts, ref)\n        |> then(fn state ->\n          if new_socket.sticky? do\n            {:noreply, drop_redirect(state)}\n          else\n            stop_shutdown_redirect(state, :live_redirect, opts)\n          end\n        end)\n\n      {:live, :patch, %{to: _to, kind: _kind} = opts} when root_pid == self() ->\n        {params, action} = patch_params_and_action!(new_socket, opts)\n\n        new_state\n        |> drop_redirect()\n        |> check_patch_redirect_limit!()\n        |> Map.update!(:socket, &Utils.replace_flash(&1, flash))\n        |> sync_handle_params_with_live_redirect(params, action, opts, ref)\n\n      {:live, :patch, %{to: _to, kind: _kind}} = patch ->\n        send(new_socket.root_pid, {@prefix, :redirect, patch, flash})\n        {:diff, diff, new_state} = render_diff(new_state, new_socket, false)\n\n        {:noreply,\n         new_state\n         |> drop_redirect()\n         |> push_diff(diff, ref)}\n    end\n  end\n\n  defp push_pending_events_on_redirect(state, socket) do\n    if diff = Diff.get_push_events_diff(socket), do: push_diff(state, diff, nil)\n    state\n  end\n\n  defp patch_params_and_action!(socket, %{to: to}) do\n    destructure [path, query], :binary.split(to, [\"?\", \"#\"], [:global])\n    to = %{socket.host_uri | path: path, query: query}\n\n    case Route.live_link_info!(socket, socket.private.root_view, to) do\n      {:internal, %Route{params: params, action: action}} ->\n        {params, action}\n\n      {:external, _uri} ->\n        raise ArgumentError,\n              \"cannot push_patch/2 to #{inspect(to)} because the given path \" <>\n                \"does not point to the current root view #{inspect(socket.private.root_view)}\"\n    end\n  end\n\n  defp stop_shutdown_redirect(state, kind, opts) do\n    send(state.socket.transport_pid, {:socket_close, self(), {kind, opts}})\n    {:stop, {:shutdown, {kind, opts}}, state}\n  end\n\n  defp drop_redirect(state) do\n    put_in(state.socket.redirected, nil)\n  end\n\n  defp sync_handle_params_with_live_redirect(state, params, action, %{to: to} = opts, ref) do\n    %{socket: socket} = state\n\n    {:noreply, %Socket{} = new_socket} =\n      socket\n      |> assign_action(action)\n      |> Utils.call_handle_params!(socket.view, params, build_uri(state, to))\n\n    handle_changed(state, new_socket, ref, opts)\n  end\n\n  defp push_live_patch(state, nil), do: state\n  defp push_live_patch(state, opts), do: push(state, \"live_patch\", opts)\n\n  defp push_redirect(state, opts, nil = _ref) do\n    push(state, \"redirect\", opts)\n  end\n\n  defp push_redirect(state, opts, ref) do\n    reply(state, ref, :ok, %{redirect: opts})\n  end\n\n  defp push_live_redirect(state, opts, nil = _ref) do\n    push(state, \"live_redirect\", opts)\n  end\n\n  defp push_live_redirect(state, opts, ref) do\n    reply(state, ref, :ok, %{live_redirect: opts})\n  end\n\n  defp push_noop(state, nil = _ref), do: state\n  defp push_noop(state, ref), do: reply(state, ref, :ok, %{})\n\n  defp push_diff(state, diff, ref) when diff == %{}, do: push_noop(state, ref)\n  defp push_diff(state, diff, nil = _ref), do: push(state, \"diff\", diff)\n  defp push_diff(state, diff, ref), do: reply(state, ref, :ok, %{diff: diff})\n\n  defp copy_flash(_state, flash, opts) when flash == %{},\n    do: opts\n\n  defp copy_flash(state, flash, opts),\n    do: Map.put(opts, :flash, Utils.sign_flash(state.socket.endpoint, flash))\n\n  defp maybe_diff(%{socket: socket} = state, force?) do\n    socket.redirected || render_diff(state, socket, force?)\n  end\n\n  defp render_diff(state, socket, force?) do\n    changed? = Utils.changed?(socket)\n\n    {socket, diff, fingerprints, components} =\n      if force? or changed? do\n        :telemetry.span(\n          [:phoenix, :live_view, :render],\n          %{socket: socket, force?: force?, changed?: changed?},\n          fn ->\n            rendered = Phoenix.LiveView.Renderer.to_rendered(socket, socket.view)\n\n            {diff, fingerprints, components} =\n              Diff.render(socket, rendered, state.fingerprints, state.components)\n\n            socket =\n              socket\n              |> Lifecycle.after_render()\n              |> Utils.clear_changed()\n\n            {\n              {socket, diff, fingerprints, components},\n              %{socket: socket, force?: force?, changed?: changed?}\n            }\n          end\n        )\n      else\n        {socket, %{}, state.fingerprints, state.components}\n      end\n\n    diff = Diff.render_private(socket, diff)\n    new_socket = Utils.clear_temp(socket)\n\n    {:diff, diff,\n     %{state | socket: new_socket, fingerprints: fingerprints, components: components}}\n  end\n\n  defp reply(state, {ref, extra}, status, payload) do\n    reply(state, ref, status, Map.merge(payload, extra))\n  end\n\n  defp reply(state, ref, status, payload) when is_binary(ref) do\n    reply_ref = {state.socket.transport_pid, state.serializer, state.topic, ref, state.join_ref}\n    Phoenix.Channel.reply(reply_ref, {status, payload})\n    state\n  end\n\n  defp push(state, event, payload) do\n    message = %Message{\n      topic: state.topic,\n      event: event,\n      payload: payload,\n      join_ref: state.join_ref\n    }\n\n    send(state.socket.transport_pid, state.serializer.encode!(message))\n    state\n  end\n\n  ## Mount\n\n  defp mount(%{\"session\" => session_token} = params, from, phx_socket) do\n    %Phoenix.Socket{endpoint: endpoint, topic: topic} = phx_socket\n\n    case Session.verify_session(endpoint, topic, session_token, params[\"static\"]) do\n      {:ok, %Session{} = verified} ->\n        %Phoenix.Socket{private: %{connect_info: connect_info}} = phx_socket\n\n        case connect_info do\n          %{session: nil} ->\n            Logger.debug(\"\"\"\n            LiveView session was misconfigured or the user token is outdated.\n\n            1) Ensure your session configuration in your endpoint is in a module attribute:\n\n                @session_options [\n                  ...\n                ]\n\n            2) Change the `plug Plug.Session` to use said attribute:\n\n                plug Plug.Session, @session_options\n\n            3) Also pass the `@session_options` to your LiveView socket:\n\n                socket \"/live\", Phoenix.LiveView.Socket,\n                  websocket: [connect_info: [session: @session_options]]\n\n            4) Ensure the `protect_from_forgery` plug is in your router pipeline:\n\n                plug :protect_from_forgery\n\n            5) Define the CSRF meta tag inside the `<head>` tag in your layout:\n\n                <meta name=\"csrf-token\" content={Plug.CSRFProtection.get_csrf_token()} />\n\n            6) Pass it forward in your app.js:\n\n                let csrfToken = document.querySelector(\"meta[name='csrf-token']\").getAttribute(\"content\");\n                let liveSocket = new LiveSocket(\"/live\", Socket, {params: {_csrf_token: csrfToken}});\n            \"\"\")\n\n            GenServer.reply(from, {:error, %{reason: \"stale\"}})\n            {:stop, :shutdown, :no_state}\n\n          %{} ->\n            with {:ok, %Session{view: view} = new_verified, route, url} <-\n                   authorize_session(verified, endpoint, params),\n                 {:ok, config} <- load_live_view(view) do\n              # TODO: replace with Process.put_label/2 when we require Elixir 1.17\n              Process.put(:\"$process_label\", {Phoenix.LiveView, view, phx_socket.topic})\n              Process.put(:\"$phx_transport_pid\", phx_socket.transport_pid)\n\n              verified_mount(\n                new_verified,\n                config,\n                route,\n                url,\n                params,\n                from,\n                phx_socket,\n                connect_info\n              )\n            else\n              {:error, :unauthorized} ->\n                GenServer.reply(from, {:error, %{reason: \"unauthorized\"}})\n                {:stop, :shutdown, :no_state}\n\n              {:error, _reason} ->\n                GenServer.reply(from, {:error, %{reason: \"stale\"}})\n                {:stop, :shutdown, :no_state}\n            end\n        end\n\n      {:error, _reason} ->\n        GenServer.reply(from, {:error, %{reason: \"stale\"}})\n        {:stop, :shutdown, :no_state}\n    end\n  end\n\n  defp mount(%{}, from, phx_socket) do\n    Logger.error(\"Mounting #{phx_socket.topic} failed because no session was provided\")\n    GenServer.reply(from, {:error, %{reason: \"stale\"}})\n    {:stop, :shutdown, :no_session}\n  end\n\n  defp load_live_view(view) do\n    # Make sure the view is loaded. Otherwise if the first request\n    # ever is a LiveView connection, the view won't be loaded and\n    # the mount/handle_params callbacks won't be invoked as they\n    # are optional, leading to errors.\n    {:ok, view.__live__()}\n  rescue\n    # If it fails, then the only possible answer is that the live\n    # view has been renamed. So we force the client to reconnect.\n    _ -> {:error, :stale}\n  end\n\n  defp verified_mount(\n         %Session{} = verified,\n         config,\n         route,\n         url,\n         params,\n         from,\n         phx_socket,\n         connect_info\n       ) do\n    %Session{\n      id: id,\n      view: view,\n      parent_pid: parent,\n      root_pid: root_pid,\n      session: verified_user_session,\n      router: router\n    } = verified\n\n    %Phoenix.Socket{\n      endpoint: endpoint,\n      transport_pid: transport_pid\n    } = phx_socket\n\n    Process.put(:\"$initial_call\", {view, :mount, 3})\n\n    case params do\n      %{\"caller\" => {pid, _}} when is_pid(pid) -> Process.put(:\"$callers\", [pid])\n      _ -> Process.put(:\"$callers\", [transport_pid])\n    end\n\n    # Optional parameter handling\n    connect_params = params[\"params\"]\n\n    # Optional verified parts\n    flash = verify_flash(endpoint, verified, params[\"flash\"], connect_params)\n\n    # connect_info is either a Plug.Conn during tests or a Phoenix.Socket map\n    socket_session = Map.get(connect_info, :session, %{})\n\n    Process.monitor(transport_pid)\n    load_csrf_token(endpoint, socket_session)\n\n    socket = %Socket{\n      endpoint: endpoint,\n      view: view,\n      transport_pid: transport_pid,\n      parent_pid: parent,\n      root_pid: root_pid || self(),\n      id: id,\n      router: router,\n      sticky?: params[\"sticky\"]\n    }\n\n    {params, host_uri, action} =\n      case route do\n        %Route{uri: %URI{host: host}} = route when byte_size(host) <= @max_host_size ->\n          {route.params, route.uri, route.action}\n\n        nil ->\n          {@not_mounted_at_router, @not_mounted_at_router, nil}\n      end\n\n    merged_session = Map.merge(socket_session, verified_user_session)\n    lifecycle = load_lifecycle(config, route)\n\n    case mount_private(verified, connect_params, connect_info, lifecycle) do\n      {:ok, mount_priv} ->\n        socket = Utils.configure_socket(socket, mount_priv, action, flash, host_uri)\n\n        try do\n          socket\n          |> load_layout(route)\n          |> Utils.maybe_call_live_view_mount!(view, params, merged_session, url)\n          |> build_state(phx_socket)\n          |> maybe_call_mount_handle_params(router, url, params)\n          |> reply_mount(from, verified, route)\n          |> maybe_subscribe_to_live_reload()\n        rescue\n          exception ->\n            status = Plug.Exception.status(exception)\n\n            if status >= 400 and status < 500 do\n              # only forward the stack and exception module to the signed cookie if debug_errors is enabled\n              # which already exposes the stacktrace and exception information to the client\n              {exception_mod, stack} =\n                if endpoint.config(:debug_errors) do\n                  {inspect(exception.__struct__), __STACKTRACE__}\n                else\n                  {nil, []}\n                end\n\n              token =\n                Phoenix.LiveView.Static.sign_token(endpoint, %{\n                  status: status,\n                  view: inspect(view),\n                  exception: exception_mod,\n                  stack: stack\n                })\n\n              GenServer.reply(from, {:error, %{reason: \"reload\", status: status, token: token}})\n              {:stop, :shutdown, :no_state}\n            else\n              reraise(exception, __STACKTRACE__)\n            end\n        end\n\n      {:error, :noproc} ->\n        GenServer.reply(from, {:error, %{reason: \"stale\"}})\n        {:stop, :shutdown, :no_state}\n    end\n  end\n\n  defp verify_flash(endpoint, %Session{} = verified, flash_token, connect_params) do\n    cond do\n      # flash_token is given by the client on live_redirects and has higher priority.\n      flash_token ->\n        Utils.verify_flash(endpoint, flash_token)\n\n      # verified.flash comes from the disconnected render, therefore we only want\n      # to load it we are not inside a live redirect and if it is our first mount.\n      not verified.redirected? && connect_params[\"_mounts\"] == 0 && verified.flash ->\n        verified.flash\n\n      true ->\n        %{}\n    end\n  end\n\n  defp load_csrf_token(endpoint, socket_session) do\n    if token = socket_session[\"_csrf_token\"] do\n      state = Plug.CSRFProtection.dump_state_from_session(token)\n      secret_key_base = endpoint.config(:secret_key_base)\n      Plug.CSRFProtection.load_state(secret_key_base, state)\n    end\n  end\n\n  defp load_lifecycle(\n         %{lifecycle: lifecycle},\n         %Route{live_session: %{extra: %{on_mount: on_mount}}}\n       ) do\n    update_in(lifecycle.mount, &(on_mount ++ &1))\n  end\n\n  defp load_lifecycle(%{lifecycle: lifecycle}, _) do\n    lifecycle\n  end\n\n  defp load_layout(socket, %Route{live_session: %{extra: %{layout: layout}}}) do\n    put_in(socket.private[:live_layout], layout)\n  end\n\n  defp load_layout(socket, _route) do\n    socket\n  end\n\n  defp mount_private(%Session{parent_pid: nil} = session, connect_params, connect_info, lifecycle) do\n    %{\n      root_view: root_view,\n      assign_new: assign_new,\n      live_session_name: live_session_name\n    } = session\n\n    {:ok,\n     %{\n       connect_params: connect_params,\n       connect_info: connect_info,\n       assign_new: {%{}, assign_new},\n       lifecycle: lifecycle,\n       root_view: root_view,\n       live_temp: %{},\n       live_session_name: live_session_name\n     }}\n  end\n\n  defp mount_private(\n         %Session{parent_pid: parent} = session,\n         connect_params,\n         connect_info,\n         lifecycle\n       ) do\n    %{\n      root_view: root_view,\n      assign_new: assign_new,\n      live_session_name: live_session_name\n    } = session\n\n    case sync_with_parent(parent, assign_new) do\n      {:ok, parent_assigns} ->\n        # Child live views always ignore the layout on `:use`.\n        {:ok,\n         %{\n           connect_params: connect_params,\n           connect_info: connect_info,\n           assign_new: {parent_assigns, assign_new},\n           live_layout: false,\n           lifecycle: lifecycle,\n           root_view: root_view,\n           live_temp: %{},\n           live_session_name: live_session_name\n         }}\n\n      {:error, :noproc} ->\n        {:error, :noproc}\n    end\n  end\n\n  defp sync_with_parent(parent, assign_new) do\n    try do\n      GenServer.call(parent, {@prefix, :child_mount, self(), assign_new})\n    catch\n      :exit, {:noproc, _} -> {:error, :noproc}\n    end\n  end\n\n  defp put_container(%Session{} = session, %Route{} = route, %{} = diff) do\n    if container = session.redirected? && Route.container(route) do\n      {tag, attrs} = container\n\n      attrs = attrs |> resolve_class_attribute_as_list() |> Enum.into(%{})\n\n      Map.put(diff, :container, [tag, attrs])\n    else\n      diff\n    end\n  end\n\n  defp put_container(%Session{}, nil = _route, %{} = diff), do: diff\n\n  defp resolve_class_attribute_as_list(attrs) do\n    case attrs[:class] do\n      c when is_list(c) -> Keyword.put(attrs, :class, Enum.join(c, \" \"))\n      _ -> attrs\n    end\n  end\n\n  defp reply_mount(result, from, %Session{} = session, route) do\n    lv_vsn = to_string(Application.spec(:phoenix_live_view)[:vsn])\n\n    case result do\n      {:ok, diff, :mount, new_state} ->\n        diff = maybe_put_debug_pid(%{rendered: diff, liveview_version: lv_vsn})\n        reply = put_container(session, route, diff)\n        GenServer.reply(from, {:ok, reply})\n        {:noreply, post_verified_mount(new_state)}\n\n      {:ok, diff, {:live_patch, opts}, new_state} ->\n        reply =\n          put_container(session, route, %{\n            rendered: diff,\n            live_patch: opts,\n            liveview_version: lv_vsn\n          })\n\n        GenServer.reply(from, {:ok, reply})\n        {:noreply, post_verified_mount(new_state)}\n\n      {:live_redirect, opts, new_state} ->\n        GenServer.reply(from, {:error, %{live_redirect: opts}})\n        {:stop, :shutdown, new_state}\n\n      {:redirect, opts, new_state} ->\n        GenServer.reply(from, {:error, %{redirect: opts}})\n        {:stop, :shutdown, new_state}\n    end\n  end\n\n  defp maybe_put_debug_pid(diff) do\n    if Application.get_env(:phoenix_live_view, :debug_attributes, false) do\n      Map.put(diff, :pid, inspect(self()))\n    else\n      diff\n    end\n  end\n\n  defp build_state(%Socket{} = lv_socket, %Phoenix.Socket{} = phx_socket) do\n    %{\n      join_ref: phx_socket.join_ref,\n      serializer: phx_socket.serializer,\n      socket: lv_socket,\n      topic: phx_socket.topic,\n      components: Diff.new_components(),\n      fingerprints: Diff.new_fingerprints(),\n      redirect_count: 0,\n      upload_names: %{},\n      upload_pids: %{}\n    }\n  end\n\n  defp build_uri(%{socket: socket}, \"/\" <> _ = to) do\n    URI.to_string(%{socket.host_uri | path: to})\n  end\n\n  defp post_verified_mount(%{socket: socket} = state) do\n    %{state | socket: Utils.post_mount_prune(socket)}\n  end\n\n  defp assign_action(socket, action) do\n    Phoenix.LiveView.Utils.assign(socket, :live_action, action)\n  end\n\n  defp maybe_update_uploads(%Socket{} = socket, %{\"uploads\" => uploads} = payload) do\n    cid = payload[\"cid\"]\n\n    Enum.reduce(uploads, socket, fn {ref, entries}, acc ->\n      upload_conf = Upload.get_upload_by_ref!(acc, ref)\n\n      case Upload.put_entries(acc, upload_conf, entries, cid) do\n        {:ok, new_socket} -> new_socket\n        {:error, _error_resp, %Socket{} = new_socket} -> new_socket\n      end\n    end)\n  end\n\n  defp maybe_update_uploads(%Socket{} = socket, %{} = _payload), do: socket\n\n  defp register_entry_upload(state, from, info) do\n    %{channel_pid: pid, ref: ref, entry_ref: entry_ref, cid: cid} = info\n\n    write_socket(state, cid, nil, fn socket, _ ->\n      conf = Upload.get_upload_by_ref!(socket, ref)\n\n      case Upload.register_entry_upload(socket, conf, pid, entry_ref) do\n        {:ok, new_socket, entry} ->\n          reply = %{\n            max_file_size: entry.client_size,\n            chunk_timeout: conf.chunk_timeout,\n            writer: writer!(socket, conf.name, entry, conf.writer)\n          }\n\n          GenServer.reply(from, {:ok, reply})\n          new_state = put_upload_pid(state, pid, ref, entry_ref, cid)\n          {new_socket, {:ok, nil, new_state}}\n\n        {:error, reason} ->\n          GenServer.reply(from, {:error, reason})\n          {socket, :error}\n      end\n    end)\n  end\n\n  defp writer!(socket, name, entry, writer) do\n    case writer.(name, entry, socket) do\n      {mod, opts} when is_atom(mod) ->\n        {mod, opts}\n\n      other ->\n        raise \"\"\"\n        expected :writer function to return a tuple of {module, opts}, got: #{inspect(other)}\n        \"\"\"\n    end\n  end\n\n  defp read_socket(state, nil = _cid, func) do\n    func.(state.socket, nil)\n  end\n\n  defp read_socket(state, cid, func) do\n    %{socket: socket, components: components} = state\n    Diff.read_component(socket, cid, components, func)\n  end\n\n  # If :error is returned, the socket must not change,\n  # otherwise we need to call push_diff on all cases.\n  defp write_socket(state, nil, ref, fun) do\n    {new_socket, return} = fun.(state.socket, nil)\n\n    case return do\n      {:ok, ref_reply, new_state} ->\n        {:noreply, new_state} = handle_changed(new_state, new_socket, ref_reply)\n        new_state\n\n      :error ->\n        push_noop(state, ref)\n    end\n  end\n\n  defp write_socket(state, cid, ref, fun) do\n    %{socket: socket, components: components} = state\n\n    {diff, new_components, return} =\n      case Diff.write_component(socket, cid, components, fun) do\n        {_diff, _new_components, _return} = triplet -> triplet\n        :error -> {%{}, components, :error}\n      end\n\n    case return do\n      {:ok, ref_reply, new_state} ->\n        new_state = %{new_state | components: new_components}\n        push_diff(new_state, diff, ref_reply)\n\n      :error ->\n        push_noop(state, ref)\n    end\n  end\n\n  defp delete_components(state, cids) do\n    upload_cids = Enum.into(state.upload_names, MapSet.new(), fn {_name, {_ref, cid}} -> cid end)\n\n    Enum.flat_map_reduce(cids, state, fn cid, acc ->\n      {deleted_cids, new_components} = Diff.delete_component(cid, acc.components)\n\n      canceled_confs =\n        Enum.flat_map(deleted_cids, fn deleted_cid ->\n          read_socket(acc, deleted_cid, fn c_socket, component ->\n            :telemetry.execute([:phoenix, :live_component, :destroyed], %{}, %{\n              socket: c_socket,\n              component: component,\n              cid: deleted_cid,\n              live_view_socket: acc.socket\n            })\n\n            if deleted_cid in upload_cids do\n              {_new_c_socket, canceled_confs} = Upload.maybe_cancel_uploads(c_socket)\n              canceled_confs\n            else\n              []\n            end\n          end)\n        end)\n\n      new_state =\n        Enum.reduce(canceled_confs, acc, fn conf, acc -> drop_upload_name(acc, conf.name) end)\n\n      {deleted_cids, %{new_state | components: new_components}}\n    end)\n  end\n\n  defp ensure_unique_upload_name!(state, conf) do\n    upload_ref = conf.ref\n    cid = conf.cid\n\n    case Map.fetch(state.upload_names, conf.name) do\n      {:ok, {^upload_ref, ^cid}} ->\n        :ok\n\n      :error ->\n        :ok\n\n      {:ok, {_existing_ref, existing_cid}} ->\n        raise RuntimeError, \"\"\"\n        existing upload for #{conf.name} already allowed in another component (#{existing_cid})\n\n        If you want to allow simultaneous uploads across different components,\n        pass a unique upload name to allow_upload/3\n        \"\"\"\n    end\n  end\n\n  defp authorize_session(%Session{} = session, endpoint, %{\"redirect\" => url}) do\n    if redir_route = session_route(session, endpoint, url) do\n      case Session.authorize_root_redirect(session, redir_route) do\n        {:ok, %Session{} = new_session} ->\n          {:ok, new_session, redir_route, url}\n\n        :error ->\n          Logger.warning(\n            \"navigate event to #{inspect(url)} failed because you are redirecting across live_sessions. \" <>\n              \"A full page reload will be performed instead\"\n          )\n\n          {:error, :unauthorized}\n      end\n    else\n      Logger.warning(\n        \"navigate event to #{inspect(url)} failed because the URL does not point to a LiveView. \" <>\n          \"A full page reload will be performed instead\"\n      )\n\n      {:error, :unauthorized}\n    end\n  end\n\n  defp authorize_session(%Session{} = session, endpoint, %{\"url\" => url}) do\n    %Session{view: view, live_session_name: session_name} = session\n\n    if Session.main?(session) do\n      # Ensure the session's LV module and live session name still match on connect.\n      # If the route has changed the LV module or has moved live sessions (typically\n      # during a deployment), the client will fallback to full page redirect to the\n      # current URL.\n      case session_route(session, endpoint, url) do\n        %Route{view: ^view, live_session: %{name: ^session_name}} = route ->\n          {:ok, session, route, url}\n\n        # if we have a session, then it no longer matches and is unauthorized\n        _ ->\n          {:error, :unauthorized}\n      end\n    else\n      {:ok, session, _route = nil, _url = nil}\n    end\n  end\n\n  defp authorize_session(%Session{} = session, _endpoint, %{} = _params) do\n    {:ok, session, _route = nil, _url = nil}\n  end\n\n  defp session_route(%Session{} = session, endpoint, url) do\n    case Route.live_link_info_without_checks(endpoint, session.router, url) do\n      {:internal, %Route{} = route} -> route\n      _ -> nil\n    end\n  end\n\n  defp maybe_subscribe_to_live_reload({:noreply, state}) do\n    live_reload_config = state.socket.endpoint.config(:live_reload)\n\n    if live_reload_config[:notify][:live_view] do\n      state.socket.endpoint.subscribe(\"live_view\")\n\n      reloader = live_reload_config[:reloader] || {Phoenix.CodeReloader, :reload, []}\n      state = put_in(state.socket.private[:phoenix_reloader], reloader)\n      {:noreply, state}\n    else\n      {:noreply, state}\n    end\n  end\n\n  defp maybe_subscribe_to_live_reload(response), do: response\n\n  defp component_asyncs(state) do\n    %{components: {components, _ids, _}} = state\n\n    Enum.reduce(components, %{}, fn {cid, {_mod, _id, _assigns, private, _prints}}, acc ->\n      Map.merge(acc, socket_asyncs(private, cid))\n    end)\n  end\n\n  defp all_asyncs(state) do\n    %{socket: socket} = state\n\n    socket.private\n    |> socket_asyncs(nil)\n    |> Map.merge(component_asyncs(state))\n  end\n\n  defp socket_asyncs(private, cid) do\n    case private do\n      %{live_async: ref_pids} ->\n        Enum.into(ref_pids, %{}, fn {key, {ref, pid, kind}} -> {pid, {key, ref, cid, kind}} end)\n\n      %{} ->\n        %{}\n    end\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/colocated_hook.ex",
    "content": "defmodule Phoenix.LiveView.ColocatedHook do\n  @moduledoc ~S'''\n  A special HEEx `:type` that extracts [hooks](js-interop.md#client-hooks-via-phx-hook)\n  from a co-located `<script>` tag at compile time.\n\n  Note: To use `ColocatedHook`, you need to run Phoenix 1.8+.\n\n  ## Introduction\n\n  Colocated hooks are defined as with `:type={Phoenix.LiveView.ColocatedHook}`:\n\n      defmodule MyAppWeb.DemoLive do\n        use MyAppWeb, :live_view\n\n        def mount(_params, _session, socket) do\n          {:ok, socket}\n        end\n\n        def render(assigns) do\n          ~H\"\"\"\n          <input type=\"text\" name=\"user[phone_number]\" id=\"user-phone-number\" phx-hook=\".PhoneNumber\" />\n          <script :type={Phoenix.LiveView.ColocatedHook} name=\".PhoneNumber\">\n            export default {\n              mounted() {\n                this.el.addEventListener(\"input\", e => {\n                  let match = this.el.value.replace(/\\D/g, \"\").match(/^(\\d{3})(\\d{3})(\\d{4})$/)\n                  if(match) {\n                    this.el.value = `${match[1]}-${match[2]}-${match[3]}`\n                  }\n                })\n              }\n            }\n          </script>\n          \"\"\"\n        end\n      end\n\n  You can read more about the internals of colocated hooks in the [documentation for colocated JS](`Phoenix.LiveView.ColocatedJS#internals`).\n  A brief summary: at compile time, the hook's code is extracted into a special folder, typically in your `_build` directory.\n  Each hook is also `import`ed into a special *manifest* file. The manifest file provides\n  [a named export](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export)\n  which allows it to be imported by any JavaScript bundler that supports [ES modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules):\n\n  ```javascript\n  import {hooks} from \"phoenix-colocated/my_app\"\n\n  console.log(hooks);\n  /*\n  {\n    \"MyAppWeb.DemoLive.PhoneNumber\": {...},\n    ...\n  }\n  */\n  ```\n\n  > #### Compilation order {: .info}\n  >\n  > Colocated hooks are only written when the corresponding component is compiled.\n  > Therefore, whenever you need to access a colocated hook, you need to ensure\n  > `mix compile` runs first. This automatically happens in development.\n  >\n  > If you have a custom mix alias, instead of\n  >     release: [\"assets.deploy\", \"release\"]\n  > do\n  >     release: [\"compile\", \"assets.deploy\", \"release\"]\n  > to ensure that all colocated hooks are extracted before esbuild or any other bundler runs.\n\n  ## Options\n\n  Colocated hooks are configured through the attributes of the `<script>` tag.\n  The supported attributes are:\n\n    * `name` - The name of the hook. This is required and must start with a dot,\n      for example: `name=\".myhook\"`. The same name must be used when referring to this\n      hook in the `phx-hook` attribute of another HTML element.\n\n    * `runtime` - If present, the hook is not extracted, but instead registered at runtime.\n      You should only use this option if you know that you need it. It comes with some limitations:\n\n        1. The content is not processed by any bundler, therefore it must only use features\n           supported by the targeted browsers.\n        2. You need to take special care about any [Content Security Policies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP)\n           that may be in place. See the section on runtime hooks below for more details.\n\n  ## Runtime hooks\n\n  Runtime hooks are a special kind of colocated hook that are not removed from the DOM\n  when rendering the component. Instead, the hook's code is executed directly in the\n  browser with no bundler involved.\n\n  One example where this can be useful is when you are creating a custom page for a library\n  like `Phoenix.LiveDashboard`. The live dashboard already bundles its hooks, therefore there\n  is no way to add new hooks to the bundle when the live dashboard is used inside your application.\n\n  Because of this, runtime hooks must also use a slightly different syntax. While in normal\n  colocated hooks you'd write an `export default` statement, runtime hooks must evaluate to the\n  hook itself:\n\n  ```heex\n  <script :type={Phoenix.LiveView.ColocatedHook} name=\".MyHook\" runtime>\n    {\n      mounted() {\n        ...\n      }\n    }\n  </script>\n  ```\n\n  This is because the hook's code is wrapped by LiveView into something like this:\n\n  ```javascript\n  window[\"phx_hook_HASH\"] = function() {\n    return {\n      mounted() {\n        ...\n      }\n    }\n  }\n  ```\n\n  Still, even for runtime hooks, the hook's name needs to start with a dot and is automatically\n  prefixed with the module name to avoid conflicts with other hooks.\n\n  When using runtime hooks, it is important to think about any limitations that content security\n  policies may impose. If CSP is involved, the only way to use runtime hooks is by using CSP nonces:\n\n  ```heex\n  <script :type={Phoenix.LiveView.ColocatedHook} name=\".MyHook\" runtime nonce={@script_csp_nonce}>\n    function() {\n      return ...;\n    }\n  </script>\n  ```\n\n  This is assuming that the `@script_csp_nonce` assign contains the nonce value that is also\n  sent in the `Content-Security-Policy` header.\n  '''\n\n  @behaviour Phoenix.Component.MacroComponent\n\n  @impl true\n  def transform({\"script\", attributes, [text_content], _tag_meta} = _ast, meta) do\n    validate_phx_version!()\n\n    opts = Map.new(attributes)\n\n    name =\n      case opts do\n        %{\"name\" => \".\" <> name} ->\n          \"#{inspect(meta.env.module)}.#{name}\"\n\n        %{\"name\" => name} when is_binary(name) ->\n          raise ArgumentError,\n                \"\"\"\n                colocated hook names must start with a dot, invalid hook name: #{name}\n\n                Hint: name your hook <script :type={ColocatedHook} name=\".#{name}\" ...>\n                \"\"\"\n\n        %{\"name\" => name} ->\n          raise ArgumentError,\n                \"the name attribute of a colocated hook must be a compile-time string. Got: #{Macro.to_string(name)}\"\n\n        %{} ->\n          raise ArgumentError, \"missing required name attribute for ColocatedHook\"\n      end\n\n    case opts do\n      %{\"runtime\" => _} ->\n        new_content = \"\"\"\n        window[\"phx_hook_#{Phoenix.HTML.javascript_escape(name)}\"] = function() {\n          return #{String.trim_leading(text_content)}\n        }\n        \"\"\"\n\n        attrs = Enum.to_list(Map.drop(opts, [\"name\", \"runtime\"]))\n        {:ok, {\"script\", [{\"data-phx-runtime-hook\", name} | attrs], [new_content], %{}}}\n\n      _ ->\n        # a colocated hook is just a special type of colocated JS,\n        # exported under the top-level `hooks` key.\n        opts =\n          opts\n          |> Map.put(\"key\", \"hooks\")\n          |> Map.put(\"name\", name)\n\n        data = Phoenix.LiveView.ColocatedJS.extract(opts, text_content, meta)\n        {:ok, \"\", data}\n    end\n  end\n\n  def transform(_ast, _meta) do\n    raise ArgumentError, \"a ColocatedHook can only be defined on script tags\"\n  end\n\n  defp validate_phx_version! do\n    phoenix_version = to_string(Application.spec(:phoenix, :vsn))\n\n    if not Version.match?(phoenix_version, \"~> 1.8\") do\n      raise ArgumentError, ~s|ColocatedHook requires at least {:phoenix, \"~> 1.8\"}|\n    end\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/colocated_js.ex",
    "content": "defmodule Phoenix.LiveView.ColocatedJS do\n  @moduledoc ~S'''\n  A special HEEx `:type` that extracts any JavaScript code from a co-located\n  `<script>` tag at compile time.\n\n  Note: To use `ColocatedJS`, you need to run Phoenix 1.8+.\n\n  Colocated JavaScript is a more generalized version of `Phoenix.LiveView.ColocatedHook`.\n  In fact, colocated hooks are built on top of `ColocatedJS`.\n\n  You can use `ColocatedJS` to define any JavaScript code (Web Components, global event listeners, etc.)\n  that do not necessarily need the functionalities of hooks, for example:\n\n  ```heex\n  <script :type={Phoenix.LiveView.ColocatedJS} name=\"MyWebComponent\">\n    export default class MyWebComponent extends HTMLElement {\n      connectedCallback() {\n        this.innerHTML = \"Hello, world!\";\n      }\n    }\n  </script>\n  ```\n\n  Then, in your `app.js` file, you could import it like this:\n\n  ```javascript\n  import colocated from \"phoenix-colocated/my_app\";\n  customElements.define(\"my-web-component\", colocated.MyWebComponent);\n  ```\n\n  In this example, you don't actually need to have special code for the web component\n  inside your `app.js` file, since you could also directly call `customElements.define`\n  inside the colocated JavaScript. However, this example shows how you can access the\n  exported values inside your bundle.\n\n  > #### A note on dependencies and umbrella projects {: .info}\n  >\n  > For each application that uses colocated JavaScript, a separate directory is created\n  > inside the `phoenix-colocated` folder. This allows to have clear separation between\n  > hooks and code of dependencies, but also applications inside umbrella projects.\n  >\n  > While dependencies would typically still bundle their own hooks and colocated JavaScript\n  > into a separate file before publishing, simple hooks or code snippets that do not require\n  > access to third-party libraries can also be directly imported into your own bundle.\n  > If a library requires this, it should be stated in its documentation.\n\n  ## Internals\n\n  While compiling the template, colocated JavaScript is extracted into a special folder inside the\n  `Mix.Project.build_path()`, called `phoenix-colocated`. This is customizable, as we'll see below,\n  but it is important that it is a directory that is not tracked by version control, because the\n  components are the source of truth for the code. Also, the directory is shared between applications\n  (this also applies to applications in umbrella projects), so it should typically also be a shared\n  directory not specific to a single application.\n\n  The colocated JS directory follows this structure:\n\n  ```text\n  _build/$MIX_ENV/phoenix-colocated/\n  _build/$MIX_ENV/phoenix-colocated/my_app/\n  _build/$MIX_ENV/phoenix-colocated/my_app/index.js\n  _build/$MIX_ENV/phoenix-colocated/my_app/MyAppWeb.DemoLive/line_HASH.js\n  _build/$MIX_ENV/phoenix-colocated/my_dependency/MyDependency.Module/line_HASH.js\n  ...\n  ```\n\n  Each application has its own folder. Inside, each module also gets its own folder, which allows\n  us to track and clean up outdated code.\n\n  To use colocated JS from your `app.js`, your bundler needs to be configured to resolve the\n  `phoenix-colocated` folder. For new Phoenix applications, this configuration is already included\n  in the esbuild configuration inside `config.exs`:\n\n      config :esbuild,\n        ...\n        my_app: [\n          args:\n            ~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.),\n          cd: Path.expand(\"../assets\", __DIR__),\n          env: %{\n            \"NODE_PATH\" => [Path.expand(\"../deps\", __DIR__), Mix.Project.build_path()]\n          }\n        ]\n\n  The important part here is the `NODE_PATH` environment variable, which tells esbuild to also look\n  for packages inside the `deps` folder, as well as the `Mix.Project.build_path()`, which resolves to\n  `_build/$MIX_ENV`. If you use a different bundler, you'll need to configure it accordingly. If it is not\n  possible to configure the `NODE_PATH`, you can also change the folder to which LiveView writes colocated\n  JavaScript by setting the `:target_directory` option in your `config.exs`:\n\n  ```elixir\n  config :phoenix_live_view, :colocated_js,\n    target_directory: Path.expand(\"../assets/node_modules/phoenix-colocated\", __DIR__)\n  ```\n\n  An alternative approach could be to symlink the `phoenix-colocated` folder into your `node_modules`\n  folder.\n\n  > #### Tip {: .info}\n  >\n  > If you remove or modify the contents of the `:target_directory` folder, you can use\n  > `mix clean --all` and `mix compile` to regenerate all colocated JavaScript.\n\n  > #### Warning! {: .warning}\n  >\n  > LiveView assumes full ownership over the configured `:target_directory`. When\n  > compiling, it will **delete** any files and folders inside the `:target_directory`,\n  > that it does not associate with a colocated JavaScript module or manifest.\n\n  ### Imports in colocated JS\n\n  The colocated JS files are fully handled by your bundler. For Phoenix apps, this is typically\n  `esbuild`. Because colocated JS is extracted to a folder outside the regular `assets` folder,\n  special care is necessary when you need to import other files inside the colocated JS:\n\n  ```elixir\n  def sha256(assigns) do\n    ~H\"\"\"\n    <div id=\"sha-256\" phx-hook=\".Sha256\">Hello World</div>\n    <script :type={Phoenix.LiveView.ColocatedHook} name=\".Sha256\">\n      import { sha256 } from \"my-example-sha256-library\"\n      import { reverse } from \"@/vendor/vendored-file\"\n      export default {\n        mounted() {\n          this.el.innerHTML = sha256(reverse(this.el.innerHTML))\n        }\n      }\n    </script>\n    \"\"\"\n  end\n  ```\n\n  While dependencies from `node_modules` should work out of the box, you cannot simply refer to your\n  `assets/vendor` folder using a relative path. Instead, your bundler needs to be configured to handle\n  an alias like `@` to resolve to your local `assets` folder. This is configured by default in the\n  esbuild configuration for new Phoenix 1.8 applications using `esbuild`'s [alias option](https://esbuild.github.io/api/#alias),\n  as can be seen in the config snippet above (`--alias=@=.`).\n\n  If your `node_modules` location is not `assets/node_modules` or `node_modules`, you may need to\n  configure the `:node_modules_path` option:\n\n  ```elixir\n  # mix.exs\n  def project do\n    [\n      ...\n      compilers: [:phoenix_live_view] ++ Mix.compilers(),\n      phoenix_live_view: [colocated_js: [node_modules_path: \"assets/node_modules\"]],\n      ...\n    ]\n  end\n  ```\n\n  This example shows the default behavior.\n\n  Note: In contrast to `:target_directory`, the `:node_modules_path` is a project\n  specific setting you need to set in your `mix.exs`.\n\n  ## Options\n\n  Colocated JavaScript can be configured through the attributes of the `<script>` tag.\n  The supported attributes are:\n\n    * `name` - The name under which the default export of the script is available when importing\n      the manifest. If omitted, the file will be imported for side effects only.\n\n    * `key` - A custom key to use for the export. This is used by `Phoenix.LiveView.ColocatedHook` to\n      export all hooks under the named `hooks` export (`export { ... as hooks }`).\n      For example, you could set this to `web_components` for each colocated script that defines\n      a web component and then import all of them as `import { web_components } from \"phoenix-colocated/my_app\"`.\n      Defaults to `:default`, which means the export will be available under the manifest's `default` export.\n      This needs to be a valid JavaScript identifier. When given, a `name` is required as well.\n\n    * `extension` - a custom extension to use when writing the extracted file. The default is `js`.\n\n    * `manifest` - a custom manifest file to use instead of the default `index.js`. For example,\n      `web_components.ts`. If you change the manifest, you will need to change the\n      path of your JavaScript imports accordingly.\n\n  '''\n\n  @behaviour Phoenix.Component.MacroComponent\n\n  alias Phoenix.Component.MacroComponent\n\n  @impl true\n  def transform({\"script\", attributes, [text_content], _tag_meta} = _ast, meta) do\n    validate_phx_version!()\n\n    opts = Map.new(attributes)\n    validate_name!(opts)\n    data = extract(opts, text_content, meta)\n\n    # we always drop colocated JS from the rendered output\n    {:ok, \"\", data}\n  end\n\n  def transform(_ast, _meta) do\n    raise ArgumentError, \"ColocatedJS can only be used on script tags\"\n  end\n\n  defp validate_phx_version! do\n    phoenix_version = to_string(Application.spec(:phoenix, :vsn))\n\n    if not Version.match?(phoenix_version, \"~> 1.8\") do\n      raise ArgumentError, ~s|ColocatedJS requires at least {:phoenix, \"~> 1.8\"}|\n    end\n  end\n\n  defp validate_name!(opts) do\n    case opts do\n      %{\"name\" => name} when is_binary(name) ->\n        :ok\n\n      %{\"name\" => name} ->\n        raise ArgumentError,\n              \"the name attribute of a colocated script must be a compile-time string. Got: #{Macro.to_string(name)}\"\n\n      %{\"key\" => _} ->\n        raise ArgumentError,\n              \"a name is required when a key is given\"\n\n      _ ->\n        :ok\n    end\n  end\n\n  @doc false\n  def extract(opts, text_content, meta) do\n    # _build/dev/phoenix-colocated/otp_app/MyApp.MyComponent/line_no.js\n    target_path =\n      target_dir()\n      |> Path.join(inspect(meta.env.module))\n\n    filename_opts =\n      %{name: opts[\"name\"]}\n      |> maybe_put_opt(opts, \"key\", :key)\n      |> maybe_put_opt(opts, \"manifest\", :manifest)\n\n    hashed_name =\n      (filename_opts.name || text_content)\n      |> then(&:crypto.hash(:md5, &1))\n      |> Base.encode32(case: :lower, padding: false)\n\n    filename = \"#{meta.env.line}_#{hashed_name}.#{opts[\"extension\"] || \"js\"}\"\n\n    File.mkdir_p!(target_path)\n    File.write!(Path.join(target_path, filename), text_content)\n\n    {filename, filename_opts}\n  end\n\n  defp maybe_put_opt(map, opts, opts_key, target_key) do\n    case opts do\n      %{^opts_key => value} ->\n        Map.put(map, target_key, value)\n\n      _ ->\n        map\n    end\n  end\n\n  @doc false\n  def compile do\n    # this step runs after all modules have been compiled\n    # so we can write the final manifests and remove outdated hooks\n    clear_manifests!()\n    files = clear_outdated_and_get_files!()\n    write_new_manifests!(files)\n    maybe_link_node_modules!()\n  end\n\n  defp clear_manifests! do\n    target_dir = target_dir()\n\n    manifests =\n      Path.wildcard(Path.join(target_dir, \"*\"))\n      |> Enum.filter(&File.regular?(&1))\n\n    for manifest <- manifests, do: File.rm!(manifest)\n  end\n\n  defp clear_outdated_and_get_files! do\n    target_dir = target_dir()\n    modules = subdirectories(target_dir)\n\n    Enum.flat_map(modules, fn module_folder ->\n      module = Module.concat([Path.basename(module_folder)])\n      process_module(module_folder, module)\n    end)\n  end\n\n  defp process_module(module_folder, module) do\n    with true <- Code.ensure_loaded?(module),\n         data when data != [] <- get_data(module) do\n      expected_files = Enum.map(data, fn {filename, _opts} -> filename end)\n      files = File.ls!(module_folder)\n\n      outdated_files = files -- expected_files\n\n      for file <- outdated_files do\n        File.rm!(Path.join(module_folder, file))\n      end\n\n      Enum.map(data, fn {filename, config} ->\n        absolute_file_path = Path.join(module_folder, filename)\n        {absolute_file_path, config}\n      end)\n    else\n      _ ->\n        # either the module does not exist any more or\n        # does not have any colocated hooks / JS\n        File.rm_rf!(module_folder)\n        []\n    end\n  end\n\n  defp get_data(module) do\n    hooks_data = MacroComponent.get_data(module, Phoenix.LiveView.ColocatedHook)\n    js_data = MacroComponent.get_data(module, Phoenix.LiveView.ColocatedJS)\n\n    hooks_data ++ js_data\n  end\n\n  defp write_new_manifests!(files) do\n    if files == [] do\n      File.mkdir_p!(target_dir())\n\n      File.write!(\n        Path.join(target_dir(), \"index.js\"),\n        \"export const hooks = {};\\nexport default {};\"\n      )\n    else\n      files\n      |> Enum.group_by(fn {_file, config} ->\n        config[:manifest] || \"index.js\"\n      end)\n      |> Enum.each(fn {manifest, entries} ->\n        write_manifest(manifest, entries)\n      end)\n    end\n  end\n\n  defp write_manifest(manifest, entries) do\n    target_dir = target_dir()\n\n    content =\n      entries\n      |> Enum.group_by(fn {_file, config} -> config[:key] || :default end)\n      |> Enum.reduce([\"const js = {}; export default js;\\n\"], fn group, acc ->\n        case group do\n          {:default, entries} ->\n            [\n              acc,\n              Enum.map(entries, fn\n                {file, %{name: nil}} ->\n                  ~s[import \"./#{Path.relative_to(file, target_dir)}\";\\n]\n\n                {file, %{name: name}} ->\n                  import_name =\n                    \"js_\" <> Base.encode32(:crypto.hash(:md5, file), case: :lower, padding: false)\n\n                  escaped_name = Phoenix.HTML.javascript_escape(name)\n\n                  ~s<import #{import_name} from \"./#{Path.relative_to(file, target_dir)}\"; js[\"#{escaped_name}\"] = #{import_name};\\n>\n              end)\n            ]\n\n          {key, entries} ->\n            tmp_name = \"imp_#{Base.encode32(key, case: :lower, padding: false)}\"\n\n            [\n              acc,\n              ~s<const #{tmp_name} = {}; export { #{tmp_name} as #{key} };\\n>,\n              Enum.map(entries, fn\n                {file, %{name: nil}} ->\n                  ~s[import \"./#{Path.relative_to(file, target_dir)}\";\\n]\n\n                {file, %{name: name}} ->\n                  import_name =\n                    \"js_\" <> Base.encode32(:crypto.hash(:md5, file), case: :lower, padding: false)\n\n                  escaped_name = Phoenix.HTML.javascript_escape(name)\n\n                  ~s<import #{import_name} from \"./#{Path.relative_to(file, target_dir)}\"; #{tmp_name}[\"#{escaped_name}\"] = #{import_name};\\n>\n              end)\n            ]\n        end\n      end)\n\n    File.write!(Path.join(target_dir, manifest), content)\n  end\n\n  defp maybe_link_node_modules! do\n    settings = project_settings()\n\n    case Keyword.get(settings, :node_modules_path, {:fallback, \"assets/node_modules\"}) do\n      {:fallback, rel_path} ->\n        location = Path.absname(rel_path)\n        do_symlink(location, true)\n\n      path when is_binary(path) ->\n        location = Path.absname(path)\n        do_symlink(location, false)\n    end\n  end\n\n  defp relative_to_target(location) do\n    if function_exported?(Path, :relative_to, 3) do\n      apply(Path, :relative_to, [location, target_dir(), [force: true]])\n    else\n      Path.relative_to(location, target_dir())\n    end\n  end\n\n  defp do_symlink(node_modules_path, is_fallback) do\n    relative_node_modules_path = relative_to_target(node_modules_path)\n\n    with {:error, reason} when reason != :eexist <-\n           File.ln_s(relative_node_modules_path, Path.join(target_dir(), \"node_modules\")),\n         false <- Keyword.get(global_settings(), :disable_symlink_warning, false) do\n      disable_hint = \"\"\"\n      If you don't use colocated hooks / js or you don't need to import files from \"assets/node_modules\"\n      in your hooks, you can simply disable this warning by setting\n\n          config :phoenix_live_view, :colocated_js,\n            disable_symlink_warning: true\n      \"\"\"\n\n      IO.warn(\"\"\"\n      Failed to symlink node_modules folder for Phoenix.LiveView.ColocatedJS: #{inspect(reason)}\n\n      On Windows, you can address this issue by starting your Windows terminal at least once\n      with \"Run as Administrator\" and then running your Phoenix application.#{is_fallback && \"\\n\\n\" <> disable_hint}\n      \"\"\")\n    end\n  end\n\n  defp global_settings do\n    Application.get_env(:phoenix_live_view, :colocated_js, [])\n  end\n\n  defp project_settings do\n    Mix.Project.config()\n    |> Keyword.get(:phoenix_live_view, [])\n    |> Keyword.get(:colocated_js, [])\n  end\n\n  defp target_dir do\n    default = Path.join(Mix.Project.build_path(), \"phoenix-colocated\")\n    app = to_string(Mix.Project.config()[:app])\n\n    global_settings()\n    |> Keyword.get(:target_directory, default)\n    |> Path.join(app)\n  end\n\n  defp subdirectories(path) do\n    Path.wildcard(Path.join(path, \"*\")) |> Enum.filter(&File.dir?(&1))\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/controller.ex",
    "content": "defmodule Phoenix.LiveView.Controller do\n  @moduledoc \"\"\"\n  Helpers for rendering LiveViews from a controller.\n  \"\"\"\n\n  alias Phoenix.LiveView\n  alias Phoenix.LiveView.Socket\n\n  @doc \"\"\"\n  Renders a live view from a Plug request and sends an HTML response\n  from within a controller.\n\n  It also automatically sets the `@live_module` assign with the value\n  of the LiveView to be rendered.\n\n  ## Options\n\n  See `Phoenix.Component.live_render/3` for all supported options.\n\n  ## Examples\n\n      defmodule ThermostatController do\n        use MyAppWeb, :controller\n\n        # \"use MyAppWeb, :controller\" should import Phoenix.LiveView.Controller.\n        # If it does not, you can either import it there or uncomment the line below:\n        # import Phoenix.LiveView.Controller\n\n        def show(conn, %{\"id\" => thermostat_id}) do\n          live_render(conn, ThermostatLive, session: %{\n            \"thermostat_id\" => thermostat_id,\n            \"current_user_id\" => get_session(conn, :user_id)\n          })\n        end\n      end\n\n  \"\"\"\n  def live_render(%Plug.Conn{} = conn, view, opts \\\\ []) do\n    case LiveView.Static.render(conn, view, opts) do\n      {:ok, content, socket_assigns} ->\n        conn\n        |> Plug.Conn.fetch_query_params()\n        |> ensure_format()\n        |> Phoenix.Controller.put_view(LiveView.Static)\n        |> Phoenix.Controller.render(\n          :template,\n          Map.merge(socket_assigns, %{content: content, live_module: view})\n        )\n\n      {:stop, %Socket{redirected: {:redirect, %{status: status} = opts}} = socket} ->\n        redirect_opts = Map.delete(opts, :status) |> Map.to_list()\n\n        conn\n        |> Plug.Conn.put_status(status)\n        |> put_flash(LiveView.Utils.get_flash(socket))\n        |> Phoenix.Controller.redirect(redirect_opts)\n\n      {:stop, %Socket{redirected: {:live, _, %{to: to}}} = socket} ->\n        conn\n        |> put_flash(LiveView.Utils.get_flash(socket))\n        |> Plug.Conn.put_private(:phoenix_live_redirect, true)\n        |> Phoenix.Controller.redirect(to: to)\n    end\n  end\n\n  defp ensure_format(conn) do\n    if Phoenix.Controller.get_format(conn) do\n      conn\n    else\n      Phoenix.Controller.put_format(conn, \"html\")\n    end\n  end\n\n  defp put_flash(conn, nil), do: conn\n\n  defp put_flash(conn, flash),\n    do: Enum.reduce(flash, conn, fn {k, v}, acc -> Phoenix.Controller.put_flash(acc, k, v) end)\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/debug.ex",
    "content": "defmodule Phoenix.LiveView.Debug do\n  @moduledoc \"\"\"\n  Functions for runtime introspection and debugging of LiveViews.\n\n  This module provides utilities for inspecting and debugging LiveView processes\n  at runtime. It allows you to:\n\n    * List all currently connected LiveViews\n    * Check if a process is a LiveView\n    * Get the socket of a LiveView process\n    * Inspect LiveComponents rendered in a LiveView\n\n  ## Examples\n\n      # List all LiveViews\n      iex> Phoenix.LiveView.Debug.list_liveviews()\n      [%{pid: #PID<0.123.0>, view: MyAppWeb.PostLive.Index, topic: \"lv:12345678\", transport_pid: #PID<0.122.0>}]\n\n      # Check if a process is a LiveView\n      iex> Phoenix.LiveView.Debug.liveview_process?(pid(0,123,0))\n      true\n\n      # Get the socket of a LiveView process\n      iex> Phoenix.LiveView.Debug.socket(pid(0,123,0))\n      {:ok, %Phoenix.LiveView.Socket{...}}\n\n      # Get information about LiveComponents\n      iex> Phoenix.LiveView.Debug.live_components(pid(0,123,0))\n      {:ok, [%{id: \"component-1\", module: MyAppWeb.PostLive.Index.Component1, ...}]}\n\n  \"\"\"\n\n  @doc \"\"\"\n  Returns a list of all currently connected LiveView processes (on the current node).\n\n  Each entry is a map with the following keys:\n\n    - `pid`: The PID of the LiveView process.\n    - `view`: The module of the LiveView.\n    - `topic`: The topic of the LiveView's channel.\n    - `transport_pid`: The PID of the transport process.\n\n  The `transport_pid` can be used to group LiveViews on the same page.\n\n  ## Examples\n\n      iex> list_liveviews()\n      [%{pid: #PID<0.123.0>, view: MyAppWeb.PostLive.Index, topic: \"lv:12345678\", transport_pid: #PID<0.122.0>}]\n\n  \"\"\"\n  def list_liveviews do\n    for pid <- Process.list(), dict = lv_process_dict(pid), not is_nil(dict) do\n      {Phoenix.LiveView, view, topic} = keyfind(dict, :\"$process_label\")\n      %{pid: pid, view: view, topic: topic, transport_pid: keyfind(dict, :\"$phx_transport_pid\")}\n    end\n  end\n\n  defp keyfind(list, key) do\n    case List.keyfind(list, key, 0) do\n      {^key, value} -> value\n      _ -> nil\n    end\n  end\n\n  defp lv_process_dict(pid) do\n    # LiveViews set the \"$process_label\" to {Phoenix.LiveView, view, topic}\n    with info when is_list(info) <- Process.info(pid, [:dictionary]),\n         dictionary when not is_nil(dictionary) <- keyfind(info, :dictionary),\n         label when not is_nil(label) <- keyfind(dictionary, :\"$process_label\"),\n         {Phoenix.LiveView, view, topic} when is_atom(view) and is_binary(topic) <- label do\n      dictionary\n    else\n      _ -> nil\n    end\n  end\n\n  @doc \"\"\"\n  Checks if the given pid is a LiveView process.\n\n  ## Examples\n\n      iex> list_liveviews() |> Enum.at(0) |> Map.fetch!(:pid) |> liveview_process?()\n      true\n\n      iex> liveview_process?(pid(0,456,0))\n      false\n\n  \"\"\"\n  def liveview_process?(pid) do\n    not is_nil(lv_process_dict(pid))\n  end\n\n  @doc \"\"\"\n  Returns the socket of the LiveView process.\n\n  ## Examples\n\n      iex> list_liveviews() |> Enum.at(0) |> Map.fetch!(:pid) |> socket()\n      {:ok, %Phoenix.LiveView.Socket{...}}\n\n      iex> socket(pid(0,123,0))\n      {:error, :not_alive_or_not_a_liveview}\n\n  \"\"\"\n  def socket(liveview_pid) do\n    GenServer.call(liveview_pid, {:phoenix, :debug_get_socket})\n  catch\n    :exit, _ -> {:error, :not_alive_or_not_a_liveview}\n  end\n\n  @doc \"\"\"\n  Returns a list with information about all LiveComponents rendered in the LiveView.\n\n  ## Examples\n\n      iex> live_components(pid)\n      {:ok,\n       [\n         %{\n           id: \"component-1\",\n           module: MyAppWeb.PostLive.Index.Component1,\n           cid: 1,\n           assigns: %{\n             id: \"component-1\",\n             __changed__: %{},\n             flash: %{},\n             myself: %Phoenix.LiveComponent.CID{cid: 1},\n             ...\n           }\n         }\n       ]}\n\n  \"\"\"\n  def live_components(liveview_pid) do\n    GenServer.call(liveview_pid, {:phoenix, :debug_live_components})\n  catch\n    :exit, _ -> {:error, :not_alive_or_not_a_liveview}\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/diff.ex",
    "content": "defmodule Phoenix.LiveView.Diff do\n  # The diff engine is responsible for tracking the rendering state.\n  # Given that components are part of said state, they are also\n  # handled here.\n  @moduledoc false\n\n  alias Phoenix.LiveView.{\n    Component,\n    Comprehension,\n    Lifecycle,\n    Rendered,\n    Utils\n  }\n\n  @components :c\n  @static :s\n  @keyed :k\n  @keyed_count :kc\n  @events :e\n  @reply :r\n  @title :t\n  @template :p\n  @stream :stream\n\n  # We use this to track which components have been marked\n  # for deletion. If the component is used after being marked,\n  # it should not be deleted.\n  @marked_for_deletion :marked_for_deletion\n\n  @doc \"\"\"\n  Returns the diff component state.\n  \"\"\"\n  def new_components(uuids \\\\ 1) do\n    {_cid_to_component = %{}, _id_to_cid = %{}, uuids}\n  end\n\n  @doc \"\"\"\n  Returns the diff fingerprint state.\n  \"\"\"\n  def new_fingerprints do\n    {nil, %{}}\n  end\n\n  @doc \"\"\"\n  Converts a diff into iodata.\n\n  It only accepts a full render diff.\n  \"\"\"\n  def to_iodata(map, component_mapper \\\\ fn _cid, content -> content end) do\n    to_iodata(map, Map.get(map, @components, %{}), Map.get(map, @template), component_mapper)\n    |> elem(0)\n  end\n\n  defp to_iodata(\n         %{@static => static, @keyed => keyed} = kc,\n         components,\n         template,\n         mapper\n       ) do\n    template = template || kc[@template]\n\n    if !keyed or keyed[@keyed_count] == 0 do\n      {[], components}\n    else\n      keyed_to_iodata(0, keyed[@keyed_count], keyed, static, components, template, mapper, [])\n    end\n  end\n\n  defp to_iodata(%{@static => static} = parts, components, template, mapper) do\n    static = template_static(static, template)\n    one_to_iodata(static, parts, 0, [], components, template, mapper)\n  end\n\n  defp to_iodata(cid, components, _template, mapper) when is_integer(cid) do\n    # Resolve component pointers and update the component entries\n    components = resolve_components_xrefs(cid, components)\n    {iodata, components} = to_iodata(Map.fetch!(components, cid), components, nil, mapper)\n    {mapper.(cid, iodata), components}\n  end\n\n  defp to_iodata(binary, components, _template, _mapper) when is_binary(binary) do\n    {binary, components}\n  end\n\n  defp keyed_to_iodata(index, limit, keyed, static, components, template, mapper, acc)\n       when index < limit do\n    diff = Map.fetch!(keyed, index)\n    {iodata, components} = to_iodata(Map.put(diff, @static, static), components, template, mapper)\n    keyed_to_iodata(index + 1, limit, keyed, static, components, template, mapper, [iodata | acc])\n  end\n\n  defp keyed_to_iodata(_index, _limit, _keyed, _static, components, _template, _mapper, acc) do\n    {Enum.reverse(acc), components}\n  end\n\n  defp one_to_iodata([last], _parts, _counter, acc, components, _template, _mapper) do\n    {Enum.reverse([last | acc]), components}\n  end\n\n  defp one_to_iodata([head | tail], parts, counter, acc, components, template, mapper) do\n    {iodata, components} = to_iodata(Map.fetch!(parts, counter), components, template, mapper)\n    one_to_iodata(tail, parts, counter + 1, [iodata, head | acc], components, template, mapper)\n  end\n\n  defp template_static(static, template) when is_integer(static), do: Map.fetch!(template, static)\n  defp template_static(static, _template) when is_list(static), do: static\n\n  defp resolve_components_xrefs(cid, components) do\n    case components[cid] do\n      %{@static => static} = diff when is_integer(static) ->\n        static = abs(static)\n        components = resolve_components_xrefs(static, components)\n        Map.put(components, cid, deep_merge(components[static], Map.delete(diff, @static)))\n\n      %{} ->\n        components\n    end\n  end\n\n  defp deep_merge(_original, %{@static => _} = extra), do: extra\n\n  defp deep_merge(original, extra) do\n    Map.merge(original, extra, fn\n      _, %{} = original, %{} = extra -> deep_merge(original, extra)\n      _, _original, extra -> extra\n    end)\n  end\n\n  @doc \"\"\"\n  Render information stored in private changed.\n  \"\"\"\n  def render_private(socket, diff) do\n    diff\n    |> maybe_put_reply(socket)\n    |> maybe_put_events(socket)\n  end\n\n  @doc \"\"\"\n  Renders a diff for the rendered struct in regards to the given socket.\n  \"\"\"\n  def render(\n        socket,\n        %Rendered{fingerprint: actual} = rendered,\n        {expected, _},\n        {_, _, uuids}\n      )\n      when expected != nil and expected != actual do\n    render(socket, rendered, new_fingerprints(), new_components(uuids))\n  end\n\n  def render(socket, %Rendered{} = rendered, prints, components) do\n    {diff, prints, pending, components, template} =\n      traverse(rendered, prints, %{}, components, {%{}, %{}}, true)\n\n    # cid_to_component is used by maybe_reuse_static and it must be a copy before changes.\n    # However, given traverse does not change cid_to_component, we can read it now.\n    {cid_to_component, _, _} = components\n\n    {cdiffs, components} =\n      render_pending_components(socket, pending, cid_to_component, %{}, components)\n\n    diff =\n      diff\n      |> maybe_add_template(template)\n      |> maybe_put_title(socket)\n\n    {diff, cdiffs} = extract_events({diff, cdiffs})\n    {maybe_put_cdiffs(diff, cdiffs), prints, components}\n  end\n\n  defp maybe_put_cdiffs(diff, cdiffs) when cdiffs == %{}, do: diff\n  defp maybe_put_cdiffs(diff, cdiffs), do: Map.put(diff, @components, cdiffs)\n\n  @doc \"\"\"\n  Returns a diff containing only the events that have been pushed.\n  \"\"\"\n  def get_push_events_diff(socket) do\n    if events = Utils.get_push_events(socket), do: %{@events => events}\n  end\n\n  defp maybe_put_title(diff, socket) do\n    if Utils.changed?(socket.assigns, :page_title) do\n      Map.put(diff, @title, socket.assigns.page_title)\n    else\n      diff\n    end\n  end\n\n  defp maybe_put_events(diff, socket) do\n    case Utils.get_push_events(socket) do\n      [_ | _] = events -> Map.update(diff, @events, events, &(&1 ++ events))\n      [] -> diff\n    end\n  end\n\n  defp extract_events({diff, component_diffs}) do\n    case component_diffs do\n      %{@events => component_events} ->\n        {Map.update(diff, @events, component_events, &(&1 ++ component_events)),\n         Map.delete(component_diffs, @events)}\n\n      %{} ->\n        {diff, component_diffs}\n    end\n  end\n\n  defp maybe_put_reply(diff, socket) do\n    case Utils.get_reply(socket) do\n      nil -> diff\n      reply -> Map.put(diff, @reply, reply)\n    end\n  end\n\n  @doc \"\"\"\n  Execute the `fun` with the component `cid` with the given `socket` as template.\n\n  It returns the updated `cdiffs` and the updated `components` or\n  `:error` if the component cid does not exist.\n  \"\"\"\n  def write_component(socket, cid, components, fun) when is_integer(cid) do\n    # We need to extract the original cid_to_component for maybe_reuse_static later\n    {cids, _, _} = components\n\n    case cids do\n      %{^cid => {component, id, assigns, private, prints}} ->\n        {csocket, extra} =\n          socket\n          |> configure_socket_for_component(assigns, private)\n          |> fun.(component)\n\n        diff = render_private(csocket, %{})\n\n        {pending, cdiffs, components} =\n          render_component(csocket, component, id, prints, cid, false, cids, %{}, components)\n\n        {cdiffs, components} =\n          render_pending_components(socket, pending, cids, cdiffs, components)\n\n        {diff, cdiffs} = extract_events({diff, cdiffs})\n        {maybe_put_cdiffs(diff, cdiffs), components, extra}\n\n      %{} ->\n        :error\n    end\n  end\n\n  @doc \"\"\"\n  Execute the `fun` with the component `cid` with the given `socket` and returns the result.\n\n  `:error` if the component cid does not exist.\n  \"\"\"\n  def read_component(socket, cid, components, fun) when is_integer(cid) do\n    {cid_to_component, _id_to_cid, _} = components\n\n    case cid_to_component do\n      %{^cid => {component, _id, assigns, private, _prints}} ->\n        socket\n        |> configure_socket_for_component(assigns, private)\n        |> fun.(component)\n\n      %{} ->\n        :error\n    end\n  end\n\n  @doc \"\"\"\n  Sends an update to a component.\n\n  Like `write_component/5`, it will store the result under the `cid\n   key in the `component_diffs` map.\n\n  If the component exists, a `{diff, new_components}` tuple\n  is returned. Otherwise, `:noop` is returned.\n\n  The component is preloaded before the update callback is invoked.\n\n  ## Example\n\n      {diff, new_components} = Diff.update_component(socket, state.components, update)\n  \"\"\"\n  def update_component(socket, components, {ref, updated_assigns}) do\n    case fetch_cid(ref, components) do\n      {:ok, {cid, module}} ->\n        updated_assigns = maybe_call_preload!(module, updated_assigns)\n\n        {diff, new_components, :noop} =\n          write_component(socket, cid, components, fn component_socket, component ->\n            telemetry_metadata = %{\n              socket: socket,\n              component: component,\n              assigns_sockets: [{updated_assigns, component_socket}]\n            }\n\n            sockets =\n              :telemetry.span([:phoenix, :live_component, :update], telemetry_metadata, fn ->\n                {Utils.maybe_call_update!(component_socket, component, updated_assigns),\n                 telemetry_metadata}\n              end)\n\n            {sockets, :noop}\n          end)\n\n        {diff, new_components}\n\n      :error ->\n        :noop\n    end\n  end\n\n  @doc \"\"\"\n  Marks a component for deletion.\n\n  It won't be deleted if the component is used meanwhile.\n  \"\"\"\n  def mark_for_deletion_component(cid, {cid_to_component, id_to_cid, uuids}) do\n    cid_to_component =\n      case cid_to_component do\n        %{^cid => {component, id, assigns, private, prints}} ->\n          private = Map.put(private, @marked_for_deletion, true)\n          Map.put(cid_to_component, cid, {component, id, assigns, private, prints})\n\n        %{} ->\n          cid_to_component\n      end\n\n    {cid_to_component, id_to_cid, uuids}\n  end\n\n  @doc \"\"\"\n  Deletes a component by `cid` if it has not been used meanwhile.\n  \"\"\"\n  def delete_component(cid, {cid_to_component, id_to_cid, uuids}) do\n    case cid_to_component do\n      %{^cid => {component, id, _, %{@marked_for_deletion => true}, _}} ->\n        id_to_cid =\n          case id_to_cid do\n            %{^component => inner} ->\n              case Map.delete(inner, id) do\n                inner when inner == %{} -> Map.delete(id_to_cid, component)\n                inner -> Map.put(id_to_cid, component, inner)\n              end\n\n            %{} ->\n              id_to_cid\n          end\n\n        {[cid], {Map.delete(cid_to_component, cid), id_to_cid, uuids}}\n\n      _ ->\n        {[], {cid_to_component, id_to_cid, uuids}}\n    end\n  end\n\n  @doc \"\"\"\n  Converts a component to a rendered struct.\n  \"\"\"\n  def component_to_rendered(socket, component, assigns, mount_assigns) when is_map(assigns) do\n    component_socket = mount_component(socket, component, mount_assigns)\n    assigns = maybe_call_preload!(component, assigns)\n\n    telemetry_metadata = %{\n      socket: socket,\n      component: component,\n      assigns_sockets: [{assigns, component_socket}]\n    }\n\n    :telemetry.span([:phoenix, :live_component, :update], telemetry_metadata, fn ->\n      result =\n        component_socket\n        |> Utils.maybe_call_update!(component, assigns)\n        |> component_to_rendered(component, assigns[:id])\n\n      {result, telemetry_metadata}\n    end)\n  end\n\n  defp component_to_rendered(socket, component, id) do\n    rendered = Phoenix.LiveView.Renderer.to_rendered(socket, component)\n\n    if rendered.root != true and id != nil do\n      reason =\n        case rendered.root do\n          nil -> \"Stateful components must return a HEEx template (~H sigil or .heex extension)\"\n          false -> \"Stateful components must have a single static HTML tag at the root\"\n        end\n\n      raise ArgumentError,\n            \"error on #{inspect(component)}.render/1 with id of #{inspect(id)}. #{reason}\"\n    end\n\n    rendered\n  end\n\n  ## Traversal\n\n  defp traverse(\n         %Rendered{fingerprint: fingerprint} = rendered,\n         {fingerprint, children},\n         pending,\n         components,\n         template,\n         changed?\n       ) do\n    {_counter, diff, children, pending, components, template} =\n      traverse_dynamic(\n        invoke_dynamic(rendered, changed?),\n        children,\n        pending,\n        components,\n        template,\n        changed?\n      )\n\n    {diff, {fingerprint, children}, pending, components, template}\n  end\n\n  defp traverse(\n         %Rendered{fingerprint: fingerprint, static: static} = rendered,\n         _,\n         pending,\n         components,\n         template,\n         changed?\n       ) do\n    {_counter, diff, children, pending, components, template} =\n      traverse_dynamic(\n        invoke_dynamic(rendered, false),\n        %{},\n        pending,\n        components,\n        template,\n        changed?\n      )\n\n    diff = if rendered.root, do: Map.put(diff, :r, 1), else: diff\n    {diff, template} = maybe_share_template(diff, fingerprint, static, template)\n    {diff, {fingerprint, children}, pending, components, template}\n  end\n\n  defp traverse(\n         %Component{} = component,\n         _fingerprints_tree,\n         pending,\n         components,\n         template,\n         _changed?\n       ) do\n    {cid, pending, components} = traverse_component(component, pending, components)\n    {cid, nil, pending, components, template}\n  end\n\n  defp traverse(\n         %Comprehension{\n           fingerprint: fingerprint,\n           entries: entries,\n           stream: stream,\n           has_key?: has_key?\n         },\n         {fingerprint, previous_prints},\n         pending,\n         components,\n         template,\n         changed?\n       ) do\n    if template do\n      {keyed, keyed_prints, pending, components, template} =\n        traverse_keyed(\n          entries,\n          previous_prints,\n          pending,\n          components,\n          template,\n          changed?,\n          stream != nil,\n          has_key?\n        )\n\n      diff =\n        %{}\n        |> maybe_add_keyed(keyed)\n        |> maybe_add_stream(stream)\n\n      {diff, {fingerprint, keyed_prints}, pending, components, template}\n    else\n      {keyed, keyed_prints, pending, components, template} =\n        traverse_keyed(\n          entries,\n          previous_prints,\n          pending,\n          components,\n          {%{}, %{}},\n          changed?,\n          stream != nil,\n          has_key?\n        )\n\n      diff =\n        %{}\n        |> maybe_add_keyed(keyed)\n        |> maybe_add_stream(stream)\n        |> maybe_add_template(template)\n\n      {diff, {fingerprint, keyed_prints}, pending, components, nil}\n    end\n  end\n\n  defp traverse(\n         %Comprehension{entries: [], stream: nil},\n         _,\n         pending,\n         components,\n         template,\n         _changed?\n       ) do\n    # The comprehension has no elements and it was not rendered yet,\n    # so we can skip it as long as it doesn't have a stream.\n    {\"\", nil, pending, components, template}\n  end\n\n  defp traverse(\n         %Comprehension{\n           static: static,\n           fingerprint: fingerprint,\n           entries: entries,\n           stream: stream,\n           has_key?: has_key?\n         },\n         _,\n         pending,\n         components,\n         template,\n         changed?\n       ) do\n    if template do\n      {keyed, keyed_prints, pending, components, template} =\n        traverse_keyed(\n          entries,\n          %{},\n          pending,\n          components,\n          template,\n          changed?,\n          stream != nil,\n          has_key?\n        )\n\n      {diff, template} =\n        %{@keyed => keyed}\n        |> maybe_add_stream(stream)\n        |> maybe_share_template(fingerprint, static, template)\n\n      {diff, {fingerprint, keyed_prints}, pending, components, template}\n    else\n      {keyed, keyed_prints, pending, components, template} =\n        traverse_keyed(\n          entries,\n          %{},\n          pending,\n          components,\n          {%{}, %{}},\n          changed?,\n          stream != nil,\n          has_key?\n        )\n\n      diff =\n        %{@static => static, @keyed => keyed}\n        |> maybe_add_stream(stream)\n        |> maybe_add_template(template)\n\n      {diff, {fingerprint, keyed_prints}, pending, components, nil}\n    end\n  end\n\n  defp traverse(nil, fingerprint_tree, pending, components, template, _changed?) do\n    {nil, fingerprint_tree, pending, components, template}\n  end\n\n  defp traverse(iodata, _, pending, components, template, _changed?) do\n    {IO.iodata_to_binary(iodata), nil, pending, components, template}\n  end\n\n  defp invoke_dynamic(%Rendered{caller: :not_available, dynamic: dynamic}, changed?) do\n    dynamic.(changed?)\n  end\n\n  defp invoke_dynamic(%Rendered{caller: caller, dynamic: dynamic}, changed?) do\n    try do\n      dynamic.(changed?)\n    rescue\n      e ->\n        {mod, {function, arity}, file, line} = caller\n        entry = {mod, function, arity, file: String.to_charlist(file), line: line}\n        reraise e, inject_stacktrace(__STACKTRACE__, entry)\n    end\n  end\n\n  defp inject_stacktrace([{__MODULE__, :invoke_dynamic, 2, _} | stacktrace], entry) do\n    [entry | Enum.drop_while(stacktrace, &(elem(&1, 0) == __MODULE__))]\n  end\n\n  defp inject_stacktrace([head | tail], entry) do\n    [head | inject_stacktrace(tail, entry)]\n  end\n\n  defp inject_stacktrace([], entry) do\n    [entry]\n  end\n\n  defp traverse_dynamic(dynamic, children, pending, components, template, changed?) do\n    Enum.reduce(dynamic, {0, %{}, children, pending, components, template}, fn\n      entry, {counter, diff, children, pending, components, template} ->\n        child = Map.get(children, counter)\n\n        {serialized, child_fingerprint, pending, components, template} =\n          traverse(entry, child, pending, components, template, changed?)\n\n        # If serialized is nil, it means no changes.\n        # If it is an empty map, then it means it is a rendered struct\n        # that did not change, so we don't have to emit it either.\n        diff =\n          if serialized != nil and serialized != %{} do\n            Map.put(diff, counter, serialized)\n          else\n            diff\n          end\n\n        children =\n          if child_fingerprint do\n            Map.put(children, counter, child_fingerprint)\n          else\n            Map.delete(children, counter)\n          end\n\n        {counter + 1, diff, children, pending, components, template}\n    end)\n  end\n\n  defp traverse_keyed(\n         entries,\n         previous_prints,\n         pending,\n         components,\n         template,\n         changed?,\n         stream?,\n         has_key?\n       ) do\n    diff = %{}\n    new_prints = %{}\n\n    {{diff, count, new_prints, pending, components, template}, _seen_keys} =\n      Enum.reduce(\n        entries,\n        {{diff, 0, new_prints, pending, components, template}, MapSet.new()},\n        fn\n          {key, vars, render},\n          {{_diff, index, _new_prints, _pending, _components, _template} = acc, seen_keys} ->\n            {key, seen_keys} =\n              cond do\n                not has_key? ->\n                  # no need to check for duplicates if we use the index\n                  {index, seen_keys}\n\n                MapSet.member?(seen_keys, key) ->\n                  raise \"found duplicate key #{inspect(key)} in comprehension\"\n\n                true ->\n                  {key, MapSet.put(seen_keys, key)}\n              end\n\n            {process_keyed({key, vars, render}, previous_prints, changed?, stream?, acc),\n             seen_keys}\n        end\n      )\n\n    # we don't need to send the diff if nothing changed;\n    if diff == %{} and count > 0 and count == map_size(previous_prints) do\n      {nil, new_prints, pending, components, template}\n    else\n      {Map.put(diff, @keyed_count, count), new_prints, pending, components, template}\n    end\n  end\n\n  # it's an existing entry and we are change tracking\n  defp process_keyed({key, new_vars, render}, previous_prints, true = changed?, stream?, acc)\n       when is_map_key(previous_prints, key) and not stream? do\n    {diff, index, new_prints, pending, components, template} = acc\n\n    %{vars: previous_vars, index: previous_index, child_prints: child_prints} =\n      Map.fetch!(previous_prints, key)\n\n    vars_changed =\n      Enum.reduce(new_vars, Map.put(previous_vars, :__changed__, %{}), fn\n        {key, value}, acc ->\n          Phoenix.Component.assign(acc, key, value)\n      end)\n      |> Map.fetch!(:__changed__)\n\n    {_counter, child_diff, child_prints, pending, components, template} =\n      traverse_dynamic(\n        render.(vars_changed, changed?),\n        child_prints,\n        pending,\n        components,\n        template,\n        changed?\n      )\n\n    new_prints =\n      Map.put(new_prints, key, %{index: index, vars: new_vars, child_prints: child_prints})\n\n    # if the diff is empty, we need to check if the item moved\n    if child_diff == %{} or child_diff == nil do\n      # check if the entry moved, then annotate it with the previous index\n      diff = if previous_index != index, do: Map.put(diff, index, previous_index), else: diff\n      {diff, index + 1, new_prints, pending, components, template}\n    else\n      child_diff =\n        if previous_index != index do\n          [previous_index, child_diff]\n        else\n          child_diff\n        end\n\n      {Map.put(diff, index, child_diff), index + 1, new_prints, pending, components, template}\n    end\n  end\n\n  # it's a new entry\n  defp process_keyed({key, vars, render}, _previous_prints, _changed?, stream?, acc) do\n    {diff, index, new_prints, pending, components, template} = acc\n\n    {_counter, child_diff, child_prints, pending, components, template} =\n      traverse_dynamic(\n        render.(%{}, false),\n        %{},\n        pending,\n        components,\n        template,\n        # we need to disable change-tracking to force a full render,\n        # even if some parts of the template might not have changed themselves\n        false\n      )\n\n    # if this is a stream, we don't store any fingerprints\n    new_prints =\n      if stream? do\n        {%{}, nil}\n      else\n        Map.put(new_prints, key, %{index: index, vars: vars, child_prints: child_prints})\n      end\n\n    diff = Map.put(diff, index, child_diff)\n\n    {diff, index + 1, new_prints, pending, components, template}\n  end\n\n  defp maybe_share_template(map, fingerprint, static, {print_to_pos, pos_to_static}) do\n    case print_to_pos do\n      %{^fingerprint => pos} ->\n        {Map.put(map, @static, pos), {print_to_pos, pos_to_static}}\n\n      %{} ->\n        pos = map_size(pos_to_static)\n        pos_to_static = Map.put(pos_to_static, pos, static)\n        print_to_pos = Map.put(print_to_pos, fingerprint, pos)\n        {Map.put(map, @static, pos), {print_to_pos, pos_to_static}}\n    end\n  end\n\n  defp maybe_share_template(map, _fingerprint, static, nil) do\n    {Map.put(map, @static, static), nil}\n  end\n\n  defp maybe_add_template(map, {_, template}) when template != %{},\n    do: Map.put(map, @template, template)\n\n  defp maybe_add_template(map, _new_template), do: map\n\n  defp maybe_add_stream(diff, nil = _stream), do: diff\n  defp maybe_add_stream(diff, stream), do: Map.put(diff, @stream, stream)\n\n  defp maybe_add_keyed(diff, nil = _keyed), do: diff\n  defp maybe_add_keyed(diff, keyed), do: Map.put(diff, @keyed, keyed)\n\n  ## Stateful components helpers\n\n  defp traverse_component(\n         %Component{id: id, assigns: assigns, component: component},\n         pending,\n         {cid_to_component, id_to_cid, uuids}\n       ) do\n    {cid, new?, components} =\n      case id_to_cid do\n        %{^component => %{^id => cid}} -> {cid, false, {cid_to_component, id_to_cid, uuids}}\n        %{} -> {uuids, true, {cid_to_component, id_to_cid, uuids + 1}}\n      end\n\n    entry = {cid, id, new?, assigns}\n    pending = Map.update(pending, component, [entry], &[entry | &1])\n    {cid, pending, components}\n  end\n\n  ## Component rendering\n\n  defp render_pending_components(socket, pending, cids, diffs, components) do\n    render_pending_components(socket, pending, %{}, cids, diffs, components)\n  end\n\n  defp render_pending_components(_, pending, _seen_ids, _cids, diffs, components)\n       when map_size(pending) == 0 do\n    {diffs, components}\n  end\n\n  defp render_pending_components(socket, pending, seen_ids, cids, diffs, components) do\n    acc = {{%{}, diffs, components}, seen_ids}\n\n    {{pending, diffs, components}, seen_ids} =\n      Enum.reduce(pending, acc, fn {component, entries}, acc ->\n        {{pending, diffs, components}, seen_ids} = acc\n        update_many? = function_exported?(component, :update_many, 1)\n        entries = maybe_preload_components(component, Enum.reverse(entries))\n\n        {assigns_sockets, metadata, components, seen_ids} =\n          Enum.reduce(entries, {[], [], components, seen_ids}, fn\n            {cid, id, new?, new_assigns}, {assigns_sockets, metadata, components, seen_ids} ->\n              if Map.has_key?(seen_ids, [component | id]) do\n                raise \"found duplicate ID #{inspect(id)} \" <>\n                        \"for component #{inspect(component)} when rendering template\"\n              end\n\n              {socket, components, prints} =\n                case cids do\n                  %{^cid => {_component, _id, assigns, private, prints}} ->\n                    {private, components} = unmark_for_deletion(private, components)\n                    {configure_socket_for_component(socket, assigns, private), components, prints}\n\n                  %{} ->\n                    myself_assigns = %{myself: %Phoenix.LiveComponent.CID{cid: cid}}\n\n                    {mount_component(socket, component, myself_assigns),\n                     put_cid(components, component, id, cid), new_fingerprints()}\n                end\n\n              assigns_sockets = [{new_assigns, socket} | assigns_sockets]\n              metadata = [{cid, id, prints, new?} | metadata]\n              seen_ids = Map.put(seen_ids, [component | id], true)\n              {assigns_sockets, metadata, components, seen_ids}\n          end)\n\n        assigns_sockets = Enum.reverse(assigns_sockets)\n\n        telemetry_metadata = %{\n          socket: socket,\n          component: component,\n          assigns_sockets: assigns_sockets\n        }\n\n        sockets =\n          :telemetry.span([:phoenix, :live_component, :update], telemetry_metadata, fn ->\n            sockets =\n              if update_many? do\n                component.update_many(assigns_sockets)\n              else\n                Enum.map(assigns_sockets, fn {assigns, socket} ->\n                  Utils.maybe_call_update!(socket, component, assigns)\n                end)\n              end\n\n            {sockets, Map.put(telemetry_metadata, :sockets, sockets)}\n          end)\n\n        metadata = Enum.reverse(metadata)\n        triplet = zip_components(sockets, metadata, component, cids, {pending, diffs, components})\n        {triplet, seen_ids}\n      end)\n\n    render_pending_components(socket, pending, seen_ids, cids, diffs, components)\n  end\n\n  defp zip_components(\n         [%{__struct__: Phoenix.LiveView.Socket} = socket | sockets],\n         [{cid, id, prints, new?} | metadata],\n         component,\n         cids,\n         {pending, diffs, components}\n       ) do\n    diffs = maybe_put_events(diffs, socket)\n\n    {new_pending, diffs, components} =\n      render_component(socket, component, id, prints, cid, new?, cids, diffs, components)\n\n    pending = Map.merge(pending, new_pending, fn _, v1, v2 -> v2 ++ v1 end)\n    zip_components(sockets, metadata, component, cids, {pending, diffs, components})\n  end\n\n  defp zip_components([], [], _component, _cids, acc) do\n    acc\n  end\n\n  defp zip_components(_sockets, _metadata, component, _cids, _acc) do\n    raise \"#{inspect(component)}.update_many/1 must return a list of Phoenix.LiveView.Socket \" <>\n            \"of the same length as the input list, got mismatched return type\"\n  end\n\n  defp maybe_preload_components(component, entries) do\n    if function_exported?(component, :preload, 1) do\n      IO.warn(\"#{inspect(component)}.preload/1 is deprecated, use update_many/1 instead\")\n      list_of_assigns = Enum.map(entries, fn {_cid, _id, _new?, new_assigns} -> new_assigns end)\n      result = component.preload(list_of_assigns)\n      zip_preloads(result, entries, component, result)\n    else\n      entries\n    end\n  end\n\n  defp maybe_call_preload!(module, assigns) do\n    if function_exported?(module, :preload, 1) do\n      IO.warn(\"#{inspect(module)}.preload/1 is deprecated, use update_many/1 instead\")\n      [new_assigns] = module.preload([assigns])\n      new_assigns\n    else\n      assigns\n    end\n  end\n\n  defp zip_preloads([new_assigns | assigns], [{cid, id, new?, _} | entries], component, preloaded)\n       when is_map(new_assigns) do\n    [{cid, id, new?, new_assigns} | zip_preloads(assigns, entries, component, preloaded)]\n  end\n\n  defp zip_preloads([], [], _component, _preloaded) do\n    []\n  end\n\n  defp zip_preloads(_, _, component, preloaded) do\n    raise ArgumentError,\n          \"expected #{inspect(component)}.preload/1 to return a list of maps of the same length \" <>\n            \"as the list of assigns given, got: #{inspect(preloaded)}\"\n  end\n\n  defp render_component(socket, component, id, prints, cid, new?, cids, diffs, components) do\n    changed? = new? or Utils.changed?(socket)\n\n    {socket, prints, pending, diff, components} =\n      if changed? do\n        rendered = component_to_rendered(socket, component, id)\n\n        {changed?, linked_cid, prints} =\n          maybe_reuse_static(rendered, component, prints, cids, components)\n\n        {diff, prints, pending, components, nil} =\n          traverse(rendered, prints, %{}, components, nil, changed?)\n\n        children_cids =\n          for {_component, list} <- pending,\n              entry <- list,\n              do: elem(entry, 0)\n\n        diff = if linked_cid, do: Map.put(diff, @static, linked_cid), else: diff\n\n        socket =\n          put_in(socket.private.children_cids, children_cids)\n          |> Lifecycle.after_render()\n          |> Utils.clear_changed()\n\n        {socket, prints, pending, diff, components}\n      else\n        {socket, prints, %{}, %{}, components}\n      end\n\n    diffs =\n      if diff != %{} or new? do\n        Map.put(diffs, cid, diff)\n      else\n        diffs\n      end\n\n    dump =\n      socket\n      |> Utils.clear_temp()\n      |> dump_component(component, id, prints)\n\n    {cid_to_component, id_to_cid, uuids} = components\n    cid_to_component = Map.put(cid_to_component, cid, dump)\n    {pending, diffs, {cid_to_component, id_to_cid, uuids}}\n  end\n\n  defp unmark_for_deletion(private, {cid_to_component, id_to_cid, uuids}) do\n    {private, cid_to_component} = do_unmark_for_deletion(private, cid_to_component)\n    {private, {cid_to_component, id_to_cid, uuids}}\n  end\n\n  defp do_unmark_for_deletion(private, cids) do\n    {marked?, private} = Map.pop(private, @marked_for_deletion, false)\n\n    cids =\n      if marked? do\n        Enum.reduce(private.children_cids, cids, fn cid, cids ->\n          case cids do\n            %{^cid => {component, id, assigns, private, prints}} ->\n              {private, cids} = do_unmark_for_deletion(private, cids)\n              Map.put(cids, cid, {component, id, assigns, private, prints})\n\n            %{} ->\n              cids\n          end\n        end)\n      else\n        cids\n      end\n\n    {private, cids}\n  end\n\n  # 32 is one bucket from large maps\n  @attempts 32\n\n  # If the component is new or is getting a new static root, we search if another\n  # component has the same tree root. If so, we will point to the whole existing\n  # component tree but say all entries require a full render.\n  #\n  # When looking up for an existing component, we first look into the tree from the\n  # previous render, then we look at the new render. This is to avoid using a tree\n  # that will be changed before it is sent to the client.\n  #\n  # We don't want to traverse all of the components, so we will try it @attempts times.\n  defp maybe_reuse_static(rendered, component, prints, old_cids, components) do\n    {new_cids, id_to_cid, _uuids} = components\n    {current_print, _} = prints\n    %{fingerprint: print} = rendered\n\n    with true <- current_print != print,\n         iterator = :maps.iterator(Map.fetch!(id_to_cid, component)),\n         {cid, existing_prints} <-\n           find_same_component_print(print, iterator, old_cids, new_cids, @attempts) do\n      {false, cid, existing_prints}\n    else\n      _ -> {true, nil, prints}\n    end\n  end\n\n  defp find_same_component_print(_print, _iterator, _old_cids, _new_cids, 0), do: :none\n\n  defp find_same_component_print(print, iterator, old_cids, new_cids, attempts) do\n    case :maps.next(iterator) do\n      {_, cid, iterator} ->\n        case old_cids do\n          # if a component is marked for deletion, we cannot share its statics since it may be removed\n          %{^cid => {_, _, _, %{@marked_for_deletion => true}, {^print, _} = _tree}} ->\n            find_same_component_print(print, iterator, old_cids, new_cids, attempts - 1)\n\n          %{^cid => {_, _, _, _private, {^print, _} = tree}} ->\n            {-cid, tree}\n\n          %{} ->\n            case new_cids do\n              %{^cid => {_, _, _, _, {^print, _} = tree}} -> {cid, tree}\n              %{} -> find_same_component_print(print, iterator, old_cids, new_cids, attempts - 1)\n            end\n        end\n\n      :none ->\n        :none\n    end\n  end\n\n  defp put_cid({id_to_components, id_to_cid, uuids}, component, id, cid) do\n    inner = Map.get(id_to_cid, component, %{})\n    {id_to_components, Map.put(id_to_cid, component, Map.put(inner, id, cid)), uuids}\n  end\n\n  defp fetch_cid(\n         %Phoenix.LiveComponent.CID{cid: cid},\n         {cid_to_components, _id_to_cid, _} = _components\n       ) do\n    case cid_to_components do\n      %{^cid => {component, _id, _assigns, _private, _fingerprints}} -> {:ok, {cid, component}}\n      %{} -> :error\n    end\n  end\n\n  defp fetch_cid({component, id}, {_cid_to_components, id_to_cid, _} = _components) do\n    case id_to_cid do\n      %{^component => %{^id => cid}} -> {:ok, {cid, component}}\n      %{} -> :error\n    end\n  end\n\n  defp mount_component(socket, component, assigns) do\n    private =\n      socket.private\n      |> Map.take([:conn_session, :root_view])\n      |> Map.put(:live_temp, %{})\n      |> Map.put(:children_cids, [])\n      |> Map.put(:lifecycle, %Phoenix.LiveView.Lifecycle{})\n\n    socket =\n      configure_socket_for_component(socket, assigns, private)\n      |> Utils.assign(:flash, %{})\n\n    Utils.maybe_call_live_component_mount!(socket, component)\n  end\n\n  defp configure_socket_for_component(socket, assigns, private) do\n    %{\n      socket\n      | assigns: Map.put(assigns, :__changed__, %{}),\n        private: private,\n        redirected: nil\n    }\n  end\n\n  defp dump_component(socket, component, id, prints) do\n    {component, id, socket.assigns, socket.private, prints}\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/engine.ex",
    "content": "defmodule Phoenix.LiveView.Component do\n  @moduledoc \"\"\"\n  The struct returned by components in .heex templates.\n\n  This component is never meant to be output directly\n  into the template. It should always be handled by\n  the diffing algorithm.\n  \"\"\"\n\n  defstruct [:id, :component, :assigns]\n\n  @type t :: %__MODULE__{\n          id: binary(),\n          component: module(),\n          assigns: map()\n        }\n\n  defimpl Phoenix.HTML.Safe do\n    def to_iodata(%{id: id, component: component}) do\n      raise ArgumentError, \"\"\"\n      cannot convert component #{inspect(component)} with id #{inspect(id)} to HTML.\n\n      A component must always be returned directly as part of a LiveView template.\n\n      For example, this is not allowed:\n\n          <%= content_tag :div do %>\n            <.live_component module={SomeComponent} id=\"myid\" />\n          <% end %>\n\n      That's because the component is inside `content_tag`. However, this works:\n\n          <div>\n            <.live_component module={SomeComponent} id=\"myid\" />\n          </div>\n\n      Components are also allowed inside Elixir's special forms, such as\n      `if`, `for`, `case`, and friends.\n\n          <%= for item <- items do %>\n            <.live_component module={SomeComponent} id={item} />\n          <% end %>\n\n      However, using other module functions such as `Enum`, will not work:\n\n          <%= Enum.map(items, fn item -> %>\n            <.live_component module={SomeComponent} id={item} />\n          <% end %>\n      \"\"\"\n    end\n  end\nend\n\ndefmodule Phoenix.LiveView.Comprehension do\n  @moduledoc \"\"\"\n  The struct returned by for-comprehensions in .heex templates.\n  \"\"\"\n  defstruct [:static, :has_key?, :entries, :fingerprint, :stream]\n\n  @type key :: term()\n  @type keyed_render_fun :: (map(), boolean() -> [Phoenix.LiveView.Rendered.dyn()])\n\n  @type t :: %__MODULE__{\n          static: [String.t()] | non_neg_integer(),\n          has_key?: boolean(),\n          # Each entry is a three-element tuple.\n          #\n          #   The first element is the evaluated key (or nil if there is none).\n          #\n          #   The second element is a map of variables to be change-tracked.\n          #\n          #   The third element is the keyed render function that receives the vars_changed map,\n          #   and a boolean to enable or disable change tracking.\n          #\n          entries: [{key(), map(), keyed_render_fun()}],\n          fingerprint: term(),\n          stream: list() | nil\n        }\n\n  defimpl Phoenix.HTML.Safe do\n    def to_iodata(%Phoenix.LiveView.Comprehension{static: static, entries: entries}) do\n      for {_key, _vars, render} <- entries, do: to_iodata(static, render.(%{}, false))\n    end\n\n    defp to_iodata([static_head | static_tail], [%_{} = struct | dynamic_tail]) do\n      dynamic_head = Phoenix.HTML.Safe.to_iodata(struct)\n      [static_head, dynamic_head | to_iodata(static_tail, dynamic_tail)]\n    end\n\n    defp to_iodata([static_head | static_tail], [dynamic_head | dynamic_tail]) do\n      [static_head, dynamic_head | to_iodata(static_tail, dynamic_tail)]\n    end\n\n    defp to_iodata([static_head], []) do\n      [static_head]\n    end\n  end\nend\n\ndefmodule Phoenix.LiveView.Rendered do\n  @moduledoc \"\"\"\n  The struct returned by .heex templates.\n\n  See a description about its fields and use cases\n  in `Phoenix.LiveView.Engine` docs.\n  \"\"\"\n\n  defstruct [:static, :dynamic, :fingerprint, :root, caller: :not_available]\n\n  @type dyn ::\n          nil\n          | iodata()\n          | Phoenix.LiveView.Rendered.t()\n          | Phoenix.LiveView.Comprehension.t()\n          | Phoenix.LiveView.Component.t()\n\n  @type t :: %__MODULE__{\n          static: [String.t()],\n          dynamic: (boolean() -> [dyn()]),\n          fingerprint: integer(),\n          root: nil | true | false,\n          caller:\n            :not_available\n            | {module(), function :: {atom(), non_neg_integer()}, file :: String.t(),\n               line :: pos_integer()}\n        }\n\n  defimpl Phoenix.HTML.Safe do\n    def to_iodata(data) do\n      recur_iodata(data)\n    end\n\n    defp recur_iodata(%Phoenix.LiveView.Rendered{static: static, dynamic: dynamic}) do\n      recur_iodata(static, dynamic.(false), [])\n    end\n\n    defp recur_iodata(%_{} = struct) do\n      Phoenix.HTML.Safe.to_iodata(struct)\n    end\n\n    defp recur_iodata(other) do\n      other\n    end\n\n    defp recur_iodata([static_head | static_tail], [dynamic_head | dynamic_tail], acc) do\n      recur_iodata(static_tail, dynamic_tail, [recur_iodata(dynamic_head), static_head | acc])\n    end\n\n    defp recur_iodata([static_head], [], acc) do\n      Enum.reverse([static_head | acc])\n    end\n  end\nend\n\ndefmodule Phoenix.LiveView.Engine do\n  @moduledoc ~S\"\"\"\n  An `EEx` template engine that tracks changes.\n\n  This is often used by `Phoenix.LiveView.TagEngine` which also adds\n  HTML validation. In the documentation below, we will explain how it\n  works internally. For user-facing documentation, see `Phoenix.LiveView`.\n\n  ## Phoenix.LiveView.Rendered\n\n  Whenever you render a live template, it returns a\n  `Phoenix.LiveView.Rendered` structure. This structure has\n  three fields: `:static`, `:dynamic` and `:fingerprint`.\n\n  The `:static` field is a list of literal strings. This\n  allows the Elixir compiler to optimize this list and avoid\n  allocating its strings on every render.\n\n  The `:dynamic` field contains a function that takes a boolean argument\n  (see \"Tracking changes\" below), and returns a list of dynamic content.\n  Each element in the list is either one of:\n\n    1. iodata - which is the dynamic content\n    2. nil - the dynamic content did not change\n    3. another `Phoenix.LiveView.Rendered` struct, see \"Nesting and fingerprinting\" below\n    4. a `Phoenix.LiveView.Comprehension` struct, see \"Comprehensions\" below\n    5. a `Phoenix.LiveView.Component` struct, see \"Component\" below\n\n  When you render a live template, you can convert the\n  rendered structure to iodata by alternating the static\n  and dynamic fields, always starting with a static entry\n  followed by a dynamic entry. The last entry will always\n  be static too. So the following structure:\n\n      %Phoenix.LiveView.Rendered{\n        static: [\"foo\", \"bar\", \"baz\"],\n        dynamic: fn track_changes? -> [\"left\", \"right\"] end\n      }\n\n  Results in the following content to be sent over the wire\n  as iodata:\n\n      [\"foo\", \"left\", \"bar\", \"right\", \"baz\"]\n\n  This is also what calling `Phoenix.HTML.Safe.to_iodata/1`\n  with a `Phoenix.LiveView.Rendered` structure returns.\n\n  Of course, the benefit of live templates is exactly\n  that you do not need to send both static and dynamic\n  segments every time. So let's talk about tracking changes.\n\n  ## Tracking changes\n\n  By default, a live template does not track changes.\n  Change tracking can be enabled by including a changed\n  map in the assigns with the key `__changed__` and passing\n  `true` to the dynamic parts. The map should contain\n  the name of any changed field as key and the boolean\n  true as value. If a field is not listed in `__changed__`,\n  then it is always considered unchanged.\n\n  If a field is unchanged and live believes a dynamic\n  expression no longer needs to be computed, its value\n  in the `dynamic` list will be `nil`. This information\n  can be leveraged to avoid sending data to the client.\n\n  ## Nesting and fingerprinting\n\n  `Phoenix.LiveView` also tracks changes across live\n  templates. Therefore, if your view has this:\n\n  ```heex\n  {render(\"form.html\", assigns)}\n  ```\n\n  Phoenix will be able to track what is static and dynamic\n  across templates, as well as what changed. A rendered\n  nested `live` template will appear in the `dynamic`\n  list as another `Phoenix.LiveView.Rendered` structure,\n  which must be handled recursively.\n\n  However, because the rendering of live templates can\n  be dynamic in itself, it is important to distinguish\n  which live template was rendered. For example,\n  imagine this code:\n\n  ```heex\n  <%= if something?, do: render(\"one.html\", assigns), else: render(\"other.html\", assigns) %>\n  ```\n\n  To solve this, all `Phoenix.LiveView.Rendered` structs\n  also contain a fingerprint field that uniquely identifies\n  it. If the fingerprints are equal, you have the same\n  template, and therefore it is possible to only transmit\n  its changes.\n\n  ## Comprehensions\n\n  Another optimization done by live templates is to\n  track comprehensions. If your code has this:\n\n  ```heex\n  <%= for point <- @points do %>\n    x: {point.x}\n    y: {point.y}\n  <% end %>\n  ```\n\n  Instead of rendering all points with both static and\n  dynamic parts, it returns a `Phoenix.LiveView.Comprehension`\n  struct with the static parts, that are shared across all\n  points, and a list of entries with a render function for the\n  dynamics inside. If `@points` is a list with `%{x: 1, y: 2}`\n  and `%{x: 3, y: 4}`, the above expression would return:\n\n      %Phoenix.LiveView.Comprehension{\n        static: [\"\\n  x: \", \"\\n  y: \", \"\\n\"],\n        entries: [\n          {nil, %{point: %{x: 1, y: 2}}, fn vars_changed, track_changes? -> ... end,\n          {nil, %{point: %{x: 3, y: 4}}, fn vars_changed, track_changes? -> ... end,\n        ]\n      }\n\n  This allows live templates to send the static parts only once,\n  regardless of the number of items. Moreover, the diff algorithm\n  also tracks the variables introduced by the comprehension as part\n  of the entries and calculates which variables changed between renders.\n\n  In HEEx templates, a `:key` attribute can be provided when using `:for`\n  on a tag to make the diffing more efficient. By default, the index\n  of each item is used for diffing, which means that if an element is\n  prepended to the list, the whole collection is sent again.\n\n  ## Components\n\n  Live also supports stateful components defined with\n  `Phoenix.LiveComponent`. Since they are stateful, they are always\n  handled lazily by the diff algorithm.\n  \"\"\"\n\n  alias Phoenix.HTML.Form\n  @behaviour Phoenix.Template.Engine\n\n  @impl true\n  def compile(path, _name) do\n    trim = Application.get_env(:phoenix, :trim_on_html_eex_engine, true)\n    EEx.compile_file(path, engine: __MODULE__, line: 1, trim: trim)\n  end\n\n  @behaviour EEx.Engine\n  @assigns_var Macro.var(:assigns, nil)\n\n  @impl true\n  def init(opts) do\n    # Phoenix.LiveView.TagEngine calls this engine in a non-linear order\n    # to evaluate slots, which can lead to variable conflicts. Therefore we\n    # use a counter to ensure all variable names are unique.\n    %{\n      static: [],\n      dynamic: [],\n      counter: :counters.new(1, []),\n      caller: opts[:caller]\n    }\n  end\n\n  @impl true\n  def handle_begin(state) do\n    %{state | static: [], dynamic: []}\n  end\n\n  @impl true\n  def handle_end(state, opts \\\\ []) do\n    %{static: static, dynamic: dynamic} = state\n    safe = {:safe, Enum.reverse(static)}\n    meta = [live_rendered: true] ++ Keyword.get(opts, :meta, [])\n    {:__block__, meta, Enum.reverse([safe | dynamic])}\n  end\n\n  @impl true\n  def handle_body(state, opts \\\\ []) do\n    {:ok, rendered} =\n      state\n      |> handle_end(opts)\n      |> to_rendered_struct({:untainted, %{}}, %{}, state.caller, opts)\n\n    quote do\n      require Phoenix.LiveView.Engine\n      vars_changed = nil\n      unquote(rendered)\n    end\n  end\n\n  @impl true\n  def handle_text(state, _meta, text) do\n    %{static: static} = state\n    %{state | static: [text | static]}\n  end\n\n  @impl true\n  def handle_expr(state, \"=\", ast) do\n    %{static: static, dynamic: dynamic, counter: counter} = state\n    i = :counters.get(counter, 1)\n    var = Macro.var(:\"v#{i}\", __MODULE__)\n    ast = quote do: unquote(var) = unquote(__MODULE__).to_safe(unquote(ast))\n\n    :counters.add(counter, 1, 1)\n    %{state | dynamic: [ast | dynamic], static: [var | static]}\n  end\n\n  def handle_expr(state, \"\", ast) do\n    %{dynamic: dynamic} = state\n    %{state | dynamic: [ast | dynamic]}\n  end\n\n  def handle_expr(state, marker, ast) do\n    EEx.Engine.handle_expr(state, marker, ast)\n  end\n\n  ## Entry point for rendered structs\n\n  defp to_rendered_struct(expr, vars, assigns, caller, opts) do\n    with {:__block__, [live_rendered: true] ++ meta, entries} <- expr,\n         {dynamic, [{:safe, static}]} <- Enum.split(entries, -1) do\n      {block, static, dynamic, fingerprint} =\n        analyze_static_and_dynamic(static, dynamic, vars, assigns, caller)\n\n      static =\n        case Keyword.fetch(meta, :template_annotation) do\n          {:ok, {before, aft}} ->\n            case static do\n              [] ->\n                [\"#{before}#{aft}\"]\n\n              [first | rest] ->\n                List.update_at([to_string(before) <> first | rest], -1, &(&1 <> to_string(aft)))\n            end\n\n          :error ->\n            static\n        end\n\n      changed =\n        quote generated: true do\n          case unquote(@assigns_var) do\n            %{__changed__: changed} when track_changes? -> changed\n            _ -> nil\n          end\n        end\n\n      root = Keyword.get(opts, :root, meta[:root])\n\n      dynamic =\n        if dynamic == [] do\n          quote do\n            fn _ ->\n              _ = unquote(@assigns_var)\n              []\n            end\n          end\n        else\n          quote generated: true do\n            fn track_changes? ->\n              changed = unquote(changed)\n              vars_changed = if track_changes?, do: vars_changed\n              unquote({:__block__, [], block})\n              unquote(dynamic)\n            end\n          end\n        end\n\n      {:ok,\n       quote do\n         dynamic = unquote(dynamic)\n\n         %Phoenix.LiveView.Rendered{\n           static: unquote(static),\n           dynamic: dynamic,\n           fingerprint: unquote(fingerprint),\n           root: unquote(root)\n         }\n       end}\n    else\n      _ -> :error\n    end\n  end\n\n  defmacrop to_safe_match(var, ast) do\n    quote do\n      {:=, [],\n       [\n         {_, _, __MODULE__} = unquote(var),\n         {{:., _, [__MODULE__, :to_safe]}, _, [unquote(ast)]}\n       ]}\n    end\n  end\n\n  defp analyze_static_and_dynamic(static, dynamic, initial_vars, assigns, caller) do\n    {block, _} =\n      Enum.map_reduce(dynamic, initial_vars, fn\n        to_safe_match(var, ast), vars ->\n          vars = set_vars(initial_vars, vars)\n          {ast, keys, vars} = analyze_and_return_tainted_keys(ast, vars, assigns, caller)\n          live_struct = to_live_struct(ast, vars, assigns, caller)\n          {to_conditional_var(keys, var, live_struct), vars}\n\n        ast, vars ->\n          vars = set_vars(initial_vars, vars)\n          {ast, vars, _} = analyze(ast, vars, assigns, caller)\n          {ast, vars}\n      end)\n\n    {static, dynamic} = bins_and_vars(static)\n    {block, static, dynamic, fingerprint(block, static)}\n  end\n\n  ## Optimize possible expressions into live structs (rendered / comprehensions)\n\n  defp to_live_struct({:for, _, [_ | _]} = expr, vars, assigns, caller) do\n    with {:for, meta, [gen | args]} <- expr,\n         {:<-, gen_meta, [gen_pattern, gen_collection]} <- gen,\n         {filters, [[do: {:__block__, _, block}]]} <- Enum.split(args, -1),\n         {dynamic, [{:safe, static}]} <- Enum.split(block, -1) do\n      gen_var = Macro.unique_var(:for, __MODULE__)\n\n      gen_collection =\n        quote do\n          unquote(gen_var) =\n            Phoenix.LiveView.LiveStream.mark_consumable(unquote(gen_collection))\n        end\n\n      {gen_pattern, variables} = mark_variables_as_change_tracked(gen_pattern, %{})\n      {gen_pattern, vars, _} = analyze(gen_pattern, vars, assigns, caller)\n\n      {block, static, dynamic, fingerprint} =\n        analyze_static_and_dynamic(static, dynamic, vars, %{}, caller)\n\n      key_expr =\n        case Keyword.get(gen_meta, :key_expr) do\n          nil ->\n            nil\n\n          expr ->\n            {expr, _vars, _} = analyze(expr, vars, assigns, caller)\n            expr\n        end\n\n      dynamic =\n        quote generated: true do\n          fn local_vars_changed, track_changes? ->\n            vars_changed =\n              case local_vars_changed do\n                %{} when track_changes? ->\n                  Map.merge(vars_changed || %{}, local_vars_changed)\n\n                _ ->\n                  nil\n              end\n\n            changed = if track_changes?, do: changed\n            unquote({:__block__, [], block ++ [dynamic]})\n          end\n        end\n\n      entry =\n        quote do\n          {unquote(key_expr), %{unquote_splicing(Map.to_list(variables))}, unquote(dynamic)}\n        end\n\n      gen = {:<-, gen_meta, [gen_pattern, gen_var]}\n      for = {:for, meta, [gen | filters] ++ [[do: entry]]}\n\n      quote do\n        unquote(gen_collection)\n\n        Phoenix.LiveView.LiveStream.annotate_comprehension(\n          %Phoenix.LiveView.Comprehension{\n            has_key?: unquote(key_expr != nil),\n            static: unquote(static),\n            entries: unquote(for),\n            fingerprint: unquote(fingerprint)\n          },\n          unquote(gen_var)\n        )\n      end\n    else\n      _ -> to_safe(expr, true)\n    end\n  end\n\n  defp to_live_struct({left, meta, [_ | _] = args}, vars, assigns, caller) do\n    call = extract_call(left)\n\n    args =\n      with :live <- classify_taint(call, args),\n           {args, [opts]} when is_list(opts) <- Enum.split(args, -1) do\n        # The reason we can safely ignore assigns here is because\n        # each branch in the live/render constructs are their own\n        # rendered struct and, if the rendered has a new fingerprint,\n        # then change tracking is fully disabled.\n        #\n        # For example, take this code:\n        #\n        #     <%= if @foo do %>\n        #       <%= @bar %>\n        #     <% else %>\n        #       <%= @baz %>\n        #     <% end %>\n        #\n        # In theory, @bar and @baz should be recomputed whenever\n        # @foo changes, because changing @foo may require a value\n        # that was not available on the page to show. However,\n        # given the branches have different fingerprints, the\n        # diff mechanism takes care of forcing all assigns to\n        # be rendered without us needing to handle it here.\n        #\n        # Similarly, when expanding the blocks, we can remove all\n        # untainting, as the parent untainting is already causing\n        # the block to be rendered and then we can proceed with\n        # its own tainting.\n        {args, vars, _} = analyze_list(args, vars, assigns, caller, [])\n\n        opts =\n          for {key, value} <- opts do\n            {key, maybe_block_to_rendered(value, vars, caller)}\n          end\n\n        args ++ [opts]\n      else\n        _ -> args\n      end\n\n    args =\n      case {call, args} do\n        # If we have a component, we provide change tracking to individual keys.\n        {:component, [fun, expr, metadata]} ->\n          [fun, to_component_tracking(meta, fun, expr, [], vars, caller), metadata]\n\n        {_, _} ->\n          args\n      end\n\n    to_safe({left, meta, args}, true)\n  end\n\n  defp to_live_struct(expr, _vars, _assigns, _caller) do\n    to_safe(expr, true)\n  end\n\n  @doc false\n  def mark_variables_as_change_tracked({:^, _, [_]} = ast, vars) do\n    {ast, vars}\n  end\n\n  def mark_variables_as_change_tracked({:\"::\", meta, [left, right]}, vars) do\n    {left, vars} = mark_variables_as_change_tracked(left, vars)\n    {{:\"::\", meta, [left, right]}, vars}\n  end\n\n  def mark_variables_as_change_tracked({name, meta, context}, vars)\n      when is_atom(name) and is_list(meta) and is_atom(context) do\n    case Atom.to_string(name) do\n      \"_\" <> _rest ->\n        {{name, meta, context}, vars}\n\n      _ ->\n        var = {name, [change_track: true] ++ meta, context}\n        {var, Map.put(vars, name, var)}\n    end\n  end\n\n  def mark_variables_as_change_tracked({left, meta, right}, vars) do\n    {left, vars} = mark_variables_as_change_tracked(left, vars)\n    {right, vars} = mark_variables_as_change_tracked(right, vars)\n    {{left, meta, right}, vars}\n  end\n\n  def mark_variables_as_change_tracked({left, right}, vars) do\n    {left, vars} = mark_variables_as_change_tracked(left, vars)\n    {right, vars} = mark_variables_as_change_tracked(right, vars)\n    {{left, right}, vars}\n  end\n\n  def mark_variables_as_change_tracked([_ | _] = list, vars) do\n    Enum.map_reduce(list, vars, &mark_variables_as_change_tracked/2)\n  end\n\n  def mark_variables_as_change_tracked(other, vars) do\n    {other, vars}\n  end\n\n  defp extract_call({:., _, [{:__aliases__, _, [:Phoenix, :LiveView, :TagEngine]}, func]}),\n    do: func\n\n  defp extract_call(call),\n    do: call\n\n  defp maybe_block_to_rendered([{:->, _, _} | _] = blocks, vars, caller) do\n    for {:->, meta, [args, block]} <- blocks do\n      {args, vars, assigns} = analyze_list(args, vars, %{}, caller, [])\n\n      case to_rendered_struct(block, untaint_vars(vars), assigns, caller, []) do\n        {:ok, rendered} -> {:->, meta, [args, rendered]}\n        :error -> {:->, meta, [args, block]}\n      end\n    end\n  end\n\n  defp maybe_block_to_rendered(block, vars, caller) do\n    case to_rendered_struct(block, untaint_vars(vars), %{}, caller, []) do\n      {:ok, rendered} -> rendered\n      :error -> block\n    end\n  end\n\n  defp to_conditional_var(:all, var, live_struct) do\n    quote do: unquote(var) = unquote(live_struct)\n  end\n\n  defp to_conditional_var(keys, var, live_struct) when keys == %{} do\n    quote generated: true do\n      unquote(var) =\n        case changed do\n          %{} -> nil\n          _ -> unquote(live_struct)\n        end\n    end\n  end\n\n  defp to_conditional_var(keys, var, live_struct) do\n    quote do\n      unquote(var) =\n        case unquote(changed_assigns(keys)) do\n          true -> unquote(live_struct)\n          false -> nil\n        end\n    end\n  end\n\n  defp changed_assigns(assigns) do\n    checks =\n      for {{changed_var, key}, _} <- assigns, not nested_and_parent_is_checked?(key, assigns) do\n        changed = Macro.var(changed_var, __MODULE__)\n\n        case key do\n          [assign] ->\n            quote do\n              unquote(__MODULE__).changed_assign?(unquote(changed), unquote(assign))\n            end\n\n          [assign | tail] ->\n            assigns_var =\n              case changed_var do\n                :changed ->\n                  @assigns_var\n\n                :vars_changed ->\n                  # we pass a map %{var: var} for nested change tracking\n                  quote do\n                    %{unquote(assign) => unquote(Macro.var(assign, nil))}\n                  end\n              end\n\n            quote do\n              unquote(__MODULE__).nested_changed_assign?(\n                unquote(tail),\n                unquote(assign),\n                unquote(assigns_var),\n                unquote(changed)\n              )\n            end\n        end\n      end\n\n    Enum.reduce(checks, &{:or, [], [&1, &2]})\n  end\n\n  defguardp is_access(mod)\n            when mod == Access or\n                   (is_tuple(mod) and elem(mod, 0) == :__aliases__ and elem(mod, 2) == [:Access])\n\n  # If we are accessing @foo.bar.baz but in the same place we also pass\n  # @foo.bar or @foo, we don't need to check for @foo.bar.baz.\n\n  # If there is no nesting, then we are not nesting.\n  defp nested_and_parent_is_checked?([_], _assigns),\n    do: false\n\n  # Otherwise, we convert @foo.bar.baz into [:baz, :bar, :foo], discard :baz,\n  # and then check if [:foo, :bar] and then [:foo] is in it.\n  defp nested_and_parent_is_checked?(keys, assigns),\n    do: parent_is_checked?(tl(Enum.reverse(keys)), assigns)\n\n  defp parent_is_checked?([], _assigns),\n    do: false\n\n  defp parent_is_checked?(rest, assigns),\n    do: Map.has_key?(assigns, Enum.reverse(rest)) or parent_is_checked?(tl(rest), assigns)\n\n  ## Component keys change tracking\n\n  defp to_component_tracking(meta, fun, expr, extra, vars, caller) do\n    # Separate static and dynamic parts\n    {static, dynamic} =\n      case expr do\n        {{:., _, [{:__aliases__, _, [:Map]}, :merge]}, _, [dynamic, {:%{}, _, static}]} ->\n          {static, dynamic}\n\n        {:%{}, _, static} ->\n          {static, %{}}\n\n        static ->\n          {static, %{}}\n      end\n\n    # And now validate the static bits. If they are not valid,\n    # treat the whole thing as dynamic.\n    {static, dynamic} =\n      if Keyword.keyword?(static) do\n        {static, dynamic}\n      else\n        {[], expr}\n      end\n\n    static_extra = extra ++ static\n\n    # Rewrite slots in static parts\n    static = slots_to_rendered(static, vars, caller, Keyword.get(meta, :slots, []))\n\n    static =\n      cond do\n        # Live components have their own tracking, so we skip the logic below\n        match?({:&, _, [{:/, _, [{:live_component, _, _}, 1]}]}, fun) ->\n          static\n\n        # Compute change tracking for static parts\n        static_extra != [] and (dynamic == %{} or without_dependencies?(dynamic, vars, caller)) ->\n          keys =\n            for {key, value} <- static_extra,\n                # We pass empty assigns because if this code is rendered,\n                # it means that upstream assigns were change tracked.\n                {_, keys, _} = analyze_and_return_tainted_keys(value, vars, %{}, caller),\n                # If keys are empty, it is never changed.\n                keys != %{},\n                do: {key, to_component_keys(keys)}\n\n          # [id: [changed: [:id]], children: [vars_changed: [:children]]]\n          # -> [changed: [:id], vars_changed: [:children]]\n          vars_changed_keys =\n            Enum.flat_map(keys, fn\n              {_assign, :all} -> []\n              {_assign, keys} -> keys\n            end)\n\n          static_changed =\n            quote do\n              unquote(__MODULE__).to_component_static(\n                unquote(keys),\n                unquote(@assigns_var),\n                changed,\n                %{unquote_splicing(vars_changed_vars(vars_changed_keys))},\n                vars_changed\n              )\n            end\n\n          [__changed__: static_changed] ++ static\n\n        true ->\n          # We must disable change tracking when there is a non empty dynamic part\n          # (for example `<.my_component {assigns}>`) in case the parent assigns\n          # already contain a `__changed__` key\n          [__changed__: nil] ++ static\n      end\n\n    if dynamic == %{} do\n      quote do: %{unquote_splicing(static)}\n    else\n      quote do: Map.merge(unquote(dynamic), %{unquote_splicing(static)})\n    end\n  end\n\n  defp without_dependencies?(ast, vars, caller) do\n    {_, keys, _} = analyze_and_return_tainted_keys(ast, vars, %{}, caller)\n    keys == %{}\n  end\n\n  defp to_component_keys(:all), do: :all\n  defp to_component_keys(map), do: Map.keys(map)\n\n  defp vars_changed_vars(:all), do: []\n\n  defp vars_changed_vars(keys) do\n    # when we calculate the changed map for components, we need to\n    # also pass the variables for vars_changed;\n    # we do that by going over the keys [changed: [:id], vars_changed: [:item, struct: :name]]\n    # which would mean that the component accesses @id and item.name, thus\n    # we need to pass %{item: item} as \"vars_changed_vars\" to be later checked by nested_changed_assign\n    Enum.flat_map(keys, fn\n      {:vars_changed, [var | _]} ->\n        [{var, Macro.var(var, nil)}]\n\n      _ ->\n        []\n    end)\n    |> Enum.uniq()\n  end\n\n  @doc false\n  def to_component_static(_keys, _assigns, nil, _vars_changed_vars, nil) do\n    nil\n  end\n\n  def to_component_static(keys, assigns, changed, vars_changed_vars, vars_changed) do\n    for {assign, entries} <- keys,\n        changed = component_changed(entries, assigns, changed, vars_changed_vars, vars_changed),\n        into: %{},\n        do: {assign, changed}\n  end\n\n  defp component_changed(:all, _assigns, _changed, _vars_changed_vars, _vars_changed), do: true\n\n  defp component_changed([path], assigns, changed, vars_changed_vars, vars_changed) do\n    case path do\n      {:changed, [key]} ->\n        changed_assign(changed, key)\n\n      {:changed, [key | tail]} ->\n        nested_changed_assign(tail, key, assigns, changed)\n\n      {:vars_changed, [key]} ->\n        changed_assign(vars_changed, key)\n\n      {:vars_changed, [key | tail]} ->\n        nested_changed_assign(tail, key, vars_changed_vars, vars_changed)\n    end\n  end\n\n  defp component_changed(entries, assigns, changed, vars_changed_vars, vars_changed) do\n    Enum.any?(entries, fn\n      {:changed, [key]} ->\n        changed_assign?(changed, key)\n\n      {:changed, [key | tail]} ->\n        nested_changed_assign?(tail, key, assigns, changed)\n\n      {:vars_changed, [key]} ->\n        changed_assign?(vars_changed, key)\n\n      {:vars_changed, [key | tail]} ->\n        nested_changed_assign?(tail, key, vars_changed_vars, vars_changed)\n    end)\n  end\n\n  defp slots_to_rendered(static, vars, caller, slots) do\n    for {key, value} <- static do\n      value =\n        if key in slots do\n          slot_to_rendered(value, key, vars, caller)\n        else\n          value\n        end\n\n      {key, value}\n    end\n  end\n\n  defp slot_to_rendered(\n         {:%{}, map_meta, [__slot__: key, inner_block: inner_block] ++ attrs} = maybe_slot,\n         key,\n         vars,\n         caller\n       ) do\n    with {call, meta, [^key, [do: block]]} <- inner_block,\n         :inner_block <- extract_call(call) do\n      inner_block =\n        case block do\n          [\n            {:->, _,\n             [\n               [{:_, _, _}],\n               {:__block__, [live_rendered: true] ++ _meta, [{:safe, _}]} = static_block\n             ]}\n          ] ->\n            # This is an optimization that removes the extra anonymous function from\n            # the TagEngine's build_component_clauses function used for slots.\n            # We can do this when the slot does not contain any dynamics, which also helps\n            # to prevent uncovered lines when rendering a component with only a named slot.\n            # In that case, the component still has an inner_block, but it only consists of\n            # whitespace.\n            maybe_block_to_rendered(static_block, vars, caller)\n\n          _ ->\n            {call, meta, [key, [do: maybe_block_to_rendered(block, vars, caller)]]}\n        end\n\n      {:%{}, map_meta, [__slot__: key, inner_block: inner_block] ++ attrs}\n    else\n      _ -> maybe_slot\n    end\n  end\n\n  defp slot_to_rendered({left, meta, args}, key, vars, caller) do\n    {left, meta, slot_to_rendered(args, key, vars, caller)}\n  end\n\n  defp slot_to_rendered({left, right}, key, vars, caller) do\n    {slot_to_rendered(left, key, vars, caller), slot_to_rendered(right, key, vars, caller)}\n  end\n\n  defp slot_to_rendered(list, key, vars, caller) when is_list(list) do\n    Enum.map(list, &slot_to_rendered(&1, key, vars, caller))\n  end\n\n  defp slot_to_rendered(other, _key, _vars, _caller) do\n    other\n  end\n\n  ## Extracts binaries and variable from iodata\n\n  defp bins_and_vars(acc),\n    do: bins_and_vars(acc, [], [])\n\n  defp bins_and_vars([bin1, bin2 | acc], bins, vars) when is_binary(bin1) and is_binary(bin2),\n    do: bins_and_vars([bin1 <> bin2 | acc], bins, vars)\n\n  defp bins_and_vars([bin, var | acc], bins, vars) when is_binary(bin) and is_tuple(var),\n    do: bins_and_vars(acc, [bin | bins], [var | vars])\n\n  defp bins_and_vars([var | acc], bins, vars) when is_tuple(var),\n    do: bins_and_vars(acc, [\"\" | bins], [var | vars])\n\n  defp bins_and_vars([bin], bins, vars) when is_binary(bin),\n    do: {Enum.reverse([bin | bins]), Enum.reverse(vars)}\n\n  defp bins_and_vars([], bins, vars),\n    do: {Enum.reverse([\"\" | bins]), Enum.reverse(vars)}\n\n  ## Assigns tracking\n\n  # Here we compute if an expression should be always computed,\n  # never computed, or some times computed based on assigns.\n  #\n  # If any assign is used, we store it in the assigns and use it to compute\n  # if it should be changed or not.\n  #\n  # However, operations that change the lexical scope, such as imports and\n  # defining variables, taint the analysis. Because variables can be set at\n  # any moment in Elixir, via macros, without appearing on the left side of\n  # `=` or in a clause, whenever we see a variable, we consider it as tainted,\n  # regardless of its position.\n  #\n  # The tainting that happens from lexical scope is called weak-tainting,\n  # because it is disabled under certain special forms. There is also\n  # strong-tainting, which are always computed. Strong-tainting only happens\n  # if the `assigns` variable is used.\n  defp analyze_and_return_tainted_keys(ast, vars, assigns, caller) do\n    {ast, vars, assigns} = analyze(ast, vars, assigns, caller)\n    {tainted_assigns?, assigns} = Map.pop(assigns, __MODULE__, false)\n    keys = if match?({:tainted, _}, vars) or tainted_assigns?, do: :all, else: assigns\n    {ast, keys, vars}\n  end\n\n  # if we find a variable (or something more complex handled by the other clauses)\n  # like foo[:bar][:baz] and foo is marked as :change_track in vars, we consider it\n  # as an assign, but look into vars_changed instead of changed\n  defp analyze_assign(\n         {name, _, context} = expr,\n         {type, map} = vars,\n         assigns,\n         caller,\n         nest\n       )\n       when is_atom(name) and is_atom(context) and is_map_key(map, name) and type != :tainted do\n    if map[name] == :change_track do\n      {expr, vars, Map.put(assigns, {:vars_changed, [name | nest]}, true)}\n    else\n      analyze(expr, vars, assigns, caller)\n    end\n  end\n\n  # @name\n  defp analyze_assign({:@, meta, [{name, _, context}]}, vars, assigns, _caller, nest)\n       when is_atom(name) and is_atom(context) do\n    expr = {{:., meta, [@assigns_var, name]}, [no_parens: true] ++ meta, []}\n    {expr, vars, Map.put(assigns, {:changed, [name | nest]}, true)}\n  end\n\n  # assigns.name\n  defp analyze_assign(\n         {{:., _, [{:assigns, _, nil}, name]}, _, args} = expr,\n         vars,\n         assigns,\n         _caller,\n         nest\n       )\n       when is_atom(name) and args in [[], nil] do\n    {expr, vars, Map.put(assigns, {:changed, [name | nest]}, true)}\n  end\n\n  # assigns[:name]\n  defp analyze_assign(\n         {{:., _, [access, :get]}, _, [{:assigns, _, nil}, name]} = expr,\n         vars,\n         assigns,\n         _caller,\n         nest\n       )\n       when is_atom(name) and is_access(access) do\n    {expr, vars, Map.put(assigns, {:changed, [name | nest]}, true)}\n  end\n\n  # Maybe: assigns.foo[:bar]\n  defp analyze_assign(\n         {{:., dot_meta, [access, :get]}, meta, [left, right]},\n         vars,\n         assigns,\n         caller,\n         nest\n       )\n       when is_access(access) do\n    {args, vars, assigns} =\n      if Macro.quoted_literal?(right) do\n        {left, vars, assigns} =\n          analyze_assign(left, vars, assigns, caller, [{:access, right} | nest])\n\n        {[left, right], vars, assigns}\n      else\n        {left, vars, assigns} = analyze(left, vars, assigns, caller)\n        {right, vars, assigns} = analyze(right, vars, assigns, caller)\n        {[left, right], vars, assigns}\n      end\n\n    {{{:., dot_meta, [Access, :get]}, meta, args}, vars, assigns}\n  end\n\n  # Maybe: assigns.foo.bar\n  defp analyze_assign({{:., dot_meta, [left, right]}, meta, args}, vars, assigns, caller, nest)\n       when args in [[], nil] do\n    {left, vars, assigns} = analyze_assign(left, vars, assigns, caller, [{:struct, right} | nest])\n    {{{:., dot_meta, [left, right]}, meta, []}, vars, assigns}\n  end\n\n  defp analyze_assign(expr, vars, assigns, caller, _nest) do\n    analyze(expr, vars, assigns, caller)\n  end\n\n  # Delegates to analyze assign\n  defp analyze({{:., _, [access, :get]}, _, [_, _]} = expr, vars, assigns, caller)\n       when is_access(access) do\n    analyze_assign(expr, vars, assigns, caller, [])\n  end\n\n  defp analyze({{:., _, [_, _]}, _, args} = expr, vars, assigns, caller) when args in [[], nil] do\n    analyze_assign(expr, vars, assigns, caller, [])\n  end\n\n  defp analyze({:@, _, [{name, _, context}]} = expr, vars, assigns, caller)\n       when is_atom(name) and is_atom(context) do\n    analyze_assign(expr, vars, assigns, caller, [])\n  end\n\n  # Assigns is a strong-taint\n  defp analyze({:assigns, _, nil} = expr, vars, assigns, _caller) do\n    {expr, vars, taint_assigns(assigns)}\n  end\n\n  # Ignore underscore\n  defp analyze({:_, _, context} = expr, vars, assigns, _caller) when is_atom(context) do\n    {expr, vars, assigns}\n  end\n\n  # Also skip special variables\n  defp analyze({name, _, context} = expr, vars, assigns, _caller)\n       when name in [:__MODULE__, :__ENV__, :__STACKTRACE__, :__DIR__] and is_atom(context) do\n    {expr, vars, assigns}\n  end\n\n  # Vars always taint unless we are in restricted mode\n  # or the variable is marked as `:change_track` for vars_changed.\n  defp analyze({name, meta, nil} = expr, {:restricted, map} = vars, assigns, caller)\n       when is_atom(name) do\n    case map do\n      %{^name => :tainted} ->\n        maybe_warn_taint(name, meta, caller)\n        {expr, {:tainted, map}, assigns}\n\n      %{^name => :change_track} ->\n        {expr, vars, Map.put(assigns, {:vars_changed, [name]}, true)}\n\n      _ ->\n        {expr, {:restricted, map}, assigns}\n    end\n  end\n\n  defp analyze({name, meta, nil} = expr, {type, map}, assigns, caller)\n       when is_atom(name) do\n    cond do\n      Map.get(map, name) == :change_track ->\n        {expr, {type, map}, Map.put(assigns, {:vars_changed, [name]}, true)}\n\n      Keyword.get(meta, :change_track) ->\n        # this is a variable inside the left-hand side of a keyed for expression;\n        # we mark it as change_track in the vars map so that we treat it as change-tracked\n        # when we see it used again later (see the previous analyze clause above)\n        {expr, {type, Map.put(map, name, :change_track)}, assigns}\n\n      true ->\n        maybe_warn_taint(name, meta, caller)\n        {expr, {:tainted, Map.put(map, name, :tainted)}, assigns}\n    end\n  end\n\n  # Quoted vars are ignored as they come from engine code.\n  defp analyze({name, _meta, context} = expr, vars, assigns, _caller)\n       when is_atom(name) and is_atom(context) do\n    {expr, vars, assigns}\n  end\n\n  # Ignore right side of |> if a variable\n  defp analyze({:|>, meta, [left, {_, _, context} = right]}, vars, assigns, caller)\n       when is_atom(context) do\n    {left, vars, assigns} = analyze(left, vars, assigns, caller)\n    {{:|>, meta, [left, right]}, vars, assigns}\n  end\n\n  # Ignore binary modifiers\n  defp analyze({:\"::\", meta, [left, right]}, vars, assigns, caller) do\n    {left, vars, assigns} = analyze(left, vars, assigns, caller)\n    {{:\"::\", meta, [left, right]}, vars, assigns}\n  end\n\n  # Handle for/with to consider the first generator.\n  # Ideally we would track all variables on the patterns and expand all generators\n  # but except for the unlikely scenario of combinations, all comprehensions will\n  # be using nested generators.\n  defp analyze({for_with, meta, [{:<-, arrow_meta, [left, right]} | args]}, vars, assigns, caller)\n       when for_with in [:for, :with] do\n    {right, vars, assigns} = analyze(right, vars, assigns, caller)\n\n    {[left | args], vars, assigns} =\n      analyze_with_restricted_vars([left | args], vars, assigns, caller)\n\n    {{for_with, meta, [{:<-, arrow_meta, [left, right]} | args]}, vars, assigns}\n  end\n\n  # Classify calls\n  defp analyze({left, meta, args}, vars, assigns, caller) do\n    call = extract_call(left)\n\n    case classify_taint(call, args) do\n      :special_form ->\n        code = quote do: unquote(__MODULE__).__raise__(unquote(call), unquote(length(args)))\n        {code, vars, assigns}\n\n      :none ->\n        {left, vars, assigns} = analyze(left, vars, assigns, caller)\n        {args, vars, assigns} = analyze_list(args, vars, assigns, caller, [])\n        {{left, meta, args}, vars, assigns}\n\n      :live ->\n        {args, [opts]} = Enum.split(args, -1)\n        {args, vars, assigns} = analyze_skip_assignment_list(args, vars, assigns, caller, [])\n        {opts, vars, assigns} = analyze_with_restricted_vars(opts, vars, assigns, caller)\n        {{left, meta, args ++ [opts]}, vars, assigns}\n\n      :never ->\n        {args, vars, assigns} = analyze_with_restricted_vars(args, vars, assigns, caller)\n        {{left, meta, args}, vars, assigns}\n    end\n  end\n\n  defp analyze({left, right}, vars, assigns, caller) do\n    {left, vars, assigns} = analyze(left, vars, assigns, caller)\n    {right, vars, assigns} = analyze(right, vars, assigns, caller)\n    {{left, right}, vars, assigns}\n  end\n\n  defp analyze([_ | _] = list, vars, assigns, caller) do\n    analyze_list(list, vars, assigns, caller, [])\n  end\n\n  defp analyze(other, vars, assigns, _caller) do\n    {other, vars, assigns}\n  end\n\n  defp analyze_list([head | tail], vars, assigns, caller, acc) do\n    {head, vars, assigns} = analyze(head, vars, assigns, caller)\n    analyze_list(tail, vars, assigns, caller, [head | acc])\n  end\n\n  defp analyze_list([], vars, assigns, _caller, acc) do\n    {Enum.reverse(acc), vars, assigns}\n  end\n\n  defp analyze_skip_assignment_list(\n         [{:=, meta, [left, right]} | tail],\n         vars,\n         assigns,\n         caller,\n         acc\n       ) do\n    {right, vars, assigns} = analyze(right, vars, assigns, caller)\n    analyze_skip_assignment_list(tail, vars, assigns, caller, [{:=, meta, [left, right]} | acc])\n  end\n\n  defp analyze_skip_assignment_list([head | tail], vars, assigns, caller, acc) do\n    {head, vars, assigns} = analyze(head, vars, assigns, caller)\n    analyze_skip_assignment_list(tail, vars, assigns, caller, [head | acc])\n  end\n\n  defp analyze_skip_assignment_list([], vars, assigns, _caller, acc) do\n    {Enum.reverse(acc), vars, assigns}\n  end\n\n  # vars is one of:\n  #\n  #   * {:tainted, map}\n  #   * {:restricted, map}\n  #   * {:untainted, map}\n  #\n  # Seeing a variable at any moment taints it unless we are inside a\n  # scope. For example, in case/cond/with/fn/try, the variable is only\n  # tainted if it came from outside of the case/cond/with/fn/try.\n  # So for those constructs we set the mode to restricted and stop\n  # collecting vars.\n  defp analyze_with_restricted_vars(ast, {kind, map}, assigns, caller) do\n    {ast, {new_kind, _}, assigns} =\n      analyze(ast, {unless_tainted(kind, :restricted), map}, assigns, caller)\n\n    {ast, {unless_tainted(new_kind, kind), map}, assigns}\n  end\n\n  defp set_vars({kind, _}, {_, map}), do: {kind, map}\n  defp untaint_vars({_, map}), do: {:untainted, map}\n\n  defp unless_tainted(:tainted, _), do: :tainted\n  defp unless_tainted(_, kind), do: kind\n\n  defp taint_assigns(assigns), do: Map.put(assigns, __MODULE__, true)\n\n  ## Callbacks\n\n  defp maybe_warn_taint(name, meta, caller) do\n    if caller && Macro.Env.has_var?(caller, {name, nil}) do\n      message = \"\"\"\n      you are accessing the variable \\\"#{name}\\\" inside a LiveView template.\n\n      Using variables in HEEx templates are discouraged as they disable change tracking. \\\n      You are only allowed to access variables defined by Elixir control-flow structures, \\\n      such as if/case/for, or those defined by the special attributes :let/:if/:for. \\\n      If you are shadowing a variable defined outside of the template using a control-flow \\\n      structure, you must choose a unique variable name within the template.\n\n      Instead of:\n\n          def add(assigns) do\n            result = assigns.a + assigns.b\n            ~H\"the result is: {result}\"\n          end\n\n      You must do:\n\n          def add(assigns) do\n            assigns = assign(assigns, :result, assigns.a + assigns.b)\n            ~H\"the result is: {@result}\"\n          end\n      \"\"\"\n\n      line = meta[:line] || caller.line\n      IO.warn(message, Macro.Env.stacktrace(%{caller | line: line}))\n    end\n  end\n\n  defp fingerprint(block, static) do\n    # The fingerprint must be unique and we don’t check for collisions in the\n    # Diff module as doing so would be expensive. Therefore it is important\n    # that the algorithm we use here has a low number of collisions.\n\n    <<fingerprint::8*16>> =\n      [block | static]\n      |> :erlang.term_to_binary()\n      |> :erlang.md5()\n\n    fingerprint\n  end\n\n  @doc false\n  defmacro __raise__(special_form, arity) do\n    message = \"cannot invoke special form #{special_form}/#{arity} inside HEEx templates\"\n    reraise ArgumentError.exception(message), Macro.Env.stacktrace(__CALLER__)\n  end\n\n  @doc false\n  defmacro to_safe(ast) do\n    to_safe(ast, false)\n  end\n\n  defp to_safe(ast, bool) do\n    to_safe(ast, line_from_expr(ast), bool)\n  end\n\n  defp line_from_expr({_, meta, _}) when is_list(meta), do: Keyword.get(meta, :line, 0)\n  defp line_from_expr(_), do: 0\n\n  defp to_safe(literal, _line, _extra_clauses?)\n       when is_binary(literal) or is_atom(literal) or is_number(literal) do\n    literal\n    |> Phoenix.HTML.Safe.to_iodata()\n    |> IO.iodata_to_binary()\n  end\n\n  defp to_safe(literal, line, _extra_clauses?) when is_list(literal) do\n    quote line: line, do: Phoenix.HTML.Safe.List.to_iodata(unquote(literal))\n  end\n\n  defp to_safe(expr, line, false) do\n    quote line: line, do: unquote(__MODULE__).safe_to_iodata(unquote(expr))\n  end\n\n  defp to_safe(expr, line, true) do\n    quote line: line, do: unquote(__MODULE__).live_to_iodata(unquote(expr))\n  end\n\n  @doc false\n  def safe_to_iodata(expr) do\n    case expr do\n      {:safe, data} -> data\n      bin when is_binary(bin) -> Plug.HTML.html_escape_to_iodata(bin)\n      other -> Phoenix.HTML.Safe.to_iodata(other)\n    end\n  end\n\n  @doc false\n  def live_to_iodata(expr) do\n    case expr do\n      {:safe, data} -> data\n      %{__struct__: Phoenix.LiveView.Rendered} = other -> other\n      %{__struct__: Phoenix.LiveView.Component} = other -> other\n      %{__struct__: Phoenix.LiveView.Comprehension} = other -> other\n      bin when is_binary(bin) -> Plug.HTML.html_escape_to_iodata(bin)\n      other -> Phoenix.HTML.Safe.to_iodata(other)\n    end\n  end\n\n  @doc false\n  def changed_assign?(changed, name) do\n    case changed do\n      %{^name => _} -> true\n      %{} -> false\n      nil -> true\n    end\n  end\n\n  defp changed_assign(changed, name) do\n    case changed do\n      %{^name => value} -> value\n      %{} -> false\n      nil -> true\n    end\n  end\n\n  @doc false\n  def nested_changed_assign?(tail, head, assigns, changed),\n    do: nested_changed_assign(tail, head, assigns, changed) != false\n\n  defp nested_changed_assign(tail, head, assigns, changed) do\n    case changed do\n      %{^head => changed} ->\n        case assigns do\n          %{^head => assigns} -> recur_changed_assign(tail, assigns, changed)\n          %{} -> true\n        end\n\n      %{} ->\n        false\n\n      nil ->\n        true\n    end\n  end\n\n  defp recur_changed_assign([{:struct, head} | tail], assigns, changed) do\n    recur_changed_assign(tail, head, assigns, changed)\n  end\n\n  defp recur_changed_assign([{:access, head}], %Form{} = form1, %Form{} = form2) do\n    # Phoenix.HTML does not know about LiveView's _unused_ input tracking,\n    # therefore we also need to check if the input's unused state changed\n    Form.input_changed?(form1, form2, head) or\n      Phoenix.Component.used_input?(form1[head]) !== Phoenix.Component.used_input?(form2[head])\n  end\n\n  defp recur_changed_assign([{:access, head} | tail], assigns, changed) do\n    if match?(%_{}, assigns) or match?(%_{}, changed) do\n      true\n    else\n      recur_changed_assign(tail, head, assigns, changed)\n    end\n  end\n\n  defp recur_changed_assign([], head, assigns, changed) do\n    case {assigns, changed} do\n      {%{^head => value}, %{^head => value}} -> false\n      {m1, m2} when not is_map_key(m1, head) and not is_map_key(m2, head) -> false\n      {_, %{^head => value}} when is_map(value) -> value\n      {_, _} -> true\n    end\n  end\n\n  defp recur_changed_assign(tail, head, assigns, changed) do\n    case {assigns, changed} do\n      {%{^head => assigns_value}, %{^head => changed_value}} ->\n        recur_changed_assign(tail, assigns_value, changed_value)\n\n      {_, _} ->\n        true\n    end\n  end\n\n  # For case/if/unless in particular, we are not leaking the\n  # variables defined in arguments, such as `if var = ... do`.\n  # This does not follow Elixir semantics, but yields better\n  # optimizations.\n  defp classify_taint(:case, [_, _]), do: :live\n  defp classify_taint(:if, [_, _]), do: :live\n  defp classify_taint(:unless, [_, _]), do: :live\n  defp classify_taint(:cond, [_]), do: :live\n  defp classify_taint(:try, [_]), do: :live\n  defp classify_taint(:receive, [_]), do: :live\n\n  # with/for are specially handled during analyze\n  defp classify_taint(:with, [_ | _]), do: :live\n  defp classify_taint(:for, [_ | _]), do: :live\n\n  # Constructs from TagEngine\n  defp classify_taint(:inner_block, [_, [do: _]]), do: :live\n\n  # Constructs from Phoenix.View\n  defp classify_taint(:render_layout, [_, _, _, [do: _]]), do: :live\n\n  # Special forms are forbidden and raise.\n  defp classify_taint(:alias, [_]), do: :special_form\n  defp classify_taint(:import, [_]), do: :special_form\n  defp classify_taint(:require, [_]), do: :special_form\n  defp classify_taint(:alias, [_, _]), do: :special_form\n  defp classify_taint(:import, [_, _]), do: :special_form\n  defp classify_taint(:require, [_, _]), do: :special_form\n\n  defp classify_taint(:&, [_]), do: :never\n  defp classify_taint(:fn, _), do: :never\n  defp classify_taint(_, _), do: :none\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/helpers.ex",
    "content": "defmodule Phoenix.LiveView.Helpers do\n  @moduledoc false\n\n  import Phoenix.Component\n  alias Phoenix.LiveView.Socket\n\n  @doc \"\"\"\n  Provides `~L` sigil with HTML safe Live EEx syntax inside source files.\n\n      iex> ~L\"\\\"\"\n      ...> Hello <%= \"world\" %>\n      ...> \"\\\"\"\n      {:safe, [\"Hello \", \"world\", \"\\\\n\"]}\n\n  \"\"\"\n  @deprecated \"Use ~H instead\"\n  defmacro sigil_L({:<<>>, meta, [expr]}, []) do\n    options = [\n      engine: Phoenix.LiveView.Engine,\n      file: __CALLER__.file,\n      line: __CALLER__.line + 1,\n      indentation: meta[:indentation] || 0\n    ]\n\n    EEx.compile_string(expr, options)\n  end\n\n  @deprecated \"Use link/1 instead\"\n  def live_patch(opts) when is_list(opts) do\n    live_link(\"patch\", Keyword.fetch!(opts, :do), Keyword.delete(opts, :do))\n  end\n\n  @deprecated \"Use <.link> instead\"\n  def live_patch(text, opts)\n\n  def live_patch(%Socket{}, _) do\n    raise \"\"\"\n    you are invoking live_patch/2 with a socket but a socket is not expected.\n\n    If you want to live_patch/2 inside a LiveView, use push_patch/2 instead.\n    If you are inside a template, make sure the first argument is a string.\n    \"\"\"\n  end\n\n  def live_patch(opts, do: block) when is_list(opts) do\n    live_link(\"patch\", block, opts)\n  end\n\n  def live_patch(text, opts) when is_list(opts) do\n    live_link(\"patch\", text, opts)\n  end\n\n  @deprecated \"Use <.link> instead\"\n  def live_redirect(opts) when is_list(opts) do\n    live_link(\"redirect\", Keyword.fetch!(opts, :do), Keyword.delete(opts, :do))\n  end\n\n  @deprecated \"Use <.link> instead\"\n  def live_redirect(text, opts)\n\n  def live_redirect(%Socket{}, _) do\n    raise \"\"\"\n    you are invoking live_redirect/2 with a socket but a socket is not expected.\n\n    If you want to live_redirect/2 inside a LiveView, use push_redirect/2 instead.\n    If you are inside a template, make sure the first argument is a string.\n    \"\"\"\n  end\n\n  def live_redirect(opts, do: block) when is_list(opts) do\n    live_link(\"redirect\", block, opts)\n  end\n\n  def live_redirect(text, opts) when is_list(opts) do\n    live_link(\"redirect\", text, opts)\n  end\n\n  defp live_link(type, block_or_text, opts) do\n    uri = Keyword.fetch!(opts, :to)\n    replace = Keyword.get(opts, :replace, false)\n    kind = if replace, do: \"replace\", else: \"push\"\n\n    data = [phx_link: type, phx_link_state: kind]\n\n    opts =\n      opts\n      |> Keyword.update(:data, data, &Keyword.merge(&1, data))\n      |> Keyword.put(:href, uri)\n      |> Keyword.delete(:to)\n\n    assigns = %{opts: opts, content: block_or_text}\n\n    ~H|<a {@opts}>{@content}</a>|\n  end\n\n  @deprecated \"Use <.live_title> instead\"\n  def live_title_tag(title, opts \\\\ []) do\n    assigns = %{title: title, prefix: opts[:prefix], suffix: opts[:suffix]}\n\n    ~H\"\"\"\n    <Phoenix.Component.live_title prefix={@prefix} suffix={@suffix}>\n      {@title}\n    </Phoenix.Component.live_title>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/html_algebra.ex",
    "content": "defmodule Phoenix.LiveView.HTMLAlgebra do\n  @moduledoc false\n\n  import Inspect.Algebra, except: [format: 2]\n\n  @languages ~w(style script)\n\n  # Reference for all inline elements so that we can tell the formatter to not\n  # force a line break. This list has been taken from here:\n  #\n  # https://web.archive.org/web/20220405120608/https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements#list_of_inline_elements\n  #\n  # A notable omission is `<script>`, which is handled separately in `html_algebra.ex`.\n  @inline_tags ~w(a abbr acronym audio b bdi bdo big br button canvas cite\n  code data datalist del dfn em embed i iframe img input ins kbd label map\n  mark meter noscript object output picture progress q ruby s samp select slot\n  small span strong sub sup svg template textarea time u tt var video wbr)\n\n  # The formatter has two modes:\n  #\n  # * :normal\n  # * :preserve - for preserving text in <pre>, <script>, <style> and HTML Comment tags\n  #\n  def build(tree, opts) when is_list(tree) do\n    {inline_matcher, opts} = Keyword.pop(opts, :inline_matcher, [\"link\", \"button\"])\n    {migrate, opts} = Keyword.pop(opts, :migrate_eex_to_curly_interpolation, true)\n    {attr_formatters, opts} = Keyword.pop!(opts, :attribute_formatters)\n\n    tree\n    |> block_to_algebra(%{\n      mode: :normal,\n      migrate: migrate,\n      attr_formatters: attr_formatters,\n      opts: opts,\n      inline_matcher: inline_matcher\n    })\n    |> group()\n  end\n\n  defp block_to_algebra([], _context), do: empty()\n\n  defp block_to_algebra(block, %{mode: :preserve} = context) do\n    concat =\n      Enum.reduce(block, empty(), fn node, doc ->\n        {_type, next_doc} = to_algebra(node, context)\n        concat(doc, next_doc)\n      end)\n\n    {force_unfit?, meta} =\n      Enum.find_value(block, fn\n        {:text, text, meta} ->\n          {String.contains?(text, \"\\n\"), meta}\n\n        _ ->\n          {false, %{}}\n      end)\n\n    doc =\n      if force_unfit? do\n        concat |> force_unfit() |> group()\n      else\n        concat |> group()\n      end\n\n    newline_or_empty_before_text =\n      if meta[:newlines_before_text] && meta[:newlines_before_text] > 1 do\n        line()\n      else\n        empty()\n      end\n\n    newline_or_empty_after_text =\n      if meta[:newlines_after_text] && meta[:newlines_after_text] > 1 do\n        line()\n      else\n        empty()\n      end\n\n    concat([newline_or_empty_before_text, doc, newline_or_empty_after_text])\n  end\n\n  defp block_to_algebra([head | tail], context) do\n    {type, doc} =\n      head\n      |> to_algebra(context)\n      |> maybe_force_unfit()\n\n    Enum.reduce(tail, {head, type, doc}, fn next_node, {prev_node, prev_type, prev_doc} ->\n      {next_type, next_doc} =\n        next_node\n        |> to_algebra(context)\n        |> maybe_force_unfit()\n\n      doc =\n        cond do\n          prev_type == :inline and next_type == :inline ->\n            on_break =\n              if next_doc == empty() do\n                \"\"\n              else\n                inline_break(prev_node, next_node)\n              end\n\n            concat([prev_doc, on_break, next_doc])\n\n          prev_type == :newline and next_type == :inline ->\n            concat([prev_doc, line(), next_doc])\n\n          next_type == :newline ->\n            {:text, _text, %{newlines_before_text: newlines}} = next_node\n\n            if newlines > 1 do\n              concat([prev_doc, nest(line(), :reset), next_doc])\n            else\n              concat([prev_doc, next_doc])\n            end\n\n          true ->\n            # For most cases, we do want to `break(\"\")` here because they are\n            # block tags (div, p, etc..). But, in case the previous or next token\n            # is a text without whitespace, such as:\n            #\n            #   (<div label=\"application programming interface\">API</div>).\n            #\n            # We don't want to break(\"\") otherwise it would format it like this:\n            #\n            #   (\n            #     <div label=\"application programming interface\">API</div>\n            #   ).\n            #\n            # Therefore, this check if the previous or next token is not a text\n            # and, if it is a text, check if that contains whitespace.\n            cond do\n              text_ends_with_space?(prev_node) or text_starts_with_space?(next_node) ->\n                concat([prev_doc, break(\"\"), next_doc])\n\n              text?(prev_node) or text?(next_node) or (tag?(prev_node) and tag?(next_node)) ->\n                concat([prev_doc, next_doc])\n\n              true ->\n                concat([prev_doc, break(\"\"), next_doc])\n            end\n        end\n\n      {next_node, next_type, doc}\n    end)\n    |> elem(2)\n    |> group()\n  end\n\n  defp inline_break(prev_node, next_node) do\n    cond do\n      # We do not insert breaks when preserving.\n      # We may insert spaces though if both sides are text.\n      block_preserve?(prev_node) or block_preserve?(next_node) ->\n        if (text_ends_with_space?(prev_node) or text_starts_with_space?(next_node)) and\n             not (text_preserve?(prev_node) or text_preserve?(next_node)) do\n          \" \"\n        else\n          \"\"\n        end\n\n      # If we have a block followed by anything that is not a tag,\n      # we force a break.\n      tag_block?(prev_node) and not tag?(next_node) ->\n        break(\" \")\n\n      (text_ends_with_space?(prev_node) or text_starts_with_space?(next_node)) and\n          not text_preserve?(prev_node) ->\n        flex_break(\" \")\n\n      true ->\n        \"\"\n    end\n  end\n\n  defp tag_block?({:block, _, _, _, _, _, _}), do: true\n  defp tag_block?(_node), do: false\n\n  defp tag?({:block, _, _, _, _, _, _}), do: true\n  defp tag?({:self_close, _, _, _, _}), do: true\n  defp tag?(_node), do: false\n\n  defp text?({:text, _, _}), do: true\n  defp text?(_), do: false\n\n  @codepoints ~c\"\\s\\n\\r\\t\"\n\n  defp text_starts_with_space?({:text, text, _meta}) when text != \"\",\n    do: :binary.first(text) in @codepoints\n\n  defp text_starts_with_space?(_node), do: false\n\n  defp text_ends_with_space?({:text, text, _meta}) when text != \"\",\n    do: :binary.last(text) in @codepoints\n\n  defp text_ends_with_space?(_node), do: false\n\n  defp block_preserve?({:block, _, _, _, _, %{mode: :preserve}, _}), do: true\n  defp block_preserve?({:body_expr, _, _}), do: true\n  defp block_preserve?({:eex, _, _}), do: true\n  defp block_preserve?(_node), do: false\n\n  defp text_preserve?({:text, _, %{mode: :preserve}}), do: true\n  defp text_preserve?(_), do: false\n\n  defp to_algebra({:html_comment, block}, context) do\n    children = block_to_algebra(block, %{context | mode: :preserve})\n    {:block, group(nest(children, :reset))}\n  end\n\n  defp to_algebra(\n         {:block, _type, _name, attrs, block, %{tag_name: name} = meta, _close_meta},\n         context\n       )\n       when name in @languages do\n    children = block_to_algebra(block, %{context | mode: :preserve})\n\n    # Convert the whole block to text as there are no\n    # tags inside script/style, only text and EEx blocks.\n    lines =\n      children\n      |> Inspect.Algebra.format(:infinity)\n      |> IO.iodata_to_binary()\n      |> String.split([\"\\r\\n\", \"\\n\"])\n      |> Enum.drop_while(&(String.trim_leading(&1) == \"\"))\n\n    indentation =\n      lines\n      |> Enum.map(&count_indentation(&1, 0))\n      |> Enum.min(fn -> :infinity end)\n      |> case do\n        :infinity -> 0\n        min -> min\n      end\n\n    doc =\n      case lines do\n        [] ->\n          line()\n\n        _ ->\n          text =\n            lines\n            |> Enum.map(&remove_indentation(&1, indentation))\n            |> text_to_algebra(0, [])\n\n          case meta do\n            %{mode: :preserve} -> text\n            _ -> concat(nest(concat(line(), text), 2), line())\n          end\n      end\n\n    group =\n      concat([format_tag_open(name, attrs, context), doc, \"</#{name}>\"])\n      |> group()\n\n    {:block, group}\n  end\n\n  defp to_algebra(\n         {:block, _type, _name, attrs, block, %{tag_name: name} = meta, _close_meta},\n         %{mode: :preserve} = context\n       ) do\n    children = block_to_algebra(block, context)\n\n    children =\n      if inline?(name, context) and meta.mode != :preserve do\n        children\n      else\n        nest(children, 2)\n      end\n\n    tag =\n      concat([format_tag_open(name, attrs, context), children, \"</#{name}>\"])\n      |> group()\n\n    {:inline, tag}\n  end\n\n  defp to_algebra(\n         {:block, _type, _name, _attrs, _block, %{mode: :preserve}, _close_meta} = doc,\n         context\n       ) do\n    to_algebra(doc, %{context | mode: :preserve})\n  end\n\n  defp to_algebra({:block, _type, _name, attrs, block, %{tag_name: name}, _close_meta}, context) do\n    inline? = inline?(name, context)\n    {block, force_newline?} = trim_block_newlines(block, inline?)\n    inline? = inline? and not force_newline?\n\n    children =\n      case block do\n        [] -> empty()\n        _ when inline? -> block_to_algebra(block, context)\n        _ -> nest(concat(break(\"\"), block_to_algebra(block, context)), 2)\n      end\n\n    children = if force_newline?, do: force_unfit(children), else: children\n\n    doc =\n      concat([\n        format_tag_open(name, attrs, context),\n        children,\n        if(inline?, do: empty(), else: break(\"\")),\n        \"</#{name}>\"\n      ])\n      |> group()\n\n    if inline? do\n      {:inline, doc}\n    else\n      {:block, doc}\n    end\n  end\n\n  defp to_algebra({:self_close, _type, _name, attrs, %{tag_name: name}}, context) do\n    doc =\n      concat([\n        \"<#{name}\",\n        build_attrs(attrs, \" \", context.attr_formatters, context.opts),\n        \"/>\"\n      ])\n\n    {:inline, group(doc)}\n  end\n\n  # Handle EEX blocks within preserve tags\n  defp to_algebra({:eex_block, expr, block, meta}, %{mode: :preserve} = context) do\n    doc =\n      Enum.reduce(block, empty(), fn {block, expr, _clause_meta}, doc ->\n        children = block_to_algebra(block, context)\n        expr = \"<% #{expr} %>\"\n        concat([doc, children, expr])\n      end)\n\n    {:block, group(concat(\"<%#{meta.opt} #{expr} %>\", doc))}\n  end\n\n  # Handle EEX blocks\n  defp to_algebra({:eex_block, expr, block, meta}, context) do\n    {doc, _stab} =\n      Enum.reduce(block, {empty(), false}, fn {block, expr, _clause_meta}, {doc, stab?} ->\n        {block, _force_newline?} = trim_block_newlines(block, false)\n        {next_doc, stab?} = eex_block_to_algebra(expr, block, stab?, context)\n        {concat(doc, force_unfit(next_doc)), stab?}\n      end)\n\n    {:block, group(concat(\"<%#{meta.opt} #{expr} %>\", doc))}\n  end\n\n  defp to_algebra({:eex_comment, text}, _context) do\n    {:inline, concat([\"<%!--\", text, \"--%>\"])}\n  end\n\n  defp to_algebra({:eex, text, %{opt: opt} = meta}, context) do\n    cond do\n      context.mode == :preserve ->\n        {:inline, concat([\"<%#{opt} \", text, \" %>\"])}\n\n      context.migrate and opt == ~c\"=\" and safe_to_migrate?(text, 0) ->\n        to_algebra({:body_expr, text, meta}, context)\n\n      true ->\n        doc = expr_to_code_algebra(text, meta, context.opts)\n        {:inline, concat([\"<%#{opt} \", doc, \" %>\"])}\n    end\n  end\n\n  defp to_algebra({:body_expr, text, meta}, context) do\n    if context.mode == :preserve do\n      {:inline, concat([\"{\", text, \"}\"])}\n    else\n      doc = expr_to_code_algebra(text, meta, context.opts)\n      {:inline, concat([\"{\", doc, \"}\"])}\n    end\n  end\n\n  # Handle text within <pre>/<script>/<style>/comment tags.\n  defp to_algebra({:text, text, _meta}, %{mode: :preserve}) when is_binary(text) do\n    {:inline, string(text)}\n  end\n\n  defp to_algebra({:text, text, %{mode: :preserve}}, _context) when is_binary(text) do\n    {:inline, string(text)}\n  end\n\n  # Handle text within other tags.\n  defp to_algebra({:text, text, _meta}, _context) when is_binary(text) do\n    case classify_leading(text) do\n      :spaces ->\n        {:inline, empty()}\n\n      :newline ->\n        {:newline, empty()}\n\n      :other ->\n        {:inline,\n         text\n         |> String.split([\"\\r\\n\", \"\\n\"])\n         |> Enum.map(&String.trim/1)\n         |> Enum.drop_while(&(&1 == \"\"))\n         |> text_to_algebra(0, [])}\n    end\n  end\n\n  defp inline?(name, context) do\n    name in @inline_tags or Enum.any?(context.inline_matcher, &(name =~ &1))\n  end\n\n  # Empty newline\n  defp text_to_algebra([\"\" | lines], newlines, acc),\n    do: text_to_algebra(lines, newlines + 1, acc)\n\n  # Text\n  # Text\n  defp text_to_algebra([line | lines], 0, acc),\n    do: text_to_algebra(lines, 0, [string(line), line() | acc])\n\n  # Text\n  #\n  # Text\n  defp text_to_algebra([line | lines], _newlines, acc),\n    do: text_to_algebra(lines, 0, [string(line), line(), nest(line(), :reset) | acc])\n\n  # Final clause: single line\n  defp text_to_algebra([], _, [doc, _line]),\n    do: doc\n\n  defp text_to_algebra([], _, []),\n    do: empty()\n\n  # Final clause: multiple lines\n  defp text_to_algebra([], _, acc),\n    do: acc |> Enum.reverse() |> tl() |> concat()\n\n  defp build_attrs([], on_break, _formatters, _opts), do: on_break\n\n  defp build_attrs([attr], on_break, formatters, opts) do\n    concat([\" \", render_attribute(attr, formatters, opts), on_break])\n  end\n\n  defp build_attrs(attrs, on_break, formatters, opts) do\n    doc =\n      attrs\n      |> Enum.sort_by(&attrs_sorter/1)\n      |> Enum.reduce(\n        empty(),\n        &concat([&2, break(\" \"), render_attribute(&1, formatters, opts)])\n      )\n      |> nest(2)\n      |> concat(break(on_break))\n\n    if distinct_lines?(attrs, -1) do\n      doc |> force_unfit() |> group()\n    else\n      group(doc)\n    end\n  end\n\n  defp distinct_lines?([{_, _, %{line: line}} | _], line), do: false\n  defp distinct_lines?([{_, _, %{line: line}} | tail], _line), do: distinct_lines?(tail, line)\n  defp distinct_lines?([], _line), do: true\n\n  @attrs_order %{\n    \":let\" => 1,\n    \":for\" => 2,\n    \":if\" => 3\n  }\n\n  # Sort attrs by @attrs_order. This will set :let, :for and :if at the beginning\n  # and ordinary HTML attributes at the end. HTML attributes will not change their\n  # order.\n  defp attrs_sorter({attr_name, _, _}) do\n    case @attrs_order[attr_name] do\n      nil -> 4\n      attrs_order -> attrs_order\n    end\n  end\n\n  defp format_tag_open(name, attrs, context),\n    do: concat([\"<#{name}\", build_attrs(attrs, \"\", context.attr_formatters, context.opts), \">\"])\n\n  defp render_attribute({:root, {:expr, expr, _}, _}, _formatters, _opts), do: ~s({#{expr}})\n\n  defp render_attribute({name, _, _} = attr, formatters, opts)\n       when is_map_key(formatters, name) do\n    attr\n    |> formatters[name].render_attribute(opts)\n    |> render_attribute(%{}, opts)\n  end\n\n  defp render_attribute({attr, {:string, value, %{delimiter: ?'}}, _}, _formatters, _opts) do\n    if String.contains?(value, [\"\\\"\", \"'\"]) do\n      ~s(#{attr}='#{value}')\n    else\n      ~s(#{attr}=\"#{value}\")\n    end\n  end\n\n  defp render_attribute({attr, {:string, value, _meta}, _}, _formatters, _opts),\n    do: ~s(#{attr}=\"#{value}\")\n\n  defp render_attribute({attr, {:expr, value, meta}, _}, _formatters, opts) do\n    case expr_to_quoted(value, meta, opts) do\n      {{:__block__, meta, [string]} = block, []} when is_binary(string) ->\n        has_quotes? = String.contains?(string, \"\\\"\")\n        delimiter = Keyword.get(meta, :delimiter)\n\n        if has_quotes? or delimiter == \"\\\"\\\"\\\"\" do\n          # don't try to flatten heredocs or strings with quotes\n          group(concat([\"#{attr}={\", quoted_to_code_algebra(block, [], opts), \"}\"]))\n        else\n          # attr={\"foo\"} -> attr=\"foo\"\n          ~s(#{attr}=\"#{string}\")\n        end\n\n      {{atom, _, _}, []} when atom in [:<<>>, :<>] ->\n        concat([\"#{attr}={\", string(value), \"}\"])\n\n      {{:__block__, _, [[_ | _]]} = quoted, []} ->\n        expr = quoted_to_code_algebra(quoted, [], opts)\n        group(concat([\"#{attr}={\", expr, \"}\"]))\n\n      {quoted, comments} ->\n        expr =\n          break(\"\")\n          |> concat(quoted_to_code_algebra(quoted, comments, opts))\n          |> nest(2)\n\n        group(concat([\"#{attr}={\", expr, concat(break(\"\"), \"}\")]))\n    end\n  end\n\n  defp render_attribute({attr, {_, value, _meta}, _}, _formatters, _opts),\n    do: ~s(#{attr}=#{value})\n\n  defp render_attribute({attr, nil, _}, _formatters, _opts), do: ~s(#{attr})\n\n  # Handle EEx clauses\n  #\n  # {[], \"something ->\", %{...}}\n  # {[{:block, :tag, \"p\", [], [...], %{...}, %{...}}], \"else\", %{...}}\n  defp eex_block_to_algebra(expr, block, stab?, context) when is_list(block) do\n    indent = if stab?, do: 4, else: 2\n\n    document =\n      if block == [] do\n        # The first clause in cond/case and general empty clauses.\n        empty()\n      else\n        line()\n        |> concat(block_to_algebra(block, context))\n        |> nest(indent)\n      end\n\n    stab? = String.ends_with?(expr, \"->\")\n    indent = if stab?, do: 2, else: 0\n\n    next =\n      line()\n      |> concat(\"<% #{expr} %>\")\n      |> nest(indent)\n\n    {concat(document, next), stab?}\n  end\n\n  defp expr_to_quoted(expr, meta, opts) do\n    string_to_quoted_opts = [\n      literal_encoder: &{:ok, {:__block__, &2, [&1]}},\n      token_metadata: true,\n      unescape: false,\n      line: meta.line,\n      column: meta.column,\n      file: Keyword.get(opts, :file, \"nofile\")\n    ]\n\n    Code.string_to_quoted_with_comments!(expr, string_to_quoted_opts)\n  end\n\n  defp expr_to_code_algebra(expr, meta, opts) do\n    {quoted, comments} = expr_to_quoted(expr, meta, opts)\n    quoted_to_code_algebra(quoted, comments, opts)\n  end\n\n  defp quoted_to_code_algebra(quoted, comments, opts) do\n    Code.quoted_to_algebra(quoted, Keyword.merge(opts, escape: false, comments: comments))\n  end\n\n  def classify_leading(text), do: classify_leading(text, :spaces)\n\n  def classify_leading(<<char, rest::binary>>, mode) when char in [?\\s, ?\\t],\n    do: classify_leading(rest, mode)\n\n  def classify_leading(<<?\\n, rest::binary>>, _), do: classify_leading(rest, :newline)\n  def classify_leading(<<>>, mode), do: mode\n  def classify_leading(_rest, _), do: :other\n\n  defp maybe_force_unfit({:block, doc}), do: {:block, force_unfit(doc)}\n  defp maybe_force_unfit(doc), do: doc\n\n  defp trim_block_newlines(block, inline?) do\n    {tail, first_force?} =\n      pop_head_if_only_spaces_or_newlines(block, inline?, :newlines_before_text)\n\n    {block, last_force?} =\n      tail\n      |> Enum.reverse()\n      |> pop_head_if_only_spaces_or_newlines(inline?, :newlines_after_text)\n\n    force? = if Enum.empty?(block), do: false, else: first_force? or last_force?\n\n    {Enum.reverse(block), force?}\n  end\n\n  defp pop_head_if_only_spaces_or_newlines(\n         [{:text, text, meta} | tail] = block,\n         inline?,\n         newlines_key\n       ) do\n    force? = Map.fetch!(meta, newlines_key) > 0\n\n    cond do\n      String.trim_leading(text) == \"\" ->\n        {tail, force?}\n\n      inline? and not force? and whitespace_around?(text, newlines_key) ->\n        text = cleanup_extra_spaces(text, newlines_key)\n        meta = Map.put(meta, :mode, :preserve)\n        {[{:text, text, meta} | tail], false}\n\n      true ->\n        {block, force?}\n    end\n  end\n\n  defp pop_head_if_only_spaces_or_newlines(block, _inline?, _where), do: {block, false}\n\n  defp cleanup_extra_spaces(text, :newlines_before_text), do: \" \" <> String.trim_leading(text)\n  defp cleanup_extra_spaces(text, :newlines_after_text), do: String.trim_trailing(text) <> \" \"\n\n  defp whitespace_around?(text, :newlines_before_text), do: :binary.first(text) in ~c\"\\s\\t\"\n  defp whitespace_around?(text, :newlines_after_text), do: :binary.last(text) in ~c\"\\s\\t\"\n\n  defp count_indentation(<<?\\t, rest::binary>>, indent), do: count_indentation(rest, indent + 2)\n  defp count_indentation(<<?\\s, rest::binary>>, indent), do: count_indentation(rest, indent + 1)\n  defp count_indentation(<<>>, _indent), do: :infinity\n  defp count_indentation(_, indent), do: indent\n\n  defp remove_indentation(rest, 0), do: rest\n  defp remove_indentation(<<?\\t, rest::binary>>, indent), do: remove_indentation(rest, indent - 2)\n  defp remove_indentation(<<?\\s, rest::binary>>, indent), do: remove_indentation(rest, indent - 1)\n  defp remove_indentation(rest, _indent), do: rest\n\n  defp safe_to_migrate?(~S[\\{] <> rest, acc), do: safe_to_migrate?(rest, acc)\n  defp safe_to_migrate?(~S[\\}] <> rest, acc), do: safe_to_migrate?(rest, acc)\n  defp safe_to_migrate?(\"{\" <> rest, acc), do: safe_to_migrate?(rest, acc + 1)\n  defp safe_to_migrate?(\"}\" <> rest, acc), do: safe_to_migrate?(rest, acc - 1)\n  defp safe_to_migrate?(<<_::utf8, rest::binary>>, acc), do: safe_to_migrate?(rest, acc)\n  defp safe_to_migrate?(<<>>, acc), do: acc == 0\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/html_engine.ex",
    "content": "defmodule Phoenix.LiveView.HTMLEngine do\n  @moduledoc \"\"\"\n  The HTMLEngine that powers `.heex` templates and the `~H` sigil.\n\n  It works by adding a HTML parsing and validation layer on top\n  of `Phoenix.LiveView.TagEngine`.\n  \"\"\"\n\n  @behaviour Phoenix.Template.Engine\n\n  @impl true\n  def compile(path, _name) do\n    # We need access for the caller, so we return a call to a macro.\n    quote do\n      require Phoenix.LiveView.HTMLEngine\n      Phoenix.LiveView.HTMLEngine.compile(unquote(path))\n    end\n  end\n\n  @doc false\n  defmacro compile(path) do\n    trim = Application.get_env(:phoenix, :trim_on_html_eex_engine, true)\n    source = File.read!(path)\n\n    options = [\n      engine: Phoenix.LiveView.Engine,\n      file: path,\n      line: 1,\n      caller: __CALLER__,\n      tag_handler: __MODULE__,\n      trim: trim\n    ]\n\n    Phoenix.LiveView.TagEngine.compile(source, options)\n  end\n\n  @behaviour Phoenix.LiveView.TagEngine\n\n  @impl true\n  def classify_type(\":inner_block\"), do: {:error, \"the slot name :inner_block is reserved\"}\n  def classify_type(\":\" <> name), do: {:slot, name}\n\n  def classify_type(<<first, _::binary>> = name) when first in ?A..?Z,\n    do: {:remote_component, name}\n\n  def classify_type(\".\"), do: {:error, \"a component name is required after .\"}\n  def classify_type(\".\" <> name), do: {:local_component, name}\n  def classify_type(name), do: {:tag, name}\n\n  @impl true\n  for void <- ~w(area base br col hr img input link meta param command keygen source) do\n    def void?(unquote(void)), do: true\n  end\n\n  def void?(_), do: false\n\n  @impl true\n  def handle_attributes(ast, meta) do\n    if is_list(ast) and literal_keys?(ast) do\n      # Optimization: if keys are known at compilation time, we\n      # inline the dynamic attributes\n      attrs =\n        Enum.map(ast, fn {key, value} ->\n          name = to_string(key)\n\n          case handle_attr_escape(name, value, meta) do\n            :error -> handle_attrs_escape([{safe_unless_special(name), value}], meta)\n            parts -> {name, parts}\n          end\n        end)\n\n      {:attributes, attrs}\n    else\n      {:quoted, handle_attrs_escape(ast, meta)}\n    end\n  end\n\n  defp literal_keys?([{key, _value} | rest]) when is_atom(key) or is_binary(key),\n    do: literal_keys?(rest)\n\n  defp literal_keys?([]), do: true\n  defp literal_keys?(_other), do: false\n\n  defp handle_attrs_escape(attrs, meta) do\n    quote line: meta[:line] do\n      unquote(__MODULE__).attributes_escape(unquote(attrs))\n    end\n  end\n\n  defp handle_attr_escape(\"class\", [head | tail], meta) when is_binary(head) do\n    {bins, tail} = Enum.split_while(tail, &is_binary/1)\n    encoded = class_attribute_encode([head | bins])\n\n    if tail == [] do\n      [IO.iodata_to_binary(encoded)]\n    else\n      tail =\n        quote line: meta[:line] do\n          {:safe, unquote(__MODULE__).class_attribute_encode(unquote(tail))}\n        end\n\n      [IO.iodata_to_binary([encoded, ?\\s]), tail]\n    end\n  end\n\n  defp handle_attr_escape(\"class\", value, meta) do\n    [\n      quote(\n        line: meta[:line],\n        do: {:safe, unquote(__MODULE__).class_attribute_encode(unquote(value))}\n      )\n    ]\n  end\n\n  defp handle_attr_escape(\"style\", value, meta) do\n    [\n      quote(\n        line: meta[:line],\n        do: {:safe, unquote(__MODULE__).empty_attribute_encode(unquote(value))}\n      )\n    ]\n  end\n\n  defp handle_attr_escape(_name, value, meta) do\n    case extract_binaries(value, true, [], meta) do\n      :error -> :error\n      reversed -> Enum.reverse(reversed)\n    end\n  end\n\n  defp extract_binaries({:<>, _, [left, right]}, _root?, acc, meta) do\n    extract_binaries(right, false, extract_binaries(left, false, acc, meta), meta)\n  end\n\n  defp extract_binaries({:<<>>, _, parts} = binary, _root?, acc, meta) do\n    Enum.reduce(parts, acc, fn\n      part, acc when is_binary(part) ->\n        [binary_encode(part) | acc]\n\n      {:\"::\", _, [binary, {:binary, _, _}]}, acc ->\n        [quoted_binary_encode(binary, meta) | acc]\n\n      _, _ ->\n        throw(:unknown_part)\n    end)\n  catch\n    :unknown_part ->\n      [quoted_binary_encode(binary, meta) | acc]\n  end\n\n  defp extract_binaries(binary, _root?, acc, _meta) when is_binary(binary),\n    do: [binary_encode(binary) | acc]\n\n  defp extract_binaries(value, false, acc, meta),\n    do: [quoted_binary_encode(value, meta) | acc]\n\n  defp extract_binaries(_value, true, _acc, _meta),\n    do: :error\n\n  @doc false\n  def attributes_escape(attrs) do\n    # We don't want to dasherize keys, which Phoenix.HTML does for atoms,\n    # so we convert those to strings\n    attrs\n    |> Enum.map(fn\n      {key, value} when is_atom(key) -> {Atom.to_string(key), value}\n      other -> other\n    end)\n    |> Phoenix.HTML.attributes_escape()\n  end\n\n  @doc false\n  def class_attribute_encode(list) when is_list(list),\n    do: list |> class_attribute_list() |> Phoenix.HTML.Engine.encode_to_iodata!()\n\n  def class_attribute_encode(other),\n    do: empty_attribute_encode(other)\n\n  defp class_attribute_list(value) do\n    value\n    |> Enum.flat_map(fn\n      nil -> []\n      false -> []\n      inner when is_list(inner) -> [class_attribute_list(inner)]\n      other -> [other]\n    end)\n    |> Enum.join(\" \")\n  end\n\n  @doc false\n  def empty_attribute_encode(nil), do: \"\"\n  def empty_attribute_encode(false), do: \"\"\n  def empty_attribute_encode(true), do: \"\"\n  def empty_attribute_encode(value), do: Phoenix.HTML.Engine.encode_to_iodata!(value)\n\n  @doc false\n  def binary_encode(value) when is_binary(value) do\n    value\n    |> Phoenix.HTML.Engine.encode_to_iodata!()\n    |> IO.iodata_to_binary()\n  end\n\n  def binary_encode(value) do\n    raise ArgumentError, \"expected a binary in <>, got: #{inspect(value)}\"\n  end\n\n  defp quoted_binary_encode(binary, meta) do\n    quote line: meta[:line] do\n      {:safe, unquote(__MODULE__).binary_encode(unquote(binary))}\n    end\n  end\n\n  # We mark attributes as safe so we don't escape them\n  # at rendering time. However, some attributes are\n  # specially handled, so we keep them as strings shape.\n  defp safe_unless_special(\"id\"), do: :id\n  defp safe_unless_special(\"aria\"), do: :aria\n  defp safe_unless_special(\"class\"), do: :class\n  defp safe_unless_special(\"data\"), do: :data\n  defp safe_unless_special(name), do: {:safe, name}\n\n  @impl true\n  def annotate_body(%Macro.Env{} = caller) do\n    if debug_annotations?(caller) do\n      %Macro.Env{module: mod, function: {fun, _}, file: file, line: line} = caller\n      name = \"#{inspect(mod)}.#{fun}\"\n      annotate_source(name, file, line)\n    end\n  end\n\n  @impl true\n  def annotate_slot(name, %{line: line}, _close_meta, %{file: file} = caller) do\n    if debug_annotations?(caller) do\n      annotate_source(\":#{name}\", file, line)\n    end\n  end\n\n  defp annotate_source(name, file, line) do\n    line = if line == 0, do: 1, else: line\n    file = Path.relative_to_cwd(file)\n    {\"<!-- <#{name}> #{file}:#{line} (#{current_otp_app()}) -->\", \"<!-- </#{name}> -->\"}\n  end\n\n  @impl true\n  def annotate_caller(file, line, caller) do\n    if debug_annotations?(caller) do\n      line = if line == 0, do: 1, else: line\n      file = Path.relative_to_cwd(file)\n\n      \"<!-- @caller #{file}:#{line} (#{current_otp_app()}) -->\"\n    end\n  end\n\n  defp current_otp_app do\n    Application.get_env(:logger, :compile_time_application)\n  end\n\n  defp debug_annotations?(caller) do\n    if Module.open?(caller.module) do\n      case Module.get_attribute(caller.module, :debug_heex_annotations) do\n        false -> false\n        _ -> Application.get_env(:phoenix_live_view, :debug_heex_annotations, false)\n      end\n    else\n      Application.get_env(:phoenix_live_view, :debug_heex_annotations, false)\n    end\n  rescue\n    _ -> Application.get_env(:phoenix_live_view, :debug_heex_annotations, false)\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/html_formatter/tag_formatter.ex",
    "content": "defmodule Phoenix.LiveView.HTMLFormatter.TagFormatter do\n  @moduledoc \"\"\"\n  A behaviour for formatting specific tags.\n  \"\"\"\n\n  @doc \"\"\"\n  Callback invoked to format each tag.\n\n  The callback receives a tuple of `{tag_name, attrs, content}`\n  and the Mix [formatter options](https://hexdocs.pm/mix/Mix.Tasks.Format.html#module-formatting-options).\n\n  Note: since the formatter does not compile code, the `:type` attribute of a\n  macro component is given as a string as in the template.\n  If you aliased `Phoenix.LiveView.ColocatedHook`, you will receive the aliased\n  version as a string. For example:\n\n  ```heex\n  <script :type={ColocatedHook} manifest=\"foo.ts\">\n    export default {\n      mounted() {\n        console.log(\"mounted\");\n      }\n    }\n  </script>\n  ```\n\n  will invoke the callback with:\n\n    * `{tag_name, attrs, content}`\n      with `tag_name` being `\"script\"`,\n      attrs as `%{\"manifest\" => \"foo.ts\", \":type\" => \"ColocatedHook\"}`\n      and `content` being the string content inside the `<script>` tag\n    * opts: `[file: \"/path/to/template.html.heex\", line: ...]`\n      the Mix [formatter options](https://hexdocs.pm/mix/Mix.Tasks.Format.html#module-formatting-options)\n\n  ### Example for formatting with [`prettier`](https://prettier.io/)\n\n  ```elixir\n  defmodule Prettier do\n    @moduledoc false\n\n    @behaviour Phoenix.LiveView.HTMLFormatter.TagFormatter\n\n    require Logger\n\n    @impl true\n    def render_tag({\"script\", attrs, content}, _opts) do\n      suffix =\n        case attrs do\n          %{\":type\" => _} ->\n            # assume ColocatedHook / ColocatedJS and check for extension in manifest attribute\n            Map.get(attrs, \"manifest\", \"index.js\")\n\n          _ ->\n            \"tmp.js\"\n        end\n\n      tmp_file =\n        Path.join(System.tmp_dir!(), \"prettier_\\#{System.unique_integer([:positive])}_\\#{suffix}\")\n\n      try do\n        File.write!(tmp_file, content)\n\n        # This example assumes that your project has prettier installed as a dependency\n        # in your package.json. If not, you should pin prettier to a specific version like\n        # \"prettier@3.8.1\" to avoid potential issues when prettier updates.\n        case System.cmd(\"npx\", [\"prettier\", tmp_file], stderr_to_stdout: true) do\n          {output, 0} ->\n            {:ok, String.trim(output)}\n\n          {error, _} ->\n            Logger.error(\"Failed to format with prettier: \\#{error}\")\n            :skip\n        end\n      after\n        File.rm(tmp_file)\n      end\n    end\n  end\n  ```\n\n  ```\n  # .formatter.exs\n  [\n    plugins: [Phoenix.LiveView.HTMLFormatter],\n    tag_formatters: %{script: Prettier}\n  ]\n  ```\n\n  \"\"\"\n  @callback render_tag(\n              tag :: {String.t(), map(), String.t()},\n              opts :: keyword()\n            ) :: {:ok, String.t()} | :skip\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/html_formatter.ex",
    "content": "defmodule Phoenix.LiveView.HTMLFormatter do\n  @moduledoc \"\"\"\n  Format HEEx templates from `.heex` files or `~H` sigils.\n\n  This is a `mix format` [plugin](https://hexdocs.pm/mix/main/Mix.Tasks.Format.html#module-plugins).\n\n  ## Setup\n\n  Add it as a plugin to your `.formatter.exs` file and make sure to put\n  the `heex` extension in the `inputs` option.\n\n  ```elixir\n  [\n    plugins: [Phoenix.LiveView.HTMLFormatter],\n    inputs: [\"*.{heex,ex,exs}\", \"priv/*/seeds.exs\", \"{config,lib,test}/**/*.{heex,ex,exs}\"],\n    # ...\n  ]\n  ```\n\n  > ### For umbrella projects {: .info}\n  >\n  > In umbrella projects you must also change two files at the umbrella root,\n  > add `:phoenix_live_view` to your `deps` in the `mix.exs` file\n  > and add `plugins: [Phoenix.LiveView.HTMLFormatter]` in the `.formatter.exs` file.\n  > This is because the formatter does not attempt to load the dependencies of\n  > all children applications.\n\n  ### Editor support\n\n  Most editors that support `mix format` integration should automatically format\n  `.heex` and `~H` templates. Other editors may require custom integration or\n  even provide additional functionality. Here are some reference posts:\n\n    * [Formatting HEEx templates in VS Code](https://pragmaticstudio.com/tutorials/formatting-heex-templates-in-vscode)\n\n  ## Options\n\n    * `:line_length` - The Elixir formatter defaults to a maximum line length\n      of 98 characters, which can be overwritten with the `:line_length` option\n      in your `.formatter.exs` file.\n\n    * `:heex_line_length` - change the line length only for the HEEx formatter.\n\n      ```elixir\n      [\n        # ...omitted\n        heex_line_length: 300\n      ]\n      ```\n\n    * `:migrate_eex_to_curly_interpolation` - Automatically migrate single expression\n      `<%= ... %>` EEx expression to the curly braces one. Defaults to true.\n\n    * `:attribute_formatters` - Specify formatters for certain attributes.\n\n      ```elixir\n      [\n        plugins: [Phoenix.LiveView.HTMLFormatter],\n        attribute_formatters: %{class: ClassFormatter},\n      ]\n      ```\n\n    * `:tag_formatters` - Specify formatters for certain tags. Right now, only `style` and `script` are allowed.\n\n      ```elixir\n      [\n        plugins: [Phoenix.LiveView.HTMLFormatter],\n        tag_formatters: %{script: MyApp.PrettierFormatter}\n      ]\n      ```\n\n      See the documentation for `Phoenix.LiveView.HTMLFormatter.TagFormatter` for details.\n\n    * `:inline_matcher` - a list of regular expressions to determine if a component\n      should be treated as inline.\n      Defaults to `[\"link\", \"button\"]`, which treats any component with `link`\n      or `button` in its name as inline.\n      Can be disabled by setting it to an empty list.\n\n  ## Formatting\n\n  This formatter tries to be as consistent as possible with the Elixir formatter\n  and also take into account \"block\" and \"inline\" HTML elements.\n\n  In the past, HTML elements were categorized as either \"block-level\" or\n  \"inline\". While now these concepts are specified by CSS, the historical\n  distinction remains as it typically dictates the default browser rendering\n  behavior. In particular, adding or removing whitespace between the start and\n  end tags of a block-level element will not change the rendered output, while\n  it may for inline elements.\n\n  The following links further explain these concepts:\n\n  * https://developer.mozilla.org/en-US/docs/Glossary/Block-level_content\n  * https://developer.mozilla.org/en-US/docs/Glossary/Inline-level_content\n\n  Given HTML like this:\n\n  ```heex\n    <section><h1>   <b>{@user.name}</b></h1></section>\n  ```\n\n  It will be formatted as:\n\n  ```heex\n  <section>\n    <h1><b>{@user.name}</b></h1>\n  </section>\n  ```\n\n  A block element will go to the next line, while inline elements will be kept in the current line\n  as long as they fit within the configured line length.\n\n  It will also keep inline elements in their own lines if you intentionally write them this way:\n\n  ```heex\n  <section>\n    <h1>\n      <b>{@user.name}</b>\n    </h1>\n  </section>\n  ```\n\n  This formatter will place all attributes on their own lines when they do not all fit in the\n  current line. Therefore this:\n\n  ```heex\n  <section id=\"user-section-id\" class=\"sm:focus:block flex w-full p-3\" phx-click=\"send-event\">\n    <p>Hi</p>\n  </section>\n  ```\n\n  Will be formatted to:\n\n  ```heex\n  <section\n    id=\"user-section-id\"\n    class=\"sm:focus:block flex w-full p-3\"\n    phx-click=\"send-event\"\n  >\n    <p>Hi</p>\n  </section>\n  ```\n\n  This formatter **does not** format Elixir expressions with `do...end`.\n  The content within it will be formatted accordingly though. Therefore, the given\n  input:\n\n  ```eex\n  <%= live_redirect(\n         to: \"/my/path\",\n    class: \"my class\"\n  ) do %>\n          My Link\n  <% end %>\n  ```\n\n  Will be formatted to\n\n  ```eex\n  <%= live_redirect(\n         to: \"/my/path\",\n    class: \"my class\"\n  ) do %>\n    My Link\n  <% end %>\n  ```\n\n  Note that only the text `My Link` has been formatted.\n\n  ### Intentional new lines\n\n  The formatter will keep intentional new lines. However, the formatter will\n  always keep a maximum of one line break in case you have multiple ones:\n\n  ```heex\n  <p>\n    text\n\n\n    text\n  </p>\n  ```\n\n  Will be formatted to:\n\n  ```heex\n  <p>\n    text\n\n    text\n  </p>\n  ```\n\n  ### Inline elements\n\n  We don't format inline elements when there is a text without whitespace before\n  or after the element. Otherwise it would compromise what is rendered adding\n  an extra whitespace.\n\n  The formatter will consider these tags as inline elements:\n\n  - `<a>`\n  - `<abbr>`\n  - `<acronym>`\n  - `<audio>`\n  - `<b>`\n  - `<bdi>`\n  - `<bdo>`\n  - `<big>`\n  - `<br>`\n  - `<button>`\n  - `<canvas>`\n  - `<cite>`\n  - `<code>`\n  - `<data>`\n  - `<datalist>`\n  - `<del>`\n  - `<dfn>`\n  - `<em>`\n  - `<embed>`\n  - `<i>`\n  - `<iframe>`\n  - `<img>`\n  - `<input>`\n  - `<ins>`\n  - `<kbd>`\n  - `<label>`\n  - `<map>`\n  - `<mark>`\n  - `<meter>`\n  - `<noscript>`\n  - `<object>`\n  - `<output>`\n  - `<picture>`\n  - `<progress>`\n  - `<q>`\n  - `<ruby>`\n  - `<s>`\n  - `<samp>`\n  - `<select>`\n  - `<slot>`\n  - `<small>`\n  - `<span>`\n  - `<strong>`\n  - `<sub>`\n  - `<sup>`\n  - `<svg>`\n  - `<template>`\n  - `<textarea>`\n  - `<time>`\n  - `<u>`\n  - `<tt>`\n  - `<var>`\n  - `<video>`\n  - `<wbr>`\n  - Tags/components that match the `:inline_matcher` option.\n\n  All other tags are considered block elements.\n\n  ## Skip formatting\n\n  In case you don't want part of your HTML to be automatically formatted.\n  You can use the special `phx-no-format` attribute so that the formatter will\n  skip the element block. Note that this attribute will not be rendered.\n\n  Therefore:\n\n  ```heex\n  <.textarea phx-no-format>My content</.textarea>\n  ```\n\n  Will be kept as is your code editor, but rendered as:\n\n  ```heex\n  <textarea>My content</textarea>\n  ```\n  \"\"\"\n\n  require Logger\n\n  alias Phoenix.LiveView.HTMLAlgebra\n  alias Phoenix.LiveView.TagEngine.Parser\n  alias Phoenix.LiveView.TagEngine.Tokenizer.ParseError\n\n  # Default line length to be used in case nothing is specified in the `.formatter.exs` options.\n  @default_line_length 98\n\n  @behaviour Mix.Tasks.Format\n\n  @impl Mix.Tasks.Format\n  def features(_opts) do\n    [sigils: [:H], extensions: [\".heex\"]]\n  end\n\n  @impl Mix.Tasks.Format\n  def format(source, opts) do\n    if opts[:sigil] === :H and opts[:modifiers] === ~c\"noformat\" do\n      source\n    else\n      line_length = opts[:heex_line_length] || opts[:line_length] || @default_line_length\n      newlines = :binary.matches(source, [\"\\r\\n\", \"\\n\"])\n\n      opts =\n        Enum.reduce([:attribute_formatters, :tag_formatters], opts, fn type, opts ->\n          Keyword.update(opts, type, %{}, fn formatters ->\n            Enum.reduce(formatters, %{}, fn {attr, formatter}, formatters ->\n              if Code.ensure_loaded?(formatter) do\n                Map.put(formatters, to_string(attr), formatter)\n              else\n                Logger.error(\"module #{inspect(formatter)} is not loaded and could not be found\")\n                formatters\n              end\n            end)\n          end)\n        end)\n\n      formatted =\n        source\n        |> Parser.parse(\n          tag_handler: Phoenix.LiveView.HTMLEngine,\n          file: \"nofile\",\n          skip_macro_components: true,\n          prune_text_after_slots: false,\n          process_buffer: &process_buffer/1\n        )\n        |> case do\n          {:ok, result} ->\n            result.nodes\n            |> transform_tree(source, newlines, opts)\n            |> HTMLAlgebra.build(opts)\n            |> Inspect.Algebra.format(line_length)\n\n          {:error, line, column, message} ->\n            file = Keyword.get(opts, :file, \"nofile\")\n            raise ParseError, line: line, column: column, file: file, description: message\n        end\n\n      # If the opening delimiter is a single character, such as ~H\"...\", or the formatted code is empty,\n      # do not add trailing newline.\n      newline =\n        if match?(<<_>>, opts[:opening_delimiter]) or formatted == [] or formatted == \"\",\n          do: [],\n          else: ?\\n\n\n      IO.iodata_to_binary([formatted, newline])\n    end\n  end\n\n  # Buffer processing callback for Parser - handles preserve mode propagation and text metadata\n  defp process_buffer([{:text, text, meta} | rest]) do\n    rest = may_set_preserve_on_block(rest, text)\n\n    meta =\n      meta\n      |> Map.put_new(:newlines_before_text, count_newlines_before_text(text))\n      |> Map.put_new(:newlines_after_text, count_newlines_after_text(text))\n\n    [{:text, text, meta} | rest]\n  end\n\n  defp process_buffer([{:body_expr, _, _} = node | rest]) do\n    [node | set_preserve_on_block(rest)]\n  end\n\n  defp process_buffer([{:eex, _, _} = node | rest]) do\n    [node | set_preserve_on_block(rest)]\n  end\n\n  defp process_buffer(buffer), do: buffer\n\n  # In case the closing tag is immediately followed by non-whitespace text,\n  # we want to set mode as preserve.\n  defp may_set_preserve_on_block(\n         [{:block, type, name, attrs, block, meta, close_meta} | rest],\n         text\n       ) do\n    mode =\n      if String.trim_leading(text) != \"\" and :binary.first(text) not in ~c\"\\s\\t\\n\\r\" do\n        :preserve\n      else\n        Map.get(meta, :mode, :normal)\n      end\n\n    [{:block, type, name, attrs, block, Map.put(meta, :mode, mode), close_meta} | rest]\n  end\n\n  defp may_set_preserve_on_block(buffer, _text), do: buffer\n\n  # Set preserve on block when it is immediately followed by interpolation.\n  defp set_preserve_on_block([{:block, type, name, attrs, block, meta, close_meta} | rest]) do\n    [{:block, type, name, attrs, block, Map.put(meta, :mode, :preserve), close_meta} | rest]\n  end\n\n  defp set_preserve_on_block(buffer), do: buffer\n\n  defp count_newlines_before_text(binary),\n    do: count_newlines_until_text(binary, 0, 0, 1)\n\n  defp count_newlines_after_text(binary),\n    do: count_newlines_until_text(binary, 0, byte_size(binary) - 1, -1)\n\n  defp count_newlines_until_text(binary, counter, pos, inc) do\n    try do\n      :binary.at(binary, pos)\n    rescue\n      _ -> counter\n    else\n      char when char in [?\\s, ?\\t] -> count_newlines_until_text(binary, counter, pos + inc, inc)\n      ?\\n -> count_newlines_until_text(binary, counter + 1, pos + inc, inc)\n      _ -> counter\n    end\n  end\n\n  defp leading_whitespace(binary) do\n    binary_part(binary, 0, count_leading_whitespace(binary, 0))\n  end\n\n  defp count_leading_whitespace(<<char, rest::binary>>, count) when char in ~c\"\\s\\t\\n\\r\",\n    do: count_leading_whitespace(rest, count + 1)\n\n  defp count_leading_whitespace(_rest, count),\n    do: count\n\n  defp trailing_whitespace(binary) do\n    trailing_whitespace(binary, byte_size(binary) - 1, 0)\n  end\n\n  defp trailing_whitespace(binary, pos, len) do\n    try do\n      :binary.at(binary, pos)\n    rescue\n      _ -> binary_part(binary, byte_size(binary) - len, len)\n    else\n      char when char in [?\\s, ?\\t, ?\\n, ?\\r] -> trailing_whitespace(binary, pos - 1, len + 1)\n      _ -> binary_part(binary, byte_size(binary) - len, len)\n    end\n  end\n\n  # Tree transformation - augments Parser output with formatter metadata\n  defp transform_tree(nodes, source, newlines, opts) do\n    state = %{source: {source, newlines}, opts: opts, tag_formatters: opts[:tag_formatters]}\n    augment_nodes(nodes, state)\n  end\n\n  # Augment nodes with formatter-specific metadata\n  defp augment_nodes(nodes, state) when is_list(nodes) do\n    nodes\n    |> reduce_html_comments([])\n    |> Enum.map(&augment_node(&1, state))\n  end\n\n  # Group text nodes with :comment_start/:comment_end context into {:html_comment, block}\n  defp reduce_html_comments([], acc), do: Enum.reverse(acc)\n\n  # Single node that is both comment start and end\n  defp reduce_html_comments(\n         [{:text, text, %{context: [:comment_start, :comment_end]}} | rest],\n         acc\n       ) do\n    # Split leading/trailing whitespace into separate text nodes when they\n    # contain intentional blank lines (2+ newlines). This lets the normal\n    # blank line handling in block_to_algebra produce clean empty lines\n    # without trailing whitespace.\n    leading = leading_whitespace(text)\n    leading_newlines = count_newlines_before_text(leading)\n\n    acc =\n      if leading_newlines > 1 do\n        meta = %{\n          newlines_before_text: leading_newlines,\n          newlines_after_text: count_newlines_after_text(leading)\n        }\n\n        [{:text, leading, meta} | acc]\n      else\n        acc\n      end\n\n    comment_meta = %{newlines_before_text: 0, newlines_after_text: 0}\n    comment = {:html_comment, [{:text, String.trim(text), comment_meta}]}\n    acc = [comment | acc]\n\n    trailing = trailing_whitespace(text)\n    trailing_newlines = count_newlines_before_text(trailing)\n\n    acc =\n      if trailing_newlines > 1 do\n        meta = %{\n          newlines_before_text: trailing_newlines,\n          newlines_after_text: count_newlines_after_text(trailing)\n        }\n\n        [{:text, trailing, meta} | acc]\n      else\n        acc\n      end\n\n    reduce_html_comments(rest, acc)\n  end\n\n  # Comment start - begin accumulating comment content\n  defp reduce_html_comments(\n         [{:text, text, %{context: [:comment_start]}} | rest],\n         acc\n       ) do\n    collect_comment(rest, [{:text, String.trim_leading(text), %{}}], acc)\n  end\n\n  # Regular node - pass through\n  defp reduce_html_comments([node | rest], acc) do\n    reduce_html_comments(rest, [node | acc])\n  end\n\n  # Collect comment content until we hit comment_end\n  defp collect_comment(\n         [{:text, text, %{context: [:comment_end | _rest]}} | rest],\n         comment_buffer,\n         acc\n       ) do\n    meta = %{\n      newlines_before_text: count_newlines_before_text(text),\n      newlines_after_text: count_newlines_after_text(text)\n    }\n\n    end_text = {:text, String.trim_trailing(text), meta}\n    block = Enum.reverse([end_text | comment_buffer])\n    comment = {:html_comment, block}\n    reduce_html_comments(rest, [comment | acc])\n  end\n\n  defp collect_comment([node | rest], comment_buffer, acc) do\n    collect_comment(rest, [node | comment_buffer], acc)\n  end\n\n  # Handle block tags - add mode and recursively augment children\n  defp augment_node({:block, type, name, attrs, children, meta, close_meta}, state) do\n    tag_name = meta.tag_name\n    mode = determine_mode(tag_name, attrs, meta)\n\n    {children, meta} =\n      if mode == :preserve do\n        content =\n          content_from_source(state.source, meta.inner_location, close_meta.inner_location)\n\n        {[{:text, content, %{newlines_before_text: 0, newlines_after_text: 0}}],\n         Map.put(meta, :mode, :preserve)}\n      else\n        {augment_nodes(children, state), Map.put(meta, :mode, :normal)}\n      end\n\n    maybe_format_tag({:block, type, name, attrs, children, meta, close_meta}, state)\n  end\n\n  # Recursively augment eex_block children\n  defp augment_node({:eex_block, expr, blocks, meta}, state) do\n    blocks =\n      Enum.map(blocks, fn {children, clause, clause_meta} ->\n        {augment_nodes(children, state), clause, clause_meta}\n      end)\n\n    {:eex_block, expr, blocks, meta}\n  end\n\n  # html_comment - recursively augment block content\n  defp augment_node({:html_comment, block}, state) do\n    {:html_comment, augment_nodes(block, state)}\n  end\n\n  # Pass through other node types\n  defp augment_node(node, _state), do: node\n\n  # Determine mode based on tag name, attributes, and existing meta\n  defp determine_mode(tag_name, attrs, meta) do\n    cond do\n      Map.get(meta, :mode) == :preserve -> :preserve\n      tag_name in [\"pre\", \"textarea\"] -> :preserve\n      contains_special_attrs?(attrs) -> :preserve\n      true -> :normal\n    end\n  end\n\n  defp contains_special_attrs?(attrs) do\n    Enum.any?(attrs, fn\n      {\"contenteditable\", {:string, \"false\", _meta}, _} -> false\n      {\"contenteditable\", _v, _} -> true\n      {\"phx-no-format\", _v, _} -> true\n      _ -> false\n    end)\n  end\n\n  # Extract content from source between two locations\n  defp content_from_source(\n         {source, newlines},\n         {line_start, column_start},\n         {line_end, column_end}\n       ) do\n    lines = Enum.slice([{0, 0} | newlines], (line_start - 1)..(line_end - 1))\n    [first_line | _] = lines\n    [last_line | _] = Enum.reverse(lines)\n\n    offset_start = line_byte_offset(source, first_line, column_start)\n    offset_end = line_byte_offset(source, last_line, column_end)\n\n    binary_part(source, offset_start, offset_end - offset_start)\n  end\n\n  defp line_byte_offset(source, {line_before, line_size}, column) do\n    line_offset = line_before + line_size\n\n    line_extra =\n      source\n      |> binary_part(line_offset, byte_size(source) - line_offset)\n      |> String.slice(0, column - 1)\n      |> byte_size()\n\n    line_offset + line_extra\n  end\n\n  defp maybe_format_tag(\n         {:block, :tag, name, attrs, [{:text, content, _meta}] = children, meta, close_meta},\n         state\n       )\n       when is_map_key(state.tag_formatters, name) and name in [\"style\", \"script\"] do\n    simple_attrs =\n      for {key, value, _attr_meta}\n          when (is_tuple(value) and elem(value, 0) == :string) or is_nil(value) <- attrs,\n          into: %{} do\n        case value do\n          {:string, value, _meta} -> {key, value}\n          nil -> {key, true}\n        end\n      end\n\n    simple_attrs =\n      case meta do\n        %{macro_component: mc} ->\n          Map.put(simple_attrs, \":type\", mc)\n\n        _ ->\n          simple_attrs\n      end\n\n    children =\n      case state.tag_formatters[name].render_tag({name, simple_attrs, content}, state.opts) do\n        :skip ->\n          children\n\n        {:ok, formatted_content} ->\n          [{:text, formatted_content, %{newlines_before_text: 0, newlines_after_text: 0}}]\n      end\n\n    {:block, :tag, name, attrs, children, meta, close_meta}\n  end\n\n  defp maybe_format_tag(node, _state), do: node\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/igniter/upgrade_to_1_1.ex",
    "content": "if Code.ensure_loaded?(Igniter) do\n  defmodule Phoenix.LiveView.Igniter.UpgradeTo1_1 do\n    @moduledoc false\n\n    def run(igniter, _opts) do\n      igniter\n      |> Igniter.Project.Deps.add_dep({:lazy_html, \">= 0.0.0\", only: :test}, on_exists: :skip)\n      |> Igniter.Project.MixProject.update(:project, [:compilers], fn\n        nil ->\n          {:ok, {:code, \"[:phoenix_live_view] ++ Mix.compilers()\"}}\n\n        zipper ->\n          cond do\n            Igniter.Code.List.list?(zipper) and\n                !Igniter.Code.List.find_list_item_index(zipper, &(&1 == :phoenix_live_view)) ->\n              Igniter.Code.List.prepend_to_list(zipper, :phoenix_live_view)\n\n            expected_compilers?(zipper) ->\n              {:ok, zipper}\n\n            true ->\n              {:warning,\n               \"\"\"\n               Failed to automatically configure compilers. Please add the following code to the project section of your mix.exs:\n\n                  compilers: [:phoenix_live_view] ++ Mix.compilers()\n               \"\"\"}\n          end\n      end)\n      |> maybe_update_reloadable_compilers()\n      |> maybe_update_esbuild_config()\n      |> maybe_update_debug_config()\n    end\n\n    defp maybe_update_reloadable_compilers(igniter) do\n      endpoint_mod = Igniter.Libs.Phoenix.web_module(igniter) |> Module.concat(Endpoint)\n      app_name = Igniter.Project.Application.app_name(igniter)\n\n      warning = \"\"\"\n      You have `:reloadable_compilers` configured on your dev endpoint in config/dev.exs.\n\n      Ensure that `:phoenix_live_view` is set in there as the first entry!\n\n          config :#{app_name}, #{inspect(endpoint_mod)},\n            reloadable_compilers: [:phoenix_live_view, :elixir, :app]\n      \"\"\"\n\n      if Igniter.Project.Config.configures_key?(igniter, \"dev.exs\", app_name, [\n           endpoint_mod,\n           :reloadable_compilers\n         ]) do\n        Igniter.Project.Config.configure(\n          igniter,\n          \"dev.exs\",\n          app_name,\n          [endpoint_mod, :reloadable_compilers],\n          nil,\n          updater: fn zipper ->\n            if Igniter.Code.List.list?(zipper) do\n              index =\n                Igniter.Code.List.find_list_item_index(zipper, fn zipper ->\n                  case Igniter.Code.Common.expand_literal(zipper) do\n                    {:ok, :phoenix_live_view} -> true\n                    _ -> false\n                  end\n                end)\n\n              cond do\n                index == nil ->\n                  Igniter.Code.List.prepend_to_list(zipper, :phoenix_live_view)\n\n                index == 0 ->\n                  {:ok, zipper}\n\n                index > 0 ->\n                  zipper\n                  |> Igniter.Code.List.remove_index(index)\n                  |> case do\n                    {:ok, zipper} ->\n                      Igniter.Code.List.prepend_to_list(zipper, :phoenix_live_view)\n\n                    :error ->\n                      {:warning, warning}\n                  end\n              end\n            else\n              {:warning, warning}\n            end\n          end\n        )\n      else\n        igniter\n      end\n    end\n\n    defp expected_compilers?(zipper) do\n      Igniter.Code.Function.function_call?(zipper, {Kernel, :++}) &&\n        Igniter.Code.Function.argument_equals?(zipper, 0, [:phoenix_live_view]) &&\n        Igniter.Code.Function.argument_matches_predicate?(zipper, 1, fn zipper ->\n          Igniter.Code.Function.function_call?(zipper, {Mix, :compilers})\n        end)\n    end\n\n    defp maybe_update_esbuild_config(igniter) do\n      if igniter.args.options[:yes] ||\n           Igniter.Util.IO.yes?(\n             \"Do you want to update your esbuild configuration for colocated hooks?\"\n           ) do\n        app_name = Igniter.Project.Application.app_name(igniter)\n\n        warning =\n          \"\"\"\n          Failed to update esbuild configuration for colocated hooks. Please manually:\n\n          1. append `--alias:@=.` to the `args` list\n          2. configure `NODE_PATH` to be a list including `Mix.Project.build_path()`:\n\n              config :esbuild,\n                #{app_name}: [\n                  args:\n                    ~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.),\n                  cd: \"...\",\n                  env: %{\"NODE_PATH\" => [Path.expand(\"../deps\", __DIR__), Mix.Project.build_path()]}\n                ]\n          \"\"\"\n\n        if Igniter.Project.Config.configures_key?(igniter, \"config.exs\", :esbuild, app_name) do\n          config_exs_vsn = Rewrite.Source.version(igniter.rewrite.sources[\"config/config.exs\"])\n\n          igniter =\n            igniter\n            |> Igniter.Project.Deps.add_dep({:esbuild, \"~> 0.10\"}, on_exists: :overwrite)\n            |> Igniter.Project.Deps.set_dep_option(\n              :esbuild,\n              :runtime,\n              quote(do: Mix.env() == :dev)\n            )\n            |> Igniter.Project.Config.configure(\n              \"config.exs\",\n              :esbuild,\n              app_name,\n              nil,\n              updater: fn zipper ->\n                if Igniter.Code.Keyword.keyword_has_path?(zipper, [:args]) and\n                     Igniter.Code.Keyword.keyword_has_path?(zipper, [:env]) do\n                  with {:ok, zipper} <- update_esbuild_args(zipper, warning),\n                       {:ok, zipper} <- update_esbuild_env(zipper) do\n                    {:ok, zipper}\n                  end\n                else\n                  {:warning, warning}\n                end\n              end\n            )\n\n          if config_exs_vsn ==\n               Rewrite.Source.version(igniter.rewrite.sources[\"config/config.exs\"]) do\n            igniter\n          else\n            igniter\n            |> Igniter.add_notice(\"\"\"\n            Final step for colocated hooks:\n\n            Add an import to your `app.js` and configure the hooks option of the LiveSocket:\n\n              ...\n                import {LiveSocket} from \"phoenix_live_view\"\n              + import {hooks as colocatedHooks} from \"phoenix-colocated/#{app_name}\"\n                import topbar from \"../vendor/topbar\"\n              ...\n                const liveSocket = new LiveSocket(\"/live\", Socket, {\n                  longPollFallbackMs: 2500,\n                  params: {_csrf_token: csrfToken},\n              +   hooks: {...colocatedHooks}\n                })\n\n            \"\"\")\n          end\n        else\n          igniter\n        end\n      else\n        igniter\n      end\n    end\n\n    defp update_esbuild_args(zipper, warning) do\n      Igniter.Code.Keyword.put_in_keyword(zipper, [:args], nil, fn zipper ->\n        if Igniter.Code.List.list?(zipper) do\n          Igniter.Code.List.append_new_to_list(zipper, \"--alias:@=.\")\n        else\n          # ~w()\n          case zipper.node do\n            {:sigil_w, _meta, [{:<<>>, _str_meta, [str]}, []]} ->\n              if str =~ \"--alias:@=.\" do\n                {:ok, zipper}\n              else\n                {:ok,\n                 Igniter.Code.Common.replace_code(\n                   zipper,\n                   ~s[~w(#{str <> \" --alias:@=.\"})]\n                 )}\n              end\n\n            _ ->\n              {:warning, warning}\n          end\n        end\n      end)\n    end\n\n    defp update_esbuild_env(zipper) do\n      Igniter.Code.Keyword.put_in_keyword(\n        zipper,\n        [:env],\n        # we already checked that env is configured\n        nil,\n        fn zipper ->\n          Igniter.Code.Map.put_in_map(\n            zipper,\n            [\"NODE_PATH\"],\n            ~s<\"[Path.expand(\"../deps\", __DIR__), Mix.Project.build_path()])>,\n            fn zipper ->\n              if Igniter.Code.List.list?(zipper) do\n                index =\n                  Igniter.Code.List.find_list_item_index(zipper, fn zipper ->\n                    if Igniter.Code.Function.function_call?(\n                         zipper,\n                         {Mix.Project, :build_path},\n                         0\n                       ) do\n                      true\n                    end\n                  end)\n\n                if index do\n                  {:ok, zipper}\n                else\n                  Igniter.Code.List.append_to_list(zipper, {:code, \"Mix.Project.build_path()\"})\n                end\n              else\n                # If NODE_PATH is not a list, convert it to a list with the original value and Mix.Project.build_path()\n                zipper\n                |> Igniter.Code.Common.replace_code(\"[Mix.Project.build_path()]\")\n                |> Igniter.Code.List.prepend_to_list(zipper.node)\n              end\n            end\n          )\n        end\n      )\n    end\n\n    defp maybe_update_debug_config(igniter) do\n      if Igniter.Project.Config.configures_key?(\n           igniter,\n           \"dev.exs\",\n           :phoenix_live_view,\n           :debug_heex_annotations\n         ) do\n        if Igniter.Project.Config.configures_key?(\n             igniter,\n             \"dev.exs\",\n             :phoenix_live_view,\n             :debug_attributes\n           ) do\n          igniter\n        else\n          Igniter.Project.Config.configure(\n            igniter,\n            \"dev.exs\",\n            :phoenix_live_view,\n            :debug_attributes,\n            true\n          )\n        end\n      else\n        igniter\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/js.ex",
    "content": "defmodule Phoenix.LiveView.JS do\n  @moduledoc ~S'''\n  Provides commands for executing JavaScript utility operations on the client.\n\n  JS commands support a variety of utility operations for common client-side\n  needs, such as adding or removing CSS classes, setting or removing tag attributes,\n  showing or hiding content, and transitioning in and out with animations.\n  While these operations can be accomplished via client-side hooks,\n  JS commands are DOM-patch aware, so operations applied\n  by the JS APIs will stick to elements across patches from the server.\n\n  In addition to purely client-side utilities, the JS commands include a\n  rich `push` API, for extending the default `phx-` binding pushes with\n  options to customize targets, loading states, and additional payload values.\n\n  If you need to trigger these commands via JavaScript, see [JavaScript interoperability](js-interop.md#js-commands).\n\n  ## Client Utility Commands\n\n  The following utilities are included:\n\n    * `add_class` - Add classes to elements, with optional transitions\n    * `remove_class` - Remove classes from elements, with optional transitions\n    * `toggle_class` - Sets or removes classes from elements, with optional transitions\n    * `set_attribute` - Set an attribute on elements\n    * `remove_attribute` - Remove an attribute from elements\n    * `toggle_attribute` - Sets or removes element attribute based on attribute presence.\n    * `ignore_attributes` - Marks attributes as ignored, skipping them when patching the DOM.\n    * `show` - Show elements, with optional transitions\n    * `hide` - Hide elements, with optional transitions\n    * `toggle` - Shows or hides elements based on visibility, with optional transitions\n    * `transition` - Apply a temporary transition to elements for animations\n    * `dispatch` - Dispatch a DOM event to elements\n\n  For example, the following modal component can be shown or hidden on the\n  client without a trip to the server:\n\n      alias Phoenix.LiveView.JS\n\n      def hide_modal(js \\\\ %JS{}) do\n        js\n        |> JS.hide(transition: \"fade-out\", to: \"#modal\")\n        |> JS.hide(transition: \"fade-out-scale\", to: \"#modal-content\")\n      end\n\n      def modal(assigns) do\n        ~H\"\"\"\n        <div id=\"modal\" class=\"phx-modal\" phx-remove={hide_modal()}>\n          <div\n            id=\"modal-content\"\n            class=\"phx-modal-content\"\n            phx-click-away={hide_modal()}\n            phx-window-keydown={hide_modal()}\n            phx-key=\"escape\"\n          >\n            <button class=\"phx-modal-close\" phx-click={hide_modal()}>✖</button>\n            <p>{@text}</p>\n          </div>\n        </div>\n        \"\"\"\n      end\n\n  ## Enhanced push events\n\n  The `push/1` command allows you to extend the built-in pushed event handling\n  when a `phx-` event is pushed to the server. For example, you may wish to\n  target a specific component, specify additional payload values to include\n  with the event, apply loading states to external elements, etc. For example,\n  given this basic `phx-click` event:\n\n  ```heex\n  <button phx-click=\"inc\">+</button>\n  ```\n\n  Imagine you need to target your current component, and apply a loading state\n  to the parent container while the client awaits the server acknowledgement:\n\n      alias Phoenix.LiveView.JS\n\n      ~H\"\"\"\n      <button phx-click={JS.push(\"inc\", loading: \".thermo\", target: @myself)}>+</button>\n      \"\"\"\n\n  Push commands also compose with all other utilities. For example,\n  to add a class when pushing:\n\n  ```heex\n  <button phx-click={\n    JS.push(\"inc\", loading: \".thermo\", target: @myself)\n    |> JS.add_class(\"warmer\", to: \".thermo\")\n  }>+</button>\n  ```\n\n  Any `phx-value-*` attributes will also be included in the payload, their\n  values will be overwritten by values given directly to `push/1`. Any\n  `phx-target` attribute will also be used, and overwritten.\n\n  ```heex\n  <button\n    phx-click={JS.push(\"inc\", value: %{limit: 40})}\n    phx-value-room=\"bedroom\"\n    phx-value-limit=\"this value will be 40\"\n    phx-target={@myself}\n  >+</button>\n  ```\n\n  ## DOM Selectors\n\n  The client utility commands in this module all take an optional DOM selector\n  using the `:to` option.\n\n  This can be a string for a regular DOM selector such as:\n\n  ```elixir\n  JS.add_class(\"warmer\", to: \".thermo\")\n  JS.hide(to: \"#modal\")\n  JS.show(to: \"body a:nth-child(2)\")\n  ```\n\n  It is also possible to provide scopes to the DOM selector. The following scopes\n  are available:\n\n   * `{:inner, \"selector\"}` To target an element within the interacted element.\n   * `{:closest, \"selector\"}` To target the closest element from the interacted\n   element upwards.\n\n   For example, if building a dropdown component, the button could use the `:inner`\n   scope:\n\n   ```heex\n   <div phx-click={JS.show(to: {:inner, \".menu\"})}>\n     <div>Open me</div>\n     <div class=\"menu hidden\" phx-click-away={JS.hide()}>\n       I'm in the dropdown menu\n     </div>\n   </div>\n   ```\n\n  ## Custom JS events with `JS.dispatch/1` and `window.addEventListener`\n\n  `dispatch/1` can be used to dispatch custom JavaScript events to\n  elements. For example, you can use `JS.dispatch(\"click\", to: \"#foo\")`,\n  to dispatch a click event to an element.\n\n  This also means you can augment your elements with custom events,\n  by using JavaScript's `window.addEventListener` and invoking them\n  with `dispatch/1`. For example, imagine you want to provide\n  a copy-to-clipboard functionality in your application. You can\n  add a custom event for it:\n\n  ```javascript\n  window.addEventListener(\"my_app:clipcopy\", (event) => {\n    if (\"clipboard\" in navigator) {\n      const text = event.target.textContent;\n      navigator.clipboard.writeText(text);\n    } else {\n      alert(\"Sorry, your browser does not support clipboard copy.\");\n    }\n  });\n  ```\n\n  Now you can have a button like this:\n\n  ```heex\n  <button phx-click={JS.dispatch(\"my_app:clipcopy\", to: \"#element-with-text-to-copy\")}>\n    Copy content\n  </button>\n  ```\n\n  The combination of `dispatch/1` with `window.addEventListener` is\n  a powerful mechanism to increase the amount of actions you can trigger\n  client-side from your LiveView code.\n\n  You can also use `window.addEventListener` to listen to events pushed\n  from the server. You can learn more in our [JS interoperability guide](js-interop.md).\n\n  ## Composing JS commands\n\n  All the functions in this module optionally accept an existing `%JS{}` struct as the first argument,\n  allowing you to chain multiple commands, like pushing an event to the server and optimistically hiding\n  a modal:\n\n  ```heex\n  <div id=\"modal\" class=\"modal\">\n    My Modal\n  </div>\n\n  <button phx-click={JS.push(\"modal-closed\") |> JS.remove_class(\"show\", to: \"#modal\", transition: \"fade-out\")}>\n    hide modal\n  </button>\n  ```\n\n  Note that the commands themselves are executed on the client in the order they are composed\n  and the client does not wait for a confirmation before executing the next command. If you chain\n  `JS.push(...) |> JS.hide(...)`, since hide is a fully client-side command, it hides immediately\n  after pushing the event, not waiting for the server to respond.\n\n  JS commands interacting with the server are documented as such. If you chain multiple commands that\n  interact with the server, those are also guaranteed to be executed in the order they are composed,\n  since a LiveView can only handle one event at a time. Therefore, if you do something like\n\n  ```elixir\n  JS.push(\"my-event\") |> JS.patch(\"/my-path?foo=bar\")\n  ```\n\n  it is guaranteed that the event will be pushed first and the patch will only be handled after\n  the first event was handled by the LiveView.\n\n  '''\n  alias Phoenix.LiveView.JS\n\n  defstruct ops: []\n\n  @opaque internal :: []\n  @type t :: %__MODULE__{ops: internal}\n\n  @default_transition_time 200\n\n  defimpl Phoenix.HTML.Safe, for: Phoenix.LiveView.JS do\n    def to_iodata(%Phoenix.LiveView.JS{} = js) do\n      js\n      |> JS.to_encodable()\n      |> Phoenix.json_library().encode!()\n      |> Phoenix.HTML.Engine.html_escape()\n    end\n  end\n\n  if Code.ensure_loaded?(Jason.Encoder) do\n    defimpl Jason.Encoder, for: Phoenix.LiveView.JS do\n      def encode(%Phoenix.LiveView.JS{} = js, opts) do\n        Jason.Encode.list(JS.to_encodable(js), opts)\n      end\n    end\n  end\n\n  if Code.ensure_loaded?(JSON.Encoder) do\n    defimpl JSON.Encoder, for: Phoenix.LiveView.JS do\n      def encode(%Phoenix.LiveView.JS{} = js, encoder) do\n        JSON.Encoder.encode(JS.to_encodable(js), encoder)\n      end\n    end\n  end\n\n  @doc ~S\"\"\"\n  Returns a JSON-encodable opaque intermediate representation of the JS command.\n\n  Most of the time you will not need to call this function directly, as\n  JS commands are automatically encoded where they are typically used: in\n  [HEEx templates](assigns-eex.md) or within the payload of\n  `Phoenix.LiveView.push_event/3`.\n\n  This function is useful when you use a custom JSON library. JS commands\n  implement the `Jason.Encoder` and `JSON.Encoder` protocols, such that they\n  are automatically encoded when you use either of those JSON libraries.\n\n  ## Examples\n\n  On the server, dynamically compute some JS commands and push them to the\n  client:\n\n  ```elixir\n  socket\n  |> push_event(\"myapp:exec_js\", %{\n    to: \"#items-#{item.id}\",\n    js: js_commands_for(item) |> JS.to_encodable()\n  })\n  ```\n\n  > #### Automatic encoding {: .tip}\n  >\n  > Note that you don't need to call `to_encodable/1` if you are using `Jason` or\n  > `JSON`, instead you can pass the JS commands directly:\n  >\n  > ```elixir\n  > socket\n  > |> push_event(\"myapp:exec_js\", %{\n  >   to: \"#items-#{item.id}\",\n  >   js: JS.show()\n  > })\n  > ```\n\n  On the client, handle the event and execute the commands:\n\n  ```javascript\n  window.addEventListener(\"phx:myapp:exec_js\", e => {\n    const {to, js} = e.detail;\n    const el = document.querySelector(to);\n    if (el && js) {\n      window.liveSocket.execJS(el, js);\n    }\n  });\n  ```\n\n  The common case, though, is having the JS commands stored in an HTML attribute\n  (`phx-*` or `data-*`), such that client-side JavaScript can refer to them\n  later. For example, in a LiveView template:\n\n  ```heex\n  <div id={\"items-#{item.id}\"} data-js={JS.show()}>\n    Hello!\n  </div>\n  ```\n\n  Now the server can push an event that refers to the `data-js` attribute:\n\n  ```elixir\n  socket\n  |> push_event(\"myapp:exec_attr\", %{\n    to: \"#items-#{item.id}\",\n    attr: \"data-js\"\n  })\n  ```\n\n  Finally, on the client, you can read and execute the commands:\n\n  ```javascript\n  window.addEventListener(\"phx:myapp:exec_attr\", e => {\n    const {to, attr} = e.detail;\n    const el = document.querySelector(to);\n    const js = el && attr && el.getAttribute(attr);\n    if (el && js) {\n      window.liveSocket.execJS(el, js);\n    }\n  });\n  ```\n\n  Note how in the code above we didn't need to encode JS commands explicitly,\n  nor to pass them in the event payload, thanks to rendering them in the\n  template.\n\n  \"\"\"\n  @spec to_encodable(js :: JS.t()) :: internal()\n  def to_encodable(%JS{} = js), do: js.ops\n\n  @doc \"\"\"\n  Pushes an event to the server.\n\n    * `event` - The string event name to push.\n\n  ## Options\n\n    * `:target` - A selector or component ID to push to. This value will\n      overwrite any `phx-target` attribute present on the element.\n    * `:loading` - A selector to apply the phx loading classes to,\n      such as `phx-click-loading` in case the event was triggered by\n      `phx-click`. The element will be locked from server updates\n      until the push is acknowledged by the server.\n    * `:page_loading` - Boolean to trigger the phx:page-loading-start and\n      phx:page-loading-stop events for this push. Defaults to `false`.\n    * `:value` - A map of values to send to the server. These values will be\n      merged over any `phx-value-*` attributes that are present on the element.\n      All keys will be treated as strings when merging. When used on a form event\n      like `phx-change` or `phx-submit`, the precedence is\n      `JS.push value > phx-value-* > input value`.\n\n  ## Examples\n\n  ```heex\n  <button phx-click={JS.push(\"clicked\")}>click me!</button>\n  <button phx-click={JS.push(\"clicked\", value: %{id: @id})}>click me!</button>\n  <button phx-click={JS.push(\"clicked\", page_loading: true)}>click me!</button>\n  ```\n  \"\"\"\n  def push(event) when is_binary(event) do\n    push(%JS{}, event, [])\n  end\n\n  @doc \"See `push/1`.\"\n  def push(event, opts) when is_binary(event) and is_list(opts) do\n    push(%JS{}, event, opts)\n  end\n\n  def push(%JS{} = js, event) when is_binary(event) do\n    push(js, event, [])\n  end\n\n  @doc \"See `push/1`.\"\n  def push(%JS{} = js, event, opts) when is_binary(event) and is_list(opts) do\n    opts =\n      opts\n      |> validate_keys(:push, [:target, :loading, :page_loading, :value])\n      |> put_target()\n      |> put_value()\n\n    put_op(js, \"push\", Keyword.put(opts, :event, event))\n  end\n\n  @doc \"\"\"\n  Dispatches an event to the DOM.\n\n    * `event` - The string event name to dispatch.\n\n  *Note*: All events dispatched are of a type\n  [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent),\n  with the exception of `\"click\"`. For a `\"click\"`, a\n  [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent)\n  is dispatched to properly simulate a UI click.\n\n  For emitted `CustomEvent`'s, the event detail will contain a `dispatcher`,\n  which references the DOM node that dispatched the JS event to the target\n  element.\n\n  ## Options\n\n    * `:to` - An optional DOM selector to dispatch the event to.\n      Defaults to the interacted element. See the `DOM selectors`\n      section for details.\n    * `:detail` - An optional detail map to dispatch along\n      with the client event. The details will be available in the\n      `event.detail` attribute for event listeners.\n    * `:bubbles` – A boolean flag to bubble the event or not. Defaults to `true`.\n    * `:blocking` - A boolean flag to block the UI until the event handler calls `event.detail.done()`.\n      The done function is injected by LiveView and *must* be called eventually to unblock the UI.\n      This is useful to integrate with third party JavaScript based animation libraries.\n\n  ## Examples\n\n  ```javascript\n  window.addEventListener(\"click\", e => console.log(\"clicked!\", e.detail))\n  ```\n\n  ```heex\n  <button phx-click={JS.dispatch(\"click\", to: \".nav\")}>Click me!</button>\n  ```\n  \"\"\"\n  def dispatch(js \\\\ %JS{}, event)\n  def dispatch(%JS{} = js, event), do: dispatch(js, event, [])\n  def dispatch(event, opts), do: dispatch(%JS{}, event, opts)\n\n  @doc \"See `dispatch/2`.\"\n  def dispatch(%JS{} = js, event, opts) do\n    opts = validate_keys(opts, :dispatch, [:to, :detail, :bubbles, :blocking])\n    args = [event: event, to: opts[:to]]\n\n    args =\n      case Keyword.fetch(opts, :bubbles) do\n        {:ok, val} when is_boolean(val) ->\n          Keyword.put(args, :bubbles, val)\n\n        {:ok, other} ->\n          raise ArgumentError, \"expected :bubbles to be a boolean, got: #{inspect(other)}\"\n\n        :error ->\n          args\n      end\n\n    if opts[:blocking] do\n      case opts[:detail] do\n        map when is_map(map) and (is_map_key(map, \"done\") or is_map_key(map, :done)) ->\n          raise ArgumentError, \"\"\"\n          the detail map passed to JS.dispatch must not contain a `done` key\n          when `blocking: true` is used!\n\n          Got: #{inspect(map)}\n          \"\"\"\n\n        _ ->\n          :ok\n      end\n    end\n\n    args =\n      case {event, Keyword.fetch(opts, :detail)} do\n        {\"click\", {:ok, _detail}} ->\n          raise ArgumentError, \"\"\"\n          click events cannot be dispatched with details.\n\n          The browser rewrites `MouseEvent` details to an integer. If you would like to\n          handle a click event with custom details, dispatch your own proxy event, read the\n          details, then trigger the click, for example:\n\n              JS.dispatch(\"myapp:click\", detail: %{...})\n              window.addEventListener(\"myapp:click\", e => {\n                console.log(\"details\", e.detail)\n                e.target.click() // forward click event\n              })\n          \"\"\"\n\n        {_, {:ok, detail}} when is_map(detail) ->\n          Keyword.put(args, :detail, detail)\n\n        {_, {:ok, detail}} ->\n          raise ArgumentError, \"\"\"\n          the detail option to JS.dispatch must be a map, got: #{inspect(detail)}\n          \"\"\"\n\n        {_, :error} ->\n          args\n      end\n\n    args =\n      case Keyword.get(opts, :blocking) do\n        true ->\n          Keyword.put(args, :blocking, opts[:blocking])\n\n        _ ->\n          args\n      end\n\n    put_op(js, \"dispatch\", args)\n  end\n\n  @doc \"\"\"\n  Toggles element visibility.\n\n  ## Options\n\n    * `:to` - An optional DOM selector to toggle.\n      Defaults to the interacted element. See the `DOM selectors`\n      section for details.\n    * `:in` - A string of classes to apply when toggling in, or\n      a 3-tuple containing the transition class, the class to apply\n      to start the transition, and the ending transition class, such as:\n      `{\"ease-out duration-300\", \"opacity-0\", \"opacity-100\"}`\n    * `:out` - A string of classes to apply when toggling out, or\n      a 3-tuple containing the transition class, the class to apply\n      to start the transition, and the ending transition class, such as:\n      `{\"ease-out duration-300\", \"opacity-100\", \"opacity-0\"}`\n    * `:time` - The time in milliseconds to apply the transition `:in` and `:out` classes.\n      Defaults to #{@default_transition_time}.\n    * `:display` - An optional display value to set when toggling in. Defaults\n      to `\"block\"`.\n    * `:blocking` - A boolean flag to block the UI during the transition. Defaults `true`.\n\n  When the toggle is complete on the client, a `phx:show-start` or `phx:hide-start`, and\n  `phx:show-end` or `phx:hide-end` event will be dispatched to the toggled elements.\n\n  ## Examples\n\n  ```heex\n  <div id=\"item\">My Item</div>\n\n  <button phx-click={JS.toggle(to: \"#item\")}>\n    toggle item!\n  </button>\n\n  <button phx-click={JS.toggle(to: \"#item\", in: \"fade-in-scale\", out: \"fade-out-scale\")}>\n    toggle fancy!\n  </button>\n  ```\n  \"\"\"\n  def toggle(opts \\\\ [])\n  def toggle(%JS{} = js), do: toggle(js, [])\n  def toggle(opts) when is_list(opts), do: toggle(%JS{}, opts)\n\n  @doc \"See `toggle/1`.\"\n  def toggle(js, opts) when is_list(opts) do\n    opts = validate_keys(opts, :toggle, [:to, :in, :out, :display, :time, :blocking])\n    in_classes = transition_class_names(opts[:in])\n    out_classes = transition_class_names(opts[:out])\n    time = opts[:time]\n\n    put_op(js, \"toggle\",\n      to: opts[:to],\n      display: opts[:display],\n      ins: in_classes,\n      outs: out_classes,\n      time: time,\n      blocking: opts[:blocking]\n    )\n  end\n\n  @doc \"\"\"\n  Shows elements.\n\n  *Note*: Only targets elements that are hidden, meaning they have a height and/or width equal to zero.\n\n  ## Options\n\n    * `:to` - An optional DOM selector to show.\n      Defaults to the interacted element. See the `DOM selectors`\n      section for details.\n    * `:transition` - A string of classes to apply before showing or\n      a 3-tuple containing the transition class, the class to apply\n      to start the transition, and the ending transition class, such as:\n      `{\"ease-out duration-300\", \"opacity-0\", \"opacity-100\"}`\n    * `:time` - The time in milliseconds to apply the transition from `:transition`.\n      Defaults to #{@default_transition_time}.\n    * `:blocking` - A boolean flag to block the UI during the transition. Defaults `true`.\n    * `:display` - An optional display value to set when showing. Defaults to `\"block\"`.\n\n  During the process, the following events will be dispatched to the shown elements:\n\n    * When the action is triggered on the client, `phx:show-start` is dispatched.\n    * After the time specified by `:time`, `phx:show-end` is dispatched.\n\n  ## Examples\n\n  ```heex\n  <div id=\"item\">My Item</div>\n\n  <button phx-click={JS.show(to: \"#item\")}>\n    show!\n  </button>\n\n  <button phx-click={JS.show(to: \"#item\", transition: \"fade-in-scale\")}>\n    show fancy!\n  </button>\n  ```\n  \"\"\"\n  def show(opts \\\\ [])\n  def show(%JS{} = js), do: show(js, [])\n  def show(opts) when is_list(opts), do: show(%JS{}, opts)\n\n  @doc \"See `show/1`.\"\n  def show(js, opts) when is_list(opts) do\n    opts = validate_keys(opts, :show, [:to, :transition, :display, :time, :blocking])\n    transition = transition_class_names(opts[:transition])\n    time = opts[:time]\n\n    put_op(js, \"show\",\n      to: opts[:to],\n      display: opts[:display],\n      transition: transition,\n      time: time,\n      blocking: opts[:blocking]\n    )\n  end\n\n  @doc \"\"\"\n  Hides elements.\n\n  *Note*: Only targets elements that are visible, meaning they have a height and/or width greater than zero.\n\n  ## Options\n\n    * `:to` - An optional DOM selector to hide.\n      Defaults to the interacted element. See the `DOM selectors`\n      section for details.\n    * `:transition` - A string of classes to apply before hiding or\n      a 3-tuple containing the transition class, the class to apply\n      to start the transition, and the ending transition class, such as:\n      `{\"ease-out duration-300\", \"opacity-100\", \"opacity-0\"}`\n    * `:time` - The time in milliseconds to apply the transition from `:transition`.\n      Defaults to #{@default_transition_time}.\n    * `:blocking` - A boolean flag to block the UI during the transition. Defaults `true`.\n\n  During the process, the following events will be dispatched to the hidden elements:\n\n    * When the action is triggered on the client, `phx:hide-start` is dispatched.\n    * After the time specified by `:time`, `phx:hide-end` is dispatched.\n\n  ## Examples\n\n  ```heex\n  <div id=\"item\">My Item</div>\n\n  <button phx-click={JS.hide(to: \"#item\")}>\n    hide!\n  </button>\n\n  <button phx-click={JS.hide(to: \"#item\", transition: \"fade-out-scale\")}>\n    hide fancy!\n  </button>\n  ```\n  \"\"\"\n  def hide(opts \\\\ [])\n  def hide(%JS{} = js), do: hide(js, [])\n  def hide(opts) when is_list(opts), do: hide(%JS{}, opts)\n\n  @doc \"See `hide/1`.\"\n  def hide(js, opts) when is_list(opts) do\n    opts = validate_keys(opts, :hide, [:to, :transition, :time, :blocking])\n    transition = transition_class_names(opts[:transition])\n    time = opts[:time]\n\n    put_op(js, \"hide\",\n      to: opts[:to],\n      transition: transition,\n      time: time,\n      blocking: opts[:blocking]\n    )\n  end\n\n  @doc \"\"\"\n  Adds classes to elements.\n\n    * `names` - A string with one or more class names to add.\n\n  ## Options\n\n    * `:to` - An optional DOM selector to add classes to.\n      Defaults to the interacted element. See the `DOM selectors`\n      section for details.\n    * `:transition` - A string of classes to apply before adding classes or\n      a 3-tuple containing the transition class, the class to apply\n      to start the transition, and the ending transition class, such as:\n      `{\"ease-out duration-300\", \"opacity-0\", \"opacity-100\"}`\n    * `:time` - The time in milliseconds to apply the transition from `:transition`.\n      Defaults to #{@default_transition_time}.\n    * `:blocking` - A boolean flag to block the UI during the transition. Defaults `true`.\n\n  ## Examples\n\n  ```heex\n  <div id=\"item\">My Item</div>\n  <button phx-click={JS.add_class(\"highlight underline\", to: \"#item\")}>\n    highlight!\n  </button>\n  ```\n  \"\"\"\n  def add_class(names) when is_binary(names), do: add_class(%JS{}, names, [])\n\n  @doc \"See `add_class/1`.\"\n  def add_class(%JS{} = js, names) when is_binary(names) do\n    add_class(js, names, [])\n  end\n\n  def add_class(names, opts) when is_binary(names) and is_list(opts) do\n    add_class(%JS{}, names, opts)\n  end\n\n  @doc \"See `add_class/1`.\"\n  def add_class(%JS{} = js, names, opts) when is_binary(names) and is_list(opts) do\n    opts = validate_keys(opts, :add_class, [:to, :transition, :time, :blocking])\n    time = opts[:time]\n\n    put_op(js, \"add_class\",\n      to: opts[:to],\n      names: class_names(names),\n      transition: transition_class_names(opts[:transition]),\n      time: time,\n      blocking: opts[:blocking]\n    )\n  end\n\n  @doc \"\"\"\n  Adds or removes element classes based on presence.\n\n    * `names` - A string with one or more class names to toggle.\n\n  ## Options\n\n    * `:to` - An optional DOM selector to target.\n      Defaults to the interacted element. See the `DOM selectors`\n      section for details.\n    * `:transition` - A string of classes to apply before adding classes or\n      a 3-tuple containing the transition class, the class to apply\n      to start the transition, and the ending transition class, such as:\n      `{\"ease-out duration-300\", \"opacity-0\", \"opacity-100\"}`\n    * `:time` - The time in milliseconds to apply the transition from `:transition`.\n      Defaults to #{@default_transition_time}.\n    * `:blocking` - A boolean flag to block the UI during the transition. Defaults `true`.\n\n  ## Examples\n\n  ```heex\n  <div id=\"item\">My Item</div>\n  <button phx-click={JS.toggle_class(\"active\", to: \"#item\")}>\n    toggle active!\n  </button>\n  ```\n  \"\"\"\n  def toggle_class(names) when is_binary(names), do: toggle_class(%JS{}, names, [])\n\n  def toggle_class(%JS{} = js, names) when is_binary(names) do\n    toggle_class(js, names, [])\n  end\n\n  def toggle_class(names, opts) when is_binary(names) and is_list(opts) do\n    toggle_class(%JS{}, names, opts)\n  end\n\n  def toggle_class(%JS{} = js, names, opts) when is_binary(names) and is_list(opts) do\n    opts = validate_keys(opts, :toggle_class, [:to, :transition, :time, :blocking])\n    time = opts[:time]\n\n    put_op(js, \"toggle_class\",\n      to: opts[:to],\n      names: class_names(names),\n      transition: transition_class_names(opts[:transition]),\n      time: time,\n      blocking: opts[:blocking]\n    )\n  end\n\n  @doc \"\"\"\n  Removes classes from elements.\n\n    * `names` - A string with one or more class names to remove.\n\n  ## Options\n\n    * `:to` - An optional DOM selector to remove classes from.\n      Defaults to the interacted element. See the `DOM selectors`\n      section for details.\n    * `:transition` - A string of classes to apply before removing classes or\n      a 3-tuple containing the transition class, the class to apply\n      to start the transition, and the ending transition class, such as:\n      `{\"ease-out duration-300\", \"opacity-0\", \"opacity-100\"}`\n    * `:time` - The time in milliseconds to apply the transition from `:transition`.\n      Defaults to #{@default_transition_time}.\n    * `:blocking` - A boolean flag to block the UI during the transition. Defaults `true`.\n\n  ## Examples\n\n  ```heex\n  <div id=\"item\">My Item</div>\n  <button phx-click={JS.remove_class(\"highlight underline\", to: \"#item\")}>\n    remove highlight!\n  </button>\n  ```\n  \"\"\"\n  def remove_class(names) when is_binary(names), do: remove_class(%JS{}, names, [])\n\n  @doc \"See `remove_class/1`.\"\n  def remove_class(%JS{} = js, names) when is_binary(names) do\n    remove_class(js, names, [])\n  end\n\n  def remove_class(names, opts) when is_binary(names) and is_list(opts) do\n    remove_class(%JS{}, names, opts)\n  end\n\n  @doc \"See `remove_class/1`.\"\n  def remove_class(%JS{} = js, names, opts) when is_binary(names) and is_list(opts) do\n    opts = validate_keys(opts, :remove_class, [:to, :transition, :time, :blocking])\n    time = opts[:time]\n\n    put_op(js, \"remove_class\",\n      to: opts[:to],\n      names: class_names(names),\n      transition: transition_class_names(opts[:transition]),\n      time: time,\n      blocking: opts[:blocking]\n    )\n  end\n\n  @doc \"\"\"\n  Transitions elements.\n\n    * `transition` - A string of classes to apply during the transition or\n      a 3-tuple containing the transition class, the class to apply\n      to start the transition, and the ending transition class, such as:\n      `{\"ease-out duration-300\", \"opacity-0\", \"opacity-100\"}`\n\n  Transitions are useful for temporarily adding an animation class\n  to elements, such as for highlighting content changes.\n\n  ## Options\n\n    * `:to` - An optional DOM selector to apply transitions to.\n      Defaults to the interacted element. See the `DOM selectors`\n      section for details.\n    * `:time` - The time in milliseconds to apply the transition from `:transition`.\n      Defaults to #{@default_transition_time}.\n    * `:blocking` - A boolean flag to block the UI during the transition. Defaults `true`.\n\n  ## Examples\n\n  ```heex\n  <div id=\"item\">My Item</div>\n  <button phx-click={JS.transition(\"shake\", to: \"#item\")}>Shake!</button>\n\n  <div phx-mounted={JS.transition({\"ease-out duration-300\", \"opacity-0\", \"opacity-100\"}, time: 300)}>\n      duration-300 milliseconds matches time: 300 milliseconds\n  </div>\n  ```\n  \"\"\"\n  def transition(transition) when is_binary(transition) or is_tuple(transition) do\n    transition(%JS{}, transition, [])\n  end\n\n  @doc \"See `transition/1`.\"\n  def transition(transition, opts)\n      when (is_binary(transition) or is_tuple(transition)) and is_list(opts) do\n    transition(%JS{}, transition, opts)\n  end\n\n  def transition(%JS{} = js, transition) when is_binary(transition) or is_tuple(transition) do\n    transition(js, transition, [])\n  end\n\n  @doc \"See `transition/1`.\"\n  def transition(%JS{} = js, transition, opts)\n      when (is_binary(transition) or is_tuple(transition)) and is_list(opts) do\n    opts = validate_keys(opts, :transition, [:to, :time, :blocking])\n    time = opts[:time]\n\n    put_op(js, \"transition\",\n      time: time,\n      to: opts[:to],\n      transition: transition_class_names(transition),\n      blocking: opts[:blocking]\n    )\n  end\n\n  @doc \"\"\"\n  Sets an attribute on elements.\n\n  Accepts a tuple containing the string attribute name/value pair.\n\n  ## Options\n\n    * `:to` - An optional DOM selector to add attributes to.\n      Defaults to the interacted element. See the `DOM selectors`\n      section for details.\n\n  ## Examples\n\n  ```heex\n  <button phx-click={JS.set_attribute({\"aria-expanded\", \"true\"}, to: \"#dropdown\")}>\n    show\n  </button>\n  ```\n\n  > #### A note on properties {: .warning}\n  >\n  > `JS.set_attribute/1` cannot be used to set DOM properties such as the [`value` of an input](https://jakearchibald.com/2024/attributes-vs-properties/#value-on-input-fields).\n  > So if you find yourself wanting to do `JS.set_attribute({\"value\", \"...\"})` on an input, and\n  > see that updated value reflected in a form event, you should use `JS.dispatch/2`\n  > instead:\n  >\n  > Instead of\n  >\n  > ```heex\n  > <.button phx-click={JS.set_attribute({\"value\", \"\"}, to: \"#my_input\")}>...</.button>\n  > ```\n  >\n  > do\n  >\n  > ```heex\n  > <script :type={Phoenix.LiveView.ColocatedJS} name=\"clear_input\">\n  >   window.addEventListener(\"input:clear\", (e) => {\n  >     e.target.value = \"\"\n  >     e.target.dispatchEvent(new Event(\"input\", {bubbles: true}))\n  >   })\n  > </script>\n  > <.button phx-click={JS.dispatch(\"input:clear\", to: \"#my_input\")}>...</.button>\n  > ```\n  >\n  > Note: this uses `Phoenix.LiveView.ColocatedJS`, but you can also define the event listener directly inside\n  > your `app.js` instead.\n  \"\"\"\n  def set_attribute({attr, val}), do: set_attribute(%JS{}, {attr, val}, [])\n\n  @doc \"See `set_attribute/1`.\"\n  def set_attribute({attr, val}, opts) when is_list(opts),\n    do: set_attribute(%JS{}, {attr, val}, opts)\n\n  def set_attribute(%JS{} = js, {attr, val}), do: set_attribute(js, {attr, val}, [])\n\n  @doc \"See `set_attribute/1`.\"\n  def set_attribute(%JS{} = js, {attr, val}, opts) when is_list(opts) do\n    opts = validate_keys(opts, :set_attribute, [:to])\n    put_op(js, \"set_attr\", to: opts[:to], attr: [attr, val])\n  end\n\n  @doc \"\"\"\n  Removes an attribute from elements.\n\n    * `attr` - The string attribute name to remove.\n\n  ## Options\n\n    * `:to` - An optional DOM selector to remove attributes from.\n      Defaults to the interacted element. See the `DOM selectors`\n      section for details.\n\n  ## Examples\n\n  ```heex\n  <button phx-click={JS.remove_attribute(\"aria-expanded\", to: \"#dropdown\")}>\n    hide\n  </button>\n  ```\n  \"\"\"\n  def remove_attribute(attr), do: remove_attribute(%JS{}, attr, [])\n\n  @doc \"See `remove_attribute/1`.\"\n  def remove_attribute(attr, opts) when is_list(opts),\n    do: remove_attribute(%JS{}, attr, opts)\n\n  def remove_attribute(%JS{} = js, attr), do: remove_attribute(js, attr, [])\n\n  @doc \"See `remove_attribute/1`.\"\n  def remove_attribute(%JS{} = js, attr, opts) when is_list(opts) do\n    opts = validate_keys(opts, :remove_attribute, [:to])\n    put_op(js, \"remove_attr\", to: opts[:to], attr: attr)\n  end\n\n  @doc \"\"\"\n  Sets or removes element attribute based on attribute presence.\n\n  Accepts a two or three-element tuple:\n\n  * `{attr, val}` - Sets the attribute to the given value or removes it\n  * `{attr, val1, val2}` - Toggles the attribute between `val1` and `val2`\n\n  ## Options\n\n    * `:to` - An optional DOM selector to set or remove attributes from.\n      Defaults to the interacted element. See the `DOM selectors`\n      section for details.\n\n  ## Examples\n\n  ```heex\n  <button phx-click={JS.toggle_attribute({\"aria-expanded\", \"true\", \"false\"}, to: \"#dropdown\")}>\n    toggle\n  </button>\n\n  <button phx-click={JS.toggle_attribute({\"open\", \"true\"}, to: \"#dialog\")}>\n    toggle\n  </button>\n  ```\n\n  \"\"\"\n  def toggle_attribute({attr, val}), do: toggle_attribute(%JS{}, {attr, val}, [])\n  def toggle_attribute({attr, val1, val2}), do: toggle_attribute(%JS{}, {attr, val1, val2}, [])\n\n  @doc \"See `toggle_attribute/1`.\"\n  def toggle_attribute({attr, val}, opts) when is_list(opts),\n    do: toggle_attribute(%JS{}, {attr, val}, opts)\n\n  def toggle_attribute({attr, val1, val2}, opts) when is_list(opts),\n    do: toggle_attribute(%JS{}, {attr, val1, val2}, opts)\n\n  def toggle_attribute(%JS{} = js, {attr, val}), do: toggle_attribute(js, {attr, val}, [])\n\n  def toggle_attribute(%JS{} = js, {attr, val1, val2}),\n    do: toggle_attribute(js, {attr, val1, val2}, [])\n\n  @doc \"See `toggle_attribute/1`.\"\n  def toggle_attribute(%JS{} = js, {attr, val}, opts) when is_list(opts) do\n    opts = validate_keys(opts, :toggle_attribute, [:to])\n    put_op(js, \"toggle_attr\", to: opts[:to], attr: [attr, val])\n  end\n\n  def toggle_attribute(%JS{} = js, {attr, val1, val2}, opts) when is_list(opts) do\n    opts = validate_keys(opts, :toggle_attribute, [:to])\n    put_op(js, \"toggle_attr\", to: opts[:to], attr: [attr, val1, val2])\n  end\n\n  @doc \"\"\"\n  Mark attributes as ignored, skipping them when patching the DOM.\n\n  Accepts a single attribute name or a list of attribute names.\n  An asterisk `*` can be used as a wildcard.\n\n  Once set, the given attributes will not be patched across LiveView updates.\n  This includes attributes that are removed by the server.\n\n  If you need to \"unmark\" an attribute, you need to call `ignore_attributes/1` again\n  with an updated list of attributes.\n\n  This is mostly useful in combination with the `phx-mounted` binding, for example:\n\n  ```heex\n  <dialog phx-mounted={JS.ignore_attributes(\"open\")}>\n    ...\n  </dialog>\n  ```\n\n  > #### A note on the behavior of phx-mounted {: .info}\n  >\n  > The `phx-mounted` binding executes when the LiveView is mounted.\n  > This means that you cannot use `ignore_attributes/1` to retain attributes\n  > that are set on the client during the disconnected render.\n  > `JS.ignore_attributes/0` will only ever ignore future changes from the server.\n\n  ## Options\n\n    * `:to` - An optional DOM selector to select the target element.\n      Defaults to the interacted element. See the `DOM selectors`\n      section for details.\n\n  ## Examples\n\n      JS.ignore_attributes([\"open\", \"data-*\"], to: \"#my-dialog\")\n\n  \"\"\"\n\n  def ignore_attributes(attrs) when is_list(attrs) or is_binary(attrs),\n    do: ignore_attributes(%JS{}, attrs, [])\n\n  def ignore_attributes(attrs, opts) when (is_list(attrs) or is_binary(attrs)) and is_list(opts),\n    do: ignore_attributes(%JS{}, attrs, opts)\n\n  def ignore_attributes(%JS{} = js, attrs, opts)\n      when (is_list(attrs) or is_binary(attrs)) and is_list(opts) do\n    attrs =\n      case attrs do\n        attr when is_binary(attr) -> [attr]\n        attrs when is_list(attrs) -> attrs\n      end\n\n    opts = validate_keys(opts, :ignore_attributes, [:attrs, :to])\n    put_op(js, \"ignore_attrs\", to: opts[:to], attrs: attrs)\n  end\n\n  @doc \"\"\"\n  Sends focus to a selector.\n\n  ## Options\n\n    * `:to` - An optional DOM selector to send focus to.\n      Defaults to the interacted element. See the `DOM selectors`\n      section for details.\n\n  ## Examples\n\n      JS.focus(to: \"main\")\n  \"\"\"\n  def focus(opts \\\\ [])\n  def focus(%JS{} = js), do: focus(js, [])\n  def focus(opts) when is_list(opts), do: focus(%JS{}, opts)\n\n  @doc \"See `focus/1`.\"\n  def focus(%JS{} = js, opts) when is_list(opts) do\n    opts = validate_keys(opts, :focus, [:to])\n    put_op(js, \"focus\", to: opts[:to])\n  end\n\n  @doc \"\"\"\n  Sends focus to the first focusable child in selector.\n\n  ## Options\n\n    * `:to` - An optional DOM selector to focus.\n      Defaults to the interacted element. See the `DOM selectors`\n      section for details.\n\n  ## Examples\n\n      JS.focus_first(to: \"#modal\")\n  \"\"\"\n  def focus_first(opts \\\\ [])\n  def focus_first(%JS{} = js), do: focus_first(js, [])\n  def focus_first(opts) when is_list(opts), do: focus_first(%JS{}, opts)\n\n  @doc \"See `focus_first/1`.\"\n  def focus_first(%JS{} = js, opts) when is_list(opts) do\n    opts = validate_keys(opts, :focus_first, [:to])\n    put_op(js, \"focus_first\", to: opts[:to])\n  end\n\n  @doc \"\"\"\n  Pushes focus from the source element to be later popped.\n\n  ## Options\n\n    * `:to` - An optional DOM selector to push focus to.\n      Defaults to the interacted element. See the `DOM selectors`\n      section for details.\n\n  ## Examples\n\n      JS.push_focus()\n      JS.push_focus(to: \"#my-button\")\n  \"\"\"\n  def push_focus(opts \\\\ [])\n  def push_focus(%JS{} = js), do: push_focus(js, [])\n  def push_focus(opts) when is_list(opts), do: push_focus(%JS{}, opts)\n\n  @doc \"See `push_focus/1`.\"\n  def push_focus(%JS{} = js, opts) when is_list(opts) do\n    opts = validate_keys(opts, :push_focus, [:to])\n    put_op(js, \"push_focus\", to: opts[:to])\n  end\n\n  @doc \"\"\"\n  Focuses the last pushed element.\n\n  ## Examples\n\n      JS.pop_focus()\n  \"\"\"\n  def pop_focus(%JS{} = js \\\\ %JS{}) do\n    put_op(js, \"pop_focus\", [])\n  end\n\n  @doc \"\"\"\n  Sends a navigation event to the server and updates the browser's pushState history.\n\n  ## Options\n\n    * `:replace` - Whether to replace the browser's pushState history. Defaults to `false`.\n\n  ## Examples\n\n      JS.navigate(\"/my-path\")\n  \"\"\"\n  def navigate(href) when is_binary(href) do\n    navigate(%JS{}, href, [])\n  end\n\n  @doc \"See `navigate/1`.\"\n  def navigate(href, opts) when is_binary(href) and is_list(opts) do\n    navigate(%JS{}, href, opts)\n  end\n\n  def navigate(%JS{} = js, href) when is_binary(href) do\n    navigate(js, href, [])\n  end\n\n  @doc \"See `navigate/1`.\"\n  def navigate(%JS{} = js, href, opts) when is_binary(href) and is_list(opts) do\n    opts = validate_keys(opts, :navigate, [:replace])\n    put_op(js, \"navigate\", href: href, replace: !!opts[:replace])\n  end\n\n  @doc \"\"\"\n  Sends a patch event to the server and updates the browser's pushState history.\n\n  ## Options\n\n    * `:replace` - Whether to replace the browser's pushState history. Defaults to `false`.\n\n  ## Examples\n\n      JS.patch(\"/my-path\")\n  \"\"\"\n  def patch(href) when is_binary(href) do\n    patch(%JS{}, href, [])\n  end\n\n  @doc \"See `patch/1`.\"\n  def patch(href, opts) when is_binary(href) and is_list(opts) do\n    patch(%JS{}, href, opts)\n  end\n\n  def patch(%JS{} = js, href) when is_binary(href) do\n    patch(js, href, [])\n  end\n\n  @doc \"See `patch/1`.\"\n  def patch(%JS{} = js, href, opts) when is_binary(href) and is_list(opts) do\n    opts = validate_keys(opts, :patch, [:replace])\n    put_op(js, \"patch\", href: href, replace: !!opts[:replace])\n  end\n\n  @doc \"\"\"\n  Executes JS commands located in an element's attribute.\n\n    * `attr` - The string attribute where the JS command is specified\n\n  ## Options\n\n    * `:to` - An optional DOM selector to fetch the attribute from.\n      Defaults to the interacted element. See the `DOM selectors`\n      section for details.\n\n  ## Examples\n\n  ```heex\n  <div id=\"modal\" phx-remove={JS.hide(\"#modal\")}>...</div>\n  <button phx-click={JS.exec(\"phx-remove\", to: \"#modal\")}>close</button>\n  ```\n  \"\"\"\n  def exec(attr) when is_binary(attr) do\n    exec(%JS{}, attr, [])\n  end\n\n  @doc \"See `exec/1`.\"\n  def exec(attr, opts) when is_binary(attr) and is_list(opts) do\n    exec(%JS{}, attr, opts)\n  end\n\n  def exec(%JS{} = js, attr) when is_binary(attr) do\n    exec(js, attr, [])\n  end\n\n  @doc \"See `exec/1`.\"\n  def exec(%JS{} = js, attr, opts) when is_binary(attr) and is_list(opts) do\n    opts = validate_keys(opts, :exec, [:to])\n    put_op(js, \"exec\", attr: attr, to: opts[:to])\n  end\n\n  @doc \"\"\"\n  Combines two JS commands, appending the second to the first.\n  \"\"\"\n  def concat(%JS{ops: first}, %JS{ops: second}), do: %JS{ops: first ++ second}\n\n  defp put_op(%JS{ops: ops} = js, kind, args) do\n    args = drop_nil_values(args)\n    struct!(js, ops: ops ++ [[kind, args]])\n  end\n\n  defp drop_nil_values(args) when is_list(args) do\n    Enum.reject(args, fn {_k, v} -> is_nil(v) end)\n    |> Map.new()\n  end\n\n  defp class_names(names) do\n    String.split(names, \" \", trim: true)\n  end\n\n  defp transition_class_names(nil), do: nil\n\n  defp transition_class_names(transition) when is_binary(transition),\n    do: [class_names(transition), [], []]\n\n  defp transition_class_names({transition, tstart, tend})\n       when is_binary(tstart) and is_binary(transition) and is_binary(tend) do\n    [class_names(transition), class_names(tstart), class_names(tend)]\n  end\n\n  defp validate_keys(opts, kind, allowed_keys) do\n    Enum.map(opts, fn\n      {:to, {scope, _selector}} when scope not in [:closest, :inner, :document] ->\n        raise ArgumentError, \"\"\"\n        invalid scope for :to option in #{kind}.\n        Valid scopes are :closest, :inner, :document. Got: #{inspect(scope)}\n        \"\"\"\n\n      {:to, {:document, selector}} ->\n        {:to, selector}\n\n      {:to, {scope, selector}} ->\n        {:to, %{scope => selector}}\n\n      {:to, selector} when is_binary(selector) ->\n        {:to, selector}\n\n      {key, val} ->\n        if key not in allowed_keys do\n          raise ArgumentError, \"\"\"\n          invalid option for #{kind}\n          Expected keys to be one of #{inspect(allowed_keys)}, got: #{inspect(key)}\n          \"\"\"\n        end\n\n        {key, val}\n    end)\n  end\n\n  defp put_value(opts) do\n    case Keyword.fetch(opts, :value) do\n      {:ok, val} when is_map(val) -> Keyword.put(opts, :value, val)\n      {:ok, val} -> raise ArgumentError, \"push :value expected to be a map, got: #{inspect(val)}\"\n      :error -> opts\n    end\n  end\n\n  defp put_target(opts) do\n    case Keyword.fetch(opts, :target) do\n      {:ok, %Phoenix.LiveComponent.CID{cid: cid}} -> Keyword.put(opts, :target, cid)\n      {:ok, selector} -> Keyword.put(opts, :target, selector)\n      :error -> opts\n    end\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/lifecycle.ex",
    "content": "defmodule Phoenix.LiveView.Lifecycle do\n  @moduledoc false\n  alias Phoenix.LiveView.{Socket, Utils}\n\n  @lifecycle :lifecycle\n\n  @type hook :: map()\n\n  @type t :: %__MODULE__{\n          after_render: [hook],\n          handle_async: [hook],\n          handle_event: [hook],\n          handle_info: [hook],\n          handle_params: [hook],\n          mount: [hook]\n        }\n\n  defstruct after_render: [],\n            handle_async: [],\n            handle_event: [],\n            handle_info: [],\n            handle_params: [],\n            mount: []\n\n  @doc \"\"\"\n  Returns a map of infos about the lifecycle stage for the given `view`.\n  \"\"\"\n  def stage_info(%Socket{} = socket, view, stage, arity) do\n    callbacks? = callbacks?(socket, stage)\n    exported? = function_exported?(view, stage, arity)\n\n    %{\n      any?: callbacks? or exported?,\n      callbacks?: callbacks?,\n      exported?: exported?\n    }\n  end\n\n  defp callbacks?(%Socket{private: %{@lifecycle => lifecycle}}, stage)\n       when stage in [:handle_async, :handle_event, :handle_info, :handle_params, :mount] do\n    lifecycle |> Map.fetch!(stage) |> Kernel.!=([])\n  end\n\n  def attach_hook(%Socket{router: nil}, id, :handle_params, _fun) do\n    raise \"cannot attach hook with id #{inspect(id)} on :handle_params because\" <>\n            \" the view was not mounted at the router with the live/3 macro\"\n  end\n\n  def attach_hook(%Socket{} = socket, id, stage, fun)\n      when stage in [:handle_async, :handle_event, :handle_info, :handle_params, :after_render] do\n    lifecycle = lifecycle(socket, stage)\n    hook = hook!(id, stage, fun)\n    existing = Enum.find(Map.fetch!(lifecycle, stage), &(&1.id == id))\n\n    if existing do\n      raise ArgumentError, \"\"\"\n      existing hook #{inspect(hook.id)} already attached on #{inspect(hook.stage)}.\n      \"\"\"\n    end\n\n    update_lifecycle(socket, stage, fn hooks ->\n      hooks ++ [hook]\n    end)\n  end\n\n  def attach_hook(%Socket{}, _id, stage, _fun) do\n    raise ArgumentError, \"\"\"\n    invalid lifecycle event provided to attach_hook.\n\n    Expected one of: :handle_async | :handle_event | :handle_info | :handle_params | :after_render\n\n    Got: #{inspect(stage)}\n    \"\"\"\n  end\n\n  def detach_hook(%Socket{} = socket, id, stage)\n      when stage in [:handle_async, :handle_event, :handle_info, :handle_params, :after_render] do\n    update_lifecycle(socket, stage, fn hooks ->\n      for hook <- hooks, hook.id != id, do: hook\n    end)\n  end\n\n  def detach_hook(%Socket{}, _id, stage) do\n    raise ArgumentError, \"\"\"\n    invalid lifecycle event provided to detach_hook.\n\n    Expected one of: :handle_async | :handle_event | :handle_info | :handle_params | :after_render\n\n    Got: #{inspect(stage)}\n    \"\"\"\n  end\n\n  defp hook!(id, stage, fun) when is_atom(stage) and is_function(fun) do\n    %{id: id, stage: stage, function: fun}\n  end\n\n  defp lifecycle(socket, stage) do\n    if Utils.cid(socket) && stage not in [:after_render, :handle_event, :handle_async] do\n      raise ArgumentError, \"lifecycle hooks are not supported on stateful components.\"\n    end\n\n    Map.fetch!(socket.private, @lifecycle)\n  end\n\n  defp update_lifecycle(socket, stage, fun) do\n    lifecycle = lifecycle(socket, stage)\n    new_lifecycle = Map.update!(lifecycle, stage, fun)\n    put_lifecycle(socket, new_lifecycle)\n  end\n\n  defp put_lifecycle(socket, lifecycle) do\n    put_private(socket, @lifecycle, lifecycle)\n  end\n\n  defp put_private(%Socket{private: private} = socket, key, value) when is_atom(key) do\n    %{socket | private: Map.put(private, key, value)}\n  end\n\n  @doc false\n  def validate_on_mount!(_view, {module, arg}) when is_atom(module) do\n    {module, arg}\n  end\n\n  def validate_on_mount!(_view, module) when is_atom(module) do\n    {module, :default}\n  end\n\n  def validate_on_mount!(view, result) do\n    raise ArgumentError, \"\"\"\n    invalid on_mount hook declared in #{inspect(view)}.\n\n    Expected one of:\n\n        Module\n        {Module, arg}\n\n    Got: #{inspect(result)}\n    \"\"\"\n  end\n\n  @doc false\n  def prepare_on_mount!(hooks) do\n    for {module, _fun} = id <- hooks do\n      hook!(id, :mount, Function.capture(module, :on_mount, 4))\n    end\n  end\n\n  # Lifecycle Event API\n\n  @doc false\n  def build(mount_hooks) when is_list(mount_hooks) do\n    %__MODULE__{mount: prepare_on_mount!(mount_hooks)}\n  end\n\n  @doc false\n  def mount(params, session, %Socket{private: %{@lifecycle => lifecycle}} = socket) do\n    reduce_socket(lifecycle.mount, socket, fn %{id: {mod, arg}} = hook, acc ->\n      case hook.function.(arg, params, session, acc) do\n        {:halt, %Socket{redirected: nil}} ->\n          raise_halt_without_redirect!(hook)\n\n        {:halt, %Socket{redirected: nil}, _opts} ->\n          raise_halt_without_redirect!(hook)\n\n        {:cont, %Socket{redirected: to}} when not is_nil(to) ->\n          raise_continue_with_redirect!(hook)\n\n        {:cont, %Socket{redirected: to}, _opts} when not is_nil(to) ->\n          raise_continue_with_redirect!(hook)\n\n        {:cont, socket, opts} ->\n          {:cont, Utils.handle_mount_options!(socket, opts, {mod, :on_mount, 4})}\n\n        ok ->\n          ok\n      end\n    end)\n  end\n\n  @doc false\n  def handle_event(event, val, %Socket{private: %{@lifecycle => lifecycle}} = socket) do\n    reduce_handle_event(lifecycle.handle_event, socket, fn hook, acc ->\n      hook.function.(event, val, acc)\n    end)\n  end\n\n  defp reduce_handle_event([hook | hooks], acc, function) do\n    case function.(hook, acc) do\n      {:cont, %Socket{} = socket} -> reduce_handle_event(hooks, socket, function)\n      {:halt, %Socket{} = socket} -> {:halt, socket}\n      {:halt, reply, %Socket{} = socket} -> {:halt, reply, socket}\n      other -> bad_lifecycle_response!(other, hook)\n    end\n  end\n\n  defp reduce_handle_event([], acc, _function), do: {:cont, acc}\n\n  @doc false\n  def handle_params(params, uri, %Socket{private: %{@lifecycle => lifecycle}} = socket) do\n    reduce_socket(lifecycle.handle_params, socket, fn hook, acc ->\n      hook.function.(params, uri, acc)\n    end)\n  end\n\n  @doc false\n  def handle_info(msg, %Socket{private: %{@lifecycle => lifecycle}} = socket) do\n    reduce_socket(lifecycle.handle_info, socket, fn hook, acc ->\n      hook.function.(msg, acc)\n    end)\n  end\n\n  @doc false\n  def handle_async(key, result, %Socket{private: %{@lifecycle => lifecycle}} = socket) do\n    reduce_socket(lifecycle.handle_async, socket, fn hook, acc ->\n      hook.function.(key, result, acc)\n    end)\n  end\n\n  @doc false\n  def after_render(%Socket{private: %{@lifecycle => lifecycle}} = socket) do\n    {:cont, new_socket} =\n      reduce_socket(lifecycle.after_render, socket, fn hook, acc ->\n        case hook.function.(acc) do\n          %Socket{} = new_socket ->\n            {:cont, new_socket}\n\n          other ->\n            raise ArgumentError,\n                  \"expected after_render hook to return a socket, got: #{inspect(other)}\"\n        end\n      end)\n\n    new_socket\n  end\n\n  defp reduce_socket([hook | hooks], acc, function) do\n    case function.(hook, acc) do\n      {:cont, %Socket{} = socket} -> reduce_socket(hooks, socket, function)\n      {:halt, %Socket{} = socket} -> {:halt, socket}\n      other -> bad_lifecycle_response!(other, hook)\n    end\n  end\n\n  defp reduce_socket([], acc, _function), do: {:cont, acc}\n\n  defp bad_lifecycle_response!(result, hook) do\n    raise ArgumentError, \"\"\"\n    invalid return from hook #{inspect(hook.id)} for lifecycle event #{inspect(hook.stage)}.\n\n    Expected one of:\n\n    #{expected_return(hook)}\n\n    Got: #{inspect(result)}\n    \"\"\"\n  end\n\n  defp expected_return(%{stage: :handle_event}) do\n    \"\"\"\n    {:cont, %Socket{}}\n    {:halt, %Socket{}}\n    {:halt, map, %Socket{}}\n    \"\"\"\n  end\n\n  defp expected_return(_) do\n    \"\"\"\n    {:cont, %Socket{}}\n    {:halt, %Socket{}}\n    \"\"\"\n  end\n\n  defp raise_halt_without_redirect!(hook) do\n    raise ArgumentError,\n          \"the hook #{inspect(hook.id)} for lifecycle event :mount attempted to halt without redirecting.\"\n  end\n\n  defp raise_continue_with_redirect!(hook) do\n    raise ArgumentError,\n          \"the hook #{inspect(hook.id)} for lifecycle event :mount attempted to redirect without halting.\"\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/live_stream.ex",
    "content": "defmodule Phoenix.LiveView.LiveStream do\n  @moduledoc false\n\n  defstruct name: nil,\n            dom_id: nil,\n            ref: nil,\n            inserts: [],\n            deletes: [],\n            reset?: false,\n            consumable?: false\n\n  alias Phoenix.LiveView.LiveStream\n\n  def new(name, ref, items, opts) when is_list(opts) do\n    dom_prefix = to_string(name)\n    dom_id = Keyword.get_lazy(opts, :dom_id, fn -> &default_id(dom_prefix, &1) end)\n\n    if not is_function(dom_id, 1) do\n      raise ArgumentError,\n            \"stream :dom_id must return a function which accepts each item, got: #{inspect(dom_id)}\"\n    end\n\n    # We need to go through the items one time to map them into the proper insert tuple format.\n    # Conveniently, we reverse the list in this pass, which we need to in order to be consistent\n    # with manually calling stream_insert multiple times, as stream_insert prepends.\n    items_list =\n      for item <- items, reduce: [] do\n        items -> [{dom_id.(item), -1, item, opts[:limit], opts[:update_only]} | items]\n      end\n\n    %LiveStream{\n      ref: ref,\n      name: name,\n      dom_id: dom_id,\n      inserts: items_list,\n      deletes: [],\n      reset?: false\n    }\n  end\n\n  defp default_id(dom_prefix, %{id: id} = _struct_or_map), do: dom_prefix <> \"-#{to_string(id)}\"\n\n  defp default_id(dom_prefix, other) do\n    raise ArgumentError, \"\"\"\n    expected stream :#{dom_prefix} to be a struct or map with :id key, got: #{inspect(other)}\n\n    If you would like to generate custom DOM id's based on other keys, use `Phoenix.LiveView.stream_configure/3` with the :dom_id option beforehand.\n    \"\"\"\n  end\n\n  def reset(%LiveStream{} = stream) do\n    %{stream | reset?: true}\n  end\n\n  def prune(%LiveStream{} = stream) do\n    %{stream | inserts: [], deletes: [], reset?: false}\n  end\n\n  def delete_item(%LiveStream{} = stream, item) do\n    delete_item_by_dom_id(stream, stream.dom_id.(item))\n  end\n\n  def delete_item_by_dom_id(%LiveStream{} = stream, dom_id) do\n    %{stream | deletes: [dom_id | stream.deletes]}\n  end\n\n  def insert_item(%LiveStream{} = stream, item, at, limit, update_only) do\n    item_id = stream.dom_id.(item)\n\n    %{stream | inserts: [{item_id, at, item, limit, update_only} | stream.inserts]}\n  end\n\n  def mark_consumable(%Phoenix.LiveView.LiveStream{} = stream) do\n    %{stream | consumable?: true}\n  end\n\n  def mark_consumable(collection), do: collection\n\n  def annotate_comprehension(comprehension, %Phoenix.LiveView.LiveStream{} = stream) do\n    inserts =\n      for {id, at, _item, limit, update_only} <- stream.inserts, do: [id, at, limit, update_only]\n\n    data = [stream.ref, inserts, stream.deletes]\n\n    if stream.reset? do\n      Map.put(comprehension, :stream, data ++ [true])\n    else\n      Map.put(comprehension, :stream, data)\n    end\n  end\n\n  def annotate_comprehension(comprehension, _collection), do: comprehension\n\n  defimpl Enumerable, for: LiveStream do\n    def count(%LiveStream{inserts: inserts}), do: {:ok, length(inserts)}\n\n    def member?(%LiveStream{}, _item), do: raise(RuntimeError, \"not implemented\")\n\n    def reduce(%LiveStream{} = stream, acc, fun) do\n      if stream.consumable? do\n        # the inserts are stored in reverse insert order, so we need to reverse them\n        # before rendering; we also remove duplicates to only use the most recent\n        # inserts, which, as the items are reversed, are first\n        {inserts, _} =\n          for {id, _, _, _, _} = insert <- stream.inserts, reduce: {[], MapSet.new()} do\n            {inserts, ids} ->\n              if MapSet.member?(ids, id) do\n                # skip duplicates\n                {inserts, ids}\n              else\n                {[insert | inserts], MapSet.put(ids, id)}\n              end\n          end\n\n        do_reduce(inserts, acc, fun)\n      else\n        raise ArgumentError, \"\"\"\n        streams can only be consumed directly by a for comprehension.\n        If you are attempting to consume the stream ahead of time, such as with\n        `Enum.with_index(@streams.#{stream.name})`, you need to place the relevant information\n        within the stream items instead.\n        \"\"\"\n      end\n    end\n\n    defp do_reduce(_list, {:halt, acc}, _fun), do: {:halted, acc}\n    defp do_reduce(list, {:suspend, acc}, fun), do: {:suspended, acc, &do_reduce(list, &1, fun)}\n    defp do_reduce([], {:cont, acc}, _fun), do: {:done, acc}\n\n    defp do_reduce([{dom_id, _at, item, _limit, _update_only} | tail], {:cont, acc}, fun) do\n      do_reduce(tail, fun.({dom_id, item}, acc), fun)\n    end\n\n    # Returns a function that slices the data structure contiguously.\n    def slice(%LiveStream{}), do: raise(RuntimeError, \"not implemented\")\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/logger.ex",
    "content": "defmodule Phoenix.LiveView.Logger do\n  @moduledoc \"\"\"\n  Instrumenter to handle logging of `Phoenix.LiveView` and `Phoenix.LiveComponent` life-cycle events.\n\n  ## Installation\n\n  The logger is installed automatically when Live View starts.\n  By default, the log level is set to `:debug`.\n\n  ## Module configuration\n\n  The log level can be overridden for an individual Live View module:\n\n      use Phoenix.LiveView, log: :debug\n\n  To disable logging for an individual Live View module:\n\n      use Phoenix.LiveView, log: false\n\n  ## Telemetry\n\n  The following `Phoenix.LiveView` and `Phoenix.LiveComponent` events are logged:\n\n    - `[:phoenix, :live_view, :mount, :start]`\n    - `[:phoenix, :live_view, :mount, :stop]`\n    - `[:phoenix, :live_view, :handle_params, :start]`\n    - `[:phoenix, :live_view, :handle_params, :stop]`\n    - `[:phoenix, :live_view, :handle_event, :start]`\n    - `[:phoenix, :live_view, :handle_event, :stop]`\n    - `[:phoenix, :live_component, :handle_event, :start]`\n    - `[:phoenix, :live_component, :handle_event, :stop]`\n\n  See the [Telemetry](./guides/server/telemetry.md) guide for more information.\n\n  ## Parameter filtering\n\n  If enabled, `Phoenix.LiveView.Logger` will filter parameters based on the configuration of `Phoenix.Logger`.\n  \"\"\"\n\n  import Phoenix.LiveView, only: [connected?: 1]\n\n  import Phoenix.Logger, only: [duration: 1, filter_values: 1]\n\n  require Logger\n\n  @doc false\n  def install do\n    handlers = %{\n      [:phoenix, :live_view, :mount, :start] => &__MODULE__.lv_mount_start/4,\n      [:phoenix, :live_view, :mount, :stop] => &__MODULE__.lv_mount_stop/4,\n      [:phoenix, :live_view, :handle_params, :start] => &__MODULE__.lv_handle_params_start/4,\n      [:phoenix, :live_view, :handle_params, :stop] => &__MODULE__.lv_handle_params_stop/4,\n      [:phoenix, :live_view, :handle_event, :start] => &__MODULE__.lv_handle_event_start/4,\n      [:phoenix, :live_view, :handle_event, :stop] => &__MODULE__.lv_handle_event_stop/4,\n      [:phoenix, :live_component, :handle_event, :start] => &__MODULE__.lc_handle_event_start/4,\n      [:phoenix, :live_component, :handle_event, :stop] => &__MODULE__.lc_handle_event_stop/4\n    }\n\n    for {key, fun} <- handlers do\n      :telemetry.attach({__MODULE__, key}, key, fun, %{})\n    end\n  end\n\n  defp log_level(socket) do\n    Map.fetch!(socket.view.__live__(), :log)\n  end\n\n  @doc false\n  def lv_mount_start(_event, measurement, metadata, _config) do\n    %{socket: socket, params: params, session: session, uri: _uri} = metadata\n    %{system_time: _system_time} = measurement\n    level = log_level(socket)\n\n    if level && connected?(socket) do\n      Logger.log(level, fn ->\n        [\n          \"MOUNT \",\n          inspect(socket.view),\n          ?\\n,\n          \"  Parameters: \",\n          inspect(filter_values(params)),\n          ?\\n,\n          \"  Session: \",\n          inspect(session)\n        ]\n      end)\n    end\n\n    :ok\n  end\n\n  @doc false\n  def lv_mount_stop(_event, measurement, metadata, _config) do\n    %{socket: socket, params: _params, session: _session, uri: _uri} = metadata\n    %{duration: duration} = measurement\n    level = log_level(socket)\n\n    if level && connected?(socket) do\n      Logger.log(level, fn ->\n        [\n          \"Replied in \",\n          duration(duration)\n        ]\n      end)\n    end\n\n    :ok\n  end\n\n  @doc false\n  def lv_handle_params_start(_event, measurement, metadata, _config) do\n    %{socket: socket, params: params, uri: _uri} = metadata\n    %{system_time: _system_time} = measurement\n    level = log_level(socket)\n\n    if level && connected?(socket) do\n      Logger.log(level, fn ->\n        [\n          \"HANDLE PARAMS in \",\n          inspect(socket.view),\n          ?\\n,\n          \"  Parameters: \",\n          inspect(filter_values(params))\n        ]\n      end)\n    end\n\n    :ok\n  end\n\n  @doc false\n  def lv_handle_params_stop(_event, measurement, metadata, _config) do\n    %{socket: socket, params: _params, uri: _uri} = metadata\n    %{duration: duration} = measurement\n    level = log_level(socket)\n\n    if level && connected?(socket) do\n      Logger.log(level, fn ->\n        [\n          \"Replied in \",\n          duration(duration)\n        ]\n      end)\n    end\n\n    :ok\n  end\n\n  @doc false\n  def lv_handle_event_start(_event, measurement, metadata, _config) do\n    %{socket: socket, event: event, params: params} = metadata\n    %{system_time: _system_time} = measurement\n    level = log_level(socket)\n\n    if level do\n      Logger.log(level, fn ->\n        [\n          \"HANDLE EVENT \",\n          inspect(event),\n          \" in \",\n          inspect(socket.view),\n          ?\\n,\n          \"  Parameters: \",\n          inspect(filter_values(params))\n        ]\n      end)\n    end\n\n    :ok\n  end\n\n  @doc false\n  def lv_handle_event_stop(_event, measurement, metadata, _config) do\n    %{socket: socket, event: _event, params: _params} = metadata\n    %{duration: duration} = measurement\n    level = log_level(socket)\n\n    if level do\n      Logger.log(level, fn ->\n        [\n          \"Replied in \",\n          duration(duration)\n        ]\n      end)\n    end\n\n    :ok\n  end\n\n  @doc false\n  def lc_handle_event_start(_event, measurement, metadata, _config) do\n    %{socket: socket, component: component, event: event, params: params} = metadata\n    %{system_time: _system_time} = measurement\n    level = log_level(socket)\n\n    if level do\n      Logger.log(level, fn ->\n        [\n          \"HANDLE EVENT \",\n          inspect(event),\n          \" in \",\n          inspect(socket.view),\n          \"\\n  Component: \",\n          inspect(component),\n          \"\\n  Parameters: \",\n          inspect(filter_values(params))\n        ]\n      end)\n    end\n\n    :ok\n  end\n\n  @doc false\n  def lc_handle_event_stop(_event, measurement, metadata, _config) do\n    %{socket: socket, component: _component, event: _event, params: _params} = metadata\n    %{duration: duration} = measurement\n    level = log_level(socket)\n\n    if level do\n      Logger.log(level, fn ->\n        [\n          \"Replied in \",\n          duration(duration)\n        ]\n      end)\n    end\n\n    :ok\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/plug.ex",
    "content": "defmodule Phoenix.LiveView.Plug do\n  @moduledoc false\n\n  @behaviour Plug\n\n  @impl Plug\n  def init(view) when is_atom(view), do: view\n\n  @impl Plug\n  def call(%Plug.Conn{private: %{phoenix_live_view: {view, opts, live_session}}} = conn, _) do\n    %{extra: live_session_extra} = live_session\n    session = live_session(live_session_extra, conn)\n    opts = Keyword.put(opts, :session, session)\n\n    conn\n    |> Phoenix.Controller.put_layout(false)\n    |> put_root_layout_from_router(live_session_extra)\n    |> Phoenix.LiveView.Controller.live_render(view, opts)\n  end\n\n  defp live_session(opts, conn) do\n    case opts[:session] do\n      {mod, fun, args} when is_atom(mod) and is_atom(fun) and is_list(args) ->\n        apply(mod, fun, [conn | args])\n\n      %{} = session ->\n        session\n\n      nil ->\n        %{}\n    end\n  end\n\n  defp put_root_layout_from_router(conn, extra) do\n    case Map.fetch(extra, :root_layout) do\n      {:ok, layout} -> Phoenix.Controller.put_root_layout(conn, layout)\n      :error -> conn\n    end\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/renderer.ex",
    "content": "defmodule Phoenix.LiveView.Renderer do\n  @moduledoc false\n\n  alias Phoenix.LiveView.{Rendered, Socket}\n\n  defmacro __before_compile__(%{module: module, file: file} = env) do\n    render? = Module.defines?(module, {:render, 1})\n    root = Path.dirname(file)\n    filename = template_filename(module)\n    templates = Phoenix.Template.find_all(root, filename)\n\n    case {render?, templates} do\n      {true, [template | _]} ->\n        IO.warn(\n          \"ignoring template #{inspect(template)} because the LiveView \" <>\n            \"#{inspect(env.module)} defines a render/1 function\",\n          Macro.Env.stacktrace(env)\n        )\n\n        :ok\n\n      {true, []} ->\n        :ok\n\n      {false, [template]} ->\n        ext = template |> Path.extname() |> String.trim_leading(\".\") |> String.to_atom()\n        engine = Map.fetch!(Phoenix.Template.engines(), ext)\n        ast = engine.compile(template, filename)\n\n        quote do\n          @file unquote(template)\n          @external_resource unquote(template)\n          def render(var!(assigns)) when is_map(var!(assigns)) do\n            unquote(ast)\n          end\n        end\n\n      {false, [_ | _]} ->\n        IO.warn(\n          \"multiple templates were found for #{inspect(env.module)}: #{inspect(templates)}\",\n          Macro.Env.stacktrace(env)\n        )\n\n        :ok\n\n      {false, []} ->\n        template = Path.join(root, filename <> \".heex\")\n\n        quote do\n          @external_resource unquote(template)\n        end\n    end\n  end\n\n  defp template_filename(module) do\n    module\n    |> Module.split()\n    |> List.last()\n    |> Macro.underscore()\n    |> Kernel.<>(\".html\")\n  end\n\n  @doc \"\"\"\n  Renders the view with socket into a rendered struct.\n  \"\"\"\n  def to_rendered(socket, view) do\n    assigns = render_assigns(socket)\n\n    inner_content =\n      case socket do\n        %{private: %{render_with: render_with}} ->\n          assigns\n          |> render_with.()\n          |> check_rendered!(render_with)\n\n        %{} ->\n          if function_exported?(view, :render, 1) do\n            assigns\n            |> view.render()\n            |> check_rendered!(view)\n          else\n            template =\n              view.__info__(:compile)[:source]\n              |> Path.dirname()\n              |> Path.join(template_filename(view) <> \".heex\")\n\n            raise ~s'''\n            render/1 was not implemented for #{inspect(view)}.\n\n            In order to render templates in LiveView/LiveComponent, you must either:\n\n            1. Define a render/1 function that receives assigns and uses the ~H sigil:\n\n                def render(assigns) do\n                  ~H\"\"\"\n                  <div>...</div>\n                  \"\"\"\n                end\n\n            2. Create a file at #{inspect(template)} with template contents\n\n            3. Call Phoenix.LiveView.render_with/2 with a custom rendering function\n            '''\n          end\n      end\n\n    case layout(socket, view) do\n      {layout_mod, layout_template} ->\n        assigns = put_in(assigns[:inner_content], inner_content)\n        assigns = put_in(assigns.__changed__[:inner_content], true)\n\n        layout_mod\n        |> Phoenix.Template.render(to_string(layout_template), \"html\", assigns)\n        |> check_rendered!(layout_mod)\n\n      false ->\n        inner_content\n    end\n  end\n\n  defp render_assigns(%Socket{assigns: assigns} = socket) do\n    socket = %{socket | assigns: %Socket.AssignsNotInSocket{__assigns__: assigns}}\n    Map.put(assigns, :socket, socket)\n  end\n\n  defp check_rendered!(%Rendered{} = rendered, _view), do: rendered\n\n  defp check_rendered!(other, view) do\n    raise RuntimeError, \"\"\"\n    expected #{inspect(view)} to return a %Phoenix.LiveView.Rendered{} struct\n\n    Ensure your render function uses ~H, or your template uses the .heex extension.\n\n    Got:\n\n        #{inspect(other)}\n\n    \"\"\"\n  end\n\n  defp layout(socket, view) do\n    case socket.private do\n      %{live_layout: layout} -> layout\n      %{} -> view.__live__()[:layout]\n    end\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/route.ex",
    "content": "defmodule Phoenix.LiveView.Route do\n  @moduledoc false\n\n  alias Phoenix.LiveView.{Route, Socket}\n\n  defstruct path: nil,\n            view: nil,\n            action: nil,\n            opts: [],\n            live_session: %{},\n            params: %{},\n            uri: nil\n\n  @doc \"\"\"\n  Computes the container from the route options and falls backs to use options.\n  \"\"\"\n  def container(%Route{} = route) do\n    route.opts[:container] || route.view.__live__()[:container]\n  end\n\n  @doc \"\"\"\n  Returns the internal or external matched LiveView route info for the given socket\n  and uri, raises if none is available.\n  \"\"\"\n  def live_link_info!(%Socket{router: nil}, view, _uri) do\n    raise ArgumentError,\n          \"cannot invoke handle_params/3 on #{inspect(view)} \" <>\n            \"because it is not mounted nor accessed through the router live/3 macro\"\n  end\n\n  def live_link_info!(%Socket{} = socket, view, uri) do\n    %{private: %{live_session_name: session_name}} = socket\n\n    case live_link_info_without_checks(socket.endpoint, socket.router, uri) do\n      {:internal, %Route{view: ^view, live_session: %{name: ^session_name}} = route} ->\n        {:internal, route}\n\n      {:internal, %Route{} = route} ->\n        {:external, route.uri}\n\n      {:external, _parsed_uri} = external ->\n        external\n\n      :error ->\n        raise ArgumentError,\n              \"cannot invoke handle_params nor navigate/patch to #{inspect(uri)} \" <>\n                \"because it isn't defined in #{inspect(socket.router)}\"\n    end\n  end\n\n  @doc \"\"\"\n  Returns the internal or external matched LiveView route info for the given uri.\n  \"\"\"\n  def live_link_info_without_checks(endpoint, router, uri) when is_binary(uri) do\n    live_link_info_without_checks(endpoint, router, URI.parse(uri))\n  end\n\n  def live_link_info_without_checks(endpoint, router, %URI{} = parsed_uri)\n      when is_atom(endpoint) and is_atom(router) do\n    %URI{host: host, path: path, query: query} = parsed_uri\n    query_params = if query, do: Plug.Conn.Query.decode(query), else: %{}\n\n    split_path =\n      for segment <- String.split(path || \"\", \"/\"), segment != \"\", do: URI.decode(segment)\n\n    route_path = strip_segments(endpoint.script_name(), split_path) || split_path\n\n    case Phoenix.Router.route_info(router, \"GET\", route_path, host) do\n      %{plug: Phoenix.LiveView.Plug, phoenix_live_view: lv, path_params: path_params} ->\n        {view, action, opts, live_session} = lv\n\n        route = %Route{\n          view: view,\n          path: route_path,\n          action: action,\n          uri: parsed_uri,\n          opts: opts,\n          live_session: live_session,\n          params: Map.merge(query_params, path_params)\n        }\n\n        {:internal, route}\n\n      %{} ->\n        {:external, parsed_uri}\n\n      :error ->\n        :error\n    end\n  end\n\n  defp strip_segments([head | tail1], [head | tail2]), do: strip_segments(tail1, tail2)\n  defp strip_segments([], tail2), do: tail2\n  defp strip_segments(_, _), do: nil\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/router.ex",
    "content": "defmodule Phoenix.LiveView.Router do\n  @moduledoc \"\"\"\n  Provides LiveView routing for Phoenix routers.\n  \"\"\"\n\n  @cookie_key \"__phoenix_flash__\"\n\n  @doc ~S\"\"\"\n  Defines a LiveView route.\n\n  A LiveView can be routed to by using the `live` macro with a path and\n  the name of the LiveView:\n\n      live \"/thermostat\", ThermostatLive\n\n  To navigate to this route within your app, you can use `Phoenix.VerifiedRoutes`:\n\n      push_navigate(socket, to: ~p\"/thermostat\")\n      push_patch(socket, to: ~p\"/thermostat?page=#{page}\")\n\n  > #### HTTP requests {: .info}\n  >\n  > The HTTP request method that a route defined by the `live/4` macro\n  > responds to is `GET`.\n\n  ## Actions and live navigation\n\n  It is common for a LiveView to have multiple states and multiple URLs.\n  For example, you can have a single LiveView that lists all articles on\n  your web app. For each article there is an \"Edit\" button which, when\n  pressed, opens up a modal on the same page to edit the article. It is a\n  best practice to use live navigation in those cases, so when you click\n  edit, the URL changes to \"/articles/1/edit\", even though you are still\n  within the same LiveView. Similarly, you may also want to show a \"New\"\n  button, which opens up the modal to create new entries, and you want\n  this to be reflected in the URL as \"/articles/new\".\n\n  In order to make it easier to recognize the current \"action\" your\n  LiveView is on, you can pass the action option when defining LiveViews\n  too:\n\n      live \"/articles\", ArticleLive.Index, :index\n      live \"/articles/new\", ArticleLive.Index, :new\n      live \"/articles/:id/edit\", ArticleLive.Index, :edit\n\n  The current action will always be available inside the LiveView as\n  the `@live_action` assign, that can be used to render a LiveComponent:\n\n  ```heex\n  <.live_component :if={@live_action == :new} module={MyAppWeb.ArticleLive.FormComponent} id=\"form\" />\n  ```\n\n  Or can be used to show or hide parts of the template:\n\n  ```heex\n  {if @live_action == :edit, do: render(\"form.html\", user: @user)}\n  ```\n\n  Note that `@live_action` will be `nil` if no action is given on the route definition.\n\n  ## Options\n\n    * `:container` - an optional tuple for the HTML tag and DOM attributes to\n      be used for the LiveView container. For example: `{:li, style: \"color: blue;\"}`.\n      See `Phoenix.Component.live_render/3` for more information and examples.\n\n    * `:as` - optionally configures the named helper. Defaults to `:live` when\n      using a LiveView without actions or defaults to the LiveView name when using\n      actions.\n\n    * `:metadata` - a map to optional feed metadata used on telemetry events and route info,\n      for example: `%{route_name: :foo, access: :user}`. This data can be retrieved by\n      calling `Phoenix.Router.route_info/4` with the `uri` from the `handle_params`\n      callback. This can be used to customize a LiveView which may be invoked from\n      different routes.\n\n    * `:private` - an optional map of private data to put in the *plug connection*,\n      for example: `%{route_name: :foo, access: :user}`. The data will be available\n      inside `conn.private` in plug functions.\n\n  ## Examples\n\n      defmodule MyApp.Router\n        use Phoenix.Router\n        import Phoenix.LiveView.Router\n\n        scope \"/\", MyApp do\n          pipe_through [:browser]\n\n          live \"/thermostat\", ThermostatLive\n          live \"/clock\", ClockLive\n          live \"/dashboard\", DashboardLive, container: {:main, class: \"row\"}\n        end\n      end\n\n      iex> MyApp.Router.Helpers.live_path(MyApp.Endpoint, MyApp.ThermostatLive)\n      \"/thermostat\"\n\n  \"\"\"\n  defmacro live(path, live_view, action \\\\ nil, opts \\\\ []) do\n    live_view = Macro.expand_literals(live_view, %{__CALLER__ | function: {:live, 4}})\n    action = Macro.expand_literals(action, %{__CALLER__ | function: {:live, 4}})\n    opts = Macro.expand_literals(opts, %{__CALLER__ | function: {:live, 4}})\n\n    quote bind_quoted: binding() do\n      {action, router_options} =\n        Phoenix.LiveView.Router.__live__(__MODULE__, live_view, action, opts)\n\n      Phoenix.Router.get(path, Phoenix.LiveView.Plug, action, router_options)\n    end\n  end\n\n  @doc \"\"\"\n  Defines a live session for live redirects within a group of live routes.\n\n  `live_session/3` allow routes defined with `live/4` to support\n  `navigate` redirects from the client with navigation purely over the existing\n  websocket connection. This allows live routes defined in the router to\n  mount a new root LiveView without additional HTTP requests to the server.\n  For backwards compatibility reasons, all live routes defined outside\n  of any live session are considered part of a single unnamed live session.\n\n  ## Security Considerations\n\n  In a regular web application, we perform authentication and authorization\n  checks on every request. Given LiveViews start as a regular HTTP request,\n  they share the authentication logic with regular requests through plugs.\n  Once the user is authenticated, we typically validate the sessions on\n  the `mount` callback. Authorization rules generally happen on `mount`\n  (for instance, is the user allowed to see this page?) and also on\n  `handle_event` (is the user allowed to delete this item?). Performing\n  authorization on mount is important because `navigate`s *do not go\n  through the plug pipeline*.\n\n  `live_session` can be used to draw boundaries between groups of LiveViews.\n  Redirecting between `live_session`s will always force a full page reload\n  and establish a brand new LiveView connection. This is useful when LiveViews\n  require different authentication strategies or simply when they use different\n  root layouts (as the root layout is not updated between live redirects).\n\n  Please [read our guide on the security model](security-model.md) for a\n  detailed description and general tips on authentication, authorization,\n  and more.\n\n  > #### `live_session` and `forward` {: .warning}\n  >\n  > `live_session` does not currently work with `forward`. LiveView expects\n  > your `live` routes to always be directly defined within the main router\n  > of your application.\n\n  > #### `live_session` and `scope` {: .warning}\n  >\n  > Aliases set with `Phoenix.Router.scope/2` are not expanded in `live_session` arguments.\n  > You must use the full module name instead.\n\n  ## Options\n\n    * `:session` - An optional extra session map or MFA tuple to be merged with\n      the LiveView session. For example, `%{\"admin\" => true}` or `{MyMod, :session, []}`.\n      For MFA, the function is invoked and the `Plug.Conn` struct is prepended\n      to the arguments list.\n\n    * `:root_layout` - An optional root layout tuple for the initial HTTP render to\n      override any existing root layout set in the router.\n\n    * `:on_mount` - An optional list of hooks to attach to the mount lifecycle _of\n      each LiveView in the session_. See `Phoenix.LiveView.on_mount/1`. Passing a\n      single value is also accepted.\n\n    * `:layout` - An optional layout the LiveView will be rendered in. Setting\n      this option overrides the layout via `use Phoenix.LiveView`. This option\n      may be overridden inside a LiveView by returning `{:ok, socket, layout: ...}`\n      from the mount callback\n\n  ## Examples\n\n      scope \"/\", MyAppWeb do\n        pipe_through :browser\n\n        live_session :default do\n          live \"/feed\", FeedLive, :index\n          live \"/status\", StatusLive, :index\n          live \"/status/:id\", StatusLive, :show\n        end\n\n        live_session :admin, on_mount: MyAppWeb.AdminLiveAuth do\n          live \"/admin\", AdminDashboardLive, :index\n          live \"/admin/posts\", AdminPostLive, :index\n        end\n      end\n\n  In the example above, we have two live sessions. Live navigation between live views\n  in the different sessions is not possible and will always require a full page reload.\n  This is important in the example above because the `:admin` live session has authentication\n  requirements, defined by `on_mount: MyAppWeb.AdminLiveAuth`, that the other LiveViews\n  do not have.\n\n  If you have both regular HTTP routes (via get, post, etc) and `live` routes, then\n  you need to perform the same authentication and authorization rules in both.\n  For example, if you were to add a `get \"/admin/health\"` route, then you must create\n  your own plug that performs the same authentication and authorization rules as\n  `MyAppWeb.AdminLiveAuth`, and then pipe through it:\n\n      scope \"/\" do\n        # Regular routes\n        pipe_through [MyAppWeb.AdminPlugAuth]\n        get \"/admin/health\", AdminHealthController, :index\n\n        # Live routes\n        live_session :admin, on_mount: MyAppWeb.AdminLiveAuth do\n          live \"/admin\", AdminDashboardLive, :index\n          live \"/admin/posts\", AdminPostLive, :index\n        end\n      end\n\n  \"\"\"\n  defmacro live_session(name, opts \\\\ [], do: block) do\n    opts = Macro.expand_literals(opts, %{__CALLER__ | function: {:live_session, 3}})\n\n    quote do\n      unquote(__MODULE__).__live_session__(__MODULE__, unquote(opts), unquote(name))\n      unquote(block)\n      Module.delete_attribute(__MODULE__, :phoenix_live_session_current)\n    end\n  end\n\n  @doc false\n  def __live_session__(module, opts, name) do\n    Module.register_attribute(module, :phoenix_live_sessions, accumulate: true)\n\n    if not is_atom(name) do\n      raise ArgumentError, \"\"\"\n      expected live_session name to be an atom, got: #{inspect(name)}\n      \"\"\"\n    end\n\n    extra = validate_live_session_opts(opts, module, name)\n\n    if nested = Module.get_attribute(module, :phoenix_live_session_current) do\n      raise \"\"\"\n      attempting to define live_session #{inspect(name)} inside #{inspect(nested.name)}.\n      live_session definitions cannot be nested.\n      \"\"\"\n    end\n\n    if name in Module.get_attribute(module, :phoenix_live_sessions) do\n      raise \"\"\"\n      attempting to redefine live_session #{inspect(name)}.\n      live_session routes must be declared in a single named block.\n      \"\"\"\n    end\n\n    current = %{name: name, extra: extra}\n    Module.put_attribute(module, :phoenix_live_session_current, current)\n\n    Module.put_attribute(module, :phoenix_live_sessions, name)\n  end\n\n  @live_session_opts [:layout, :on_mount, :root_layout, :session]\n  defp validate_live_session_opts(opts, module, _name) when is_list(opts) do\n    Enum.reduce(opts, %{}, fn\n      {:session, val}, acc when is_map(val) or (is_tuple(val) and tuple_size(val) == 3) ->\n        Map.put(acc, :session, val)\n\n      {:session, bad_session}, _acc ->\n        raise ArgumentError, \"\"\"\n        invalid live_session :session\n\n        expected a map with string keys or an MFA tuple, got #{inspect(bad_session)}\n        \"\"\"\n\n      {:root_layout, {mod, template}}, acc when is_atom(mod) and is_atom(template) ->\n        Map.put(acc, :root_layout, {mod, template})\n\n      {:root_layout, false}, acc ->\n        Map.put(acc, :root_layout, false)\n\n      {:root_layout, bad_layout}, _acc ->\n        raise ArgumentError, \"\"\"\n        invalid live_session :root_layout\n\n        expected a tuple with the view module and template atom name, got #{inspect(bad_layout)}\n        \"\"\"\n\n      {:layout, {mod, template}}, acc when is_atom(mod) and is_atom(template) ->\n        Map.put(acc, :layout, {mod, template})\n\n      {:layout, false}, acc ->\n        Map.put(acc, :layout, false)\n\n      {:layout, bad_layout}, _acc ->\n        raise ArgumentError, \"\"\"\n        invalid live_session :layout\n\n        expected a tuple with the view module and template string or atom name, got #{inspect(bad_layout)}\n        \"\"\"\n\n      {:on_mount, on_mount}, acc ->\n        hooks =\n          on_mount\n          |> List.wrap()\n          |> Enum.map(&Phoenix.LiveView.Lifecycle.validate_on_mount!(module, &1))\n          |> Phoenix.LiveView.Lifecycle.prepare_on_mount!()\n\n        Map.put(acc, :on_mount, hooks)\n\n      {key, _val}, _acc ->\n        raise ArgumentError, \"\"\"\n        unknown live_session option \"#{inspect(key)}\"\n\n        Supported options include: #{inspect(@live_session_opts)}\n        \"\"\"\n    end)\n  end\n\n  defp validate_live_session_opts(invalid, _module, name) do\n    raise ArgumentError, \"\"\"\n    expected second argument to live_session to be a list of options, got:\n\n        live_session #{inspect(name)}, #{inspect(invalid)}\n    \"\"\"\n  end\n\n  @doc \"\"\"\n  Fetches the LiveView and merges with the controller flash.\n\n  Replaces the default `:fetch_flash` plug used by `Phoenix.Router`.\n\n  ## Examples\n\n      defmodule MyAppWeb.Router do\n        use LiveGenWeb, :router\n        import Phoenix.LiveView.Router\n\n        pipeline :browser do\n          ...\n          plug :fetch_live_flash\n        end\n        ...\n      end\n  \"\"\"\n  def fetch_live_flash(%Plug.Conn{} = conn, _opts \\\\ []) do\n    case cookie_flash(conn) do\n      {conn, nil} ->\n        Phoenix.Controller.fetch_flash(conn, [])\n\n      {conn, flash} ->\n        conn\n        |> Phoenix.Controller.fetch_flash([])\n        |> Phoenix.Controller.merge_flash(flash)\n    end\n  end\n\n  @doc false\n  def __live__(router, live_view, action, opts)\n      when is_list(action) and is_list(opts) do\n    __live__(router, live_view, nil, Keyword.merge(action, opts))\n  end\n\n  def __live__(router, live_view, action, opts)\n      when is_atom(action) and is_list(opts) do\n    live_session =\n      Module.get_attribute(router, :phoenix_live_session_current) ||\n        %{name: :default, extra: %{}}\n\n    helpers = Module.get_attribute(router, :phoenix_helpers)\n\n    live_view = Phoenix.Router.scoped_alias(router, live_view)\n    {private, metadata, warn_on_verify, opts} = validate_live_opts!(opts)\n\n    opts =\n      opts\n      |> Keyword.put(:router, router)\n      |> Keyword.put(:action, action)\n\n    {as_helper, as_action} =\n      if helpers do\n        inferred_as(live_view, opts[:as], action)\n      else\n        {nil, action}\n      end\n\n    # TODO: Remove :log_module when we require Phoenix v1.8+\n    metadata =\n      metadata\n      |> Map.put(:phoenix_live_view, {live_view, action, opts, live_session})\n      |> Map.put(:mfa, {live_view, :__live__, 0})\n      |> Map.put(:log_module, live_view)\n\n    {as_action,\n     alias: false,\n     as: as_helper,\n     warn_on_verify: warn_on_verify,\n     private: Map.put(private, :phoenix_live_view, {live_view, opts, live_session}),\n     metadata: metadata}\n  end\n\n  defp validate_live_opts!(opts) do\n    {private, opts} = Keyword.pop(opts, :private, %{})\n    {metadata, opts} = Keyword.pop(opts, :metadata, %{})\n    {warn_on_verify, opts} = Keyword.pop(opts, :warn_on_verify, false)\n\n    Enum.each(opts, fn\n      {:container, {tag, attrs}} when is_atom(tag) and is_list(attrs) ->\n        :ok\n\n      {:container, val} ->\n        raise ArgumentError, \"\"\"\n        expected live :container to be a tuple matching {atom, attrs :: list}, got: #{inspect(val)}\n        \"\"\"\n\n      {:as, as} when is_atom(as) ->\n        :ok\n\n      {:as, bad_val} ->\n        raise ArgumentError, \"\"\"\n        expected live :as to be an atom, got: #{inspect(bad_val)}\n        \"\"\"\n\n      {key, %{} = meta} when key in [:metadata, :private] and is_map(meta) ->\n        :ok\n\n      {key, bad_val} when key in [:metadata, :private] ->\n        raise ArgumentError, \"\"\"\n        expected live :#{key} to be a map, got: #{inspect(bad_val)}\n        \"\"\"\n\n      {key, val} ->\n        raise ArgumentError, \"\"\"\n        unknown live option :#{key}.\n\n        Supported options include: :container, :as, :metadata, :private, :warn_on_verify.\n\n        Got: #{inspect([{key, val}])}\n        \"\"\"\n    end)\n\n    {private, metadata, warn_on_verify, opts}\n  end\n\n  defp inferred_as(live_view, as, nil), do: {as || :live, live_view}\n\n  defp inferred_as(live_view, nil, action) do\n    live_view\n    |> Module.split()\n    |> Enum.drop_while(&(not String.ends_with?(&1, \"Live\")))\n    |> Enum.map(&(&1 |> String.replace_suffix(\"Live\", \"\") |> Macro.underscore()))\n    |> Enum.reject(&(&1 == \"\"))\n    |> Enum.join(\"_\")\n    |> case do\n      \"\" ->\n        raise ArgumentError,\n              \"could not infer :as option because a live action was given and the LiveView \" <>\n                \"does not have a \\\"Live\\\" suffix. Please pass :as explicitly or make sure your \" <>\n                \"LiveView is named like \\\"FooLive\\\" or \\\"FooLive.Index\\\"\"\n\n      as ->\n        {String.to_atom(as), action}\n    end\n  end\n\n  defp inferred_as(_live_view, as, action), do: {as, action}\n\n  defp cookie_flash(%Plug.Conn{cookies: %{@cookie_key => token}} = conn) do\n    endpoint = Phoenix.Controller.endpoint_module(conn)\n\n    flash =\n      case Phoenix.LiveView.Utils.verify_flash(endpoint, token) do\n        %{} = flash when flash != %{} -> flash\n        %{} -> nil\n      end\n\n    {Plug.Conn.delete_resp_cookie(conn, @cookie_key), flash}\n  end\n\n  defp cookie_flash(%Plug.Conn{} = conn), do: {conn, nil}\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/session.ex",
    "content": "defmodule Phoenix.LiveView.Session do\n  @moduledoc false\n  alias Phoenix.LiveView.{Session, Route, Static}\n\n  defstruct id: nil,\n            view: nil,\n            root_view: nil,\n            parent_pid: nil,\n            root_pid: nil,\n            session: %{},\n            redirected?: false,\n            router: nil,\n            flash: nil,\n            live_session_name: nil,\n            assign_new: []\n\n  def main?(%Session{} = session), do: session.router != nil and session.parent_pid == nil\n\n  def authorize_root_redirect(%Session{} = session, %Route{} = route) do\n    %Session{live_session_name: session_name} = session\n\n    case route.live_session do\n      %{name: ^session_name} ->\n        {:ok, replace_root(session, route.view, self())}\n\n      %{} ->\n        :error\n    end\n  end\n\n  defp replace_root(%Session{} = session, new_root_view, root_pid) when is_pid(root_pid) do\n    %{\n      session\n      | view: new_root_view,\n        root_view: new_root_view,\n        root_pid: root_pid,\n        assign_new: [],\n        redirected?: true\n    }\n  end\n\n  @doc \"\"\"\n  Verifies the session token.\n\n  Returns the decoded map of session data or an error.\n\n  ## Examples\n\n      iex> verify_session(AppWeb.Endpoint, \"topic\", encoded_token, static_token)\n      {:ok, %Session{} = decoded_session}\n\n      iex> verify_session(AppWeb.Endpoint, \"topic\", \"bad token\", \"bac static\")\n      {:error, :invalid}\n\n      iex> verify_session(AppWeb.Endpoint, \"topic\", \"expired\", \"expired static\")\n      {:error, :expired}\n  \"\"\"\n  def verify_session(endpoint, topic, session_token, static_token) do\n    with {:ok, %{id: id} = session} <- Static.verify_token(endpoint, session_token),\n         :ok <- verify_topic(topic, id),\n         {:ok, static} <- verify_static_token(endpoint, id, static_token) do\n      merged_session = Map.merge(session, static)\n      live_session_name = merged_session[:live_session_name]\n\n      session = %Session{\n        id: id,\n        view: merged_session.view,\n        root_view: merged_session.root_view,\n        parent_pid: merged_session.parent_pid,\n        root_pid: merged_session.root_pid,\n        session: merged_session.session,\n        assign_new: merged_session.assign_new,\n        live_session_name: live_session_name,\n        # optional keys\n        router: merged_session[:router],\n        flash: merged_session[:flash]\n      }\n\n      {:ok, session}\n    end\n  end\n\n  defp verify_topic(\"lv:\" <> session_id, session_id), do: :ok\n  defp verify_topic(_topic, _session_id), do: {:error, :invalid}\n\n  defp verify_static_token(_endpoint, _id, nil), do: {:ok, %{assign_new: []}}\n\n  defp verify_static_token(endpoint, id, token) do\n    case Static.verify_token(endpoint, token) do\n      {:ok, %{id: ^id}} = ok -> ok\n      {:ok, _} -> {:error, :invalid}\n      {:error, _} = error -> error\n    end\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/socket.ex",
    "content": "defmodule Phoenix.LiveView.Socket.AssignsNotInSocket do\n  @moduledoc false\n\n  defimpl Inspect do\n    def inspect(_, _) do\n      \"#Phoenix.LiveView.Socket.AssignsNotInSocket<>\"\n    end\n  end\n\n  defstruct [:__assigns__]\n  @type t :: %__MODULE__{}\nend\n\ndefmodule Phoenix.LiveView.Socket do\n  @moduledoc \"\"\"\n  The LiveView socket for Phoenix Endpoints.\n\n  This is typically mounted directly in your endpoint.\n\n      socket \"/live\", Phoenix.LiveView.Socket,\n        websocket: [connect_info: [session: @session_options]]\n\n  To share an underlying transport connection between regular\n  Phoenix channels and LiveView processes, `use Phoenix.LiveView.Socket`\n  from your own `MyAppWeb.UserSocket` module.\n\n  Next, declare your `channel` definitions and optional `connect/3`, and\n  `id/1` callbacks to handle your channel specific needs, then mount\n  your own socket in your endpoint:\n\n      socket \"/live\", MyAppWeb.UserSocket,\n        websocket: [connect_info: [session: @session_options]]\n\n  If you require session options to be set at runtime, you can use\n  an MFA tuple. The function it designates must return a keyword list.\n\n      socket \"/live\", MyAppWeb.UserSocket,\n        websocket: [connect_info: [session: {__MODULE__, :runtime_opts, []}]]\n\n      # ...\n\n      def runtime_opts() do\n        Keyword.put(@session_options, :domain, host())\n      end\n  \"\"\"\n  use Phoenix.Socket\n\n  @derive {Inspect,\n           only: [\n             :id,\n             :endpoint,\n             :router,\n             :view,\n             :parent_pid,\n             :root_pid,\n             :assigns,\n             :transport_pid,\n             :sticky?\n           ]}\n\n  defstruct id: nil,\n            endpoint: nil,\n            view: nil,\n            parent_pid: nil,\n            root_pid: nil,\n            router: nil,\n            assigns: %{__changed__: %{}},\n            private: %{live_temp: %{}},\n            redirected: nil,\n            host_uri: nil,\n            transport_pid: nil,\n            sticky?: false\n\n  @typedoc \"Struct returned when `assigns` is not in the socket.\"\n  @opaque assigns_not_in_socket :: Phoenix.LiveView.Socket.AssignsNotInSocket.t()\n\n  @typedoc \"The data in a LiveView as stored in the socket.\"\n  @type assigns :: map | assigns_not_in_socket()\n\n  @type t :: %__MODULE__{\n          id: binary(),\n          endpoint: module(),\n          view: module(),\n          parent_pid: nil | pid(),\n          root_pid: pid(),\n          router: module(),\n          assigns: assigns,\n          private: map(),\n          redirected: nil | tuple(),\n          host_uri: URI.t() | :not_mounted_at_router,\n          transport_pid: pid() | nil\n        }\n\n  channel \"lvu:*\", Phoenix.LiveView.UploadChannel\n  channel \"lv:*\", Phoenix.LiveView.Channel\n\n  @impl Phoenix.Socket\n  def connect(_params, %Phoenix.Socket{} = socket, connect_info) do\n    {:ok, put_in(socket.private[:connect_info], connect_info)}\n  end\n\n  @impl Phoenix.Socket\n  def id(socket), do: socket.private.connect_info[:session][\"live_socket_id\"]\n\n  defmacro __using__(_opts) do\n    quote do\n      use Phoenix.Socket\n\n      channel \"lvu:*\", Phoenix.LiveView.UploadChannel\n      channel \"lv:*\", Phoenix.LiveView.Channel\n\n      def connect(params, socket, info), do: {:ok, socket}\n      defdelegate id(socket), to: unquote(__MODULE__)\n\n      defoverridable connect: 3, id: 1\n\n      @before_compile unquote(__MODULE__)\n    end\n  end\n\n  defmacro __before_compile__(_env) do\n    quote do\n      defoverridable connect: 3, id: 1\n\n      def connect(params, %Phoenix.Socket{} = socket, connect_info) do\n        with {:ok, %Phoenix.Socket{} = new_socket} <- super(params, socket, connect_info) do\n          Phoenix.LiveView.Socket.connect(params, new_socket, connect_info)\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/static.ex",
    "content": "defmodule Phoenix.LiveView.ReloadError do\n  defexception [:message, :plug_status]\nend\n\ndefmodule Phoenix.LiveView.Static do\n  # Holds the logic for static rendering.\n  @moduledoc false\n\n  alias Phoenix.LiveView.{Socket, Utils, Diff, Route, Lifecycle}\n\n  # Token version. Should be changed whenever new data is stored.\n  @token_vsn 6\n  @phoenix_reload_status \"__phoenix_reload_status__\"\n\n  def token_vsn, do: @token_vsn\n\n  # Max session age in seconds. Equivalent to 2 weeks.\n  @max_session_age 1_209_600\n\n  @doc \"\"\"\n  Acts as a view via put_view to maintain the\n  controller render + instrumentation stack.\n  \"\"\"\n  def render(_, %{content: content}) do\n    content\n  end\n\n  @doc \"\"\"\n  Verifies a LiveView token.\n  \"\"\"\n  def verify_token(endpoint, token) do\n    case Phoenix.Token.verify(endpoint, Utils.salt!(endpoint), token, max_age: @max_session_age) do\n      {:ok, {@token_vsn, term}} -> {:ok, term}\n      {:ok, _} -> {:error, :outdated}\n      {:error, :missing} -> {:error, :invalid}\n      {:error, reason} when reason in [:expired, :invalid] -> {:error, reason}\n    end\n  end\n\n  defp live_session(%Plug.Conn{} = conn) do\n    case conn.private[:phoenix_live_view] do\n      {_view, _opts, %{name: _name, extra: _extra} = lv_session} -> lv_session\n      nil -> nil\n    end\n  end\n\n  defp load_session(conn_or_socket_session, opts) do\n    user_session = Keyword.get(opts, :session, %{})\n    validate_session(user_session)\n    {user_session, Map.merge(conn_or_socket_session, user_session)}\n  end\n\n  defp validate_session(session) do\n    if is_map(session) and Enum.all?(session, fn {k, _} -> is_binary(k) end) do\n      :ok\n    else\n      raise ArgumentError,\n            \"LiveView :session must be a map with string keys, got: #{inspect(session)}\"\n    end\n  end\n\n  defp maybe_get_session(conn) do\n    Plug.Conn.get_session(conn)\n  rescue\n    _ -> %{}\n  end\n\n  defp maybe_put_live_layout(private, %{extra: %{layout: layout}}) do\n    Map.put(private, :live_layout, layout)\n  end\n\n  defp maybe_put_live_layout(private, _live_session) do\n    private\n  end\n\n  @doc \"\"\"\n  Renders a live view without spawning a LiveView server.\n\n    * `conn` - the Plug.Conn struct form the HTTP request\n    * `view` - the LiveView module\n\n  ## Options\n\n    * `:router` - the router the live view was built at\n    * `:action` - the router action\n    * `:session` - the required map of session data\n    * `:container` - the optional tuple for the HTML tag and DOM attributes\n  \"\"\"\n  def render(%Plug.Conn{} = conn, view, opts) do\n    endpoint = Phoenix.Controller.endpoint_module(conn)\n\n    case conn.req_cookies do\n      %Plug.Conn.Unfetched{} ->\n        do_render(conn, endpoint, view, opts)\n\n      %{@phoenix_reload_status => status_token} ->\n        conn = Plug.Conn.delete_resp_cookie(conn, @phoenix_reload_status)\n\n        {status, exception, errored_view, stack} =\n          case verify_token(endpoint, status_token) do\n            {:ok, %{status: status, exception: exception, view: errored_view, stack: stack}}\n            when is_integer(status) ->\n              {status, exception, errored_view, stack}\n\n            {:error, _reason} ->\n              {500, nil, nil, []}\n          end\n\n        message = \"\"\"\n        #{errored_view} raised #{exception} during connected mount sending a #{status} response\n        \"\"\"\n\n        raise Plug.Conn.WrapperError,\n          conn: conn,\n          kind: :error,\n          reason: %Phoenix.LiveView.ReloadError{message: message, plug_status: status},\n          stack: stack\n\n      %{} ->\n        do_render(conn, endpoint, view, opts)\n    end\n  end\n\n  defp do_render(%Plug.Conn{} = conn, endpoint, view, opts) do\n    conn_session = maybe_get_session(conn)\n    {to_sign_session, mount_session} = load_session(conn_session, opts)\n    live_session = live_session(conn)\n    config = load_live!(view, :view)\n    lifecycle = lifecycle(config, live_session)\n    {tag, extended_attrs} = container(config, opts)\n    router = Keyword.get(opts, :router)\n    action = Keyword.get(opts, :action)\n    flash = Map.get(conn.assigns, :flash) || Map.get(conn.private, :phoenix_flash, %{})\n    request_url = Plug.Conn.request_url(conn)\n    host_uri = URI.parse(request_url)\n\n    socket =\n      Utils.configure_socket(\n        %Socket{endpoint: endpoint, view: view, router: router},\n        %{\n          assign_new: {conn.assigns, []},\n          connect_params: %{},\n          connect_info: conn,\n          conn_session: conn_session,\n          lifecycle: lifecycle,\n          root_view: view,\n          live_temp: %{}\n        }\n        |> maybe_put_live_layout(live_session),\n        action,\n        flash,\n        host_uri\n      )\n\n    case call_mount_and_handle_params!(socket, view, mount_session, conn.params, request_url) do\n      {:ok, socket} ->\n        data_attrs = [\n          phx_session: sign_root_session(socket, router, view, to_sign_session, live_session),\n          phx_static: sign_static_token(socket)\n        ]\n\n        data_attrs = if(router, do: [phx_main: true], else: []) ++ data_attrs\n\n        attrs = [\n          {:id, socket.id},\n          {:data, data_attrs}\n          | extended_attrs\n        ]\n\n        try do\n          {:ok, to_rendered_content_tag(socket, tag, view, attrs), socket.assigns}\n        catch\n          :throw, {:phoenix, :child_redirect, redirected, flash} ->\n            {:stop, Utils.replace_flash(%{socket | redirected: redirected}, flash)}\n        end\n\n      {:stop, socket} ->\n        {:stop, socket}\n    end\n  end\n\n  @doc \"\"\"\n  Renders a nested live view without spawning a server.\n\n    * `parent` - the parent `%Phoenix.LiveView.Socket{}`\n    * `view` - the child LiveView module\n\n  Accepts the same options as `render/3`.\n  \"\"\"\n  def nested_render(\n        %Socket{endpoint: endpoint, transport_pid: transport_pid} = parent,\n        view,\n        opts\n      ) do\n    config = load_live!(view, :view)\n    container = container(config, opts)\n    sticky? = Keyword.get(opts, :sticky, false)\n\n    child_id =\n      opts[:id] ||\n        raise ArgumentError,\n              \"an :id is required when rendering child LiveView. \" <>\n                \"The :id must uniquely identify the child.\"\n\n    socket =\n      Utils.configure_socket(\n        %Socket{\n          id: to_string(child_id),\n          view: view,\n          endpoint: endpoint,\n          root_pid: if(sticky?, do: nil, else: parent.root_pid),\n          parent_pid: if(sticky?, do: nil, else: self()),\n          sticky?: sticky?,\n          router: parent.router\n        },\n        %{\n          assign_new: {parent.assigns.__assigns__, []},\n          lifecycle: config.lifecycle,\n          live_layout: false,\n          root_view: if(sticky?, do: view, else: parent.private.root_view),\n          live_temp: %{}\n        },\n        nil,\n        %{},\n        parent.host_uri\n      )\n\n    if transport_pid do\n      connected_nested_render(parent, socket, view, container, opts, sticky?)\n    else\n      disconnected_nested_render(parent, socket, view, container, opts, sticky?)\n    end\n  end\n\n  defp disconnected_nested_render(parent, socket, view, container, opts, sticky?) do\n    conn_session = parent.private.conn_session\n    {to_sign_session, mount_session} = load_session(conn_session, opts)\n    {tag, extended_attrs} = container\n\n    socket = put_in(socket.private[:conn_session], conn_session)\n\n    socket =\n      Utils.maybe_call_live_view_mount!(socket, view, :not_mounted_at_router, mount_session)\n\n    session_token =\n      if sticky?, do: sign_nested_session(parent, socket, view, to_sign_session, sticky?)\n\n    if redir = socket.redirected do\n      throw({:phoenix, :child_redirect, redir, Utils.get_flash(socket)})\n    end\n\n    if Lifecycle.stage_info(socket, view, :handle_params, 3).any? do\n      raise ArgumentError, \"handle_params/3 is not allowed on child LiveViews, only at the root\"\n    end\n\n    attrs = [\n      {:id, socket.id},\n      {:data,\n       [\n         phx_session: session_token || \"\",\n         phx_static: sign_static_token(socket)\n       ] ++ if(sticky?, do: [phx_sticky: true], else: [phx_parent_id: parent.id])}\n      | extended_attrs\n    ]\n\n    to_rendered_content_tag(socket, tag, view, attrs)\n  end\n\n  defp connected_nested_render(parent, socket, view, container, opts, sticky?) do\n    {to_sign_session, _} = load_session(%{}, opts)\n    {tag, extended_attrs} = container\n    session_token = sign_nested_session(parent, socket, view, to_sign_session, sticky?)\n\n    attrs = [\n      {:id, socket.id},\n      {:data,\n       [\n         phx_session: session_token,\n         phx_static: \"\"\n       ] ++ if(sticky?, do: [phx_sticky: true], else: [phx_parent_id: parent.id])}\n      | extended_attrs\n    ]\n\n    content_tag(tag, attrs, \"\")\n  end\n\n  defp to_rendered_content_tag(socket, tag, view, attrs) do\n    rendered = Phoenix.LiveView.Renderer.to_rendered(socket, view)\n\n    {diff, _, _} =\n      Diff.render(socket, rendered, Diff.new_fingerprints(), Diff.new_components())\n\n    content_tag(tag, attrs, Diff.to_iodata(diff))\n  end\n\n  defp content_tag(tag, attrs, content) do\n    tag = to_string(tag)\n    {:safe, attrs} = Phoenix.HTML.attributes_escape(attrs)\n    {:safe, [?<, tag, attrs, ?>, content, ?<, ?/, tag, ?>]}\n  end\n\n  defp load_live!(view_or_component, kind) do\n    case view_or_component.__live__() do\n      %{kind: ^kind} = config ->\n        config\n\n      %{kind: other} ->\n        raise \"expected #{inspect(view_or_component)} to be a #{kind}, but it is a #{other}\"\n    end\n  end\n\n  defp lifecycle(%{lifecycle: lifecycle}, %{extra: %{on_mount: on_mount}}) do\n    %{lifecycle | mount: on_mount ++ lifecycle.mount}\n  end\n\n  defp lifecycle(%{lifecycle: lifecycle}, _) do\n    lifecycle\n  end\n\n  defp call_mount_and_handle_params!(socket, view, session, params, uri) do\n    mount_params = if socket.router, do: params, else: :not_mounted_at_router\n\n    socket\n    |> Utils.maybe_call_live_view_mount!(view, mount_params, session, uri)\n    |> mount_handle_params(view, params, uri)\n    |> case do\n      {:noreply, %Socket{redirected: {:live, _, _}} = socket} ->\n        {:stop, socket}\n\n      {:noreply, %Socket{redirected: {:redirect, _opts}} = new_socket} ->\n        {:stop, new_socket}\n\n      {:noreply, %Socket{redirected: nil} = new_socket} ->\n        {:ok, new_socket}\n    end\n  end\n\n  defp mount_handle_params(%Socket{redirected: mount_redir} = socket, view, params, uri) do\n    lifecycle = Lifecycle.stage_info(socket, view, :handle_params, 3)\n\n    cond do\n      mount_redir ->\n        {:noreply, socket}\n\n      not lifecycle.any? ->\n        {:noreply, socket}\n\n      is_nil(socket.router) ->\n        # Let the callback fail for the usual reasons\n        Route.live_link_info!(socket, view, uri)\n\n      true ->\n        Utils.call_handle_params!(socket, view, lifecycle.exported?, params, uri)\n    end\n  end\n\n  defp sign_root_session(%Socket{} = socket, router, view, session, live_session) do\n    live_session_name =\n      case live_session do\n        %{name: name} -> name\n        nil -> nil\n      end\n\n    # IMPORTANT: If you change the second argument, @token_vsn has to be bumped.\n    sign_token(socket.endpoint, %{\n      id: socket.id,\n      view: view,\n      root_view: view,\n      router: router,\n      live_session_name: live_session_name,\n      parent_pid: nil,\n      root_pid: nil,\n      session: session\n    })\n  end\n\n  defp sign_nested_session(%Socket{} = parent, %Socket{} = child, view, session, sticky?) do\n    # IMPORTANT: If you change the second argument, @token_vsn has to be bumped.\n    sign_token(parent.endpoint, %{\n      id: child.id,\n      view: view,\n      root_view: if(sticky?, do: view, else: parent.private.root_view),\n      router: parent.router,\n      parent_pid: if(sticky?, do: nil, else: self()),\n      root_pid: if(sticky?, do: nil, else: parent.root_pid),\n      session: session\n    })\n  end\n\n  # The static token is computed only on disconnected render and it keeps\n  # the information that is only available during disconnected renders,\n  # such as assign_new.\n  defp sign_static_token(%Socket{id: id, endpoint: endpoint} = socket) do\n    # IMPORTANT: If you change the second argument, @token_vsn has to be bumped.\n    sign_token(endpoint, %{\n      id: id,\n      flash: socket.assigns.flash,\n      assign_new: assign_new_keys(socket)\n    })\n  end\n\n  @doc \"\"\"\n  Signs a LiveView token.\n  \"\"\"\n  def sign_token(endpoint, data) do\n    Phoenix.Token.sign(endpoint, Utils.salt!(endpoint), {@token_vsn, data})\n  end\n\n  defp container(%{container: {tag, attrs}}, opts) do\n    case opts[:container] do\n      {tag, extra} -> {tag, Keyword.merge(attrs, extra)}\n      nil -> {tag, attrs}\n    end\n  end\n\n  defp assign_new_keys(socket) do\n    {_, keys} = socket.private.assign_new\n    keys\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/tag_engine/compiler.ex",
    "content": "defmodule Phoenix.LiveView.TagEngine.Compiler do\n  @moduledoc false\n\n  alias Phoenix.LiveView.TagEngine.Parser\n  alias Phoenix.LiveView.TagEngine.Tokenizer.ParseError\n\n  @doc \"\"\"\n  Compiles the node tag tree into Elixir code.\n\n  Under the hood, this uses the `Phoenix.LiveView.Engine`\n  to convert template parts into static and dynamic parts\n  and perform change tracking. See the Engine documentation\n  for more details.\n\n  This function is responsible for converting the nodes into\n  text and expression parts and properly invoking the engine\n  with the correct code for features like components and slots.\n  \"\"\"\n  def compile(%Parser{nodes: nodes, directives: directives}, opts) do\n    {engine, opts} = Keyword.pop(opts, :engine, Phoenix.LiveView.Engine)\n    tag_handler = Keyword.fetch!(opts, :tag_handler)\n\n    state = %{\n      engine: engine,\n      file: Keyword.get(opts, :file, \"nofile\"),\n      indentation: Keyword.get(opts, :indentation, 0),\n      caller: Keyword.fetch!(opts, :caller),\n      source: Keyword.fetch!(opts, :source),\n      tag_handler: tag_handler,\n      root_tag_attribute: Application.get_env(:phoenix_live_view, :root_tag_attribute),\n      root_tag_attributes: Keyword.get_values(directives, :root_tag_attribute),\n      # The following keys are updated when traversing nodes\n      slots: [],\n      local_root?: true\n    }\n\n    # Live components require a single, static root tag.\n    # This is because they are patched independently on the client\n    # and morphdom requires a single DOM node as an entrypoint for patching.\n    # It needs to be static, because if it is not, we cannot guarantee\n    # that it might render multiple tags at runtime.\n    #\n    # Because the parser already resolves macro components and trims\n    # leading and trailing whitespace, the root check can be a simple\n    # pattern match.\n    root =\n      case nodes do\n        # We do not allow any special attribute (:for, :if),\n        # because these violate the static requirement.\n        [{:block, :tag, _name, _attrs, _children, meta, _close_meta}]\n        when is_map_key(meta, :special) and meta.special == [] ->\n          true\n\n        [{:self_close, :tag, _name, _attrs, meta}]\n        when is_map_key(meta, :special) and meta.special == [] ->\n          true\n\n        _ ->\n          false\n      end\n\n    {state, substate} = handle_node(nodes, engine.init(caller: opts[:caller]), state)\n\n    caller = state.caller\n    body_opts = [root: root]\n\n    body_opts =\n      if annotation = caller && has_tags?(nodes) && tag_handler.annotate_body(caller) do\n        [meta: [template_annotation: annotation]] ++ body_opts\n      else\n        body_opts\n      end\n\n    ast = state.engine.handle_body(substate, body_opts)\n\n    quote do\n      require Phoenix.LiveView.TagEngine\n      unquote(ast)\n    end\n  end\n\n  defp handle_node(nodes, substate, state) when is_list(nodes) do\n    Enum.reduce(nodes, {state, substate}, fn node, {state, substate} ->\n      handle_node(node, substate, state)\n    end)\n  end\n\n  defp handle_node({:text, \"\", _meta}, substate, state) do\n    {state, substate}\n  end\n\n  defp handle_node({:text, text, _meta}, substate, state) do\n    substate = state.engine.handle_text(substate, [], text)\n    {state, substate}\n  end\n\n  ## HEEx interpolation {...}\n  defp handle_node({:body_expr, expr, %{line: line, column: column}}, substate, state) do\n    ast = Code.string_to_quoted!(expr, line: line, column: column, file: state.file)\n    substate = state.engine.handle_expr(substate, \"=\", ast)\n    {state, substate}\n  end\n\n  ## EEx expression (<% ... %> or any modifier like <%= ... %>)\n  defp handle_node({:eex, expr, %{opt: opt, line: line, column: column}}, substate, state) do\n    ast = Code.string_to_quoted!(expr, line: line, column: column, file: state.file)\n    # opt is a charlist from the tokenizer, convert to string for the engine\n    marker = to_string(opt)\n    substate = state.engine.handle_expr(substate, marker, ast)\n    {state, substate}\n  end\n\n  ## eex_block (if/case/for/etc)\n  #\n  # Uses the same approach as EEx.Compiler: builds up the complete expression string\n  # with __EEX__(key) placeholders, parses it as Elixir code, then replaces the\n  # placeholders with the actual compiled content.\n  defp handle_node(\n         {:eex_block, expr, blocks, %{line: line, column: column, opt: opt}},\n         substate,\n         state\n       ) do\n    # EEx block structure: expr is \"case @status do\", blocks are [{children, clause_expr}, ...]\n    # For example: imagine this template\n    #\n    # ```heex\n    # <%= case @status do %>\n    #   <% :connecting -> %>\n    #     <.status status={@status} />\n    #   <% :loading -> %>\n    #     <.status status={@status} />\n    #   <% :connected -> %>\n    #     <.status status={@status} />\n    #   <% :loaded -> %>\n    #     <.live_component module={__MODULE__.Form} id=\"my-form\" name={@name} email={@email} />\n    # <% end %>\n    # ```\n    #\n    # This ends up as an eex_block like this:\n    #\n    # [\n    #   {:eex_block, \"case @status do\",\n    #   [\n    #     {[{:text, \"\\n      \", %{}}], \":connecting ->\"},\n    #     {[\n    #        {:text, \"\\n        \", %{}},\n    #        {:self_close, :local_component, \"status\", [...], %{}},\n    #        {:text, \"\\n      \", %{}}\n    #      ], \":loading ->\"},\n    #     {children, \":connected ->\"},\n    #     {children, \":loaded ->\"},\n    #     {children, \"end\"}\n    #   ], %{line: 1, opt: ~c\"=\", column: 1}}\n    # ]\n    #\n    # So we start with the beginning, then we get pairs of children we need to handle,\n    # followed by the next clause / end.\n    #\n    # Each clause is its own EEx nesting, so we call handle_begin, process the children\n    # and get the compiled AST for the clause.\n    #\n    # Now, since we also need to compile the text parts, we also build a string with\n    # placeholders that ends up looking like this:\n    #\n    # case @status do\n    #  :connecting -> __EEX__(0);\n    #\n    #  :loading -> __EEX__(1);\n    #\n    #  :connected -> __EEX__(2);\n    #\n    #  :loaded -> __EEX__(3);\n    #\n    #  end\n    #\n    # Afterwards, the placeholders are replaced with the compiled content.\n    #\n    {quoted, combined_expr, _current_line} =\n      Enum.reduce(blocks, {[], expr, line}, fn\n        {children, clause_expr, clause_meta}, {quoted, acc_expr, prev_line} ->\n          # Calculate newlines needed to reach this clause's line\n          clause_line = clause_meta.line\n          newlines = String.duplicate(\"\\n\", clause_line - prev_line)\n\n          if all_spaces?(children) do\n            # This handles the case where the start expression is immediately followed\n            # by a middle expression, since we don't want to generate\n            # case @status do __EEX__(0); :connecting -> __EEX__(1) ...\n            # (we nened to skip adding the first placeholder)\n            # and instead generate\n            # case @status do :connecting -> __EEX__(0); ...\n            {quoted, acc_expr <> newlines <> \" \" <> clause_expr, clause_line}\n          else\n            inner_substate = state.engine.handle_begin(substate)\n            {_state, inner_substate} = handle_node(children, inner_substate, state)\n            clause_ast = state.engine.handle_end(inner_substate)\n\n            key = length(quoted)\n            placeholder = \"__EEX__(#{key});\"\n            quoted = [{key, clause_ast} | quoted]\n            acc_expr = acc_expr <> \" \" <> placeholder <> newlines <> \" \" <> clause_expr\n            {quoted, acc_expr, clause_line}\n          end\n      end)\n\n    # Calculate column offset: column points to '<', add length of '<%' + marker\n    # opt is a charlist like ~c\"=\" for <%= or ~c\"\" for <%\n    expr_column = column + 2 + length(opt)\n\n    # Parse the complete expression with placeholders\n    block_ast =\n      Code.string_to_quoted!(combined_expr,\n        line: line,\n        column: expr_column,\n        columns: true,\n        file: state.file\n      )\n\n    # Replace placeholders with actual content\n    final_ast = insert_quoted(block_ast, quoted)\n\n    # opt is a charlist from the tokenizer, convert to string for the engine\n    marker = to_string(opt)\n    substate = state.engine.handle_expr(substate, marker, final_ast)\n    {state, substate}\n  end\n\n  ## Self-closing tag (<div />)\n  defp handle_node({:self_close, :tag, name, attrs, meta}, substate, state) do\n    %{closing: closing} = meta\n    suffix = if closing == :void, do: \">\", else: \"></#{name}>\"\n    attrs = postprocess_attrs(attrs, state)\n    validate_phx_attrs!(attrs, meta, state)\n    validate_tag_attrs!(attrs, meta, state)\n\n    with_special_attrs(attrs, meta, substate, state, fn attrs, meta, substate, state ->\n      substate = handle_tag_and_attrs(name, attrs, suffix, to_location(meta), substate, state)\n      {state, substate}\n    end)\n  end\n\n  ## Self-closing slot (<:some_slot />)\n  defp handle_node({:self_close, :slot, slot_name, attrs, meta}, substate, state) do\n    slot_name = String.to_atom(slot_name)\n    attrs = postprocess_attrs(attrs, state)\n    %{line: line} = meta\n    {special, roots, attrs, attr_info} = split_component_attrs({\"slot\", slot_name}, attrs, state)\n    let = special[\":let\"]\n\n    with {_, let_meta} <- let do\n      message = \"cannot use :let on a slot without inner content\"\n      raise_syntax_error!(message, let_meta, state)\n    end\n\n    attrs = [__slot__: slot_name, inner_block: nil] ++ attrs\n    assigns = wrap_special_slot(special, merge_component_attrs(roots, attrs, line))\n\n    state = add_slot(state, slot_name, assigns, attr_info, meta, special)\n    {state, substate}\n  end\n\n  ## Self-closing local component (<.some_component />)\n  defp handle_node({:self_close, :local_component, name, attrs, meta}, substate, state) do\n    fun = String.to_atom(name)\n    %{line: line, column: column} = meta\n    attrs = postprocess_attrs(attrs, state)\n\n    {assigns, attr_info} =\n      build_self_close_component_assigns({\"local component\", fun}, attrs, line, state)\n\n    mod = actual_component_module(state.caller, fun)\n    store_component_call({mod, fun}, attr_info, [], line, state)\n    call_meta = [line: line, column: column]\n    call = {fun, call_meta, __MODULE__}\n\n    ast =\n      quote line: line do\n        Phoenix.LiveView.TagEngine.component(\n          &(unquote(call) / 1),\n          unquote(assigns),\n          {__MODULE__, __ENV__.function, __ENV__.file, unquote(line)}\n        )\n      end\n\n    with_special_attrs(attrs, meta, substate, state, fn _new_attrs, _new_meta, substate, state ->\n      substate = maybe_anno_caller(substate, call_meta, state.file, line, state)\n      substate = state.engine.handle_expr(substate, \"=\", ast)\n      {state, substate}\n    end)\n  end\n\n  ## Self-closing remote component (<MyModule.some_component />)\n  defp handle_node({:self_close, :remote_component, name, attrs, meta}, substate, state) do\n    attrs = postprocess_attrs(attrs, state)\n    {mod_ast, mod_size, fun} = decompose_remote_component_tag!(name, meta, state)\n    %{line: line, column: column} = meta\n\n    {assigns, attr_info} =\n      build_self_close_component_assigns({\"remote component\", name}, attrs, meta.line, state)\n\n    mod = expand_with_line(mod_ast, line, state.caller)\n    store_component_call({mod, fun}, attr_info, [], line, state)\n    call_meta = [line: line, column: column + mod_size]\n    call = {{:., call_meta, [mod_ast, fun]}, call_meta, []}\n\n    ast =\n      quote line: meta.line do\n        Phoenix.LiveView.TagEngine.component(\n          &(unquote(call) / 1),\n          unquote(assigns),\n          {__MODULE__, __ENV__.function, __ENV__.file, unquote(meta.line)}\n        )\n      end\n\n    with_special_attrs(attrs, meta, substate, state, fn _new_attrs, _new_meta, substate, state ->\n      substate = maybe_anno_caller(substate, call_meta, state.file, line, state)\n      substate = state.engine.handle_expr(substate, \"=\", ast)\n      {state, substate}\n    end)\n  end\n\n  ## Regular HTML tag with content (<div>...</div>)\n  defp handle_node({:block, :tag, name, attrs, children, meta, close_meta}, substate, state) do\n    attrs = postprocess_attrs(attrs, state)\n    validate_phx_attrs!(attrs, meta, state)\n    validate_tag_attrs!(attrs, meta, state)\n\n    with_special_attrs(attrs, meta, substate, state, fn attrs, meta, substate, state ->\n      substate = handle_tag_and_attrs(name, attrs, \">\", to_location(meta), substate, state)\n      {_child_state, substate} = handle_node(children, substate, %{state | local_root?: false})\n      substate = state.engine.handle_text(substate, [to_location(close_meta)], \"</#{name}>\")\n      {state, substate}\n    end)\n  end\n\n  ## Slot with content (<:slot>...</:slot>)\n  defp handle_node({:block, :slot, slot_name, attrs, children, meta, close_meta}, substate, state) do\n    slot_name = String.to_atom(slot_name)\n    attrs = postprocess_attrs(attrs, state)\n    %{line: line} = meta\n\n    {special, roots, attrs, attr_info} =\n      split_component_attrs({\"slot\", slot_name}, attrs, state)\n\n    # The parser verifies that slots are direct component children,\n    # so can ignore slots here, as they are always empty.\n    {clauses, _slots} =\n      build_component_clauses(\n        special[\":let\"],\n        slot_name,\n        children,\n        meta,\n        close_meta,\n        substate,\n        %{state | local_root?: true}\n      )\n\n    ast =\n      quote line: line do\n        Phoenix.LiveView.TagEngine.inner_block(unquote(slot_name), do: unquote(clauses))\n      end\n\n    attrs = [__slot__: slot_name, inner_block: ast] ++ attrs\n    assigns = wrap_special_slot(special, merge_component_attrs(roots, attrs, line))\n    inner = add_inner_block(attr_info, ast, meta)\n\n    state = add_slot(state, slot_name, assigns, inner, meta, special)\n    {state, substate}\n  end\n\n  ## Local component with content (<.some_component>...</.some_component>)\n  defp handle_node(\n         {:block, :local_component, name, attrs, children, meta, close_meta},\n         substate,\n         state\n       ) do\n    fun = String.to_atom(name)\n    %{line: line, column: column} = meta\n    attrs = postprocess_attrs(attrs, state)\n    mod = actual_component_module(state.caller, fun)\n    ref = {\"local component\", fun}\n\n    with_special_attrs(attrs, meta, substate, state, fn attrs, meta, substate, state ->\n      {assigns, attr_info, slot_info} =\n        build_component_assigns(ref, attrs, children, meta, close_meta, substate, %{\n          state\n          | local_root?: true\n        })\n\n      store_component_call({mod, fun}, attr_info, slot_info, line, state)\n      call_meta = [line: line, column: column]\n      call = {fun, call_meta, __MODULE__}\n\n      ast =\n        quote line: line do\n          Phoenix.LiveView.TagEngine.component(\n            &(unquote(call) / 1),\n            unquote(assigns),\n            {__MODULE__, __ENV__.function, __ENV__.file, unquote(line)}\n          )\n        end\n        |> tag_slots(slot_info)\n\n      substate = maybe_anno_caller(substate, call_meta, state.file, line, state)\n      substate = state.engine.handle_expr(substate, \"=\", ast)\n      {state, substate}\n    end)\n  end\n\n  ## Remote component with content (<MyModule.some_component>...</MyModule.some_component>)\n  defp handle_node(\n         {:block, :remote_component, name, attrs, children, meta, close_meta},\n         substate,\n         state\n       ) do\n    {mod_ast, mod_size, fun} = decompose_remote_component_tag!(name, meta, state)\n    %{line: line, column: column} = meta\n    attrs = postprocess_attrs(attrs, state)\n    mod = expand_with_line(mod_ast, line, state.caller)\n    ref = {\"remote component\", name}\n\n    with_special_attrs(attrs, meta, substate, state, fn attrs, meta, substate, state ->\n      # Process children in a new nesting\n      {assigns, attr_info, slot_info} =\n        build_component_assigns(ref, attrs, children, meta, close_meta, substate, %{\n          state\n          | local_root?: true\n        })\n\n      store_component_call({mod, fun}, attr_info, slot_info, line, state)\n      call_meta = [line: line, column: column + mod_size]\n      call = {{:., call_meta, [mod_ast, fun]}, call_meta, []}\n\n      ast =\n        quote line: line do\n          Phoenix.LiveView.TagEngine.component(\n            &(unquote(call) / 1),\n            unquote(assigns),\n            {__MODULE__, __ENV__.function, __ENV__.file, unquote(line)}\n          )\n        end\n        |> tag_slots(slot_info)\n\n      substate = maybe_anno_caller(substate, call_meta, state.file, line, state)\n      substate = state.engine.handle_expr(substate, \"=\", ast)\n      {state, substate}\n    end)\n  end\n\n  ## EEx block helpers\n\n  defp all_spaces?(children) do\n    Enum.all?(children, fn\n      {:eex_comment, _} -> true\n      {:text, text, _} -> String.trim_leading(text) == \"\"\n      _ -> false\n    end)\n  end\n\n  # Replace __EEX__(key) placeholders with actual compiled content\n  # This is taken from EEx.Compiler\n  defp insert_quoted({:__EEX__, _, [key]}, quoted) do\n    {^key, value} = List.keyfind(quoted, key, 0)\n    value\n  end\n\n  defp insert_quoted({left, meta, right}, quoted) do\n    {insert_quoted(left, quoted), meta, insert_quoted(right, quoted)}\n  end\n\n  defp insert_quoted({left, right}, quoted) do\n    {insert_quoted(left, quoted), insert_quoted(right, quoted)}\n  end\n\n  defp insert_quoted(list, quoted) when is_list(list) do\n    Enum.map(list, &insert_quoted(&1, quoted))\n  end\n\n  defp insert_quoted(other, _quoted) do\n    other\n  end\n\n  ## Tag attributes\n\n  defp handle_tag_and_attrs(name, attrs, suffix, meta, substate, state) do\n    text =\n      \"<#{name}\"\n      |> maybe_add_phx_loc(state, meta)\n      |> maybe_add_root_tag_attributes(state, meta)\n\n    substate = state.engine.handle_text(substate, meta, text)\n    substate = handle_tag_attrs(meta, attrs, substate, state)\n    state.engine.handle_text(substate, meta, suffix)\n  end\n\n  defp maybe_add_phx_loc(text, %{caller: caller}, meta) do\n    if debug_attributes?(caller) do\n      \"#{text} data-phx-loc=\\\"#{meta[:line]}\\\"\"\n    else\n      text\n    end\n  end\n\n  defp maybe_add_root_tag_attributes(text, %{local_root?: true} = state, _meta) do\n    case state do\n      %{root_tag_attribute: root_tag_attribute} when is_binary(root_tag_attribute) ->\n        attrs =\n          [{root_tag_attribute, true} | state.root_tag_attributes]\n          |> Phoenix.HTML.attributes_escape()\n          |> Phoenix.HTML.safe_to_string()\n\n        # Phoenix.HTML.attributes_escape/1 adds a leading space automatically\n        \"#{text}#{attrs}\"\n\n      %{root_tag_attribute: _} ->\n        text\n    end\n  end\n\n  defp maybe_add_root_tag_attributes(text, _state, _meta), do: text\n\n  defp assign?({:@, _, [_]}), do: true\n  defp assign?({{:., _, [lhs, _rhs]}, _, []}), do: assign?(lhs)\n  defp assign?(_), do: false\n\n  defp handle_tag_attrs(meta, attrs, substate, state) do\n    Enum.reduce(attrs, substate, fn\n      {:root, {:expr, _, _} = expr, _attr_meta}, substate ->\n        ast = parse_expr!(expr, state.file)\n\n        ast =\n          if assign?(ast) do\n            ast\n          else\n            expand_with_line(ast, meta[:line], state.caller)\n          end\n\n        # If we have a map of literal keys, we unpack it as a list\n        # to simplify the downstream check.\n        ast =\n          with {:%{}, _meta, pairs} <- ast,\n               true <- literal_keys?(pairs) do\n            pairs\n          else\n            _ -> ast\n          end\n\n        handle_tag_expr_attrs(meta, ast, substate, state)\n\n      {name, {:expr, _, _} = expr, _attr_meta}, substate ->\n        handle_tag_expr_attrs(meta, [{name, parse_expr!(expr, state.file)}], substate, state)\n\n      {name, {:string, value, %{delimiter: ?\"}}, _attr_meta}, substate ->\n        state.engine.handle_text(substate, meta, ~s( #{name}=\"#{value}\"))\n\n      {name, {:string, value, %{delimiter: ?'}}, _attr_meta}, substate ->\n        state.engine.handle_text(substate, meta, ~s( #{name}='#{value}'))\n\n      {name, nil, _attr_meta}, substate ->\n        state.engine.handle_text(substate, meta, \" #{name}\")\n    end)\n  end\n\n  defp handle_tag_expr_attrs(meta, ast, substate, state) do\n    # It is safe to List.wrap/1 because if we receive nil,\n    # it would become the interpolation of nil, which is an\n    # empty string anyway.\n    case state.tag_handler.handle_attributes(ast, meta) do\n      {:attributes, attrs} ->\n        Enum.reduce(attrs, substate, fn\n          {name, value}, substate ->\n            substate = state.engine.handle_text(substate, meta, ~s( #{name}=\"))\n\n            substate =\n              value\n              |> List.wrap()\n              |> Enum.reduce(substate, fn\n                binary, substate when is_binary(binary) ->\n                  state.engine.handle_text(substate, meta, binary)\n\n                expr, substate ->\n                  state.engine.handle_expr(substate, \"=\", expr)\n              end)\n\n            state.engine.handle_text(substate, meta, ~s(\"))\n\n          quoted, substate ->\n            state.engine.handle_expr(substate, \"=\", quoted)\n        end)\n\n      {:quoted, quoted} ->\n        state.engine.handle_expr(substate, \"=\", quoted)\n    end\n  end\n\n  defp parse_expr!({:expr, value, %{line: line, column: col}}, file) do\n    Code.string_to_quoted!(value, line: line, column: col, file: file)\n  end\n\n  defp literal_keys?([{key, _value} | rest]) when is_atom(key) or is_binary(key),\n    do: literal_keys?(rest)\n\n  defp literal_keys?([]), do: true\n  defp literal_keys?(_other), do: false\n\n  ## Component assign helpers\n\n  defp build_self_close_component_assigns(type_component, attrs, line, state) do\n    {special, roots, attrs, attr_info} = split_component_attrs(type_component, attrs, state)\n    raise_if_let!(special[\":let\"], state.file)\n    {merge_component_attrs(roots, attrs, line), attr_info}\n  end\n\n  defp build_component_assigns(\n         type_component,\n         attrs,\n         children,\n         tag_meta,\n         tag_close_meta,\n         substate,\n         state\n       ) do\n    %{line: line} = tag_meta\n    {special, roots, attrs, attr_info} = split_component_attrs(type_component, attrs, state)\n\n    {clauses, slots} =\n      build_component_clauses(\n        special[\":let\"],\n        :inner_block,\n        children,\n        tag_meta,\n        tag_close_meta,\n        substate,\n        state\n      )\n\n    inner_block =\n      quote line: line do\n        Phoenix.LiveView.TagEngine.inner_block(:inner_block, do: unquote(clauses))\n      end\n\n    inner_block_assigns =\n      quote line: line do\n        %{\n          __slot__: :inner_block,\n          inner_block: unquote(inner_block)\n        }\n      end\n\n    {slot_assigns, slot_info} = slots\n\n    slot_info = [\n      {:inner_block, [{tag_meta, add_inner_block({false, [], []}, inner_block, tag_meta)}]}\n      | slot_info\n    ]\n\n    attrs = attrs ++ [{:inner_block, [inner_block_assigns]} | slot_assigns]\n    {merge_component_attrs(roots, attrs, line), attr_info, slot_info}\n  end\n\n  defp split_component_attrs(type_component, attrs, state) do\n    {special, roots, attrs, locs} =\n      attrs\n      |> Enum.reverse()\n      |> Enum.reduce(\n        {%{}, [], [], []},\n        &split_component_attr(&1, &2, state, type_component)\n      )\n\n    {special, roots, attrs, {roots != [], attrs, locs}}\n  end\n\n  defp split_component_attr(\n         {:root, {:expr, value, %{line: line, column: col}}, _attr_meta},\n         {special, r, a, locs},\n         state,\n         _type_component\n       ) do\n    quoted_value = Code.string_to_quoted!(value, line: line, column: col, file: state.file)\n    quoted_value = quote line: line, do: Map.new(unquote(quoted_value))\n    {special, [quoted_value | r], a, locs}\n  end\n\n  @special_attrs ~w(:let :if :for :key)\n  defp split_component_attr(\n         {\":key\", _expr, attr_meta},\n         _,\n         state,\n         {\"slot\", slot_name}\n       ) do\n    message = \":key is not supported on slots: #{slot_name}\"\n    raise_syntax_error!(message, attr_meta, state)\n  end\n\n  defp split_component_attr(\n         {attr, {:expr, value, %{line: line, column: col} = meta}, attr_meta},\n         {special, r, a, locs},\n         state,\n         _type_component\n       )\n       when attr in @special_attrs do\n    case special do\n      %{^attr => {_, attr_meta}} ->\n        message = \"\"\"\n        cannot define multiple #{attr} attributes. \\\n        Another #{attr} has already been defined at line #{meta.line}\\\n        \"\"\"\n\n        raise_syntax_error!(message, attr_meta, state)\n\n      %{} ->\n        quoted_value = Code.string_to_quoted!(value, line: line, column: col, file: state.file)\n        validate_quoted_special_attr!(attr, quoted_value, attr_meta, state)\n        {Map.put(special, attr, {quoted_value, attr_meta}), r, a, locs}\n    end\n  end\n\n  defp split_component_attr({attr, _, meta}, _state, state, {type, component_or_slot})\n       when attr in @special_attrs do\n    message = \"#{attr} must be a pattern between {...} in #{type}: #{component_or_slot}\"\n    raise_syntax_error!(message, meta, state)\n  end\n\n  defp split_component_attr({\":\" <> _ = name, _, meta}, _state, state, {type, component_or_slot}) do\n    message = \"unsupported attribute #{inspect(name)} in #{type}: #{component_or_slot}\"\n    raise_syntax_error!(message, meta, state)\n  end\n\n  defp split_component_attr(\n         {name, {:expr, value, %{line: line, column: col}}, attr_meta},\n         {special, r, a, locs},\n         state,\n         _type_component\n       ) do\n    quoted_value = Code.string_to_quoted!(value, line: line, column: col, file: state.file)\n    {special, r, [{String.to_atom(name), quoted_value} | a], [line_column(attr_meta) | locs]}\n  end\n\n  defp split_component_attr(\n         {name, {:string, value, _meta}, attr_meta},\n         {special, r, a, locs},\n         _state,\n         _type_component\n       ) do\n    {special, r, [{String.to_atom(name), value} | a], [line_column(attr_meta) | locs]}\n  end\n\n  defp split_component_attr(\n         {name, nil, attr_meta},\n         {special, r, a, locs},\n         _state,\n         _type_component\n       ) do\n    {special, r, [{String.to_atom(name), true} | a], [line_column(attr_meta) | locs]}\n  end\n\n  defp line_column(%{line: line, column: column}), do: {line, column}\n\n  defp merge_component_attrs(roots, attrs, line) do\n    entries =\n      case {roots, attrs} do\n        {[], []} -> [{:%{}, [], []}]\n        {_, []} -> roots\n        {_, _} -> roots ++ [{:%{}, [], attrs}]\n      end\n\n    Enum.reduce(entries, fn expr, acc ->\n      quote line: line, do: Map.merge(unquote(acc), unquote(expr))\n    end)\n  end\n\n  defp decompose_remote_component_tag!(tag_name, tag_meta, state) do\n    case String.split(tag_name, \".\") |> Enum.reverse() do\n      [<<first, _::binary>> = fun_name | rest] when first in ?a..?z ->\n        size = byte_size(tag_name) - byte_size(fun_name) + 1\n        aliases = rest |> Enum.reverse() |> Enum.map(&String.to_atom/1)\n        fun = String.to_atom(fun_name)\n        %{line: line, column: column} = tag_meta\n        {{:__aliases__, [line: line, column: column], aliases}, size, fun}\n\n      _ ->\n        message = \"invalid tag <#{tag_meta.tag_name}>\"\n        raise_syntax_error!(message, tag_meta, state)\n    end\n  end\n\n  defp raise_if_let!(let, file) do\n    with {_pattern, %{line: line}} <- let do\n      message = \"cannot use :let on a component without inner content\"\n      raise CompileError, line: line, file: file, description: message\n    end\n  end\n\n  # Given the child nodes, this function builds the clause AST for a component.\n  # If a component is defined as <.my_component>, this looks like this:\n  #\n  # _ -> ast\n  #\n  # If a component is defined as as <.my_component :let={%{foo: foo, bar: bar}}, we get:\n  #\n  # %{foo: foo, bar: bar} -> ast\n  # other -> Phoenix.LiveView.TagEngine.__unmatched_let__!(\"%{foo: foo, bar: bar}\", other)\n  #\n  # Which is later wrapped by the inner_block macro.\n  #\n  # If there are any named slots that are part of the children,\n  # those are recursively converted into clauses (slots can also use :let)\n  # and returned as {slot_assigns, slot_info}.\n  defp build_component_clauses(\n         let,\n         name,\n         children,\n         tag_meta,\n         tag_close_meta,\n         substate,\n         %{caller: caller} = state\n       ) do\n    inner_substate = state.engine.handle_begin(substate)\n    state = init_slots(state)\n    {inner_state, inner_substate} = handle_node(children, inner_substate, state)\n    {slot_assigns, slot_info, _state} = pop_slots(inner_state)\n\n    opts =\n      if annotation =\n           caller && has_tags?(children) &&\n             state.tag_handler.annotate_slot(name, tag_meta, tag_close_meta, caller) do\n        [meta: [template_annotation: annotation]]\n      else\n        []\n      end\n\n    ast = state.engine.handle_end(inner_substate, opts)\n\n    clauses =\n      case let do\n        # If we have a var, we can skip the catch-all clause\n        {{var, _, ctx} = pattern, %{line: line}} when is_atom(var) and is_atom(ctx) ->\n          quote line: line do\n            unquote(pattern) -> unquote(ast)\n          end\n\n        {pattern, %{line: line}} ->\n          quote line: line do\n            unquote(pattern) -> unquote(ast)\n          end ++\n            quote generated: true do\n              other ->\n                Phoenix.LiveView.TagEngine.__unmatched_let__!(\n                  unquote(Macro.to_string(pattern)),\n                  other\n                )\n            end\n\n        _ ->\n          quote do\n            _ -> unquote(ast)\n          end\n      end\n\n    {clauses, {slot_assigns, slot_info}}\n  end\n\n  defp store_component_call(component, attr_info, slot_info, line, %{caller: caller} = state) do\n    module = caller.module\n\n    if module && Module.open?(module) do\n      pruned_slots =\n        for {slot_name, slot_values} <- slot_info, into: %{} do\n          values =\n            for {tag_meta, {root?, attrs, locs}} <- slot_values do\n              %{line: tag_meta.line, root: root?, attrs: attrs_for_call(attrs, locs)}\n            end\n\n          {slot_name, values}\n        end\n\n      {root?, attrs, locs} = attr_info\n      pruned_attrs = attrs_for_call(attrs, locs)\n\n      call = %{\n        component: component,\n        slots: pruned_slots,\n        attrs: pruned_attrs,\n        file: state.file,\n        line: line,\n        root: root?\n      }\n\n      # This may still fail under a very specific scenario where\n      # we are defining a template dynamically inside a function\n      # (most likely a test) that starts running while the module\n      # is still open.\n      try do\n        Module.put_attribute(module, :__components_calls__, call)\n      rescue\n        _ -> :ok\n      end\n    end\n  end\n\n  defp attrs_for_call(attrs, locs) do\n    for {{attr, value}, {line, column}} <- Enum.zip(attrs, locs),\n        do: {attr, {line, column, attr_type(value)}},\n        into: %{}\n  end\n\n  defp attr_type({:<<>>, _, _} = value), do: {:string, value}\n  defp attr_type(value) when is_list(value), do: {:list, value}\n  defp attr_type(value = {:%{}, _, _}), do: {:map, value}\n  defp attr_type(value) when is_binary(value), do: {:string, value}\n  defp attr_type(value) when is_integer(value), do: {:integer, value}\n  defp attr_type(value) when is_float(value), do: {:float, value}\n  defp attr_type(value) when is_boolean(value), do: {:boolean, value}\n  defp attr_type(value) when is_atom(value), do: {:atom, value}\n  defp attr_type({:fn, _, [{:->, _, [args, _]}]}), do: {:fun, length(args)}\n  defp attr_type({:&, _, [{:/, _, [_, arity]}]}), do: {:fun, arity}\n\n  # this could be a &myfun(&1, &2)\n  defp attr_type({:&, _, args}) do\n    {_ast, arity} =\n      Macro.prewalk(args, 0, fn\n        {:&, _, [n]} = ast, acc when is_integer(n) ->\n          {ast, max(n, acc)}\n\n        ast, acc ->\n          {ast, acc}\n      end)\n\n    (arity > 0 && {:fun, arity}) || :any\n  end\n\n  defp attr_type(_value), do: :any\n\n  ## Slot helpers\n\n  defp init_slots(state) do\n    %{state | slots: [[] | state.slots]}\n  end\n\n  defp add_inner_block({roots?, attrs, locs}, ast, tag_meta) do\n    {roots?, [{:inner_block, ast} | attrs], [line_column(tag_meta) | locs]}\n  end\n\n  defp add_slot(state, slot_name, slot_assigns, slot_info, tag_meta, special_attrs) do\n    %{slots: [slots | other_slots]} = state\n    slot = {slot_name, slot_assigns, special_attrs, {tag_meta, slot_info}}\n    %{state | slots: [[slot | slots] | other_slots]}\n  end\n\n  defp pop_slots(%{slots: [slots | other_slots]} = state) do\n    # Perform group_by by hand as we need to group two distinct maps.\n    {acc_assigns, acc_info, specials} =\n      Enum.reduce(slots, {%{}, %{}, %{}}, fn\n        {key, assigns, special, info}, {acc_assigns, acc_info, specials} ->\n          special? = Map.has_key?(special, \":if\") or Map.has_key?(special, \":for\")\n          specials = Map.update(specials, key, special?, &(&1 or special?))\n\n          case acc_assigns do\n            %{^key => existing_assigns} ->\n              acc_assigns = %{acc_assigns | key => [assigns | existing_assigns]}\n              %{^key => existing_info} = acc_info\n              acc_info = %{acc_info | key => [info | existing_info]}\n              {acc_assigns, acc_info, specials}\n\n            %{} ->\n              {Map.put(acc_assigns, key, [assigns]), Map.put(acc_info, key, [info]), specials}\n          end\n      end)\n\n    acc_assigns =\n      Enum.into(acc_assigns, %{}, fn {key, assigns_ast} ->\n        cond do\n          # No special entry, return it as a list\n          not Map.fetch!(specials, key) ->\n            {key, assigns_ast}\n\n          # We have a special entry and multiple entries, we have to flatten\n          match?([_, _ | _], assigns_ast) ->\n            {key, quote(do: List.flatten(unquote(assigns_ast)))}\n\n          # A single special entry is guaranteed to return a list from the expression\n          true ->\n            {key, hd(assigns_ast)}\n        end\n      end)\n\n    {Map.to_list(acc_assigns), Map.to_list(acc_info), %{state | slots: other_slots}}\n  end\n\n  defp wrap_special_slot(special, ast) do\n    case special do\n      %{\":for\" => {for_expr, %{line: line}}, \":if\" => {if_expr, %{line: _line}}} ->\n        quote line: line do\n          for unquote(for_expr), unquote(if_expr), do: unquote(ast)\n        end\n\n      %{\":for\" => {for_expr, %{line: line}}} ->\n        quote line: line do\n          for unquote(for_expr), do: unquote(ast)\n        end\n\n      %{\":if\" => {if_expr, %{line: line}}} ->\n        quote line: line do\n          if unquote(if_expr), do: [unquote(ast)], else: []\n        end\n\n      %{} ->\n        ast\n    end\n  end\n\n  defp tag_slots({call, meta, args}, slot_info) do\n    {call, [slots: Keyword.keys(slot_info)] ++ meta, args}\n  end\n\n  ## Special expressions (:if, :for, :key)\n\n  # Handles :for, :if wrapping by executing the given function\n  # in a new handle_begin / handle_end block, and building the\n  # correct wrapper AST.\n  defp with_special_attrs(attrs, meta, substate, state, fun) do\n    case pop_special_attrs!(attrs, meta, state) do\n      {false, meta, attrs} ->\n        fun.(attrs, meta, substate, state)\n\n      {true, new_meta, new_attrs} ->\n        inner_substate = state.engine.handle_begin(substate)\n        {state, inner_substate} = fun.(new_attrs, new_meta, inner_substate, state)\n        inner_ast = state.engine.handle_end(inner_substate)\n\n        ast = handle_special_expr(new_meta, inner_ast, state)\n        substate = state.engine.handle_expr(substate, \"=\", ast)\n        {state, substate}\n    end\n  end\n\n  # Pops all special attributes from attrs. Raises if any given attr is duplicated within\n  # attrs.\n  #\n  # Examples:\n  #\n  #   attrs = [{\":for\", {...}}, {\"class\", {...}}]\n  #   pop_special_attrs!(attrs, %{}, state)\n  #   => {true, %{for: parsed_ast, ...}, [{\"class\", {...}]}}\n  #\n  #   attrs = [{\"class\", {...}}]\n  #   pop_special_attrs!(attrs, %{}, state)\n  #   => {false, %{}, [{\"class\", {...}}]}\n  defp pop_special_attrs!(attrs, tag_meta, state) do\n    Enum.reduce([for: \":for\", if: \":if\", key: \":key\"], {false, tag_meta, attrs}, fn\n      {attr, string_attr}, {special_acc, meta_acc, attrs_acc} ->\n        attrs_acc\n        |> List.keytake(string_attr, 0)\n        |> raise_if_duplicated_special_attr!(state)\n        |> case do\n          {{^string_attr, {:expr, _, _} = expr, meta}, attrs} ->\n            parsed_expr = parse_expr!(expr, state.file)\n            validate_quoted_special_attr!(string_attr, parsed_expr, meta, state)\n            {true, Map.put(meta_acc, attr, parsed_expr), attrs}\n\n          {{^string_attr, _expr, meta}, _attrs} ->\n            message = \"#{string_attr} must be an expression between {...}\"\n            raise_syntax_error!(message, meta, state)\n\n          nil ->\n            {special_acc, meta_acc, attrs_acc}\n        end\n    end)\n  end\n\n  defp raise_if_duplicated_special_attr!({{attr, _expr, _meta}, attrs} = result, state) do\n    case List.keytake(attrs, attr, 0) do\n      {{attr, _expr, meta}, _attrs} ->\n        message =\n          \"cannot define multiple #{inspect(attr)} attributes. Another #{inspect(attr)} has already been defined at line #{meta.line}\"\n\n        raise_syntax_error!(message, meta, state)\n\n      nil ->\n        result\n    end\n  end\n\n  defp raise_if_duplicated_special_attr!(nil, _state), do: nil\n\n  defp handle_special_expr(tag_meta, inner_ast, state) do\n    case tag_meta do\n      %{for: _for_expr, if: if_expr} ->\n        for_expr = maybe_keyed(tag_meta)\n\n        quote do\n          for unquote(for_expr), unquote(if_expr), do: unquote(inner_ast)\n        end\n\n      %{for: _for_expr} ->\n        for_expr = maybe_keyed(tag_meta)\n\n        quote do\n          for unquote(for_expr), do: unquote(inner_ast)\n        end\n\n      %{if: if_expr} ->\n        quote do\n          if unquote(if_expr), do: unquote(inner_ast)\n        end\n\n      %{key: _} ->\n        raise_syntax_error!(\"cannot use :key without :for\", tag_meta, state)\n    end\n  end\n\n  defp maybe_keyed(%{key: key_expr, for: for_expr}) do\n    # we already validated that the for expression has the correct shape in\n    # validate_quoted_special_attr\n    {:<-, for_meta, [lhs, rhs]} = for_expr\n    {:<-, [keyed_comprehension: true, key_expr: key_expr] ++ for_meta, [lhs, rhs]}\n  end\n\n  defp maybe_keyed(%{for: for_expr}), do: for_expr\n\n  ## Generic helpers\n\n  defp to_location(%{line: line, column: column}), do: [line: line, column: column]\n  defp to_location(_), do: []\n\n  defp actual_component_module(env, fun) do\n    case Macro.Env.lookup_import(env, {fun, 1}) do\n      [{_, module} | _] -> module\n      _ -> env.module\n    end\n  end\n\n  # removes phx-no-format, etc. and maps phx-hook=\".name\" to the fully qualified name\n  defp postprocess_attrs(attrs, state) do\n    attrs_to_remove = ~w(phx-no-format phx-no-curly-interpolation)\n\n    for {key, value, meta} <- attrs,\n        key not in attrs_to_remove do\n      case {key, value, meta} do\n        {\"phx-hook\", {:string, \".\" <> name, str_meta}, meta} ->\n          {key, {:string, \"#{inspect(state.caller.module)}.#{name}\", str_meta}, meta}\n\n        _ ->\n          {key, value, meta}\n      end\n    end\n  end\n\n  defp validate_tag_attrs!(attrs, %{tag_name: \"input\"}, state) do\n    # warn if using name=\"id\" on an input\n    case Enum.find(attrs, &match?({\"name\", {:string, \"id\", _}, _}, &1)) do\n      {_name, _value, attr_meta} ->\n        meta = [\n          line: attr_meta.line,\n          column: attr_meta.column,\n          file: state.file,\n          module: state.caller.module,\n          function: state.caller.function\n        ]\n\n        IO.warn(\n          \"\"\"\n          Setting the \"name\" attribute to \"id\" on an input tag overrides the ID of the corresponding form element.\n          This leads to unexpected behavior, especially when using LiveView, and is not recommended.\n\n          You should use a different value for the \"name\" attribute, e.g. \"_id\" and remap the value in the\n          corresponding handle_event/3 callback or controller.\n          \"\"\",\n          meta\n        )\n\n      _ ->\n        :ok\n    end\n  end\n\n  defp validate_tag_attrs!(_attrs, _meta, _state), do: :ok\n\n  # Check if `phx-update`, `phx-hook` is present in attrs and raises in case\n  # there is no ID attribute set.\n  defp validate_phx_attrs!(attrs, meta, state) do\n    validate_phx_attrs!(attrs, meta, state, nil, false)\n  end\n\n  defp validate_phx_attrs!([], meta, state, attr, false)\n       when attr in [\"phx-update\", \"phx-hook\"] do\n    message = \"attribute \\\"#{attr}\\\" requires the \\\"id\\\" attribute to be set\"\n\n    raise_syntax_error!(message, meta, state)\n  end\n\n  defp validate_phx_attrs!([], _meta, _state, _attr, _id?), do: :ok\n\n  # Handle <div phx-update=\"ignore\" {@some_var}>Content</div> since here the ID\n  # might be inserted dynamically so we can't raise at compile time.\n  defp validate_phx_attrs!([{:root, _, _} | t], meta, state, attr, _id?),\n    do: validate_phx_attrs!(t, meta, state, attr, true)\n\n  defp validate_phx_attrs!([{\"id\", _, _} | t], meta, state, attr, _id?),\n    do: validate_phx_attrs!(t, meta, state, attr, true)\n\n  defp validate_phx_attrs!(\n         [{\"phx-update\", {:string, value, _meta}, attr_meta} | t],\n         meta,\n         state,\n         _attr,\n         id?\n       ) do\n    cond do\n      value in ~w(ignore stream replace) ->\n        validate_phx_attrs!(t, meta, state, \"phx-update\", id?)\n\n      value in ~w(append prepend) ->\n        line = meta[:line] || state.caller.line\n\n        IO.warn(\n          \"phx-update=\\\"#{value}\\\" is deprecated, please use streams instead\",\n          Macro.Env.stacktrace(%{state.caller | line: line})\n        )\n\n        validate_phx_attrs!(t, meta, state, \"phx-update\", id?)\n\n      true ->\n        message =\n          \"the value of the attribute \\\"phx-update\\\" must be: ignore, stream, append, prepend, or replace\"\n\n        raise_syntax_error!(message, attr_meta, state)\n    end\n  end\n\n  defp validate_phx_attrs!([{\"phx-update\", _attrs, _} | t], meta, state, _attr, id?) do\n    validate_phx_attrs!(t, meta, state, \"phx-update\", id?)\n  end\n\n  defp validate_phx_attrs!([{\"phx-hook\", _, _} | t], meta, state, _attr, id?),\n    do: validate_phx_attrs!(t, meta, state, \"phx-hook\", id?)\n\n  defp validate_phx_attrs!([{special, value, attr_meta} | t], meta, state, attr, id?)\n       when special in ~w(:if :for :type) do\n    case value do\n      {:expr, _, _} ->\n        validate_phx_attrs!(t, meta, state, attr, id?)\n\n      _ ->\n        message = \"#{special} must be an expression between {...}\"\n        raise_syntax_error!(message, attr_meta, state)\n    end\n  end\n\n  defp validate_phx_attrs!([{\":\" <> name, _, attr_meta} | _], _meta, state, _attr, _id?)\n       when name not in ~w(if for key) do\n    message = \"unsupported attribute :#{name} in tags\"\n    raise_syntax_error!(message, attr_meta, state)\n  end\n\n  defp validate_phx_attrs!([_h | t], meta, state, attr, id?),\n    do: validate_phx_attrs!(t, meta, state, attr, id?)\n\n  defp validate_quoted_special_attr!(attr, quoted_value, attr_meta, state) do\n    if attr == \":for\" and not match?({:<-, _, [_, _]}, quoted_value) do\n      message = \":for must be a generator expression (pattern <- enumerable) between {...}\"\n\n      raise_syntax_error!(message, attr_meta, state)\n    else\n      :ok\n    end\n  end\n\n  defp expand_with_line(ast, line, env) do\n    Macro.expand(ast, %{env | line: line})\n  end\n\n  defp raise_syntax_error!(message, meta, state) do\n    raise ParseError,\n      line: meta.line,\n      column: meta.column,\n      file: state.file,\n      description: message <> ParseError.code_snippet(state.source, meta, state.indentation)\n  end\n\n  defp maybe_anno_caller(substate, meta, file, line, state) do\n    annotate =\n      if function_exported?(state.tag_handler, :annotate_caller, 3) do\n        fn file, line -> state.tag_handler.annotate_caller(file, line, state.caller) end\n      else\n        fn file, line -> state.tag_handler.annotate_caller(file, line) end\n      end\n\n    if anno = annotate.(file, line) do\n      state.engine.handle_text(substate, meta, anno)\n    else\n      substate\n    end\n  end\n\n  defp has_tags?([]), do: false\n\n  defp has_tags?([{:text, _, _} | rest]), do: has_tags?(rest)\n  defp has_tags?([{:body_expr, _, _} | rest]), do: has_tags?(rest)\n  defp has_tags?([{:eex, _, _} | rest]), do: has_tags?(rest)\n  defp has_tags?([{:eex_comment, _} | rest]), do: has_tags?(rest)\n  defp has_tags?([{:html_comment, _} | rest]), do: has_tags?(rest)\n\n  # EEx blocks - check children in each clause\n  defp has_tags?([{:eex_block, _, blocks, _} | rest]) do\n    Enum.any?(blocks, fn {children, _clause_expr, _clause_meta} -> has_tags?(children) end) or\n      has_tags?(rest)\n  end\n\n  # Skip slots\n  defp has_tags?([{:self_close, :slot, _, _, _} | rest]), do: has_tags?(rest)\n  defp has_tags?([{:block, :slot, _, _, _, _, _} | rest]), do: has_tags?(rest)\n\n  # Tags and components count as having tags\n  defp has_tags?([{:self_close, _type, _, _, _} | _]), do: true\n  defp has_tags?([{:block, _type, _, _, _, _, _} | _]), do: true\n\n  defp debug_attributes?(caller) do\n    if Module.open?(caller.module) do\n      case Module.get_attribute(caller.module, :debug_attributes) do\n        false -> false\n        _ -> Application.get_env(:phoenix_live_view, :debug_attributes, false)\n      end\n    else\n      Application.get_env(:phoenix_live_view, :debug_attributes, false)\n    end\n  rescue\n    _ -> Application.get_env(:phoenix_live_view, :debug_attributes, false)\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/tag_engine/parser.ex",
    "content": "defmodule Phoenix.LiveView.TagEngine.Parser do\n  @moduledoc false\n\n  defstruct [:nodes, :directives]\n\n  @type t :: %__MODULE__{\n          nodes: list(tag_node()),\n          directives: Phoenix.Component.MacroComponent.directives()\n        }\n\n  @type tag_node() :: text() | comment() | block() | self_close() | expression()\n  @type text() :: {:text, binary(), meta()}\n  @type comment() :: {:eex_comment, binary(), meta()}\n  @type block() :: {:block, atom(), binary(), list(attr()), list(tag_node()), meta(), meta()}\n  @type self_close() :: {:self_close, atom(), binary(), list(attr()), meta()}\n  @type expression() ::\n          {:body_expr, binary(), meta()}\n          | {:eex, binary(), meta()}\n          | {:eex_block, binary(), list(eex_clause()), meta()}\n  @type eex_clause() :: {list(tag_node()), binary(), meta()}\n  @type attr :: {:root | binary(), attr_value(), meta()}\n  @type attr_value :: {:expr, binary(), meta()} | {:string, binary(), meta()}\n  @type meta() :: map()\n\n  alias Phoenix.LiveView.TagEngine.Tokenizer\n  alias Phoenix.Component.MacroComponent\n\n  defguardp is_tag_open(tag_type) when tag_type not in [:close, :eex]\n\n  defguardp is_tag_block(node)\n            when is_tuple(node) and tuple_size(node) == 7 and elem(node, 0) == :block\n\n  defguardp is_self_close(node)\n            when is_tuple(node) and tuple_size(node) == 5 and elem(node, 0) == :self_close\n\n  defguardp is_macro_component(node)\n            when (is_tag_block(node) and is_map_key(elem(node, 5), :macro_component)) or\n                   (is_self_close(node) and is_map_key(elem(node, 4), :macro_component))\n\n  def parse(source, opts \\\\ []) do\n    tag_handler = Keyword.fetch!(opts, :tag_handler)\n    caller = Keyword.get(opts, :caller)\n    skip_macro_components = Keyword.get(opts, :skip_macro_components, false)\n    prune_text_after_slots = Keyword.get(opts, :prune_text_after_slots, true)\n    process_buffer = Keyword.get(opts, :process_buffer)\n\n    source\n    |> tokenize(opts)\n    |> to_tree([], [], %{\n      tag_handler: tag_handler,\n      caller: caller,\n      skip_macro_components: skip_macro_components,\n      prune_text_after_slots: prune_text_after_slots,\n      process_buffer: process_buffer,\n      directives: []\n    })\n  catch\n    {:syntax_error, line, column, message} ->\n      {:error, line, column, message}\n  end\n\n  def parse!(source, opts \\\\ []) do\n    case parse(source, opts) do\n      {:ok, nodes} ->\n        nodes\n\n      {:error, line, column, message} ->\n        raise Tokenizer.ParseError,\n          line: line,\n          column: column,\n          file: opts[:file] || \"nofile\",\n          description:\n            message <>\n              Tokenizer.ParseError.code_snippet(\n                source,\n                %{line: line, column: column},\n                opts[:indentation] || 0\n              )\n    end\n  end\n\n  # Tokenize contents using EEx.tokenize and Phoenix.LiveView.TagEngine.Tokenizer respectively.\n  #\n  # The following content:\n  #\n  # \"<section>\\n  <p><%= user.name %></p>\\n  <%= if true do %> <p>this</p><% else %><p>that</p><% end %>\\n</section>\\n\"\n  #\n  # Will be tokenized as:\n  #\n  # [\n  #   {:tag, \"section\", [], %{column: 1, line: 1}},\n  #   {:text, \"\\n  \", %{column_end: 3, line_end: 2}},\n  #   {:tag, \"p\", [], %{column: 3, line: 2}},\n  #   {:eex, :start_expr, \"<%= user.name ></p>\\n  <%= if true do %>\", %{block?: true, column: 6, line: 1}},\n  #   {:text, \" \", %{column_end: 2, line_end: 1}},\n  #   {:tag, \"p\", [], %{column: 2, line: 1}},\n  #   {:text, \"this\", %{column_end: 12, line_end: 1}},\n  #   {:close, :tag, \"p\", %{column: 12, line: 1}},\n  #   {:eex, :middle_expr, \"<% else %>\", %{block?: false, column: 35, line: 2}},\n  #   {:tag, \"p\", [], %{column: 1, line: 1}},\n  #   {:text, \"that\", %{column_end: 14, line_end: 1}},\n  #   {:close, :tag, \"p\", %{column: 14, line: 1}},\n  #   {:eex, :end_expr, \"<% end %>\", %{block?: false, column: 62, line: 2}},\n  #   {:text, \"\\n\", %{column_end: 1, line_end: 2}},\n  #   {:close, :tag, \"section\", %{column: 1, line: 2}}\n  # ]\n  #\n  @eex_expr [:start_expr, :expr, :end_expr, :middle_expr]\n\n  @doc false\n  def tokenize(source, opts) do\n    file = Keyword.get(opts, :file, \"nofile\")\n    indentation = Keyword.get(opts, :indentation, 0)\n    trim_eex = Keyword.get(opts, :trim_eex, true)\n    strip_eex_comments = Keyword.get(opts, :strip_eex_comments, false)\n    {:ok, eex_nodes} = EEx.tokenize(source, opts)\n\n    {tokens, cont} =\n      Enum.reduce(\n        eex_nodes,\n        {[], {:text, :enabled}},\n        &do_tokenize(&1, &2, source, %{\n          file: file,\n          indentation: indentation,\n          trim_eex: trim_eex,\n          strip_eex_comments: strip_eex_comments\n        })\n      )\n\n    Tokenizer.finalize(tokens, file, cont, source)\n  end\n\n  defp do_tokenize({:text, text, meta}, {tokens, cont}, source, %{\n         file: file,\n         indentation: indentation\n       }) do\n    text = List.to_string(text)\n    meta = [line: meta.line, column: meta.column]\n    state = Tokenizer.init(indentation, file, source, Phoenix.LiveView.HTMLEngine)\n    Tokenizer.tokenize(text, meta, tokens, cont, state)\n  end\n\n  defp do_tokenize({:comment, text, meta}, {tokens, cont}, _contents, opts) do\n    if opts.strip_eex_comments do\n      {tokens, cont}\n    else\n      {[{:eex_comment, List.to_string(text), meta} | tokens], cont}\n    end\n  end\n\n  defp do_tokenize(\n         {type, opt, expr, %{column: column, line: line}},\n         {tokens, cont},\n         _contents,\n         opts\n       )\n       when type in @eex_expr do\n    meta = %{opt: opt, line: line, column: column}\n\n    {[{:eex, type, expr |> List.to_string() |> maybe_trim_eex(opts.trim_eex), meta} | tokens],\n     cont}\n  end\n\n  defp do_tokenize(_node, acc, _contents, _opts) do\n    acc\n  end\n\n  defp maybe_trim_eex(string, true), do: String.trim(string)\n  defp maybe_trim_eex(string, _), do: string\n\n  # Build an HTML Tree according to the tokens from the EEx and HTML tokenizers.\n  #\n  # This is a recursive algorithm that will build an HTML tree from a flat list of\n  # tokens. For instance, given this input:\n  #\n  # [\n  #   {:tag, \"div\", [], %{column: 1, line: 1}},\n  #   {:tag, \"h1\", [], %{column: 6, line: 1}},\n  #   {:text, \"Hello\", %{column_end: 15, line_end: 1}},\n  #   {:close, :tag, \"h1\", %{column: 15, line: 1}},\n  #   {:close, :tag, \"div\", %{column: 20, line: 1}},\n  #   {:tag, \"div\", [], %{column: 1, line: 2}},\n  #   {:tag, \"h1\", [], %{column: 6, line: 2}},\n  #   {:text, \"World\", %{column_end: 15, line_end: 2}},\n  #   {:close, :tag, \"h1\", %{column: 15, line: 2}},\n  #   {:close, :tag, \"div\", %{column: 20, line: 2}}\n  # ]\n  #\n  # The output will be:\n  #\n  # [\n  #   {:block, :tag, \"div\", [],\n  #    [{:block, :tag, \"h1\", [],\n  #      [{:text, \"Hello\", %{...}}],\n  #      %{line: 1, column: 6, ...},\n  #      %{line: 1, column: 15, ...}}],\n  #    %{line: 1, column: 1, ...},\n  #    %{line: 1, column: 20, ...}},\n  #   {:block, :tag, \"div\", [],\n  #    [{:block, :tag, \"h1\", [],\n  #      [{:text, \"World\", %{...}}],\n  #      %{line: 2, column: 6, ...},\n  #      %{line: 2, column: 15, ...}}],\n  #    %{line: 2, column: 1, ...},\n  #    %{line: 2, column: 20, ...}}\n  # ]\n  #\n  # Note that a `:block` has been created so that its fifth argument is a list of\n  # its nested content, followed by open_meta and close_meta.\n  #\n  # ### How does this algorithm work?\n  #\n  # As this is a recursive algorithm, it starts with an empty buffer and an empty\n  # stack. The buffer will be accumulated until it finds a `{:tag, ..., ...}`.\n  #\n  # As soon as the `tag_open` arrives, a new buffer will be started and we move\n  # the previous buffer to the stack along with the `tag_open`:\n  #\n  #   ```\n  #   defp to_tree([{type, name, attrs, meta} | tokens], buffer, stack, state)\n  #        when is_tag_open(type) do\n  #     to_tree(tokens, [], [{type, name, attrs, meta, buffer} | stack], state)\n  #   end\n  #   ```\n  #\n  # Then, we start to populate the buffer again until a `{:close, :tag, ...} arrives:\n  #\n  #   ```\n  #   defp to_tree([{:close, type, name, close_meta} | tokens], buffer, [{type, name, attrs, open_meta, upper_buffer} | stack], state) do\n  #     to_tree(tokens, [{:block, type, name, attrs, Enum.reverse(buffer), open_meta, close_meta} | upper_buffer], stack)\n  #   end\n  #   ```\n  #\n  # In the snippet above, we build the `:block` tuple with the accumulated buffer,\n  # putting the buffer accumulated before the tag open (upper_buffer) on top.\n  #\n  # We apply the same logic for `eex` expressions but, instead of `tag_open` and\n  # `tag_close`, eex expressions use `start_expr`, `middle_expr` and `end_expr`.\n  # The only real difference is that also need to handle `middle_buffer`.\n  #\n  # So given this eex input:\n  #\n  # ```elixir\n  # [\n  #   {:eex, :start_expr, \"if true do\", %{column: 0, line: 0, opt: '='}},\n  #   {:text, \"\\n  \", %{column_end: 3, line_end: 2}},\n  #   {:eex, :expr, \"\\\"Hello\\\"\", %{column: 3, line: 1, opt: '='}},\n  #   {:text, \"\\n\", %{column_end: 1, line_end: 2}},\n  #   {:eex, :middle_expr, \"else\", %{column: 1, line: 2, opt: []}},\n  #   {:text, \"\\n  \", %{column_end: 3, line_end: 2}},\n  #   {:eex, :expr, \"\\\"World\\\"\", %{column: 3, line: 3, opt: '='}},\n  #   {:text, \"\\n\", %{column_end: 1, line_end: 2}},\n  #   {:eex, :end_expr, \"end\", %{column: 1, line: 4, opt: []}}\n  # ]\n  # ```\n  #\n  # The output will be:\n  #\n  # ```elixir\n  # [\n  #   {:eex_block, \"if true do\",\n  #    [\n  #      {[{:eex, \"\\\"Hello\\\"\", %{column: 3, line: 1, opt: '='}}], \"else\", %{line: 2, column: 1}},\n  #      {[{:eex, \"\\\"World\\\"\", %{column: 3, line: 3, opt: '='}}], \"end\", %{line: 4, column: 1}}\n  #    ], %{line: 0, column: 0, opt: '='}}\n  # ]\n  # ```\n  defp to_tree([], buffer, [], state) do\n    {:ok, %__MODULE__{nodes: Enum.reverse(buffer), directives: state.directives}}\n  end\n\n  defp to_tree(\n         [],\n         _buffer,\n         [{_type, _name, _, %{line: line, column: column} = meta, _} | _],\n         _state\n       ) do\n    message = \"end of template reached without closing tag for <#{meta.tag_name}>\"\n    {:error, line, column, message}\n  end\n\n  defp to_tree([{:text, text, meta} | tokens], buffer, stack, state) do\n    # Preserve context for HTML comment handling in formatter\n    text_meta = Map.take(meta, [:context])\n    buffer = process_buffer([{:text, text, text_meta} | buffer], state)\n    to_tree(tokens, buffer, stack, state)\n  end\n\n  defp to_tree([{:body_expr, value, meta} | tokens], buffer, stack, state) do\n    buffer = process_buffer([{:body_expr, value, meta} | buffer], state)\n    to_tree(tokens, buffer, stack, state)\n  end\n\n  # Self-closing slot - valid only as direct child of component\n  defp to_tree(\n         [{:slot, name, attrs, %{closing: _} = meta} | tokens],\n         buffer,\n         [{parent_type, _, _, _, _} | _] = stack,\n         state\n       )\n       when parent_type in [:local_component, :remote_component] do\n    meta = meta |> Map.put(:special, extract_special_attrs(attrs)) |> maybe_macro_component(attrs)\n\n    {tokens, buffer, state} =\n      maybe_process_macro_component(\n        {:self_close, :slot, name, attrs, meta},\n        tokens,\n        buffer,\n        state\n      )\n\n    tokens = if state.prune_text_after_slots, do: prune_text(tokens), else: tokens\n\n    to_tree(tokens, buffer, stack, state)\n  end\n\n  # Self-closing slot - invalid context (not direct child of component)\n  defp to_tree(\n         [{:slot, name, _attrs, %{closing: _} = meta} | _tokens],\n         _buffer,\n         _stack,\n         _state\n       ) do\n    %{line: line, column: column} = meta\n    message = \"invalid slot entry <:#{name}>. A slot entry must be a direct child of a component\"\n    {:error, line, column, message}\n  end\n\n  # Opening slot - valid only as direct child of component\n  defp to_tree(\n         [{:slot, name, attrs, meta} | tokens],\n         buffer,\n         [{parent_type, _, _, _, _} | _] = stack,\n         state\n       )\n       when parent_type in [:local_component, :remote_component] do\n    meta = meta |> Map.put(:special, extract_special_attrs(attrs)) |> maybe_macro_component(attrs)\n    to_tree(tokens, [], [{:slot, name, attrs, meta, buffer} | stack], state)\n  end\n\n  # Opening slot - invalid context (not direct child of component)\n  defp to_tree([{:slot, name, _attrs, meta} | _tokens], _buffer, _stack, _state) do\n    %{line: line, column: column} = meta\n    message = \"invalid slot entry <:#{name}>. A slot entry must be a direct child of a component\"\n    {:error, line, column, message}\n  end\n\n  # Closing a slot\n  defp to_tree(\n         [{:close, :slot, _name, close_meta} | tokens],\n         reversed_buffer,\n         [{:slot, tag_name, attrs, open_meta, upper_buffer} | stack],\n         state\n       ) do\n    block = Enum.reverse(reversed_buffer)\n    tag_block = {:block, :slot, tag_name, attrs, block, open_meta, close_meta}\n    tokens = if state.prune_text_after_slots, do: prune_text(tokens), else: tokens\n    to_tree(tokens, [tag_block | upper_buffer], stack, state)\n  end\n\n  # Self-closing tag or component\n  defp to_tree([{type, name, attrs, %{closing: _} = meta} | tokens], buffer, stack, state)\n       when is_tag_open(type) do\n    meta = meta |> Map.put(:special, extract_special_attrs(attrs)) |> maybe_macro_component(attrs)\n\n    {tokens, buffer, state} =\n      maybe_process_macro_component({:self_close, type, name, attrs, meta}, tokens, buffer, state)\n\n    to_tree(tokens, buffer, stack, state)\n  end\n\n  # Opening tag or component\n  defp to_tree([{type, name, attrs, meta} | tokens], buffer, stack, state)\n       when is_tag_open(type) do\n    meta = meta |> Map.put(:special, extract_special_attrs(attrs)) |> maybe_macro_component(attrs)\n    to_tree(tokens, [], [{type, name, attrs, meta, buffer} | stack], state)\n  end\n\n  # Matching close tag\n  defp to_tree(\n         [{:close, _type, name, close_meta} | tokens],\n         reversed_buffer,\n         [{type, name, attrs, open_meta, upper_buffer} | stack],\n         state\n       ) do\n    block = Enum.reverse(reversed_buffer)\n\n    {tokens, buffer, state} =\n      maybe_process_macro_component(\n        {:block, type, name, attrs, block, open_meta, close_meta},\n        tokens,\n        upper_buffer,\n        state\n      )\n\n    to_tree(tokens, buffer, stack, state)\n  end\n\n  # Mismatched close tag\n  defp to_tree(\n         [{:close, _close_type, close_name, close_meta} | _tokens],\n         _buffer,\n         [{_open_type, open_name, _attrs, open_meta, _upper_buffer} | _stack],\n         state\n       ) do\n    %{line: line, column: column} = close_meta\n    void_note = void_tag_note(close_name, state)\n\n    message =\n      \"unmatched closing tag. Expected </#{open_name}> for <#{open_name}> at line #{open_meta.line}, got: </#{close_name}>#{void_note}\"\n\n    {:error, line, column, message}\n  end\n\n  # Orphaned close tag - no matching open tag on stack\n  defp to_tree([{:close, _type, name, meta} | _tokens], _buffer, [], state) do\n    %{line: line, column: column} = meta\n    void_note = void_tag_note(name, state)\n    message = \"missing opening tag for </#{name}>#{void_note}\"\n    {:error, line, column, message}\n  end\n\n  # EEx\n\n  defp to_tree([{:eex_comment, text, _meta} | tokens], buffer, stack, state) do\n    to_tree(tokens, [{:eex_comment, text} | buffer], stack, state)\n  end\n\n  defp to_tree([{:eex, :start_expr, expr, meta} | tokens], buffer, stack, state) do\n    to_tree(tokens, [], [{:eex_block, expr, meta, buffer} | stack], state)\n  end\n\n  defp to_tree(\n         [{:eex, :middle_expr, middle_expr, middle_meta} | tokens],\n         buffer,\n         [{:eex_block, expr, meta, upper_buffer, middle_buffer} | stack],\n         state\n       ) do\n    middle_buffer = [{Enum.reverse(buffer), middle_expr, middle_meta} | middle_buffer]\n\n    to_tree(\n      tokens,\n      [],\n      [{:eex_block, expr, meta, upper_buffer, middle_buffer} | stack],\n      state\n    )\n  end\n\n  defp to_tree(\n         [{:eex, :middle_expr, middle_expr, middle_meta} | tokens],\n         buffer,\n         [{:eex_block, expr, meta, upper_buffer} | stack],\n         state\n       ) do\n    middle_buffer = [{Enum.reverse(buffer), middle_expr, middle_meta}]\n\n    to_tree(\n      tokens,\n      [],\n      [{:eex_block, expr, meta, upper_buffer, middle_buffer} | stack],\n      state\n    )\n  end\n\n  defp to_tree(\n         [{:eex, :end_expr, end_expr, end_meta} | tokens],\n         buffer,\n         [{:eex_block, expr, meta, upper_buffer, middle_buffer} | stack],\n         state\n       ) do\n    block = Enum.reverse([{Enum.reverse(buffer), end_expr, end_meta} | middle_buffer])\n    to_tree(tokens, [{:eex_block, expr, block, meta} | upper_buffer], stack, state)\n  end\n\n  defp to_tree(\n         [{:eex, :end_expr, end_expr, end_meta} | tokens],\n         buffer,\n         [{:eex_block, expr, meta, upper_buffer} | stack],\n         state\n       ) do\n    block = [{Enum.reverse(buffer), end_expr, end_meta}]\n    to_tree(tokens, [{:eex_block, expr, block, meta} | upper_buffer], stack, state)\n  end\n\n  # end_expr reached but unclosed tag on stack (inside a do-block)\n  defp to_tree(\n         [{:eex, :end_expr, _end_expr, _end_meta} | _tokens],\n         _buffer,\n         [{_type, _name, _attrs, %{line: line, column: column} = meta, _upper_buffer} | _stack],\n         _state\n       ) do\n    message = \"end of do-block reached without closing tag for <#{meta.tag_name}>\"\n    {:error, line, column, message}\n  end\n\n  defp to_tree([{:eex, _type, expr, meta} | tokens], buffer, stack, state) do\n    buffer = process_buffer([{:eex, expr, meta} | buffer], state)\n    to_tree(tokens, buffer, stack, state)\n  end\n\n  @special_attrs ~w(:if :for :key)\n  defp extract_special_attrs(attrs) do\n    for {name, _, _} <- attrs, name in @special_attrs, do: name\n  end\n\n  # Prune leading whitespace from the next text token (used after slots)\n  defp prune_text([{:text, text, meta} | tokens]) do\n    [{:text, String.trim_leading(text), meta} | tokens]\n  end\n\n  defp prune_text(tokens), do: tokens\n\n  # Allow callers to hook into buffer processing (used by formatter for preserve mode propagation)\n  defp process_buffer(buffer, %{process_buffer: fun}) when is_function(fun), do: fun.(buffer)\n  defp process_buffer(buffer, _state), do: buffer\n\n  # Removes empty whitespace nodes at the start of the tokens (used after macro components)\n  defp strip_text([{:text, \"\", _meta} | tokens]), do: strip_text(tokens)\n\n  defp strip_text([{:text, text, meta} | tokens]) do\n    case String.trim_leading(text) do\n      \"\" ->\n        strip_text(tokens)\n\n      text ->\n        [{:text, text, meta} | tokens]\n    end\n  end\n\n  defp strip_text(tokens), do: tokens\n\n  defp void_tag_note(name, state) do\n    if state.tag_handler.void?(name) do\n      \" (note <#{name}> is a void tag and cannot have any content)\"\n    else\n      \"\"\n    end\n  end\n\n  ## Macro component handling\n\n  defp maybe_macro_component(meta, attrs) do\n    case List.keyfind(attrs, \":type\", 0) do\n      {\":type\", {:expr, code, _expr_meta}, _attr_meta} ->\n        Map.put(meta, :macro_component, code)\n\n      _ ->\n        meta\n    end\n  end\n\n  defguardp skip_macro_components(state)\n            when is_map_key(state, :skip_macro_components) and state.skip_macro_components == true\n\n  defp maybe_process_macro_component(tree_node, tokens, buffer, state)\n       when is_macro_component(tree_node) and not skip_macro_components(state) do\n    caller = state.caller\n\n    if is_nil(caller) do\n      raise ArgumentError, \"macro components require a caller environment\"\n    end\n\n    Macro.Env.required?(caller, Phoenix.Component) ||\n      raise ArgumentError,\n            \"macro components are only supported in modules that `use Phoenix.Component`\"\n\n    tree_node =\n      case tree_node do\n        # Remove :type from attrs for the macro component AST\n        {:self_close, :tag, name, attrs, meta} ->\n          {:self_close, :tag, name, List.keydelete(attrs, \":type\", 0), meta}\n\n        {:block, :tag, name, attrs, children, meta, close_meta} ->\n          {:block, :tag, name, List.keydelete(attrs, \":type\", 0), children, meta, close_meta}\n\n        # Macro components are currently only allowed on non-special tags\n        {:self_close, type, _name, _attrs, meta} ->\n          throw_syntax_error!(\n            \"macro components are only supported on HTML tags, not #{format_type(type)}\",\n            meta\n          )\n\n        {:block, type, _name, _attrs, _children, meta, _close_meta} ->\n          throw_syntax_error!(\n            \"macro components are only supported on HTML tags, not #{format_type(type)}\",\n            meta\n          )\n      end\n\n    meta = get_meta(tree_node)\n    module_string = meta.macro_component\n    module = validate_module!(module_string, meta, state)\n\n    case process_macro_component(tree_node, module, state) do\n      {_new_node, directives} when directives != [] and buffer != [] ->\n        throw_syntax_error!(\n          \"macro component #{inspect(module)} specified directives and therefore must appear at the very beginning of the template\",\n          get_meta(tree_node)\n        )\n\n      {{:text, \"\", _}, directives} ->\n        {strip_text(tokens), buffer, %{state | directives: directives}}\n\n      {new_node, directives} ->\n        {strip_text(tokens), [new_node | buffer], %{state | directives: directives}}\n    end\n  end\n\n  defp maybe_process_macro_component(tree_node, tokens, buffer, state) do\n    {tokens, [tree_node | buffer], state}\n  end\n\n  # Process a macro component: call transform and convert result back to tree nodes\n  defp process_macro_component(tree_node, module, state) do\n    # Macro components work by converting the tree nodes into a macro AST\n    # (see Phoenix.Component.MacroComponent) and then calling the transform\n    # function on the macro component module, which can return a transformed\n    # AST.\n    #\n    # The AST is limited in functionality and we convert it back to the regular\n    # node format afterwards.\n    meta = get_meta(tree_node)\n\n    case MacroComponent.build_ast(tree_node, state.caller) do\n      {:ok, macro_ast} ->\n        try do\n          # Call the transform function\n          case module.transform(macro_ast, %{env: state.caller}) do\n            {:ok, new_ast} ->\n              {MacroComponent.ast_to_tree(new_ast, meta), []}\n\n            {:ok, new_ast, data} ->\n              Module.put_attribute(state.caller.module, :__macro_components__, {module, data})\n              {MacroComponent.ast_to_tree(new_ast, meta), []}\n\n            {:ok, new_ast, data, directives} ->\n              Module.put_attribute(state.caller.module, :__macro_components__, {module, data})\n\n              {MacroComponent.ast_to_tree(new_ast, meta),\n               validate_directives!(module, directives, meta)}\n\n            other ->\n              throw_syntax_error!(\n                \"a macro component must return {:ok, ast}, {:ok, ast, data}, or {:ok, ast, data, directives}, got: #{inspect(other)}\",\n                meta\n              )\n          end\n        rescue\n          e in ArgumentError ->\n            throw_syntax_error!(Exception.message(e), meta)\n        end\n\n      {:error, message, error_meta} ->\n        throw_syntax_error!(message, error_meta)\n    end\n  end\n\n  defp get_meta({:self_close, _type, _name, _attrs, meta}), do: meta\n  defp get_meta({:block, _type, _name, _attrs, _children, meta, _close_meta}), do: meta\n\n  defp validate_module!(module_string, tag_meta, state) do\n    module =\n      Code.string_to_quoted!(module_string,\n        file: state.caller.file,\n        line: tag_meta.line,\n        column: tag_meta.column\n      )\n      |> Macro.expand(state.caller)\n\n    if not is_atom(module) do\n      throw_syntax_error!(\n        \"the given macro component #{inspect(module_string)} is not a valid module\",\n        tag_meta\n      )\n    end\n\n    _ = Code.ensure_compiled!(module)\n\n    if not function_exported?(module, :transform, 2) do\n      throw_syntax_error!(\n        \"the given macro component #{inspect(module)} does not implement the `Phoenix.Component.MacroComponent` behaviour\",\n        tag_meta\n      )\n    end\n\n    module\n  end\n\n  defp validate_directives!(module, directives, meta) do\n    Enum.each(directives, fn {key, value} ->\n      validate_directive!(module, key, value, meta)\n    end)\n\n    directives\n  end\n\n  defp validate_directive!(_module, :root_tag_attribute, nil, _), do: :ok\n\n  defp validate_directive!(_module, :root_tag_attribute, {name, value}, _meta)\n       when is_binary(name) and (is_binary(value) or value == true) do\n    :ok\n  end\n\n  defp validate_directive!(module, :root_tag_attribute, other, meta) do\n    throw_syntax_error!(\n      \"\"\"\n      expected {name, value} for :root_tag_attribute directive from macro component #{inspect(module)}, got: #{inspect(other)}\n\n      name must be a compile-time string, and value must be a compile-time string or true\n      \"\"\",\n      meta\n    )\n  end\n\n  defp validate_directive!(module, directive, value, meta) do\n    throw_syntax_error!(\n      \"unknown directive #{inspect({directive, value})} provided by macro component #{inspect(module)}\",\n      meta\n    )\n  end\n\n  defp throw_syntax_error!(message, meta) do\n    throw({:syntax_error, meta.line, meta.column, message})\n  end\n\n  defp format_type(:slot), do: \"slots\"\n  defp format_type(_), do: \"components\"\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/tag_engine/tokenizer.ex",
    "content": "defmodule Phoenix.LiveView.TagEngine.Tokenizer do\n  @moduledoc false\n  @space_chars ~c\"\\s\\t\\f\"\n  @quote_chars ~c\"\\\"'\"\n  @stop_chars ~c\">/=\\r\\n\" ++ @quote_chars ++ @space_chars\n\n  defmodule ParseError do\n    @moduledoc false\n    defexception [:file, :line, :column, :description]\n\n    @impl true\n    def message(exception) do\n      location =\n        exception.file\n        |> Path.relative_to_cwd()\n        |> Exception.format_file_line_column(exception.line, exception.column)\n\n      \"#{location} #{exception.description}\"\n    end\n\n    def code_snippet(source, meta, indentation \\\\ 0) do\n      line_start = max(meta.line - 3, 1)\n      line_end = meta.line\n      digits = line_end |> Integer.to_string() |> byte_size()\n      number_padding = String.duplicate(\" \", digits)\n      indentation = String.duplicate(\" \", indentation)\n\n      source\n      |> String.split([\"\\r\\n\", \"\\n\"])\n      |> Enum.slice((line_start - 1)..(line_end - 1))\n      |> Enum.map_reduce(line_start, fn\n        expr, line_number when line_number == line_end ->\n          arrow = String.duplicate(\" \", meta.column - 1) <> \"^\"\n          acc = \"#{line_number} | #{indentation}#{expr}\\n #{number_padding}| #{arrow}\"\n          {acc, line_number + 1}\n\n        expr, line_number ->\n          line_number_padding = String.pad_leading(\"#{line_number}\", digits)\n          {\"#{line_number_padding} | #{indentation}#{expr}\", line_number + 1}\n      end)\n      |> case do\n        {[], _} ->\n          \"\"\n\n        {snippet, _} ->\n          Enum.join([\"\\n #{number_padding}|\" | snippet], \"\\n\")\n      end\n    end\n  end\n\n  def finalize(_tokens, file, {:comment, line, column}, source) do\n    message = \"expected closing `-->` for comment\"\n    meta = %{line: line, column: column}\n    raise_syntax_error!(message, meta, %{source: source, file: file, indentation: 0})\n  end\n\n  def finalize(tokens, _file, _cont, _source) do\n    tokens\n    |> strip_text_token_fully()\n    |> Enum.reverse()\n    |> strip_text_token_fully()\n  end\n\n  @doc \"\"\"\n  Initiate the Tokenizer state.\n\n  ### Params\n\n  * `indentation` - An integer that indicates the current indentation.\n  * `file` - Can be either a file or a string \"nofile\".\n  * `source` - The contents of the file as binary used to be tokenized.\n  * `tag_handler` - Tag handler to classify the tags. See `Phoenix.LiveView.TagEngine`\n    behaviour.\n  \"\"\"\n  def init(indentation, file, source, tag_handler) do\n    %{\n      file: file,\n      column_offset: indentation + 1,\n      braces: :enabled,\n      context: [],\n      source: source,\n      indentation: indentation,\n      tag_handler: tag_handler\n    }\n  end\n\n  @doc \"\"\"\n  Tokenize the given text according to the given params.\n\n  ### Params\n\n  * `text` - The content to be tokenized.\n  * `meta` - A keyword list with `:line` and `:column`. Both must be integers.\n  * `tokens` - A list of tokens.\n  * `cont` - An atom that is `:text`, `:style`, or `:script`, or a tuple\n    {:comment, line, column}.\n  * `state` - The tokenizer state that must be initiated by `Tokenizer.init/4`\n\n  ### Examples\n\n      iex> alias Phoenix.LiveView.Tokenizer\n\n      iex> state =\n        Tokenizer.init(indent, file, [text: \"<section><div/></section>\"], HTMLEngine)\n\n      iex> Tokenizer.tokenize(state)\n      {[\n         {:close, :tag, \"section\", %{column: 16, line: 1}},\n         {:tag, \"div\", [], %{column: 10, line: 1, closing: :self}},\n         {:tag, \"section\", [], %{column: 1, line: 1}}\n       ], {:text, :enabled}}\n  \"\"\"\n  def tokenize(text, meta, tokens, cont, state) do\n    line = Keyword.get(meta, :line, 1)\n    column = Keyword.get(meta, :column, 1)\n\n    case cont do\n      {:text, braces} -> handle_text(text, line, column, [], tokens, %{state | braces: braces})\n      :style -> handle_style(text, line, column, [], tokens, state)\n      :script -> handle_script(text, line, column, [], tokens, state)\n      {:comment, _, _} -> handle_comment(text, line, column, [], tokens, state)\n    end\n  end\n\n  ## handle_text\n\n  defp handle_text(\"\\r\\n\" <> rest, line, _column, buffer, acc, state) do\n    handle_text(rest, line + 1, state.column_offset, [\"\\r\\n\" | buffer], acc, state)\n  end\n\n  defp handle_text(\"\\n\" <> rest, line, _column, buffer, acc, state) do\n    handle_text(rest, line + 1, state.column_offset, [\"\\n\" | buffer], acc, state)\n  end\n\n  defp handle_text(\"<!doctype\" <> rest, line, column, buffer, acc, state) do\n    handle_doctype(rest, line, column + 9, [\"<!doctype\" | buffer], acc, state)\n  end\n\n  defp handle_text(\"<!DOCTYPE\" <> rest, line, column, buffer, acc, state) do\n    handle_doctype(rest, line, column + 9, [\"<!DOCTYPE\" | buffer], acc, state)\n  end\n\n  defp handle_text(\"<!--\" <> rest, line, column, buffer, acc, state) do\n    state = update_in(state.context, &[:comment_start | &1])\n    handle_comment(rest, line, column + 4, [\"<!--\" | buffer], acc, state)\n  end\n\n  defp handle_text(\"</\" <> rest, line, column, buffer, acc, state) do\n    text_to_acc = text_to_acc(buffer, acc, line, column, state.context)\n    handle_tag_close(rest, line, column + 2, text_to_acc, %{state | context: []})\n  end\n\n  defp handle_text(\"<\" <> rest, line, column, buffer, acc, state) do\n    text_to_acc = text_to_acc(buffer, acc, line, column, state.context)\n    handle_tag_open(rest, line, column + 1, text_to_acc, %{state | context: []})\n  end\n\n  defp handle_text(\"{\" <> rest, line, column, buffer, acc, %{braces: :enabled} = state) do\n    text_to_acc = text_to_acc(buffer, acc, line, column, state.context)\n    state = put_in(state.context, [])\n\n    case handle_interpolation(rest, line, column + 1, [], 0, state) do\n      {:ok, value, new_line, new_column, rest} ->\n        acc = [{:body_expr, value, %{line: line, column: column}} | text_to_acc]\n        handle_text(rest, new_line, new_column, [], acc, state)\n\n      {:error, message} ->\n        meta = %{line: line, column: column}\n        raise_syntax_error!(message, meta, state)\n    end\n  end\n\n  defp handle_text(<<c::utf8, rest::binary>>, line, column, buffer, acc, state) do\n    handle_text(rest, line, column + 1, [char_or_bin(c) | buffer], acc, state)\n  end\n\n  defp handle_text(<<>>, line, column, buffer, acc, state) do\n    ok(text_to_acc(buffer, acc, line, column, state.context), {:text, state.braces})\n  end\n\n  ## handle_doctype\n\n  defp handle_doctype(<<?>, rest::binary>>, line, column, buffer, acc, state) do\n    handle_text(rest, line, column + 1, [?> | buffer], acc, state)\n  end\n\n  defp handle_doctype(\"\\r\\n\" <> rest, line, _column, buffer, acc, state) do\n    handle_doctype(rest, line + 1, state.column_offset, [\"\\r\\n\" | buffer], acc, state)\n  end\n\n  defp handle_doctype(\"\\n\" <> rest, line, _column, buffer, acc, state) do\n    handle_doctype(rest, line + 1, state.column_offset, [\"\\n\" | buffer], acc, state)\n  end\n\n  defp handle_doctype(<<c::utf8, rest::binary>>, line, column, buffer, acc, state) do\n    handle_doctype(rest, line, column + 1, [char_or_bin(c) | buffer], acc, state)\n  end\n\n  defp handle_doctype(<<>>, line, column, _buffer, _acc, state) do\n    raise_syntax_error!(\n      \"unexpected end of string inside tag\",\n      %{line: line, column: column},\n      state\n    )\n  end\n\n  ## handle_script\n\n  defp handle_script(\"</script>\" <> rest, line, column, buffer, acc, state) do\n    acc = [\n      {:close, :tag, \"script\", %{line: line, column: column, inner_location: {line, column}}}\n      | text_to_acc(buffer, acc, line, column, [])\n    ]\n\n    handle_text(rest, line, column + 9, [], acc, state)\n  end\n\n  defp handle_script(\"\\r\\n\" <> rest, line, _column, buffer, acc, state) do\n    handle_script(rest, line + 1, state.column_offset, [\"\\r\\n\" | buffer], acc, state)\n  end\n\n  defp handle_script(\"\\n\" <> rest, line, _column, buffer, acc, state) do\n    handle_script(rest, line + 1, state.column_offset, [\"\\n\" | buffer], acc, state)\n  end\n\n  defp handle_script(<<c::utf8, rest::binary>>, line, column, buffer, acc, state) do\n    handle_script(rest, line, column + 1, [char_or_bin(c) | buffer], acc, state)\n  end\n\n  defp handle_script(<<>>, line, column, buffer, acc, _state) do\n    ok(text_to_acc(buffer, acc, line, column, []), :script)\n  end\n\n  ## handle_style\n\n  defp handle_style(\"</style>\" <> rest, line, column, buffer, acc, state) do\n    acc = [\n      {:close, :tag, \"style\", %{line: line, column: column, inner_location: {line, column}}}\n      | text_to_acc(buffer, acc, line, column, [])\n    ]\n\n    handle_text(rest, line, column + 9, [], acc, state)\n  end\n\n  defp handle_style(\"\\r\\n\" <> rest, line, _column, buffer, acc, state) do\n    handle_style(rest, line + 1, state.column_offset, [\"\\r\\n\" | buffer], acc, state)\n  end\n\n  defp handle_style(\"\\n\" <> rest, line, _column, buffer, acc, state) do\n    handle_style(rest, line + 1, state.column_offset, [\"\\n\" | buffer], acc, state)\n  end\n\n  defp handle_style(<<c::utf8, rest::binary>>, line, column, buffer, acc, state) do\n    handle_style(rest, line, column + 1, [char_or_bin(c) | buffer], acc, state)\n  end\n\n  defp handle_style(<<>>, line, column, buffer, acc, _state) do\n    ok(text_to_acc(buffer, acc, line, column, []), :style)\n  end\n\n  ## handle_comment\n\n  defp handle_comment(rest, line, column, buffer, acc, state) do\n    case handle_comment(rest, line, column, buffer, state) do\n      {:text, rest, line, column, buffer} ->\n        state = update_in(state.context, &[:comment_end | &1])\n        handle_text(rest, line, column, buffer, acc, state)\n\n      {:ok, line_end, column_end, buffer} ->\n        acc = text_to_acc(buffer, acc, line_end, column_end, state.context)\n        # We do column - 4 to point to the opening <!--\n        ok(acc, {:comment, line, column - 4})\n    end\n  end\n\n  defp handle_comment(\"\\r\\n\" <> rest, line, _column, buffer, state) do\n    handle_comment(rest, line + 1, state.column_offset, [\"\\r\\n\" | buffer], state)\n  end\n\n  defp handle_comment(\"\\n\" <> rest, line, _column, buffer, state) do\n    handle_comment(rest, line + 1, state.column_offset, [\"\\n\" | buffer], state)\n  end\n\n  defp handle_comment(\"-->\" <> rest, line, column, buffer, _state) do\n    {:text, rest, line, column + 3, [\"-->\" | buffer]}\n  end\n\n  defp handle_comment(<<c::utf8, rest::binary>>, line, column, buffer, state) do\n    handle_comment(rest, line, column + 1, [char_or_bin(c) | buffer], state)\n  end\n\n  defp handle_comment(<<>>, line, column, buffer, _state) do\n    {:ok, line, column, buffer}\n  end\n\n  ## handle_tag_open\n\n  defp handle_tag_open(text, line, column, acc, state) do\n    case handle_tag_name(text, column, []) do\n      {:ok, name, new_column, rest} ->\n        meta = %{line: line, column: column - 1, inner_location: nil, tag_name: name}\n\n        case state.tag_handler.classify_type(name) do\n          {:error, message} ->\n            raise_syntax_error!(message, %{line: line, column: column}, state)\n\n          {type, name} ->\n            acc = [{type, name, [], meta} | acc]\n            handle_maybe_tag_open_end(rest, line, new_column, acc, state)\n        end\n\n      :error ->\n        message =\n          \"expected tag name after <. If you meant to use < as part of a text, use &lt; instead\"\n\n        meta = %{line: line, column: column}\n\n        raise_syntax_error!(message, meta, state)\n    end\n  end\n\n  ## handle_tag_close\n\n  defp handle_tag_close(text, line, column, acc, state) do\n    case handle_tag_name(text, column, []) do\n      {:ok, name, new_column, \">\" <> rest} ->\n        meta = %{\n          line: line,\n          column: column - 2,\n          inner_location: {line, column - 2},\n          tag_name: name\n        }\n\n        case state.tag_handler.classify_type(name) do\n          {:error, message} ->\n            raise_syntax_error!(message, meta, state)\n\n          {type, name} ->\n            acc = [{:close, type, name, meta} | acc]\n            handle_text(rest, line, new_column + 1, [], acc, pop_braces(state))\n        end\n\n      {:ok, _, new_column, _} ->\n        message = \"expected closing `>`\"\n        meta = %{line: line, column: new_column}\n        raise_syntax_error!(message, meta, state)\n\n      :error ->\n        message = \"expected tag name after </\"\n        meta = %{line: line, column: column}\n        raise_syntax_error!(message, meta, state)\n    end\n  end\n\n  ## handle_tag_name\n\n  defp handle_tag_name(<<c::utf8, _rest::binary>> = text, column, buffer)\n       when c in @stop_chars do\n    done_tag_name(text, column, buffer)\n  end\n\n  defp handle_tag_name(<<c::utf8, rest::binary>>, column, buffer) do\n    handle_tag_name(rest, column + 1, [char_or_bin(c) | buffer])\n  end\n\n  defp handle_tag_name(<<>>, column, buffer) do\n    done_tag_name(<<>>, column, buffer)\n  end\n\n  defp done_tag_name(_text, _column, []) do\n    :error\n  end\n\n  defp done_tag_name(text, column, buffer) do\n    {:ok, buffer_to_string(buffer), column, text}\n  end\n\n  ## handle_maybe_tag_open_end\n\n  defp handle_maybe_tag_open_end(\"\\r\\n\" <> rest, line, _column, acc, state) do\n    handle_maybe_tag_open_end(rest, line + 1, state.column_offset, acc, state)\n  end\n\n  defp handle_maybe_tag_open_end(\"\\n\" <> rest, line, _column, acc, state) do\n    handle_maybe_tag_open_end(rest, line + 1, state.column_offset, acc, state)\n  end\n\n  defp handle_maybe_tag_open_end(<<c::utf8, rest::binary>>, line, column, acc, state)\n       when c in @space_chars do\n    handle_maybe_tag_open_end(rest, line, column + 1, acc, state)\n  end\n\n  defp handle_maybe_tag_open_end(\"/>\" <> rest, line, column, acc, state) do\n    acc = normalize_tag(acc, line, column + 2, true, state)\n    handle_text(rest, line, column + 2, [], acc, state)\n  end\n\n  defp handle_maybe_tag_open_end(\">\" <> rest, line, column, acc, state) do\n    case normalize_tag(acc, line, column + 1, false, state) do\n      [{:tag, \"script\", _, _} | _] = acc ->\n        handle_script(rest, line, column + 1, [], acc, state)\n\n      [{:tag, \"style\", _, _} | _] = acc ->\n        handle_style(rest, line, column + 1, [], acc, state)\n\n      acc ->\n        handle_text(rest, line, column + 1, [], acc, push_braces(state))\n    end\n  end\n\n  defp handle_maybe_tag_open_end(\"{\" <> rest, line, column, acc, state) do\n    handle_root_attribute(rest, line, column + 1, acc, state)\n  end\n\n  defp handle_maybe_tag_open_end(<<>>, line, column, _acc, state) do\n    message = ~S\"\"\"\n    expected closing `>` or `/>`\n\n    Make sure the tag is properly closed. This may happen if there\n    is an EEx interpolation inside a tag, which is not supported.\n    For instance, instead of\n\n        <div id=\"<%= @id %>\">Content</div>\n\n    do\n\n        <div id={@id}>Content</div>\n\n    If @id is nil or false, then no attribute is sent at all.\n\n    Inside {...} you can place any Elixir expression. If you want\n    to interpolate in the middle of an attribute value, instead of\n\n        <a class=\"foo bar <%= @class %>\">Text</a>\n\n    you can pass an Elixir string with interpolation:\n\n        <a class={\"foo bar #{@class}\"}>Text</a>\n    \"\"\"\n\n    raise_syntax_error!(message, %{line: line, column: column}, state)\n  end\n\n  defp handle_maybe_tag_open_end(text, line, column, acc, state) do\n    handle_attribute(text, line, column, acc, state)\n  end\n\n  ## handle_attribute\n\n  defp handle_attribute(text, line, column, acc, state) do\n    case handle_attr_name(text, column, []) do\n      {:ok, name, new_column, rest} ->\n        attr_meta = %{line: line, column: column}\n        {text, line, column, value} = handle_maybe_attr_value(rest, line, new_column, state)\n        acc = put_attr(acc, name, attr_meta, value)\n        maybe_warn_missing_attr_space(value, text, line, column, state)\n\n        state =\n          if name == \"phx-no-curly-interpolation\" and state.braces == :enabled and\n               not script_or_style?(acc) do\n            %{state | braces: 0}\n          else\n            state\n          end\n\n        handle_maybe_tag_open_end(text, line, column, acc, state)\n\n      {:error, message, column} ->\n        meta = %{line: line, column: column}\n        raise_syntax_error!(message, meta, state)\n    end\n  end\n\n  defp maybe_warn_missing_attr_space(nil, _text, _line, _column, _state), do: :ok\n\n  defp maybe_warn_missing_attr_space(_value, <<c, _::binary>>, line, column, state)\n       when c not in @space_chars and c not in ~c\">/\\r\\n\" do\n    IO.warn(\n      \"missing space before attribute\",\n      line: line,\n      column: column,\n      file: state.file\n    )\n  end\n\n  defp maybe_warn_missing_attr_space(_value, _text, _line, _column, _state), do: :ok\n\n  defp script_or_style?([{:tag, name, _, _} | _]) when name in ~w(script style), do: true\n  defp script_or_style?(_), do: false\n\n  ## handle_root_attribute\n\n  defp handle_root_attribute(text, line, column, acc, state) do\n    case handle_interpolation(text, line, column, [], 0, state) do\n      {:ok, value, new_line, new_column, rest} ->\n        meta = %{line: line, column: column}\n        acc = put_attr(acc, :root, meta, {:expr, value, meta})\n        handle_maybe_tag_open_end(rest, new_line, new_column, acc, state)\n\n      {:error, message} ->\n        # We do column - 1 to point to the opening {\n        meta = %{line: line, column: column - 1}\n        raise_syntax_error!(message, meta, state)\n    end\n  end\n\n  ## handle_attr_name\n\n  defp handle_attr_name(<<\"}\"::utf8, _rest::binary>>, column, _buffer) do\n    {:error, \"expected attribute, but found end of interpolation: }\", column}\n  end\n\n  defp handle_attr_name(<<c::utf8, _rest::binary>>, column, _buffer)\n       when c in @quote_chars do\n    {:error, \"invalid character in attribute name: #{<<c>>}\", column}\n  end\n\n  defp handle_attr_name(<<c::utf8, _rest::binary>>, column, [])\n       when c in @stop_chars do\n    {:error, \"expected attribute name\", column}\n  end\n\n  defp handle_attr_name(<<c::utf8, _rest::binary>> = text, column, buffer)\n       when c in @stop_chars do\n    {:ok, buffer_to_string(buffer), column, text}\n  end\n\n  defp handle_attr_name(<<c::utf8, rest::binary>>, column, buffer) do\n    handle_attr_name(rest, column + 1, [char_or_bin(c) | buffer])\n  end\n\n  defp handle_attr_name(<<>>, column, _buffer) do\n    {:error, \"unexpected end of string inside tag\", column}\n  end\n\n  ## handle_maybe_attr_value\n\n  defp handle_maybe_attr_value(\"\\r\\n\" <> rest, line, _column, state) do\n    handle_maybe_attr_value(rest, line + 1, state.column_offset, state)\n  end\n\n  defp handle_maybe_attr_value(\"\\n\" <> rest, line, _column, state) do\n    handle_maybe_attr_value(rest, line + 1, state.column_offset, state)\n  end\n\n  defp handle_maybe_attr_value(<<c::utf8, rest::binary>>, line, column, state)\n       when c in @space_chars do\n    handle_maybe_attr_value(rest, line, column + 1, state)\n  end\n\n  defp handle_maybe_attr_value(\"=\" <> rest, line, column, state) do\n    handle_attr_value_begin(rest, line, column + 1, state)\n  end\n\n  defp handle_maybe_attr_value(text, line, column, _state) do\n    {text, line, column, nil}\n  end\n\n  ## handle_attr_value_begin\n\n  defp handle_attr_value_begin(\"\\r\\n\" <> rest, line, _column, state) do\n    handle_attr_value_begin(rest, line + 1, state.column_offset, state)\n  end\n\n  defp handle_attr_value_begin(\"\\n\" <> rest, line, _column, state) do\n    handle_attr_value_begin(rest, line + 1, state.column_offset, state)\n  end\n\n  defp handle_attr_value_begin(<<c::utf8, rest::binary>>, line, column, state)\n       when c in @space_chars do\n    handle_attr_value_begin(rest, line, column + 1, state)\n  end\n\n  defp handle_attr_value_begin(\"\\\"\" <> rest, line, column, state) do\n    handle_attr_value_quote(rest, ?\", line, column + 1, [], state)\n  end\n\n  defp handle_attr_value_begin(\"'\" <> rest, line, column, state) do\n    handle_attr_value_quote(rest, ?', line, column + 1, [], state)\n  end\n\n  defp handle_attr_value_begin(\"{\" <> rest, line, column, state) do\n    handle_attr_value_as_expr(rest, line, column + 1, state)\n  end\n\n  defp handle_attr_value_begin(_text, line, column, state) do\n    message =\n      \"invalid attribute value after `=`. Expected either a value between quotes \" <>\n        \"(such as \\\"value\\\" or \\'value\\') or an Elixir expression between curly braces (such as `{expr}`)\"\n\n    meta = %{line: line, column: column}\n    raise_syntax_error!(message, meta, state)\n  end\n\n  ## handle_attr_value_quote\n\n  defp handle_attr_value_quote(\"\\r\\n\" <> rest, delim, line, _column, buffer, state) do\n    column = state.column_offset\n    handle_attr_value_quote(rest, delim, line + 1, column, [\"\\r\\n\" | buffer], state)\n  end\n\n  defp handle_attr_value_quote(\"\\n\" <> rest, delim, line, _column, buffer, state) do\n    column = state.column_offset\n    handle_attr_value_quote(rest, delim, line + 1, column, [\"\\n\" | buffer], state)\n  end\n\n  defp handle_attr_value_quote(<<delim, rest::binary>>, delim, line, column, buffer, _state) do\n    value = buffer_to_string(buffer)\n    {rest, line, column + 1, {:string, value, %{delimiter: delim}}}\n  end\n\n  defp handle_attr_value_quote(<<c::utf8, rest::binary>>, delim, line, column, buffer, state) do\n    handle_attr_value_quote(rest, delim, line, column + 1, [char_or_bin(c) | buffer], state)\n  end\n\n  defp handle_attr_value_quote(<<>>, delim, line, column, _buffer, state) do\n    message = \"\"\"\n    expected closing `#{<<delim>>}` for attribute value\n\n    Make sure the attribute is properly closed. This may also happen if\n    there is an EEx interpolation inside a tag, which is not supported.\n    Instead of\n\n        <div <%= @some_attributes %>>\n        </div>\n\n    do\n\n        <div {@some_attributes}>\n        </div>\n\n    Where @some_attributes must be a keyword list or a map.\n    \"\"\"\n\n    meta = %{line: line, column: column}\n    raise_syntax_error!(message, meta, state)\n  end\n\n  ## handle_attr_value_as_expr\n\n  defp handle_attr_value_as_expr(text, line, column, state) do\n    case handle_interpolation(text, line, column, [], 0, state) do\n      {:ok, value, new_line, new_column, rest} ->\n        {rest, new_line, new_column, {:expr, value, %{line: line, column: column}}}\n\n      {:error, message} ->\n        # We do column - 1 to point to the opening {\n        meta = %{line: line, column: column - 1}\n        raise_syntax_error!(message, meta, state)\n    end\n  end\n\n  ## handle_interpolation\n\n  defp handle_interpolation(\"\\r\\n\" <> rest, line, _column, buffer, braces, state) do\n    handle_interpolation(rest, line + 1, state.column_offset, [\"\\r\\n\" | buffer], braces, state)\n  end\n\n  defp handle_interpolation(\"\\n\" <> rest, line, _column, buffer, braces, state) do\n    handle_interpolation(rest, line + 1, state.column_offset, [\"\\n\" | buffer], braces, state)\n  end\n\n  defp handle_interpolation(\"}\" <> rest, line, column, buffer, 0, _state) do\n    value = buffer_to_string(buffer)\n    {:ok, value, line, column + 1, rest}\n  end\n\n  defp handle_interpolation(~S(\\}) <> rest, line, column, buffer, braces, state) do\n    handle_interpolation(rest, line, column + 2, [~S(\\}) | buffer], braces, state)\n  end\n\n  defp handle_interpolation(~S(\\{) <> rest, line, column, buffer, braces, state) do\n    handle_interpolation(rest, line, column + 2, [~S(\\{) | buffer], braces, state)\n  end\n\n  defp handle_interpolation(\"}\" <> rest, line, column, buffer, braces, state) do\n    handle_interpolation(rest, line, column + 1, [\"}\" | buffer], braces - 1, state)\n  end\n\n  defp handle_interpolation(\"{\" <> rest, line, column, buffer, braces, state) do\n    handle_interpolation(rest, line, column + 1, [\"{\" | buffer], braces + 1, state)\n  end\n\n  defp handle_interpolation(<<c::utf8, rest::binary>>, line, column, buffer, braces, state) do\n    handle_interpolation(rest, line, column + 1, [char_or_bin(c) | buffer], braces, state)\n  end\n\n  defp handle_interpolation(<<>>, _line, _column, _buffer, _braces, _state) do\n    {:error,\n     \"\"\"\n     expected closing `}` for expression\n\n     In case you don't want `{` to begin a new interpolation, \\\n     you may write it using `&lbrace;` or using `<%= \"{\" %>`\\\n     \"\"\"}\n  end\n\n  ## helpers\n\n  @compile {:inline, ok: 2, char_or_bin: 1}\n  defp ok(acc, cont), do: {acc, cont}\n\n  defp char_or_bin(c) when c <= 127, do: c\n  defp char_or_bin(c), do: <<c::utf8>>\n\n  defp buffer_to_string(buffer) do\n    IO.iodata_to_binary(Enum.reverse(buffer))\n  end\n\n  defp text_to_acc(buffer, acc, line, column, context)\n\n  defp text_to_acc([], acc, _line, _column, _context),\n    do: acc\n\n  defp text_to_acc(buffer, acc, line, column, context) do\n    meta = %{line_end: line, column_end: column}\n\n    meta =\n      if context == [] do\n        meta\n      else\n        Map.put(meta, :context, trim_context(context))\n      end\n\n    [{:text, buffer_to_string(buffer), meta} | acc]\n  end\n\n  defp trim_context([:comment_end, :comment_start | [_ | _] = rest]), do: trim_context(rest)\n  defp trim_context(rest), do: Enum.reverse(rest)\n\n  defp push_braces(%{braces: :enabled} = state), do: state\n  defp push_braces(%{braces: braces} = state), do: %{state | braces: braces + 1}\n\n  defp pop_braces(%{braces: :enabled} = state), do: state\n  defp pop_braces(%{braces: 1} = state), do: %{state | braces: :enabled}\n  defp pop_braces(%{braces: braces} = state), do: %{state | braces: braces - 1}\n\n  defp put_attr([{type, name, attrs, meta} | acc], attr, attr_meta, value) do\n    attrs = [{attr, value, attr_meta} | attrs]\n    [{type, name, attrs, meta} | acc]\n  end\n\n  defp normalize_tag([{type, name, attrs, meta} | acc], line, column, self_close?, state) do\n    attrs = Enum.reverse(attrs)\n    meta = %{meta | inner_location: {line, column}}\n\n    meta =\n      cond do\n        type == :tag and state.tag_handler.void?(name) -> Map.put(meta, :closing, :void)\n        self_close? -> Map.put(meta, :closing, :self)\n        true -> meta\n      end\n\n    [{type, name, attrs, meta} | acc]\n  end\n\n  defp strip_text_token_fully(tokens) do\n    with [{:text, text, _} | rest] <- tokens,\n         \"\" <- String.trim_leading(text) do\n      strip_text_token_fully(rest)\n    else\n      _ -> tokens\n    end\n  end\n\n  defp raise_syntax_error!(message, meta, state) do\n    raise ParseError,\n      file: state.file,\n      line: meta.line,\n      column: meta.column,\n      description: message <> ParseError.code_snippet(state.source, meta, state.indentation)\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/tag_engine.ex",
    "content": "defmodule Phoenix.LiveView.TagEngine do\n  @moduledoc \"\"\"\n  Building blocks for tag based `Phoenix.Template.Engine`s.\n\n  This cannot be directly used by Phoenix applications.\n  Instead, it is the building block for engines such as\n  `Phoenix.LiveView.HTMLEngine`.\n\n  It is typically invoked like this:\n\n      Phoenix.LiveView.TagEngine.compile(source,\n        line: 1,\n        file: path,\n        caller: __CALLER__,\n        source: source,\n        tag_handler: FooBarEngine\n      )\n\n  Where `:tag_handler` implements the behaviour defined by this module.\n  \"\"\"\n\n  alias Phoenix.LiveView.TagEngine\n\n  @doc \"\"\"\n  Compiles the given string into Elixir AST.\n\n  The accepted options are:\n\n    * `tag_handler` - Required. The module implementing the `Phoenix.LiveView.TagEngine` behavior.\n    * `caller` - Required. The `Macro.Env`.\n    * `line` - the starting line offset. Defaults to 1.\n    * `file` - the file of the template. Defaults to `\"nofile\"`.\n    * `indentation` - the indentation of the template. Defaults to 0.\n\n  \"\"\"\n  def compile(source, options) do\n    options =\n      Keyword.validate!(options, [\n        :caller,\n        :tag_handler,\n        :trim,\n        line: 1,\n        indentation: 0,\n        file: \"nofile\",\n        engine: Phoenix.LiveView.Engine\n      ])\n      |> Keyword.merge(source: source, trim_eex: false, strip_eex_comments: true)\n\n    source\n    |> TagEngine.Parser.parse!(options)\n    |> TagEngine.Compiler.compile(options)\n  end\n\n  @doc \"\"\"\n  Classify the tag type from the given binary.\n\n  This must return a tuple containing the type of the tag and the name of tag.\n  For instance, for LiveView which uses HTML as default tag handler this would\n  return `{:tag, 'div'}` in case the given binary is identified as HTML tag.\n\n  You can also return `{:error, \"reason\"}` so that the compiler will display this\n  error.\n  \"\"\"\n  @callback classify_type(name :: binary()) :: {type :: atom(), name :: binary()}\n\n  @doc \"\"\"\n  Returns if the given tag name is void or not.\n\n  That's mainly useful for HTML tags and used internally by the compiler. You\n  can just implement as `def void?(_), do: false` if you want to ignore this.\n  \"\"\"\n  @callback void?(name :: binary()) :: boolean()\n\n  @doc \"\"\"\n  Implements processing of attributes.\n\n  It returns a quoted expression or attributes. If attributes are returned,\n  the second element is a list where each element in the list represents\n  one attribute. If the list element is a two-element tuple, it is assumed\n  the key is the name to be statically written in the template. The second\n  element is the value which is also statically written to the template whenever\n  possible (such as binaries or binaries inside a list).\n  \"\"\"\n  @callback handle_attributes(ast :: Macro.t(), meta :: keyword) ::\n              {:attributes, [{binary(), Macro.t()} | Macro.t()]} | {:quoted, Macro.t()}\n\n  @doc \"\"\"\n  Callback invoked to add annotations around the whole body of a template.\n  \"\"\"\n  @callback annotate_body(caller :: Macro.Env.t()) :: {String.t(), String.t()} | nil\n\n  @doc \"\"\"\n  Callback invoked to add annotations around each slot of a template.\n\n  In case the slot is an implicit inner block, the tag meta points to\n  the component.\n  \"\"\"\n  @callback annotate_slot(\n              name :: atom(),\n              tag_meta :: %{line: non_neg_integer(), column: non_neg_integer()},\n              close_tag_meta :: %{line: non_neg_integer(), column: non_neg_integer()},\n              caller :: Macro.Env.t()\n            ) :: {String.t(), String.t()} | nil\n\n  @doc \"\"\"\n  Callback invoked to add caller annotations before a function component is invoked.\n  \"\"\"\n  @callback annotate_caller(file :: String.t(), line :: integer(), caller :: Macro.Env.t()) ::\n              String.t() | nil\n\n  @doc \"\"\"\n  Renders a component defined by the given function.\n\n  This function is rarely invoked directly by users. Instead, it is used by `~H`\n  and other engine implementations to render `Phoenix.Component`s. For example,\n  the following:\n\n  ```heex\n  <MyApp.Weather.city name=\"Kraków\" />\n  ```\n\n  Is the same as:\n\n  ```heex\n  <%= component(\n        &MyApp.Weather.city/1,\n        [name: \"Kraków\"],\n        {__ENV__.module, __ENV__.function, __ENV__.file, __ENV__.line}\n      ) %>\n  ```\n\n  \"\"\"\n  def component(func, assigns, caller)\n      when (is_function(func, 1) and is_list(assigns)) or is_map(assigns) do\n    assigns =\n      case assigns do\n        %{__changed__: _} -> assigns\n        _ -> assigns |> Map.new() |> Map.put_new(:__changed__, nil)\n      end\n\n    case func.(assigns) do\n      %Phoenix.LiveView.Rendered{} = rendered ->\n        %{rendered | caller: caller}\n\n      %Phoenix.LiveView.Component{} = component ->\n        component\n\n      other ->\n        raise RuntimeError, \"\"\"\n        expected #{inspect(func)} to return a %Phoenix.LiveView.Rendered{} struct\n\n        Ensure your render function uses ~H to define its template.\n\n        Got:\n\n            #{inspect(other)}\n\n        \"\"\"\n    end\n  end\n\n  @doc \"\"\"\n  Define a inner block, generally used by slots.\n\n  This macro is mostly used by custom HTML engines that provide\n  a `slot` implementation and rarely called directly. The\n  `name` must be the assign name the slot/block will be stored\n  under.\n\n  If you're using HEEx templates, you should use its higher\n  level `<:slot>` notation instead. See `Phoenix.Component`\n  for more information.\n  \"\"\"\n  defmacro inner_block(name, do: do_block) do\n    # TODO: Remove the catch-all clause, it is no longer used\n    case do_block do\n      [{:->, meta, _} | _] ->\n        inner_fun = {:fn, meta, do_block}\n\n        quote do\n          fn parent_changed, arg ->\n            var!(assigns) =\n              unquote(__MODULE__).__assigns__(var!(assigns), unquote(name), parent_changed)\n\n            _ = var!(assigns)\n            unquote(inner_fun).(arg)\n          end\n        end\n\n      _ ->\n        quote do\n          fn parent_changed, arg ->\n            var!(assigns) =\n              unquote(__MODULE__).__assigns__(var!(assigns), unquote(name), parent_changed)\n\n            _ = var!(assigns)\n            unquote(do_block)\n          end\n        end\n    end\n  end\n\n  @doc false\n  def __assigns__(assigns, key, parent_changed) do\n    # If the component is in its initial render (parent_changed == nil)\n    # or the slot/block key is in parent_changed, then we render the\n    # function with the assigns as is.\n    #\n    # Otherwise, we will set changed to an empty list, which is the same\n    # as marking everything as not changed. This is correct because\n    # parent_changed will always be marked as changed whenever any of the\n    # assigns it references inside is changed. It will also be marked as\n    # changed if it has any variable (such as the ones coming from let).\n    if is_nil(parent_changed) or Map.has_key?(parent_changed, key) do\n      assigns\n    else\n      Map.put(assigns, :__changed__, %{})\n    end\n  end\n\n  @doc false\n  def __unmatched_let__!(pattern, value) do\n    message = \"\"\"\n    cannot match arguments sent from render_slot/2 against the pattern in :let.\n\n    Expected a value matching `#{pattern}`, got: #{inspect(value)}\\\n    \"\"\"\n\n    stacktrace =\n      self()\n      |> Process.info(:current_stacktrace)\n      |> elem(1)\n      |> Enum.drop(2)\n\n    reraise(message, stacktrace)\n  end\n\n  @behaviour EEx.Engine\n\n  @impl true\n  def init(opts) do\n    IO.warn(\"\"\"\n    Using Phoenix.LiveView.TagEngine as an EEx.Engine is deprecated!\n\n    To compile HEEx, use Phoenix.LiveView.TagEngine.compile/2 instead.\n    \"\"\")\n\n    {subengine, opts} = Keyword.pop(opts, :subengine, Phoenix.LiveView.Engine)\n    tag_handler = Keyword.fetch!(opts, :tag_handler)\n    caller = Keyword.fetch!(opts, :caller)\n\n    %{\n      subengine: subengine,\n      substate: subengine.init(opts),\n      file: Keyword.get(opts, :file, \"nofile\"),\n      line: Keyword.get(opts, :line, caller.line),\n      indentation: Keyword.get(opts, :indentation, 0),\n      caller: caller,\n      source: Keyword.fetch!(opts, :source),\n      tag_handler: tag_handler\n    }\n  end\n\n  ## EEx.Engine callbacks\n  ## These delegate to the subengine to satisfy EEx's expectations,\n  ## but handle_body ignores everything and reparses with TagEngine.Parser + TagEngine.Compiler.\n\n  @impl true\n  def handle_body(state) do\n    trim = Application.get_env(:phoenix, :trim_on_html_eex_engine, true)\n\n    %{\n      source: source,\n      file: file,\n      line: line,\n      caller: caller,\n      tag_handler: tag_handler,\n      subengine: subengine,\n      indentation: indentation\n    } = state\n\n    options = [\n      engine: subengine,\n      file: file,\n      line: line,\n      caller: caller,\n      indentation: indentation,\n      source: source,\n      tag_handler: tag_handler,\n      trim_tokens: true,\n      trim: trim\n    ]\n\n    compile(source, options)\n  end\n\n  @impl true\n  def handle_end(_state) do\n    nil\n  end\n\n  @impl true\n  def handle_begin(_state) do\n    nil\n  end\n\n  @impl true\n  def handle_text(state, _meta, _text) do\n    state\n  end\n\n  @impl true\n  def handle_expr(state, _marker, _expr) do\n    state\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/test/client_proxy.ex",
    "content": "defmodule Phoenix.LiveViewTest.ClientProxy do\n  @moduledoc false\n  use GenServer\n\n  @data_phx_upload_ref \"data-phx-upload-ref\"\n  @events :e\n  @title :t\n  @reply :r\n\n  defstruct session_token: nil,\n            static_token: nil,\n            module: nil,\n            endpoint: nil,\n            router: nil,\n            pid: nil,\n            proxy: nil,\n            topic: nil,\n            ref: nil,\n            rendered: nil,\n            children: [],\n            child_statics: %{},\n            id: nil,\n            uri: nil,\n            connect_params: %{},\n            connect_info: %{},\n            on_error: nil\n\n  alias Plug.Conn.Query\n  alias Phoenix.LiveViewTest.{ClientProxy, DOM, Diff, Element, TreeDOM, Upload, View}\n  import Phoenix.LiveViewTest.Utils, only: [stringify: 2]\n\n  @doc \"\"\"\n  Encoding used by the Channel serializer.\n  \"\"\"\n  def encode!(msg), do: msg\n\n  @doc \"\"\"\n  Stops the client proxy gracefully.\n  \"\"\"\n  def stop(proxy_pid, reason) do\n    GenServer.call(proxy_pid, {:stop, reason})\n  end\n\n  @doc \"\"\"\n  Returns the tokens of the root view.\n  \"\"\"\n  def root_view(proxy_pid) do\n    GenServer.call(proxy_pid, :root_view)\n  end\n\n  @doc \"\"\"\n  Reports upload progress to the proxy.\n  \"\"\"\n  def report_upload_progress(proxy_pid, from, element, entry_ref, percent, cid) do\n    GenServer.call(proxy_pid, {:upload_progress, from, element, entry_ref, percent, cid})\n  end\n\n  @doc \"\"\"\n  Starts a client proxy.\n\n  ## Options\n\n    * `:caller` - the required `{ref, pid}` pair identifying the caller.\n    * `:view` - the required `%Phoenix.LiveViewTest.View{}`\n    * `:html` - the required string of HTML for the document.\n\n  \"\"\"\n  def start_link(opts) do\n    GenServer.start_link(__MODULE__, opts)\n  end\n\n  def init(opts) do\n    # Since we are always running in the test client, we will disable\n    # our own logging and let the client do the job.\n    Logger.put_process_level(self(), :none)\n\n    %{\n      caller: {_, ref} = caller,\n      html: response_html,\n      connect_params: connect_params,\n      connect_info: connect_info,\n      live_module: module,\n      endpoint: endpoint,\n      router: router,\n      session: session,\n      url: url,\n      test_supervisor: test_supervisor,\n      on_error: on_error,\n      start_location: start_location\n    } = opts\n\n    # We can assume there is at least one LiveView\n    # because the live_module assign was set.\n    #\n    # On live_redirect, we only have a fragment of the full HTML response,\n    # because the root layout is not included in the redirect response.\n    html =\n      case response_html do\n        {:document, html} ->\n          DOM.parse_document(html, fn type, msg -> send(self(), {:test_error, type, msg}) end)\n\n        {:fragment, html} ->\n          DOM.parse_fragment(html, fn type, msg -> send(self(), {:test_error, type, msg}) end)\n      end\n\n    {lazy_html, html_tree} = html\n\n    {id, session_token, static_token, redirect_url} =\n      case Map.fetch(opts, :live_redirect) do\n        {:ok, {id, session_token, static_token}} ->\n          {id, session_token, static_token, url}\n\n        :error ->\n          [{id, session_token, static_token} | _] = TreeDOM.find_live_views(html_tree)\n          {id, session_token, static_token, nil}\n      end\n\n    root_view = %ClientProxy{\n      id: id,\n      ref: ref,\n      connect_params: connect_params,\n      connect_info: connect_info,\n      session_token: session_token,\n      static_token: static_token,\n      module: module,\n      endpoint: endpoint,\n      router: router,\n      uri: URI.parse(url),\n      child_statics: Map.delete(DOM.find_static_views(lazy_html), id),\n      topic: \"lv:#{id}\",\n      # we store on_error in the view ClientProxy struct as well\n      # to pass it when live_redirecting\n      on_error: on_error\n    }\n\n    # We build an absolute path to any relative\n    # static assets through the root LiveView's endpoint.\n    priv_dir = :otp_app |> endpoint.config() |> Application.app_dir(\"priv\")\n    static_url = endpoint.config(:static_url) || []\n\n    static_path =\n      case Keyword.get(static_url, :path) do\n        nil ->\n          Path.join(priv_dir, \"static\")\n\n        _path ->\n          priv_dir\n      end\n\n    # clear stream elements from static render\n    html_tree = TreeDOM.remove_stream_children(html_tree)\n\n    state = %{\n      join_ref: 0,\n      ref: 0,\n      caller: caller,\n      views: %{},\n      ids: %{},\n      pids: %{},\n      replies: %{},\n      dropped_replies: %{},\n      root_view: nil,\n      html_tree: html_tree,\n      lazy_cache: %{root_view.id => DOM.by_id!(lazy_html, root_view.id)},\n      static_path: static_path,\n      session: session,\n      test_supervisor: test_supervisor,\n      url: url,\n      page_title: :unset,\n      on_error: on_error,\n      start_location: start_location\n    }\n\n    try do\n      {root_view, rendered, resp} = mount_view(state, root_view, url, redirect_url)\n\n      new_state =\n        state\n        |> maybe_put_container(resp)\n        |> Map.put(:root_view, root_view)\n        |> put_view(root_view, rendered)\n        |> detect_added_or_removed_children(root_view, html_tree, [])\n\n      send_caller(\n        new_state,\n        {:ok, build_client_view(root_view), TreeDOM.to_html(new_state.html_tree)}\n      )\n\n      {:ok, new_state}\n    catch\n      :throw, {:stop, {:shutdown, reason}, _state} ->\n        send_caller(state, {:error, reason})\n        :ignore\n\n      :throw, {:stop, reason, _} ->\n        Process.unlink(elem(caller, 0))\n        {:stop, reason}\n    end\n  end\n\n  defp maybe_put_container(state, %{container: container}) do\n    [tag, attrs] = container\n\n    %{\n      state\n      | html_tree: TreeDOM.replace_root_container(state.html_tree, tag, attrs),\n        lazy_cache: %{}\n    }\n  end\n\n  defp maybe_put_container(state, %{} = _resp), do: state\n\n  defp build_client_view(%ClientProxy{} = proxy) do\n    %{id: id, ref: ref, topic: topic, module: module, endpoint: endpoint, pid: pid} = proxy\n    %View{id: id, pid: pid, proxy: {ref, topic, self()}, module: module, endpoint: endpoint}\n  end\n\n  defp mount_view(state, view, url, redirect_url) do\n    ref = make_ref()\n\n    case start_supervised_channel(state, view, ref, url, redirect_url) do\n      {:ok, pid} ->\n        mon_ref = Process.monitor(pid)\n\n        receive do\n          {^ref, {:ok, %{rendered: rendered} = resp}} ->\n            Process.demonitor(mon_ref, [:flush])\n            {%{view | pid: pid}, Diff.merge_diff(%{}, rendered), resp}\n\n          {^ref, {:error, %{live_redirect: opts}}} ->\n            throw(stop_redirect(state, view.topic, {:live_redirect, opts}))\n\n          {^ref, {:error, %{redirect: opts}}} ->\n            throw(stop_redirect(state, view.topic, {:redirect, opts}))\n\n          {^ref, {:error, %{reason: reason}}} when reason in ~w(stale unauthorized) ->\n            redir_to =\n              case redirect_url do\n                %URI{} = uri -> URI.to_string(uri)\n                nil -> url\n              end\n\n            throw(stop_redirect(state, view.topic, {:redirect, %{to: redir_to}}))\n\n          {^ref, {:error, reason}} ->\n            throw({:stop, reason, state})\n\n          {:DOWN, ^mon_ref, _, _, reason} ->\n            throw({:stop, reason, state})\n        end\n\n      {:error, reason} ->\n        throw({:stop, reason, state})\n    end\n  end\n\n  defp start_supervised_channel(state, view, ref, url, redirect_url) do\n    socket = %Phoenix.Socket{\n      transport_pid: self(),\n      serializer: __MODULE__,\n      channel: view.module,\n      endpoint: view.endpoint,\n      private: %{connect_info: Map.put_new(view.connect_info, :session, state.session)},\n      topic: view.topic,\n      join_ref: state.join_ref\n    }\n\n    params = %{\n      \"session\" => view.session_token,\n      \"static\" => view.static_token,\n      \"params\" => Map.put(view.connect_params, \"_mounts\", 0),\n      \"caller\" => state.caller\n    }\n\n    params = put_non_nil(params, \"url\", url)\n    params = put_non_nil(params, \"redirect\", redirect_url)\n\n    from = {self(), ref}\n\n    spec = %{\n      id: make_ref(),\n      start: {Phoenix.LiveView.Channel, :start_link, [{view.endpoint, from}]},\n      restart: :temporary\n    }\n\n    with {:ok, pid} <- Supervisor.start_child(state.test_supervisor, spec) do\n      send(pid, {Phoenix.Channel, params, from, socket})\n      {:ok, pid}\n    end\n  end\n\n  defp put_non_nil(%{} = map, _key, nil), do: map\n  defp put_non_nil(%{} = map, key, val), do: Map.put(map, key, val)\n\n  def handle_info({:sync_children, topic, from}, state) do\n    view = fetch_view_by_topic!(state, topic)\n\n    children =\n      Enum.flat_map(view.children, fn {id, _session} ->\n        case fetch_view_by_id(state, id) do\n          {:ok, child} -> [build_client_view(child)]\n          :error -> []\n        end\n      end)\n\n    GenServer.reply(from, {:ok, children})\n    {:noreply, state}\n  end\n\n  def handle_info({:sync_render_element, operation, topic_or_element, from}, state) do\n    view = fetch_view_by_topic!(state, proxy_topic(topic_or_element))\n\n    render_with_selector = fn topic_or_element ->\n      {state, root} = root(state, view)\n      result = select_node(root, topic_or_element)\n\n      {state,\n       case {operation, result} do\n         {:find_element, {:ok, node}} -> {:ok, node}\n         {:find_element, {:error, _, message}} -> {:raise, ArgumentError.exception(message)}\n         {:has_element?, {:error, :none, _}} -> {:ok, false}\n         {:has_element?, _} -> {:ok, true}\n       end}\n    end\n\n    {state, reply} =\n      case topic_or_element do\n        %Element{} = element ->\n          render_with_selector.(element)\n\n        {_, _, nil} ->\n          {state, {:ok, TreeDOM.by_id!(state.html_tree, view.id)}}\n\n        {_, _, selector} when not is_nil(selector) ->\n          render_with_selector.(selector)\n      end\n\n    GenServer.reply(from, reply)\n    {:noreply, state}\n  end\n\n  def handle_info({:sync_render_event, topic_or_element, type, value, from}, state) do\n    {state, result} =\n      case topic_or_element do\n        {topic, event, selector} ->\n          view = fetch_view_by_topic!(state, topic)\n          {state, root} = root(state, view)\n\n          cids =\n            if selector do\n              DOM.targets_from_selector(root, selector)\n            else\n              [nil]\n            end\n\n          {values, upload} =\n            case value do\n              %Upload{} = upload -> {%{}, upload}\n              _ -> {stringify(value, & &1), nil}\n            end\n\n          {state, [{:event, view, cids, event, values, upload}]}\n\n        %Element{} = element ->\n          view = fetch_view_by_topic!(state, proxy_topic(element))\n          {state, root} = root(state, view)\n\n          result =\n            with {:ok, node} <- select_node(root, element),\n                 :ok <- maybe_enabled(type, node, element),\n                 {:ok, event_or_js, fallback} <- maybe_event(type, node, element),\n                 {:ok, dom_values} <- maybe_values(type, root, node, element) do\n              case maybe_js_commands(event_or_js, root, view, node, value, dom_values) do\n                [] when fallback != [] ->\n                  fallback\n\n                [] ->\n                  {:error, :invalid,\n                   \"no push or navigation command found within JS commands: #{event_or_js}\"}\n\n                events ->\n                  events\n              end\n            end\n\n          {state, result}\n      end\n\n    case result do\n      [_ | _] = events ->\n        last_event = length(events) - 1\n\n        events\n        |> Enum.with_index()\n        |> Enum.reduce({:noreply, state}, fn\n          {event, event_index}, {:noreply, state} ->\n            case event do\n              {:event, view, cids, event, values, upload} ->\n                last_cid = length(cids) - 1\n\n                state =\n                  cids\n                  |> Enum.with_index()\n                  |> Enum.reduce(state, fn {cid, cid_index}, acc ->\n                    {acc, root} = root(acc, view)\n\n                    payload =\n                      encode_payload(type, event, values)\n                      |> maybe_put_cid(cid)\n                      |> maybe_put_uploads(root, upload)\n\n                    push_with_callback(acc, from, view, \"event\", payload, fn reply, state ->\n                      if event_index == last_event and cid_index == last_cid do\n                        {:noreply, render_reply(reply, from, state)}\n                      else\n                        {:noreply, state}\n                      end\n                    end)\n                  end)\n\n                {:noreply, state}\n\n              {:patch, topic, path} ->\n                handle_call({:render_patch, topic, path}, from, state)\n\n              {:allow_upload, topic, ref} ->\n                handle_call({:render_allow_upload, topic, ref, value}, from, state)\n\n              {:upload_progress, topic, upload_ref} ->\n                payload = Map.put(value, \"ref\", upload_ref)\n                view = fetch_view_by_topic!(state, topic)\n                {:noreply, push_with_reply(state, from, view, \"progress\", payload)}\n\n              {:stop, topic, reason} ->\n                stop_redirect(state, topic, reason)\n            end\n\n          {_event, _event_index}, return ->\n            return\n        end)\n\n      {:error, _, message} ->\n        GenServer.reply(from, {:raise, ArgumentError.exception(message)})\n        {:noreply, state}\n    end\n  end\n\n  def handle_info(\n        %Phoenix.Socket.Message{\n          event: \"redirect\",\n          topic: _topic,\n          payload: %{to: _to} = opts\n        },\n        state\n      ) do\n    stop_redirect(state, state.root_view.topic, {:redirect, opts})\n  end\n\n  def handle_info(\n        %Phoenix.Socket.Message{\n          event: \"live_patch\",\n          topic: _topic,\n          payload: %{to: _to} = opts\n        },\n        state\n      ) do\n    send_patch(state, state.root_view.topic, opts)\n    {:noreply, state}\n  end\n\n  def handle_info(\n        %Phoenix.Socket.Message{\n          event: \"live_redirect\",\n          topic: _topic,\n          payload: %{to: _to} = opts\n        },\n        state\n      ) do\n    stop_redirect(state, state.root_view.topic, {:live_redirect, opts})\n  end\n\n  def handle_info(\n        %Phoenix.Socket.Message{\n          event: \"diff\",\n          topic: topic,\n          payload: diff\n        },\n        state\n      ) do\n    {:noreply, merge_rendered(state, topic, diff)}\n  end\n\n  def handle_info(%Phoenix.Socket.Reply{ref: ref} = reply, state) do\n    case fetch_reply(state, ref) do\n      {:ok, {_pid, _from, callback}} ->\n        case handle_reply(state, reply) do\n          {:ok, new_state} -> callback.(reply, drop_reply(new_state, ref))\n          other -> other\n        end\n\n      :error ->\n        case Map.fetch(state.dropped_replies, ref) do\n          {:ok, from} ->\n            from && GenServer.reply(from, {:ok, nil})\n            {:noreply, %{state | dropped_replies: Map.delete(state.dropped_replies, ref)}}\n\n          :error ->\n            {:noreply, state}\n        end\n    end\n  end\n\n  def handle_info({:DOWN, _ref, :process, pid, reason}, state) do\n    case fetch_view_by_pid(state, pid) do\n      {:ok, _view} ->\n        {:stop, reason, state}\n\n      :error ->\n        {:noreply, state}\n    end\n  end\n\n  def handle_info({:socket_close, pid, reason}, state) do\n    case fetch_view_by_pid(state, pid) do\n      {:ok, view} ->\n        {:noreply, drop_view_by_id(state, view.id, reason)}\n\n      :error ->\n        {:noreply, state}\n    end\n  end\n\n  def handle_info({:test_error, type, message}, state) do\n    case {configured_test_warning(type, state.on_error), default_test_error(type)} do\n      {nil, :raise} ->\n        raise \"\"\"\n        #{String.trim(message)}\n\n        You can change this error by setting\n\n            config :phoenix_live_view, :test_warnings,\n              #{inspect(type)}: :warn # can be one of :warn, :raise, or :ignore\n\n        See the `Phoenix.LiveViewTest` documentation for more details.\n        \"\"\"\n\n      {nil, :warn} ->\n        IO.warn(\n          \"\"\"\n          #{String.trim(message)}\n\n          You can change this warning by setting\n\n              config :phoenix_live_view, :test_warnings,\n                #{inspect(type)}: :raise # can be one of :warn, :raise, or :ignore\n\n          See the `Phoenix.LiveViewTest` documentation for more details.\n          \"\"\",\n          state.start_location\n        )\n\n      {:raise, _} ->\n        raise message\n\n      {:warn, _} ->\n        IO.warn(message, state.start_location)\n\n      {:ignore, _} ->\n        :noop\n    end\n\n    {:noreply, state}\n  end\n\n  defp configured_test_warning(type, on_error) do\n    case on_error do\n      nil ->\n        Phoenix.LiveViewTest.configured_test_warning(type)\n\n      generic when is_atom(generic) ->\n        generic\n\n      config when is_list(config) ->\n        Keyword.get_lazy(config, type, fn ->\n          Phoenix.LiveViewTest.configured_test_warning(type)\n        end)\n    end\n  end\n\n  # We only warn for missing form IDs, because it is not necessarily an error\n  # Note: remember to add a clause handling :ignore if we default some checks\n  # to :ignore in the future.\n  defp default_test_error(:missing_form_id), do: :warn\n  defp default_test_error(_), do: :raise\n\n  def handle_call({:upload_progress, from, %Element{} = el, entry_ref, progress, cid}, _, state) do\n    payload = maybe_put_cid(%{\"entry_ref\" => entry_ref, \"progress\" => progress}, cid)\n    topic = proxy_topic(el)\n    %{pid: pid} = fetch_view_by_topic!(state, topic)\n\n    ping!(pid, state, fn ->\n      send(self(), {:sync_render_event, el, :upload_progress, payload, from})\n      {:reply, :ok, state}\n    end)\n  end\n\n  def handle_call(:page_title, _from, %{page_title: :unset} = state) do\n    state = %{state | page_title: root_page_title(state.html_tree)}\n    {:reply, {:ok, state.page_title}, state}\n  end\n\n  def handle_call(:page_title, _from, state) do\n    {:reply, {:ok, state.page_title}, state}\n  end\n\n  def handle_call(:url, _from, state) do\n    {:reply, {:ok, state.url}, state}\n  end\n\n  def handle_call(:html, _from, state) do\n    {:reply, {:ok, {state.html_tree, state.static_path}}, state}\n  end\n\n  def handle_call(:root_view, _from, state) do\n    {:reply, {state.session, state.root_view}, state}\n  end\n\n  def handle_call({:live_children, topic}, from, state) do\n    view = fetch_view_by_topic!(state, topic)\n\n    ping!(view.pid, state, fn ->\n      send(self(), {:sync_children, view.topic, from})\n      {:noreply, state}\n    end)\n  end\n\n  def handle_call({:render_element, operation, topic_or_element}, from, state) do\n    topic = proxy_topic(topic_or_element)\n    %{pid: pid} = fetch_view_by_topic!(state, topic)\n\n    ping!(pid, state, fn ->\n      send(self(), {:sync_render_element, operation, topic_or_element, from})\n      {:noreply, state}\n    end)\n  end\n\n  def handle_call({:async_pids, topic_or_element}, _from, state) do\n    topic = proxy_topic(topic_or_element)\n    %{pid: pid} = fetch_view_by_topic!(state, topic)\n    {:reply, Phoenix.LiveView.Channel.async_pids(pid), state}\n  end\n\n  def handle_call({:render_event, topic_or_element, type, value}, from, state) do\n    topic = proxy_topic(topic_or_element)\n    %{pid: pid} = fetch_view_by_topic!(state, topic)\n\n    ping!(pid, state, fn ->\n      send(self(), {:sync_render_event, topic_or_element, type, value, from})\n      {:noreply, state}\n    end)\n  end\n\n  def handle_call({:render_patch, topic, path}, from, state) do\n    view = fetch_view_by_topic!(state, topic)\n    path = URI.merge(state.root_view.uri, URI.parse(path)) |> to_string()\n    state = push_with_reply(state, from, view, \"live_patch\", %{\"url\" => path})\n    send_patch(state, state.root_view.topic, %{to: path})\n    {:noreply, state}\n  end\n\n  def handle_call({:render_allow_upload, topic, ref, {entries, cid}}, from, state) do\n    view = fetch_view_by_topic!(state, topic)\n    payload = maybe_put_cid(%{\"ref\" => ref, \"entries\" => entries}, cid)\n\n    new_state =\n      push_with_callback(state, from, view, \"allow_upload\", payload, fn reply, state ->\n        GenServer.reply(from, {:ok, reply.payload})\n        {:noreply, state}\n      end)\n\n    {:noreply, new_state}\n  end\n\n  def handle_call({:stop, reason}, _from, state) do\n    %{caller: {pid, _}} = state\n    Process.unlink(pid)\n    {:stop, :ok, reason, state}\n  end\n\n  def handle_call({:sync_with_root, topic}, _from, state) do\n    view = fetch_view_by_topic!(state, topic)\n\n    ping!(view.pid, state, fn ->\n      # if we target a child view, we ping the root view as well\n      if view.pid !== state.root_view.pid do\n        ping!(state.root_view.pid, state, fn ->\n          {:reply, :ok, state}\n        end)\n      else\n        {:reply, :ok, state}\n      end\n    end)\n  end\n\n  def handle_call({:get_lazy, %Element{} = element}, _from, state) do\n    view = fetch_view_by_topic!(state, proxy_topic(element))\n    {state, root} = root(state, view.id)\n    {:reply, {:ok, root}, state}\n  end\n\n  def handle_call({:get_lazy, id}, _from, state) do\n    {state, root} = root(state, id)\n    {:reply, {:ok, root}, state}\n  end\n\n  defp ping!(pid, state, fun) do\n    try do\n      # We send a message to the channel for synchronization purposes.\n      #\n      # It can happen that the channel shuts down before the ping is processed,\n      # or even that the channel is already dead, therefore we catch the exit\n      # and let it be handled by the regular handle_info callback for\n      # the DOWN message.\n      Phoenix.LiveView.Channel.ping(pid)\n    catch\n      :exit, _ ->\n        receive do\n          {:DOWN, _ref, :process, ^pid, _reason} = down ->\n            handle_info(down, state)\n        end\n    else\n      :ok -> fun.()\n    end\n  end\n\n  defp drop_view_by_id(state, id, reason) do\n    {:ok, view} = fetch_view_by_id(state, id)\n    state = push(state, view, \"phx_leave\", %{})\n\n    state =\n      Enum.reduce(view.children, state, fn {child_id, _child_session}, acc ->\n        drop_view_by_id(acc, child_id, reason)\n      end)\n\n    flush_replies(\n      %{\n        state\n        | ids: Map.delete(state.ids, view.id),\n          views: Map.delete(state.views, view.topic),\n          pids: Map.delete(state.pids, view.pid)\n      },\n      view.pid\n    )\n  end\n\n  defp flush_replies(state, pid) do\n    Enum.reduce(state.replies, state, fn\n      {ref, {^pid, _from, _callback}}, acc -> drop_reply(acc, ref)\n      {_ref, {_pid, _from, _callback}}, acc -> acc\n    end)\n  end\n\n  defp fetch_reply(state, ref) do\n    Map.fetch(state.replies, ref)\n  end\n\n  defp put_reply(state, ref, pid, from, callback) do\n    %{state | replies: Map.put(state.replies, ref, {pid, from, callback})}\n  end\n\n  defp drop_reply(state, ref) do\n    dropped_replies =\n      case Map.fetch(state.replies, ref) do\n        {:ok, {_pid, from, _callback}} -> Map.put(state.dropped_replies, ref, from)\n        :error -> state.dropped_replies\n      end\n\n    %{\n      state\n      | replies: Map.delete(state.replies, ref),\n        dropped_replies: dropped_replies\n    }\n  end\n\n  defp put_child(state, %ClientProxy{} = parent, id, session) do\n    update_in(state.views[parent.topic], fn %ClientProxy{} = parent ->\n      %{parent | children: [{id, session} | parent.children]}\n    end)\n  end\n\n  defp drop_child(state, %ClientProxy{} = parent, id, reason) do\n    update_in(state.views[parent.topic], fn %ClientProxy{} = parent ->\n      new_children = Enum.reject(parent.children, fn {cid, _session} -> id == cid end)\n      %{parent | children: new_children}\n    end)\n    |> drop_view_by_id(id, reason)\n  end\n\n  defp verify_session(%ClientProxy{} = view) do\n    Phoenix.LiveView.Session.verify_session(\n      view.endpoint,\n      view.topic,\n      view.session_token,\n      view.static_token\n    )\n  end\n\n  defp put_view(state, %ClientProxy{pid: pid} = view, rendered) do\n    {:ok, %Phoenix.LiveView.Session{view: module}} = verify_session(view)\n    new_view = %{view | module: module, proxy: self(), pid: pid, rendered: rendered}\n    Process.monitor(pid)\n\n    rendered = maybe_push_events(rendered, state)\n\n    patch_view(\n      %{\n        state\n        | views: Map.put(state.views, new_view.topic, new_view),\n          pids: Map.put(state.pids, pid, new_view.topic),\n          ids: Map.put(state.ids, new_view.id, new_view.topic)\n      },\n      view,\n      Diff.render_diff(rendered),\n      rendered.streams\n    )\n  end\n\n  defp patch_view(state, view, child_html, streams) do\n    result =\n      TreeDOM.patch_id(view.id, state.html_tree, child_html, streams, fn type, msg ->\n        send(self(), {:test_error, type, msg})\n      end)\n\n    # IO.puts(\"PATCH VIEW #{view.id}\")\n    # dbg(child_html)\n\n    case result do\n      {new_html, [_ | _] = will_destroy_cids} ->\n        topic = view.topic\n        state = %{state | html_tree: new_html, lazy_cache: %{}}\n        payload = %{\"cids\" => will_destroy_cids}\n\n        push_with_callback(state, nil, view, \"cids_will_destroy\", payload, fn _, state ->\n          still_there_cids = TreeDOM.component_ids(view.id, state.html_tree)\n          payload = %{\"cids\" => Enum.reject(will_destroy_cids, &(&1 in still_there_cids))}\n\n          state =\n            push_with_callback(state, nil, view, \"cids_destroyed\", payload, fn reply, state ->\n              cids = reply.payload.cids\n              {:noreply, update_in(state.views[topic].rendered, &Diff.drop_cids(&1, cids))}\n            end)\n\n          {:noreply, state}\n        end)\n\n      {new_html, [] = _deleted_cids} ->\n        %{state | html_tree: new_html, lazy_cache: %{}}\n    end\n  end\n\n  defp stop_redirect(%{caller: {pid, _}} = state, topic, {_kind, opts} = reason)\n       when is_binary(topic) do\n    send_caller(state, {:redirect, topic, opts})\n    Process.unlink(pid)\n    {:stop, {:shutdown, reason}, state}\n  end\n\n  defp fetch_view_by_topic!(state, topic), do: Map.fetch!(state.views, topic)\n  defp fetch_view_by_topic(state, topic), do: Map.fetch(state.views, topic)\n\n  defp fetch_view_by_pid(state, pid) when is_pid(pid) do\n    with {:ok, topic} <- Map.fetch(state.pids, pid) do\n      fetch_view_by_topic(state, topic)\n    end\n  end\n\n  defp fetch_view_by_id(state, id) do\n    with {:ok, topic} <- Map.fetch(state.ids, id) do\n      fetch_view_by_topic(state, topic)\n    end\n  end\n\n  defp render_reply(reply, from, state) do\n    case fetch_view_by_topic(state, reply.topic) do\n      {:ok, view} ->\n        GenServer.reply(\n          from,\n          {:ok, state.html_tree |> TreeDOM.inner_html!(view.id) |> TreeDOM.to_html()}\n        )\n\n        state\n\n      :error ->\n        state\n    end\n  end\n\n  defp merge_rendered(state, topic, %{diff: diff}), do: merge_rendered(state, topic, diff)\n\n  defp merge_rendered(%{html_tree: html_before} = state, topic, %{} = diff) do\n    {diff, state} =\n      diff\n      |> maybe_push_events(state)\n      |> maybe_push_reply(state)\n      |> maybe_push_title(state)\n\n    if diff == %{} do\n      state\n    else\n      case fetch_view_by_topic(state, topic) do\n        {:ok, %ClientProxy{} = view} ->\n          rendered = Diff.merge_diff(view.rendered, diff)\n          new_view = %{view | rendered: rendered}\n          streams = Diff.extract_streams(rendered, rendered.streams)\n\n          %{state | views: Map.update!(state.views, topic, fn _ -> new_view end)}\n          |> patch_view(new_view, Diff.render_diff(rendered), streams)\n          |> detect_added_or_removed_children(new_view, html_before, streams)\n\n        :error ->\n          state\n      end\n    end\n  end\n\n  defp detect_added_or_removed_children(state, view, html_before, streams) do\n    new_state = recursive_detect_added_or_removed_children(state, view, html_before, streams)\n    {:ok, new_view} = fetch_view_by_topic(new_state, view.topic)\n\n    ids_after =\n      new_state.html_tree\n      |> TreeDOM.reverse_filter(&TreeDOM.attribute(&1, \"data-phx-session\"))\n      |> TreeDOM.all_attributes(\"id\")\n      |> MapSet.new()\n\n    Enum.reduce(new_view.children, new_state, fn {id, _session}, acc ->\n      if id in ids_after do\n        acc\n      else\n        drop_child(acc, new_view, id, {:shutdown, :left})\n      end\n    end)\n  end\n\n  defp recursive_detect_added_or_removed_children(state, view, html_before, streams) do\n    state.html_tree\n    |> TreeDOM.inner_html!(view.id)\n    |> TreeDOM.find_live_views()\n    |> Enum.reduce(state, fn {id, session, static}, acc ->\n      case fetch_view_by_id(acc, id) do\n        {:ok, view} ->\n          streams = Diff.extract_streams(view.rendered, streams)\n          patch_view(acc, view, TreeDOM.inner_html!(html_before, view.id), streams)\n\n        :error ->\n          static = static || Map.get(state.root_view.child_statics, id)\n          child_view = build_child(view, id: id, session_token: session, static_token: static)\n\n          {child_view, rendered, _resp} = mount_view(acc, child_view, nil, nil)\n          streams = Diff.extract_streams(rendered, streams)\n\n          acc\n          |> put_view(child_view, rendered)\n          |> put_child(view, id, child_view.session_token)\n          |> recursive_detect_added_or_removed_children(child_view, acc.html_tree, streams)\n      end\n    end)\n  end\n\n  defp send_caller(%{caller: {pid, ref}}, msg) when is_pid(pid) do\n    send(pid, {ref, msg})\n  end\n\n  defp send_patch(state, topic, %{to: to} = opts) do\n    relative =\n      case URI.parse(to) do\n        %{path: nil} -> \"\"\n        %{path: path, query: nil} -> path\n        %{path: path, query: query} -> path <> \"?\" <> query\n      end\n\n    send_caller(state, {:patch, topic, %{opts | to: relative}})\n  end\n\n  defp push(state, view, event, payload) do\n    ref = state.ref + 1\n\n    message = %Phoenix.Socket.Message{\n      join_ref: state.join_ref,\n      topic: view.topic,\n      event: event,\n      payload: payload,\n      ref: to_string(ref)\n    }\n\n    send(view.pid, message)\n\n    %{state | ref: ref}\n  end\n\n  defp push_with_reply(state, from, view, event, payload) do\n    push_with_callback(state, from, view, event, payload, fn reply, state ->\n      {:noreply, render_reply(reply, from, state)}\n    end)\n  end\n\n  defp handle_reply(state, reply) do\n    %{payload: payload, topic: topic} = reply\n\n    new_state =\n      case payload do\n        %{diff: diff} -> merge_rendered(state, topic, diff)\n        %{} = diff -> merge_rendered(state, topic, diff)\n      end\n\n    case payload do\n      %{live_redirect: %{to: _to} = opts} ->\n        stop_redirect(new_state, topic, {:live_redirect, opts})\n\n      %{live_patch: %{to: _to} = opts} ->\n        send_patch(new_state, topic, opts)\n        {:ok, new_state}\n\n      %{redirect: %{to: _to} = opts} ->\n        stop_redirect(new_state, topic, {:redirect, opts})\n\n      %{} ->\n        {:ok, new_state}\n    end\n  end\n\n  defp push_with_callback(state, from, view, event, payload, callback) do\n    ref = to_string(state.ref + 1)\n\n    state\n    |> push(view, event, payload)\n    |> put_reply(ref, view.pid, from, callback)\n  end\n\n  defp build_child(%ClientProxy{ref: ref, proxy: proxy, endpoint: endpoint}, attrs) do\n    attrs_with_defaults =\n      Keyword.merge(attrs,\n        ref: ref,\n        proxy: proxy,\n        endpoint: endpoint,\n        topic: \"lv:#{Keyword.fetch!(attrs, :id)}\"\n      )\n\n    struct!(__MODULE__, attrs_with_defaults)\n  end\n\n  ## Element helpers\n\n  defp encode_payload(type, event, value) when type in [:change, :submit],\n    do: %{\n      \"type\" => \"form\",\n      \"event\" => event,\n      \"value\" => Plug.Conn.Query.encode(value)\n    }\n\n  defp encode_payload(type, event, value),\n    do: %{\n      \"type\" => Atom.to_string(type),\n      \"event\" => event,\n      \"value\" => value\n    }\n\n  defp proxy_topic({topic, _, _}) when is_binary(topic), do: topic\n  defp proxy_topic(%{proxy: {_ref, topic, _pid}}), do: topic\n\n  defp root(state, %ClientProxy{id: id}), do: root(state, id)\n\n  defp root(state, id) when is_binary(id) do\n    case state.lazy_cache do\n      %{^id => lazy} ->\n        {state, lazy}\n\n      _ ->\n        view_tree = TreeDOM.by_id!(state.html_tree, id)\n        lazy = DOM.to_lazy(List.wrap(view_tree))\n        lazy_cache = Map.put(state.lazy_cache, id, lazy)\n        {%{state | lazy_cache: lazy_cache}, lazy}\n    end\n  end\n\n  defp select_node(root, %Element{selector: selector, text_filter: nil}) do\n    select_node(root, selector)\n  end\n\n  defp select_node(root, %Element{selector: selector, text_filter: text_filter}) do\n    nodes =\n      root\n      |> DOM.child_nodes()\n      |> DOM.all(selector)\n      |> DOM.to_tree()\n\n    select_node_by_text(root, nodes, text_filter, selector)\n  end\n\n  defp select_node(root, selector) when is_binary(selector) do\n    case root\n         |> DOM.child_nodes()\n         |> DOM.maybe_one(selector) do\n      {:ok, result} -> {:ok, DOM.to_tree(result) |> hd()}\n      error -> error\n    end\n  end\n\n  defp select_node_by_text(root, nodes, text_filter, selector) do\n    filtered_nodes = Enum.filter(nodes, &(TreeDOM.to_text(&1) =~ text_filter))\n\n    case {nodes, filtered_nodes} do\n      {_, [filtered_node]} ->\n        {:ok, filtered_node}\n\n      {[], _} ->\n        {:error, :none,\n         \"selector #{inspect(selector)} did not return any element within: \\n\\n\" <>\n           DOM.to_html(root)}\n\n      {[node], []} ->\n        {:error, :none,\n         \"selector #{inspect(selector)} did not match text filter #{inspect(text_filter)}, \" <>\n           \"got: \\n\\n#{TreeDOM.inspect_html(node)}\"}\n\n      {_, []} ->\n        {:error, :none,\n         \"selector #{inspect(selector)} returned #{length(nodes)} elements \" <>\n           \"but none matched the text filter #{inspect(text_filter)}: \\n\\n\" <>\n           TreeDOM.inspect_html(nodes)}\n\n      {_, _} ->\n        {:error, :many,\n         \"selector #{inspect(selector)} returned #{length(nodes)} elements \" <>\n           \"and #{length(filtered_nodes)} of them matched the text filter #{inspect(text_filter)}: \\n\\n \" <>\n           TreeDOM.inspect_html(filtered_nodes)}\n    end\n  end\n\n  defp maybe_event(:upload_progress, node, %Element{} = element) do\n    if ref = TreeDOM.attribute(node, @data_phx_upload_ref) do\n      [{:upload_progress, proxy_topic(element), ref}]\n    else\n      {:error, :invalid,\n       \"element selected by #{inspect(element.selector)} does not have a #{@data_phx_upload_ref} attribute\"}\n    end\n  end\n\n  defp maybe_event(:allow_upload, node, %Element{} = element) do\n    if ref = TreeDOM.attribute(node, @data_phx_upload_ref) do\n      [{:allow_upload, proxy_topic(element), ref}]\n    else\n      {:error, :invalid,\n       \"element selected by #{inspect(element.selector)} does not have a #{@data_phx_upload_ref} attribute\"}\n    end\n  end\n\n  defp maybe_event(:hook, node, %Element{event: event} = element) do\n    true = is_binary(event)\n\n    cond do\n      TreeDOM.attribute(node, \"phx-hook\") ->\n        if TreeDOM.attribute(node, \"id\") do\n          {:ok, event, []}\n        else\n          {:error, :invalid,\n           \"element selected by #{inspect(element.selector)} for phx-hook does not have an ID\"}\n        end\n\n      TreeDOM.attribute(node, \"phx-viewport-top\") ||\n          TreeDOM.attribute(node, \"phx-viewport-bottom\") ->\n        {:ok, event, []}\n\n      true ->\n        {:error, :invalid,\n         \"element selected by #{inspect(element.selector)} does not have phx-hook attribute\"}\n    end\n  end\n\n  defp maybe_event(:click, {\"a\", _, _} = node, element) do\n    # If there is a phx-click, that's what we will use, otherwise fallback to href\n    fallback =\n      if to = TreeDOM.attribute(node, \"href\") do\n        case TreeDOM.attribute(node, \"data-phx-link\") do\n          \"patch\" ->\n            [{:patch, proxy_topic(element), to}]\n\n          \"redirect\" ->\n            kind = TreeDOM.attribute(node, \"data-phx-link-state\") || \"push\"\n            opts = %{to: to, kind: String.to_atom(kind)}\n            [{:stop, proxy_topic(element), {:live_redirect, opts}}]\n\n          nil ->\n            [{:stop, proxy_topic(element), {:redirect, %{to: to}}}]\n        end\n      else\n        []\n      end\n\n    cond do\n      event = TreeDOM.attribute(node, \"phx-click\") ->\n        {:ok, event, fallback}\n\n      fallback != [] ->\n        fallback\n\n      true ->\n        message =\n          \"clicked link selected by #{inspect(element.selector)} does not have phx-click or href attributes\"\n\n        {:error, :invalid, message}\n    end\n  end\n\n  defp maybe_event(type, node, element) when type in [:keyup, :keydown] do\n    cond do\n      event = TreeDOM.attribute(node, \"phx-#{type}\") ->\n        {:ok, event, []}\n\n      event = TreeDOM.attribute(node, \"phx-window-#{type}\") ->\n        {:ok, event, []}\n\n      true ->\n        {:error, :invalid,\n         \"element selected by #{inspect(element.selector)} does not have \" <>\n           \"phx-#{type} or phx-window-#{type} attributes\"}\n    end\n  end\n\n  defp maybe_event(type, node, element) do\n    if event = TreeDOM.attribute(node, \"phx-#{type}\") do\n      {:ok, event, []}\n    else\n      {:error, :invalid,\n       \"element selected by #{inspect(element.selector)} does not have phx-#{type} attribute\"}\n    end\n  end\n\n  defp maybe_js_decode(\"[\" <> _ = encoded_js), do: Phoenix.json_library().decode!(encoded_js)\n  defp maybe_js_decode(event), do: [[\"push\", %{\"event\" => event}]]\n\n  defp maybe_js_commands(event_or_js, root, view, node, value, dom_values) do\n    event_or_js\n    |> maybe_js_decode()\n    |> Enum.flat_map(fn\n      [\"push\", %{\"event\" => event} = args] ->\n        js_values = args[\"value\"] || %{}\n        js_target_selector = args[\"target\"]\n        event_values = Map.merge(dom_values, js_values)\n\n        {values, uploads} =\n          case value do\n            %Upload{} = upload -> {event_values, upload}\n            other -> {deep_merge(event_values, stringify(other, & &1)), nil}\n          end\n\n        js_targets = DOM.targets_from_selector(root, js_target_selector)\n        node_targets = DOM.targets_from_node(root, node)\n\n        targets =\n          case {js_targets, node_targets} do\n            {[nil], right} -> right\n            {left, [nil]} -> left\n            {left, right} -> Enum.uniq(left ++ right)\n          end\n\n        [{:event, view, targets, event, values, uploads}]\n\n      [\"patch\", %{\"href\" => to}] ->\n        [{:patch, view.topic, to}]\n\n      [\"navigate\", %{\"href\" => to, \"replace\" => true}] ->\n        [{:stop, view.topic, {:live_redirect, %{to: to, kind: :replace}}}]\n\n      [\"navigate\", %{\"href\" => to}] ->\n        [{:stop, view.topic, {:live_redirect, %{to: to, kind: :push}}}]\n\n      _ ->\n        []\n    end)\n  end\n\n  defp maybe_enabled(_type, {tag, _, _}, %{form_data: form_data})\n       when tag != \"form\" and form_data != nil do\n    {:error, :invalid,\n     \"a form element was given but the selected node is not a form, got #{inspect(tag)}}\"}\n  end\n\n  defp maybe_enabled(type, node, element) do\n    if TreeDOM.attribute(node, \"disabled\") do\n      {:error, :invalid,\n       \"cannot #{type} element #{inspect(element.selector)} because it is disabled\"}\n    else\n      :ok\n    end\n  end\n\n  defp maybe_values(:hook, _root, _node, _element), do: {:ok, %{}}\n\n  defp maybe_values(type, root, {tag, _, _} = node, element)\n       when type in [:change, :submit] do\n    cond do\n      tag == \"form\" ->\n        value_inputs = DOM.all_value_inputs(node, root)\n        defaults = DOM.collect_form_values(node, root, fn defaults -> defaults end)\n\n        lazy_submitter =\n          case TreeDOM.attribute(node, \"id\") do\n            nil ->\n              # to collect the submitter by selector,\n              # need to convert the tree to a lazy here :(\n              fn -> DOM.to_lazy([node]) end\n\n            id ->\n              # a lazy function that returns a lazy node with all form inputs\n              # that could be the submitter to collect the submitter by selector\n              fn -> DOM.all(root, ~s<\n                ##{id} :is(input, button):not([form]:not([form=\"#{id}\"])),\n                :is(input, button)[form=\"#{id}\"]\n              >) end\n          end\n\n        with {:ok, defaults} <-\n               maybe_submitter(defaults, type, lazy_submitter, element),\n             {:ok, value} <-\n               fill_in_map(Enum.to_list(element.form_data || %{}), \"\", value_inputs, []) do\n          {:ok,\n           defaults\n           |> Query.decode_done()\n           |> deep_merge(TreeDOM.all_values(node))\n           |> deep_merge(value)}\n        else\n          {:error, _, _} = error -> error\n        end\n\n      type == :change and tag in ~w(input select textarea) ->\n        {:ok, DOM.collect_input_values(node)}\n\n      true ->\n        {:error, :invalid, \"phx-#{type} is only allowed in forms, got #{inspect(tag)}\"}\n    end\n  end\n\n  defp maybe_values(_type, _root, node, _element) do\n    {:ok, TreeDOM.all_values(node)}\n  end\n\n  defp deep_merge(%{} = target, %{} = source),\n    do: Map.merge(target, source, fn _, t, s -> deep_merge(t, s) end)\n\n  defp deep_merge(_target, source),\n    do: source\n\n  defp maybe_submitter(defaults, :submit, lazy, %Element{meta: %{submitter: element}}) do\n    base = lazy.()\n\n    case DOM.maybe_one(base, element.selector) do\n      {:ok, node} -> collect_submitter(node, base, element, defaults)\n      {:error, _, msg} -> {:error, :invalid, \"invalid form submitter, \" <> msg}\n    end\n  end\n\n  defp maybe_submitter(defaults, _, _, _), do: {:ok, defaults}\n\n  defp collect_submitter(node, base, element, defaults) do\n    name = DOM.attribute(node, \"name\")\n\n    cond do\n      is_nil(name) ->\n        {:error, :invalid,\n         \"form submitter selected by #{inspect(element.selector)} must have a name\"}\n\n      submitter?(node) and is_nil(DOM.attribute(node, \"disabled\")) ->\n        {:ok, Plug.Conn.Query.decode_each({name, DOM.attribute(node, \"value\")}, defaults)}\n\n      true ->\n        {:error, :invalid,\n         \"could not find non-disabled submit input or button with name #{inspect(name)} within:\\n\\n\" <>\n           DOM.to_html(base)}\n    end\n  end\n\n  defp submitter?(node) do\n    case DOM.tag(node) do\n      \"input\" ->\n        DOM.attribute(node, \"type\") == \"submit\"\n\n      \"button\" ->\n        DOM.attribute(node, \"type\") in [\"submit\", nil]\n\n      _ ->\n        false\n    end\n  end\n\n  defp maybe_push_events(diff, state) do\n    case diff do\n      %{@events => events} ->\n        for [name, payload] <- events, do: send_caller(state, {:push_event, name, payload})\n        Map.delete(diff, @events)\n\n      %{} ->\n        diff\n    end\n  end\n\n  defp maybe_push_reply(diff, state) do\n    case diff do\n      %{@reply => reply} ->\n        send_caller(state, {:reply, reply})\n        Map.delete(diff, @reply)\n\n      %{} ->\n        diff\n    end\n  end\n\n  defp maybe_push_title(diff, state) do\n    case diff do\n      %{@title => title} ->\n        escaped_title =\n          title\n          |> Phoenix.HTML.html_escape()\n          |> Phoenix.HTML.safe_to_string()\n\n        {Map.delete(diff, @title), %{state | page_title: escaped_title}}\n\n      %{} ->\n        {diff, state}\n    end\n  end\n\n  defp fill_in_map([{key, value} | rest], prefix, node, acc) do\n    key = to_string(key)\n\n    case fill_in_type(value, fill_in_name(prefix, key), node) do\n      {:ok, value} -> fill_in_map(rest, prefix, node, [{key, value} | acc])\n      {:error, _, _} = error -> error\n    end\n  end\n\n  defp fill_in_map([], _prefix, _node, acc) do\n    {:ok, Map.new(acc)}\n  end\n\n  defp fill_in_type([{_, _} | _] = value, key, node), do: fill_in_map(value, key, node, [])\n  defp fill_in_type(%_{} = value, key, node), do: fill_in_value(value, key, node)\n  defp fill_in_type(%{} = value, key, node), do: fill_in_map(Map.to_list(value), key, node, [])\n  defp fill_in_type(value, key, node), do: fill_in_value(value, key, node)\n\n  @limited [\"select\", \"multiple select\", \"checkbox\", \"radio\", \"hidden\"]\n  @forbidden [\"submit\", \"image\"]\n\n  defp fill_in_value(non_string_value, name, node) do\n    value = stringify(non_string_value, &to_string/1)\n    name = if is_list(value), do: name <> \"[]\", else: name\n\n    {types, dom_values} =\n      node\n      |> TreeDOM.filter(fn node ->\n        TreeDOM.attribute(node, \"name\") == name and is_nil(TreeDOM.attribute(node, \"disabled\"))\n      end)\n      |> collect_values([], [])\n\n    limited? = Enum.all?(types, &(&1 in @limited))\n\n    cond do\n      calendar_value = calendar_value(types, non_string_value, name, node) ->\n        {:ok, calendar_value}\n\n      types == [] ->\n        {:error, :invalid,\n         \"could not find non-disabled input, select or textarea with name #{inspect(name)} within:\\n\\n\" <>\n           TreeDOM.to_html(TreeDOM.filter(node, fn node -> TreeDOM.attribute(node, \"name\") end))}\n\n      forbidden_type = Enum.find(types, &(&1 in @forbidden)) ->\n        {:error, :invalid,\n         \"cannot provide value to #{inspect(name)} because #{forbidden_type} inputs are never submitted\"}\n\n      forbidden_value = limited? && value |> List.wrap() |> Enum.find(&(&1 not in dom_values)) ->\n        {:error, :invalid,\n         \"value for #{hd(types)} #{inspect(name)} must be one of #{inspect(dom_values)}, \" <>\n           \"got: #{inspect(forbidden_value)}\"}\n\n      true ->\n        {:ok, value}\n    end\n  end\n\n  @calendar_fields ~w(year month day hour minute second)a\n\n  defp calendar_value([], %{calendar: _} = calendar_type, name, node) do\n    @calendar_fields\n    |> Enum.flat_map(fn field ->\n      string_field = Atom.to_string(field)\n\n      with value when not is_nil(value) <- Map.get(calendar_type, field),\n           {:ok, string_value} <- fill_in_value(value, name <> \"[\" <> string_field <> \"]\", node) do\n        [{string_field, string_value}]\n      else\n        _ -> []\n      end\n    end)\n    |> case do\n      [] -> nil\n      pairs -> Map.new(pairs)\n    end\n  end\n\n  defp calendar_value(_, _, _, _) do\n    nil\n  end\n\n  defp collect_values(nodes, types, values) do\n    {types, values} =\n      Enum.reduce(nodes, {types, values}, fn node, {types, values} ->\n        tag = TreeDOM.tag(node)\n        collect_values(tag, node, types, values)\n      end)\n\n    {types, Enum.reverse(values)}\n  end\n\n  defp collect_values(\"textarea\", _node, types, values) do\n    {[\"textarea\" | types], values}\n  end\n\n  defp collect_values(\"input\", node, types, values) do\n    type = TreeDOM.attribute(node, \"type\") || \"text\"\n\n    if type in [\"radio\", \"checkbox\", \"hidden\"] do\n      value = TreeDOM.attribute(node, \"value\") || DOM.default_value(type)\n      {[type | types], [value | values]}\n    else\n      {[type | types], values}\n    end\n  end\n\n  defp collect_values(\"select\", node, types, values) do\n    options =\n      node\n      |> TreeDOM.filter(&(TreeDOM.tag(&1) == \"option\"))\n      |> Enum.map(&(TreeDOM.attribute(&1, \"value\") || \"\"))\n\n    if TreeDOM.attribute(node, \"multiple\") do\n      {[\"multiple select\" | types], Enum.reverse(options, values)}\n    else\n      {[\"select\" | types], Enum.reverse(options, values)}\n    end\n  end\n\n  defp collect_values(_tag, _node, types, values) do\n    {types, values}\n  end\n\n  defp fill_in_name(\"\", name), do: name\n  defp fill_in_name(prefix, name), do: prefix <> \"[\" <> name <> \"]\"\n\n  defp maybe_put_uploads(payload, root, %Upload{} = upload) do\n    {:ok, node} = select_node(root, upload.element)\n    ref = TreeDOM.attribute(node, \"data-phx-upload-ref\")\n    Map.put(payload, \"uploads\", %{ref => upload.entries})\n  end\n\n  defp maybe_put_uploads(payload, _root, nil), do: payload\n\n  defp maybe_put_cid(payload, nil), do: payload\n  defp maybe_put_cid(payload, cid), do: Map.put(payload, \"cid\", cid)\n\n  defp root_page_title(root_html) do\n    case TreeDOM.filter(root_html, fn node -> TreeDOM.tag(node) == \"head\" end) do\n      [node] ->\n        case TreeDOM.filter(node, fn node -> TreeDOM.tag(node) == \"title\" end) do\n          [title] -> TreeDOM.to_text(title)\n          _ -> nil\n        end\n\n      _ ->\n        nil\n    end\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/test/diff.ex",
    "content": "defmodule Phoenix.LiveViewTest.Diff do\n  @moduledoc false\n\n  alias Phoenix.LiveViewTest.DOM\n\n  @components :c\n  @static :s\n  @keyed :k\n  @keyed_count :kc\n  @stream_id :stream\n  @template :p\n  @phx_component \"data-phx-component\"\n\n  def merge_diff(rendered, diff) do\n    old = Map.get(rendered, @components, %{})\n    # must extract streams from diff before we pop components\n    streams = extract_streams(diff, [])\n    {new, diff} = Map.pop(diff, @components)\n    rendered = deep_merge_diff(rendered, diff)\n\n    # If we have any component, we need to get the components\n    # sent by the diff and remove any link between components\n    # statics. We cannot let those links reside in the diff\n    # as components can be removed at any time.\n    rendered =\n      cond do\n        new ->\n          {acc, _} =\n            Enum.reduce(new, {old, %{}}, fn {cid, cdiff}, {acc, cache} ->\n              {value, cache} = find_component(cid, cdiff, old, new, cache)\n              {Map.put(acc, cid, value), cache}\n            end)\n\n          Map.put(rendered, @components, acc)\n\n        old != %{} ->\n          Map.put(rendered, @components, old)\n\n        true ->\n          rendered\n      end\n\n    Map.put(rendered, :streams, streams)\n  end\n\n  defp find_component(cid, cdiff, old, new, cache) do\n    case cache do\n      %{^cid => cached} ->\n        {cached, cache}\n\n      %{} ->\n        {res, cache} =\n          case cdiff do\n            %{@static => cid} when is_integer(cid) and cid > 0 ->\n              {res, cache} = find_component(cid, new[cid], old, new, cache)\n              {deep_merge_diff(res, Map.delete(cdiff, @static)), cache}\n\n            %{@static => cid} when is_integer(cid) and cid < 0 ->\n              {deep_merge_diff(old[-cid], Map.delete(cdiff, @static)), cache}\n\n            %{} ->\n              {deep_merge_diff(Map.get(old, cid, %{}), cdiff), cache}\n          end\n\n        {res, Map.put(cache, cid, res)}\n    end\n  end\n\n  def drop_cids(rendered, cids) do\n    update_in(rendered[@components], &Map.drop(&1, cids))\n  end\n\n  defp deep_merge_diff(target, %{@template => template} = source),\n    do: deep_merge_diff(target, resolve_templates(Map.delete(source, @template), template))\n\n  defp deep_merge_diff(target, %{@keyed => source_keyed} = source) when is_map(target) do\n    target_keyed = target[@keyed]\n\n    merged_keyed =\n      case source_keyed[@keyed_count] do\n        0 ->\n          %{@keyed_count => 0}\n\n        count ->\n          for pos <- 0..(count - 1), into: %{@keyed_count => count} do\n            value =\n              case source_keyed[pos] do\n                nil -> target_keyed[pos]\n                value when is_number(value) -> target_keyed[value]\n                value when is_map(value) -> deep_merge_diff(target_keyed[pos], value)\n                [old_pos, value] -> deep_merge_diff(target_keyed[old_pos], value)\n              end\n\n            {pos, value}\n          end\n      end\n\n    merged = deep_merge_diff(Map.delete(target, @keyed), Map.delete(source, @keyed))\n    Map.put(merged, @keyed, merged_keyed)\n  end\n\n  defp deep_merge_diff(_target, %{@static => _} = source),\n    do: source\n\n  defp deep_merge_diff(%{} = target, %{} = source),\n    do: Map.merge(target, source, fn _, t, s -> deep_merge_diff(t, s) end)\n\n  defp deep_merge_diff(_target, source),\n    do: source\n\n  # we resolve any templates when merging, because subsequent patches can\n  # contain more templates that are not compatible with previous diffs\n  defp resolve_templates(%{@template => template} = rendered, nil) do\n    resolve_templates(Map.delete(rendered, @template), template)\n  end\n\n  defp resolve_templates(%{@static => static} = rendered, template) when is_integer(static) do\n    resolve_templates(Map.put(rendered, @static, Map.fetch!(template, static)), template)\n  end\n\n  defp resolve_templates(rendered, template) when is_map(rendered) and not is_struct(rendered) do\n    Map.new(rendered, fn {k, v} -> {k, resolve_templates(v, template)} end)\n  end\n\n  defp resolve_templates(other, _template), do: other\n\n  def extract_streams(%{} = source, streams) when not is_struct(source) do\n    Enum.reduce(source, streams, fn\n      {@stream_id, stream}, acc -> [stream | acc]\n      {_key, value}, acc -> extract_streams(value, acc)\n    end)\n  end\n\n  # streams can also be in the dynamic part of the diff\n  def extract_streams(source, streams) when is_list(source) do\n    Enum.reduce(source, streams, fn el, acc -> extract_streams(el, acc) end)\n  end\n\n  def extract_streams(_value, acc), do: acc\n\n  # Diff rendering\n\n  def render_diff(rendered) do\n    rendered\n    |> Phoenix.LiveView.Diff.to_iodata(&add_cid_attr/2)\n    |> IO.iodata_to_binary()\n    |> DOM.parse_fragment()\n    |> elem(1)\n  end\n\n  defp add_cid_attr(cid, [head | tail]) do\n    head_with_cid =\n      Regex.replace(\n        ~r/^(\\s*(?:<!--.*?-->\\s*)*)<([^\\s\\/>]+)/,\n        IO.iodata_to_binary(head),\n        \"\\\\0 #{@phx_component}=\\\"#{to_string(cid)}\\\"\",\n        global: false\n      )\n\n    [head_with_cid | tail]\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/test/dom.ex",
    "content": "defmodule Phoenix.LiveViewTest.DOM do\n  @moduledoc false\n\n  @phx_component \"data-phx-component\"\n\n  alias Phoenix.LiveViewTest.TreeDOM, as: Tree\n  alias Plug.Conn.Query\n\n  import Phoenix.LiveViewTest, only: [configured_test_warning: 1]\n\n  defguardp is_lazy(html) when is_struct(html, LazyHTML)\n\n  def ensure_loaded! do\n    if not Code.ensure_loaded?(LazyHTML) do\n      raise \"\"\"\n      Phoenix LiveView requires lazy_html as a test dependency.\n      Please add to your mix.exs:\n\n      {:lazy_html, \">= 0.1.0\", only: :test}\n      \"\"\"\n    end\n  end\n\n  @spec parse_document(binary) :: {LazyHTML.t(), LazyHTML.Tree.t()}\n  def parse_document(html, error_reporter \\\\ nil) do\n    lazydoc = LazyHTML.from_document(html)\n    tree = LazyHTML.to_tree(lazydoc)\n    run_checks(lazydoc, error_reporter)\n\n    {lazydoc, tree}\n  end\n\n  @spec parse_fragment(binary) :: {LazyHTML.t(), LazyHTML.Tree.t()}\n  def parse_fragment(html, error_reporter \\\\ nil) do\n    lazydoc = LazyHTML.from_fragment(html)\n    tree = LazyHTML.to_tree(lazydoc)\n    run_checks(lazydoc, error_reporter)\n\n    {lazydoc, tree}\n  end\n\n  defp run_checks(lazydoc, error_reporter) do\n    if is_function(error_reporter, 2) do\n      if configured_test_warning(:duplicate_id) != :ignore,\n        do: detect_duplicate_ids(lazydoc, error_reporter)\n\n      if configured_test_warning(:missing_form_id) != :ignore,\n        do: detect_forms_without_id(lazydoc, error_reporter)\n    end\n  end\n\n  defp detect_duplicate_ids(lazydoc, error_reporter) do\n    lazydoc\n    |> LazyHTML.query(\"[id]\")\n    |> LazyHTML.attribute(\"id\")\n    |> Enum.frequencies()\n    |> Enum.each(fn {id, count} ->\n      if count > 1 do\n        error_reporter.(:duplicate_id, \"\"\"\n        Duplicate id found while testing LiveView: #{id}\n\n        #{lazydoc |> by_id!(id) |> to_tree() |> Tree.inspect_html()}\n\n        LiveView requires that all elements have unique ids, duplicate IDs will cause\n        undefined behavior at runtime, as DOM patching will not be able to target the correct\n        elements.\n        \"\"\")\n      end\n    end)\n  end\n\n  defp detect_forms_without_id(lazydoc, error_reporter) do\n    lazydoc\n    |> LazyHTML.query(\"form:not([id])\")\n    |> Enum.each(fn el ->\n      error_reporter.(:missing_form_id, \"\"\"\n      Detected a form with phx-change but missing id:\n\n      #{el |> to_tree() |> Tree.inspect_html()}\n\n      Without an id, LiveView will not be able to perform form recovery,\n      for more information see:\n\n      https://hexdocs.pm/phoenix_live_view/form-bindings.html#recovery-following-crashes-or-disconnects\n      \"\"\")\n    end)\n  end\n\n  def all(lazy, selector) do\n    LazyHTML.query(lazy, selector)\n  end\n\n  def maybe_one(lazy, selector, type \\\\ :selector) do\n    result = all(lazy, selector)\n    count = Enum.count(result)\n\n    case count do\n      1 ->\n        {:ok, result}\n\n      0 ->\n        {:error, :none,\n         \"expected #{type} #{inspect(selector)} to return a single element, but got none \" <>\n           \"within: \\n\\n\" <> to_html(lazy)}\n\n      _ ->\n        {:error, :many,\n         \"expected #{type} #{inspect(selector)} to return a single element, \" <>\n           \"but got #{count}: \\n\\n\" <> to_html(result)}\n    end\n  end\n\n  def targets_from_node(lazy, node) do\n    case node && Tree.all_attributes(node, \"phx-target\") do\n      nil -> [nil]\n      [] -> [nil]\n      [selector] -> targets_from_selector(lazy, selector)\n    end\n  end\n\n  def targets_from_selector(lazy, selector)\n\n  def targets_from_selector(_lazy, nil), do: [nil]\n\n  def targets_from_selector(_lazy, cid) when is_integer(cid), do: [cid]\n\n  def targets_from_selector(lazy, selector) when is_binary(selector) do\n    case Integer.parse(selector) do\n      {cid, \"\"} ->\n        [cid]\n\n      _ ->\n        result =\n          for element <- all(lazy, selector) do\n            if cid = component_id(element) do\n              String.to_integer(cid)\n            end\n          end\n\n        if result == [] do\n          [nil]\n        else\n          result\n        end\n    end\n  end\n\n  defp component_id(tree) do\n    LazyHTML.attribute(tree, @phx_component)\n    |> List.first()\n  end\n\n  def tag(node) do\n    case LazyHTML.tag(node) do\n      [tag | _] -> tag\n      _ -> nil\n    end\n  end\n\n  def attribute(node, key) do\n    case LazyHTML.attribute(node, key) do\n      [value | _] -> value\n      _ -> nil\n    end\n  end\n\n  def to_html(lazy) when is_lazy(lazy) do\n    LazyHTML.to_html(lazy, skip_whitespace_nodes: true)\n  end\n\n  def to_text(node) do\n    LazyHTML.text(node)\n    |> String.replace(~r/[\\s]+/, \" \")\n    |> String.trim()\n  end\n\n  def child_nodes(lazy) when is_lazy(lazy) do\n    LazyHTML.child_nodes(lazy)\n  end\n\n  def by_id!(lazy, id) do\n    LazyHTML.query_by_id(lazy, id)\n  end\n\n  @doc \"\"\"\n  Turns a lazy into a tree.\n  \"\"\"\n  def to_tree(lazy, opts \\\\ []) when is_struct(lazy, LazyHTML), do: LazyHTML.to_tree(lazy, opts)\n\n  @doc \"\"\"\n  Turns a tree into a lazy.\n  \"\"\"\n  def to_lazy(tree), do: LazyHTML.from_tree(tree)\n\n  @doc \"\"\"\n  Escapes a string for use as a CSS identifier.\n\n  ## Examples\n\n      iex> css_escape(\"hello world\")\n      \"hello\\\\\\\\ world\"\n\n      iex> css_escape(\"-123\")\n      \"-\\\\\\\\31 23\"\n\n  \"\"\"\n  @spec css_escape(String.t()) :: String.t()\n  def css_escape(value) when is_binary(value) do\n    # This is a direct translation of\n    # https://github.com/mathiasbynens/CSS.escape/blob/master/css.escape.js\n    # into Elixir.\n    value\n    |> String.to_charlist()\n    |> escape_css_chars()\n    |> IO.iodata_to_binary()\n  end\n\n  defp escape_css_chars(chars) do\n    case chars do\n      # If the character is the first character and is a `-` (U+002D), and\n      # there is no second character, […]\n      [?- | []] -> [\"\\\\-\"]\n      _ -> escape_css_chars(chars, 0, [])\n    end\n  end\n\n  defp escape_css_chars([], _, acc), do: Enum.reverse(acc)\n\n  defp escape_css_chars([char | rest], index, acc) do\n    escaped =\n      cond do\n        # If the character is NULL (U+0000), then the REPLACEMENT CHARACTER\n        # (U+FFFD).\n        char == 0 ->\n          <<0xFFFD::utf8>>\n\n        # If the character is in the range [\\1-\\1F] (U+0001 to U+001F) or is\n        # U+007F,\n        # if the character is the first character and is in the range [0-9]\n        # (U+0030 to U+0039),\n        # if the character is the second character and is in the range [0-9]\n        # (U+0030 to U+0039) and the first character is a `-` (U+002D),\n        char in 0x0001..0x001F or char == 0x007F or\n          (index == 0 and char in ?0..?9) or\n            (index == 1 and char in ?0..?9 and hd(acc) == \"-\") ->\n          # https://drafts.csswg.org/cssom/#escape-a-character-as-code-point\n          [\"\\\\\", Integer.to_string(char, 16), \" \"]\n\n        # If the character is not handled by one of the above rules and is\n        # greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or\n        # is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to\n        # U+005A), or [a-z] (U+0061 to U+007A), […]\n        char >= 0x0080 or char in [?-, ?_] or char in ?0..?9 or char in ?A..?Z or char in ?a..?z ->\n          # the character itself\n          <<char::utf8>>\n\n        true ->\n          # Otherwise, the escaped character.\n          # https://drafts.csswg.org/cssom/#escape-a-character\n          [\"\\\\\", <<char::utf8>>]\n      end\n\n    escape_css_chars(rest, index + 1, [escaped | acc])\n  end\n\n  ## Functions specific for LiveView\n\n  @doc \"\"\"\n  Find static information in the given HTML tree.\n  \"\"\"\n  def find_static_views(lazy) do\n    all(lazy, \"[data-phx-static]\")\n    |> Enum.into(%{}, fn node ->\n      {attribute(node, \"id\"), attribute(node, \"data-phx-static\")}\n    end)\n  end\n\n  ## Forms\n\n  def all_value_inputs({\"form\", attrs, _} = form, root) do\n    form_inputs = filtered_inputs(form)\n\n    case Enum.into(attrs, %{}) do\n      %{\"id\" => id} ->\n        by_form_id = all(root, ~s<[form=\"#{id}\"]>) |> to_tree()\n        named_inputs = filtered_inputs(by_form_id)\n\n        # All inputs including buttons\n        # Remove the named inputs first to remove any possible\n        # duplicates if the child inputs also had a form attribite.\n        (form_inputs -- named_inputs) ++ named_inputs\n\n      _ ->\n        form_inputs\n    end\n  end\n\n  def collect_form_values(form, root, done \\\\ &Query.decode_done/1) do\n    form\n    |> all_value_inputs(root)\n    |> Enum.reduce(Query.decode_init(), &form_defaults/2)\n    |> then(done)\n  end\n\n  def collect_input_values(node) do\n    form_defaults(node, Query.decode_init()) |> Query.decode_done()\n  end\n\n  defp form_defaults(node, acc) do\n    tag = Tree.tag(node)\n\n    if name = Tree.attribute(node, \"name\") do\n      form_defaults(tag, node, name, acc)\n    else\n      acc\n    end\n  end\n\n  # Selectedness algorithm as outlined in\n  # https://html.spec.whatwg.org/multipage/form-elements.html#the-select-element\n  defp form_defaults(\"select\", node, name, acc) do\n    options = Tree.filter(node, &(Tree.tag(&1) == \"option\"))\n\n    multiple_display_size =\n      case valid_display_size(node) do\n        int when is_integer(int) and int > 1 -> true\n        _ -> false\n      end\n\n    all_selected =\n      if Tree.attribute(node, \"multiple\") || multiple_display_size do\n        Enum.filter(options, &Tree.attribute(&1, \"selected\"))\n      else\n        List.wrap(\n          Enum.find(Enum.reverse(options), &Tree.attribute(&1, \"selected\")) ||\n            Enum.find(options, &(!Tree.attribute(&1, \"disabled\")))\n        )\n      end\n\n    Enum.reduce(all_selected, acc, fn selected, acc ->\n      Plug.Conn.Query.decode_each({name, Tree.attribute(selected, \"value\")}, acc)\n    end)\n  end\n\n  defp form_defaults(\"textarea\", node, name, acc) do\n    value = Tree.to_text(node, false)\n\n    if value == \"\" do\n      Plug.Conn.Query.decode_each({name, \"\"}, acc)\n    else\n      Plug.Conn.Query.decode_each({name, String.replace_prefix(value, \"\\n\", \"\")}, acc)\n    end\n  end\n\n  defp form_defaults(\"input\", node, name, acc) do\n    type = Tree.attribute(node, \"type\") || \"text\"\n    value = Tree.attribute(node, \"value\") || default_value(type)\n\n    cond do\n      type in [\"radio\", \"checkbox\"] ->\n        if Tree.attribute(node, \"checked\") do\n          Plug.Conn.Query.decode_each({name, value}, acc)\n        else\n          acc\n        end\n\n      type in [\"image\", \"submit\"] ->\n        acc\n\n      true ->\n        Plug.Conn.Query.decode_each({name, value}, acc)\n    end\n  end\n\n  def default_value(\"checkbox\"), do: \"on\"\n  def default_value(_type), do: \"\"\n\n  defp valid_display_size(node) do\n    with size when not is_nil(size) <- Tree.attribute(node, \"size\"),\n         {int, \"\"} when int > 0 <- Integer.parse(size) do\n      int\n    else\n      _ -> nil\n    end\n  end\n\n  defp filtered_inputs(nodes) do\n    Tree.filter(nodes, fn node ->\n      Tree.tag(node) in ~w(input textarea select) and\n        is_nil(Tree.attribute(node, \"disabled\"))\n    end)\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/test/live_view_test.ex",
    "content": "defmodule Phoenix.LiveViewTest do\n  @moduledoc ~S'''\n  Conveniences for testing function components as well as\n  LiveViews and LiveComponents.\n\n  ## Testing function components\n\n  There are two mechanisms for testing function components. Imagine the\n  following component:\n\n      def greet(assigns) do\n        ~H\"\"\"\n        <div>Hello, {@name}!</div>\n        \"\"\"\n      end\n\n  You can test it by using `render_component/3`, passing the function\n  reference to the component as first argument:\n\n      import Phoenix.LiveViewTest\n\n      test \"greets\" do\n        assert render_component(&MyComponents.greet/1, name: \"Mary\") ==\n                 \"<div>Hello, Mary!</div>\"\n      end\n\n  However, for complex components, often the simplest way to test them\n  is by using the `~H` sigil itself:\n\n      import Phoenix.Component\n      import Phoenix.LiveViewTest\n\n      test \"greets\" do\n        assigns = %{}\n        assert rendered_to_string(~H\"\"\"\n               <MyComponents.greet name=\"Mary\" />\n               \"\"\") ==\n                 \"<div>Hello, Mary!</div>\"\n      end\n\n  The difference is that we use `rendered_to_string/1` to convert the rendered\n  template to a string for testing.\n\n  ## Testing LiveViews and LiveComponents\n\n  In LiveComponents and LiveView tests, we interact with views\n  via process communication in substitution of a browser.\n  Like a browser, our test process receives messages about the\n  rendered updates from the view which can be asserted against\n  to test the life-cycle and behavior of LiveViews and their\n  children.\n\n  ### Testing LiveViews\n\n  The life-cycle of a LiveView as outlined in the `Phoenix.LiveView`\n  docs details how a view starts as a stateless HTML render in a disconnected\n  socket state. Once the browser receives the HTML, it connects to the\n  server and a new LiveView process is started, remounted in a connected\n  socket state, and the view continues statefully. The LiveView test functions\n  support testing both disconnected and connected mounts separately, for example:\n\n      import Plug.Conn\n      import Phoenix.ConnTest\n      import Phoenix.LiveViewTest\n      @endpoint MyEndpoint\n\n      test \"disconnected and connected mount\", %{conn: conn} do\n        conn = get(conn, \"/my-path\")\n        assert html_response(conn, 200) =~ \"<h1>My Disconnected View</h1>\"\n\n        {:ok, view, html} = live(conn)\n      end\n\n      test \"redirected mount\", %{conn: conn} do\n        assert {:error, {:redirect, %{to: \"/somewhere\"}}} = live(conn, \"my-path\")\n      end\n\n  Here, we start by using the familiar `Phoenix.ConnTest` function, `get/2` to\n  test the regular HTTP GET request which invokes mount with a disconnected socket.\n  Next, `live/1` is called with our sent connection to mount the view in a connected\n  state, which starts our stateful LiveView process.\n\n  In general, it's often more convenient to test the mounting of a view\n  in a single step, provided you don't need the result of the stateless HTTP\n  render. This is done with a single call to `live/2`, which performs the\n  `get` step for us:\n\n      test \"connected mount\", %{conn: conn} do\n        {:ok, _view, html} = live(conn, \"/my-path\")\n        assert html =~ \"<h1>My Connected View</h1>\"\n      end\n\n  ### Testing Events\n\n  The browser can send a variety of events to a LiveView via `phx-` bindings,\n  which are sent to the `handle_event/3` callback. To test events sent by the\n  browser and assert on the rendered side effect of the event, use the\n  `render_*` functions:\n\n    * `render_click/1` - sends a phx-click event and value, returning\n      the rendered result of the `handle_event/3` callback.\n\n    * `render_focus/2` - sends a phx-focus event and value, returning\n      the rendered result of the `handle_event/3` callback.\n\n    * `render_blur/1` - sends a phx-blur event and value, returning\n      the rendered result of the `handle_event/3` callback.\n\n    * `render_submit/1` - sends a form phx-submit event and value, returning\n      the rendered result of the `handle_event/3` callback.\n\n    * `render_change/1` - sends a form phx-change event and value, returning\n      the rendered result of the `handle_event/3` callback.\n\n    * `render_keydown/1` - sends a form phx-keydown event and value, returning\n      the rendered result of the `handle_event/3` callback.\n\n    * `render_keyup/1` - sends a form phx-keyup event and value, returning\n      the rendered result of the `handle_event/3` callback.\n\n    * `render_hook/3` - sends a hook event and value, returning\n      the rendered result of the `handle_event/3` callback.\n\n  For example:\n\n      {:ok, view, _html} = live(conn, \"/thermo\")\n\n      assert view\n             |> element(\"button#inc\")\n             |> render_click() =~ \"The temperature is: 31℉\"\n\n  In the example above, we are looking for a particular element on the page\n  and triggering its phx-click event. LiveView takes care of making sure the\n  element has a phx-click and automatically sends its values to the server.\n\n  You can also bypass the element lookup and directly trigger the LiveView\n  event in most functions:\n\n      assert render_click(view, :inc, %{}) =~ \"The temperature is: 31℉\"\n\n  The `element` style is preferred as much as possible, as it helps LiveView\n  perform validations and ensure the events in the HTML actually matches the\n  event names on the server.\n\n  ### Testing regular messages\n\n  LiveViews are `GenServer`'s under the hood, and can send and receive messages\n  just like any other server. To test the side effects of sending or receiving\n  messages, simply message the view and use the `render` function to test the\n  result:\n\n      send(view.pid, {:set_temp, 50})\n      assert render(view) =~ \"The temperature is: 50℉\"\n\n  ### Testing LiveComponents\n\n  LiveComponents can be tested in two ways. One way is to use the same\n  `render_component/2` function as function components. This will mount\n  the LiveComponent and render it once, without testing any of its events:\n\n      assert render_component(MyComponent, id: 123, user: %User{}) =~\n               \"some markup in component\"\n\n  However, if you want to test how components are mounted by a LiveView\n  and interact with DOM events, you must use the regular `live/2` macro\n  to build the LiveView with the component and then scope events by\n  passing the view and a **DOM selector** in a list:\n\n      {:ok, view, html} = live(conn, \"/users\")\n      html = view |> element(\"#user-13 a\", \"Delete\") |> render_click()\n      refute html =~ \"user-13\"\n      refute view |> element(\"#user-13\") |> has_element?()\n\n  In the example above, LiveView will lookup for an element with\n  ID=user-13 and retrieve its `phx-target`. If `phx-target` points\n  to a component, that will be the component used, otherwise it will\n  fallback to the view.\n\n  ### Optional test warnings\n\n  During LiveView tests (using `live/3` or `live_isolated/3`), LiveView performs\n  some additional checks by default. Those include detection of duplicate DOM IDs\n  and LiveComponents. When LiveViewTest detects such an issue, it is either raised\n  as an exception or as a warning.\n\n  You can configure this behavior in two ways:\n\n    1. Application environment config\n\n        You can configure specific issue types in your application config:\n\n        ```elixir\n        config :phoenix_live_view, :test_warnings,\n          duplicate_id: :warn, # one of :warn, :raise, :ignore\n          ...\n        ```\n\n        The supported keys are:\n\n          - `:duplicate_id` - when LiveViewTest detects a duplicate DOM ID\n          - `:duplicate_live_component` - when LiveViewTest detects a LiveComponent being rendered multiple times with the same ID\n          - `:missing_form_id` - when LiveViewTest detects a form without an ID attribute (this prevents [form recovery](form-bindings.html#recovery-following-crashes-or-disconnects))\n\n        The supported values are:\n\n          - `:raise` - crashes the test, default\n          - `:warn` - only emits a warning (will still fail tests if combined with `--warnings-as-errors`)\n          - `:ignore` - ignores the check\n\n    2. `on_error` option on `live/3` or `live_isolated/3`:\n\n        By writing `live(conn, \"/path\", on_error: :warn)`, the default for this specific test\n        can be changed. This example sets all detection types to `:warn`. You can also override\n        specific types only:\n\n        ```elixir\n        {:ok, view, html} = live(conn, \"/path\", on_error: [duplicate_id: :ignore])\n        ```\n\n        which will be merged with the global configuration.\n\n  Note that if a check is marked as `:ignore` in the config, it cannot be re-enabled in `on_error`.\n  '''\n\n  @flash_cookie \"__phoenix_flash__\"\n\n  alias Phoenix.LiveView.{Diff, Socket}\n  alias Phoenix.LiveViewTest.{ClientProxy, DOM, TreeDOM, Element, View, Upload, UploadClient}\n\n  @doc \"\"\"\n  Puts connect params to be used on LiveView connections.\n\n  See `Phoenix.LiveView.get_connect_params/1`.\n  \"\"\"\n  def put_connect_params(conn, params) when is_map(params) do\n    Plug.Conn.put_private(conn, :live_view_connect_params, params)\n  end\n\n  @doc \"\"\"\n  Spawns a connected LiveView process.\n\n  If a `path` is given, then a regular `get(conn, path)`\n  is done and the page is upgraded to a LiveView. If\n  no path is given, it assumes a previously rendered\n  `%Plug.Conn{}` is given, which will be converted to\n  a LiveView immediately.\n\n  ## Options\n\n    * `:on_error` - Can be either `:raise` or `:warn` to control whether\n       detected errors like duplicate IDs or live components fail the test or just log\n       a warning. Defaults to `:raise`.\n\n  ## Examples\n\n      {:ok, view, html} = live(conn, \"/path\")\n      assert view.module == MyLive\n      assert html =~ \"the count is 3\"\n\n      assert {:error, {:redirect, %{to: \"/somewhere\"}}} = live(conn, \"/path\")\n\n  \"\"\"\n  defmacro live(conn, path \\\\ nil, opts \\\\ []) do\n    quote bind_quoted: binding(), generated: true do\n      cond do\n        is_binary(path) ->\n          Phoenix.LiveViewTest.__live__(get(conn, path), path, opts)\n\n        is_nil(path) ->\n          Phoenix.LiveViewTest.__live__(conn, nil, opts)\n\n        true ->\n          raise RuntimeError, \"path must be nil or a binary, got: #{inspect(path)}\"\n      end\n    end\n  end\n\n  @doc \"\"\"\n  Spawns a connected LiveView process mounted in isolation as the sole rendered element.\n\n  Useful for testing LiveViews that are not directly routable, such as those\n  built as small components to be re-used in multiple parents. Testing routable\n  LiveViews is still recommended whenever possible since features such as\n  live navigation require routable LiveViews.\n\n  ## Options\n\n    * `:session` - the session to be given to the LiveView\n    * `:on_error` - Can be either `:raise` or `:warn` to control whether\n       detected errors like duplicate IDs or live components fail the test or just log\n       a warning. Defaults to `:raise`.\n\n  All other options are forwarded to the LiveView for rendering. Refer to\n  `Phoenix.Component.live_render/3` for a list of supported render\n  options.\n\n  ## Examples\n\n      {:ok, view, html} =\n        live_isolated(conn, MyAppWeb.ClockLive, session: %{\"tz\" => \"EST\"})\n\n  Use `put_connect_params/2` to put connect params for a call to\n  `Phoenix.LiveView.get_connect_params/1` in `c:Phoenix.LiveView.mount/3`:\n\n      {:ok, view, html} =\n        conn\n        |> put_connect_params(%{\"param\" => \"value\"})\n        |> live_isolated(AppWeb.ClockLive, session: %{\"tz\" => \"EST\"})\n\n\n  \"\"\"\n  defmacro live_isolated(conn, live_view, opts \\\\ []) do\n    endpoint = Module.get_attribute(__CALLER__.module, :endpoint)\n\n    quote bind_quoted: binding(), unquote: true do\n      unquote(__MODULE__).__isolated__(conn, endpoint, live_view, opts)\n    end\n  end\n\n  @doc false\n  def __isolated__(conn, endpoint, live_view, opts) do\n    put_in(conn.private[:phoenix_endpoint], endpoint || raise(\"no @endpoint set in test module\"))\n    |> Plug.Test.init_test_session(%{})\n    |> Phoenix.LiveView.Router.fetch_live_flash([])\n    |> Phoenix.LiveView.Controller.live_render(live_view, opts)\n    |> connect_from_static_token(nil, opts)\n  end\n\n  @doc false\n  def __live__(%Plug.Conn{state: state, status: status} = conn, _path = nil, opts) do\n    path = rebuild_path(conn)\n\n    case {state, status} do\n      {:sent, 200} ->\n        connect_from_static_token(conn, path, opts)\n\n      {:sent, 302} ->\n        error_redirect_conn(conn)\n\n      {:sent, _} ->\n        raise ArgumentError,\n              \"request to #{conn.request_path} received unexpected #{status} response\"\n\n      {_, _} ->\n        raise ArgumentError, \"\"\"\n        a request has not yet been sent.\n\n        live/1 must use a connection with a sent response. Either call get/2\n        prior to live/1, or use live/2 while providing a path to have a get\n        request issued for you. For example issuing a get yourself:\n\n            {:ok, view, _html} =\n              conn\n              |> get(\"#{path}\")\n              |> live()\n\n        or performing the GET and live connect in a single step:\n\n            {:ok, view, _html} = live(conn, \"#{path}\")\n        \"\"\"\n    end\n  end\n\n  @doc false\n  def __live__(conn, path, opts) do\n    connect_from_static_token(conn, path, opts)\n  end\n\n  defp connect_from_static_token(\n         %Plug.Conn{status: 200, assigns: %{live_module: live_module}} = conn,\n         path,\n         opts\n       ) do\n    DOM.ensure_loaded!()\n\n    router =\n      try do\n        Phoenix.Controller.router_module(conn)\n      rescue\n        KeyError -> nil\n      end\n\n    start_location =\n      case Process.info(self(), :current_stacktrace) do\n        {:current_stacktrace,\n         [\n           {Process, :info, 2, _},\n           {Phoenix.LiveViewTest, :connect_from_static_token, _, _},\n           {_user_module, _test_name, 1, meta} | _\n         ]} ->\n          meta\n\n        _ ->\n          []\n      end\n\n    start_proxy(path, %{\n      response: {:document, Phoenix.ConnTest.response(conn, 200)},\n      connect_params: conn.private[:live_view_connect_params] || %{},\n      connect_info: conn.private[:live_view_connect_info] || prune_conn(conn),\n      live_module: live_module,\n      router: router,\n      endpoint: Phoenix.Controller.endpoint_module(conn),\n      session: maybe_get_session(conn),\n      url: Plug.Conn.request_url(conn),\n      on_error: opts[:on_error],\n      start_location: start_location\n    })\n  end\n\n  defp connect_from_static_token(%Plug.Conn{status: 200}, _path, _opts) do\n    {:error, :nosession}\n  end\n\n  defp connect_from_static_token(%Plug.Conn{status: redir} = conn, _path, _opts)\n       when redir in [301, 302, 303] do\n    error_redirect_conn(conn)\n  end\n\n  defp prune_conn(conn) do\n    %{conn | resp_body: nil, resp_headers: []}\n  end\n\n  defp error_redirect_conn(conn) do\n    to = hd(Plug.Conn.get_resp_header(conn, \"location\"))\n\n    opts =\n      if flash = conn.assigns[:flash] || conn.private[:phoenix_flash] do\n        %{to: to, flash: flash}\n      else\n        %{to: to}\n      end\n\n    {:error, {error_redirect_key(conn), opts}}\n  end\n\n  defp error_redirect_key(%{private: %{phoenix_live_redirect: true}}), do: :live_redirect\n  defp error_redirect_key(_), do: :redirect\n\n  defp start_proxy(path, %{} = opts) do\n    ref = make_ref()\n\n    opts =\n      Map.merge(opts, %{\n        caller: {self(), ref},\n        html: opts.response,\n        connect_params: opts.connect_params,\n        connect_info: opts.connect_info,\n        live_module: opts.live_module,\n        endpoint: opts.endpoint,\n        session: opts.session,\n        url: opts.url,\n        test_supervisor: fetch_test_supervisor!(),\n        on_error: opts.on_error\n      })\n\n    case ClientProxy.start_link(opts) do\n      {:ok, _pid} ->\n        receive do\n          {^ref, {:ok, view, html}} -> {:ok, view, html}\n        end\n\n      {:error, reason} ->\n        exit({reason, {__MODULE__, :live, [path]}})\n\n      :ignore ->\n        receive do\n          {^ref, {:error, reason}} -> {:error, reason}\n        end\n    end\n  end\n\n  defp fetch_test_supervisor!() do\n    case ExUnit.fetch_test_supervisor() do\n      {:ok, sup} -> sup\n      :error -> raise ArgumentError, \"LiveView helpers can only be invoked from the test process\"\n    end\n  end\n\n  defp maybe_get_session(%Plug.Conn{} = conn) do\n    try do\n      Plug.Conn.get_session(conn)\n    rescue\n      _ -> %{}\n    end\n  end\n\n  defp rebuild_path(%Plug.Conn{request_path: request_path, query_string: \"\"}),\n    do: request_path\n\n  defp rebuild_path(%Plug.Conn{request_path: request_path, query_string: query_string}),\n    do: request_path <> \"?\" <> query_string\n\n  @doc \"\"\"\n  Renders a component.\n\n  The first argument may either be a function component, as an\n  anonymous function:\n\n      assert render_component(&Weather.city/1, name: \"Kraków\") =~\n               \"some markup in component\"\n\n  Or a stateful component as a module. In this case, this function\n  will mount, update, and render the component. The `:id` option is\n  a required argument:\n\n      assert render_component(MyComponent, id: 123, user: %User{}) =~\n               \"some markup in component\"\n\n  If your component is using the router, you can pass it as argument:\n\n      assert render_component(MyComponent, %{id: 123, user: %User{}}, router: SomeRouter) =~\n               \"some markup in component\"\n\n  \"\"\"\n  defmacro render_component(component, assigns \\\\ Macro.escape(%{}), opts \\\\ []) do\n    endpoint = Module.get_attribute(__CALLER__.module, :endpoint)\n\n    component =\n      if is_atom(component) do\n        quote do\n          unquote(component).__live__()\n          unquote(component)\n        end\n      else\n        component\n      end\n\n    quote do\n      Phoenix.LiveViewTest.__render_component__(\n        unquote(endpoint),\n        unquote(component),\n        unquote(assigns),\n        unquote(opts)\n      )\n    end\n  end\n\n  @doc false\n  def __render_component__(endpoint, component, assigns, opts) when is_atom(component) do\n    socket = %Socket{endpoint: endpoint, router: opts[:router]}\n\n    assigns = Map.new(assigns)\n\n    # TODO: Make the ID required once we support only stateful module components as live_component\n    mount_assigns = if assigns[:id], do: %{myself: %Phoenix.LiveComponent.CID{cid: -1}}, else: %{}\n\n    socket\n    |> Diff.component_to_rendered(component, assigns, mount_assigns)\n    |> rendered_to_diff_string(socket)\n  end\n\n  def __render_component__(endpoint, function, assigns, opts) when is_function(function, 1) do\n    socket = %Socket{endpoint: endpoint, router: opts[:router]}\n\n    assigns\n    |> Map.new()\n    |> Map.put_new(:__changed__, %{})\n    |> function.()\n    |> rendered_to_diff_string(socket)\n  end\n\n  defp rendered_to_diff_string(rendered, socket) do\n    {diff, _, _} =\n      Diff.render(socket, rendered, Diff.new_fingerprints(), Diff.new_components())\n\n    diff |> Diff.to_iodata() |> IO.iodata_to_binary()\n  end\n\n  @doc ~S'''\n  Converts a rendered template to a string.\n\n  ## Examples\n\n      import Phoenix.Component\n      import Phoenix.LiveViewTest\n\n      test \"greets\" do\n        assigns = %{}\n        assert rendered_to_string(~H\"\"\"\n               <MyComponents.greet name=\"Mary\" />\n               \"\"\") ==\n                 \"<div>Hello, Mary!</div>\"\n      end\n\n  '''\n  def rendered_to_string(rendered) do\n    rendered\n    |> Phoenix.HTML.html_escape()\n    |> Phoenix.HTML.safe_to_string()\n  end\n\n  @doc \"\"\"\n  Sends a click event given by `element` and returns the rendered result.\n\n  The `element` is created with `element/3` and must point to a single\n  element on the page with a `phx-click` attribute in it. The event name\n  given set on `phx-click` is then sent to the appropriate LiveView\n  (or component if `phx-target` is set accordingly). All `phx-value-*`\n  entries in the element are sent as values. Extra values can be given\n  with the `value` argument.\n\n  If the element does not have a `phx-click` attribute but it is\n  a link (the `<a>` tag), the link will be followed accordingly:\n\n    * if the link is a `patch`, the current view will be patched\n    * if the link is a `navigate`, this function will return\n      `{:error, {:live_redirect, %{to: url}}}`, which can be followed\n      with `follow_redirect/2`\n    * if the link is a regular link, this function will return\n      `{:error, {:redirect, %{to: url}}}`, which can be followed\n      with `follow_redirect/2`\n\n  It returns the contents of the whole LiveView or an `{:error, redirect}`\n  tuple.\n\n  ## Examples\n\n      {:ok, view, html} = live(conn, \"/thermo\")\n\n      assert view\n             |> element(\"button\", \"Increment\")\n             |> render_click() =~ \"The temperature is: 30℉\"\n  \"\"\"\n  def render_click(element, value \\\\ %{})\n  def render_click(%Element{} = element, value), do: render_event(element, :click, value)\n  def render_click(view, event), do: render_click(view, event, %{})\n\n  @doc \"\"\"\n  Sends a click `event` to the `view` with `value` and returns the rendered result.\n\n  It returns the contents of the whole LiveView or an `{:error, redirect}`\n  tuple.\n\n  ## Examples\n\n      {:ok, view, html} = live(conn, \"/thermo\")\n      assert html =~ \"The temperature is: 30℉\"\n      assert render_click(view, :inc) =~ \"The temperature is: 31℉\"\n\n  \"\"\"\n  def render_click(view, event, value) do\n    render_event(view, :click, event, value)\n  end\n\n  @doc \"\"\"\n  Puts the submitter `element_or_selector` on the given `form` element.\n\n  A submitter is an element that initiates the form's submit event on the client. When a submitter\n  is put on an element created with `form/3` and then the form is submitted via `render_submit/2`,\n  the name/value pair of the submitter will be included in the submit event payload.\n\n  The given element or selector must exist within the form and match one of the following:\n\n  - A `button` or `input` element with `type=\"submit\"`.\n\n  - A `button` element without a `type` attribute.\n\n  ## Examples\n\n      form = view |> form(\"#my-form\")\n\n      assert form\n             |> put_submitter(\"button[name=example]\")\n             |> render_submit() =~ \"Submitted example\"\n  \"\"\"\n  def put_submitter(form, element_or_selector)\n\n  def put_submitter(%Element{proxy: proxy} = form, submitter) when is_binary(submitter) do\n    put_submitter(form, %Element{proxy: proxy, selector: submitter})\n  end\n\n  def put_submitter(%Element{} = form, %Element{} = submitter) do\n    %{form | meta: Map.put(form.meta, :submitter, submitter)}\n  end\n\n  @doc ~S'''\n  Sends a form submit event given by `element` and returns the rendered result.\n\n  The `element` is created with `element/3` and must point to a single\n  element on the page with a `phx-submit` attribute in it. The event name\n  given set on `phx-submit` is then sent to the appropriate LiveView\n  (or component if `phx-target` is set accordingly). All `phx-value-*`\n  entries in the element are sent as values. Extra values, including hidden\n  input fields, can be given with the `value` argument.\n\n  It returns the contents of the whole LiveView or an `{:error, redirect}`\n  tuple.\n\n  ## Examples\n\n      {:ok, view, html} = live(conn, \"/thermo\")\n\n      assert view\n             |> element(\"form\")\n             |> render_submit(%{deg: 123, avatar: upload}) =~ \"123 exceeds limits\"\n\n  To submit a form along with some hidden input values:\n\n      assert view\n             |> form(\"#term\", user: %{name: \"hello\"})\n             |> render_submit(%{user: %{\"hidden_field\" => \"example\"}}) =~ \"Name updated\"\n\n  To submit a form by a specific submit element via `put_submitter/2`:\n\n      assert view\n             |> form(\"#term\", user: %{name: \"hello\"})\n             |> put_submitter(\"button[name=example_action]\")\n             |> render_submit() =~ \"Action taken\"\n\n  > #### Anti-pattern: bypassing LiveViewTest's form validation unnecessarily {: .warning}\n  >\n  > **DO NOT** use the value parameter to pass data that you expect to be filled\n  > by regular input fields in the form. Values given directly to `render_submit`\n  > are not checked against the inputs rendered as part of the form.\n  >\n  > Imagine you have this code:\n  >\n  > ```elixir\n  > def render(assigns) do\n  >   ~H\"\"\"\n  >   <form phx-submit=\"save\">\n  >     <input name=\"name\" value=\"\" />\n  >     <button type=\"submit\">Submit</button>\n  >   </form>\n  >   \"\"\"\n  > end\n  >\n  > def handle_event(\"save\", %{\"name\" => name}, socket) do\n  >   ...\n  > end\n  > ```\n  >\n  > And you test it with:\n  >\n  > ```elixir\n  > view |> form(\"form\") |> render_submit(%{name: \"hello\"})\n  > ```\n  >\n  > Because the values given to `render_submit` are not checked against the\n  > form, if you later change the input name to something, the test will not fail.\n  > Instead, you should always pass values that are part of visible input fields\n  > as part of the `form/3` call:\n  >\n  > ```elixir\n  > view |> form(\"form\", %{name: \"hello\"}) |> render_submit()\n  > ```\n  >\n  > This way, if you run the tests and your input field is called `<input name=\"other_name\">`,\n  > you will get an error:\n  >\n  > ```text\n  > ** (ArgumentError) could not find non-disabled input, select or textarea with name \"name\" within:\n  >\n  > <input name=\"other_name\" value=\"\"/>\n  > ```\n  >\n  > Only use the `value` parameter to pass values for hidden input fields or submit events from a hook\n  > that cannot be passed to `form/3`. The same applies to `render_change/2`.\n\n  '''\n  def render_submit(element, value \\\\ %{})\n  def render_submit(%Element{} = element, value), do: render_event(element, :submit, value)\n  def render_submit(view, event), do: render_submit(view, event, %{})\n\n  @doc \"\"\"\n  Sends a form submit event to the view and returns the rendered result.\n\n  It returns the contents of the whole LiveView or an `{:error, redirect}`\n  tuple.\n\n  ## Examples\n\n      {:ok, view, html} = live(conn, \"/thermo\")\n      assert html =~ \"The temp is: 30℉\"\n      assert render_submit(view, :refresh, %{deg: 32}) =~ \"The temp is: 32℉\"\n  \"\"\"\n  def render_submit(view, event, value) do\n    render_event(view, :submit, event, value)\n  end\n\n  @doc \"\"\"\n  Sends a form change event given by `element` and returns the rendered result.\n\n  The `element` is created with `element/3` and must point to a single\n  element on the page with a `phx-change` attribute in it. The event name\n  given set on `phx-change` is then sent to the appropriate LiveView\n  (or component if `phx-target` is set accordingly). All `phx-value-*`\n  entries in the element are sent as values.\n\n  If you need to pass any extra values or metadata, such as the \"_target\"\n  parameter, you can do so by giving a map under the `value` argument.\n\n  It returns the contents of the whole LiveView or an `{:error, redirect}`\n  tuple.\n\n  ## Examples\n\n      {:ok, view, html} = live(conn, \"/thermo\")\n\n      assert view\n             |> element(\"form\")\n             |> render_change(%{deg: 123}) =~ \"123 exceeds limits\"\n\n      # Passing metadata\n      {:ok, view, html} = live(conn, \"/thermo\")\n\n      assert view\n             |> element(\"form\")\n             |> render_change(%{_target: [\"deg\"], deg: 123}) =~ \"123 exceeds limits\"\n\n  As with `render_submit/2`, hidden input field values can be provided like so:\n\n      refute view\n            |> form(\"#term\", user: %{name: \"hello\"})\n            |> render_change(%{user: %{\"hidden_field\" => \"example\"}}) =~ \"can't be blank\"\n\n  \"\"\"\n  def render_change(element, value \\\\ %{})\n  def render_change(%Element{} = element, value), do: render_event(element, :change, value)\n  def render_change(view, event), do: render_change(view, event, %{})\n\n  @doc \"\"\"\n  Sends a form change event to the view and returns the rendered result.\n\n  It returns the contents of the whole LiveView or an `{:error, redirect}`\n  tuple.\n\n  ## Examples\n\n      {:ok, view, html} = live(conn, \"/thermo\")\n      assert html =~ \"The temp is: 30℉\"\n      assert render_change(view, :validate, %{deg: 123}) =~ \"123 exceeds limits\"\n  \"\"\"\n  def render_change(view, event, value) do\n    render_event(view, :change, event, value)\n  end\n\n  @doc \"\"\"\n  Sends a keydown event given by `element` and returns the rendered result.\n\n  The `element` is created with `element/3` and must point to a single element\n  on the page with a `phx-keydown` or `phx-window-keydown` attribute in it.\n  The event name given set on `phx-keydown` is then sent to the appropriate\n  LiveView (or component if `phx-target` is set accordingly). All `phx-value-*`\n  entries in the element are sent as values. Extra values can be given with\n  the `value` argument.\n\n  It returns the contents of the whole LiveView or an `{:error, redirect}`\n  tuple.\n\n  ## Examples\n\n      {:ok, view, html} = live(conn, \"/thermo\")\n      assert html =~ \"The temp is: 30℉\"\n      assert view |> element(\"#inc\") |> render_keydown() =~ \"The temp is: 31℉\"\n\n  \"\"\"\n  def render_keydown(element, value \\\\ %{})\n\n  def render_keydown(%Element{} = element, value),\n    do: render_event(element, :keydown, value)\n\n  def render_keydown(view, event), do: render_keydown(view, event, %{})\n\n  @doc \"\"\"\n  Sends a keydown event to the view and returns the rendered result.\n\n  It returns the contents of the whole LiveView or an `{:error, redirect}`\n  tuple.\n\n  ## Examples\n\n      {:ok, view, html} = live(conn, \"/thermo\")\n      assert html =~ \"The temp is: 30℉\"\n      assert render_keydown(view, :inc) =~ \"The temp is: 31℉\"\n\n  \"\"\"\n  def render_keydown(view, event, value) do\n    render_event(view, :keydown, event, value)\n  end\n\n  @doc \"\"\"\n  Sends a keyup event given by `element` and returns the rendered result.\n\n  The `element` is created with `element/3` and must point to a single\n  element on the page with a `phx-keyup` or `phx-window-keyup` attribute\n  in it. The event name given set on `phx-keyup` is then sent to the\n  appropriate LiveView (or component if `phx-target` is set accordingly).\n  All `phx-value-*` entries in the element are sent as values. Extra values\n  can be given with the `value` argument.\n\n  It returns the contents of the whole LiveView or an `{:error, redirect}`\n  tuple.\n\n  ## Examples\n\n      {:ok, view, html} = live(conn, \"/thermo\")\n      assert html =~ \"The temp is: 30℉\"\n      assert view |> element(\"#inc\") |> render_keyup() =~ \"The temp is: 31℉\"\n\n  \"\"\"\n  def render_keyup(element, value \\\\ %{})\n  def render_keyup(%Element{} = element, value), do: render_event(element, :keyup, value)\n  def render_keyup(view, event), do: render_keyup(view, event, %{})\n\n  @doc \"\"\"\n  Sends a keyup event to the view and returns the rendered result.\n\n  It returns the contents of the whole LiveView or an `{:error, redirect}`\n  tuple.\n\n  ## Examples\n\n      {:ok, view, html} = live(conn, \"/thermo\")\n      assert html =~ \"The temp is: 30℉\"\n      assert render_keyup(view, :inc) =~ \"The temp is: 31℉\"\n\n  \"\"\"\n  def render_keyup(view, event, value) do\n    render_event(view, :keyup, event, value)\n  end\n\n  @doc \"\"\"\n  Sends a blur event given by `element` and returns the rendered result.\n\n  The `element` is created with `element/3` and must point to a single\n  element on the page with a `phx-blur` attribute in it. The event name\n  given set on `phx-blur` is then sent to the appropriate LiveView\n  (or component if `phx-target` is set accordingly). All `phx-value-*`\n  entries in the element are sent as values. Extra values can be given\n  with the `value` argument.\n\n  It returns the contents of the whole LiveView or an `{:error, redirect}`\n  tuple.\n\n  ## Examples\n\n      {:ok, view, html} = live(conn, \"/thermo\")\n\n      assert view\n             |> element(\"#inactive\")\n             |> render_blur() =~ \"Tap to wake\"\n  \"\"\"\n  def render_blur(element, value \\\\ %{})\n  def render_blur(%Element{} = element, value), do: render_event(element, :blur, value)\n  def render_blur(view, event), do: render_blur(view, event, %{})\n\n  @doc \"\"\"\n  Sends a blur event to the view and returns the rendered result.\n\n  It returns the contents of the whole LiveView or an `{:error, redirect}`\n  tuple.\n\n  ## Examples\n\n      {:ok, view, html} = live(conn, \"/thermo\")\n      assert html =~ \"The temp is: 30℉\"\n      assert render_blur(view, :inactive) =~ \"Tap to wake\"\n\n  \"\"\"\n  def render_blur(view, event, value) do\n    render_event(view, :blur, event, value)\n  end\n\n  @doc \"\"\"\n  Sends a focus event given by `element` and returns the rendered result.\n\n  The `element` is created with `element/3` and must point to a single\n  element on the page with a `phx-focus` attribute in it. The event name\n  given set on `phx-focus` is then sent to the appropriate LiveView\n  (or component if `phx-target` is set accordingly). All `phx-value-*`\n  entries in the element are sent as values. Extra values can be given\n  with the `value` argument.\n\n  It returns the contents of the whole LiveView or an `{:error, redirect}`\n  tuple.\n\n  ## Examples\n\n      {:ok, view, html} = live(conn, \"/thermo\")\n\n      assert view\n             |> element(\"#inactive\")\n             |> render_focus() =~ \"Tap to wake\"\n  \"\"\"\n  def render_focus(element, value \\\\ %{})\n  def render_focus(%Element{} = element, value), do: render_event(element, :focus, value)\n  def render_focus(view, event), do: render_focus(view, event, %{})\n\n  @doc \"\"\"\n  Sends a focus event to the view and returns the rendered result.\n\n  It returns the contents of the whole LiveView or an `{:error, redirect}`\n  tuple.\n\n  ## Examples\n\n      {:ok, view, html} = live(conn, \"/thermo\")\n      assert html =~ \"The temp is: 30℉\"\n      assert render_focus(view, :inactive) =~ \"Tap to wake\"\n\n  \"\"\"\n  def render_focus(view, event, value) do\n    render_event(view, :focus, event, value)\n  end\n\n  @doc \"\"\"\n  Sends a hook event to the view or an element and returns the rendered result.\n\n  It returns the contents of the whole LiveView or an `{:error, redirect}`\n  tuple.\n\n  ## Examples\n\n      {:ok, view, html} = live(conn, \"/thermo\")\n      assert html =~ \"The temp is: 30℉\"\n      assert render_hook(view, :refresh, %{deg: 32}) =~ \"The temp is: 32℉\"\n\n  If you are pushing events from a hook to a component, then you must pass\n  an `element`, created with `element/3`, as first argument and it must point\n  to a single element on the page with a `phx-target` attribute in it:\n\n      {:ok, view, _html} = live(conn, \"/thermo\")\n      assert view\n             |> element(\"#thermo-component\")\n             |> render_hook(:refresh, %{deg: 32}) =~ \"The temp is: 32℉\"\n\n  \"\"\"\n  def render_hook(view_or_element, event, value \\\\ %{})\n\n  def render_hook(%Element{} = element, event, value) do\n    render_event(%{element | event: to_string(event)}, :hook, value)\n  end\n\n  def render_hook(view, event, value) do\n    render_event(view, :hook, event, value)\n  end\n\n  defp render_event(%Element{} = element, type, value) when is_map(value) or is_list(value) do\n    call(element, {:render_event, element, type, value})\n  end\n\n  defp render_event(%View{} = view, type, event, value) when is_map(value) or is_list(value) do\n    call(view, {:render_event, {proxy_topic(view), to_string(event), view.target}, type, value})\n  end\n\n  @doc \"\"\"\n  Awaits all current `assign_async`, `stream_async` and `start_async` tasks\n  for a given LiveView or element.\n\n  It renders the LiveView or Element once complete and returns the result.\n  The default `timeout` is [ExUnit](https://hexdocs.pm/ex_unit/ExUnit.html#configure/1)'s\n  `assert_receive_timeout` (100 ms).\n\n  ## Examples\n\n      {:ok, lv, html} = live(conn, \"/path\")\n      assert html =~ \"loading data...\"\n      assert render_async(lv) =~ \"data loaded!\"\n  \"\"\"\n  def render_async(\n        view_or_element,\n        timeout \\\\ Application.fetch_env!(:ex_unit, :assert_receive_timeout)\n      ) do\n    pids =\n      case view_or_element do\n        %View{} = view -> call(view, {:async_pids, {proxy_topic(view), nil, nil}})\n        %Element{} = element -> call(element, {:async_pids, element})\n      end\n\n    timeout_ref = make_ref()\n    Process.send_after(self(), {timeout_ref, :timeout}, timeout)\n\n    pids\n    |> Enum.map(&Process.monitor(&1))\n    |> Enum.each(fn ref ->\n      receive do\n        {^timeout_ref, :timeout} ->\n          raise RuntimeError, \"expected async processes to finish within #{timeout}ms\"\n\n        {:DOWN, ^ref, :process, _pid, _reason} ->\n          :ok\n      end\n    end)\n\n    if !Process.cancel_timer(timeout_ref) do\n      receive do\n        {^timeout_ref, :timeout} -> :noop\n      after\n        0 -> :noop\n      end\n    end\n\n    render(view_or_element)\n  end\n\n  @doc \"\"\"\n  Simulates a `push_patch` to the given `path` and returns the rendered result.\n  \"\"\"\n  def render_patch(%View{} = view, path) when is_binary(path) do\n    call(view, {:render_patch, proxy_topic(view), path})\n  end\n\n  @doc \"\"\"\n  Returns the current list of LiveView children for the `parent` LiveView.\n\n  Children are returned in the order they appear in the rendered HTML.\n\n  ## Examples\n\n      {:ok, view, _html} = live(conn, \"/thermo\")\n      assert [clock_view] = live_children(view)\n      assert render_click(clock_view, :snooze) =~ \"snoozing\"\n  \"\"\"\n  def live_children(%View{} = parent) do\n    call(parent, {:live_children, proxy_topic(parent)})\n  end\n\n  @doc \"\"\"\n  Gets the nested LiveView child by `child_id` from the `parent` LiveView.\n\n  ## Examples\n\n      {:ok, view, _html} = live(conn, \"/thermo\")\n      assert clock_view = find_live_child(view, \"clock\")\n      assert render_click(clock_view, :snooze) =~ \"snoozing\"\n  \"\"\"\n  def find_live_child(%View{} = parent, child_id) do\n    parent\n    |> live_children()\n    |> Enum.find(fn %View{id: id} -> id == child_id end)\n  end\n\n  @doc \"\"\"\n  Checks if the given element exists on the page.\n\n  ## Examples\n\n      assert view |> element(\"#some-element\") |> has_element?()\n\n  \"\"\"\n  def has_element?(%Element{} = element) do\n    call(element, {:render_element, :has_element?, element})\n  end\n\n  defguardp is_text_filter(text_filter)\n            when is_binary(text_filter) or is_struct(text_filter, Regex) or is_nil(text_filter)\n\n  @doc \"\"\"\n  Checks if the given `selector` with `text_filter` is on `view`.\n\n  See `element/3` for more information.\n\n  ## Examples\n\n      assert has_element?(view, \"#some-element\")\n\n  \"\"\"\n  def has_element?(%View{} = view, selector, text_filter \\\\ nil)\n      when is_binary(selector) and is_text_filter(text_filter) do\n    has_element?(element(view, selector, text_filter))\n  end\n\n  @doc \"\"\"\n  Returns the HTML string of the rendered view or element.\n\n  If a view is provided, the entire LiveView is rendered.\n  If a view after calling `with_target/2` or an element\n  are given, only that particular context is returned.\n\n  ## Examples\n\n      {:ok, view, _html} = live(conn, \"/thermo\")\n      assert render(view) =~ ~s|<button id=\"alarm\">Snooze</div>|\n\n      assert view\n             |> element(\"#alarm\")\n             |> render() == \"Snooze\"\n  \"\"\"\n  def render(view_or_element) do\n    case render_tree(view_or_element) do\n      {:error, reason} -> {:error, reason}\n      html -> TreeDOM.to_html(html)\n    end\n  end\n\n  @doc \"\"\"\n  Sets the target of the view for events.\n\n  This emulates `phx-target` directly in tests, without\n  having to dispatch the event to a specific element.\n  This can be useful for invoking events to one or\n  multiple components at the same time:\n\n      view\n      |> with_target(\"#user-1,#user-2\")\n      |> render_click(\"Hide\", %{})\n\n  \"\"\"\n  def with_target(%View{} = view, target) do\n    %{view | target: target}\n  end\n\n  defp render_tree(%View{} = view) do\n    render_tree(view, {proxy_topic(view), \"render\", view.target})\n  end\n\n  defp render_tree(%Element{} = element) do\n    render_tree(element, element)\n  end\n\n  defp render_tree(view_or_element, topic_or_element) do\n    call(view_or_element, {:render_element, :find_element, topic_or_element})\n  end\n\n  defp call(view_or_element, tuple) do\n    try do\n      GenServer.call(proxy_pid(view_or_element), tuple, :infinity)\n    catch\n      :exit, {{:shutdown, {kind, opts}}, _} when kind in [:redirect, :live_redirect] ->\n        {:error, {kind, opts}}\n\n      :exit, {{exception, stack}, _} ->\n        exit({{exception, stack}, {__MODULE__, :call, [view_or_element]}})\n    else\n      :ok -> :ok\n      {:ok, result} -> result\n      {:raise, exception} -> raise exception\n    end\n  end\n\n  @doc \"\"\"\n  Returns an element to scope a function to.\n\n  It expects the current LiveView, a query selector, and a text filter.\n\n  An optional text filter may be given to filter the results by the query\n  selector. If the text filter is a string or a regex, it will match any\n  element that contains the string (including as a substring) or matches the\n  regex.\n\n  So a link containing the text \"unopened\" will match `element(\"a\", \"opened\")`.\n  To prevent this, a regex could specify that \"opened\" appear without the prefix \"un\".\n  For example, `element(\"a\", ~r{(?<!un)opened})`.\n  But it may be clearer to add an HTML attribute to make the element easier to\n  select.\n\n  After the text filter is applied, only one element must remain, otherwise an\n  error is raised.\n\n  If no text filter is given, then the query selector itself must return\n  a single element.\n\n      assert view\n            |> element(\"#term > :first-child\", \"Increment\")\n            |> render() =~ \"Increment</a>\"\n\n  Attribute selectors are also supported, and may be used on special cases\n  like ids which contain periods:\n\n      assert view\n             |> element(~s{[href=\"/foo\"][id=\"foo.bar.baz\"]})\n             |> render() =~ \"Increment</a>\"\n  \"\"\"\n  def element(%View{proxy: proxy}, selector, text_filter \\\\ nil)\n      when is_binary(selector) and is_text_filter(text_filter) do\n    %Element{proxy: proxy, selector: selector, text_filter: text_filter}\n  end\n\n  @doc \"\"\"\n  Returns a form element to scope a function to.\n\n  It expects the current LiveView, a query selector, and the form data.\n  The query selector must return a single element.\n\n  The form data will be validated directly against the form markup and\n  make sure the data you are changing/submitting actually exists, failing\n  otherwise.\n\n  ## Examples\n\n      assert view\n            |> form(\"#term\", user: %{name: \"hello\"})\n            |> render_submit() =~ \"Name updated\"\n\n  This function is meant to mimic what the user can actually do, so you cannot\n   set hidden input values. However, hidden values can be given when calling\n   `render_submit/2` or `render_change/2`, see their docs for examples.\n  \"\"\"\n  def form(%View{proxy: proxy}, selector, form_data \\\\ %{}) when is_binary(selector) do\n    %Element{proxy: proxy, selector: selector, form_data: form_data}\n  end\n\n  @doc \"\"\"\n  Builds a file input for testing uploads within a form.\n\n  Given the form DOM selector, the upload name, and a list of maps of client metadata\n  for the upload, the returned file input can be passed to `render_upload/2`.\n\n  Client metadata takes the following form:\n\n    * `:last_modified` - the last modified timestamp\n    * `:name` - the name of the file\n    * `:content` - the binary content of the file\n    * `:size` - the byte size of the content\n    * `:type` - the MIME type of the file\n    * `:relative_path` - for simulating webkitdirectory metadata\n    * `:meta` - optional metadata sent by the client\n\n  ## Examples\n\n      avatar = file_input(lv, \"#my-form-id\", :avatar, [%{\n        last_modified: 1_594_171_879_000,\n        name: \"myfile.jpeg\",\n        content: File.read!(\"myfile.jpg\"),\n        size: 1_396_009,\n        type: \"image/jpeg\"\n      }])\n\n      assert render_upload(avatar, \"myfile.jpeg\") =~ \"100%\"\n  \"\"\"\n  defmacro file_input(view, form_selector, name, entries) do\n    quote bind_quoted: [view: view, selector: form_selector, name: name, entries: entries] do\n      require Phoenix.ChannelTest\n      builder = fn -> Phoenix.ChannelTest.connect(Phoenix.LiveView.Socket, %{}) end\n      Phoenix.LiveViewTest.__file_input__(view, selector, name, entries, builder)\n    end\n  end\n\n  @doc false\n  def __file_input__(view, selector, name, entries, builder) do\n    cid = find_cid!(view, selector)\n\n    for entry <- entries do\n      content = entry[:content]\n      size = entry[:size]\n\n      if content && size && byte_size(content) != size do\n        raise ArgumentError, \"\"\"\n        entry content size must match provided size.\n\n        By default the content size is calculated using byte_size/1, so you\n        rarely need to provide the size option yourself unless you are testing\n        for misbehaving clients.\n        \"\"\"\n      end\n    end\n\n    case Phoenix.LiveView.Channel.fetch_upload_config(view.pid, name, cid) do\n      {:ok, %{external: false}} ->\n        start_upload_client(builder, view, selector, name, entries, cid)\n\n      {:ok, %{external: func}} when is_function(func) ->\n        start_external_upload_client(view, selector, name, entries, cid)\n\n      :error ->\n        raise \"no uploads allowed for #{name}\"\n    end\n  end\n\n  defp find_cid!(view, selector) do\n    sync_with_root!(view)\n    lazy = call(view, {:get_lazy, view.id})\n\n    with {:ok, form} <- DOM.maybe_one(lazy, selector),\n         [form] <- DOM.to_tree(form) do\n      [cid | _] = DOM.targets_from_node(lazy, form)\n      cid\n    else\n      {:error, _reason, msg} -> raise ArgumentError, msg\n    end\n  end\n\n  defp start_upload_client(builder, view, form_selector, name, entries, cid) do\n    {:ok, socket} = builder.()\n    spec = {UploadClient, socket: socket, cid: cid}\n    {:ok, pid} = Supervisor.start_child(fetch_test_supervisor!(), spec)\n    Upload.new(pid, view, form_selector, name, entries, cid)\n  end\n\n  defp start_external_upload_client(view, form_selector, name, entries, cid) do\n    spec = {UploadClient, cid: cid}\n    {:ok, pid} = Supervisor.start_child(fetch_test_supervisor!(), spec)\n    Upload.new(pid, view, form_selector, name, entries, cid)\n  end\n\n  @doc \"\"\"\n  Returns the most recent title that was updated via a `page_title` assign.\n\n  ## Examples\n\n      render_click(view, :event_that_triggers_page_title_update)\n      assert page_title(view) =~ \"my title\"\n\n  \"\"\"\n  def page_title(view) do\n    call(view, :page_title)\n  end\n\n  @doc \"\"\"\n  Asserts a live patch will happen within `timeout` milliseconds.\n  The default `timeout` is [ExUnit](https://hexdocs.pm/ex_unit/ExUnit.html#configure/1)'s\n  `assert_receive_timeout` (100 ms).\n\n  It returns the new path.\n\n  To assert on the flash message, you can assert on the result of the\n  rendered LiveView.\n\n  ## Examples\n\n      render_click(view, :event_that_triggers_patch)\n      assert_patch view\n\n      render_click(view, :event_that_triggers_patch)\n      assert_patch view, 30\n\n      render_click(view, :event_that_triggers_patch)\n      path = assert_patch view\n      assert path =~ ~r/path/\\d+/\n  \"\"\"\n  def assert_patch(view, timeout \\\\ Application.fetch_env!(:ex_unit, :assert_receive_timeout))\n\n  def assert_patch(view, timeout) when is_integer(timeout) do\n    {path, _flash} = assert_navigation(view, :patch, nil, timeout)\n    path\n  end\n\n  def assert_patch(view, to) when is_binary(to) do\n    assert_patch(view, to, Application.fetch_env!(:ex_unit, :assert_receive_timeout))\n  end\n\n  @doc \"\"\"\n  Asserts a live patch will happen to a given path within `timeout`\n  milliseconds.\n\n  The default `timeout` is [ExUnit](https://hexdocs.pm/ex_unit/ExUnit.html#configure/1)'s\n  `assert_receive_timeout` (100 ms).\n\n  It returns the new path.\n\n  To assert on the flash message, you can assert on the result of the\n  rendered LiveView.\n\n  ## Examples\n      render_click(view, :event_that_triggers_patch)\n      assert_patch view, \"/path\"\n\n      render_click(view, :event_that_triggers_patch)\n      assert_patch view, \"/path\", 30\n\n  \"\"\"\n  def assert_patch(view, to, timeout)\n      when is_binary(to) and is_integer(timeout) do\n    {path, _flash} = assert_navigation(view, :patch, to, timeout)\n    path\n  end\n\n  @doc \"\"\"\n  Asserts a live patch was performed, and returns the new path.\n\n  To assert on the flash message, you can assert on the result of\n  the rendered LiveView.\n\n  ## Examples\n\n      render_click(view, :event_that_triggers_redirect)\n      assert_patched view, \"/path\"\n\n  \"\"\"\n  def assert_patched(view, to) do\n    assert_patch(view, to, 0)\n  end\n\n  @doc \"\"\"\n  Refutes a live patch to a given path was performed.\n\n  It returns `:ok` if the specified patch isn't already in the mailbox.\n\n  ## Examples\n\n      render_click(view, :event_that_triggers_patch_to_path)\n      :ok = refute_patched view, \"/wrong_path\"\n\n  \"\"\"\n  def refute_patched(view) do\n    refute_navigation(view, :patch, nil)\n  end\n\n  def refute_patched(view, to) when is_binary(to) do\n    refute_navigation(view, :patch, to)\n  end\n\n  @doc ~S\"\"\"\n  Asserts a redirect will happen within `timeout` milliseconds.\n  The default `timeout` is [ExUnit](https://hexdocs.pm/ex_unit/ExUnit.html#configure/1)'s\n  `assert_receive_timeout` (100 ms).\n\n  It returns a tuple containing the new path and the flash messages from said\n  redirect, if any. Note the flash will contain string keys.\n\n  ## Examples\n\n      render_click(view, :event_that_triggers_redirect)\n      {path, flash} = assert_redirect view\n      assert flash[\"info\"] == \"Welcome\"\n      assert path =~ ~r/path\\/\\d+/\n\n      render_click(view, :event_that_triggers_redirect)\n      assert_redirect view, 30\n  \"\"\"\n  def assert_redirect(view, timeout \\\\ Application.fetch_env!(:ex_unit, :assert_receive_timeout))\n\n  def assert_redirect(view, timeout) when is_integer(timeout) do\n    assert_navigation(view, :redirect, nil, timeout)\n  end\n\n  def assert_redirect(view, to) when is_binary(to) do\n    assert_redirect(view, to, Application.fetch_env!(:ex_unit, :assert_receive_timeout))\n  end\n\n  @doc \"\"\"\n  Asserts a redirect will happen to a given path within `timeout` milliseconds.\n\n  The default `timeout` is [ExUnit](https://hexdocs.pm/ex_unit/ExUnit.html#configure/1)'s\n  `assert_receive_timeout` (100 ms).\n\n  It returns the flash messages from said redirect, if any.\n  Note the flash will contain string keys.\n\n  ## Examples\n\n      render_click(view, :event_that_triggers_redirect)\n      flash = assert_redirect view, \"/path\"\n      assert flash[\"info\"] == \"Welcome\"\n\n      render_click(view, :event_that_triggers_redirect)\n      assert_redirect view, \"/path\", 30\n  \"\"\"\n  def assert_redirect(view, to, timeout)\n      when is_binary(to) and is_integer(timeout) do\n    {_path, flash} = assert_navigation(view, :redirect, to, timeout)\n    flash\n  end\n\n  @doc \"\"\"\n  Asserts a redirect was performed.\n\n  It returns the flash messages from said redirect, if any. Note the\n  flash will contain string keys.\n\n  ## Examples\n\n      render_click(view, :event_that_triggers_redirect)\n      flash = assert_redirected view, \"/path\"\n      assert flash[\"info\"] == \"Welcome\"\n\n  \"\"\"\n  def assert_redirected(view, to) do\n    assert_redirect(view, to, 0)\n  end\n\n  defp assert_navigation(view, kind, to, timeout) do\n    %{proxy: {ref, topic, _}, endpoint: endpoint} = view\n\n    receive do\n      {^ref, {^kind, ^topic, %{to: new_to} = opts}} when new_to == to or to == nil ->\n        {new_to, Phoenix.LiveView.Utils.verify_flash(endpoint, opts[:flash])}\n    after\n      timeout ->\n        message =\n          if to do\n            \"expected #{inspect(view.module)} to #{kind} to #{inspect(to)}, \"\n          else\n            \"expected #{inspect(view.module)} to #{kind}, \"\n          end\n\n        case flush_navigation(ref, topic, nil) do\n          {new_kind, new_to} when new_to != to ->\n            raise ArgumentError, message <> \"but got a #{new_kind} to #{inspect(new_to)}\"\n\n          _ ->\n            raise ArgumentError, message <> \"but got none\"\n        end\n    end\n  end\n\n  @doc \"\"\"\n  Refutes a redirect to a given path was performed.\n\n  It returns :ok if the specified redirect isn't already in the mailbox.\n\n  If no path is specified, refutes any redirection on the given view.\n\n  ## Examples\n\n      render_click(view, :event_that_triggers_redirect_to_path)\n      :ok = refute_redirected view, \"/wrong_path\"\n  \"\"\"\n  def refute_redirected(view) do\n    refute_navigation(view, :redirect, nil)\n  end\n\n  def refute_redirected(view, to) when is_binary(to) do\n    refute_navigation(view, :redirect, to)\n  end\n\n  defp refute_navigation(view = %{proxy: {ref, topic, _}}, kind, to) do\n    receive do\n      {^ref, {^kind, ^topic, %{to: new_to}}} when new_to == to or to == nil ->\n        message =\n          if to do\n            \"expected #{inspect(view.module)} not to #{kind} to #{inspect(to)}, \"\n          else\n            \"expected #{inspect(view.module)} not to #{kind}, \"\n          end\n\n        raise ArgumentError, message <> \"but got a #{kind} to #{inspect(new_to)}\"\n    after\n      0 -> :ok\n    end\n  end\n\n  defp flush_navigation(ref, topic, last) do\n    receive do\n      {^ref, {kind, ^topic, %{to: to}}} when kind in [:patch, :redirect] ->\n        flush_navigation(ref, topic, {kind, to})\n    after\n      0 -> last\n    end\n  end\n\n  @doc \"\"\"\n  Open the default browser to display current HTML of `view_or_element`.\n\n  ## Examples\n\n      view\n      |> element(\"#term > :first-child\", \"Increment\")\n      |> open_browser()\n\n      assert view\n             |> form(\"#term\", user: %{name: \"hello\"})\n             |> open_browser()\n             |> render_submit() =~ \"Name updated\"\n\n  \"\"\"\n  def open_browser(view_or_element, open_fun \\\\ &open_with_system_cmd/1)\n\n  def open_browser(view_or_element, open_fun) when is_function(open_fun, 1) do\n    html = render_tree(view_or_element)\n\n    view_or_element\n    |> maybe_wrap_html(html)\n    |> write_tmp_html_file()\n    |> open_fun.()\n\n    view_or_element\n  end\n\n  defp maybe_wrap_html(view_or_element, content) do\n    {html, static_path} = call(view_or_element, :html)\n\n    head =\n      case TreeDOM.filter(html, fn node -> TreeDOM.tag(node) == \"head\" end) do\n        [head] -> head\n        _ -> {\"head\", [], []}\n      end\n\n    case TreeDOM.attribute(content, \"data-phx-main\") do\n      [\"true\" | _] ->\n        # If we are rendering the main LiveView,\n        # we return the full page html.\n        html\n\n      _ ->\n        # Otherwise we build a basic html structure around the\n        # view_or_element content.\n        [\n          {\"html\", [],\n           [\n             head,\n             {\"body\", [], List.wrap(content)}\n           ]}\n        ]\n    end\n    |> TreeDOM.walk(fn\n      {\"script\", _, _} -> []\n      {\"a\", _, _} = link -> link\n      {el, attrs, children} -> {el, maybe_prefix_static_path(attrs, static_path), children}\n      el -> el\n    end)\n  end\n\n  defp maybe_prefix_static_path(attrs, nil), do: attrs\n\n  defp maybe_prefix_static_path(attrs, static_path) do\n    Enum.map(attrs, fn\n      {\"src\", path} -> {\"src\", prefix_static_path(path, static_path)}\n      {\"href\", path} -> {\"href\", prefix_static_path(path, static_path)}\n      attr -> attr\n    end)\n  end\n\n  defp prefix_static_path(<<\"//\" <> _::binary>> = url, _prefix), do: url\n\n  defp prefix_static_path(<<\"/\" <> _::binary>> = path, prefix),\n    do: \"file://#{Path.join([prefix, path])}\"\n\n  defp prefix_static_path(url, _), do: url\n\n  defp write_tmp_html_file(html) do\n    html = TreeDOM.to_html(html)\n    path = Path.join([System.tmp_dir!(), \"#{Phoenix.LiveView.Utils.random_id()}.html\"])\n    File.write!(path, html)\n    path\n  end\n\n  defp open_with_system_cmd(path) do\n    {cmd, args} =\n      case :os.type() do\n        {:win32, _} ->\n          {\"cmd\", [\"/c\", \"start\", path]}\n\n        {:unix, :darwin} ->\n          {\"open\", [path]}\n\n        {:unix, _} ->\n          if path =~ \"\\\\\" and System.find_executable(\"cmd.exe\") != nil do\n            # Use cmd.exe for WSL with project dir under Windows path\n            {\"cmd.exe\", [\"/c\", \"start\", path]}\n          else\n            {\"xdg-open\", [path]}\n          end\n      end\n\n    System.cmd(cmd, args)\n  end\n\n  @doc \"\"\"\n  Asserts an event will be pushed within `timeout`.\n  The default `timeout` is [ExUnit](https://hexdocs.pm/ex_unit/ExUnit.html#configure/1)'s\n  `assert_receive_timeout` (100 ms).\n\n  ## Examples\n\n      assert_push_event view, \"scores\", %{points: 100, user: \"josé\"}\n  \"\"\"\n  defmacro assert_push_event(\n             view,\n             event,\n             payload,\n             timeout \\\\ Application.fetch_env!(:ex_unit, :assert_receive_timeout)\n           ) do\n    quote do\n      %{proxy: {ref, _topic, _}} = unquote(view)\n\n      assert_receive {^ref, {:push_event, unquote(event), unquote(payload)}}, unquote(timeout)\n    end\n  end\n\n  @doc \"\"\"\n  Refutes an event will be pushed within timeout.\n\n  The default `timeout` is [ExUnit](https://hexdocs.pm/ex_unit/ExUnit.html#configure/1)'s\n  `refute_receive_timeout` (100 ms).\n\n  ## Examples\n\n      refute_push_event view, \"scores\", %{points: _, user: \"josé\"}\n  \"\"\"\n  defmacro refute_push_event(\n             view,\n             event,\n             payload,\n             timeout \\\\ Application.fetch_env!(:ex_unit, :refute_receive_timeout)\n           ) do\n    quote do\n      %{proxy: {ref, _topic, _}} = unquote(view)\n\n      receive do\n        {^ref, {:push_event, unquote(event), unquote(payload) = data}} ->\n          flunk(\"\"\"\n          Unexpectedly received event \"#{unquote(event)}\"\n\n          Payload:\n\n          #{inspect(data, pretty: true)}\n          \"\"\")\n      after\n        unquote(timeout) ->\n          false\n      end\n    end\n  end\n\n  @doc \"\"\"\n  Asserts a hook reply was returned from a `handle_event` callback.\n\n  The default `timeout` is [ExUnit](https://hexdocs.pm/ex_unit/ExUnit.html#configure/1)'s\n  `assert_receive_timeout` (100 ms).\n\n  ## Examples\n\n      assert_reply view, %{result: \"ok\", transaction_id: _}\n  \"\"\"\n  defmacro assert_reply(\n             view,\n             payload,\n             timeout \\\\ Application.fetch_env!(:ex_unit, :assert_receive_timeout)\n           ) do\n    quote do\n      %{proxy: {ref, _topic, _}} = unquote(view)\n\n      assert_receive {^ref, {:reply, unquote(payload)}}, unquote(timeout)\n    end\n  end\n\n  @doc \"\"\"\n  Follows the redirect from a `render_*` action or an `{:error, redirect}`\n  tuple.\n\n  Imagine you have a LiveView that redirects on a `render_click`\n  event. You can make sure it immediately redirects after the\n  `render_click` action by calling `follow_redirect/3`:\n\n      live_view\n      |> render_click(\"redirect\")\n      |> follow_redirect(conn)\n\n  Or in the case of an error tuple:\n\n      assert {:error, {:redirect, %{to: \"/somewhere\"}}} = result = live(conn, \"my-path\")\n      {:ok, view, html} = follow_redirect(result, conn)\n\n  `follow_redirect/3` expects a connection as second argument.\n  This is the connection that will be used to perform the underlying\n  request.\n\n  If the LiveView redirects with a live redirect, this macro returns\n  `{:ok, live_view, disconnected_html}` with the content of the new\n  LiveView, the same as the `live/3` macro. If the LiveView redirects\n  with a regular redirect, this macro returns `{:ok, conn}` with the\n  rendered redirected page. In any other case, this macro raises.\n\n  Finally, note that you can optionally assert on the path you are\n  being redirected to by passing a third argument:\n\n      live_view\n      |> render_click(\"redirect\")\n      |> follow_redirect(conn, \"/redirected/page\")\n\n  \"\"\"\n  defmacro follow_redirect(reason, conn, to \\\\ nil) do\n    quote bind_quoted: binding(), generated: true do\n      case reason do\n        {:error, {:live_redirect, opts}} ->\n          {conn, to} = Phoenix.LiveViewTest.__follow_redirect__(conn, @endpoint, to, opts)\n          live(conn, to)\n\n        {:error, {:redirect, opts}} ->\n          {conn, to} = Phoenix.LiveViewTest.__follow_redirect__(conn, @endpoint, to, opts)\n          {:ok, get(conn, to)}\n\n        _ ->\n          raise \"LiveView did not redirect\"\n      end\n    end\n  end\n\n  @doc false\n  def __follow_redirect__(conn, endpoint, expected_to, %{to: to} = opts) do\n    if expected_to && expected_to != to do\n      raise ArgumentError,\n            \"expected LiveView to redirect to #{inspect(expected_to)}, but got #{inspect(to)}\"\n    end\n\n    conn = Phoenix.ConnTest.ensure_recycled(conn)\n\n    if flash = opts[:flash] do\n      {Phoenix.ConnTest.put_req_cookie(conn, @flash_cookie, ensure_signed_flash(endpoint, flash)),\n       to}\n    else\n      {conn, to}\n    end\n  end\n\n  defp ensure_signed_flash(endpoint, flash) when is_map(flash) do\n    Phoenix.LiveView.Utils.sign_flash(endpoint, flash)\n  end\n\n  defp ensure_signed_flash(_, flash), do: flash\n\n  @doc \"\"\"\n  Performs a live redirect from one LiveView to another.\n\n  When redirecting between two LiveViews of the same `live_session`,\n  mounts the new LiveView and shutsdown the previous one, which\n  mimics general browser live navigation behaviour.\n\n  When attempting to navigate from a LiveView of a different\n  `live_session`, an error redirect condition is returned indicating\n  a failed `push_navigate` from the client.\n\n  ## Examples\n\n      assert {:ok, page_live, _html} = live(conn, \"/page/1\")\n      assert {:ok, page2_live, _html} = live(conn, \"/page/2\")\n\n      assert {:error, {:redirect, _}} = live_redirect(page2_live, to: \"/admin\")\n  \"\"\"\n  def live_redirect(view, opts) do\n    __live_redirect__(view, opts)\n  end\n\n  @doc false\n  def __live_redirect__(%View{} = view, opts, token_func \\\\ & &1) do\n    {session, %ClientProxy{} = root} = ClientProxy.root_view(proxy_pid(view))\n\n    url =\n      case Keyword.fetch!(opts, :to) do\n        \"/\" <> _ = path -> URI.merge(root.uri, path)\n        url -> url\n      end\n\n    live_module =\n      case Phoenix.LiveView.Route.live_link_info_without_checks(root.endpoint, root.router, url) do\n        {:internal, route} ->\n          route.view\n\n        _ ->\n          raise ArgumentError, \"\"\"\n          attempted to navigate to a non-live route at #{inspect(url)}\n          \"\"\"\n      end\n\n    html = render(view)\n    ClientProxy.stop(proxy_pid(view), {:shutdown, :duplicate_topic})\n    root_token = token_func.(root.session_token)\n    static_token = token_func.(root.static_token)\n\n    start_location =\n      case Process.info(self(), :current_stacktrace) do\n        {:current_stacktrace,\n         [\n           {Process, :info, 2, _},\n           {Phoenix.LiveViewTest, :__live_redirect__, _, _},\n           {_user_module, _test_name, 1, meta} | _\n         ]} ->\n          meta\n\n        _ ->\n          []\n      end\n\n    start_proxy(url, %{\n      response: {:fragment, html},\n      live_redirect: {root.id, root_token, static_token},\n      connect_params: root.connect_params,\n      connect_info: root.connect_info,\n      live_module: live_module,\n      endpoint: root.endpoint,\n      router: root.router,\n      session: session,\n      url: url,\n      on_error: root.on_error,\n      start_location: start_location\n    })\n  end\n\n  @doc \"\"\"\n  Receives a `form_element` and asserts that `phx-trigger-action` has been\n  set to true, following up on that request.\n\n  Imagine you have a LiveView that sends an HTTP form submission. Say that it\n  sets the `phx-trigger-action` to true, as a response to a submit event.\n  You can follow the trigger action like this:\n\n      form = form(live_view, selector, %{\"form\" => \"data\"})\n\n      # First we submit the form. Optionally verify that phx-trigger-action\n      # is now part of the form.\n      assert render_submit(form) =~ ~r/phx-trigger-action/\n\n      # Now follow the request made by the form\n      conn = follow_trigger_action(form, conn)\n      assert conn.method == \"POST\"\n      assert conn.params == %{\"form\" => \"data\"}\n\n  \"\"\"\n  defmacro follow_trigger_action(form, conn) do\n    quote bind_quoted: binding() do\n      {method, path, form_data} =\n        Phoenix.LiveViewTest.__render_trigger_submit__(\n          form,\n          :follow_trigger_action,\n          \"phx-trigger-action\",\n          \"could not follow trigger action because form #{inspect(form.selector)} \" <>\n            \"does not have a phx-trigger-action attribute\"\n        )\n\n      dispatch(conn, @endpoint, method, path, form_data)\n    end\n  end\n\n  @doc \"\"\"\n  Receives a form element and submits the HTTP request through the plug pipeline.\n\n  Imagine you have a LiveView that validates form data, but submits the form to\n  a controller via the normal form `action` attribute. This is especially useful\n  in scenarios where the result of a form submit needs to write to the plug session.\n\n  You can follow submit the form with the `%Plug.Conn{}`, like this:\n\n      form = form(live_view, selector, %{\"form\" => \"data\"})\n\n      # Now submit the LiveView form to the plug pipeline\n      conn = submit_form(form, conn)\n      assert conn.method == \"POST\"\n      assert conn.params == %{\"form\" => \"data\"}\n  \"\"\"\n  defmacro submit_form(form, conn) do\n    quote bind_quoted: binding() do\n      {method, path, form_data} =\n        Phoenix.LiveViewTest.__render_trigger_submit__(\n          form,\n          :submit_form,\n          \"action\",\n          \"could not submit form because form #{inspect(form.selector)} \" <>\n            \"does not have an action attribute\"\n        )\n\n      dispatch(conn, @endpoint, method, path, form_data)\n    end\n  end\n\n  def __render_trigger_submit__(%Element{} = form, name, required_attr, error_msg) do\n    root = call(form, {:get_lazy, form})\n\n    case render_tree(form) do\n      {\"form\", attrs, _child_nodes} = node ->\n        if not List.keymember?(attrs, required_attr, 0) do\n          raise ArgumentError, error_msg <> \", got: #{inspect(attrs)}\"\n        end\n\n        {\"action\", path} = List.keyfind(attrs, \"action\", 0) || {\"action\", call(form, :url)}\n        {\"method\", method} = List.keyfind(attrs, \"method\", 0) || {\"method\", \"get\"}\n\n        form_data =\n          (form.form_data || %{})\n          |> Map.new()\n          |> Phoenix.LiveViewTest.Utils.stringify(&to_string/1)\n\n        values =\n          node\n          |> DOM.collect_form_values(root)\n          |> Map.merge(form_data)\n\n        {method, path, values}\n\n      {tag, _, _} ->\n        raise ArgumentError,\n              \"could not #{name} because given element did not return a form, \" <>\n                \"got #{inspect(tag)} instead\"\n    end\n  end\n\n  defp proxy_pid(%{proxy: {_ref, _topic, pid}}), do: pid\n\n  defp proxy_topic(%{proxy: {_ref, topic, _pid}}), do: topic\n\n  @doc \"\"\"\n  Performs an upload of a file input and renders the result.\n\n  See `file_input/4` for details on building a file input.\n\n  ## Examples\n\n  Given the following LiveView template:\n\n  ```heex\n  <%= for entry <- @uploads.avatar.entries do %>\n    {entry.name}: {entry.progress}%\n  <% end %>\n  ```\n\n  Your test case can assert the uploaded content:\n\n      avatar = file_input(lv, \"#my-form-id\", :avatar, [\n        %{\n          last_modified: 1_594_171_879_000,\n          name: \"myfile.jpeg\",\n          content: File.read!(\"myfile.jpg\"),\n          size: 1_396_009,\n          type: \"image/jpeg\"\n        }\n      ])\n\n      assert render_upload(avatar, \"myfile.jpeg\") =~ \"100%\"\n\n  By default, the entire file is chunked to the server, but an optional\n  percentage to chunk can be passed to test chunk-by-chunk uploads:\n\n      assert render_upload(avatar, \"myfile.jpeg\", 49) =~ \"49%\"\n      assert render_upload(avatar, \"myfile.jpeg\", 51) =~ \"100%\"\n\n  Before making assertions about the how the upload is consumed server-side,\n  you will need to call `render_submit/1`.\n\n  In the case where an upload progress callback issues a navigate, patch, or\n  redirect, the following will be returned:\n\n    * for a patch, the current view will be patched\n    * for a navigate, this function will return\n      `{:error, {:live_redirect, %{to: url}}}`, which can be followed\n      with `follow_redirect/2`\n    * for a regular redirect, this function will return\n      `{:error, {:redirect, %{to: url}}}`, which can be followed\n      with `follow_redirect/2`\n  \"\"\"\n  def render_upload(%Upload{} = upload, entry_name, percent \\\\ 100) do\n    entry_ref =\n      Enum.find_value(upload.entries, fn\n        %{\"name\" => ^entry_name, \"ref\" => ref} -> ref\n        %{} -> nil\n      end)\n\n    if !entry_name do\n      raise ArgumentError, \"no such entry with name #{inspect(entry_name)}\"\n    end\n\n    case UploadClient.fetch_allow_acknowledged(upload, entry_name) do\n      {:ok, _token} ->\n        render_chunk(upload, entry_name, percent)\n\n      {:error, :nopreflight} ->\n        case preflight_upload(upload) do\n          {:ok, %{ref: ref, config: config, entries: entries_resp, errors: errors}} ->\n            if entry_errors = errors[entry_ref] do\n              UploadClient.allowed_ack(upload, ref, config, entry_name, entries_resp, errors)\n              {:error, for(reason <- entry_errors, do: [entry_ref, reason])}\n            else\n              case UploadClient.allowed_ack(upload, ref, config, entry_name, entries_resp, errors) do\n                :ok -> render_chunk(upload, entry_name, percent)\n                {:error, reason} -> {:error, reason}\n              end\n            end\n\n          {:error, reason} ->\n            {:error, reason}\n        end\n\n      {:error, reason} ->\n        {:error, reason}\n    end\n  end\n\n  @doc \"\"\"\n  Performs a preflight upload request.\n\n  Useful for testing external uploaders to retrieve the `:external` entry metadata.\n\n  ## Examples\n\n      avatar = file_input(lv, \"#my-form-id\", :avatar, [%{name: ..., ...}, ...])\n      assert {:ok, %{ref: _ref, config: %{chunk_size: _}}} = preflight_upload(avatar)\n  \"\"\"\n  def preflight_upload(%Upload{} = upload) do\n    # LiveView channel returns error conditions as error key in payload, ie `%{error: reason}`\n    case call(\n           upload.element,\n           {:render_event, upload.element, :allow_upload, {upload.entries, upload.cid}}\n         ) do\n      %{error: reason} -> {:error, reason}\n      %{ref: _ref} = resp -> {:ok, resp}\n    end\n  end\n\n  defp render_chunk(upload, entry_name, percent) do\n    pid = proxy_pid(upload.view)\n    monitor_ref = Process.monitor(pid)\n    trap = Process.flag(:trap_exit, true)\n\n    try do\n      case UploadClient.chunk(upload, entry_name, percent, proxy_pid(upload.view)) do\n        {:ok, _} ->\n          # The chunk function returns as soon as the upload is consumed, therefore\n          # the following could happen:\n          #\n          #   1. the upload is consumed, and the channel is closed\n          #     --> we receive :ok here\n          #     --> the progress callback redirected, the channel sends a message to itself\n          #   2. we try to render and send a message to the ClientProxy, which pings the channel\n          #   3. the channel receives the ping before it is scheduled to send the redirect message to\n          #      itself, therefore the ping succeeds\n          #   4. we receive the HTML, but we expected a redirect shutdown\n          #\n          # If we synchronize here, we ensure that the channel successfully sent the redirect message\n          # before we try to render. This way, either the first sync already fails, or the second sync\n          # inside the render fails because at that time, the redirect message must have been processed\n          # by the channel.\n          sync_with_root!(upload.view)\n          render(upload.view)\n\n        {:error, reason} ->\n          {:error, reason}\n      end\n    catch\n      :exit, reason ->\n        receive do\n          {:DOWN, ^monitor_ref, :process, _pid, {:shutdown, {:live_redirect, opts}}} ->\n            {:error, {:live_redirect, opts}}\n\n          {:DOWN, ^monitor_ref, :process, _pid, {:shutdown, {:redirect, opts}}} ->\n            {:error, {:redirect, opts}}\n        after\n          0 -> exit(reason)\n        end\n    after\n      Process.flag(:trap_exit, trap)\n    end\n  end\n\n  defp sync_with_root!(%View{} = view) do\n    pid = proxy_pid(view)\n    proxy_topic = proxy_topic(view)\n    GenServer.call(pid, {:sync_with_root, proxy_topic})\n  end\n\n  @doc false\n  def configured_test_warning(type) do\n    warnings = Application.get_env(:phoenix_live_view, :test_warnings, [])\n\n    case warnings do\n      atom when atom in [:warn, :raise, :ignore] ->\n        atom\n\n      keyword when is_list(keyword) ->\n        Keyword.get(keyword, type)\n\n      other ->\n        raise ArgumentError,\n              \"Phoenix.LiveViewTest :warnings configuration must be one of :warn, :raise, :ignore, or a keyword list. Got: #{inspect(other)}\"\n    end\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/test/structs.ex",
    "content": "defmodule Phoenix.LiveViewTest.View do\n  @moduledoc \"\"\"\n  The struct for testing LiveViews.\n\n  The following public fields represent the LiveView:\n\n    * `id` - The DOM id of the LiveView\n    * `module` - The module of the running LiveView\n    * `pid` - The Pid of the running LiveView\n    * `endpoint` - The endpoint for the LiveView\n    * `target` - The target to scope events to\n\n  See the `Phoenix.LiveViewTest` documentation for usage.\n  \"\"\"\n  @derive {Inspect, only: [:id, :module, :pid, :endpoint]}\n\n  defstruct id: nil,\n            module: nil,\n            pid: nil,\n            proxy: nil,\n            endpoint: nil,\n            target: nil\nend\n\ndefmodule Phoenix.LiveViewTest.Element do\n  @moduledoc \"\"\"\n  The struct returned by `Phoenix.LiveViewTest.element/3`.\n\n  The following public fields represent the element:\n\n    * `selector` - The query selector\n    * `text_filter` - The text to further filter the element\n\n  See the `Phoenix.LiveViewTest` documentation for usage.\n  \"\"\"\n  @derive {Inspect, only: [:selector, :text_filter]}\n\n  defstruct proxy: nil,\n            selector: nil,\n            text_filter: nil,\n            event: nil,\n            form_data: nil,\n            meta: %{}\nend\n\ndefmodule Phoenix.LiveViewTest.Upload do\n  @moduledoc \"\"\"\n  The struct returned by `Phoenix.LiveViewTest.file_input/4`.\n\n  The following public fields represent the element:\n\n    * `selector` - The query selector\n    * `entries` - The list of selected file entries\n\n  See the `Phoenix.LiveViewTest` documentation for usage.\n  \"\"\"\n\n  alias Phoenix.LiveViewTest.{Upload, Element}\n  @derive {Inspect, only: [:selector, :entries]}\n\n  defstruct pid: nil,\n            view: nil,\n            element: nil,\n            ref: nil,\n            selector: nil,\n            config: %{},\n            entries: [],\n            cid: nil\n\n  @doc false\n  def new(pid, %Phoenix.LiveViewTest.View{} = view, form_selector, name, entries, cid) do\n    populated_entries = Enum.map(entries, fn entry -> populate_entry(entry) end)\n    selector = \"#{form_selector} input[type=\\\"file\\\"][name=\\\"#{name}\\\"]\"\n\n    %Upload{\n      pid: pid,\n      view: view,\n      element: %Element{proxy: view.proxy, selector: selector},\n      entries: populated_entries,\n      cid: cid\n    }\n  end\n\n  defp populate_entry(%{} = entry) do\n    name =\n      Map.get(entry, :name) ||\n        raise ArgumentError, \"a :name of the entry filename is required.\"\n\n    content =\n      Map.get(entry, :content) ||\n        raise ArgumentError, \"the :content of the binary entry file data is required.\"\n\n    relative_path = Map.get(entry, :relative_path)\n    last_modified = Map.get(entry, :last_modified)\n\n    %{\n      \"name\" => name,\n      \"content\" => content,\n      \"last_modified\" => last_modified,\n      \"relative_path\" => relative_path,\n      \"ref\" => to_string(System.unique_integer([:positive])),\n      \"size\" => entry[:size] || byte_size(content),\n      \"type\" => entry[:type] || MIME.from_path(name)\n    }\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/test/tree_dom.ex",
    "content": "defmodule Phoenix.LiveViewTest.TreeDOM do\n  @moduledoc false\n\n  @phx_static \"data-phx-static\"\n  @phx_component \"data-phx-component\"\n\n  alias Phoenix.LiveViewTest.DOM\n\n  import Phoenix.LiveViewTest, only: [configured_test_warning: 1]\n\n  @doc \"\"\"\n  Filters nodes according to `fun`. Walks the tree in a post-walk manner, visiting children before parents.\n  \"\"\"\n  def filter(node, fun) do\n    node |> reverse_filter(fun) |> Enum.reverse()\n  end\n\n  @doc \"\"\"\n  Filters nodes and returns them in reverse order.\n  \"\"\"\n  def reverse_filter(tree, fun) do\n    reduce(tree, [], fn x, acc ->\n      if fun.(x), do: [x | acc], else: acc\n    end)\n  end\n\n  @doc \"\"\"\n  Returns the tag name of the node.\n  \"\"\"\n  def tag({name, _attrs, _children}), do: name\n  def tag(_), do: nil\n\n  @doc \"\"\"\n  Returns the value of the attribute `key` from the node or nil if not found.\n  \"\"\"\n  def attribute(node, key) do\n    with {tag, attrs, _children} when is_binary(tag) <- node,\n         {_, value} <- List.keyfind(attrs, key, 0) do\n      value\n    else\n      _ -> nil\n    end\n  end\n\n  @doc \"\"\"\n  Returns the HTML representation of the node.\n  \"\"\"\n  def to_html(text) when is_binary(text), do: text\n\n  def to_html(html) do\n    LazyHTML.Tree.to_html(List.wrap(html), skip_whitespace_nodes: true)\n  end\n\n  @doc \"\"\"\n  Returns the text representation of the node, removing extra whitespace.\n  \"\"\"\n  def to_text(tree, trim \\\\ true) do\n    text =\n      tree\n      |> node_to_text()\n      |> Enum.join()\n\n    if trim do\n      text\n      |> String.replace(~r/[\\s]+/, \" \")\n      |> String.trim()\n    else\n      text\n    end\n  end\n\n  defp node_to_text({_tag, _attrs, content}), do: node_to_text(content)\n  defp node_to_text(list) when is_list(list), do: Enum.flat_map(list, &node_to_text/1)\n  defp node_to_text(text) when is_binary(text), do: [text]\n  defp node_to_text(_), do: []\n\n  @doc \"\"\"\n  Returns the node with the given `id`, raises an error if not found.\n  \"\"\"\n  def by_id!(tree, id) do\n    case tree\n         |> reverse_filter(fn\n           {_, attributes, _} -> List.keyfind(attributes, \"id\", 0) == {\"id\", id}\n           _ -> false\n         end) do\n      [node] ->\n        node\n\n      [] ->\n        raise \"expected to find one node with id #{id}, but got none within: \\n\\n#{inspect_html(tree)}\"\n\n      many ->\n        raise \"expected exactly one node with id #{id}, but got #{length(many)}: \\n\\n#{inspect_html(many)}\"\n    end\n  end\n\n  @doc \"\"\"\n  Returns the child nodes of the node.\n  \"\"\"\n  def child_nodes(tree) do\n    case tree do\n      {_, _, children} -> children\n      [{_, _, children}] -> children\n      _ -> []\n    end\n  end\n\n  @doc \"\"\"\n  Returns all attributes of the node.\n  \"\"\"\n  def attrs(tree) do\n    case tree do\n      {_, attrs, _} -> attrs\n      [{_, attrs, _}] -> attrs\n    end\n  end\n\n  @doc \"\"\"\n  Returns the children of the node with the given `id`, raises an error if not found.\n  \"\"\"\n  def inner_html!(tree, id), do: tree |> by_id!(id) |> child_nodes()\n\n  @doc \"\"\"\n  Returns all values of the attribute `name` from the node.\n  \"\"\"\n  def all_attributes(tree, name) do\n    for {_, attributes, _} <- tree |> List.wrap(),\n        {_, val} <- [List.keyfind(attributes, name, 0)],\n        do: val\n  end\n\n  @doc \"\"\"\n  Returns all values of the attributes from the node.\n\n  Handles phx-value-* attributes.\n  \"\"\"\n  def all_values(tree) do\n    attributes =\n      case tree do\n        {_, attributes, _} -> attributes\n        [{_, attributes, _} | _] -> attributes\n        _ -> []\n      end\n\n    for {attr, value} <- attributes, key = value_key(attr), do: {key, value}, into: %{}\n  end\n\n  defp value_key(\"phx-value-\" <> key), do: key\n  defp value_key(\"value\"), do: \"value\"\n  defp value_key(_), do: nil\n\n  @doc \"\"\"\n  Reduces the tree with the given function.\n  \"\"\"\n  def reduce(tree, acc, fun) when is_function(fun, 2) do\n    do_reduce(tree, acc, fn\n      text, acc when is_binary(text) -> acc\n      {:comment, _children}, acc -> acc\n      {_tag, _attrs, _children} = node, acc -> fun.(node, acc)\n    end)\n  end\n\n  defp do_reduce([], acc, _fun), do: acc\n\n  defp do_reduce([node | rest], acc, fun) do\n    acc = do_reduce(node, acc, fun)\n    do_reduce(rest, acc, fun)\n  end\n\n  defp do_reduce({tag, attrs, children}, acc, fun) do\n    acc = fun.({tag, attrs, children}, acc)\n    do_reduce(children, acc, fun)\n  end\n\n  defp do_reduce(node, acc, fun) do\n    fun.(node, acc)\n  end\n\n  @doc \"\"\"\n  Walks the tree and updates nodes based on the given function.\n  \"\"\"\n  def walk(tree, fun) when is_function(fun, 1) do\n    LazyHTML.Tree.postwalk(tree, fn\n      text when is_binary(text) -> text\n      {:comment, _children} = comment -> comment\n      {_tag, _attrs, _children} = node -> fun.(node)\n    end)\n  end\n\n  defp by_id(tree, id) do\n    case filter(tree, fn node -> attribute(node, \"id\") == id end) do\n      [node] -> node\n      _ -> nil\n    end\n  end\n\n  @doc \"\"\"\n  Sets the attribute `name` to the value `val` on the node.\n  \"\"\"\n  def set_attr({tag, attrs, children} = _el, name, val) do\n    new_attrs =\n      attrs\n      |> Enum.filter(fn {existing_name, _} -> existing_name != name end)\n      |> Kernel.++([{name, val}])\n\n    {tag, new_attrs, children}\n  end\n\n  @doc \"\"\"\n  Returns an HTML representation of the nodes for showing in error messages.\n  \"\"\"\n  def inspect_html(nodes) when is_list(nodes) do\n    for dom_node <- nodes, into: \"\", do: inspect_html(dom_node)\n  end\n\n  def inspect_html(dom_node),\n    do: \"    \" <> String.replace(to_html(dom_node), \"\\n\", \"\\n   \") <> \"\\n\"\n\n  ### Functions specific for LiveView\n\n  @doc \"\"\"\n  Find live views in the given HTML tree.\n  \"\"\"\n  def find_live_views(tree) do\n    tree\n    |> filter(fn node -> attribute(node, \"data-phx-session\") end)\n    |> Enum.map(fn {_, attributes, _} -> attributes end)\n    |> parse_live_views_attributes()\n  end\n\n  defp parse_live_views_attributes(attributes) do\n    attributes\n    |> Enum.reduce([], fn node, acc ->\n      id = keyfind(node, \"id\")\n      static = keyfind(node, @phx_static)\n      session = keyfind(node, \"data-phx-session\")\n      main = List.keymember?(node, \"data-phx-main\", 0)\n\n      static = if static in [nil, \"\"], do: nil, else: static\n      found = {id, session, static}\n\n      if main do\n        acc ++ [found]\n      else\n        [found | acc]\n      end\n    end)\n    |> Enum.reverse()\n  end\n\n  defp keyfind(list, key) do\n    case List.keyfind(list, key, 0) do\n      {_, val} -> val\n      nil -> nil\n    end\n  end\n\n  @doc \"\"\"\n  Removes stream children from the given HTML tree.\n  \"\"\"\n  def remove_stream_children(html_tree) do\n    walk(html_tree, fn {tag, attrs, children} = node ->\n      if attribute(node, \"phx-update\") == \"stream\" do\n        {tag, attrs, []}\n      else\n        {tag, attrs, children}\n      end\n    end)\n  end\n\n  def patch_id(id, html, inner_html, streams, error_reporter \\\\ nil) do\n    cids_before = component_ids(id, html)\n\n    phx_update_tree =\n      walk(inner_html, fn node ->\n        apply_phx_update(attribute(node, \"phx-update\"), html, node, streams)\n      end)\n\n    new_html =\n      walk(html, fn {tag, attrs, children} = node ->\n        if attribute(node, \"id\") == id do\n          {tag, attrs, phx_update_tree}\n        else\n          {tag, attrs, children}\n        end\n      end)\n\n    cids_after = component_ids(id, new_html)\n\n    if is_function(error_reporter, 2) do\n      if configured_test_warning(:duplicate_id) != :ignore,\n        do: detect_duplicate_ids(new_html, error_reporter)\n\n      if configured_test_warning(:duplicate_live_component) != :ignore,\n        do: detect_duplicate_components(new_html, cids_after, error_reporter)\n\n      if configured_test_warning(:missing_form_id) != :ignore,\n        do: detect_forms_without_id(new_html, error_reporter)\n    end\n\n    {new_html, cids_before -- cids_after}\n  end\n\n  def detect_duplicate_ids(tree, error_reporter),\n    do: detect_duplicate_ids(tree, tree, MapSet.new(), error_reporter)\n\n  defp detect_duplicate_ids(tree, [node | rest], ids, error_reporter) do\n    ids = detect_duplicate_ids(tree, node, ids, error_reporter)\n    detect_duplicate_ids(tree, rest, ids, error_reporter)\n  end\n\n  defp detect_duplicate_ids(tree, {_tag_name, _attrs, children} = node, ids, error_reporter) do\n    case attribute(node, \"id\") do\n      id when not is_nil(id) ->\n        if MapSet.member?(ids, id) do\n          error_reporter.(:duplicate_id, \"\"\"\n          Duplicate id found while testing LiveView: #{id}\n\n          #{inspect_html(filter(tree, fn node -> attribute(node, \"id\") == id end))}\n\n          LiveView requires that all elements have unique ids, duplicate IDs will cause\n          undefined behavior at runtime, as DOM patching will not be able to target the correct\n          elements.\n          \"\"\")\n        end\n\n        detect_duplicate_ids(tree, children, MapSet.put(ids, id), error_reporter)\n\n      _ ->\n        detect_duplicate_ids(tree, children, ids, error_reporter)\n    end\n  end\n\n  defp detect_duplicate_ids(_tree, _non_tag, seen_ids, _error_reporter), do: seen_ids\n\n  def detect_duplicate_components(tree, cids, error_reporter) do\n    cids\n    |> Enum.frequencies()\n    |> Enum.each(fn {cid, count} ->\n      if count > 1 do\n        error_reporter.(:duplicate_live_component, \"\"\"\n        Duplicate live component found while testing LiveView:\n\n        #{inspect_html(filter(tree, fn node -> attribute(node, @phx_component) == to_string(cid) end))}\n\n        This most likely means that you are conditionally rendering the same\n        LiveComponent multiple times with the same ID in the same LiveView.\n        This is not supported and will lead to broken behavior on the client.\n        \"\"\")\n      end\n    end)\n  end\n\n  defp detect_forms_without_id([_ | _] = nodes, error_reporter) do\n    Enum.each(nodes, &detect_forms_without_id(&1, error_reporter))\n  end\n\n  defp detect_forms_without_id({\"form\", attrs, _children} = node, error_reporter) do\n    case {attribute(node, \"id\"), attribute(node, \"phx-change\"),\n          attribute(node, \"phx-ignore-missing-id\")} do\n      {nil, phx_change, nil} when is_binary(phx_change) ->\n        error_reporter.(:missing_form_id, \"\"\"\n        Detected a form with phx-change but missing id:\n\n        #{inspect_html({\"form\", attrs, []})}\n\n        Without an id, LiveView will not be able to perform form recovery,\n        for more information see:\n\n        https://hexdocs.pm/phoenix_live_view/form-bindings.html#recovery-following-crashes-or-disconnects\n        \"\"\")\n\n      _ ->\n        :ok\n    end\n  end\n\n  defp detect_forms_without_id({_tag_name, _attrs, children}, error_reporter) do\n    detect_forms_without_id(children, error_reporter)\n  end\n\n  defp detect_forms_without_id(_node, _error_reporter), do: :ok\n\n  def component_ids(id, html_tree) do\n    by_id!(html_tree, id)\n    |> child_nodes()\n    |> Enum.reduce([], &traverse_component_ids/2)\n  end\n\n  defp traverse_component_ids(current, acc) do\n    acc =\n      if id = attribute(current, @phx_component) do\n        [String.to_integer(id) | acc]\n      else\n        acc\n      end\n\n    cond do\n      attribute(current, @phx_static) ->\n        acc\n\n      children = child_nodes(current) ->\n        Enum.reduce(children, acc, &traverse_component_ids/2)\n\n      true ->\n        acc\n    end\n  end\n\n  def replace_root_container(container_html, new_tag, attrs) do\n    reserved_attrs = ~w(id data-phx-session data-phx-static data-phx-main)\n    [{_container_tag, container_attrs_list, children} | _] = container_html\n    container_attrs = Enum.into(container_attrs_list, %{})\n\n    merged_attrs =\n      for {attr, value} <- attrs,\n          attr = String.downcase(to_string(attr)),\n          attr not in reserved_attrs,\n          reduce: container_attrs_list do\n        acc ->\n          if Map.has_key?(container_attrs, attr) do\n            Enum.map(acc, fn\n              {^attr, _old_val} -> {attr, value}\n              {_, _} = other -> other\n            end)\n          else\n            acc ++ [{attr, value}]\n          end\n      end\n\n    [{to_string(new_tag), merged_attrs, children}]\n  end\n\n  defp apply_phx_update(type, _html_tree, _node, _streams) when type in [\"append\", \"prepend\"] do\n    raise ArgumentError,\n          \"phx-update=#{inspect(type)} has been deprecated before v1.0 and is no longer supported in tests\"\n  end\n\n  defp apply_phx_update(\"stream\", html_tree, {tag, attrs, appended_children} = node, streams) do\n    container_id = attribute(node, \"id\")\n    verify_phx_update_id!(\"stream\", container_id, node)\n    children_before = apply_phx_update_children(html_tree, container_id)\n\n    appended_children =\n      Enum.filter(appended_children, fn node ->\n        not is_binary(node) or (is_binary(node) and String.trim_leading(node) != \"\")\n      end)\n\n    # to ensure correct DOM patching, all elements must have an ID\n    _ = apply_phx_update_children_id(\"stream\", children_before)\n    _ = apply_phx_update_children_id(\"stream\", appended_children)\n\n    streams =\n      Enum.map(streams, fn [ref, inserts, deleteIds | maybe_reset] ->\n        %{ref: ref, inserts: inserts, deleteIds: deleteIds, reset: maybe_reset == [true]}\n      end)\n\n    streamInserts =\n      Enum.reduce(streams, %{}, fn %{ref: ref, inserts: inserts}, acc ->\n        Enum.reduce(inserts, acc, fn [id, stream_at, limit, update_only], acc ->\n          Map.put(acc, id, %{\n            ref: ref,\n            stream_at: stream_at,\n            limit: limit,\n            update_only: update_only\n          })\n        end)\n      end)\n\n    # for each stream, reset if necessary and apply deletes\n    # (this corresponds to the this.streams.forEach loop in dom_patch.js)\n    filtered_children_before =\n      Enum.reduce(streams, children_before, fn stream, acc -> apply_stream(acc, stream) end)\n\n    # now apply the DOM patching (this corresponds mainly to the appendChild in dom_patch.js)\n    new_children =\n      Enum.reduce(appended_children, filtered_children_before, fn node, acc ->\n        id = attribute(node, \"id\")\n        insert = streamInserts[id]\n        current_index = Enum.find_index(acc, fn node -> attribute(node, \"id\") == id end)\n\n        new_children =\n          cond do\n            is_nil(insert) and is_nil(current_index) ->\n              # the element is not part of the stream inserts, so we append it at the end\n              # (see dom_patch.js addChild)\n              acc ++ [node]\n\n            is_nil(insert) && current_index ->\n              # not a stream item, but already in the DOM -> update in place\n              List.replace_at(acc, current_index, node)\n\n            current_index ->\n              # update stream item in place\n              List.replace_at(acc, current_index, set_attr(node, \"data-phx-stream\", insert.ref))\n\n            insert[:update_only] ->\n              # skip item if it is not already in the DOM\n              acc\n\n            true ->\n              # stream item to be inserted at specific position\n              List.insert_at(acc, insert.stream_at, set_attr(node, \"data-phx-stream\", insert.ref))\n          end\n\n        maybe_apply_stream_limit(new_children, insert)\n      end)\n\n    {tag, attrs, new_children}\n  end\n\n  defp apply_phx_update(\"ignore\", html_tree, node, _streams) do\n    container_id = attribute(node, \"id\")\n    verify_phx_update_id!(\"ignore\", container_id, node)\n\n    {new_tag, new_attrs, new_children} = node\n\n    {tag, attrs_before, children_before} =\n      case by_id(html_tree, container_id) do\n        {_tag, _attrs_before, _children_before} = triplet -> triplet\n        nil -> {new_tag, new_attrs, new_children}\n      end\n\n    merged_attrs =\n      Enum.reject(attrs_before, fn {name, _} -> String.starts_with?(name, \"data-\") end) ++\n        Enum.filter(new_attrs, fn {name, _} -> String.starts_with?(name, \"data-\") end)\n\n    {tag, merged_attrs, children_before}\n  end\n\n  defp apply_phx_update(type, _state, node, _streams) when type in [nil, \"replace\"] do\n    node\n  end\n\n  defp apply_phx_update(other, _state, _node, _streams) do\n    raise ArgumentError,\n          \"invalid phx-update value #{inspect(other)}, \" <>\n            \"expected one of \\\"stream\\\", \\\"replace\\\", \\\"append\\\", \\\"prepend\\\", \\\"ignore\\\"\"\n  end\n\n  defp apply_stream(existing_children, stream) do\n    children =\n      if stream.reset do\n        Enum.reject(existing_children, fn node ->\n          attribute(node, \"data-phx-stream\") == stream.ref\n        end)\n      else\n        existing_children\n      end\n\n    Enum.filter(children, fn node ->\n      attribute(node, \"id\") not in stream.deleteIds\n    end)\n  end\n\n  defp maybe_apply_stream_limit(children, %{limit: limit}) when is_integer(limit) do\n    Enum.take(children, limit)\n  end\n\n  defp maybe_apply_stream_limit(children, _maybe_insert), do: children\n\n  defp verify_phx_update_id!(type, id, node) when id in [\"\", nil] do\n    raise ArgumentError,\n          \"setting phx-update to #{inspect(type)} requires setting an ID on the container, \" <>\n            \"got: \\n\\n #{to_html(node)}\"\n  end\n\n  defp verify_phx_update_id!(_type, _id, _node) do\n    :ok\n  end\n\n  defp apply_phx_update_children(html_tree, id) do\n    case by_id(html_tree, id) do\n      {_, _, children_before} -> children_before\n      nil -> []\n    end\n  end\n\n  defp apply_phx_update_children_id(type, children) do\n    for {tag, _, _} = child when is_binary(tag) <- children do\n      attribute(child, \"id\") ||\n        raise ArgumentError,\n              \"setting phx-update to #{inspect(type)} requires setting an ID on each child. \" <>\n                \"No ID was found on:\\n\\n#{to_html(child)}\"\n    end\n  end\n\n  ## Test Helpers\n\n  @doc \"\"\"\n  Normalizes the given HTML to a tree with optional sorting of attributes.\n  \"\"\"\n  def normalize_to_tree(html, opts \\\\ []) do\n    sort_attributes? = Keyword.get(opts, :sort_attributes, false)\n    trim_whitespace? = Keyword.get(opts, :trim_whitespace, true)\n    full_document? = Keyword.get(opts, :full_document, false)\n\n    html =\n      case html do\n        binary when is_binary(binary) ->\n          (full_document? && DOM.parse_document(binary)) || DOM.parse_fragment(binary)\n\n        h ->\n          h\n      end\n\n    tree =\n      case html do\n        {%{} = struct, tree} when is_struct(struct, LazyHTML) -> tree\n        html when is_struct(html, LazyHTML) -> DOM.to_tree(html)\n        _ -> html\n      end\n\n    normalize_tree(tree, sort_attributes?, trim_whitespace?)\n  end\n\n  defp normalize_tree({node_type, attributes, content}, sort_attributes?, trim_whitespace?) do\n    {node_type, (sort_attributes? && Enum.sort(attributes)) || attributes,\n     normalize_tree(content, sort_attributes?, trim_whitespace?)}\n  end\n\n  defp normalize_tree(values, sort_attributes?, true) when is_list(values) do\n    for value <- values,\n        not is_binary(value) or (is_binary(value) and String.trim(value) != \"\"),\n        do: normalize_tree(value, sort_attributes?, true)\n  end\n\n  defp normalize_tree(values, sort_attributes?, false) when is_list(values) do\n    Enum.map(values, &normalize_tree(&1, sort_attributes?, false))\n  end\n\n  defp normalize_tree(binary, _sort_attributes?, true) when is_binary(binary) do\n    if String.trim(binary) != \"\" do\n      binary\n    else\n      nil\n    end\n  end\n\n  defp normalize_tree(value, _sort_attributes?, _trim_whitespace?), do: value\n\n  defmacro sigil_X({:<<>>, _, [binary]}, []) when is_binary(binary) do\n    Macro.escape(normalize_to_tree(binary, sort_attributes: true))\n  end\n\n  defmacro sigil_x(term, []) do\n    quote do\n      unquote(__MODULE__).normalize_to_tree(unquote(term), sort_attributes: true)\n    end\n  end\n\n  def t2h(template) do\n    template\n    |> Phoenix.LiveViewTest.rendered_to_string()\n    |> normalize_to_tree(sort_attributes: true)\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/test/upload_client.ex",
    "content": "defmodule Phoenix.LiveViewTest.UploadClient do\n  @moduledoc false\n  use GenServer\n  require Logger\n\n  alias Phoenix.LiveViewTest.{Upload, ClientProxy}\n\n  def child_spec(opts) do\n    %{\n      id: make_ref(),\n      start: {__MODULE__, :start_link, [opts]},\n      restart: :temporary\n    }\n  end\n\n  def channel_pids(%Upload{pid: pid}) do\n    GenServer.call(pid, :channel_pids)\n  end\n\n  def fetch_allow_acknowledged(%Upload{pid: pid}, entry_name) do\n    GenServer.call(pid, {:fetch_allow_acknowledged, entry_name})\n  end\n\n  def chunk(%Upload{pid: pid, element: element}, name, percent, proxy_pid) do\n    GenServer.call(pid, {:chunk, name, percent, proxy_pid, element})\n  catch\n    :exit, {{:shutdown, :closed}, _} -> {:ok, :closed}\n    :exit, {{:shutdown, {:redirect, opts}}, _} -> {:error, {:redirect, opts}}\n    :exit, {{:shutdown, {:live_redirect, opts}}, _} -> {:error, {:live_redirect, opts}}\n  end\n\n  def simulate_attacker_chunk(%Upload{pid: pid}, name, chunk) do\n    GenServer.call(pid, {:simulate_attacker_chunk, name, chunk})\n  end\n\n  def allowed_ack(%Upload{pid: pid, entries: entries}, ref, config, name, entries_resp, errors) do\n    GenServer.call(pid, {:allowed_ack, ref, config, name, entries, entries_resp, errors})\n  end\n\n  def start_link(opts) do\n    GenServer.start_link(__MODULE__, Keyword.merge(opts, caller: self()))\n  end\n\n  def init(opts) do\n    cid = Keyword.fetch!(opts, :cid)\n    socket = Keyword.get(opts, :socket)\n    socket = socket && put_in(socket.transport_pid, self())\n    {:ok, %{socket: socket, cid: cid, upload_ref: nil, config: %{}, entries: %{}}}\n  end\n\n  def handle_call({:fetch_allow_acknowledged, entry_name}, _from, state) do\n    case Map.fetch(state.entries, entry_name) do\n      {:ok, {:error, reason}} -> {:reply, {:error, reason}, state}\n      {:ok, token} -> {:reply, {:ok, token}, state}\n      :error -> {:reply, {:error, :nopreflight}, state}\n    end\n  end\n\n  def handle_call(\n        {:allowed_ack, upload_ref, config, name, entries, entries_resp, errors},\n        _from,\n        state\n      ) do\n    new_entries =\n      Enum.reduce(entries, state.entries, fn\n        %{\"ref\" => ref, \"name\" => name} = client_entry, acc ->\n          case entries_resp do\n            %{^ref => token} ->\n              Map.put(acc, name, build_and_join_entry(state, client_entry, token))\n\n            %{} ->\n              Map.put(acc, name, {:error, Map.get(errors, ref, :not_allowed)})\n          end\n      end)\n\n    new_state = %{state | upload_ref: upload_ref, config: config, entries: new_entries}\n\n    case new_entries do\n      %{^name => {:error, reason}} -> {:reply, {:error, reason}, new_state}\n      %{^name => _} -> {:reply, :ok, new_state}\n      %{} -> raise_unknown_entry!(state, name)\n    end\n  end\n\n  def handle_call(:channel_pids, _from, state) do\n    pids = Enum.into(state.entries, %{}, fn {name, entry} -> {name, entry.socket.channel_pid} end)\n    {:reply, pids, state}\n  end\n\n  def handle_call({:chunk, entry_name, percent, proxy_pid, element}, from, state) do\n    {:noreply, chunk_upload(state, from, entry_name, percent, proxy_pid, element)}\n  end\n\n  def handle_call({:simulate_attacker_chunk, entry_name, chunk}, _from, state) do\n    Process.flag(:trap_exit, true)\n    entry = get_entry!(state, entry_name)\n    ref = Phoenix.ChannelTest.push(entry.socket, \"chunk\", {:binary, chunk})\n\n    receive do\n      %Phoenix.Socket.Reply{ref: ^ref, status: status, payload: payload} ->\n        {:stop, :normal, {status, payload}, state}\n    after\n      get_chunk_timeout(state) -> exit(:timeout)\n    end\n  end\n\n  defp build_and_join_entry(%{socket: nil} = _state, client_entry, token) do\n    %{\n      \"name\" => name,\n      \"content\" => content,\n      \"size\" => _,\n      \"type\" => type,\n      \"ref\" => ref\n    } = client_entry\n\n    %{\n      name: name,\n      content: content,\n      size: byte_size(content),\n      type: type,\n      ref: ref,\n      token: token,\n      chunk_percent: 0\n    }\n    |> with_chunk_boundaries()\n  end\n\n  defp build_and_join_entry(state, client_entry, token) do\n    %{\n      \"name\" => name,\n      \"content\" => content,\n      \"size\" => _,\n      \"type\" => type,\n      \"ref\" => ref\n    } = client_entry\n\n    {:ok, _resp, entry_socket} =\n      Phoenix.ChannelTest.subscribe_and_join(state.socket, \"lvu:123\", %{\"token\" => token})\n\n    %{\n      name: name,\n      content: content,\n      size: byte_size(content),\n      type: type,\n      socket: entry_socket,\n      ref: ref,\n      token: token,\n      chunk_percent: 0\n    }\n    |> with_chunk_boundaries()\n  end\n\n  def with_chunk_boundaries(entry) do\n    {boundaries, _} =\n      Enum.map_reduce(99..1//-1, {100, entry.size}, fn\n        x, {prev_perc, prev_bytes} ->\n          bytes = ceil(entry.size * x / 100)\n\n          if bytes == prev_bytes do\n            {{x, {prev_perc, prev_bytes}}, {prev_perc, prev_bytes}}\n          else\n            {{x, bytes}, {x, bytes}}\n          end\n      end)\n\n    Map.put(\n      entry,\n      :chunk_boundaries,\n      boundaries |> Map.new() |> Map.merge(%{0 => 0, 100 => entry.size})\n    )\n  end\n\n  defp progress_stats(entry, percent) when percent in 0..100 do\n    start =\n      case Map.fetch!(entry.chunk_boundaries, entry.chunk_percent) do\n        bytes when is_integer(bytes) -> bytes\n      end\n\n    new_start =\n      case Map.fetch!(entry.chunk_boundaries, entry.chunk_percent + percent) do\n        {result_percent, bytes} ->\n          Logger.warning(\n            \"Filesize cannot be chunked to #{percent}%. #{result_percent - entry.chunk_percent}% will be uploaded.\"\n          )\n\n          bytes\n\n        bytes ->\n          bytes\n      end\n\n    chunk_size = new_start - start\n    new_percent = trunc(new_start / entry.size * 100)\n\n    %{\n      chunk_size: chunk_size,\n      start: start,\n      new_start: new_start,\n      new_percent: new_percent\n    }\n  end\n\n  defp chunk_upload(state, from, entry_name, percent, proxy_pid, element) do\n    entry = get_entry!(state, entry_name)\n\n    if entry.chunk_percent >= 100 do\n      state\n    else\n      do_chunk(state, from, entry, proxy_pid, element, percent)\n    end\n  end\n\n  defp do_chunk(%{socket: nil, cid: cid} = state, from, entry, proxy_pid, element, percent) do\n    stats = progress_stats(entry, percent)\n\n    :ok =\n      ClientProxy.report_upload_progress(\n        proxy_pid,\n        from,\n        element,\n        entry.ref,\n        stats.new_percent,\n        cid\n      )\n\n    update_entry_percent(state, entry, stats.new_percent)\n  end\n\n  defp do_chunk(state, from, entry, proxy_pid, element, percent) do\n    stats = progress_stats(entry, percent)\n\n    chunk =\n      if stats.start + stats.chunk_size > entry.size do\n        :binary.part(entry.content, stats.start, entry.size - stats.start)\n      else\n        :binary.part(entry.content, stats.start, stats.chunk_size)\n      end\n\n    ref = Phoenix.ChannelTest.push(entry.socket, \"chunk\", {:binary, chunk})\n\n    receive do\n      %Phoenix.Socket.Reply{ref: ^ref, status: :ok} ->\n        :ok =\n          ClientProxy.report_upload_progress(\n            proxy_pid,\n            from,\n            element,\n            entry.ref,\n            stats.new_percent,\n            state.cid\n          )\n\n        update_entry_percent(state, entry, stats.new_percent)\n\n      %Phoenix.Socket.Reply{ref: ^ref, status: :error} ->\n        :ok =\n          ClientProxy.report_upload_progress(\n            proxy_pid,\n            from,\n            element,\n            entry.ref,\n            %{\"error\" => \"failure\"},\n            state.cid\n          )\n\n        update_entry_percent(state, entry, stats.new_percent)\n    after\n      get_chunk_timeout(state) -> exit(:timeout)\n    end\n  end\n\n  defp update_entry_percent(state, entry, new_percent) do\n    new_entries =\n      Map.update!(state.entries, entry.name, fn entry -> %{entry | chunk_percent: new_percent} end)\n\n    %{state | entries: new_entries}\n  end\n\n  defp get_entry!(state, name) do\n    case Map.fetch(state.entries, name) do\n      {:ok, entry} -> entry\n      :error -> raise_unknown_entry!(state, name)\n    end\n  end\n\n  defp raise_unknown_entry!(state, name) do\n    raise \"no file input with name \\\"#{name}\\\" found in #{inspect(state.entries)}\"\n  end\n\n  defp get_chunk_timeout(state) do\n    state.socket.assigns[:chunk_timeout] || 10_000\n  end\n\n  def handle_info(:garbage_collect, state) do\n    {:noreply, state}\n  end\n\n  def handle_info({:socket_close, _pid, reason}, state) do\n    {:stop, reason, state}\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/test/utils.ex",
    "content": "defmodule Phoenix.LiveViewTest.Utils do\n  @moduledoc false\n\n  alias Phoenix.LiveViewTest.Upload\n\n  def stringify(%Upload{}, _fun), do: %{}\n\n  def stringify(%{__struct__: _} = struct, fun),\n    do: stringify_value(struct, fun)\n\n  def stringify(%{} = params, fun),\n    do: Enum.into(params, %{}, &stringify_kv(&1, fun))\n\n  def stringify([{_, _} | _] = params, fun),\n    do: Enum.into(params, %{}, &stringify_kv(&1, fun))\n\n  def stringify(params, fun) when is_list(params),\n    do: Enum.map(params, &stringify(&1, fun))\n\n  def stringify(other, fun),\n    do: stringify_value(other, fun)\n\n  def stringify_value(other, fun), do: fun.(other)\n  def stringify_kv({k, v}, fun), do: {to_string(k), stringify(v, fun)}\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/upload.ex",
    "content": "defmodule Phoenix.LiveView.Upload do\n  # Operations integrating Phoenix.LiveView.Socket with UploadConfig.\n  @moduledoc false\n\n  alias Phoenix.LiveView.{Socket, Utils, UploadConfig, UploadEntry}\n\n  @refs_to_names :__phoenix_refs_to_names__\n\n  @doc \"\"\"\n  Allows an upload.\n  \"\"\"\n  def allow_upload(%Socket{} = socket, name, opts)\n      when (is_atom(name) or is_binary(name)) and is_list(opts) do\n    case uploaded_entries(socket, name) do\n      {[], []} ->\n        :ok\n\n      {_, _} ->\n        raise ArgumentError, \"\"\"\n        cannot allow_upload on an existing upload with active entries.\n\n        Use cancel_upload and/or consume_upload to handle the active entries before allowing a new upload.\n        \"\"\"\n    end\n\n    ref = Utils.random_id()\n    uploads = socket.assigns[:uploads] || %{}\n    upload_config = UploadConfig.build(name, ref, opts)\n\n    new_uploads =\n      uploads\n      |> Map.put(name, upload_config)\n      |> Map.update(@refs_to_names, %{ref => name}, fn refs -> Map.put(refs, ref, name) end)\n\n    Utils.assign(socket, :uploads, new_uploads)\n  end\n\n  @doc \"\"\"\n  Disallows a previously allowed upload.\n  \"\"\"\n  def disallow_upload(%Socket{} = socket, name) when is_atom(name) or is_binary(name) do\n    case uploaded_entries(socket, name) do\n      {[], []} ->\n        uploads = socket.assigns[:uploads] || %{}\n\n        upload_config =\n          uploads\n          |> Map.fetch!(name)\n          |> UploadConfig.disallow()\n\n        new_refs =\n          Enum.reduce(uploads[@refs_to_names], uploads[@refs_to_names], fn\n            {ref, ^name}, acc -> Map.delete(acc, ref)\n            {_ref, _name}, acc -> acc\n          end)\n\n        new_uploads =\n          uploads\n          |> Map.put(name, upload_config)\n          |> Map.update!(@refs_to_names, fn _ -> new_refs end)\n\n        Utils.assign(socket, :uploads, new_uploads)\n\n      {_completed, _inprogress} ->\n        raise RuntimeError, \"unable to disallow_upload for an upload with active entries\"\n    end\n  end\n\n  @doc \"\"\"\n  Cancels an upload entry.\n  \"\"\"\n  def cancel_upload(socket, name, entry_ref) do\n    upload_config = Map.fetch!(socket.assigns[:uploads] || %{}, name)\n\n    case UploadConfig.get_entry_by_ref(upload_config, entry_ref) do\n      %UploadEntry{} = entry ->\n        upload_config\n        |> UploadConfig.cancel_entry(entry)\n        |> update_uploads(socket)\n\n      _ ->\n        raise ArgumentError, \"no entry in upload \\\"#{inspect(name)}\\\" with ref \\\"#{entry_ref}\\\"\"\n    end\n  end\n\n  @doc \"\"\"\n  Cancels all uploads that exist.\n\n  Returns the new socket with the cancelled upload configs.\n  \"\"\"\n  def maybe_cancel_uploads(socket) do\n    uploads = socket.assigns[:uploads] || %{}\n\n    uploads\n    |> Map.delete(@refs_to_names)\n    |> Enum.reduce({socket, []}, fn {name, conf}, {socket_acc, conf_acc} ->\n      new_socket =\n        Enum.reduce(conf.entries, socket_acc, fn entry, inner_acc ->\n          cancel_upload(inner_acc, name, entry.ref)\n        end)\n\n      {new_socket, [conf | conf_acc]}\n    end)\n  end\n\n  @doc \"\"\"\n  Updates the entry metadata.\n  \"\"\"\n  def update_upload_entry_meta(%Socket{} = socket, upload_conf_name, %UploadEntry{} = entry, meta) do\n    socket.assigns.uploads\n    |> Map.fetch!(upload_conf_name)\n    |> UploadConfig.update_entry_meta(entry.ref, meta)\n    |> update_uploads(socket)\n  end\n\n  @doc \"\"\"\n  Updates the entry progress.\n\n  Progress is either an integer percently between 0 and 100, or a map\n  with an `\"error\"` key containing the information for a failed upload\n  while in progress on the client.\n  \"\"\"\n  def update_progress(%Socket{} = socket, config_ref, entry_ref, progress)\n      when is_integer(progress) and progress >= 0 and progress <= 100 do\n    socket\n    |> get_upload_by_ref!(config_ref)\n    |> UploadConfig.update_progress(entry_ref, progress)\n    |> update_uploads(socket)\n  end\n\n  def update_progress(%Socket{} = socket, config_ref, entry_ref, %{\"error\" => reason})\n      when is_binary(reason) do\n    conf = get_upload_by_ref!(socket, config_ref)\n\n    if conf.external do\n      put_upload_error(socket, conf.name, entry_ref, :external_client_failure)\n    else\n      socket\n    end\n  end\n\n  @doc \"\"\"\n  Puts the entries into the `%UploadConfig{}`.\n  \"\"\"\n  def put_entries(%Socket{} = socket, %UploadConfig{} = conf, entries, cid) do\n    case UploadConfig.put_entries(%{conf | cid: cid}, entries) do\n      {:ok, new_config} ->\n        {:ok, update_uploads(new_config, socket)}\n\n      {:error, new_config} ->\n        errors_resp = Enum.map(new_config.errors, fn {ref, msg} -> [ref, msg] end)\n        {:error, %{ref: conf.ref, error: errors_resp}, update_uploads(new_config, socket)}\n    end\n  end\n\n  @doc \"\"\"\n  Unregisters a completed entry from an `Phoenix.LiveView.UploadChannel` process.\n  \"\"\"\n  def unregister_completed_entry_upload(%Socket{} = socket, %UploadConfig{} = conf, entry_ref) do\n    conf\n    |> UploadConfig.unregister_completed_entry(entry_ref)\n    |> update_uploads(socket)\n  end\n\n  @doc \"\"\"\n  Registers a new entry upload for an `Phoenix.LiveView.UploadChannel` process.\n  \"\"\"\n  def register_entry_upload(%Socket{} = socket, %UploadConfig{} = conf, pid, entry_ref)\n      when is_pid(pid) do\n    case UploadConfig.register_entry_upload(conf, pid, entry_ref) do\n      {:ok, new_config} ->\n        entry = UploadConfig.get_entry_by_ref(new_config, entry_ref)\n        {:ok, update_uploads(new_config, socket), entry}\n\n      {:error, reason} ->\n        {:error, reason}\n    end\n  end\n\n  @doc \"\"\"\n  Populates the errors for a given entry.\n  \"\"\"\n  def put_upload_error(%Socket{} = socket, conf_name, entry_ref, reason) do\n    conf = Map.fetch!(socket.assigns.uploads, conf_name)\n\n    conf\n    |> UploadConfig.put_error(entry_ref, reason)\n    |> update_uploads(socket)\n  end\n\n  @doc \"\"\"\n  Retrieves the `%UploadConfig{}` from the socket for the provided ref or raises.\n  \"\"\"\n  def get_upload_by_ref!(%Socket{} = socket, config_ref) do\n    uploads = socket.assigns[:uploads] || raise(ArgumentError, no_upload_allowed_message(socket))\n    name = Map.fetch!(uploads[@refs_to_names], config_ref)\n    Map.fetch!(uploads, name)\n  end\n\n  defp no_upload_allowed_message(socket) do\n    \"no uploads have been allowed on \" <>\n      if(socket.assigns[:myself], do: \"component running inside \", else: \"\") <>\n      \"LiveView named #{inspect(socket.view)}\"\n  end\n\n  @doc \"\"\"\n  Returns the `%UploadConfig{}` from the socket for the `Phoenix.LiveView.UploadChannel` pid.\n  \"\"\"\n  def get_upload_by_pid(socket, pid) when is_pid(pid) do\n    Enum.find_value(socket.assigns[:uploads] || %{}, fn\n      {@refs_to_names, _} -> false\n      {_name, %UploadConfig{} = conf} -> UploadConfig.get_entry_by_pid(conf, pid) && conf\n    end)\n  end\n\n  @doc \"\"\"\n  Returns the completed and in progress entries for the upload.\n  \"\"\"\n  def uploaded_entries(%Socket{} = socket, name) do\n    entries =\n      case Map.fetch(socket.assigns[:uploads] || %{}, name) do\n        {:ok, conf} -> conf.entries\n        :error -> []\n      end\n\n    Enum.reduce(entries, {[], []}, fn entry, {done, in_progress} ->\n      if entry.done? do\n        {[entry | done], in_progress}\n      else\n        {done, [entry | in_progress]}\n      end\n    end)\n  end\n\n  @doc \"\"\"\n  Consumes the uploaded entries or raises if entries are still in progress.\n  \"\"\"\n  def consume_uploaded_entries(%Socket{} = socket, name, func) when is_function(func, 2) do\n    conf =\n      socket.assigns[:uploads][name] ||\n        raise ArgumentError, \"no upload allowed for #{inspect(name)}\"\n\n    case uploaded_entries(socket, name) do\n      {[_ | _] = done_entries, []} ->\n        consume_entries(conf, done_entries, func)\n\n      {_, [_ | _]} ->\n        raise ArgumentError, \"cannot consume uploaded files when entries are still in progress\"\n\n      {[], []} ->\n        []\n    end\n  end\n\n  @doc \"\"\"\n  Consumes an individual entry or raises if it is still in progress.\n  \"\"\"\n  def consume_uploaded_entry(%Socket{} = socket, %UploadEntry{} = entry, func)\n      when is_function(func, 1) do\n    if !entry.done?,\n      do: raise(ArgumentError, \"cannot consume uploaded files when entries are still in progress\")\n\n    conf = Map.fetch!(socket.assigns[:uploads], entry.upload_config)\n    [result] = consume_entries(conf, [entry], func)\n\n    result\n  end\n\n  @doc \"\"\"\n  Drops all entries from the upload.\n  \"\"\"\n  def drop_upload_entries(%Socket{} = socket, %UploadConfig{} = conf, entry_refs) do\n    conf.entries\n    |> Enum.filter(fn entry -> entry.ref in entry_refs end)\n    |> Enum.reduce(conf, fn entry, acc -> UploadConfig.drop_entry(acc, entry) end)\n    |> update_uploads(socket)\n  end\n\n  defp update_uploads(%UploadConfig{} = new_conf, %Socket{} = socket) do\n    new_uploads = Map.update!(socket.assigns.uploads, new_conf.name, fn _ -> new_conf end)\n    Utils.assign(socket, :uploads, new_uploads)\n  end\n\n  defp consume_entries(%UploadConfig{} = conf, entries, func)\n       when is_list(entries) and is_function(func) do\n    if conf.external do\n      results =\n        entries\n        |> Enum.map(fn entry ->\n          meta = Map.fetch!(conf.entry_refs_to_metas, entry.ref)\n\n          result =\n            cond do\n              is_function(func, 1) -> func.(meta)\n              is_function(func, 2) -> func.(meta, entry)\n            end\n\n          case result do\n            {:ok, return} ->\n              {entry.ref, return}\n\n            {:postpone, return} ->\n              {:postpone, return}\n\n            return ->\n              IO.warn(\"\"\"\n              consuming uploads requires a return signature matching:\n\n                  {:ok, value} | {:postpone, value}\n\n              got:\n\n                  #{inspect(return)}\n              \"\"\")\n\n              {entry.ref, return}\n          end\n        end)\n\n      consumed_refs =\n        Enum.flat_map(results, fn\n          {:postpone, _result} -> []\n          {ref, _result} -> [ref]\n        end)\n\n      Phoenix.LiveView.Channel.drop_upload_entries(conf, consumed_refs)\n\n      Enum.map(results, fn {_ref, result} -> result end)\n    else\n      entries\n      |> Enum.map(fn entry -> {entry, UploadConfig.entry_pid(conf, entry)} end)\n      |> Enum.filter(fn {_entry, pid} -> is_pid(pid) end)\n      |> Enum.map(fn {entry, pid} -> Phoenix.LiveView.UploadChannel.consume(pid, entry, func) end)\n    end\n  end\n\n  @doc \"\"\"\n  Generates a preflight response by calling the `:external` function.\n  \"\"\"\n  def generate_preflight_response(%Socket{} = socket, name, cid, refs) do\n    %UploadConfig{} = conf = Map.fetch!(socket.assigns.uploads, name)\n\n    # don't send more than max_entries preflight responses\n    refs =\n      for {entry, i} <- Enum.with_index(conf.entries),\n          entry.ref in refs,\n          i < conf.max_entries && not entry.preflighted?,\n          do: entry.ref\n\n    client_meta = %{\n      max_file_size: conf.max_file_size,\n      max_entries: conf.max_entries,\n      chunk_size: conf.chunk_size,\n      chunk_timeout: conf.chunk_timeout\n    }\n\n    {new_socket, new_conf, new_entries} = mark_preflighted(socket, conf, refs)\n\n    case new_conf.external do\n      false ->\n        channel_preflight(new_socket, new_conf, new_entries, cid, client_meta)\n\n      func when is_function(func) ->\n        external_preflight(new_socket, new_conf, new_entries, client_meta)\n    end\n  end\n\n  defp mark_preflighted(socket, conf, refs) do\n    {new_conf, new_entries} = UploadConfig.mark_preflighted(conf, refs)\n    new_socket = update_uploads(new_conf, socket)\n    {new_socket, new_conf, new_entries}\n  end\n\n  defp channel_preflight(\n         %Socket{} = socket,\n         %UploadConfig{} = conf,\n         entries,\n         cid,\n         %{} = client_config_meta\n       ) do\n    reply_entries =\n      for entry <- entries, entry.valid?, into: %{} do\n        token =\n          Phoenix.LiveView.Static.sign_token(socket.endpoint, %{\n            pid: self(),\n            ref: {conf.ref, entry.ref},\n            cid: cid\n          })\n\n        {entry.ref, token}\n      end\n\n    errors =\n      for entry <- entries,\n          not entry.valid?,\n          into: %{},\n          do: {entry.ref, entry_errors(conf, entry)}\n\n    reply = %{ref: conf.ref, config: client_config_meta, entries: reply_entries, errors: errors}\n    {:ok, reply, socket}\n  end\n\n  defp entry_errors(%UploadConfig{} = conf, %UploadEntry{} = entry) do\n    for {ref, err} <- conf.errors, ref == entry.ref, do: err\n  end\n\n  defp external_preflight(%Socket{} = socket, %UploadConfig{} = conf, entries, client_config_meta) do\n    reply_entries =\n      Enum.reduce_while(entries, {:ok, %{}, %{}, socket}, fn entry, {:ok, metas, errors, acc} ->\n        if conf.auto_upload? and not entry.valid? do\n          reasons = for {ref, reason} <- conf.errors, ref == entry.ref, do: %{reason: reason}\n          new_errors = Map.put(errors, entry.ref, reasons)\n          {:cont, {:ok, metas, new_errors, acc}}\n        else\n          case conf.external.(entry, acc) do\n            {:ok, %{} = meta, new_socket} ->\n              new_socket = update_upload_entry_meta(new_socket, conf.name, entry, meta)\n              {:cont, {:ok, Map.put(metas, entry.ref, meta), errors, new_socket}}\n\n            {:error, %{} = meta, new_socket} ->\n              if conf.auto_upload? do\n                new_errors = Map.put(errors, entry.ref, [meta])\n                {:cont, {:ok, metas, new_errors, new_socket}}\n              else\n                {:halt, {:error, {entry.ref, meta}, new_socket}}\n              end\n          end\n        end\n      end)\n\n    case reply_entries do\n      {:ok, entry_metas, errors, new_socket} ->\n        reply = %{ref: conf.ref, config: client_config_meta, entries: entry_metas, errors: errors}\n        {:ok, reply, new_socket}\n\n      {:error, {entry_ref, meta_reason}, new_socket} ->\n        new_socket = put_upload_error(new_socket, conf.name, entry_ref, meta_reason)\n        {:error, %{ref: conf.ref, error: [[entry_ref, meta_reason]]}, new_socket}\n    end\n  end\n\n  def register_cid(%Socket{} = socket, ref, cid) do\n    socket\n    |> get_upload_by_ref!(ref)\n    |> UploadConfig.register_cid(cid)\n    |> update_uploads(socket)\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/upload_channel.ex",
    "content": "defmodule Phoenix.LiveView.UploadChannel do\n  @moduledoc false\n  use Phoenix.Channel, log_handle_in: false\n  @timeout :infinity\n\n  alias Phoenix.LiveView.{Static, Channel}\n\n  def cancel(pid) do\n    GenServer.call(pid, :cancel, @timeout)\n  end\n\n  def consume(pid, entry, func) when is_function(func, 1) or is_function(func, 2) do\n    case GenServer.call(pid, :consume_start, @timeout) do\n      {:ok, file_meta} ->\n        try do\n          result =\n            cond do\n              is_function(func, 1) -> func.(file_meta)\n              is_function(func, 2) -> func.(file_meta, entry)\n            end\n\n          case result do\n            {:ok, return} ->\n              GenServer.call(pid, :consume_done, @timeout)\n              return\n\n            {:postpone, return} ->\n              return\n\n            return ->\n              IO.warn(\"\"\"\n              consuming uploads requires a return signature matching:\n\n                  {:ok, value} | {:postpone, value}\n\n              got:\n\n                  #{inspect(return)}\n              \"\"\")\n\n              GenServer.call(pid, :consume_done, @timeout)\n              return\n          end\n        rescue\n          exception ->\n            GenServer.call(pid, :consume_done, @timeout)\n            reraise(exception, __STACKTRACE__)\n        end\n\n      {:error, :in_progress} ->\n        raise RuntimeError, \"cannot consume uploaded file that is still in progress\"\n    end\n  end\n\n  @impl true\n  def join(_topic, auth_payload, socket) do\n    %{\"token\" => token} = auth_payload\n\n    with {:ok, %{pid: pid, ref: ref, cid: cid}} <- Static.verify_token(socket.endpoint, token),\n         {:ok, config} <- Channel.register_upload(pid, ref, cid),\n         %{max_file_size: max_file_size, chunk_timeout: chunk_timeout} = config,\n         {writer, writer_opts} <- config.writer,\n         {:ok, writer_state} <- writer.init(writer_opts) do\n      Process.monitor(pid)\n      Process.flag(:trap_exit, true)\n\n      socket =\n        assign(socket, %{\n          writer: writer,\n          writer_state: writer_state,\n          live_view_pid: pid,\n          max_file_size: max_file_size,\n          chunk_timeout: chunk_timeout,\n          chunk_timer: nil,\n          writer_closed?: false,\n          done?: false,\n          uploaded_size: 0\n        })\n\n      {:ok, socket}\n    else\n      {:error, reason} when reason in [:expired, :invalid] ->\n        {:error, %{reason: :invalid_token}}\n\n      {:error, reason} when reason in [:already_registered, :disallowed] ->\n        {:error, %{reason: reason}}\n\n      # writer init error\n      {:error, _reason} ->\n        {:error, %{reason: :writer_error}}\n    end\n  end\n\n  @impl true\n  def handle_in(\"chunk\", {:binary, payload}, socket) do\n    %{uploaded_size: uploaded_size, max_file_size: max_file_size} = socket.assigns\n    socket = reschedule_chunk_timer(socket)\n\n    if !socket.assigns.writer_closed? and byte_size(payload) + uploaded_size <= max_file_size do\n      case write_bytes(socket, payload) do\n        {:ok, new_socket} ->\n          {:reply, :ok, new_socket}\n\n        {:error, reason, new_socket} ->\n          new_socket =\n            case close_writer(new_socket, {:error, reason}) do\n              {:ok, new_socket} -> new_socket\n              {:error, _reason, new_socket} -> new_socket\n            end\n\n          Channel.report_writer_error(socket.assigns.live_view_pid, reason)\n\n          {:reply, {:error, %{reason: :writer_error}}, new_socket}\n      end\n    else\n      reply = %{reason: :file_size_limit_exceeded, limit: max_file_size}\n      {:stop, {:shutdown, :closed}, {:error, reply}, socket}\n    end\n  end\n\n  @impl true\n  def handle_info({:EXIT, _pid, reason}, socket) do\n    {:stop, reason, socket}\n  end\n\n  def handle_info(\n        {:DOWN, _, _, live_view_pid, reason},\n        %{assigns: %{live_view_pid: live_view_pid}} = socket\n      ) do\n    reason = if reason == :normal, do: {:shutdown, :closed}, else: reason\n    {:stop, reason, maybe_cancel_writer(socket)}\n  end\n\n  def handle_info(:chunk_timeout, socket) do\n    {:stop, {:shutdown, :closed}, socket}\n  end\n\n  @impl true\n  def handle_call(:consume_start, _from, socket) do\n    if socket.assigns.done? do\n      {:reply, {:ok, file_meta(socket)}, socket}\n    else\n      {:reply, {:error, :in_progress}, socket}\n    end\n  end\n\n  @impl true\n  def handle_call(:consume_done, from, socket) do\n    GenServer.reply(from, :ok)\n    {:stop, {:shutdown, :closed}, socket}\n  end\n\n  def handle_call(:cancel, from, socket) do\n    if socket.assigns.writer_closed? do\n      GenServer.reply(from, :ok)\n      {:stop, {:shutdown, :closed}, socket}\n    else\n      case close_writer(socket, :cancel) do\n        {:ok, new_socket} ->\n          GenServer.reply(from, :ok)\n          {:stop, {:shutdown, :closed}, new_socket}\n\n        {:error, reason, new_socket} ->\n          GenServer.reply(from, {:error, reason})\n          {:stop, {:shutdown, :closed}, new_socket}\n      end\n    end\n  end\n\n  @impl true\n  def terminate(_reason, socket) do\n    _ = maybe_cancel_writer(socket)\n    :ok\n  end\n\n  defp reschedule_chunk_timer(socket) do\n    cancel_timer(socket.assigns.chunk_timer, :chunk_timeout)\n    new_timer = Process.send_after(self(), :chunk_timeout, socket.assigns.chunk_timeout)\n    assign(socket, :chunk_timer, new_timer)\n  end\n\n  defp cancel_timer(nil = _timer, _msg), do: :ok\n\n  defp cancel_timer(timer, msg) do\n    if Process.cancel_timer(timer) do\n      :ok\n    else\n      receive do\n        ^msg -> :ok\n      after\n        0 -> :ok\n      end\n    end\n  end\n\n  defp write_bytes(socket, payload) do\n    case socket.assigns.writer.write_chunk(payload, socket.assigns.writer_state) do\n      {:ok, writer_state} ->\n        socket\n        |> assign(:uploaded_size, socket.assigns.uploaded_size + byte_size(payload))\n        |> assign(:writer_state, writer_state)\n        |> maybe_close_completed_file()\n\n      {:error, reason, writer_state} ->\n        cancel_timer(socket.assigns.chunk_timer, :chunk_timeout)\n        {:error, reason, assign(socket, writer_state: writer_state, chunk_timer: nil)}\n    end\n  end\n\n  defp maybe_close_completed_file(socket) do\n    if socket.assigns.uploaded_size == socket.assigns.max_file_size do\n      case close_writer(socket, :done) do\n        {:ok, socket} -> {:ok, assign(socket, done?: true)}\n        {:error, reason, new_socket} -> {:error, reason, new_socket}\n      end\n    else\n      {:ok, socket}\n    end\n  end\n\n  # we need to handle the case where socket assigns aren't set yet because\n  # we are trapping exits and may enter terminate before joining is complete\n  defp maybe_cancel_writer(socket) do\n    case socket.assigns do\n      %{writer_closed?: false} ->\n        case close_writer(socket, :cancel) do\n          {:ok, new_socket} -> new_socket\n          {:error, _reason, new_socket} -> new_socket\n        end\n\n      %{} ->\n        socket\n    end\n  end\n\n  defp close_writer(socket, reason) do\n    cancel_timer(socket.assigns.chunk_timer, :chunk_timeout)\n    socket = assign(socket, chunk_timer: nil, writer_closed?: true)\n\n    case socket.assigns.writer.close(socket.assigns.writer_state, reason) do\n      {:ok, writer_state} ->\n        {:ok,\n         socket\n         |> assign(writer_state: writer_state)\n         |> garbage_collect()}\n\n      {:error, reason} ->\n        {:error, reason, socket}\n    end\n  end\n\n  defp garbage_collect(socket) do\n    send(socket.transport_pid, :garbage_collect)\n    :erlang.garbage_collect(self())\n\n    socket\n  end\n\n  defp file_meta(socket), do: socket.assigns.writer.meta(socket.assigns.writer_state)\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/upload_config.ex",
    "content": "defmodule Phoenix.LiveView.UploadEntry do\n  @moduledoc \"\"\"\n  The struct representing an upload entry.\n  \"\"\"\n\n  alias Phoenix.LiveView.UploadEntry\n\n  defstruct progress: 0,\n            preflighted?: false,\n            upload_config: nil,\n            upload_ref: nil,\n            ref: nil,\n            uuid: nil,\n            valid?: false,\n            done?: false,\n            cancelled?: false,\n            client_name: nil,\n            client_relative_path: nil,\n            client_size: nil,\n            client_type: nil,\n            client_last_modified: nil,\n            client_meta: nil\n\n  @type t :: %__MODULE__{\n          progress: integer(),\n          upload_config: String.t() | :atom,\n          upload_ref: String.t(),\n          ref: String.t() | nil,\n          uuid: String.t() | nil,\n          valid?: boolean(),\n          done?: boolean(),\n          cancelled?: boolean(),\n          client_name: String.t() | nil,\n          client_relative_path: String.t() | nil,\n          client_size: integer() | nil,\n          client_type: String.t() | nil,\n          client_last_modified: integer() | nil,\n          client_meta: map() | nil\n        }\n\n  @doc false\n  def put_progress(%UploadEntry{} = entry, 100) do\n    %{entry | progress: 100, done?: true}\n  end\n\n  def put_progress(%UploadEntry{} = entry, progress) do\n    %{entry | progress: progress}\n  end\nend\n\ndefmodule Phoenix.LiveView.UploadConfig do\n  @moduledoc \"\"\"\n  The struct representing an upload.\n  \"\"\"\n\n  alias Phoenix.LiveView.UploadConfig\n  alias Phoenix.LiveView.UploadEntry\n\n  @default_max_entries 1\n  @default_max_file_size 8_000_000\n  @default_chunk_size 64_000\n  @default_chunk_timeout 10_000\n\n  @unregistered :unregistered\n  @invalid :invalid\n\n  @too_many_files :too_many_files\n\n  @derive {Inspect,\n           only: [\n             :name,\n             :ref,\n             :entries,\n             :max_entries,\n             :max_file_size,\n             :accept,\n             :errors,\n             :auto_upload?,\n             :progress_event,\n             :writer\n           ]}\n\n  defstruct name: nil,\n            cid: :unregistered,\n            client_key: nil,\n            max_entries: @default_max_entries,\n            max_file_size: @default_max_file_size,\n            chunk_size: @default_chunk_size,\n            chunk_timeout: @default_chunk_timeout,\n            entries: [],\n            entry_refs_to_pids: %{},\n            entry_refs_to_metas: %{},\n            accept: [],\n            acceptable_types: MapSet.new(),\n            acceptable_exts: MapSet.new(),\n            external: false,\n            allowed?: false,\n            ref: nil,\n            errors: [],\n            auto_upload?: false,\n            progress_event: nil,\n            writer: nil\n\n  @type t :: %__MODULE__{\n          name: atom() | String.t(),\n          # a nil cid represents a LiveView socket\n          cid: :unregistered | nil | integer(),\n          client_key: String.t(),\n          max_entries: pos_integer(),\n          max_file_size: pos_integer(),\n          entries: list(),\n          entry_refs_to_pids: %{String.t() => pid() | :unregistered | :done},\n          entry_refs_to_metas: %{String.t() => map()},\n          accept: list() | :any,\n          acceptable_types: MapSet.t(),\n          acceptable_exts: MapSet.t(),\n          external:\n            (UploadEntry.t(), Phoenix.LiveView.Socket.t() ->\n               {:ok | :error, meta :: %{uploader: String.t()}, Phoenix.LiveView.Socket.t()})\n            | false,\n          allowed?: boolean,\n          errors: list(),\n          ref: String.t(),\n          auto_upload?: boolean(),\n          writer: (name :: atom() | String.t(), UploadEntry.t(), Phoenix.LiveView.Socket.t() ->\n                     {module(), term()}),\n          progress_event:\n            (name :: atom() | String.t(), UploadEntry.t(), Phoenix.LiveView.Socket.t() ->\n               {:noreply, Phoenix.LiveView.Socket.t()})\n            | nil\n        }\n\n  @doc false\n  # we require a random_ref in order to ensure unique calls to `allow_upload`\n  # invalidate old uploads on the client and expire old tokens for the same\n  # upload name\n  def build(name, random_ref, [_ | _] = opts) when is_atom(name) or is_binary(name) do\n    {html_accept, acceptable_types, acceptable_exts} =\n      case Keyword.fetch(opts, :accept) do\n        {:ok, [_ | _] = accept} ->\n          {types, exts} = validate_accept_option(accept)\n          {Enum.join(accept, \",\"), types, exts}\n\n        {:ok, :any} ->\n          {:any, MapSet.new(), MapSet.new()}\n\n        {:ok, other} ->\n          raise ArgumentError, \"\"\"\n          invalid accept filter provided to allow_upload.\n\n          A list of the following unique file type specifiers are supported:\n\n            * A valid case-insensitive filename extension, starting with a period (\".\") character.\n              For example: .jpg, .pdf, or .doc.\n\n            * A valid MIME type string, such as \"image/jpeg\" or \"image/*\"\n\n          Alternately, you can provide the atom :any to allow any kind of file. Got:\n\n          #{inspect(other)}\n          \"\"\"\n\n        :error ->\n          raise ArgumentError, \"\"\"\n          the :accept option is required when allowing uploads.\n\n          Provide a list of unique file type specifiers or the atom :any to allow any kind of file.\n          \"\"\"\n      end\n\n    external =\n      case Keyword.fetch(opts, :external) do\n        {:ok, func} when is_function(func, 2) ->\n          func\n\n        {:ok, other} ->\n          raise ArgumentError, \"\"\"\n          invalid :external value provided to allow_upload.\n\n          Only an anymous function receiving the socket as an argument is supported. Got:\n\n          #{inspect(other)}\n          \"\"\"\n\n        :error ->\n          false\n      end\n\n    max_entries =\n      case Keyword.fetch(opts, :max_entries) do\n        {:ok, pos_integer} when is_integer(pos_integer) and pos_integer > 0 ->\n          pos_integer\n\n        {:ok, other} ->\n          raise ArgumentError, \"\"\"\n          invalid :max_entries value provided to allow_upload.\n\n          Only a positive integer is supported (Defaults to #{@default_max_entries}). Got:\n\n          #{inspect(other)}\n          \"\"\"\n\n        :error ->\n          @default_max_entries\n      end\n\n    max_file_size =\n      case Keyword.fetch(opts, :max_file_size) do\n        {:ok, pos_integer} when is_integer(pos_integer) and pos_integer > 0 ->\n          pos_integer\n\n        {:ok, other} ->\n          raise ArgumentError, \"\"\"\n          invalid :max_file_size value provided to allow_upload.\n\n          Only a positive integer is supported (Defaults to #{@default_max_file_size} bytes). Got:\n\n          #{inspect(other)}\n          \"\"\"\n\n        :error ->\n          @default_max_file_size\n      end\n\n    chunk_size =\n      case Keyword.fetch(opts, :chunk_size) do\n        {:ok, pos_integer} when is_integer(pos_integer) and pos_integer > 0 ->\n          pos_integer\n\n        {:ok, other} ->\n          raise ArgumentError, \"\"\"\n          invalid :chunk_size value provided to allow_upload.\n\n          Only a positive integer is supported (Defaults to #{@default_chunk_size} bytes). Got:\n\n          #{inspect(other)}\n          \"\"\"\n\n        :error ->\n          @default_chunk_size\n      end\n\n    chunk_timeout =\n      case Keyword.fetch(opts, :chunk_timeout) do\n        {:ok, pos_integer} when is_integer(pos_integer) and pos_integer > 0 ->\n          pos_integer\n\n        {:ok, other} ->\n          raise ArgumentError, \"\"\"\n          invalid :chunk_timeout value provided to allow_upload.\n\n          Only a positive integer in milliseconds is supported (Defaults to #{@default_chunk_timeout} ms). Got:\n\n          #{inspect(other)}\n          \"\"\"\n\n        :error ->\n          @default_chunk_timeout\n      end\n\n    progress_event =\n      case Keyword.fetch(opts, :progress) do\n        {:ok, func} when is_function(func, 3) ->\n          func\n\n        {:ok, other} ->\n          raise ArgumentError, \"\"\"\n          invalid :progress value provided to allow_upload.\n\n          Only 3-arity anonymous function is supported. Got:\n\n          #{inspect(other)}\n          \"\"\"\n\n        :error ->\n          nil\n      end\n\n    writer =\n      case Keyword.fetch(opts, :writer) do\n        {:ok, func} when is_function(func, 3) ->\n          func\n\n        {:ok, other} ->\n          raise ArgumentError, \"\"\"\n          invalid :writer value provided to allow_upload.\n\n          Only a 3-arity anonymous function is supported. Got:\n\n          #{inspect(other)}\n          \"\"\"\n\n        :error ->\n          fn _name, _entry, _socket -> {Phoenix.LiveView.UploadTmpFileWriter, []} end\n      end\n\n    %UploadConfig{\n      ref: random_ref,\n      name: name,\n      max_entries: max_entries,\n      max_file_size: max_file_size,\n      entry_refs_to_pids: %{},\n      entry_refs_to_metas: %{},\n      accept: html_accept,\n      acceptable_types: acceptable_types,\n      acceptable_exts: acceptable_exts,\n      external: external,\n      chunk_size: chunk_size,\n      chunk_timeout: chunk_timeout,\n      progress_event: progress_event,\n      writer: writer,\n      auto_upload?: Keyword.get(opts, :auto_upload, false),\n      allowed?: true\n    }\n  end\n\n  @doc false\n  def entry_pid(%UploadConfig{} = conf, %UploadEntry{} = entry) do\n    case Map.fetch(conf.entry_refs_to_pids, entry.ref) do\n      {:ok, pid} when is_pid(pid) -> pid\n      {:ok, status} when status in [@unregistered, @invalid] -> nil\n    end\n  end\n\n  @doc false\n  def get_entry_by_pid(%UploadConfig{} = conf, channel_pid) when is_pid(channel_pid) do\n    Enum.find_value(conf.entry_refs_to_pids, fn {ref, pid} ->\n      if channel_pid == pid do\n        get_entry_by_ref(conf, ref)\n      end\n    end)\n  end\n\n  @doc false\n  def get_entry_by_ref(%UploadConfig{} = conf, ref) do\n    Enum.find(conf.entries, fn %UploadEntry{} = entry -> entry.ref === ref end)\n  end\n\n  @doc false\n  def unregister_completed_external_entry(%UploadConfig{} = conf, entry_ref) do\n    %UploadEntry{} = entry = get_entry_by_ref(conf, entry_ref)\n\n    drop_entry(conf, entry)\n  end\n\n  @doc false\n  def unregister_completed_entry(%UploadConfig{} = conf, entry_ref) do\n    %UploadEntry{} = entry = get_entry_by_ref(conf, entry_ref)\n\n    drop_entry(conf, entry)\n  end\n\n  @doc false\n  def registered?(%UploadConfig{} = conf) do\n    Enum.find(conf.entry_refs_to_pids, fn {_ref, maybe_pid} -> is_pid(maybe_pid) end)\n  end\n\n  @doc false\n  def mark_preflighted(%UploadConfig{} = conf, refs) do\n    new_entries =\n      for %UploadEntry{} = entry <- conf.entries do\n        %{entry | preflighted?: entry.preflighted? || entry.ref in refs}\n      end\n\n    new_conf = %{conf | entries: new_entries}\n    {new_conf, for(ref <- refs, do: get_entry_by_ref(new_conf, ref))}\n  end\n\n  @doc false\n  def register_entry_upload(%UploadConfig{} = conf, channel_pid, entry_ref)\n      when is_pid(channel_pid) do\n    case Map.fetch(conf.entry_refs_to_pids, entry_ref) do\n      {:ok, @unregistered} ->\n        {:ok,\n         %{\n           conf\n           | entry_refs_to_pids: Map.put(conf.entry_refs_to_pids, entry_ref, channel_pid)\n         }}\n\n      {:ok, existing_pid} when is_pid(existing_pid) ->\n        {:error, :already_registered}\n\n      :error ->\n        {:error, :disallowed}\n    end\n  end\n\n  # specifics on the `accept` attribute are illuminated in the spec:\n  # https://html.spec.whatwg.org/multipage/input.html#attr-input-accept\n  @accept_wildcards ~w(audio/* image/* video/*)\n\n  defp validate_accept_option(accept) do\n    {types, exts} =\n      Enum.reduce(accept, {[], []}, fn opt, {types_acc, exts_acc} ->\n        {type, exts} = accept_option!(opt)\n        {[type | types_acc], exts ++ exts_acc}\n      end)\n\n    {MapSet.new(types), MapSet.new(exts)}\n  end\n\n  # wildcards for media files\n  defp accept_option!(key) when key in @accept_wildcards, do: {key, []}\n\n  defp accept_option!(<<\".\" <> extname::binary>> = ext) do\n    if MIME.has_type?(extname) do\n      {MIME.type(extname), [ext]}\n    else\n      raise ArgumentError, \"\"\"\n        invalid accept filter provided to allow_upload.\n\n        Expected a file extension with a known MIME type.\n\n        MIME types can be extended in your application configuration as follows:\n\n        config :mime, :types, %{\n          \"application/vnd.api+json\" => [\"json-api\"]\n        }\n\n        Got:\n\n        #{inspect(extname)}\n      \"\"\"\n    end\n  end\n\n  defp accept_option!(filter) when is_binary(filter) do\n    if MIME.extensions(filter) != [] do\n      {filter, []}\n    else\n      raise ArgumentError, \"\"\"\n        invalid accept filter provided to allow_upload.\n\n        Expected a known MIME type without parameters.\n\n        MIME types can be extended in your application configuration as follows:\n\n        config :mime, :types, %{\n          \"application/vnd.api+json\" => [\"json-api\"]\n        }\n\n        Got:\n\n        #{inspect(filter)}\n      \"\"\"\n    end\n  end\n\n  @doc false\n  def disallow(%UploadConfig{} = conf), do: %{conf | allowed?: false}\n\n  @doc false\n  def uploaded_entries(%UploadConfig{} = conf) do\n    Enum.filter(conf.entries, fn %UploadEntry{} = entry -> entry.progress == 100 end)\n  end\n\n  @doc false\n  def update_entry(%UploadConfig{} = conf, entry_ref, func) do\n    new_entries =\n      Enum.map(conf.entries, fn\n        %UploadEntry{ref: ^entry_ref} = entry -> func.(entry)\n        %UploadEntry{ref: _ef} = entry -> entry\n      end)\n\n    recalculate_computed_fields(%{conf | entries: new_entries})\n  end\n\n  @doc false\n  def update_progress(%UploadConfig{} = conf, entry_ref, progress)\n      when is_integer(progress) and progress >= 0 and progress <= 100 do\n    update_entry(conf, entry_ref, fn entry -> UploadEntry.put_progress(entry, progress) end)\n  end\n\n  @doc false\n  def update_entry_meta(%UploadConfig{} = conf, entry_ref, %{} = meta) do\n    case Map.fetch(meta, :uploader) do\n      {:ok, _} ->\n        :noop\n\n      :error ->\n        raise ArgumentError,\n              \"external uploader metadata requires an :uploader key. Got: #{inspect(meta)}\"\n    end\n\n    new_metas = Map.put(conf.entry_refs_to_metas, entry_ref, meta)\n    %{conf | entry_refs_to_metas: new_metas}\n  end\n\n  @doc false\n  def put_entries(%UploadConfig{} = conf, entries) do\n    pruned_conf = maybe_replace_sole_entry(conf, entries)\n\n    new_conf =\n      Enum.reduce(entries, pruned_conf, fn client_entry, acc ->\n        if get_entry_by_ref(acc, Map.fetch!(client_entry, \"ref\")) do\n          acc\n        else\n          case cast_and_validate_entry(acc, client_entry) do\n            {:ok, new_conf} -> new_conf\n            {:error, new_conf} -> new_conf\n          end\n        end\n      end)\n\n    too_many? = too_many_files?(new_conf)\n\n    cond do\n      too_many? && new_conf.auto_upload? ->\n        {:ok, put_error(new_conf, new_conf.ref, @too_many_files)}\n\n      too_many? ->\n        {:error, put_error(new_conf, new_conf.ref, @too_many_files)}\n\n      new_conf.auto_upload? ->\n        {:ok, new_conf}\n\n      new_conf.errors != [] ->\n        {:error, new_conf}\n\n      true ->\n        {:ok, new_conf}\n    end\n  end\n\n  defp maybe_replace_sole_entry(%UploadConfig{max_entries: 1} = conf, new_entries) do\n    with [entry] <- conf.entries,\n         [new_entry] <- new_entries,\n         true <- entry.ref != Map.fetch!(new_entry, \"ref\") do\n      cancel_entry(conf, entry)\n    else\n      _ -> conf\n    end\n  end\n\n  defp maybe_replace_sole_entry(%UploadConfig{} = conf, _new_entries) do\n    conf\n  end\n\n  defp too_many_files?(%UploadConfig{entries: entries, max_entries: max}) do\n    length(entries) > max\n  end\n\n  defp cast_and_validate_entry(%UploadConfig{} = conf, %{\"ref\" => ref} = client_entry) do\n    :error = Map.fetch(conf.entry_refs_to_pids, ref)\n\n    entry = %UploadEntry{\n      ref: ref,\n      upload_ref: conf.ref,\n      upload_config: conf.name,\n      client_name: Map.fetch!(client_entry, \"name\"),\n      client_relative_path: Map.get(client_entry, \"relative_path\"),\n      client_size: Map.fetch!(client_entry, \"size\"),\n      client_type: Map.fetch!(client_entry, \"type\"),\n      client_last_modified: Map.get(client_entry, \"last_modified\"),\n      client_meta: Map.get(client_entry, \"meta\")\n    }\n\n    {:ok, entry}\n    |> validate_max_file_size(conf)\n    |> validate_accepted(conf)\n    |> case do\n      {:ok, entry} ->\n        {:ok, put_valid_entry(conf, entry)}\n\n      {:error, reason} ->\n        {:error, put_invalid_entry(conf, entry, reason)}\n    end\n  end\n\n  defp put_valid_entry(%UploadConfig{} = conf, %UploadEntry{} = entry) do\n    entry = %{entry | valid?: true, uuid: generate_uuid()}\n    new_pids = Map.put(conf.entry_refs_to_pids, entry.ref, @unregistered)\n    new_metas = Map.put(conf.entry_refs_to_metas, entry.ref, %{})\n\n    %{\n      conf\n      | entries: conf.entries ++ [entry],\n        entry_refs_to_pids: new_pids,\n        entry_refs_to_metas: new_metas\n    }\n  end\n\n  defp put_invalid_entry(%UploadConfig{} = conf, %UploadEntry{} = entry, reason) do\n    entry = %{entry | valid?: false}\n    new_pids = Map.put(conf.entry_refs_to_pids, entry.ref, @invalid)\n    new_metas = Map.put(conf.entry_refs_to_metas, entry.ref, %{})\n\n    new_conf = %{\n      conf\n      | entries: conf.entries ++ [entry],\n        entry_refs_to_pids: new_pids,\n        entry_refs_to_metas: new_metas\n    }\n\n    put_error(new_conf, entry.ref, reason)\n  end\n\n  defp validate_max_file_size(\n         {:ok, %UploadEntry{client_size: size}},\n         %UploadConfig{max_file_size: max}\n       )\n       when size > max or not is_integer(size),\n       do: {:error, :too_large}\n\n  defp validate_max_file_size({:ok, entry}, _conf), do: {:ok, entry}\n\n  defp validate_accepted({:ok, %UploadEntry{} = entry}, conf) do\n    if accepted?(conf, entry) do\n      {:ok, entry}\n    else\n      {:error, :not_accepted}\n    end\n  end\n\n  defp validate_accepted({:error, _} = error, _conf), do: error\n\n  defp accepted?(%UploadConfig{accept: :any}, %UploadEntry{}), do: true\n\n  defp accepted?(\n         %UploadConfig{acceptable_types: acceptable_types} = conf,\n         %UploadEntry{client_type: client_type} = entry\n       ) do\n    cond do\n      # wildcard\n      String.starts_with?(client_type, \"image/\") and \"image/*\" in acceptable_types -> true\n      String.starts_with?(client_type, \"audio/\") and \"audio/*\" in acceptable_types -> true\n      String.starts_with?(client_type, \"video/\") and \"video/*\" in acceptable_types -> true\n      # strict\n      client_type in acceptable_types -> true\n      String.downcase(Path.extname(entry.client_name), :ascii) in conf.acceptable_exts -> true\n      true -> false\n    end\n  end\n\n  defp recalculate_computed_fields(%UploadConfig{} = conf) do\n    recalculate_errors(conf)\n  end\n\n  defp recalculate_errors(%UploadConfig{ref: ref} = conf) do\n    if too_many_files?(conf) do\n      conf\n    else\n      new_errors =\n        Enum.filter(conf.errors, fn\n          {^ref, @too_many_files} -> false\n          _ -> true\n        end)\n\n      %{conf | errors: new_errors}\n    end\n  end\n\n  @doc false\n  def put_error(%UploadConfig{} = conf, _entry_ref, @too_many_files = reason) do\n    pair = {conf.ref, reason}\n    %{conf | errors: List.delete(conf.errors, pair) ++ [pair]}\n  end\n\n  def put_error(%UploadConfig{} = conf, entry_ref, reason) do\n    %{conf | errors: conf.errors ++ [{entry_ref, reason}]}\n  end\n\n  @doc false\n  def cancel_entry(%UploadConfig{} = conf, %UploadEntry{} = entry) do\n    case entry_pid(conf, entry) do\n      channel_pid when is_pid(channel_pid) ->\n        Phoenix.LiveView.UploadChannel.cancel(channel_pid)\n        update_entry(conf, entry.ref, fn entry -> %{entry | cancelled?: true} end)\n\n      _ ->\n        drop_entry(conf, entry)\n    end\n  end\n\n  @doc false\n  def drop_entry(%UploadConfig{} = conf, %UploadEntry{ref: ref}) do\n    new_entries = for entry <- conf.entries, entry.ref != ref, do: entry\n    new_errors = Enum.filter(conf.errors, fn {error_ref, _} -> error_ref != ref end)\n    new_refs = Map.delete(conf.entry_refs_to_pids, ref)\n    new_metas = Map.delete(conf.entry_refs_to_metas, ref)\n\n    new_conf = %{\n      conf\n      | entries: new_entries,\n        errors: new_errors,\n        entry_refs_to_pids: new_refs,\n        entry_refs_to_metas: new_metas\n    }\n\n    recalculate_computed_fields(new_conf)\n  end\n\n  @doc false\n  def register_cid(%UploadConfig{} = conf, cid) do\n    %{conf | cid: cid}\n  end\n\n  # UUID generation\n  # Copyright (c) 2013 Plataformatec\n  # Copyright (c) 2020 Dashbit\n  # https://github.com/elixir-ecto/ecto/blob/99dff4c4403c258ea939fe9bdfb4e339baf05e13/lib/ecto/uuid.ex\n  defp generate_uuid do\n    <<u0::48, _::4, u1::12, _::2, u2::62>> = :crypto.strong_rand_bytes(16)\n    bin = <<u0::48, 4::4, u1::12, 2::2, u2::62>>\n\n    <<a1::4, a2::4, a3::4, a4::4, a5::4, a6::4, a7::4, a8::4, b1::4, b2::4, b3::4, b4::4, c1::4,\n      c2::4, c3::4, c4::4, d1::4, d2::4, d3::4, d4::4, e1::4, e2::4, e3::4, e4::4, e5::4, e6::4,\n      e7::4, e8::4, e9::4, e10::4, e11::4, e12::4>> = bin\n\n    <<e(a1), e(a2), e(a3), e(a4), e(a5), e(a6), e(a7), e(a8), ?-, e(b1), e(b2), e(b3), e(b4), ?-,\n      e(c1), e(c2), e(c3), e(c4), ?-, e(d1), e(d2), e(d3), e(d4), ?-, e(e1), e(e2), e(e3), e(e4),\n      e(e5), e(e6), e(e7), e(e8), e(e9), e(e10), e(e11), e(e12)>>\n  end\n\n  @compile {:inline, e: 1}\n  defp e(0), do: ?0\n  defp e(1), do: ?1\n  defp e(2), do: ?2\n  defp e(3), do: ?3\n  defp e(4), do: ?4\n  defp e(5), do: ?5\n  defp e(6), do: ?6\n  defp e(7), do: ?7\n  defp e(8), do: ?8\n  defp e(9), do: ?9\n  defp e(10), do: ?a\n  defp e(11), do: ?b\n  defp e(12), do: ?c\n  defp e(13), do: ?d\n  defp e(14), do: ?e\n  defp e(15), do: ?f\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/upload_tmp_file_writer.ex",
    "content": "defmodule Phoenix.LiveView.UploadTmpFileWriter do\n  @moduledoc false\n\n  @behaviour Phoenix.LiveView.UploadWriter\n\n  @impl true\n  def init(_opts) do\n    with {:ok, path} <- Plug.Upload.random_file(\"live_view_upload\"),\n         {:ok, file} <- File.open(path, [:binary, :write]) do\n      {:ok, %{path: path, file: file}}\n    end\n  end\n\n  @impl true\n  def meta(state) do\n    %{path: state.path}\n  end\n\n  @impl true\n  def write_chunk(data, state) do\n    case IO.binwrite(state.file, data) do\n      :ok -> {:ok, state}\n      {:error, reason} -> {:error, reason, state}\n    end\n  end\n\n  @impl true\n  def close(state, _reason) do\n    case File.close(state.file) do\n      :ok -> {:ok, state}\n      {:error, reason} -> {:error, reason}\n    end\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/upload_writer.ex",
    "content": "defmodule Phoenix.LiveView.UploadWriter do\n  @moduledoc ~S\"\"\"\n  Provide a behavior for writing uploaded chunks to a final destination.\n\n  By default, uploads are written to a temporary file on the server and\n  consumed by the LiveView by reading the temporary file or copying it to\n  durable location. Some usecases require custom handling of the uploaded\n  chunks, such as streaming a user's upload to another server. In these cases,\n  we don't want the chunks to be written to disk since we only need to forward\n  them on.\n\n  **Note**: Upload writers run inside the channel uploader process, so\n  any blocking work will block the channel  errors will crash the channel process.\n\n  Custom implementations of `Phoenix.LiveView.UploadWriter` can be passed to\n  `allow_upload/3`. To initialize the writer with options, define a 3-arity function\n  that returns a tuple of `{writer, writer_opts}`. For example imagine\n  an upload writer that logs the chunk sizes and tracks the total bytes sent by the\n  client:\n\n      socket\n      |> allow_upload(:avatar,\n        accept: :any,\n        writer: fn _name, _entry, _socket -> {EchoWriter, level: :debug} end\n      )\n\n  And such an `EchoWriter` could look like this:\n\n      defmodule EchoWriter do\n        @behaviour Phoenix.LiveView.UploadWriter\n\n        require Logger\n\n        @impl true\n        def init(opts) do\n          {:ok, %{total: 0, level: Keyword.fetch!(opts, :level)}}\n        end\n\n        @impl true\n        def meta(state), do: %{level: state.level}\n\n        @impl true\n        def write_chunk(data, state) do\n          size = byte_size(data)\n          Logger.log(state.level, \"received chunk of #{size} bytes\")\n          {:ok, %{state | total: state.total + size}}\n        end\n\n        @impl true\n        def close(state, reason) do\n          Logger.log(state.level, \"closing upload after #{state.total} bytes, #{inspect(reason)}\")\n          {:ok, state}\n        end\n      end\n\n  When the LiveView consumes the uploaded entry, it will receive the `%{level: ...}`\n  returned from the meta callback. This allows the writer to keep state as it handles\n  chunks to be later relayed to the LiveView when consumed.\n\n  ## Close reasons\n\n  The `close/2` callback is called when the upload is complete or cancelled. The following\n  values can be passed:\n\n    * `:done` - The client sent all expected chunks and the upload is awaiting consumption\n    * `:cancel` - The upload was canceled, either by the server or the client navigating away.\n    * `{:error, reason}` - The upload was canceled due to an error returned from `write_chunk/2`.\n      For example, if `write_chunk/2` returns `{:error, :enoent, state}`, the upload will be cancelled\n      and `close/2` will be called with the reason `{:error, :enoent}`.\n  \"\"\"\n\n  @callback init(opts :: term) :: {:ok, state :: term} | {:error, term}\n  @callback meta(state :: term) :: map\n  @callback write_chunk(data :: binary, state :: term) ::\n              {:ok, state :: term} | {:error, reason :: term, state :: term}\n  @callback close(state :: term, reason :: :done | :cancel | {:error, term}) ::\n              {:ok, state :: term} | {:error, term}\nend\n"
  },
  {
    "path": "lib/phoenix_live_view/utils.ex",
    "content": "defmodule Phoenix.LiveView.Utils do\n  # Shared helpers used mostly by Channel and Diff,\n  # but also Static, and LiveViewTest.\n  @moduledoc false\n\n  alias Phoenix.LiveView.{Socket, Lifecycle}\n\n  # All available mount options\n  @mount_opts [:temporary_assigns, :layout]\n\n  @max_flash_age :timer.seconds(60)\n\n  @valid_uri_schemes [\n    \"http:\",\n    \"https:\",\n    \"ftp:\",\n    \"ftps:\",\n    \"mailto:\",\n    \"news:\",\n    \"irc:\",\n    \"gopher:\",\n    \"nntp:\",\n    \"feed:\",\n    \"telnet:\",\n    \"mms:\",\n    \"rtsp:\",\n    \"svn:\",\n    \"tel:\",\n    \"fax:\",\n    \"xmpp:\"\n  ]\n\n  @doc \"\"\"\n  Assigns a value if it changed.\n  \"\"\"\n  def assign(%Socket{} = socket, key, value) do\n    case socket do\n      %{assigns: %{^key => ^value}} -> socket\n      %{} -> force_assign(socket, key, value)\n    end\n  end\n\n  @doc \"\"\"\n  Assigns the given `key` with value from `fun` into `socket_or_assigns` if one does not yet exist.\n  \"\"\"\n  def assign_new(%Socket{} = socket, key, fun) when is_function(fun, 1) do\n    case socket do\n      %{assigns: %{^key => _}} ->\n        socket\n\n      %{private: %{assign_new: {assigns, keys}}} ->\n        # It is important to store the keys even if they are not in assigns\n        # because maybe the controller doesn't have it but the view does.\n        socket = put_in(socket.private.assign_new, {assigns, [key | keys]})\n\n        Phoenix.LiveView.Utils.force_assign(\n          socket,\n          key,\n          case assigns do\n            %{^key => value} -> value\n            %{} -> fun.(socket.assigns)\n          end\n        )\n\n      %{assigns: assigns} ->\n        Phoenix.LiveView.Utils.force_assign(socket, key, fun.(assigns))\n    end\n  end\n\n  def assign_new(%Socket{} = socket, key, fun) when is_function(fun, 0) do\n    case socket do\n      %{assigns: %{^key => _}} ->\n        socket\n\n      %{private: %{assign_new: {assigns, keys}}} ->\n        # It is important to store the keys even if they are not in assigns\n        # because maybe the controller doesn't have it but the view does.\n        socket = put_in(socket.private.assign_new, {assigns, [key | keys]})\n        Phoenix.LiveView.Utils.force_assign(socket, key, Map.get_lazy(assigns, key, fun))\n\n      %{} ->\n        Phoenix.LiveView.Utils.force_assign(socket, key, fun.())\n    end\n  end\n\n  @doc \"\"\"\n  Forces an assign on a socket.\n  \"\"\"\n  def force_assign(%Socket{assigns: assigns} = socket, key, val) do\n    %{socket | assigns: force_assign(assigns, assigns.__changed__, key, val)}\n  end\n\n  @doc \"\"\"\n  Forces an assign with the given changed map.\n  \"\"\"\n  def force_assign(assigns, nil, key, val), do: Map.put(assigns, key, val)\n\n  def force_assign(assigns, changed, key, val) do\n    # If the current value is a composite type (list, map, tuple),\n    # we store it in changed so we can perform nested change tracking.\n    # Also note the use of put_new is important.\n    # We want to keep the original value from assigns and not any\n    # intermediate ones that may appear.\n    changed_val =\n      case Map.get(assigns, key) do\n        val when is_list(val) or is_map(val) or is_tuple(val) -> val\n        _ -> true\n      end\n\n    changed = Map.put_new(changed, key, changed_val)\n    Map.put(%{assigns | __changed__: changed}, key, val)\n  end\n\n  @doc \"\"\"\n  Clears the changes from the socket assigns.\n  \"\"\"\n  def clear_changed(%Socket{private: private, assigns: assigns} = socket) do\n    temporary = Map.get(private, :temporary_assigns, %{})\n    %{socket | assigns: assigns |> Map.merge(temporary) |> Map.put(:__changed__, %{})}\n  end\n\n  @doc \"\"\"\n  Clears temporary data (flash, pushes, etc) from the socket privates.\n  \"\"\"\n  def clear_temp(socket) do\n    put_in(socket.private.live_temp, %{})\n  end\n\n  @doc \"\"\"\n  Checks if the socket changed.\n  \"\"\"\n  def changed?(%Socket{assigns: %{__changed__: changed}}), do: changed != %{}\n\n  @doc \"\"\"\n  Checks if the given assign changed.\n  \"\"\"\n  def changed?(%Socket{} = socket, assign), do: changed?(socket.assigns, assign)\n  def changed?(%{__changed__: nil}, _assign), do: true\n  def changed?(%{__changed__: changed}, assign), do: Map.has_key?(changed, assign)\n\n  @doc \"\"\"\n  Returns the CID of the given socket.\n  \"\"\"\n  def cid(%Socket{assigns: %{myself: %Phoenix.LiveComponent.CID{} = cid}}), do: cid\n  def cid(%Socket{}), do: nil\n\n  @doc \"\"\"\n  Configures the socket for use.\n  \"\"\"\n  def configure_socket(%Socket{id: nil} = socket, private, action, flash, host_uri) do\n    %{\n      socket\n      | id: random_id(),\n        private: private,\n        assigns: configure_assigns(socket.assigns, action, flash),\n        host_uri: prune_uri(host_uri)\n    }\n  end\n\n  def configure_socket(%Socket{} = socket, private, action, flash, host_uri) do\n    assigns = configure_assigns(socket.assigns, action, flash)\n    %{socket | host_uri: prune_uri(host_uri), private: private, assigns: assigns}\n  end\n\n  defp configure_assigns(assigns, action, flash) do\n    Map.merge(assigns, %{live_action: action, flash: flash})\n  end\n\n  defp prune_uri(:not_mounted_at_router), do: :not_mounted_at_router\n\n  defp prune_uri(url) do\n    %URI{host: host, port: port, scheme: scheme} = url\n\n    if host == nil do\n      raise \"client did not send full URL, missing host in #{url}\"\n    end\n\n    %URI{host: host, port: port, scheme: scheme}\n  end\n\n  @doc \"\"\"\n  Returns a random ID with valid DOM tokens\n  \"\"\"\n  def random_id do\n    \"phx-\" <> random_encoded_bytes()\n  end\n\n  @doc \"\"\"\n  Prunes any data no longer needed after mount.\n  \"\"\"\n  def post_mount_prune(%Socket{} = socket) do\n    socket\n    |> clear_changed()\n    |> clear_temp()\n    |> drop_private([:connect_info, :connect_params, :assign_new])\n  end\n\n  @doc \"\"\"\n  Validate and normalizes the layout.\n  \"\"\"\n  def normalize_layout(false), do: false\n\n  def normalize_layout({mod, layout}) when is_atom(mod) and is_atom(layout) do\n    {mod, Atom.to_string(layout)}\n  end\n\n  def normalize_layout(other) do\n    raise ArgumentError,\n          \":layout expects a tuple of the form {MyLayouts, :my_template} or false, \" <>\n            \"got: #{inspect(other)}\"\n  end\n\n  @doc \"\"\"\n  Returns the socket's flash messages.\n  \"\"\"\n  def get_flash(%Socket{assigns: assigns}), do: assigns.flash\n  def get_flash(%{} = flash, key), do: flash[key]\n\n  @doc \"\"\"\n  Puts a new flash with the socket's flash messages.\n  \"\"\"\n  def replace_flash(%Socket{} = socket, %{} = new_flash) do\n    assign(socket, :flash, new_flash)\n  end\n\n  @doc \"\"\"\n  Clears the flash.\n  \"\"\"\n  def clear_flash(%Socket{} = socket) do\n    assign(socket, :flash, %{})\n  end\n\n  @doc \"\"\"\n  Clears the key from the flash.\n  \"\"\"\n  def clear_flash(%Socket{} = socket, key) do\n    key = flash_key(key)\n    new_flash = Map.delete(socket.assigns.flash, key)\n\n    socket = assign(socket, :flash, new_flash)\n    update_in(socket.private.live_temp[:flash], &Map.delete(&1 || %{}, key))\n  end\n\n  @doc \"\"\"\n  Puts a flash message in the socket.\n  \"\"\"\n  def put_flash(%Socket{assigns: assigns} = socket, key, msg) do\n    key = flash_key(key)\n    new_flash = Map.put(assigns.flash, key, msg)\n\n    socket = assign(socket, :flash, new_flash)\n    update_in(socket.private.live_temp[:flash], &Map.put(&1 || %{}, key, msg))\n  end\n\n  @doc \"\"\"\n  Returns a map of the flash messages which have changed.\n  \"\"\"\n  def changed_flash(%Socket{} = socket) do\n    socket.private.live_temp[:flash] || %{}\n  end\n\n  defp flash_key(binary) when is_binary(binary), do: binary\n  defp flash_key(atom) when is_atom(atom), do: Atom.to_string(atom)\n\n  @doc \"\"\"\n  Annotates the changes with the event to be pushed.\n\n  By default, events are dispatched on the JavaScript side only after\n  the current patch is invoked. Therefore, if the LiveView\n  redirects, the events won't be invoked. If the `dispatch: :before` option\n  is passed, this event will be dispatched before patching the DOM.\n  \"\"\"\n  def push_event(%Socket{} = socket, event, %{} = payload, opts) do\n    opts = Keyword.validate!(opts, [:dispatch])\n    dispatch_phase = Keyword.get(opts, :dispatch, :after)\n\n    case dispatch_phase do\n      :after ->\n        update_in(socket.private.live_temp[:push_events], &[[event, payload] | &1 || []])\n\n      :before ->\n        update_in(socket.private.live_temp[:push_events], &[[event, payload, true] | &1 || []])\n    end\n  end\n\n  @doc \"\"\"\n  Annotates the reply in the socket changes.\n  \"\"\"\n  def put_reply(%Socket{} = socket, %{} = payload) do\n    put_in(socket.private.live_temp[:push_reply], payload)\n  end\n\n  @doc \"\"\"\n  Returns the push events in the socket.\n  \"\"\"\n  def get_push_events(%Socket{} = socket) do\n    Enum.reverse(socket.private.live_temp[:push_events] || [])\n  end\n\n  @doc \"\"\"\n  Returns the reply in the socket.\n  \"\"\"\n  def get_reply(%Socket{} = socket) do\n    socket.private.live_temp[:push_reply]\n  end\n\n  @doc \"\"\"\n  Returns the configured signing salt for the endpoint.\n  \"\"\"\n  def salt!(endpoint) when is_atom(endpoint) do\n    salt = endpoint.config(:live_view)[:signing_salt]\n\n    if is_binary(salt) and byte_size(salt) >= 8 do\n      salt\n    else\n      raise ArgumentError, \"\"\"\n      the signing salt for #{inspect(endpoint)} is missing or too short.\n\n      Add the following LiveView configuration to your config/runtime.exs\n      or config/config.exs:\n\n          config :my_app, MyAppWeb.Endpoint,\n              ...,\n              live_view: [signing_salt: \"#{random_encoded_bytes()}\"]\n\n      \"\"\"\n    end\n  end\n\n  @doc \"\"\"\n  Raises error message for bad live patch on mount.\n  \"\"\"\n  def raise_bad_mount_and_live_patch!() do\n    raise RuntimeError, \"\"\"\n    attempted to live patch while mounting.\n\n    a LiveView cannot be mounted while issuing a live patch to the client. \\\n    Use push_navigate/2 or redirect/2 instead if you wish to mount and redirect.\n    \"\"\"\n  end\n\n  @doc \"\"\"\n  Calls the `c:Phoenix.LiveView.mount/3` callback, otherwise returns the socket as is.\n  \"\"\"\n  def maybe_call_live_view_mount!(%Socket{} = socket, view, params, session, uri \\\\ nil) do\n    %{any?: any?, exported?: exported?} = Lifecycle.stage_info(socket, view, :mount, 3)\n\n    if any? do\n      :telemetry.span(\n        [:phoenix, :live_view, :mount],\n        %{socket: socket, params: params, session: session, uri: uri},\n        fn ->\n          socket =\n            case Lifecycle.mount(params, session, socket) do\n              {:cont, %Socket{} = socket} when exported? ->\n                view.mount(params, session, socket)\n\n              {_, %Socket{} = socket} ->\n                {:ok, socket}\n            end\n            |> handle_mount_result!({view, :mount, 3})\n\n          {socket, %{socket: socket, params: params, session: session, uri: uri}}\n        end\n      )\n    else\n      socket\n    end\n  end\n\n  @doc \"\"\"\n  Calls the `c:Phoenix.LiveComponent.mount/1` callback, otherwise returns the socket as is.\n  \"\"\"\n  def maybe_call_live_component_mount!(%Socket{} = socket, component) do\n    if Code.ensure_loaded?(component) and function_exported?(component, :mount, 1) do\n      socket\n      |> component.mount()\n      |> handle_mount_result!({component, :mount, 1})\n    else\n      socket\n    end\n  end\n\n  defp handle_mount_result!({:ok, %Socket{} = socket, opts}, context)\n       when is_list(opts) do\n    validate_mount_redirect!(socket.redirected)\n    handle_mount_options!(socket, opts, context)\n  end\n\n  defp handle_mount_result!({:ok, %Socket{} = socket}, _context) do\n    validate_mount_redirect!(socket.redirected)\n    socket\n  end\n\n  defp handle_mount_result!(response, {mod, fun, arity}) do\n    raise ArgumentError, \"\"\"\n    invalid result returned from #{inspect(mod)}.#{fun}/#{arity}.\n\n    Expected {:ok, socket} | {:ok, socket, opts}, got: #{inspect(response)}\n    \"\"\"\n  end\n\n  defp validate_mount_redirect!({:live, :patch, _}), do: raise_bad_mount_and_live_patch!()\n  defp validate_mount_redirect!(_), do: :ok\n\n  @doc \"\"\"\n  Handle all valid options on mount/on_mount.\n  \"\"\"\n  def handle_mount_options!(%Socket{} = socket, opts, {mod, fun, arity}) do\n    Enum.reduce(opts, socket, fn\n      {key, val}, socket when key in @mount_opts ->\n        handle_mount_option(socket, key, val)\n\n      {key, val}, _socket ->\n        raise ArgumentError, \"\"\"\n        invalid option returned from #{inspect(mod)}.#{fun}/#{arity}.\n\n        Expected keys to be one of #{inspect(@mount_opts)},\n        got: #{inspect(key)}: #{inspect(val)}\n        \"\"\"\n    end)\n  end\n\n  defp handle_mount_option(socket, :layout, layout) do\n    put_in(socket.private[:live_layout], normalize_layout(layout))\n  end\n\n  defp handle_mount_option(%Socket{} = socket, :temporary_assigns, temp_assigns) do\n    if not Keyword.keyword?(temp_assigns) do\n      raise \"the :temporary_assigns mount option must be keyword list\"\n    end\n\n    temp_assigns = Map.new(temp_assigns)\n\n    %{\n      socket\n      | assigns: Map.merge(temp_assigns, socket.assigns),\n        private:\n          Map.update(\n            socket.private,\n            :temporary_assigns,\n            temp_assigns,\n            &Map.merge(&1, temp_assigns)\n          )\n    }\n  end\n\n  @doc \"\"\"\n  Calls the `handle_params/3` callback, and returns the result.\n\n  This function expects the calling code has checked to see if this function has\n  been exported, otherwise it assumes the function has been exported.\n\n  Raises an `ArgumentError` on unexpected return types.\n  \"\"\"\n  def call_handle_params!(%Socket{} = socket, view, exported? \\\\ true, params, uri)\n      when is_boolean(exported?) do\n    :telemetry.span(\n      [:phoenix, :live_view, :handle_params],\n      %{socket: socket, params: params, uri: uri},\n      fn ->\n        case Lifecycle.handle_params(params, uri, socket) do\n          {:cont, %Socket{} = socket} when exported? ->\n            case view.handle_params(params, uri, socket) do\n              {:noreply, %Socket{} = socket} ->\n                {{:noreply, socket}, %{socket: socket, params: params, uri: uri}}\n\n              other ->\n                raise ArgumentError, \"\"\"\n                invalid result returned from #{inspect(view)}.handle_params/3.\n\n                Expected {:noreply, socket}, got: #{inspect(other)}\n                \"\"\"\n            end\n\n          {_, %Socket{} = socket} ->\n            {{:noreply, socket}, %{socket: socket, params: params, uri: uri}}\n        end\n      end\n    )\n  end\n\n  @doc \"\"\"\n  Calls the optional `update/2` or `update_many/1` callback, otherwise update the socket(s) directly.\n  \"\"\"\n  def maybe_call_update!(socket, component, assigns) do\n    cond do\n      function_exported?(component, :update_many, 1) ->\n        case component.update_many([{assigns, socket}]) do\n          [%Socket{} = socket] ->\n            socket\n\n          other ->\n            raise \"#{inspect(component)}.update_many/1 must return a list of Phoenix.LiveView.Socket \" <>\n                    \"of the same length as the input list, got: #{inspect(other)}\"\n        end\n\n      function_exported?(component, :update, 2) ->\n        socket =\n          case component.update(assigns, socket) do\n            {:ok, %Socket{} = socket} ->\n              socket\n\n            other ->\n              raise ArgumentError, \"\"\"\n              invalid result returned from #{inspect(component)}.update/2.\n\n              Expected {:ok, socket}, got: #{inspect(other)}\n              \"\"\"\n          end\n\n        if socket.redirected do\n          raise \"cannot redirect socket on update. Redirect before `update/2` is called\" <>\n                  \" or use `send/2` and redirect in the `handle_info/2` response\"\n        end\n\n        socket\n\n      true ->\n        Enum.reduce(assigns, socket, fn {k, v}, acc -> assign(acc, k, v) end)\n    end\n  end\n\n  @doc \"\"\"\n  Signs the socket's flash into a token if it has been set.\n  \"\"\"\n  def sign_flash(endpoint_mod, %{} = flash) do\n    Phoenix.Token.sign(endpoint_mod, flash_salt(endpoint_mod), flash)\n  end\n\n  @doc \"\"\"\n  Verifies the socket's flash token.\n  \"\"\"\n  def verify_flash(endpoint_mod, flash_token) do\n    salt = flash_salt(endpoint_mod)\n\n    case Phoenix.Token.verify(endpoint_mod, salt, flash_token, max_age: @max_flash_age) do\n      {:ok, flash} -> flash\n      {:error, _reason} -> %{}\n    end\n  end\n\n  defp random_encoded_bytes do\n    binary = <<\n      System.system_time(:nanosecond)::64,\n      :erlang.phash2({node(), self()})::16,\n      :erlang.unique_integer()::16\n    >>\n\n    Base.url_encode64(binary)\n  end\n\n  defp drop_private(%Socket{private: private} = socket, keys) do\n    %{socket | private: Map.drop(private, keys)}\n  end\n\n  defp flash_salt(endpoint_mod) when is_atom(endpoint_mod) do\n    \"flash:\" <> salt!(endpoint_mod)\n  end\n\n  def valid_destination!(%URI{} = uri, context) do\n    valid_destination!(URI.to_string(uri), context)\n  end\n\n  def valid_destination!({:safe, to}, context) do\n    {:safe, valid_string_destination!(IO.iodata_to_binary(to), context)}\n  end\n\n  def valid_destination!({other, to}, _context) when is_atom(other) do\n    [Atom.to_string(other), ?:, to]\n  end\n\n  def valid_destination!(to, context) do\n    valid_string_destination!(IO.iodata_to_binary(to), context)\n  end\n\n  for scheme <- @valid_uri_schemes do\n    def valid_string_destination!(unquote(scheme) <> _ = string, _context), do: string\n  end\n\n  def valid_string_destination!(to, context) do\n    if not match?(\"/\" <> _, to) and String.contains?(to, \":\") do\n      raise ArgumentError, \"\"\"\n      unsupported scheme given to #{context}. In case you want to link to an\n      unknown or unsafe scheme, such as javascript, use a tuple: {:javascript, rest}\n      \"\"\"\n    else\n      to\n    end\n  end\nend\n"
  },
  {
    "path": "lib/phoenix_live_view.ex",
    "content": "defmodule Phoenix.LiveView do\n  @moduledoc ~S'''\n  A LiveView is a process that receives events, updates\n  its state, and renders updates to a page as diffs.\n\n  To get started, see [the Welcome guide](welcome.md).\n  This module provides advanced documentation and features\n  about using LiveView.\n\n  ## Life-cycle\n\n  A LiveView begins as a regular HTTP request and HTML response,\n  and then upgrades to a stateful view on client connect,\n  guaranteeing a regular HTML page even if JavaScript is disabled.\n  Any time a stateful view changes or updates its socket assigns, it is\n  automatically re-rendered and the updates are pushed to the client.\n\n  Socket assigns are stateful values kept on the server side in\n  `Phoenix.LiveView.Socket`. This is different from the common stateless\n  HTTP pattern of sending the connection state to the client in the form\n  of a token or cookie and rebuilding the state on the server to service\n  every request.\n\n  You begin by rendering a LiveView typically from your router.\n  When LiveView is first rendered, the `c:mount/3` callback is invoked\n  with the current params, the current session and the LiveView socket.\n  As in a regular request, `params` contains public data that can be\n  modified by the user. The `session` always contains private data set\n  by the application itself. The `c:mount/3` callback wires up socket\n  assigns necessary for rendering the view. After mounting, `c:handle_params/3`\n  is invoked so uri and query params are handled. Finally, `c:render/1`\n  is invoked and the HTML is sent as a regular HTML response to the\n  client.\n\n  After rendering the static page, LiveView connects from the client\n  to the server where stateful views are spawned to push rendered updates\n  to the browser, and receive client events via `phx-` bindings. Just like\n  the first rendering, `c:mount/3`, is invoked  with params, session,\n  and socket state. However in the connected client case, a LiveView process\n  is spawned on the server, runs `c:handle_params/3` again and then pushes\n  the result of `c:render/1` to the client and continues on for the duration\n  of the connection. If at any point during the stateful life-cycle a crash\n  is encountered, or the client connection drops, the client gracefully\n  reconnects to the server, calling `c:mount/3` and `c:handle_params/3` again.\n\n  LiveView also allows attaching hooks to specific life-cycle stages with\n  `attach_hook/4`.\n\n  ## Template collocation\n\n  There are two possible ways of rendering content in a LiveView. The first\n  one is by explicitly defining a render function, which receives `assigns`\n  and returns a `HEEx` template defined with [the `~H` sigil](`Phoenix.Component.sigil_H/2`).\n\n      defmodule MyAppWeb.DemoLive do\n        # In a typical Phoenix app, the following line would usually be `use MyAppWeb, :live_view`\n        use Phoenix.LiveView\n\n        def render(assigns) do\n          ~H\"\"\"\n          Hello world!\n          \"\"\"\n        end\n      end\n\n  For larger templates, you can place them in a file in the same directory\n  and same name as the LiveView. For example, if the file above is placed\n  at `lib/my_app_web/live/demo_live.ex`, you can also remove the\n  `render/1` function altogether and put the template code at\n  `lib/my_app_web/live/demo_live.html.heex`.\n\n  ## Async Operations\n\n  Performing asynchronous work is common in LiveViews and LiveComponents.\n  It allows the user to get a working UI quickly while the system fetches some\n  data in the background or talks to an external service, without blocking the\n  render or event handling. For async work, you also typically need to handle\n  the different states of the async operation, such as loading, error, and the\n  successful result. You also want to catch any errors or exits and translate it\n  to a meaningful update in the UI rather than crashing the user experience.\n\n  ### Async assigns\n\n  The `assign_async/3` function takes the socket, a key or list of keys which will be assigned\n  asynchronously, and a function. This function will be wrapped in a `task` by\n  `assign_async`, making it easy for you to return the result. This function must\n  return an `{:ok, assigns}` or `{:error, reason}` tuple, where `assigns` is a map\n  of the keys passed to `assign_async`.\n  If the function returns anything else, an error is raised.\n\n  The task is only started when the socket is connected.\n\n  For example, let's say we want to async fetch a user's organization from the database,\n  as well as their profile and rank:\n\n      def mount(%{\"slug\" => slug}, _, socket) do\n        {:ok,\n         socket\n         |> assign(:foo, \"bar\")\n         |> assign_async(:org, fn -> {:ok, %{org: fetch_org!(slug)}} end)\n         |> assign_async([:profile, :rank], fn -> {:ok, %{profile: ..., rank: ...}} end)}\n      end\n\n  > ### Warning {: .warning}\n  >\n  > When using async operations it is important to not pass the socket into the function\n  > as it will copy the whole socket struct to the Task process, which can be very expensive.\n  >\n  > Instead of:\n  >\n  > ```elixir\n  > assign_async(:org, fn -> {:ok, %{org: fetch_org(socket.assigns.slug)}} end)\n  > ```\n  >\n  > We should do:\n  >\n  > ```elixir\n  > slug = socket.assigns.slug\n  > assign_async(:org, fn -> {:ok, %{org: fetch_org(slug)}} end)\n  > ```\n  >\n  > See: https://hexdocs.pm/elixir/process-anti-patterns.html#sending-unnecessary-data\n\n  The state of the async operation is stored as a `Phoenix.LiveView.AsyncResult`\n  in the socket assigns. It carries the loading and failed states, as\n  well as the result. For example, if we wanted to show the loading states in\n  the UI for the `:org`, our template could conditionally render the states:\n\n  ```heex\n  <div :if={@org.loading}>Loading organization...</div>\n  <div :if={org = @org.ok? && @org.result}>{org.name} loaded!</div>\n  ```\n\n  The `Phoenix.Component.async_result/1` function component can also be used to\n  declaratively render the different states using slots:\n\n  ```heex\n  <.async_result :let={org} assign={@org}>\n    <:loading>Loading organization...</:loading>\n    <:failed :let={_failure}>there was an error loading the organization</:failed>\n    {org.name}\n  </.async_result>\n  ```\n\n  ### Arbitrary async operations\n\n  Sometimes you need lower level control of asynchronous operations, while\n  still receiving process isolation and error handling. For this, you can use\n  `start_async/3` and the `Phoenix.LiveView.AsyncResult` module directly:\n\n      def mount(%{\"id\" => id}, _, socket) do\n        {:ok,\n         socket\n         |> assign(:org, AsyncResult.loading())\n         |> start_async(:my_task, fn -> fetch_org!(id) end)}\n      end\n\n      def handle_async(:my_task, {:ok, fetched_org}, socket) do\n        %{org: org} = socket.assigns\n        {:noreply, assign(socket, :org, AsyncResult.ok(org, fetched_org))}\n      end\n\n      def handle_async(:my_task, {:exit, reason}, socket) do\n        %{org: org} = socket.assigns\n        {:noreply, assign(socket, :org, AsyncResult.failed(org, {:exit, reason}))}\n      end\n\n  `start_async/3` is used to fetch the organization asynchronously. The\n  `c:handle_async/3` callback is called when the task completes or exits,\n  with the results wrapped in either `{:ok, result}` or `{:exit, reason}`.\n  The `AsyncResult` module provides functions to update the state of the\n  async operation, but you can also assign any value directly to the socket\n  if you want to handle the state yourself.\n\n  ## Endpoint configuration\n\n  LiveView accepts the following configuration in your endpoint under\n  the `:live_view` key:\n\n    * `:signing_salt` (required) - the salt used to sign data sent\n      to the client\n\n    * `:hibernate_after` (optional) - the idle time in milliseconds allowed in\n    the LiveView before compressing its own memory and state.\n    Defaults to 15000ms (15 seconds)\n\n  '''\n\n  alias Phoenix.LiveView.{Socket, LiveStream, Async}\n\n  @type unsigned_params :: map\n\n  @doc \"\"\"\n  The LiveView entry-point.\n\n  For each LiveView in the root of a template, `c:mount/3` is invoked twice:\n  once to do the initial page load and again to establish the live socket.\n\n  It expects three arguments:\n\n    * `params` - a map of string keys which contain public information that\n      can be set by the user. The map contains the query params as well as any\n      router path parameter. If the LiveView was not mounted at the router,\n      this argument is the atom `:not_mounted_at_router`\n    * `session` - the connection session\n    * `socket` - the LiveView socket\n\n  It must return either `{:ok, socket}` or `{:ok, socket, options}`, where\n  `options` is one of:\n\n    * `:temporary_assigns` - a keyword list of assigns that are temporary\n      and must be reset to their value after every render. Note that once\n      the value is reset, it won't be re-rendered again until it is explicitly\n      assigned\n\n    * `:layout` - the optional layout to be used by the LiveView. Setting\n      this option will override any layout previously set via\n      `Phoenix.LiveView.Router.live_session/2` or on `use Phoenix.LiveView`\n\n  \"\"\"\n  @callback mount(\n              params :: unsigned_params() | :not_mounted_at_router,\n              session :: map,\n              socket :: Socket.t()\n            ) ::\n              {:ok, Socket.t()} | {:ok, Socket.t(), keyword()}\n\n  @doc \"\"\"\n  Renders a template.\n\n  This callback is invoked whenever LiveView detects\n  new content must be rendered and sent to the client.\n\n  If you define this function, it must return a template\n  defined via the `Phoenix.Component.sigil_H/2`.\n\n  If you don't define this function, LiveView will attempt\n  to render a template in the same directory as your LiveView.\n  For example, if you have a LiveView named `MyApp.MyCustomView`\n  inside `lib/my_app/live_views/my_custom_view.ex`, Phoenix\n  will look for a template at `lib/my_app/live_views/my_custom_view.html.heex`.\n  \"\"\"\n  @callback render(assigns :: Socket.assigns()) :: Phoenix.LiveView.Rendered.t()\n\n  @doc \"\"\"\n  Invoked when the LiveView is terminating.\n\n  In case of errors, this callback is only invoked if the LiveView\n  is trapping exits. See `c:GenServer.terminate/2` for more info.\n  \"\"\"\n  @callback terminate(reason, socket :: Socket.t()) :: term\n            when reason: :normal | :shutdown | {:shutdown, :left | :closed | term}\n\n  @doc \"\"\"\n  Invoked after mount and whenever there is a live patch event.\n\n  It receives the current `params`, including parameters from\n  the router, the current `uri` from the client and the `socket`.\n  It is invoked after mount or whenever there is a live navigation\n  event caused by `push_patch/2` or `<.link patch={...}>`.\n\n  It must always return `{:noreply, socket}`, where `:noreply`\n  means no additional information is sent to the client.\n\n  > #### Note {: .warning}\n  >\n  > `handle_params` is only allowed on LiveViews mounted at the router,\n  > as it takes the current url of the page as the second parameter.\n  \"\"\"\n  @callback handle_params(unsigned_params(), uri :: String.t(), socket :: Socket.t()) ::\n              {:noreply, Socket.t()}\n\n  @doc \"\"\"\n  Invoked to handle events sent by the client.\n\n  It receives the `event` name, the event payload as a map,\n  and the socket.\n\n  It must return `{:noreply, socket}`, where `:noreply` means\n  no additional information is sent to the client, or\n  `{:reply, map(), socket}`, where the given `map()` is encoded\n  and sent as a reply to the client.\n  \"\"\"\n  @callback handle_event(event :: binary, unsigned_params(), socket :: Socket.t()) ::\n              {:noreply, Socket.t()} | {:reply, map, Socket.t()}\n\n  @doc \"\"\"\n  Invoked to handle calls from other Elixir processes.\n\n  See `GenServer.call/3` and `c:GenServer.handle_call/3`\n  for more information.\n  \"\"\"\n  @callback handle_call(msg :: term, {pid, reference}, socket :: Socket.t()) ::\n              {:noreply, Socket.t()} | {:reply, term, Socket.t()}\n\n  @doc \"\"\"\n  Invoked to handle casts from other Elixir processes.\n\n  See `GenServer.cast/2` and `c:GenServer.handle_cast/2`\n  for more information. It must always return `{:noreply, socket}`,\n  where `:noreply` means no additional information is sent\n  to the process which cast the message.\n  \"\"\"\n  @callback handle_cast(msg :: term, socket :: Socket.t()) ::\n              {:noreply, Socket.t()}\n\n  @doc \"\"\"\n  Invoked to handle messages from other Elixir processes.\n\n  See `Kernel.send/2` and `c:GenServer.handle_info/2`\n  for more information. It must always return `{:noreply, socket}`,\n  where `:noreply` means no additional information is sent\n  to the process which sent the message.\n  \"\"\"\n  @callback handle_info(msg :: term, socket :: Socket.t()) ::\n              {:noreply, Socket.t()}\n\n  @doc \"\"\"\n  Invoked when the result of an `start_async/3` operation is available.\n\n  For a deeper understanding of using this callback,\n  refer to the [\"Arbitrary async operations\"](#module-arbitrary-async-operations) section.\n  \"\"\"\n  @callback handle_async(\n              name :: term,\n              async_fun_result :: {:ok, term} | {:exit, term},\n              socket :: Socket.t()\n            ) ::\n              {:noreply, Socket.t()}\n\n  @optional_callbacks mount: 3,\n                      render: 1,\n                      terminate: 2,\n                      handle_params: 3,\n                      handle_event: 3,\n                      handle_call: 3,\n                      handle_info: 2,\n                      handle_cast: 2,\n                      handle_async: 3\n\n  @doc \"\"\"\n  Uses LiveView in the current module to mark it a LiveView.\n\n      use Phoenix.LiveView,\n        container: {:tr, class: \"colorized\"},\n        layout: {MyAppWeb.Layouts, :app},\n        log: :info\n\n  ## Options\n\n    * `:container` - an optional tuple for the HTML tag and DOM attributes to\n      be used for the LiveView container. For example: `{:li, style: \"color: blue;\"}`.\n      See `Phoenix.Component.live_render/3` for more information and examples.\n\n    * `:global_prefixes` - the global prefixes to use for components. See\n      `Global Attributes` in `Phoenix.Component` for more information.\n\n    * `:layout` - configures the layout the LiveView will be rendered in.\n      This layout can be overridden by on `c:mount/3` or via the `:layout`\n      option in `Phoenix.LiveView.Router.live_session/2`\n\n    * `:log` - configures the log level for the LiveView, either `false`\n      or a log level\n\n  \"\"\"\n\n  defmacro __using__(opts) do\n    # Expand layout if possible to avoid compile-time dependencies\n    opts =\n      with true <- Keyword.keyword?(opts),\n           {layout, template} <- Keyword.get(opts, :layout) do\n        layout = Macro.expand(layout, %{__CALLER__ | function: {:__live__, 0}})\n        Keyword.replace!(opts, :layout, {layout, template})\n      else\n        _ -> opts\n      end\n\n    quote bind_quoted: [opts: opts] do\n      import Phoenix.LiveView\n      @behaviour Phoenix.LiveView\n      @before_compile Phoenix.LiveView.Renderer\n\n      @phoenix_live_opts opts\n      Module.register_attribute(__MODULE__, :phoenix_live_mount, accumulate: true)\n      @before_compile Phoenix.LiveView\n\n      # Phoenix.Component must come last so its @before_compile runs last\n      use Phoenix.Component, Keyword.take(opts, [:global_prefixes])\n    end\n  end\n\n  defmacro __before_compile__(env) do\n    opts = Module.get_attribute(env.module, :phoenix_live_opts)\n\n    on_mount =\n      env.module\n      |> Module.get_attribute(:phoenix_live_mount)\n      |> Enum.reverse()\n\n    live = Phoenix.LiveView.__live__([on_mount: on_mount] ++ opts)\n\n    quote do\n      @doc false\n      def __live__ do\n        unquote(Macro.escape(live))\n      end\n    end\n  end\n\n  @doc \"\"\"\n  Defines metadata for a LiveView.\n\n  This must be returned from the `__live__` callback.\n\n  It accepts:\n\n    * `:container` - an optional tuple for the HTML tag and DOM attributes to\n      be used for the LiveView container. For example: `{:li, style: \"color: blue;\"}`.\n\n    * `:layout` - configures the layout the LiveView will be rendered in.\n      This layout can be overridden by on `c:mount/3` or via the `:layout`\n      option in `Phoenix.LiveView.Router.live_session/2`\n\n    * `:log` - configures the log level for the LiveView, either `false`\n      or a log level\n\n    * `:on_mount` - a list of tuples with module names and argument to be invoked\n      as `on_mount` hooks\n\n  \"\"\"\n  def __live__(opts \\\\ []) do\n    on_mount = opts[:on_mount] || []\n\n    layout =\n      Phoenix.LiveView.Utils.normalize_layout(Keyword.get(opts, :layout, false))\n\n    log =\n      case Keyword.fetch(opts, :log) do\n        {:ok, false} -> false\n        {:ok, log} when is_atom(log) -> log\n        :error -> :debug\n        _ -> raise ArgumentError, \":log expects an atom or false, got: #{inspect(opts[:log])}\"\n      end\n\n    container = opts[:container] || {:div, []}\n\n    %{\n      container: container,\n      kind: :view,\n      layout: layout,\n      lifecycle: Phoenix.LiveView.Lifecycle.build(on_mount),\n      log: log\n    }\n  end\n\n  @doc \"\"\"\n  Declares a module callback to be invoked on the LiveView's mount.\n\n  The function within the given module, which must be named `on_mount`,\n  will be invoked before both disconnected and connected mounts. The hook\n  has the option to either halt or continue the mounting process as usual.\n  If you wish to redirect the LiveView, you **must** halt, otherwise an error\n  will be raised.\n\n  Tip: if you need to define multiple `on_mount` callbacks, avoid defining\n  multiple modules. Instead, pass a tuple and use pattern matching to handle\n  different cases:\n\n      def on_mount(:admin, _params, _session, socket) do\n        {:cont, socket}\n      end\n\n      def on_mount(:user, _params, _session, socket) do\n        {:cont, socket}\n      end\n\n  And then invoke it as:\n\n      on_mount {MyAppWeb.SomeHook, :admin}\n      on_mount {MyAppWeb.SomeHook, :user}\n\n  Registering `on_mount` hooks can be useful to perform authentication\n  as well as add custom behaviour to other callbacks via `attach_hook/4`.\n\n  The `on_mount` callback can return a keyword list of options as a third\n  element in the return tuple. These options are identical to what can\n  optionally be returned in `c:mount/3`.\n\n  ## Examples\n\n  The following is an example of attaching a hook via\n  `Phoenix.LiveView.Router.live_session/3`:\n\n      # lib/my_app_web/live/init_assigns.ex\n      defmodule MyAppWeb.InitAssigns do\n        @moduledoc \"\\\"\"\n        Ensures common `assigns` are applied to all LiveViews attaching this hook.\n        \"\\\"\"\n        import Phoenix.LiveView\n        import Phoenix.Component\n\n        def on_mount(:default, _params, _session, socket) do\n          {:cont, assign(socket, :page_title, \"DemoWeb\")}\n        end\n\n        def on_mount(:user, params, session, socket) do\n          # code\n        end\n\n        def on_mount(:admin, _params, _session, socket) do\n          {:cont, socket, layout: {DemoWeb.Layouts, :admin}}\n        end\n      end\n\n      # lib/my_app_web/router.ex\n      defmodule MyAppWeb.Router do\n        use MyAppWeb, :router\n\n        # pipelines, plugs, etc.\n\n        live_session :default, on_mount: MyAppWeb.InitAssigns do\n          scope \"/\", MyAppWeb do\n            pipe_through :browser\n            live \"/\", PageLive, :index\n          end\n        end\n\n        live_session :authenticated, on_mount: {MyAppWeb.InitAssigns, :user} do\n          scope \"/\", MyAppWeb do\n            pipe_through [:browser, :require_user]\n            live \"/profile\", UserLive.Profile, :index\n          end\n        end\n\n        live_session :admins, on_mount: {MyAppWeb.InitAssigns, :admin} do\n          scope \"/admin\", MyAppWeb.Admin do\n            pipe_through [:browser, :require_user, :require_admin]\n            live \"/\", AdminLive.Index, :index\n          end\n        end\n      end\n\n  \"\"\"\n  defmacro on_mount(mod_or_mod_arg) do\n    caller = %{__CALLER__ | function: {:on_mount, 1}}\n\n    # While we could pass `mod_or_mod_arg` as a whole to\n    # expand_literals, we want to also be able to expand only\n    # the first element, even if the second element is not a literal.\n    mod_or_mod_arg =\n      case mod_or_mod_arg do\n        {mod, arg} ->\n          {Macro.expand_literals(mod, caller), Macro.expand_literals(arg, caller)}\n\n        mod_or_mod_arg ->\n          Macro.expand_literals(mod_or_mod_arg, caller)\n      end\n\n    quote do\n      Module.put_attribute(\n        __MODULE__,\n        :phoenix_live_mount,\n        Phoenix.LiveView.Lifecycle.validate_on_mount!(__MODULE__, unquote(mod_or_mod_arg))\n      )\n    end\n  end\n\n  @doc \"\"\"\n  Returns true if the socket is connected.\n\n  Useful for checking the connectivity status when mounting the view.\n  For example, on initial page render, the view is mounted statically,\n  rendered, and the HTML is sent to the client. Once the client\n  connects to the server, a LiveView is then spawned and mounted\n  statefully within a process. Use `connected?/1` to conditionally\n  perform stateful work, such as subscribing to pubsub topics,\n  sending messages, etc.\n\n  ## Examples\n\n      defmodule DemoWeb.ClockLive do\n        use Phoenix.LiveView\n        ...\n        def mount(_params, _session, socket) do\n          if connected?(socket), do: :timer.send_interval(1000, self(), :tick)\n\n          {:ok, assign(socket, date: :calendar.local_time())}\n        end\n\n        def handle_info(:tick, socket) do\n          {:noreply, assign(socket, date: :calendar.local_time())}\n        end\n      end\n  \"\"\"\n  def connected?(%Socket{transport_pid: transport_pid}), do: transport_pid != nil\n\n  @doc ~S'''\n  Configures which function to use to render a LiveView/LiveComponent.\n\n  By default, LiveView invokes the `render/1` function in the same module\n  the LiveView/LiveComponent is defined, passing `assigns` as its sole\n  argument. This function allows you to set a different rendering function.\n\n  One possible use case for this function is to set a different template\n  on disconnected render. When the user first accesses a LiveView, we will\n  perform a disconnected render to send to the browser. This is useful for\n  several reasons, such as reducing the time to first paint and for search\n  engine indexing.\n\n  However, when LiveView is gated behind an authentication page, it may be\n  useful to render a placeholder on disconnected render and perform the\n  full render once the WebSocket connects. This can be achieved with\n  `render_with/2` and is particularly useful on complex pages (such as\n  dashboards and reports).\n\n  To do so, you must simply invoke `render_with(socket, &some_function_component/1)`,\n  configuring your socket with a new rendering function.\n\n  ## Examples\n\n      @impl true\n      def mount(_params, _session, socket) do\n        if connected?(socket) do\n          {:ok,\n           socket\n           |> assign(:foos, Context.list_foos())\n           |> assign(:bars, Context.list_bars())}\n        else\n          {:ok, render_with(socket, &loading/1)}\n        end\n      end\n\n      defp loading(assigns) do\n        ~H\"\"\"\n        <div class=\"...\">\n          Loading...\n        </div>\n        \"\"\"\n      end\n\n  '''\n  def render_with(%Socket{} = socket, component) when is_function(component, 1) do\n    put_in(socket.private[:render_with], component)\n  end\n\n  @doc \"\"\"\n  Puts a new private key and value in the socket.\n\n  Privates are *not change tracked*. This storage is meant to be used by\n  users and libraries to hold state that doesn't require\n  change tracking. The keys should be prefixed with the app/library name.\n\n  ## Examples\n\n  Key values can be placed in private:\n\n      put_private(socket, :myapp_meta, %{foo: \"bar\"})\n\n  And then retrieved:\n\n      socket.private[:myapp_meta]\n  \"\"\"\n  @reserved_privates ~w(\n    connect_params\n    connect_info\n    assign_new\n    live_async\n    live_layout\n    live_temp\n    lifecycle\n    render_with\n    root_view\n  )a\n  def put_private(%Socket{} = socket, key, value) when key not in @reserved_privates do\n    %{socket | private: Map.put(socket.private, key, value)}\n  end\n\n  def put_private(%Socket{}, bad_key, _value) do\n    raise ArgumentError, \"cannot set reserved private key #{inspect(bad_key)}\"\n  end\n\n  @doc \"\"\"\n  Adds a flash message to the socket to be displayed.\n\n  The flash message will stick around until it is read.\n  If you perform a redirect or a navigation event, the message will be\n  signed and temporarily stored in the client. Therefore it is important\n  to use flash messages only for user-facing notifications. Do not store\n  sensitive information in flash messages.\n\n  In a typical LiveView application, the message will be rendered by the\n  CoreComponents’ `flash/1` component. It is up to this function to determine\n  what kind of messages it supports. By default, the `:info` and `:error`\n  kinds are handled.\n\n  *Note*: You must also place the `Phoenix.LiveView.Router.fetch_live_flash/2`\n  plug in your browser's pipeline in place of `fetch_flash` for LiveView flash\n  messages be supported, for example:\n\n      import Phoenix.LiveView.Router\n\n      pipeline :browser do\n        ...\n        plug :fetch_live_flash\n      end\n\n  ## Examples\n\n      iex> put_flash(socket, :info, \"It worked!\")\n      iex> put_flash(socket, :error, \"You can't access that page\")\n\n  ## Inside components\n\n  You can use `put_flash/3` inside a `Phoenix.LiveComponent` and\n  components have their own `@flash` assigns. The `@flash` assign\n  in a component is only copied to its parent LiveView if the component\n  calls `push_navigate/2` or `push_patch/2`.\n  \"\"\"\n  defdelegate put_flash(socket, kind, msg), to: Phoenix.LiveView.Utils\n\n  @doc \"\"\"\n  Clears the flash.\n\n  ## Examples\n\n      iex> clear_flash(socket)\n\n  Clearing the flash can also be triggered on the client and natively handled by LiveView using the `lv:clear-flash` event.\n\n  For example:\n\n  ```heex\n  <p class=\"alert\" phx-click=\"lv:clear-flash\">\n    {Phoenix.Flash.get(@flash, :info)}\n  </p>\n  ```\n  \"\"\"\n  defdelegate clear_flash(socket), to: Phoenix.LiveView.Utils\n\n  @doc \"\"\"\n  Clears a key from the flash.\n\n  ## Examples\n\n      iex> clear_flash(socket, :info)\n\n  Clearing the flash can also be triggered on the client and natively handled by LiveView using the `lv:clear-flash` event.\n\n  For example:\n\n  ```heex\n  <p class=\"alert\" phx-click=\"lv:clear-flash\" phx-value-key=\"info\">\n    {Phoenix.Flash.get(@flash, :info)}\n  </p>\n  ```\n  \"\"\"\n  defdelegate clear_flash(socket, key), to: Phoenix.LiveView.Utils\n\n  @doc \"\"\"\n  Pushes an event to the client.\n\n  Events can be handled in two ways:\n\n    1. They can be handled on `window` via `addEventListener`.\n       A \"phx:\" prefix will be added to the event name.\n\n    2. They can be handled inside a hook via `handleEvent`.\n\n  Events are dispatched to all active hooks on the client who are\n  handling the given `event`. If you need to scope events, then\n  this must be done by namespacing them.\n\n  Events pushed during `push_navigate` are currently discarded,\n  as the LiveView is immediately dismounted.\n\n  ## Hook example\n\n  If you push a \"scores\" event from your LiveView:\n\n      {:noreply, push_event(socket, \"scores\", %{points: 100, user: \"josé\"})}\n\n  A hook declared via `phx-hook` can handle it via `handleEvent`:\n\n  ```javascript\n  this.handleEvent(\"scores\", data => ...)\n  ```\n\n  ## `window` example\n\n  All events are also dispatched on the `window`. This means you can handle\n  them by adding listeners. For example, if you want to remove an element\n  from the page, you can do this:\n\n      {:noreply, push_event(socket, \"remove-el\", %{id: \"foo-bar\"})}\n\n  And now in your app.js you can register and handle it:\n\n  ```javascript\n  window.addEventListener(\n    \"phx:remove-el\",\n    e => document.getElementById(e.detail.id).remove()\n  )\n  ```\n\n  ## Specifying the dispatch phase\n\n  By default, events pushed with `push_event/3` are only dispatched after\n  the LiveView is patched. In some cases, handling an event before the LiveView\n  is patched can be useful though. To do this, the `dispatch` option can be passed\n  as fourth argument:\n\n      {:noreply, push_event(socket, \"scores\", %{points: 100, user: \"josé\"}, dispatch: :before)}\n\n  \"\"\"\n  defdelegate push_event(socket, event, payload, opts \\\\ []), to: Phoenix.LiveView.Utils\n\n  @doc ~S\"\"\"\n  Allows an upload for the provided name.\n\n  ## Options\n\n    * `:accept` - Required. A list of unique file extensions (such as \".jpeg\") or\n      mime type (such as \"image/jpeg\" or \"image/*\"). You may also pass the atom\n      `:any` instead of a list to support to allow any kind of file.\n      For example, `[\".jpeg\"]`, `:any`, etc.\n\n    * `:max_entries` - The maximum number of selected files to allow per\n      file input. Defaults to 1.\n\n    * `:max_file_size` - The maximum file size in bytes to allow to be uploaded.\n      Defaults 8MB. For example, `12_000_000`.\n\n    * `:chunk_size` - The chunk size in bytes to send when uploading.\n      Defaults `64_000`.\n\n    * `:chunk_timeout` - The time in milliseconds to wait before closing the\n      upload channel when a new chunk has not been received. Defaults to `10_000`.\n\n    * `:external` - A 2-arity function for generating metadata for external\n      client uploaders. This function must return either `{:ok, meta, socket}`\n      or `{:error, meta, socket}` where meta is a map. See the Uploads section\n      for example usage.\n\n    * `:progress` - An optional 3-arity function for receiving progress events.\n\n    * `:auto_upload` - Instructs the client to upload the file automatically\n      on file selection instead of waiting for form submits. Defaults to `false`.\n\n    * `:writer` - A module implementing the `Phoenix.LiveView.UploadWriter`\n      behaviour to use for writing the uploaded chunks. Defaults to writing to a\n      temporary file for consumption. See the `Phoenix.LiveView.UploadWriter` docs\n      for custom usage.\n\n  Raises when a previously allowed upload under the same name is still active.\n\n  ## Examples\n\n      allow_upload(socket, :avatar, accept: ~w(.jpg .jpeg), max_entries: 2)\n      allow_upload(socket, :avatar, accept: :any)\n\n  For consuming files automatically as they are uploaded, you can pair `auto_upload: true` with\n  a custom progress function to consume the entries as they are completed. For example:\n\n      allow_upload(socket, :avatar, accept: :any, progress: &handle_progress/3, auto_upload: true)\n\n      defp handle_progress(:avatar, entry, socket) do\n        if entry.done? do\n          uploaded_file =\n            consume_uploaded_entry(socket, entry, fn %{} = meta ->\n              {:ok, ...}\n            end)\n\n          {:noreply, put_flash(socket, :info, \"file #{uploaded_file.name} uploaded\")}\n        else\n          {:noreply, socket}\n        end\n      end\n  \"\"\"\n  defdelegate allow_upload(socket, name, options), to: Phoenix.LiveView.Upload\n\n  @doc \"\"\"\n  Revokes a previously allowed upload from `allow_upload/3`.\n\n  ## Examples\n\n      disallow_upload(socket, :avatar)\n  \"\"\"\n  defdelegate disallow_upload(socket, name), to: Phoenix.LiveView.Upload\n\n  @doc \"\"\"\n  Cancels an upload for the given entry.\n\n  ## Examples\n\n  ```heex\n  <%= for entry <- @uploads.avatar.entries do %>\n    ...\n    <button phx-click=\"cancel-upload\" phx-value-ref={entry.ref}>cancel</button>\n  <% end %>\n  ```\n\n      def handle_event(\"cancel-upload\", %{\"ref\" => ref}, socket) do\n        {:noreply, cancel_upload(socket, :avatar, ref)}\n      end\n  \"\"\"\n  defdelegate cancel_upload(socket, name, entry_ref), to: Phoenix.LiveView.Upload\n\n  @doc \"\"\"\n  Returns the completed and in progress entries for the upload.\n\n  ## Examples\n\n      case uploaded_entries(socket, :photos) do\n        {[_ | _] = completed, []} ->\n          # all entries are completed\n\n        {[], [_ | _] = in_progress} ->\n          # all entries are still in progress\n      end\n  \"\"\"\n  defdelegate uploaded_entries(socket, name), to: Phoenix.LiveView.Upload\n\n  @doc ~S\"\"\"\n  Consumes the uploaded entries.\n\n  Raises when there are still entries in progress.\n  Typically called when submitting a form to handle the\n  uploaded entries alongside the form data. For form submissions,\n  it is guaranteed that all entries have completed before the submit event\n  is invoked. Once entries are consumed, they are removed from the upload.\n\n  The function passed to consume may return a tagged tuple of the form\n  `{:ok, my_result}` to collect results about the consumed entries, or\n  `{:postpone, my_result}` to collect results, but postpone the file\n  consumption to be performed later.\n\n  A list of all `my_result` values produced by the passed function is\n  returned, regardless of whether they were consumed or postponed.\n\n  ## Examples\n\n      def handle_event(\"save\", _params, socket) do\n        uploaded_files =\n          consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->\n            dest = Path.join(\"priv/static/uploads\", Path.basename(path))\n            File.cp!(path, dest)\n            {:ok, ~p\"/uploads/#{Path.basename(dest)}\"}\n          end)\n        {:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}\n      end\n  \"\"\"\n  defdelegate consume_uploaded_entries(socket, name, func), to: Phoenix.LiveView.Upload\n\n  @doc ~S\"\"\"\n  Consumes an individual uploaded entry.\n\n  Raises when the entry is still in progress.\n  Typically called when submitting a form to handle the\n  uploaded entries alongside the form data. Once entries are consumed,\n  they are removed from the upload.\n\n  This is a lower-level feature than `consume_uploaded_entries/3` and useful\n  for scenarios where you want to consume entries as they are individually completed.\n\n  Like `consume_uploaded_entries/3`, the function passed to consume may return\n  a tagged tuple of the form `{:ok, my_result}` to collect results about the\n  consumed entries, or `{:postpone, my_result}` to collect results,\n  but postpone the file consumption to be performed later.\n\n  ## Examples\n\n      def handle_event(\"save\", _params, socket) do\n        case uploaded_entries(socket, :avatar) do\n          {[_|_] = entries, []} ->\n            uploaded_files = for entry <- entries do\n              consume_uploaded_entry(socket, entry, fn %{path: path} ->\n                dest = Path.join(\"priv/static/uploads\", Path.basename(path))\n                File.cp!(path, dest)\n                {:ok, ~p\"/uploads/#{Path.basename(dest)}\"}\n              end)\n            end\n            {:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}\n\n          _ ->\n            {:noreply, socket}\n        end\n      end\n  \"\"\"\n  defdelegate consume_uploaded_entry(socket, entry, func), to: Phoenix.LiveView.Upload\n\n  @doc \"\"\"\n  Annotates the socket for redirect to a destination path.\n\n  *Note*: LiveView redirects rely on instructing client\n  to perform a `window.location` update on the provided\n  redirect location. The whole page will be reloaded and\n  all state will be discarded.\n\n  Calling redirect shuts down the LiveView channel. If you need\n  to programatically open an external link without causing the\n  LiveView to shut down, for example because of `mailto:` or `tel:`\n  URL schemes, consider using `push_event/3` with a custom client-side\n  handler instead.\n\n  ## Options\n\n    * `:to` - the path to redirect to. It must always be a local path\n    * `:status` - the HTTP status code to use for the redirect. Defaults to 302.\n    * `:external` - an external path to redirect to. Either a string\n      or `{scheme, url}` to redirect to a custom scheme\n\n  ## Examples\n\n      {:noreply, redirect(socket, to: \"/\")}\n      {:noreply, redirect(socket, to: \"/\", status: 301)}\n      {:noreply, redirect(socket, external: \"https://example.com\")}\n\n  \"\"\"\n  def redirect(socket, opts \\\\ []) do\n    status = Keyword.get(opts, :status, 302)\n\n    cond do\n      Keyword.has_key?(opts, :to) ->\n        do_internal_redirect(socket, Keyword.fetch!(opts, :to), status)\n\n      Keyword.has_key?(opts, :external) ->\n        do_external_redirect(socket, Keyword.fetch!(opts, :external), status)\n\n      true ->\n        raise ArgumentError, \"expected :to or :external option in redirect/2\"\n    end\n  end\n\n  defp do_internal_redirect(%Socket{} = socket, url, redirect_status) do\n    validate_local_url!(url, \"redirect/2\")\n\n    put_redirect(socket, {:redirect, %{to: url, status: redirect_status}})\n  end\n\n  defp do_external_redirect(%Socket{} = socket, url, redirect_status) do\n    case url do\n      {scheme, rest} ->\n        put_redirect(\n          socket,\n          {:redirect, %{external: \"#{scheme}:#{rest}\", status: redirect_status}}\n        )\n\n      url when is_binary(url) ->\n        external_url = Phoenix.LiveView.Utils.valid_string_destination!(url, \"redirect/2\")\n\n        put_redirect(\n          socket,\n          {:redirect, %{external: external_url, status: redirect_status}}\n        )\n\n      other ->\n        raise ArgumentError,\n              \"expected :external option in redirect/2 to be valid URL, got: #{inspect(other)}\"\n    end\n  end\n\n  @doc \"\"\"\n  Annotates the socket for navigation within the current LiveView.\n\n  When navigating to the current LiveView, `c:handle_params/3` is\n  immediately invoked to handle the change of params and URL state.\n  Then the new state is pushed to the client, without reloading the\n  whole page while also maintaining the current scroll position.\n  For live navigation to another LiveView in the same `live_session`,\n  use `push_navigate/2`. Otherwise, use `redirect/2`.\n\n  ## Options\n\n    * `:to` - the required path to link to. It must always be a local path\n    * `:replace` - the flag to replace the current history or push a new state.\n      Defaults `false`.\n\n  ## Examples\n\n      {:noreply, push_patch(socket, to: \"/\")}\n      {:noreply, push_patch(socket, to: \"/\", replace: true)}\n\n  \"\"\"\n  def push_patch(%Socket{} = socket, opts) do\n    opts = push_opts!(opts, \"push_patch/2\")\n    put_redirect(socket, {:live, :patch, opts})\n  end\n\n  @doc \"\"\"\n  Annotates the socket for navigation to another LiveView in the same `live_session`.\n\n  The current LiveView will be shutdown and a new one will be mounted\n  in its place, without reloading the whole page. This can\n  also be used to remount the same LiveView, in case you want to start\n  fresh. If you want to navigate to the same LiveView without remounting\n  it, use `push_patch/2` instead.\n\n  ## Options\n\n    * `:to` - the required path to link to. It must always be a local path\n    * `:replace` - the flag to replace the current history or push a new state.\n      Defaults `false`.\n\n  ## Examples\n\n      {:noreply, push_navigate(socket, to: \"/\")}\n      {:noreply, push_navigate(socket, to: \"/\", replace: true)}\n\n  \"\"\"\n  def push_navigate(%Socket{} = socket, opts) do\n    opts = push_opts!(opts, \"push_navigate/2\")\n    put_redirect(socket, {:live, :redirect, opts})\n  end\n\n  @doc false\n  @deprecated \"Use push_navigate/2 instead\"\n  def push_redirect(%Socket{} = socket, opts) do\n    opts = push_opts!(opts, \"push_redirect/2\")\n    put_redirect(socket, {:live, :redirect, opts})\n  end\n\n  defp push_opts!(opts, context) do\n    to = Keyword.fetch!(opts, :to)\n    validate_local_url!(to, context)\n    kind = if opts[:replace], do: :replace, else: :push\n    %{to: to, kind: kind}\n  end\n\n  defp put_redirect(%Socket{redirected: nil} = socket, command) do\n    %{socket | redirected: command}\n  end\n\n  defp put_redirect(%Socket{redirected: to} = _socket, _command) do\n    raise ArgumentError, \"socket already prepared to redirect with #{inspect(to)}\"\n  end\n\n  @invalid_local_url_chars [\"\\\\\"]\n\n  defp validate_local_url!(\"//\" <> _ = to, where) do\n    raise_invalid_local_url!(to, where)\n  end\n\n  defp validate_local_url!(\"/\" <> _ = to, where) do\n    if String.contains?(to, @invalid_local_url_chars) do\n      raise ArgumentError, \"unsafe characters detected for #{where} in URL #{inspect(to)}\"\n    else\n      to\n    end\n  end\n\n  defp validate_local_url!(to, where) do\n    raise_invalid_local_url!(to, where)\n  end\n\n  defp raise_invalid_local_url!(to, where) do\n    raise ArgumentError, \"the :to option in #{where} expects a path but was #{inspect(to)}\"\n  end\n\n  @doc \"\"\"\n  Accesses the connect params sent by the client for use on connected mount.\n\n  Connect params are sent from the client on every connection and reconnection.\n  The parameters in the client can be computed dynamically, allowing you to pass\n  client state to the server. For example, you could use it to compute and pass\n  the user time zone from a JavaScript client:\n\n  ```javascript\n  let liveSocket = new LiveSocket(\"/live\", Socket, {\n    longPollFallbackMs: 2500,\n    params: (_liveViewName) => {\n      return {\n        _csrf_token: csrfToken,\n        time_zone: Intl.DateTimeFormat().resolvedOptions().timeZone\n      }\n    }\n  })\n  ```\n\n  By computing the parameters with a function, reconnections will reevalute\n  the code, allowing you to fetch the latest data.\n\n  On the LiveView, you will use `get_connect_params/1` to read the data,\n  which only remains available during mount. `nil` is returned when called\n  in a disconnected state and a `RuntimeError` is raised if called after\n  mount.\n\n  ## Reserved params\n\n  The following params have special meaning in LiveView:\n\n    * `\"_csrf_token\"` - the CSRF Token which must be explicitly set by the user\n      when connecting\n    * `\"_mounts\"` - the number of times the current LiveView is mounted.\n      It is 0 on first mount, then increases on each reconnect. It resets\n      when navigating away from the current LiveView or on errors\n    * `\"_track_static\"` - set automatically with a list of all href/src from\n      tags with the `phx-track-static` annotation in them. If there are no\n      such tags, nothing is sent\n    * `\"_live_referer\"` - sent by the client as the referer URL when a\n      live navigation has occurred from `push_navigate` or client link navigate.\n\n  ## Examples\n\n      def mount(_params, _session, socket) do\n        {:ok, assign(socket, width: get_connect_params(socket)[\"width\"] || @width)}\n      end\n  \"\"\"\n  def get_connect_params(%Socket{private: private} = socket) do\n    if connect_params = private[:connect_params] do\n      if connected?(socket), do: connect_params, else: nil\n    else\n      raise_root_and_mount_only!(socket, \"connect_params\")\n    end\n  end\n\n  @doc \"\"\"\n  Accesses a given connect info key from the socket.\n\n  The following keys are supported: `:peer_data`, `:trace_context_headers`,\n  `:x_headers`, `:uri`, and `:user_agent`.\n\n  The connect information is available only during mount. During disconnected\n  render, all keys are available. On connected render, only the keys explicitly\n  declared in your socket are available. See `Phoenix.Endpoint.socket/3` for\n  a complete description of the keys.\n\n  ## Examples\n\n  The first step is to declare the `connect_info` you want to receive.\n  Typically, it includes at least the session, but you must include all\n  other keys you want to access on connected mount, such as `:peer_data`:\n\n      socket \"/live\", Phoenix.LiveView.Socket,\n        websocket: [connect_info: [:peer_data, session: @session_options]]\n\n  Those values can now be accessed on the connected mount as\n  `get_connect_info/2`:\n\n      def mount(_params, _session, socket) do\n        peer_data = get_connect_info(socket, :peer_data)\n        {:ok, assign(socket, ip: peer_data.address)}\n      end\n\n  If the key is not available, usually because it was not specified\n  in `connect_info`, it returns nil.\n  \"\"\"\n  def get_connect_info(%Socket{private: private} = socket, key) when is_atom(key) do\n    if connect_info = private[:connect_info] do\n      case connect_info do\n        %Plug.Conn{} -> conn_connect_info(connect_info, key)\n        %{} -> connect_info[key]\n      end\n    else\n      raise_root_and_mount_only!(socket, \"connect_info\")\n    end\n  end\n\n  defp conn_connect_info(conn, :peer_data) do\n    Plug.Conn.get_peer_data(conn)\n  end\n\n  defp conn_connect_info(conn, :x_headers) do\n    for {header, _} = pair <- conn.req_headers,\n        String.starts_with?(header, \"x-\"),\n        do: pair\n  end\n\n  defp conn_connect_info(conn, :trace_context_headers) do\n    for {header, _} = pair <- conn.req_headers,\n        header in [\"traceparent\", \"tracestate\"],\n        do: pair\n  end\n\n  defp conn_connect_info(conn, :uri) do\n    %URI{\n      scheme: to_string(conn.scheme),\n      query: conn.query_string,\n      port: conn.port,\n      host: conn.host,\n      path: conn.request_path\n    }\n  end\n\n  defp conn_connect_info(conn, :user_agent) do\n    with {_, value} <- List.keyfind(conn.req_headers, \"user-agent\", 0) do\n      value\n    end\n  end\n\n  @doc \"\"\"\n  Returns true if the socket is connected and the tracked static assets have changed.\n\n  This function is useful to detect if the client is running on an outdated\n  version of the marked static files. It works by comparing the static paths\n  sent by the client with the one on the server.\n\n  **Note:** this functionality requires Phoenix v1.5.2 or later.\n\n  To use this functionality, the first step is to annotate which static files\n  you want to be tracked by LiveView, with the `phx-track-static`. For example:\n\n  ```heex\n  <link phx-track-static rel=\"stylesheet\" href={~p\"/assets/app.css\"} />\n  <script defer phx-track-static type=\"text/javascript\" src={~p\"/assets/app.js\"}></script>\n  ```\n\n  Now, whenever LiveView connects to the server, it will send a copy `src`\n  or `href` attributes of all tracked statics and compare those values with\n  the latest entries computed by `mix phx.digest` in the server.\n\n  The tracked statics on the client will match the ones on the server the\n  huge majority of times. However, if there is a new deployment, those values\n  may differ. You can use this function to detect those cases and show a\n  banner to the user, asking them to reload the page. To do so, first set the\n  assign on mount:\n\n      def mount(params, session, socket) do\n        {:ok, assign(socket, static_changed?: static_changed?(socket))}\n      end\n\n  And then in your views:\n\n  ```heex\n  <div :if={@static_changed?} id=\"reload-static\">\n    The app has been updated. Click here to <a href=\"#\" onclick=\"window.location.reload()\">reload</a>.\n  </div>\n  ```\n\n  For larger projects, you can extract this into [a hook](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#on_mount/1):\n\n      # MyAppWeb.CheckStaticChanged\n      def on_mount(:default, _params, _session, socket) do\n        {:cont, assign(socket, static_changed?: static_changed?(socket))}\n      end\n\n  And then add it to the existing `live_view` macro in your `my_app_web.ex` file or add it as part\n  of your `live_session` hooks.\n  If you prefer, you can also send a JavaScript script that immediately\n  reloads the page, but this will cause the client-side to lose all work in progress.\n\n  **Note:** only set `phx-track-static` on your own assets. For example, do\n  not set it in external JavaScript files:\n\n  ```heex\n  <script defer phx-track-static type=\"text/javascript\" src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js\"></script>\n  ```\n\n  Because you don't actually serve the file above, LiveView will interpret\n  the static above as missing, and this function will return true.\n  \"\"\"\n  def static_changed?(%Socket{private: private, endpoint: endpoint} = socket) do\n    if connect_params = private[:connect_params] do\n      connected?(socket) and\n        static_changed?(\n          connect_params[\"_track_static\"],\n          endpoint.config(:cache_static_manifest_latest)\n        )\n    else\n      raise_root_and_mount_only!(socket, \"static_changed?\")\n    end\n  end\n\n  defp static_changed?([_ | _] = statics, %{} = latest) do\n    latest = Map.to_list(latest)\n\n    not Enum.all?(statics, fn static ->\n      [static | _] = :binary.split(static, \"?\")\n\n      Enum.any?(latest, fn {non_digested, digested} ->\n        String.ends_with?(static, non_digested) or String.ends_with?(static, digested)\n      end)\n    end)\n  end\n\n  defp static_changed?(_, _), do: false\n\n  defp raise_root_and_mount_only!(socket, fun) do\n    if child?(socket) do\n      raise RuntimeError, \"\"\"\n      attempted to read #{fun} from a nested child LiveView #{inspect(socket.view)}.\n\n      Only the root LiveView has access to #{fun}.\n      \"\"\"\n    else\n      raise RuntimeError, \"\"\"\n      attempted to read #{fun} outside of #{inspect(socket.view)}.mount/3.\n\n      #{fun} only exists while mounting. If you require access to this information\n      after mount, store the state in socket assigns.\n      \"\"\"\n    end\n  end\n\n  @doc ~S'''\n  Asynchronously updates a `Phoenix.LiveComponent` with new assigns.\n\n  The `pid` argument is optional and it defaults to the current process,\n  which means the update instruction will be sent to a component running\n  on the same LiveView. If the current process is not a LiveView or you\n  want to send updates to a live component running on another LiveView,\n  you should explicitly pass the LiveView's pid instead.\n\n  The second argument can be either the value of the `@myself` or the module of\n  the live component. If you pass the module, then the `:id` that identifies\n  the component must be passed as part of the assigns.\n\n  When the component receives the update,\n  [`update_many/1`](`c:Phoenix.LiveComponent.update_many/1`) will be invoked if\n  it is defined, otherwise [`update/2`](`c:Phoenix.LiveComponent.update/2`) is\n  invoked with the new assigns.  If\n  [`update/2`](`c:Phoenix.LiveComponent.update/2`) is not defined all assigns\n  are simply merged into the socket. The assigns received as the first argument\n  of the [`update/2`](`c:Phoenix.LiveComponent.update/2`) callback will only\n  include the _new_ assigns passed from this function.  Pre-existing assigns may\n  be found in `socket.assigns`.\n\n  While a component may always be updated from the parent by updating some\n  parent assigns which will re-render the child, thus invoking\n  [`update/2`](`c:Phoenix.LiveComponent.update/2`) on the child component,\n  `send_update/3` is useful for updating a component that entirely manages its\n  own state, as well as messaging between components mounted in the same\n  LiveView.\n\n  ## Examples\n\n      def handle_event(\"cancel-order\", _, socket) do\n        ...\n        send_update(Cart, id: \"cart\", status: \"cancelled\")\n        {:noreply, socket}\n      end\n\n      def handle_event(\"cancel-order-asynchronously\", _, socket) do\n        ...\n        pid = self()\n\n        Task.Supervisor.start_child(MyTaskSup, fn ->\n          # Do something asynchronously\n          send_update(pid, Cart, id: \"cart\", status: \"cancelled\")\n        end)\n\n        {:noreply, socket}\n      end\n\n      def render(assigns) do\n        ~H\"\"\"\n        <.some_component on_complete={&send_update(@myself, completed: &1)} />\n        \"\"\"\n      end\n  '''\n  def send_update(pid \\\\ self(), module_or_cid, assigns)\n\n  def send_update(pid, module, assigns) when is_atom(module) and is_pid(pid) do\n    assigns = Enum.into(assigns, %{})\n\n    id =\n      assigns[:id] ||\n        raise ArgumentError, \"missing required :id in send_update. Got: #{inspect(assigns)}\"\n\n    Phoenix.LiveView.Channel.send_update(pid, {module, id}, assigns)\n  end\n\n  def send_update(pid, %Phoenix.LiveComponent.CID{} = cid, assigns) when is_pid(pid) do\n    assigns = Enum.into(assigns, %{})\n\n    Phoenix.LiveView.Channel.send_update(pid, cid, assigns)\n  end\n\n  @doc \"\"\"\n  Similar to `send_update/3` but the update will be delayed according to the given `time_in_milliseconds`.\n\n  It returns a reference which can be cancelled with `Process.cancel_timer/1`.\n\n  ## Examples\n\n      def handle_event(\"cancel-order\", _, socket) do\n        ...\n        send_update_after(Cart, [id: \"cart\", status: \"cancelled\"], 3000)\n        {:noreply, socket}\n      end\n\n      def handle_event(\"cancel-order-asynchronously\", _, socket) do\n        ...\n        pid = self()\n\n        Task.start(fn ->\n          # Do something asynchronously\n          send_update_after(pid, Cart, [id: \"cart\", status: \"cancelled\"], 3000)\n        end)\n\n        {:noreply, socket}\n      end\n  \"\"\"\n  def send_update_after(pid \\\\ self(), module_or_cid, assigns, time_in_milliseconds)\n\n  def send_update_after(pid, %Phoenix.LiveComponent.CID{} = cid, assigns, time_in_milliseconds)\n      when is_integer(time_in_milliseconds) and is_pid(pid) do\n    assigns = Enum.into(assigns, %{})\n\n    Phoenix.LiveView.Channel.send_update_after(pid, cid, assigns, time_in_milliseconds)\n  end\n\n  def send_update_after(pid, module, assigns, time_in_milliseconds)\n      when is_atom(module) and is_integer(time_in_milliseconds) and is_pid(pid) do\n    assigns = Enum.into(assigns, %{})\n\n    id =\n      assigns[:id] ||\n        raise ArgumentError, \"missing required :id in send_update_after. Got: #{inspect(assigns)}\"\n\n    Phoenix.LiveView.Channel.send_update_after(pid, {module, id}, assigns, time_in_milliseconds)\n  end\n\n  @doc \"\"\"\n  Returns the transport pid of the socket.\n\n  Raises `ArgumentError` if the socket is not connected.\n\n  ## Examples\n\n      iex> transport_pid(socket)\n      #PID<0.107.0>\n  \"\"\"\n  def transport_pid(%Socket{}) do\n    case Process.get(:\"$callers\") do\n      [transport_pid | _] -> transport_pid\n      _ -> raise ArgumentError, \"transport_pid/1 may only be called when the socket is connected.\"\n    end\n  end\n\n  defp child?(%Socket{parent_pid: pid}), do: is_pid(pid)\n\n  @doc \"\"\"\n  Attaches the given `fun` by `name` for the lifecycle `stage` into `socket`.\n\n  > Note: This function is for server-side lifecycle callbacks.\n  > For client-side hooks, see the\n  > [JS Interop guide](js-interop.html#client-hooks-via-phx-hook).\n\n  Hooks provide a mechanism to tap into key stages of the LiveView\n  lifecycle in order to bind/update assigns, intercept events,\n  patches, and regular messages when necessary, and to inject\n  common functionality. Use `attach_hook/4` on any of the following\n  lifecycle stages: `:handle_params`, `:handle_event`, `:handle_info`, `:handle_async`, and\n  `:after_render`. To attach a hook to the `:mount` stage, use `on_mount/1`.\n\n  > Note: only `:after_render`, `:handle_event` and `:handle_async` hooks are currently supported in\n  > LiveComponents.\n\n  ## Return Values\n\n  Lifecycle hooks take place immediately before a given lifecycle\n  callback is invoked on the LiveView. With the exception of `:after_render`,\n  a hook may return `{:halt, socket}` to halt the reduction, otherwise\n  it must return `{:cont, socket}` so the operation may continue until\n  all hooks have been invoked for the current stage.\n\n  For `:after_render` hooks, the `socket` itself must be returned.\n  Any updates to the socket assigns *will not* trigger a new render\n  or diff calculation to the client.\n\n  ## Halting the lifecycle\n\n  Note that halting from a hook _will halt the entire lifecycle stage_.\n  This means that when a hook returns `{:halt, socket}` then the\n  LiveView callback will **not** be invoked. This has some\n  implications.\n\n  ### Implications for plugin authors\n\n  When defining a plugin that matches on specific callbacks, you **must**\n  define a catch-all clause, as your hook will be invoked even for events\n  you may not be interested in.\n\n  ### Implications for end-users\n\n  Allowing a hook to halt the invocation of the callback means that you can\n  attach hooks to intercept specific events before detaching themselves,\n  while allowing other events to continue normally.\n\n  ## Replying to events\n\n  Hooks attached to the `:handle_event` stage are able to reply to client events\n  by returning `{:halt, reply, socket}`. This is useful especially for [JavaScript\n  interoperability](js-interop.html#client-hooks-via-phx-hook) because a client hook\n  can push an event and receive a reply.\n\n  ## Sharing event handling logic\n\n  Lifecycle hooks are an excellent way to extract related events out of the parent LiveView and\n  into separate modules without resorting unnecessarily to LiveComponents for organization.\n\n      defmodule DemoLive do\n        use Phoenix.LiveView\n\n        def render(assigns) do\n          ~H\\\"\"\"\n          <div>\n            <div>\n              Counter: {@counter}\n              <button phx-click=\"inc\">+</button>\n            </div>\n\n            <MySortComponent.display lists={[first_list: @first_list, second_list: @second_list]} />\n          </div>\n          \\\"\"\"\n        end\n\n        def mount(_params, _session, socket) do\n          first_list = for(i <- 1..9, do: \"First List \\#{i}\") |> Enum.shuffle()\n          second_list = for(i <- 1..9, do: \"Second List \\#{i}\") |> Enum.shuffle()\n\n          socket =\n            socket\n            |> assign(:counter, 0)\n            |> assign(first_list: first_list)\n            |> assign(second_list: second_list)\n            |> attach_hook(:sort, :handle_event, &MySortComponent.hooked_event/3)  # 2) Delegated events\n          {:ok, socket}\n        end\n\n        # 1) Normal event\n        def handle_event(\"inc\", _params, socket) do\n          {:noreply, update(socket, :counter, &(&1 + 1))}\n        end\n      end\n\n      defmodule MySortComponent do\n        use Phoenix.Component\n\n        def display(assigns) do\n          ~H\\\"\"\"\n          <div :for={{key, list} <- @lists}>\n            <ul><li :for={item <- list}>{item}</li></ul>\n            <button phx-click=\"shuffle\" phx-value-list={key}>Shuffle</button>\n            <button phx-click=\"sort\" phx-value-list={key}>Sort</button>\n          </div>\n          \\\"\"\"\n        end\n\n        def hooked_event(\"shuffle\", %{\"list\" => key}, socket) do\n          key = String.to_existing_atom(key)\n          shuffled = Enum.shuffle(socket.assigns[key])\n\n          {:halt, assign(socket, key, shuffled)}\n        end\n\n        def hooked_event(\"sort\", %{\"list\" => key}, socket) do\n          key = String.to_existing_atom(key)\n          sorted = Enum.sort(socket.assigns[key])\n\n          {:halt, assign(socket, key, sorted)}\n        end\n\n        def hooked_event(_event, _params, socket), do: {:cont, socket}\n      end\n\n  ## Other examples\n\n  Attaching and detaching a hook:\n\n      def mount(_params, _session, socket) do\n        socket =\n          attach_hook(socket, :my_hook, :handle_event, fn\n            \"very-special-event\", _params, socket ->\n              # Handle the very special event and then detach the hook\n              {:halt, detach_hook(socket, :my_hook, :handle_event)}\n\n            _event, _params, socket ->\n              {:cont, socket}\n          end)\n\n        {:ok, socket}\n      end\n\n  Replying to a client event:\n\n  ```javascript\n  /**\n   * @type {import(\"phoenix_live_view\").HooksOption}\n   */\n  let Hooks = {}\n  Hooks.ClientHook = {\n    mounted() {\n      this.pushEvent(\"ClientHook:mounted\", {hello: \"world\"}, (reply) => {\n        console.log(\"received reply:\", reply)\n      })\n    }\n  }\n  let liveSocket = new LiveSocket(\"/live\", Socket, {hooks: Hooks, ...})\n  ```\n\n      def render(assigns) do\n        ~H\"\\\"\"\n        <div id=\"my-client-hook\" phx-hook=\"ClientHook\"></div>\n        \"\\\"\"\n      end\n\n      def mount(_params, _session, socket) do\n        socket =\n          attach_hook(socket, :reply_on_client_hook_mounted, :handle_event, fn\n            \"ClientHook:mounted\", params, socket ->\n              {:halt, params, socket}\n\n            _, _, socket ->\n              {:cont, socket}\n          end)\n\n        {:ok, socket}\n      end\n  \"\"\"\n  defdelegate attach_hook(socket, name, stage, fun), to: Phoenix.LiveView.Lifecycle\n\n  @doc \"\"\"\n  Detaches a hook with the given `name` from the lifecycle `stage`.\n\n  > Note: This function is for server-side lifecycle callbacks.\n  > For client-side hooks, see the\n  > [JS Interop guide](js-interop.html#client-hooks-via-phx-hook).\n\n  If no hook is found, this function is a no-op.\n\n  ## Examples\n\n      def handle_event(_, socket) do\n        {:noreply, detach_hook(socket, :hook_that_was_attached, :handle_event)}\n      end\n  \"\"\"\n  defdelegate detach_hook(socket, name, stage), to: Phoenix.LiveView.Lifecycle\n\n  @doc ~S\"\"\"\n  Assigns a new stream to the socket or inserts items into an existing stream.\n  Returns an updated `socket`.\n\n  Streams are a mechanism for managing large collections on the client without\n  keeping the resources on the server.\n\n    * `name` - A string or atom name of the key to place under the\n      `@streams` assign.\n    * `items` - An enumerable of items to insert.\n\n  The following options are supported:\n\n    * `:at` - The index to insert or update the items in the\n      collection on the client. By default `-1` is used, which appends the items\n      to the parent DOM container. A value of `0` prepends the items.\n\n      Note that this operation is equal to inserting the items one by one, each at\n      the given index. Therefore, when inserting multiple items at an index other than `-1`,\n      the UI will display the items in reverse order:\n\n          stream(socket, :songs, [song1, song2, song3], at: 0)\n\n      In this case the UI will prepend `song1`, then `song2` and then `song3`, so it will show\n      `song3`, `song2`, `song1` and then any previously inserted items.\n\n      To insert in the order of the list, use `Enum.reverse/1`:\n\n          stream(socket, :songs, Enum.reverse([song1, song2, song3]), at: 0)\n\n    * `:reset` - A boolean to reset the stream on the client or not. Defaults\n      to `false`.\n\n    * `:limit` - An optional positive or negative number of results to limit\n      on the UI on the client. As new items are streamed, the UI will remove existing\n      items to maintain the limit. For example, to limit the stream to the last 10 items\n      in the UI while appending new items, pass a negative value:\n\n          stream(socket, :songs, songs, at: -1, limit: -10)\n\n      Likewise, to limit the stream to the first 10 items, while prepending new items,\n      pass a positive value:\n\n          stream(socket, :songs, songs, at: 0, limit: 10)\n\n  Once a stream is defined, a new `@streams` assign is available containing\n  the name of the defined streams. For example, in the above definition, the\n  stream may be referenced as `@streams.songs` in your template. Stream items\n  are temporary and freed from socket state immediately after the `render/1`\n  function is invoked (or a template is rendered from disk).\n\n  By default, calling `stream/4` on an existing stream will bulk insert the new items\n  on the client while leaving the existing items in place. Streams may also be reset\n  when calling `stream/4`, which we discuss below.\n\n  ## Resetting a stream\n\n  To empty a stream container on the client, you can pass `:reset` with an empty list:\n\n      stream(socket, :songs, [], reset: true)\n\n  Or you can replace the entire stream on the client with a new collection:\n\n      stream(socket, :songs, new_songs, reset: true)\n\n  ## Limiting a stream\n\n  It is often useful to limit the number of items in the UI while allowing the\n  server to stream new items in a fire-and-forget fashion. This prevents\n  the server from overwhelming the client with new results while also opening up\n  powerful features like virtualized infinite scrolling. See a complete\n  bidirectional infinite scrolling example with stream limits in the\n  [scroll events guide](bindings.md#scroll-events-and-infinite-pagination)\n\n  When a stream exceeds the limit on the client, the existing items will be pruned\n  based on the number of items in the stream container and the limit direction. A\n  positive limit will prune items from the end of the container, while a negative\n  limit will prune items from the beginning of the container.\n\n  Note that the limit is not enforced on the first `c:mount/3` render (when no websocket\n  connection was established yet), as it means more data than necessary has been\n  loaded. In such cases, you should only load and pass the desired amount of items\n  to the stream.\n\n  When inserting single items using `stream_insert/4`, the limit needs to be passed\n  as an option for it to be enforced on the client:\n\n      stream_insert(socket, :songs, song, limit: -10)\n\n  ## Required DOM attributes\n\n  For stream items to be trackable on the client, the following requirements\n  must be met:\n\n    1. The parent DOM container must include a `phx-update=\"stream\"` attribute,\n       along with a unique DOM id.\n    2. Each stream item must include its DOM id on the item's element.\n\n  > #### Note {: .warning}\n  >\n  > Failing to place `phx-update=\"stream\"` on the **immediate parent** for\n  > **each stream** will result in broken behavior.\n  >\n  > Also, do not alter the generated DOM ids, e.g., by prefixing them. Doing so will\n  > result in broken behavior.\n\n  When consuming a stream in a template, the DOM id and item is passed as a tuple,\n  allowing convenient inclusion of the DOM id for each item. For example:\n\n  ```heex\n  <table>\n    <tbody id=\"songs\" phx-update=\"stream\">\n      <tr\n        :for={{dom_id, song} <- @streams.songs}\n        id={dom_id}\n      >\n        <td>{song.title}</td>\n        <td>{song.duration}</td>\n      </tr>\n    </tbody>\n  </table>\n  ```\n\n  We consume the stream in a for comprehension by referencing the\n  `@streams.songs` assign. We used the computed DOM id to populate\n  the `<tr>` id, then we render the table row as usual.\n\n  Now `stream_insert/3` and `stream_delete/3` may be issued and new rows will\n  be inserted or deleted from the client.\n\n  ## Handling the empty case\n\n  When rendering a list of items, it is common to show a message for the empty case.\n  But when using streams, we cannot rely on `Enum.empty?/1` or similar approaches to\n  check if the list is empty. Instead we can use the CSS `:only-child` selector\n  and show the message client side:\n\n  ```heex\n  <table>\n    <tbody id=\"songs\" phx-update=\"stream\">\n      <tr id=\"songs-empty\" class=\"only:table-row hidden\">\n        <td colspan=\"2\">No songs found</td>\n      </tr>\n      <tr\n        :for={{dom_id, song} <- @streams.songs}\n        id={dom_id}\n      >\n        <td>{song.title}</td>\n        <td>{song.duration}</td>\n      </tr>\n    </tbody>\n  </table>\n  ```\n\n  It is important to set a unique ID on the empty row, otherwise it cannot be tracked\n  in the stream container and subsequent patches will duplicate the node.\n\n  ## Non-stream items in stream containers\n\n  In the section on handling the empty case, we showed how to render a message when\n  the stream is empty by rendering a non-stream item inside the stream container.\n\n  Note that for non-stream items inside a `phx-update=\"stream\"` container, the following\n  needs to be considered:\n\n    1. Non-stream items must have a unique DOM id.\n\n    2. Items can be added and updated, but not removed, even if the stream is reset.\n\n       This means that if you try to conditionally render a non-stream item inside a stream container,\n       it won't be removed if it was rendered once.\n\n    3. Items are affected by the `:at` option.\n\n       For example, when you render a non-stream item at the beginning of the stream container and then\n       prepend items (with `at: 0`) to the stream, the non-stream item will be pushed down.\n\n  \"\"\"\n  @spec stream(\n          socket :: Socket.t(),\n          name :: atom | String.t(),\n          items :: Enumerable.t(),\n          opts :: Keyword.t()\n        ) ::\n          Socket.t()\n  def stream(%Socket{} = socket, name, items, opts \\\\ []) do\n    socket\n    |> ensure_streams()\n    |> assign_stream(name, items, opts)\n  end\n\n  @doc ~S\"\"\"\n  Configures a stream.\n\n  The following options are supported:\n\n    * `:dom_id` - An optional function to generate each stream item's DOM id.\n      The function accepts each stream item and converts the item to a string id.\n      By default, the `:id` field of a map or struct will be used if the item has\n      such a field, and will be prefixed by the `name` hyphenated with the id.\n      For example, the following examples are equivalent:\n\n          stream(socket, :songs, songs)\n\n          socket\n          |> stream_configure(:songs, dom_id: &(\"songs-#{&1.id}\"))\n          |> stream(:songs, songs)\n\n  A stream must be configured before items are inserted, and once configured,\n  a stream may not be re-configured. To ensure a stream is only configured a\n  single time in a LiveComponent, use the `mount/1` callback. For example:\n\n      def mount(socket) do\n        {:ok, stream_configure(socket, :songs, dom_id: &(\"songs-#{&1.id}\"))}\n      end\n\n      def update(assigns, socket) do\n        {:ok, stream(socket, :songs, ...)}\n      end\n\n  Returns an updated `socket`.\n  \"\"\"\n  @spec stream_configure(socket :: Socket.t(), name :: atom | String.t(), opts :: Keyword.t()) ::\n          Socket.t()\n  def stream_configure(%Socket{} = socket, name, opts) when is_list(opts) do\n    new_socket = ensure_streams(socket)\n\n    case new_socket.assigns.streams do\n      %{^name => %LiveStream{}} ->\n        raise ArgumentError, \"cannot configure stream :#{name} after it has been streamed\"\n\n      %{__configured__: %{^name => _opts}} ->\n        raise ArgumentError, \"cannot re-configure stream :#{name} after it has been configured\"\n\n      %{} ->\n        Phoenix.Component.update(new_socket, :streams, fn streams ->\n          Map.update!(streams, :__configured__, fn conf -> Map.put(conf, name, opts) end)\n        end)\n    end\n  end\n\n  defp ensure_streams(%Socket{} = socket) do\n    # don't use assign_new here because we DON'T want to copy parent streams\n    # during the dead render of nested or sticky LiveViews\n    case socket.assigns do\n      %{streams: _} ->\n        socket\n\n      _ ->\n        Phoenix.LiveView.Utils.assign(socket, :streams, %{\n          __ref__: 0,\n          __changed__: MapSet.new(),\n          __configured__: %{}\n        })\n    end\n  end\n\n  @doc \"\"\"\n  Inserts a new item or updates an existing item in the stream.\n\n  Returns an updated `socket`.\n\n  See `stream/4` for inserting multiple items at once.\n\n  The following options are supported:\n\n    * `:at` - The index to insert or update the item in the collection on the client.\n      By default, the item is appended to the parent DOM container. This is the same as\n      passing a value of `-1`.\n      If the item already exists in the parent DOM container then it will be\n      updated in place.\n\n    * `:limit` - A limit of items to maintain in the UI. A limit passed to `stream/4` does\n      not affect subsequent calls to `stream_insert/4`, therefore the limit must be passed\n      here as well in order to be enforced. See `stream/4` for more information on\n      limiting streams.\n\n    * `:update_only` - A boolean to only update the item in the stream. If the item does not\n      exist on the client, it will not be inserted. Defaults to `false`.\n\n  ## Examples\n\n  Imagine you define a stream on mount with a single item:\n\n      stream(socket, :songs, [%Song{id: 1, title: \"Song 1\"}])\n\n  Then, in a callback such as `handle_info` or `handle_event`, you\n  can append a new song:\n\n      stream_insert(socket, :songs, %Song{id: 2, title: \"Song 2\"})\n\n  Or prepend a new song with `at: 0`:\n\n      stream_insert(socket, :songs, %Song{id: 2, title: \"Song 2\"}, at: 0)\n\n  Or update an existing song (in this case the `:at` option has no effect):\n\n      stream_insert(socket, :songs, %Song{id: 1, title: \"Song 1 updated\"}, at: 0)\n\n  Or append a new song while limiting the stream to the last 10 items:\n\n      stream_insert(socket, :songs, %Song{id: 2, title: \"Song 2\"}, limit: -10)\n\n  ## Updating Items\n\n  As shown, an existing item on the client can be updated by issuing a `stream_insert`\n  for the existing item. When the client updates an existing item, the item will remain\n  in the same location as it was previously, and will not be moved to the end of the\n  parent children. To both update an existing item and move it to another position,\n  issue a `stream_delete`, followed by a `stream_insert`. For example:\n\n      song = get_song!(id)\n\n      socket\n      |> stream_delete(:songs, song)\n      |> stream_insert(:songs, song, at: -1)\n\n  See `stream_delete/3` for more information on deleting items.\n  \"\"\"\n  @spec stream_insert(\n          socket :: Socket.t(),\n          name :: atom | String.t(),\n          item :: any,\n          opts :: Keyword.t()\n        ) ::\n          Socket.t()\n  def stream_insert(%Socket{} = socket, name, item, opts \\\\ []) do\n    at = Keyword.get(opts, :at, -1)\n    limit = Keyword.get(opts, :limit)\n    update_only = Keyword.get(opts, :update_only, false)\n\n    update_stream(socket, name, &LiveStream.insert_item(&1, item, at, limit, update_only))\n  end\n\n  @doc \"\"\"\n  Deletes an item from the stream.\n\n  The item's DOM is computed from the `:dom_id` provided in the `stream/3` definition.\n  Delete information for this DOM id is sent to the client and the item's element\n  is removed from the DOM, following the same behavior of element removal, such as\n  invoking `phx-remove` commands and executing client hook `destroyed()` callbacks.\n\n  ## Examples\n\n      def handle_event(\"delete\", %{\"id\" => id}, socket) do\n        song = get_song!(id)\n        {:noreply, stream_delete(socket, :songs, song)}\n      end\n\n  See `stream_delete_by_dom_id/3` to remove an item without requiring the\n  original data structure.\n\n  Returns an updated `socket`.\n  \"\"\"\n  @spec stream_delete(socket :: Socket.t(), name :: atom | String.t(), item :: any) :: Socket.t()\n  def stream_delete(%Socket{} = socket, name, item) do\n    update_stream(socket, name, &LiveStream.delete_item(&1, item))\n  end\n\n  @doc ~S'''\n  Deletes an item from the stream given its computed DOM id.\n\n  Returns an updated `socket`.\n\n  Behaves just like `stream_delete/3`, but accept the precomputed DOM id,\n  which allows deleting from a stream without fetching or building the original\n  stream data structure.\n\n  ## Examples\n\n      def render(assigns) do\n        ~H\"\"\"\n        <table>\n          <tbody id=\"songs\" phx-update=\"stream\">\n            <tr\n              :for={{dom_id, song} <- @streams.songs}\n              id={dom_id}\n            >\n              <td>{song.title}</td>\n              <td><button phx-click={JS.push(\"delete\", value: %{id: dom_id})}>delete</button></td>\n            </tr>\n          </tbody>\n        </table>\n        \"\"\"\n      end\n\n      def handle_event(\"delete\", %{\"id\" => dom_id}, socket) do\n        {:noreply, stream_delete_by_dom_id(socket, :songs, dom_id)}\n      end\n  '''\n  @spec stream_delete_by_dom_id(socket :: Socket.t(), name :: atom | String.t(), id :: String.t()) ::\n          Socket.t()\n  def stream_delete_by_dom_id(%Socket{} = socket, name, id) do\n    update_stream(socket, name, &LiveStream.delete_item_by_dom_id(&1, id))\n  end\n\n  defp assign_stream(%Socket{} = socket, name, items, opts) do\n    streams = socket.assigns.streams\n\n    case streams do\n      %{^name => %LiveStream{}} ->\n        new_socket =\n          if opts[:reset] do\n            update_stream(socket, name, &LiveStream.reset(&1))\n          else\n            socket\n          end\n\n        Enum.reduce(items, new_socket, fn item, acc -> stream_insert(acc, name, item, opts) end)\n\n      %{} ->\n        config = get_in(streams, [:__configured__, name]) || []\n        opts = Keyword.merge(opts, config)\n\n        ref =\n          if cid = socket.assigns[:myself] do\n            \"#{cid}-#{streams.__ref__}\"\n          else\n            to_string(streams.__ref__)\n          end\n\n        stream = LiveStream.new(name, ref, items, opts)\n\n        socket\n        |> Phoenix.Component.update(:streams, fn streams ->\n          %{streams | __ref__: streams.__ref__ + 1}\n          |> Map.put(name, stream)\n          |> Map.update!(:__changed__, &MapSet.put(&1, name))\n        end)\n        |> attach_hook(name, :after_render, fn hook_socket ->\n          if name in hook_socket.assigns.streams.__changed__ do\n            Phoenix.Component.update(hook_socket, :streams, fn streams ->\n              streams\n              |> Map.update!(:__changed__, &MapSet.delete(&1, name))\n              |> Map.update!(name, &LiveStream.prune(&1))\n            end)\n          else\n            hook_socket\n          end\n        end)\n    end\n  end\n\n  defp update_stream(%Socket{} = socket, name, func) do\n    Phoenix.Component.update(socket, :streams, fn streams ->\n      stream =\n        case Map.fetch(streams, name) do\n          {:ok, stream} -> stream\n          :error -> raise ArgumentError, \"no stream with name #{inspect(name)} previously defined\"\n        end\n\n      streams\n      |> Map.put(name, func.(stream))\n      |> Map.update!(:__changed__, &MapSet.put(&1, name))\n    end)\n  end\n\n  @doc \"\"\"\n  Assigns keys asynchronously.\n\n  Wraps your function in a task linked to the caller, errors are wrapped.\n  Each key passed to `assign_async/3` will be assigned to\n  an `Phoenix.LiveView.AsyncResult` struct holding the status of the operation\n  and the result when the function completes.\n\n  The function must return either a map or a keyword list with the assigns\n  to merge into the socket.\n\n  The task is only started when the socket is connected.\n\n  ## Options\n\n    * `:supervisor` - allows you to specify a `Task.Supervisor` to supervise the task.\n    * `:reset` - remove previous results during async operation when true. Possible values are\n      `true`, `false`, or a list of keys to reset. Defaults to `false`.\n\n  ## Examples\n\n  ```elixir\n  def mount(%{\"slug\" => slug}, _, socket) do\n    {:ok,\n      socket\n      |> assign(:foo, \"bar\")\n      |> assign_async(:org, fn -> {:ok, %{org: fetch_org!(slug)}} end)\n      |> assign_async([:profile, :rank], fn -> {:ok, %{profile: ..., rank: ...}} end)}\n  end\n  ```\n\n  See [Async Operations](#module-async-operations) for more information.\n\n  ## `assign_async/3` and `send_update/3`\n\n  Since the code inside `assign_async/3` runs in a separate process,\n  `send_update(Component, data)` does not work inside `assign_async/3`,\n  since `send_update/2` assumes it is running inside the LiveView process.\n  The solution is to explicitly send the update to the LiveView:\n\n  ```elixir\n  parent = self()\n  assign_async(socket, :org, fn ->\n    # ...\n    send_update(parent, Component, data)\n  end)\n  ```\n\n  ## Testing async operations\n\n  When testing LiveViews and LiveComponents with async assigns, use\n  `Phoenix.LiveViewTest.render_async/2` to ensure the test waits until the async operations\n  are complete before proceeding with assertions or before ending the test. For example:\n\n  ```elixir\n  {:ok, view, _html} = live(conn, \"/my_live_view\")\n  html = render_async(view)\n  assert html =~ \"My assertion\"\n  ```\n\n  Not calling `render_async/2` to ensure all async assigns have finished might result in errors in\n  cases where your process has side effects:\n\n  ```\n  [error] MyXQL.Connection (#PID<0.308.0>) disconnected: ** (DBConnection.ConnectionError) client #PID<0.794.0>\n  ```\n\n  \"\"\"\n  defmacro assign_async(socket, key_or_keys, func, opts \\\\ []) do\n    Async.assign_async(socket, key_or_keys, func, opts, __CALLER__)\n  end\n\n  @doc \"\"\"\n  Wraps your function in an asynchronous task and invokes a callback `name` to\n  handle the result.\n\n  The task is linked to the caller and errors/exits are wrapped.\n  The result of the task is sent to the `c:handle_async/3` callback\n  of the caller LiveView or LiveComponent.\n\n  If there is an in-flight task with the same `name`, the later `start_async` wins and the previous task’s result is ignored.\n  If you wish to replace an existing task, you can use `cancel_async/3` before `start_async/3`.\n  You are not restricted to just atoms for `name`, it can be any term such as a tuple.\n\n  The task is only started when the socket is connected.\n\n  ## Options\n\n    * `:supervisor` - allows you to specify a `Task.Supervisor` to supervise the task.\n\n  ## Examples\n\n      def mount(%{\"id\" => id}, _, socket) do\n        {:ok,\n         socket\n         |> assign(:org, AsyncResult.loading())\n         |> start_async(:my_task, fn -> fetch_org!(id) end)}\n      end\n\n      def handle_async(:my_task, {:ok, fetched_org}, socket) do\n        %{org: org} = socket.assigns\n        {:noreply, assign(socket, :org, AsyncResult.ok(org, fetched_org))}\n      end\n\n      def handle_async(:my_task, {:exit, reason}, socket) do\n        %{org: org} = socket.assigns\n        {:noreply, assign(socket, :org, AsyncResult.failed(org, {:exit, reason}))}\n      end\n\n  See the moduledoc for more information.\n  \"\"\"\n  defmacro start_async(socket, name, func, opts \\\\ []) do\n    Async.start_async(socket, name, func, opts, __CALLER__)\n  end\n\n  @doc \"\"\"\n  Inserts data into a stream asynchronously.\n\n  Wraps your function in a task linked to the caller, errors are wrapped.\n  The key passed to `stream_async/3` will be used as the stream name. Furthermore,\n  a regular assign with the same name gets assigned a `Phoenix.LiveView.AsyncResult`\n  struct holding the status of the operation. The stream is initialized to an empty list\n  before starting the asynchronous function, so accessing `@streams.name` is always possible.\n\n  The function must return `{:ok, Enumerable.t()}` or `{:ok, Enumerable.t(), opts}`\n  where the opts are the same as in `stream/4`. The enumerable contains the values to be streamed.\n\n  If the function returns `{:error, any()}`, the `AsyncResult` is assigned as failed and\n  the stream is not updated.\n\n  The task is only started when the socket is connected.\n\n  ## Options\n\n    * `:supervisor` - allows you to specify a `Task.Supervisor` to supervise the task.\n    * `:reset` - remove previous results during async operation when true. Possible values are\n      `true`, `false`, or a list of keys to reset. Defaults to `false`.\n\n  ## Examples\n\n      def mount(%{\"slug\" => slug}, _, socket) do\n        current_scope = socket.assigns.current_scope\n\n        {:ok,\n          socket\n          |> assign(:foo, \"bar\")\n          |> assign_async(:org, fn -> {:ok, %{org: fetch_org!(current_scope)}} end)\n          |> stream_async(:posts, fn -> {:ok, list_posts!(current_scope), limit: 10} end)\n      end\n\n  Note the `reset` option controls the async assign, not the stream:\n\n      def mount(_, _, socket) do\n        {:ok,\n          socket\n          # IMPORTANT: reset here does NOT reset the stream, but only the loading state\n          |> stream_async(:my_stream, fn -> {:ok, list_items!()} end, reset: true)\n          # This resets the stream\n          |> stream_async(:my_reset_stream, fn -> {:ok, list_items!(), reset: true} end)\n      end\n\n  Any stream options need to be returned as optional third argument in the return value\n  of the asynchronous function.\n\n  \"\"\"\n  defmacro stream_async(socket, name, func, opts \\\\ []) do\n    Async.stream_async(socket, name, func, opts, __CALLER__)\n  end\n\n  @doc \"\"\"\n  Cancels an async operation if one exists.\n\n  Accepts either the `%AsyncResult{}` when using `assign_async/3` or\n  the key passed to `start_async/3`.\n\n  The underlying process will be killed with the provided reason, or\n  with `{:shutdown, :cancel}` if no reason is passed. For `assign_async/3`\n  operations, the `:failed` field will be set to `{:exit, reason}`.\n  For `start_async/3`, the `c:handle_async/3` callback will receive\n  `{:exit, reason}` as the result.\n\n  Returns the `%Phoenix.LiveView.Socket{}`.\n\n  ## Examples\n\n      cancel_async(socket, :preview)\n      cancel_async(socket, :preview, :my_reason)\n      cancel_async(socket, socket.assigns.preview)\n  \"\"\"\n  def cancel_async(socket, async_or_keys, reason \\\\ {:shutdown, :cancel}) do\n    Async.cancel_async(socket, async_or_keys, reason)\n  end\nend\n"
  },
  {
    "path": "lib/prettier.ex",
    "content": "if Mix.env() == :dev do\n  defmodule Prettier do\n    @moduledoc false\n\n    @behaviour Phoenix.LiveView.HTMLFormatter.TagFormatter\n\n    require Logger\n\n    @impl true\n    def render_tag({\"script\", attrs, content}, _opts)\n        when not is_map_key(attrs, \"runtime\") do\n      manifest = Map.get(attrs, \"manifest\", \"index.js\")\n\n      tmp_file =\n        Path.join(System.tmp_dir!(), \"prettier_#{System.unique_integer([:positive])}_#{manifest}\")\n\n      try do\n        File.write!(tmp_file, content)\n\n        case System.cmd(\"npx\", [\"prettier\", tmp_file], stderr_to_stdout: true) do\n          {output, 0} ->\n            {:ok, String.trim(output)}\n\n          {error, _} ->\n            Logger.error(\"Failed to format with prettier: #{error}\")\n            :skip\n        end\n      after\n        File.rm(tmp_file)\n      end\n    end\n\n    def render_tag({_other, _attrs, _content}, _opts) do\n      :skip\n    end\n  end\nend\n"
  },
  {
    "path": "mix.exs",
    "content": "defmodule Phoenix.LiveView.MixProject do\n  use Mix.Project\n\n  @version \"1.2.0-dev\"\n\n  def project do\n    [\n      app: :phoenix_live_view,\n      version: @version,\n      elixir: \"~> 1.15\",\n      start_permanent: Mix.env() == :prod,\n      elixirc_paths: elixirc_paths(Mix.env()),\n      test_options: [docs: true],\n      test_coverage: [summary: [threshold: 85], ignore_modules: coverage_ignore_modules()],\n      xref: [exclude: [LazyHTML, LazyHTML.Tree]],\n      package: package(),\n      deps: deps(),\n      aliases: aliases(),\n      docs: &docs/0,\n      name: \"Phoenix LiveView\",\n      homepage_url: \"http://www.phoenixframework.org\",\n      description: \"\"\"\n      Rich, real-time user experiences with server-rendered HTML\n      \"\"\",\n      listeners: [Phoenix.CodeReloader],\n      # ignore misnamed test file warnings for e2e support files\n      test_ignore_filters: [&String.starts_with?(&1, \"test/e2e/support\")]\n    ]\n  end\n\n  def cli do\n    [preferred_envs: [docs: :docs]]\n  end\n\n  defp elixirc_paths(:e2e), do: [\"lib\", \"test/support\", \"test/e2e/support\"]\n  defp elixirc_paths(:test), do: [\"lib\", \"test/support\"]\n  defp elixirc_paths(_), do: [\"lib\"]\n\n  def application do\n    [\n      mod: {Phoenix.LiveView.Application, []},\n      extra_applications: [:logger]\n    ]\n  end\n\n  defp deps do\n    [\n      {:igniter, \"~> 0.6 and >= 0.6.16\", optional: true},\n      {:phoenix, \"~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0\"},\n      {:plug, \"~> 1.15\"},\n      {:phoenix_template, \"~> 1.0\"},\n      {:phoenix_html, \"~> 3.3 or ~> 4.0 or ~> 4.1\"},\n      {:telemetry, \"~> 0.4.2 or ~> 1.0\"},\n      {:esbuild, \"~> 0.2\", only: :dev},\n      {:phoenix_view, \"~> 2.0\", optional: true},\n      {:jason, \"~> 1.0\", optional: true},\n      {:lazy_html, \"~> 0.1.0\", optional: true},\n      {:ex_doc, \"~> 0.29\", only: :docs},\n      {:makeup_elixir, \"~> 1.0.1 or ~> 1.1\", only: [:docs, :e2e]},\n      {:makeup_eex, \"~> 2.0\", only: [:docs, :e2e]},\n      {:makeup_syntect, \"~> 0.1.0\", only: [:docs, :e2e]},\n      {:html_entities, \">= 0.0.0\", only: :test},\n      {:phoenix_live_reload, \"~> 1.4\", only: :test},\n      {:phoenix_html_helpers, \"~> 1.0\", only: :test},\n      {:bandit, \"~> 1.5\", only: :e2e},\n      {:ecto, \"~> 3.11\", only: :e2e},\n      {:phoenix_ecto, \"~> 4.5\", only: :e2e}\n    ]\n  end\n\n  defp docs do\n    [\n      main: \"welcome\",\n      source_ref: \"v#{@version}\",\n      source_url: \"https://github.com/phoenixframework/phoenix_live_view\",\n      extra_section: \"GUIDES\",\n      extras: extras(),\n      groups_for_extras: groups_for_extras(),\n      groups_for_modules: groups_for_modules(),\n      groups_for_docs: [\n        Components: &(&1[:type] == :component),\n        Macros: &(&1[:type] == :macro)\n      ],\n      skip_undefined_reference_warnings_on: [\"CHANGELOG.md\"],\n      before_closing_body_tag: &before_closing_body_tag/1\n    ]\n  end\n\n  defp before_closing_body_tag(:html) do\n    \"\"\"\n    <script defer src=\"https://cdn.jsdelivr.net/npm/mermaid@11.6.0/dist/mermaid.min.js\"></script>\n    <script>\n      let initialized = false;\n\n      window.addEventListener(\"exdoc:loaded\", () => {\n        if (!initialized) {\n          mermaid.initialize({\n            startOnLoad: false,\n            theme: document.body.className.includes(\"dark\") ? \"dark\" : \"default\"\n          });\n          initialized = true;\n        }\n\n        let id = 0;\n        for (const codeEl of document.querySelectorAll(\"pre code.mermaid\")) {\n          const graphDefinition = codeEl.textContent;\n          const graphId = \"mermaid-graph-\" + id++;\n          mermaid.render(graphId, graphDefinition).then(({svg, bindFunctions}) => {\n            codeEl.innerHTML = svg;\n            bindFunctions?.(codeEl);\n          });\n        }\n      });\n    </script>\n    \"\"\"\n  end\n\n  defp before_closing_body_tag(_), do: \"\"\n\n  defp extras do\n    [\"CHANGELOG.md\"] ++\n      Path.wildcard(\"guides/*/*.md\") ++\n      Path.wildcard(\"guides/cheatsheets/*.cheatmd\")\n  end\n\n  defp groups_for_extras do\n    [\n      Introduction: ~r\"guides/introduction/\",\n      \"Server-side features\": ~r\"guides/server/\",\n      \"Client-side integration\": ~r\"guides/client/\",\n      Cheatsheets: ~r\"guides/cheatsheets/\"\n    ]\n  end\n\n  defp groups_for_modules do\n    # Ungrouped Modules:\n    #\n    # Phoenix.Component\n    # Phoenix.LiveComponent\n    # Phoenix.LiveView\n    # Phoenix.LiveView.Controller\n    # Phoenix.LiveView.JS\n    # Phoenix.LiveView.Router\n    # Phoenix.LiveViewTest\n\n    [\n      Configuration: [\n        Phoenix.LiveView.HTMLFormatter,\n        Phoenix.LiveView.Logger,\n        Phoenix.LiveView.Socket\n      ],\n      \"Testing structures\": [\n        Phoenix.LiveViewTest.Element,\n        Phoenix.LiveViewTest.Upload,\n        Phoenix.LiveViewTest.View\n      ],\n      \"Upload structures\": [\n        Phoenix.LiveView.UploadConfig,\n        Phoenix.LiveView.UploadEntry,\n        Phoenix.LiveView.UploadWriter\n      ],\n      \"Plugin API\": [\n        Phoenix.LiveComponent.CID,\n        Phoenix.LiveView.Engine,\n        Phoenix.LiveView.TagEngine,\n        Phoenix.LiveView.HTMLEngine,\n        Phoenix.LiveView.Component,\n        Phoenix.LiveView.Rendered,\n        Phoenix.LiveView.Comprehension\n      ]\n    ]\n  end\n\n  defp package do\n    [\n      maintainers: [\"Chris McCord\", \"José Valim\", \"Gary Rennie\", \"Alex Garibay\", \"Scott Newcomer\"],\n      licenses: [\"MIT\"],\n      links: %{\n        Changelog: \"https://hexdocs.pm/phoenix_live_view/changelog.html\",\n        GitHub: \"https://github.com/phoenixframework/phoenix_live_view\"\n      },\n      files:\n        ~w(assets/js lib priv) ++\n          ~w(CHANGELOG.md LICENSE.md mix.exs package.json README.md .formatter.exs)\n    ]\n  end\n\n  defp aliases do\n    [\n      \"assets.build\": [\n        \"cmd npm run build\",\n        \"esbuild module\",\n        \"esbuild cdn\",\n        \"esbuild cdn_min\",\n        \"esbuild main\"\n      ],\n      \"assets.watch\": [\"cmd npm run build -- --watch\", \"esbuild module --watch\"]\n    ]\n  end\n\n  defp coverage_ignore_modules do\n    [\n      ~r/Phoenix\\.LiveViewTest\\.Support\\..*/,\n      ~r/Phoenix\\.LiveViewTest\\.E2E\\..*/,\n      ~r/Inspect\\..*/\n    ]\n  end\nend\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"phoenix_live_view\",\n  \"version\": \"1.2.0-dev\",\n  \"description\": \"The Phoenix LiveView JavaScript client.\",\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"module\": \"./priv/static/phoenix_live_view.esm.js\",\n  \"main\": \"./priv/static/phoenix_live_view.cjs.js\",\n  \"unpkg\": \"./priv/static/phoenix_live_view.min.js\",\n  \"jsdelivr\": \"./priv/static/phoenix_live_view.min.js\",\n  \"exports\": {\n    \"import\": {\n      \"types\": \"./assets/js/types/index.d.ts\",\n      \"default\": \"./priv/static/phoenix_live_view.esm.js\"\n    },\n    \"require\": \"./priv/static/phoenix_live_view.cjs.js\"\n  },\n  \"author\": \"Chris McCord <chris@chrismccord.com> (http://www.phoenixframework.org)\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git://github.com/phoenixframework/phoenix_live_view.git\"\n  },\n  \"files\": [\n    \"README.md\",\n    \"LICENSE.md\",\n    \"package.json\",\n    \"priv/static/*\",\n    \"assets/js/**\"\n  ],\n  \"types\": \"./assets/js/types/index.d.ts\",\n  \"dependencies\": {\n    \"morphdom\": \"2.7.8\"\n  },\n  \"devDependencies\": {\n    \"@babel/cli\": \"7.27.2\",\n    \"@babel/core\": \"7.27.4\",\n    \"@babel/preset-env\": \"7.27.2\",\n    \"@babel/preset-typescript\": \"^7.27.1\",\n    \"@eslint/js\": \"^9.29.0\",\n    \"@playwright/test\": \"^1.58.2\",\n    \"@types/jest\": \"^30.0.0\",\n    \"@types/phoenix\": \"^1.6.6\",\n    \"css.escape\": \"^1.5.1\",\n    \"eslint\": \"9.29.0\",\n    \"eslint-plugin-jest\": \"28.14.0\",\n    \"eslint-plugin-playwright\": \"^2.2.0\",\n    \"globals\": \"^16.2.0\",\n    \"jest\": \"^30.0.0\",\n    \"jest-environment-jsdom\": \"^30.0.0\",\n    \"jest-monocart-coverage\": \"^1.1.1\",\n    \"monocart-reporter\": \"^2.9.21\",\n    \"phoenix\": \"1.7.21\",\n    \"prettier\": \"3.5.3\",\n    \"ts-jest\": \"^29.4.0\",\n    \"typescript\": \"^5.8.3\",\n    \"typescript-eslint\": \"^8.34.0\"\n  },\n  \"scripts\": {\n    \"setup\": \"mix deps.get && npm install\",\n    \"build\": \"tsc\",\n    \"e2e:server\": \"MIX_ENV=e2e mix test --cover --export-coverage e2e test/e2e/test_helper.exs\",\n    \"e2e:test\": \"mix assets.build && cd test/e2e && npx playwright install && npx playwright test\",\n    \"js:test\": \"npm run build && jest\",\n    \"js:test.coverage\": \"npm run build && jest --coverage\",\n    \"js:test.watch\": \"npm run build && jest --watch\",\n    \"js:lint\": \"eslint\",\n    \"js:format\": \"prettier --write assets --log-level warn && prettier --write test/e2e --log-level warn\",\n    \"js:format.check\": \"prettier --check assets --log-level warn && prettier --check test/e2e --log-level warn\",\n    \"test\": \"npm run js:test && npm run e2e:test\",\n    \"typecheck:tests\": \"tsc -p assets/test/tsconfig.json\",\n    \"cover:merge\": \"node test/e2e/merge-coverage.js\",\n    \"cover\": \"npm run test && npm run cover:merge\",\n    \"cover:report\": \"npx monocart show-report cover/merged-js/index.html\"\n  }\n}\n"
  },
  {
    "path": "priv/static/phoenix_live_view.cjs.js",
    "content": "var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __getOwnPropNames = Object.getOwnPropertyNames;\nvar __hasOwnProp = Object.prototype.hasOwnProperty;\nvar __export = (target, all) => {\n  for (var name in all)\n    __defProp(target, name, { get: all[name], enumerable: true });\n};\nvar __copyProps = (to, from, except, desc) => {\n  if (from && typeof from === \"object\" || typeof from === \"function\") {\n    for (let key of __getOwnPropNames(from))\n      if (!__hasOwnProp.call(to, key) && key !== except)\n        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });\n  }\n  return to;\n};\nvar __toCommonJS = (mod) => __copyProps(__defProp({}, \"__esModule\", { value: true }), mod);\n\n// js/phoenix_live_view/index.ts\nvar phoenix_live_view_exports = {};\n__export(phoenix_live_view_exports, {\n  LiveSocket: () => LiveSocket2,\n  ViewHook: () => ViewHook,\n  createHook: () => createHook,\n  isUsedInput: () => isUsedInput\n});\nmodule.exports = __toCommonJS(phoenix_live_view_exports);\n\n// js/phoenix_live_view/constants.js\nvar CONSECUTIVE_RELOADS = \"consecutive-reloads\";\nvar MAX_RELOADS = 10;\nvar RELOAD_JITTER_MIN = 5e3;\nvar RELOAD_JITTER_MAX = 1e4;\nvar FAILSAFE_JITTER = 3e4;\nvar PHX_EVENT_CLASSES = [\n  \"phx-click-loading\",\n  \"phx-change-loading\",\n  \"phx-submit-loading\",\n  \"phx-keydown-loading\",\n  \"phx-keyup-loading\",\n  \"phx-blur-loading\",\n  \"phx-focus-loading\",\n  \"phx-hook-loading\"\n];\nvar PHX_DROP_TARGET_ACTIVE_CLASS = \"phx-drop-target-active\";\nvar PHX_COMPONENT = \"data-phx-component\";\nvar PHX_VIEW_REF = \"data-phx-view\";\nvar PHX_LIVE_LINK = \"data-phx-link\";\nvar PHX_TRACK_STATIC = \"track-static\";\nvar PHX_LINK_STATE = \"data-phx-link-state\";\nvar PHX_REF_LOADING = \"data-phx-ref-loading\";\nvar PHX_REF_SRC = \"data-phx-ref-src\";\nvar PHX_REF_LOCK = \"data-phx-ref-lock\";\nvar PHX_PENDING_REFS = \"phx-pending-refs\";\nvar PHX_TRACK_UPLOADS = \"track-uploads\";\nvar PHX_UPLOAD_REF = \"data-phx-upload-ref\";\nvar PHX_PREFLIGHTED_REFS = \"data-phx-preflighted-refs\";\nvar PHX_DONE_REFS = \"data-phx-done-refs\";\nvar PHX_DROP_TARGET = \"drop-target\";\nvar PHX_ACTIVE_ENTRY_REFS = \"data-phx-active-refs\";\nvar PHX_LIVE_FILE_UPDATED = \"phx:live-file:updated\";\nvar PHX_SKIP = \"data-phx-skip\";\nvar PHX_MAGIC_ID = \"data-phx-id\";\nvar PHX_PRUNE = \"data-phx-prune\";\nvar PHX_CONNECTED_CLASS = \"phx-connected\";\nvar PHX_LOADING_CLASS = \"phx-loading\";\nvar PHX_ERROR_CLASS = \"phx-error\";\nvar PHX_CLIENT_ERROR_CLASS = \"phx-client-error\";\nvar PHX_SERVER_ERROR_CLASS = \"phx-server-error\";\nvar PHX_PARENT_ID = \"data-phx-parent-id\";\nvar PHX_MAIN = \"data-phx-main\";\nvar PHX_ROOT_ID = \"data-phx-root-id\";\nvar PHX_VIEWPORT_TOP = \"viewport-top\";\nvar PHX_VIEWPORT_BOTTOM = \"viewport-bottom\";\nvar PHX_VIEWPORT_OVERRUN_TARGET = \"viewport-overrun-target\";\nvar PHX_TRIGGER_ACTION = \"trigger-action\";\nvar PHX_HAS_FOCUSED = \"phx-has-focused\";\nvar FOCUSABLE_INPUTS = [\n  \"text\",\n  \"textarea\",\n  \"number\",\n  \"email\",\n  \"password\",\n  \"search\",\n  \"tel\",\n  \"url\",\n  \"date\",\n  \"time\",\n  \"datetime-local\",\n  \"color\",\n  \"range\"\n];\nvar CHECKABLE_INPUTS = [\"checkbox\", \"radio\"];\nvar PHX_HAS_SUBMITTED = \"phx-has-submitted\";\nvar PHX_SESSION = \"data-phx-session\";\nvar PHX_VIEW_SELECTOR = `[${PHX_SESSION}]`;\nvar PHX_STICKY = \"data-phx-sticky\";\nvar PHX_STATIC = \"data-phx-static\";\nvar PHX_READONLY = \"data-phx-readonly\";\nvar PHX_DISABLED = \"data-phx-disabled\";\nvar PHX_DISABLE_WITH = \"disable-with\";\nvar PHX_DISABLE_WITH_RESTORE = \"data-phx-disable-with-restore\";\nvar PHX_HOOK = \"hook\";\nvar PHX_DEBOUNCE = \"debounce\";\nvar PHX_THROTTLE = \"throttle\";\nvar PHX_UPDATE = \"update\";\nvar PHX_STREAM = \"stream\";\nvar PHX_STREAM_REF = \"data-phx-stream\";\nvar PHX_PORTAL = \"data-phx-portal\";\nvar PHX_TELEPORTED_REF = \"data-phx-teleported\";\nvar PHX_TELEPORTED_SRC = \"data-phx-teleported-src\";\nvar PHX_RUNTIME_HOOK = \"data-phx-runtime-hook\";\nvar PHX_LV_PID = \"data-phx-pid\";\nvar PHX_KEY = \"key\";\nvar PHX_PRIVATE = \"phxPrivate\";\nvar PHX_AUTO_RECOVER = \"auto-recover\";\nvar PHX_NO_UNUSED_FIELD = \"no-unused-field\";\nvar PHX_LV_DEBUG = \"phx:live-socket:debug\";\nvar PHX_LV_PROFILE = \"phx:live-socket:profiling\";\nvar PHX_LV_LATENCY_SIM = \"phx:live-socket:latency-sim\";\nvar PHX_LV_HISTORY_POSITION = \"phx:nav-history-position\";\nvar PHX_PROGRESS = \"progress\";\nvar PHX_MOUNTED = \"mounted\";\nvar PHX_RELOAD_STATUS = \"__phoenix_reload_status__\";\nvar LOADER_TIMEOUT = 1;\nvar MAX_CHILD_JOIN_ATTEMPTS = 3;\nvar BEFORE_UNLOAD_LOADER_TIMEOUT = 200;\nvar DISCONNECTED_TIMEOUT = 500;\nvar BINDING_PREFIX = \"phx-\";\nvar PUSH_TIMEOUT = 3e4;\nvar DEBOUNCE_TRIGGER = \"debounce-trigger\";\nvar THROTTLED = \"throttled\";\nvar DEBOUNCE_PREV_KEY = \"debounce-prev-key\";\nvar DEFAULTS = {\n  debounce: 300,\n  throttle: 300\n};\nvar PHX_PENDING_ATTRS = [PHX_REF_LOADING, PHX_REF_SRC, PHX_REF_LOCK];\nvar STATIC = \"s\";\nvar ROOT = \"r\";\nvar COMPONENTS = \"c\";\nvar KEYED = \"k\";\nvar KEYED_COUNT = \"kc\";\nvar EVENTS = \"e\";\nvar REPLY = \"r\";\nvar TITLE = \"t\";\nvar TEMPLATES = \"p\";\nvar STREAM = \"stream\";\n\n// js/phoenix_live_view/entry_uploader.js\nvar EntryUploader = class {\n  constructor(entry, config, liveSocket) {\n    const { chunk_size, chunk_timeout } = config;\n    this.liveSocket = liveSocket;\n    this.entry = entry;\n    this.offset = 0;\n    this.chunkSize = chunk_size;\n    this.chunkTimeout = chunk_timeout;\n    this.chunkTimer = null;\n    this.errored = false;\n    this.uploadChannel = liveSocket.channel(`lvu:${entry.ref}`, {\n      token: entry.metadata()\n    });\n  }\n  error(reason) {\n    if (this.errored) {\n      return;\n    }\n    this.uploadChannel.leave();\n    this.errored = true;\n    clearTimeout(this.chunkTimer);\n    this.entry.error(reason);\n  }\n  upload() {\n    this.uploadChannel.onError((reason) => this.error(reason));\n    this.uploadChannel.join().receive(\"ok\", (_data) => this.readNextChunk()).receive(\"error\", (reason) => this.error(reason));\n  }\n  isDone() {\n    return this.offset >= this.entry.file.size;\n  }\n  readNextChunk() {\n    const reader = new window.FileReader();\n    const blob = this.entry.file.slice(\n      this.offset,\n      this.chunkSize + this.offset\n    );\n    reader.onload = (e) => {\n      if (e.target.error === null) {\n        this.offset += /** @type {ArrayBuffer} */\n        e.target.result.byteLength;\n        this.pushChunk(\n          /** @type {ArrayBuffer} */\n          e.target.result\n        );\n      } else {\n        return logError(\"Read error: \" + e.target.error);\n      }\n    };\n    reader.readAsArrayBuffer(blob);\n  }\n  pushChunk(chunk) {\n    if (!this.uploadChannel.isJoined()) {\n      return;\n    }\n    this.uploadChannel.push(\"chunk\", chunk, this.chunkTimeout).receive(\"ok\", () => {\n      this.entry.progress(this.offset / this.entry.file.size * 100);\n      if (!this.isDone()) {\n        this.chunkTimer = setTimeout(\n          () => this.readNextChunk(),\n          this.liveSocket.getLatencySim() || 0\n        );\n      }\n    }).receive(\"error\", ({ reason }) => this.error(reason));\n  }\n};\n\n// js/phoenix_live_view/utils.js\nvar logError = (msg, obj) => console.error && console.error(msg, obj);\nvar isCid = (cid) => {\n  const type = typeof cid;\n  return type === \"number\" || type === \"string\" && /^(0|[1-9]\\d*)$/.test(cid);\n};\nfunction detectDuplicateIds() {\n  const ids = /* @__PURE__ */ new Set();\n  const elems = document.querySelectorAll(\"*[id]\");\n  for (let i = 0, len = elems.length; i < len; i++) {\n    if (ids.has(elems[i].id)) {\n      console.error(\n        `Multiple IDs detected: ${elems[i].id}. Ensure unique element ids.`\n      );\n    } else {\n      ids.add(elems[i].id);\n    }\n  }\n}\nfunction detectInvalidStreamInserts(inserts) {\n  const errors = /* @__PURE__ */ new Set();\n  Object.keys(inserts).forEach((id) => {\n    const streamEl = document.getElementById(id);\n    if (streamEl && streamEl.parentElement && streamEl.parentElement.getAttribute(\"phx-update\") !== \"stream\") {\n      errors.add(\n        `The stream container with id \"${streamEl.parentElement.id}\" is missing the phx-update=\"stream\" attribute. Ensure it is set for streams to work properly.`\n      );\n    }\n  });\n  errors.forEach((error) => console.error(error));\n}\nvar debug = (view, kind, msg, obj) => {\n  if (view.liveSocket.isDebugEnabled()) {\n    console.log(`${view.id} ${kind}: ${msg} - `, obj);\n  }\n};\nvar closure = (val) => typeof val === \"function\" ? val : function() {\n  return val;\n};\nvar clone = (obj) => {\n  return JSON.parse(JSON.stringify(obj));\n};\nvar closestPhxBinding = (el, binding, borderEl) => {\n  do {\n    if (el.matches(`[${binding}]`) && !el.disabled) {\n      return el;\n    }\n    el = el.parentElement || el.parentNode;\n  } while (el !== null && el.nodeType === 1 && !(borderEl && borderEl.isSameNode(el) || el.matches(PHX_VIEW_SELECTOR)));\n  return null;\n};\nvar isObject = (obj) => {\n  return obj !== null && typeof obj === \"object\" && !(obj instanceof Array);\n};\nvar isEqualObj = (obj1, obj2) => JSON.stringify(obj1) === JSON.stringify(obj2);\nvar isEmpty = (obj) => {\n  for (const x in obj) {\n    return false;\n  }\n  return true;\n};\nvar maybe = (el, callback) => el && callback(el);\nvar channelUploader = function(entries, onError, resp, liveSocket) {\n  entries.forEach((entry) => {\n    const entryUploader = new EntryUploader(entry, resp.config, liveSocket);\n    entryUploader.upload();\n  });\n};\nvar eventContainsFiles = (e) => {\n  if (e.dataTransfer.types) {\n    for (let i = 0; i < e.dataTransfer.types.length; i++) {\n      if (e.dataTransfer.types[i] === \"Files\") {\n        return true;\n      }\n    }\n  }\n  return false;\n};\n\n// js/phoenix_live_view/browser.js\nvar Browser = {\n  canPushState() {\n    return typeof history.pushState !== \"undefined\";\n  },\n  dropLocal(localStorage, namespace, subkey) {\n    return localStorage.removeItem(this.localKey(namespace, subkey));\n  },\n  updateLocal(localStorage, namespace, subkey, initial, func) {\n    const current = this.getLocal(localStorage, namespace, subkey);\n    const key = this.localKey(namespace, subkey);\n    const newVal = current === null ? initial : func(current);\n    localStorage.setItem(key, JSON.stringify(newVal));\n    return newVal;\n  },\n  getLocal(localStorage, namespace, subkey) {\n    return JSON.parse(localStorage.getItem(this.localKey(namespace, subkey)));\n  },\n  updateCurrentState(callback) {\n    if (!this.canPushState()) {\n      return;\n    }\n    history.replaceState(\n      callback(history.state || {}),\n      \"\",\n      window.location.href\n    );\n  },\n  pushState(kind, meta, to) {\n    if (this.canPushState()) {\n      if (to !== window.location.href) {\n        if (meta.type == \"redirect\" && meta.scroll) {\n          const currentState = history.state || {};\n          currentState.scroll = meta.scroll;\n          history.replaceState(currentState, \"\", window.location.href);\n        }\n        delete meta.scroll;\n        history[kind + \"State\"](meta, \"\", to || null);\n        window.requestAnimationFrame(() => {\n          const hashEl = this.getHashTargetEl(window.location.hash);\n          if (hashEl) {\n            hashEl.scrollIntoView();\n          } else if (meta.type === \"redirect\") {\n            window.scroll(0, 0);\n          }\n        });\n      }\n    } else {\n      this.redirect(to);\n    }\n  },\n  setCookie(name, value, maxAgeSeconds) {\n    const expires = typeof maxAgeSeconds === \"number\" ? ` max-age=${maxAgeSeconds};` : \"\";\n    document.cookie = `${name}=${value};${expires} path=/`;\n  },\n  getCookie(name) {\n    return document.cookie.replace(\n      new RegExp(`(?:(?:^|.*;s*)${name}s*=s*([^;]*).*$)|^.*$`),\n      \"$1\"\n    );\n  },\n  deleteCookie(name) {\n    document.cookie = `${name}=; max-age=-1; path=/`;\n  },\n  redirect(toURL, flash, navigate = (url) => {\n    window.location.href = url;\n  }) {\n    if (flash) {\n      this.setCookie(\"__phoenix_flash__\", flash, 60);\n    }\n    navigate(toURL);\n  },\n  localKey(namespace, subkey) {\n    return `${namespace}-${subkey}`;\n  },\n  getHashTargetEl(maybeHash) {\n    const hash = maybeHash.toString().substring(1);\n    if (hash === \"\") {\n      return;\n    }\n    return document.getElementById(hash) || document.querySelector(`a[name=\"${hash}\"]`);\n  }\n};\nvar browser_default = Browser;\n\n// js/phoenix_live_view/dom.js\nvar DOM = {\n  byId(id) {\n    return document.getElementById(id) || logError(`no id found for ${id}`);\n  },\n  removeClass(el, className) {\n    el.classList.remove(className);\n    if (el.classList.length === 0) {\n      el.removeAttribute(\"class\");\n    }\n  },\n  all(node, query, callback) {\n    if (!node) {\n      return [];\n    }\n    const array = Array.from(node.querySelectorAll(query));\n    if (callback) {\n      array.forEach(callback);\n    }\n    return array;\n  },\n  childNodeLength(html) {\n    const template = document.createElement(\"template\");\n    template.innerHTML = html;\n    return template.content.childElementCount;\n  },\n  isUploadInput(el) {\n    return el.type === \"file\" && el.getAttribute(PHX_UPLOAD_REF) !== null;\n  },\n  isAutoUpload(inputEl) {\n    return inputEl.hasAttribute(\"data-phx-auto-upload\");\n  },\n  findUploadInputs(node) {\n    const formId = node.id;\n    const inputsOutsideForm = this.all(\n      document,\n      `input[type=\"file\"][${PHX_UPLOAD_REF}][form=\"${formId}\"]`\n    );\n    return this.all(node, `input[type=\"file\"][${PHX_UPLOAD_REF}]`).concat(\n      inputsOutsideForm\n    );\n  },\n  findComponentNodeList(viewId, cid, doc2 = document) {\n    return this.all(\n      doc2,\n      `[${PHX_VIEW_REF}=\"${viewId}\"][${PHX_COMPONENT}=\"${cid}\"]`\n    );\n  },\n  isPhxDestroyed(node) {\n    return node.id && DOM.private(node, \"destroyed\") ? true : false;\n  },\n  wantsNewTab(e) {\n    const wantsNewTab = e.ctrlKey || e.shiftKey || e.metaKey || e.button && e.button === 1;\n    const isDownload = e.target instanceof HTMLAnchorElement && e.target.hasAttribute(\"download\");\n    const isTargetBlank = e.target.hasAttribute(\"target\") && e.target.getAttribute(\"target\").toLowerCase() === \"_blank\";\n    const isTargetNamedTab = e.target.hasAttribute(\"target\") && !e.target.getAttribute(\"target\").startsWith(\"_\");\n    return wantsNewTab || isTargetBlank || isDownload || isTargetNamedTab;\n  },\n  isUnloadableFormSubmit(e) {\n    const isDialogSubmit = e.target && e.target.getAttribute(\"method\") === \"dialog\" || e.submitter && e.submitter.getAttribute(\"formmethod\") === \"dialog\";\n    if (isDialogSubmit) {\n      return false;\n    } else {\n      return !e.defaultPrevented && !this.wantsNewTab(e);\n    }\n  },\n  isNewPageClick(e, currentLocation) {\n    const href = e.target instanceof HTMLAnchorElement ? e.target.getAttribute(\"href\") : null;\n    let url;\n    if (e.defaultPrevented || href === null || this.wantsNewTab(e)) {\n      return false;\n    }\n    if (href.startsWith(\"mailto:\") || href.startsWith(\"tel:\")) {\n      return false;\n    }\n    if (e.target.isContentEditable) {\n      return false;\n    }\n    try {\n      url = new URL(href);\n    } catch {\n      try {\n        url = new URL(href, currentLocation);\n      } catch {\n        return true;\n      }\n    }\n    if (url.host === currentLocation.host && url.protocol === currentLocation.protocol) {\n      if (url.pathname === currentLocation.pathname && url.search === currentLocation.search) {\n        return url.hash === \"\" && !url.href.endsWith(\"#\");\n      }\n    }\n    return url.protocol.startsWith(\"http\");\n  },\n  markPhxChildDestroyed(el) {\n    if (this.isPhxChild(el)) {\n      el.setAttribute(PHX_SESSION, \"\");\n    }\n    this.putPrivate(el, \"destroyed\", true);\n  },\n  findPhxChildrenInFragment(html, parentId) {\n    const template = document.createElement(\"template\");\n    template.innerHTML = html;\n    return this.findPhxChildren(template.content, parentId);\n  },\n  isIgnored(el, phxUpdate) {\n    return (el.getAttribute(phxUpdate) || el.getAttribute(\"data-phx-update\")) === \"ignore\";\n  },\n  isPhxUpdate(el, phxUpdate, updateTypes) {\n    return el.getAttribute && updateTypes.indexOf(el.getAttribute(phxUpdate)) >= 0;\n  },\n  findPhxSticky(el) {\n    return this.all(el, `[${PHX_STICKY}]`);\n  },\n  findPhxChildren(el, parentId) {\n    return this.all(el, `${PHX_VIEW_SELECTOR}[${PHX_PARENT_ID}=\"${parentId}\"]`);\n  },\n  findExistingParentCIDs(viewId, cids) {\n    const parentCids = /* @__PURE__ */ new Set();\n    const childrenCids = /* @__PURE__ */ new Set();\n    cids.forEach((cid) => {\n      this.all(\n        document,\n        `[${PHX_VIEW_REF}=\"${viewId}\"][${PHX_COMPONENT}=\"${cid}\"]`\n      ).forEach((parent) => {\n        parentCids.add(cid);\n        this.all(parent, `[${PHX_VIEW_REF}=\"${viewId}\"][${PHX_COMPONENT}]`).map((el) => parseInt(el.getAttribute(PHX_COMPONENT))).forEach((childCID) => childrenCids.add(childCID));\n      });\n    });\n    childrenCids.forEach((childCid) => parentCids.delete(childCid));\n    return parentCids;\n  },\n  private(el, key) {\n    return el[PHX_PRIVATE] && el[PHX_PRIVATE][key];\n  },\n  deletePrivate(el, key) {\n    el[PHX_PRIVATE] && delete el[PHX_PRIVATE][key];\n  },\n  putPrivate(el, key, value) {\n    if (!el[PHX_PRIVATE]) {\n      el[PHX_PRIVATE] = {};\n    }\n    el[PHX_PRIVATE][key] = value;\n  },\n  updatePrivate(el, key, defaultVal, updateFunc) {\n    const existing = this.private(el, key);\n    if (existing === void 0) {\n      this.putPrivate(el, key, updateFunc(defaultVal));\n    } else {\n      this.putPrivate(el, key, updateFunc(existing));\n    }\n  },\n  syncPendingAttrs(fromEl, toEl) {\n    if (!fromEl.hasAttribute(PHX_REF_SRC)) {\n      return;\n    }\n    PHX_EVENT_CLASSES.forEach((className) => {\n      fromEl.classList.contains(className) && toEl.classList.add(className);\n    });\n    PHX_PENDING_ATTRS.filter((attr) => fromEl.hasAttribute(attr)).forEach(\n      (attr) => {\n        toEl.setAttribute(attr, fromEl.getAttribute(attr));\n      }\n    );\n  },\n  copyPrivates(target, source) {\n    if (source[PHX_PRIVATE]) {\n      target[PHX_PRIVATE] = source[PHX_PRIVATE];\n    }\n  },\n  putTitle(str) {\n    const titleEl = document.querySelector(\"title\");\n    if (titleEl) {\n      const { prefix, suffix, default: defaultTitle } = titleEl.dataset;\n      const isEmpty2 = typeof str !== \"string\" || str.trim() === \"\";\n      if (isEmpty2 && typeof defaultTitle !== \"string\") {\n        return;\n      }\n      const inner = isEmpty2 ? defaultTitle : str;\n      document.title = `${prefix || \"\"}${inner || \"\"}${suffix || \"\"}`;\n    } else {\n      document.title = str;\n    }\n  },\n  debounce(el, event, phxDebounce, defaultDebounce, phxThrottle, defaultThrottle, asyncFilter, callback) {\n    let debounce = el.getAttribute(phxDebounce);\n    let throttle = el.getAttribute(phxThrottle);\n    if (debounce === \"\") {\n      debounce = defaultDebounce;\n    }\n    if (throttle === \"\") {\n      throttle = defaultThrottle;\n    }\n    const value = debounce || throttle;\n    switch (value) {\n      case null:\n        return callback();\n      case \"blur\":\n        this.incCycle(el, \"debounce-blur-cycle\", () => {\n          if (asyncFilter()) {\n            callback();\n          }\n        });\n        if (this.once(el, \"debounce-blur\")) {\n          el.addEventListener(\n            \"blur\",\n            () => this.triggerCycle(el, \"debounce-blur-cycle\")\n          );\n        }\n        return;\n      default:\n        const timeout = parseInt(value);\n        const trigger = () => throttle ? this.deletePrivate(el, THROTTLED) : callback();\n        const currentCycle = this.incCycle(el, DEBOUNCE_TRIGGER, trigger);\n        if (isNaN(timeout)) {\n          return logError(`invalid throttle/debounce value: ${value}`);\n        }\n        if (throttle) {\n          let newKeyDown = false;\n          if (event.type === \"keydown\") {\n            const prevKey = this.private(el, DEBOUNCE_PREV_KEY);\n            this.putPrivate(el, DEBOUNCE_PREV_KEY, event.key);\n            newKeyDown = prevKey !== event.key;\n          }\n          if (!newKeyDown && this.private(el, THROTTLED)) {\n            return false;\n          } else {\n            callback();\n            const t = setTimeout(() => {\n              if (asyncFilter()) {\n                this.triggerCycle(el, DEBOUNCE_TRIGGER);\n              }\n            }, timeout);\n            this.putPrivate(el, THROTTLED, t);\n          }\n        } else {\n          setTimeout(() => {\n            if (asyncFilter()) {\n              this.triggerCycle(el, DEBOUNCE_TRIGGER, currentCycle);\n            }\n          }, timeout);\n        }\n        const form = el.form;\n        if (form && this.once(form, \"bind-debounce\")) {\n          form.addEventListener(\"submit\", () => {\n            Array.from(new FormData(form).entries(), ([name]) => {\n              const namedItem = form.elements.namedItem(name);\n              const input = namedItem instanceof RadioNodeList ? namedItem[0] : namedItem;\n              if (input) {\n                this.incCycle(input, DEBOUNCE_TRIGGER);\n                this.deletePrivate(input, THROTTLED);\n              }\n            });\n          });\n        }\n        if (this.once(el, \"bind-debounce\")) {\n          el.addEventListener(\"blur\", () => {\n            clearTimeout(this.private(el, THROTTLED));\n            this.triggerCycle(el, DEBOUNCE_TRIGGER);\n          });\n        }\n    }\n  },\n  triggerCycle(el, key, currentCycle) {\n    const [cycle, trigger] = this.private(el, key);\n    if (!currentCycle) {\n      currentCycle = cycle;\n    }\n    if (currentCycle === cycle) {\n      this.incCycle(el, key);\n      trigger();\n    }\n  },\n  once(el, key) {\n    if (this.private(el, key) === true) {\n      return false;\n    }\n    this.putPrivate(el, key, true);\n    return true;\n  },\n  incCycle(el, key, trigger = function() {\n  }) {\n    let [currentCycle] = this.private(el, key) || [0, trigger];\n    currentCycle++;\n    this.putPrivate(el, key, [currentCycle, trigger]);\n    return currentCycle;\n  },\n  // maintains or adds privately used hook information\n  // fromEl and toEl can be the same element in the case of a newly added node\n  // fromEl and toEl can be any HTML node type, so we need to check if it's an element node\n  maintainPrivateHooks(fromEl, toEl, phxViewportTop, phxViewportBottom) {\n    if (fromEl.hasAttribute && fromEl.hasAttribute(\"data-phx-hook\") && !toEl.hasAttribute(\"data-phx-hook\")) {\n      toEl.setAttribute(\"data-phx-hook\", fromEl.getAttribute(\"data-phx-hook\"));\n    }\n    if (toEl.hasAttribute && (toEl.hasAttribute(phxViewportTop) || toEl.hasAttribute(phxViewportBottom))) {\n      toEl.setAttribute(\"data-phx-hook\", \"Phoenix.InfiniteScroll\");\n    }\n  },\n  putCustomElHook(el, hook) {\n    if (el.isConnected) {\n      el.setAttribute(\"data-phx-hook\", \"\");\n    } else {\n      console.error(`\n        hook attached to non-connected DOM element\n        ensure you are calling createHook within your connectedCallback. ${el.outerHTML}\n      `);\n    }\n    this.putPrivate(el, \"custom-el-hook\", hook);\n  },\n  getCustomElHook(el) {\n    return this.private(el, \"custom-el-hook\");\n  },\n  isUsedInput(el) {\n    return el.nodeType === Node.ELEMENT_NODE && (this.private(el, PHX_HAS_FOCUSED) || this.private(el, PHX_HAS_SUBMITTED));\n  },\n  resetForm(form) {\n    Array.from(form.elements).forEach((input) => {\n      this.deletePrivate(input, PHX_HAS_FOCUSED);\n      this.deletePrivate(input, PHX_HAS_SUBMITTED);\n    });\n  },\n  isPhxChild(node) {\n    return node.getAttribute && node.getAttribute(PHX_PARENT_ID);\n  },\n  isPhxSticky(node) {\n    return node.getAttribute && node.getAttribute(PHX_STICKY) !== null;\n  },\n  isChildOfAny(el, parents) {\n    return !!parents.find((parent) => parent.contains(el));\n  },\n  firstPhxChild(el) {\n    return this.isPhxChild(el) ? el : this.all(el, `[${PHX_PARENT_ID}]`)[0];\n  },\n  isPortalTemplate(el) {\n    return el.tagName === \"TEMPLATE\" && el.hasAttribute(PHX_PORTAL);\n  },\n  closestViewEl(el) {\n    const portalOrViewEl = el.closest(\n      `[${PHX_TELEPORTED_REF}],${PHX_VIEW_SELECTOR}`\n    );\n    if (!portalOrViewEl) {\n      return null;\n    }\n    if (portalOrViewEl.hasAttribute(PHX_TELEPORTED_REF)) {\n      return this.byId(portalOrViewEl.getAttribute(PHX_TELEPORTED_REF));\n    } else if (portalOrViewEl.hasAttribute(PHX_SESSION)) {\n      return portalOrViewEl;\n    }\n    return null;\n  },\n  dispatchEvent(target, name, opts = {}) {\n    let defaultBubble = true;\n    const isUploadTarget = target.nodeName === \"INPUT\" && target.type === \"file\";\n    if (isUploadTarget && name === \"click\") {\n      defaultBubble = false;\n    }\n    const bubbles = opts.bubbles === void 0 ? defaultBubble : !!opts.bubbles;\n    const eventOpts = {\n      bubbles,\n      cancelable: true,\n      detail: opts.detail || {}\n    };\n    const event = name === \"click\" ? new MouseEvent(\"click\", eventOpts) : new CustomEvent(name, eventOpts);\n    target.dispatchEvent(event);\n  },\n  cloneNode(node, html) {\n    if (typeof html === \"undefined\") {\n      return node.cloneNode(true);\n    } else {\n      const cloned = node.cloneNode(false);\n      cloned.innerHTML = html;\n      return cloned;\n    }\n  },\n  // merge attributes from source to target\n  // if an element is ignored, we only merge data attributes\n  // including removing data attributes that are no longer in the source\n  mergeAttrs(target, source, opts = {}) {\n    const exclude = new Set(opts.exclude || []);\n    const isIgnored = opts.isIgnored;\n    const sourceAttrs = source.attributes;\n    for (let i = sourceAttrs.length - 1; i >= 0; i--) {\n      const name = sourceAttrs[i].name;\n      if (!exclude.has(name)) {\n        const sourceValue = source.getAttribute(name);\n        if (target.getAttribute(name) !== sourceValue && (!isIgnored || isIgnored && name.startsWith(\"data-\"))) {\n          target.setAttribute(name, sourceValue);\n        }\n      } else {\n        if (name === \"value\") {\n          const sourceValue = source.value ?? source.getAttribute(name);\n          if (target.value === sourceValue) {\n            target.setAttribute(\"value\", source.getAttribute(name));\n          }\n        }\n      }\n    }\n    const targetAttrs = target.attributes;\n    for (let i = targetAttrs.length - 1; i >= 0; i--) {\n      const name = targetAttrs[i].name;\n      if (isIgnored) {\n        if (name.startsWith(\"data-\") && !source.hasAttribute(name) && !PHX_PENDING_ATTRS.includes(name)) {\n          target.removeAttribute(name);\n        }\n      } else {\n        if (!source.hasAttribute(name)) {\n          target.removeAttribute(name);\n        }\n      }\n    }\n  },\n  mergeFocusedInput(target, source) {\n    if (!(target instanceof HTMLSelectElement)) {\n      DOM.mergeAttrs(target, source, { exclude: [\"value\"] });\n    }\n    if (source.readOnly) {\n      target.setAttribute(\"readonly\", true);\n    } else {\n      target.removeAttribute(\"readonly\");\n    }\n  },\n  hasSelectionRange(el) {\n    return el.setSelectionRange && (el.type === \"text\" || el.type === \"textarea\");\n  },\n  restoreFocus(focused, selectionStart, selectionEnd) {\n    if (focused instanceof HTMLSelectElement) {\n      focused.focus();\n    }\n    if (!DOM.isTextualInput(focused)) {\n      return;\n    }\n    const wasFocused = focused.matches(\":focus\");\n    if (!wasFocused) {\n      focused.focus();\n    }\n    if (this.hasSelectionRange(focused)) {\n      focused.setSelectionRange(selectionStart, selectionEnd);\n    }\n  },\n  isFormInput(el) {\n    if (el.localName && customElements.get(el.localName)) {\n      return customElements.get(el.localName)[`formAssociated`];\n    }\n    return /^(?:input|select|textarea)$/i.test(el.tagName) && el.type !== \"button\";\n  },\n  syncAttrsToProps(el) {\n    if (el instanceof HTMLInputElement && CHECKABLE_INPUTS.indexOf(el.type.toLocaleLowerCase()) >= 0) {\n      el.checked = el.getAttribute(\"checked\") !== null;\n    }\n  },\n  isTextualInput(el) {\n    return FOCUSABLE_INPUTS.indexOf(el.type) >= 0;\n  },\n  isNowTriggerFormExternal(el, phxTriggerExternal) {\n    return el.getAttribute && el.getAttribute(phxTriggerExternal) !== null && document.body.contains(el);\n  },\n  cleanChildNodes(container, phxUpdate) {\n    if (DOM.isPhxUpdate(container, phxUpdate, [\"append\", \"prepend\", PHX_STREAM])) {\n      const toRemove = [];\n      container.childNodes.forEach((childNode) => {\n        if (!childNode.id) {\n          const isEmptyTextNode = childNode.nodeType === Node.TEXT_NODE && childNode.nodeValue.trim() === \"\";\n          if (!isEmptyTextNode && childNode.nodeType !== Node.COMMENT_NODE) {\n            logError(\n              `only HTML element tags with an id are allowed inside containers with phx-update.\n\nremoving illegal node: \"${(childNode.outerHTML || childNode.nodeValue).trim()}\"\n\n`\n            );\n          }\n          toRemove.push(childNode);\n        }\n      });\n      toRemove.forEach((childNode) => childNode.remove());\n    }\n  },\n  replaceRootContainer(container, tagName, attrs) {\n    const retainedAttrs = /* @__PURE__ */ new Set([\n      \"id\",\n      PHX_SESSION,\n      PHX_STATIC,\n      PHX_MAIN,\n      PHX_ROOT_ID\n    ]);\n    if (container.tagName.toLowerCase() === tagName.toLowerCase()) {\n      Array.from(container.attributes).filter((attr) => !retainedAttrs.has(attr.name.toLowerCase())).forEach((attr) => container.removeAttribute(attr.name));\n      Object.keys(attrs).filter((name) => !retainedAttrs.has(name.toLowerCase())).forEach((attr) => container.setAttribute(attr, attrs[attr]));\n      return container;\n    } else {\n      const newContainer = document.createElement(tagName);\n      Object.keys(attrs).forEach(\n        (attr) => newContainer.setAttribute(attr, attrs[attr])\n      );\n      retainedAttrs.forEach(\n        (attr) => newContainer.setAttribute(attr, container.getAttribute(attr))\n      );\n      newContainer.innerHTML = container.innerHTML;\n      container.replaceWith(newContainer);\n      return newContainer;\n    }\n  },\n  getSticky(el, name, defaultVal) {\n    const op = (DOM.private(el, \"sticky\") || []).find(\n      ([existingName]) => name === existingName\n    );\n    if (op) {\n      const [_name, _op, stashedResult] = op;\n      return stashedResult;\n    } else {\n      return typeof defaultVal === \"function\" ? defaultVal() : defaultVal;\n    }\n  },\n  deleteSticky(el, name) {\n    this.updatePrivate(el, \"sticky\", [], (ops) => {\n      return ops.filter(([existingName, _]) => existingName !== name);\n    });\n  },\n  putSticky(el, name, op) {\n    const stashedResult = op(el);\n    this.updatePrivate(el, \"sticky\", [], (ops) => {\n      const existingIndex = ops.findIndex(\n        ([existingName]) => name === existingName\n      );\n      if (existingIndex >= 0) {\n        ops[existingIndex] = [name, op, stashedResult];\n      } else {\n        ops.push([name, op, stashedResult]);\n      }\n      return ops;\n    });\n  },\n  applyStickyOperations(el) {\n    const ops = DOM.private(el, \"sticky\");\n    if (!ops) {\n      return;\n    }\n    ops.forEach(([name, op, _stashed]) => this.putSticky(el, name, op));\n  },\n  isLocked(el) {\n    return el.hasAttribute && el.hasAttribute(PHX_REF_LOCK);\n  },\n  attributeIgnored(attribute, ignoredAttributes) {\n    return ignoredAttributes.some(\n      (toIgnore) => attribute.name == toIgnore || toIgnore === \"*\" || toIgnore.includes(\"*\") && attribute.name.match(toIgnore) != null\n    );\n  }\n};\nvar dom_default = DOM;\n\n// js/phoenix_live_view/upload_entry.js\nvar UploadEntry = class {\n  static isActive(fileEl, file) {\n    const isNew = file._phxRef === void 0;\n    const activeRefs = fileEl.getAttribute(PHX_ACTIVE_ENTRY_REFS).split(\",\");\n    const isActive = activeRefs.indexOf(LiveUploader.genFileRef(file)) >= 0;\n    return file.size > 0 && (isNew || isActive);\n  }\n  static isPreflighted(fileEl, file) {\n    const preflightedRefs = fileEl.getAttribute(PHX_PREFLIGHTED_REFS).split(\",\");\n    const isPreflighted = preflightedRefs.indexOf(LiveUploader.genFileRef(file)) >= 0;\n    return isPreflighted && this.isActive(fileEl, file);\n  }\n  static isPreflightInProgress(file) {\n    return file._preflightInProgress === true;\n  }\n  static markPreflightInProgress(file) {\n    file._preflightInProgress = true;\n  }\n  constructor(fileEl, file, view, autoUpload) {\n    this.ref = LiveUploader.genFileRef(file);\n    this.fileEl = fileEl;\n    this.file = file;\n    this.view = view;\n    this.meta = null;\n    this._isCancelled = false;\n    this._isDone = false;\n    this._progress = 0;\n    this._lastProgressSent = -1;\n    this._onDone = function() {\n    };\n    this._onElUpdated = this.onElUpdated.bind(this);\n    this.fileEl.addEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated);\n    this.autoUpload = autoUpload;\n  }\n  metadata() {\n    return this.meta;\n  }\n  progress(progress) {\n    this._progress = Math.floor(progress);\n    if (this._progress > this._lastProgressSent) {\n      if (this._progress >= 100) {\n        this._progress = 100;\n        this._lastProgressSent = 100;\n        this._isDone = true;\n        this.view.pushFileProgress(this.fileEl, this.ref, 100, () => {\n          LiveUploader.untrackFile(this.fileEl, this.file);\n          this._onDone();\n        });\n      } else {\n        this._lastProgressSent = this._progress;\n        this.view.pushFileProgress(this.fileEl, this.ref, this._progress);\n      }\n    }\n  }\n  isCancelled() {\n    return this._isCancelled;\n  }\n  cancel() {\n    this.file._preflightInProgress = false;\n    this._isCancelled = true;\n    this._isDone = true;\n    this._onDone();\n  }\n  isDone() {\n    return this._isDone;\n  }\n  error(reason = \"failed\") {\n    this.fileEl.removeEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated);\n    this.view.pushFileProgress(this.fileEl, this.ref, { error: reason });\n    if (!this.isAutoUpload()) {\n      LiveUploader.clearFiles(this.fileEl);\n    }\n  }\n  isAutoUpload() {\n    return this.autoUpload;\n  }\n  //private\n  onDone(callback) {\n    this._onDone = () => {\n      this.fileEl.removeEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated);\n      callback();\n    };\n  }\n  onElUpdated() {\n    const activeRefs = this.fileEl.getAttribute(PHX_ACTIVE_ENTRY_REFS).split(\",\");\n    if (activeRefs.indexOf(this.ref) === -1) {\n      LiveUploader.untrackFile(this.fileEl, this.file);\n      this.cancel();\n    }\n  }\n  toPreflightPayload() {\n    return {\n      last_modified: this.file.lastModified,\n      name: this.file.name,\n      relative_path: this.file.webkitRelativePath,\n      size: this.file.size,\n      type: this.file.type,\n      ref: this.ref,\n      meta: typeof this.file.meta === \"function\" ? this.file.meta() : void 0\n    };\n  }\n  uploader(uploaders) {\n    if (this.meta.uploader) {\n      const callback = uploaders[this.meta.uploader] || logError(`no uploader configured for ${this.meta.uploader}`);\n      return { name: this.meta.uploader, callback };\n    } else {\n      return { name: \"channel\", callback: channelUploader };\n    }\n  }\n  zipPostFlight(resp) {\n    this.meta = resp.entries[this.ref];\n    if (!this.meta) {\n      logError(`no preflight upload response returned with ref ${this.ref}`, {\n        input: this.fileEl,\n        response: resp\n      });\n    }\n  }\n};\n\n// js/phoenix_live_view/live_uploader.js\nvar liveUploaderFileRef = 0;\nvar LiveUploader = class _LiveUploader {\n  static genFileRef(file) {\n    const ref = file._phxRef;\n    if (ref !== void 0) {\n      return ref;\n    } else {\n      file._phxRef = (liveUploaderFileRef++).toString();\n      return file._phxRef;\n    }\n  }\n  static getEntryDataURL(inputEl, ref, callback) {\n    const file = this.activeFiles(inputEl).find(\n      (file2) => this.genFileRef(file2) === ref\n    );\n    callback(URL.createObjectURL(file));\n  }\n  static hasUploadsInProgress(formEl) {\n    let active = 0;\n    dom_default.findUploadInputs(formEl).forEach((input) => {\n      if (input.getAttribute(PHX_PREFLIGHTED_REFS) !== input.getAttribute(PHX_DONE_REFS)) {\n        active++;\n      }\n    });\n    return active > 0;\n  }\n  static serializeUploads(inputEl) {\n    const files = this.activeFiles(inputEl);\n    const fileData = {};\n    files.forEach((file) => {\n      const entry = { path: inputEl.name };\n      const uploadRef = inputEl.getAttribute(PHX_UPLOAD_REF);\n      fileData[uploadRef] = fileData[uploadRef] || [];\n      entry.ref = this.genFileRef(file);\n      entry.last_modified = file.lastModified;\n      entry.name = file.name || entry.ref;\n      entry.relative_path = file.webkitRelativePath;\n      entry.type = file.type;\n      entry.size = file.size;\n      if (typeof file.meta === \"function\") {\n        entry.meta = file.meta();\n      }\n      fileData[uploadRef].push(entry);\n    });\n    return fileData;\n  }\n  static clearFiles(inputEl) {\n    inputEl.value = null;\n    inputEl.removeAttribute(PHX_UPLOAD_REF);\n    dom_default.putPrivate(inputEl, \"files\", []);\n  }\n  static untrackFile(inputEl, file) {\n    dom_default.putPrivate(\n      inputEl,\n      \"files\",\n      dom_default.private(inputEl, \"files\").filter((f) => !Object.is(f, file))\n    );\n  }\n  /**\n   * @param {HTMLInputElement} inputEl\n   * @param {Array<File|Blob>} files\n   * @param {DataTransfer} [dataTransfer]\n   */\n  static trackFiles(inputEl, files, dataTransfer) {\n    if (inputEl.getAttribute(\"multiple\") !== null) {\n      const newFiles = files.filter(\n        (file) => !this.activeFiles(inputEl).find((f) => Object.is(f, file))\n      );\n      dom_default.updatePrivate(\n        inputEl,\n        \"files\",\n        [],\n        (existing) => existing.concat(newFiles)\n      );\n      inputEl.value = null;\n    } else {\n      if (dataTransfer && dataTransfer.files.length > 0) {\n        inputEl.files = dataTransfer.files;\n      }\n      dom_default.putPrivate(inputEl, \"files\", files);\n    }\n  }\n  static activeFileInputs(formEl) {\n    const fileInputs = dom_default.findUploadInputs(formEl);\n    return Array.from(fileInputs).filter(\n      (el) => el.files && this.activeFiles(el).length > 0\n    );\n  }\n  static activeFiles(input) {\n    return (dom_default.private(input, \"files\") || []).filter(\n      (f) => UploadEntry.isActive(input, f)\n    );\n  }\n  static inputsAwaitingPreflight(formEl) {\n    const fileInputs = dom_default.findUploadInputs(formEl);\n    return Array.from(fileInputs).filter(\n      (input) => this.filesAwaitingPreflight(input).length > 0\n    );\n  }\n  static filesAwaitingPreflight(input) {\n    return this.activeFiles(input).filter(\n      (f) => !UploadEntry.isPreflighted(input, f) && !UploadEntry.isPreflightInProgress(f)\n    );\n  }\n  static markPreflightInProgress(entries) {\n    entries.forEach((entry) => UploadEntry.markPreflightInProgress(entry.file));\n  }\n  constructor(inputEl, view, onComplete) {\n    this.autoUpload = dom_default.isAutoUpload(inputEl);\n    this.view = view;\n    this.onComplete = onComplete;\n    this._entries = Array.from(\n      _LiveUploader.filesAwaitingPreflight(inputEl) || []\n    ).map((file) => new UploadEntry(inputEl, file, view, this.autoUpload));\n    _LiveUploader.markPreflightInProgress(this._entries);\n    this.numEntriesInProgress = this._entries.length;\n  }\n  isAutoUpload() {\n    return this.autoUpload;\n  }\n  entries() {\n    return this._entries;\n  }\n  initAdapterUpload(resp, onError, liveSocket) {\n    this._entries = this._entries.map((entry) => {\n      if (entry.isCancelled()) {\n        this.numEntriesInProgress--;\n        if (this.numEntriesInProgress === 0) {\n          this.onComplete();\n        }\n      } else {\n        entry.zipPostFlight(resp);\n        entry.onDone(() => {\n          this.numEntriesInProgress--;\n          if (this.numEntriesInProgress === 0) {\n            this.onComplete();\n          }\n        });\n      }\n      return entry;\n    });\n    const groupedEntries = this._entries.reduce((acc, entry) => {\n      if (!entry.meta) {\n        return acc;\n      }\n      const { name, callback } = entry.uploader(liveSocket.uploaders);\n      acc[name] = acc[name] || { callback, entries: [] };\n      acc[name].entries.push(entry);\n      return acc;\n    }, {});\n    for (const name in groupedEntries) {\n      const { callback, entries } = groupedEntries[name];\n      callback(entries, onError, resp, liveSocket);\n    }\n  }\n};\n\n// js/phoenix_live_view/aria.js\nvar ARIA = {\n  anyOf(instance, classes) {\n    return classes.find((name) => instance instanceof name);\n  },\n  isFocusable(el, interactiveOnly) {\n    return el instanceof HTMLAnchorElement && el.rel !== \"ignore\" || el instanceof HTMLAreaElement && el.href !== void 0 || !el.disabled && this.anyOf(el, [\n      HTMLInputElement,\n      HTMLSelectElement,\n      HTMLTextAreaElement,\n      HTMLButtonElement\n    ]) || el instanceof HTMLIFrameElement || el.tabIndex >= 0 && el.getAttribute(\"aria-hidden\") !== \"true\" || !interactiveOnly && el.getAttribute(\"tabindex\") !== null && el.getAttribute(\"aria-hidden\") !== \"true\";\n  },\n  attemptFocus(el, interactiveOnly) {\n    if (this.isFocusable(el, interactiveOnly)) {\n      try {\n        el.focus();\n      } catch {\n      }\n    }\n    return !!document.activeElement && document.activeElement.isSameNode(el);\n  },\n  focusFirstInteractive(el) {\n    let child = el.firstElementChild;\n    while (child) {\n      if (this.attemptFocus(child, true) || this.focusFirstInteractive(child)) {\n        return true;\n      }\n      child = child.nextElementSibling;\n    }\n  },\n  focusFirst(el) {\n    let child = el.firstElementChild;\n    while (child) {\n      if (this.attemptFocus(child) || this.focusFirst(child)) {\n        return true;\n      }\n      child = child.nextElementSibling;\n    }\n  },\n  focusLast(el) {\n    let child = el.lastElementChild;\n    while (child) {\n      if (this.attemptFocus(child) || this.focusLast(child)) {\n        return true;\n      }\n      child = child.previousElementSibling;\n    }\n  }\n};\nvar aria_default = ARIA;\n\n// js/phoenix_live_view/hooks.js\nvar Hooks = {\n  LiveFileUpload: {\n    activeRefs() {\n      return this.el.getAttribute(PHX_ACTIVE_ENTRY_REFS);\n    },\n    preflightedRefs() {\n      return this.el.getAttribute(PHX_PREFLIGHTED_REFS);\n    },\n    mounted() {\n      this.js().ignoreAttributes(this.el, [\"value\"]);\n      this.preflightedWas = this.preflightedRefs();\n    },\n    updated() {\n      const newPreflights = this.preflightedRefs();\n      if (this.preflightedWas !== newPreflights) {\n        this.preflightedWas = newPreflights;\n        if (newPreflights === \"\") {\n          this.__view().cancelSubmit(this.el.form);\n        }\n      }\n      if (this.activeRefs() === \"\") {\n        this.el.value = null;\n      }\n      this.el.dispatchEvent(new CustomEvent(PHX_LIVE_FILE_UPDATED));\n    }\n  },\n  LiveImgPreview: {\n    mounted() {\n      this.ref = this.el.getAttribute(\"data-phx-entry-ref\");\n      this.inputEl = document.getElementById(\n        this.el.getAttribute(PHX_UPLOAD_REF)\n      );\n      LiveUploader.getEntryDataURL(this.inputEl, this.ref, (url) => {\n        this.url = url;\n        this.el.src = url;\n      });\n    },\n    destroyed() {\n      URL.revokeObjectURL(this.url);\n    }\n  },\n  FocusWrap: {\n    mounted() {\n      this.focusStart = this.el.firstElementChild;\n      this.focusEnd = this.el.lastElementChild;\n      this.focusStart.addEventListener(\"focus\", (e) => {\n        if (!e.relatedTarget || !this.el.contains(e.relatedTarget)) {\n          const nextFocus = e.target.nextElementSibling;\n          aria_default.attemptFocus(nextFocus) || aria_default.focusFirst(nextFocus);\n        } else {\n          aria_default.focusLast(this.el);\n        }\n      });\n      this.focusEnd.addEventListener(\"focus\", (e) => {\n        if (!e.relatedTarget || !this.el.contains(e.relatedTarget)) {\n          const nextFocus = e.target.previousElementSibling;\n          aria_default.attemptFocus(nextFocus) || aria_default.focusLast(nextFocus);\n        } else {\n          aria_default.focusFirst(this.el);\n        }\n      });\n      if (!this.el.contains(document.activeElement)) {\n        this.el.addEventListener(\"phx:show-end\", () => this.el.focus());\n        if (window.getComputedStyle(this.el).display !== \"none\") {\n          aria_default.focusFirst(this.el);\n        }\n      }\n    }\n  }\n};\nvar findScrollContainer = (el) => {\n  if ([\"HTML\", \"BODY\"].indexOf(el.nodeName.toUpperCase()) >= 0)\n    return null;\n  if ([\"scroll\", \"auto\"].indexOf(getComputedStyle(el).overflowY) >= 0)\n    return el;\n  return findScrollContainer(el.parentElement);\n};\nvar scrollTop = (scrollContainer) => {\n  if (scrollContainer) {\n    return scrollContainer.scrollTop;\n  } else {\n    return document.documentElement.scrollTop || document.body.scrollTop;\n  }\n};\nvar bottom = (scrollContainer) => {\n  if (scrollContainer) {\n    return scrollContainer.getBoundingClientRect().bottom;\n  } else {\n    return window.innerHeight || document.documentElement.clientHeight;\n  }\n};\nvar top = (scrollContainer) => {\n  if (scrollContainer) {\n    return scrollContainer.getBoundingClientRect().top;\n  } else {\n    return 0;\n  }\n};\nvar isAtViewportTop = (el, scrollContainer) => {\n  const rect = el.getBoundingClientRect();\n  return Math.ceil(rect.top) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.top) <= bottom(scrollContainer);\n};\nvar isAtViewportBottom = (el, scrollContainer) => {\n  const rect = el.getBoundingClientRect();\n  return Math.ceil(rect.bottom) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.bottom) <= bottom(scrollContainer);\n};\nvar isWithinViewport = (el, scrollContainer) => {\n  const rect = el.getBoundingClientRect();\n  return Math.ceil(rect.top) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.top) <= bottom(scrollContainer);\n};\nHooks.InfiniteScroll = {\n  mounted() {\n    this.scrollContainer = findScrollContainer(this.el);\n    let scrollBefore = scrollTop(this.scrollContainer);\n    let topOverran = false;\n    const throttleInterval = 500;\n    let pendingOp = null;\n    const onTopOverrun = this.throttle(\n      throttleInterval,\n      (topEvent, firstChild) => {\n        pendingOp = () => true;\n        this.liveSocket.js().push(this.el, topEvent, {\n          value: { id: firstChild.id, _overran: true },\n          callback: () => {\n            pendingOp = null;\n          }\n        });\n      }\n    );\n    const onFirstChildAtTop = this.throttle(\n      throttleInterval,\n      (topEvent, firstChild) => {\n        pendingOp = () => firstChild.scrollIntoView({ block: \"start\" });\n        this.liveSocket.js().push(this.el, topEvent, {\n          value: { id: firstChild.id },\n          callback: () => {\n            pendingOp = null;\n            window.requestAnimationFrame(() => {\n              if (!isWithinViewport(firstChild, this.scrollContainer)) {\n                firstChild.scrollIntoView({ block: \"start\" });\n              }\n            });\n          }\n        });\n      }\n    );\n    const onLastChildAtBottom = this.throttle(\n      throttleInterval,\n      (bottomEvent, lastChild) => {\n        pendingOp = () => lastChild.scrollIntoView({ block: \"end\" });\n        this.liveSocket.js().push(this.el, bottomEvent, {\n          value: { id: lastChild.id },\n          callback: () => {\n            pendingOp = null;\n            window.requestAnimationFrame(() => {\n              if (!isWithinViewport(lastChild, this.scrollContainer)) {\n                lastChild.scrollIntoView({ block: \"end\" });\n              }\n            });\n          }\n        });\n      }\n    );\n    this.onScroll = (_e) => {\n      const scrollNow = scrollTop(this.scrollContainer);\n      if (pendingOp) {\n        scrollBefore = scrollNow;\n        return pendingOp();\n      }\n      const rect = this.findOverrunTarget();\n      const topEvent = this.el.getAttribute(\n        this.liveSocket.binding(\"viewport-top\")\n      );\n      const bottomEvent = this.el.getAttribute(\n        this.liveSocket.binding(\"viewport-bottom\")\n      );\n      const lastChild = this.el.lastElementChild;\n      const firstChild = this.el.firstElementChild;\n      const isScrollingUp = scrollNow < scrollBefore;\n      const isScrollingDown = scrollNow > scrollBefore;\n      if (isScrollingUp && topEvent && !topOverran && rect.top >= 0) {\n        topOverran = true;\n        onTopOverrun(topEvent, firstChild);\n      } else if (isScrollingDown && topOverran && rect.top <= 0) {\n        topOverran = false;\n      }\n      if (topEvent && isScrollingUp && isAtViewportTop(firstChild, this.scrollContainer)) {\n        onFirstChildAtTop(topEvent, firstChild);\n      } else if (bottomEvent && isScrollingDown && isAtViewportBottom(lastChild, this.scrollContainer)) {\n        onLastChildAtBottom(bottomEvent, lastChild);\n      }\n      scrollBefore = scrollNow;\n    };\n    if (this.scrollContainer) {\n      this.scrollContainer.addEventListener(\"scroll\", this.onScroll);\n    } else {\n      window.addEventListener(\"scroll\", this.onScroll);\n    }\n  },\n  destroyed() {\n    if (this.scrollContainer) {\n      this.scrollContainer.removeEventListener(\"scroll\", this.onScroll);\n    } else {\n      window.removeEventListener(\"scroll\", this.onScroll);\n    }\n  },\n  throttle(interval, callback) {\n    let lastCallAt = 0;\n    let timer;\n    return (...args) => {\n      const now = Date.now();\n      const remainingTime = interval - (now - lastCallAt);\n      if (remainingTime <= 0 || remainingTime > interval) {\n        if (timer) {\n          clearTimeout(timer);\n          timer = null;\n        }\n        lastCallAt = now;\n        callback(...args);\n      } else if (!timer) {\n        timer = setTimeout(() => {\n          lastCallAt = Date.now();\n          timer = null;\n          callback(...args);\n        }, remainingTime);\n      }\n    };\n  },\n  findOverrunTarget() {\n    let rect;\n    const overrunTarget = this.el.getAttribute(\n      this.liveSocket.binding(PHX_VIEWPORT_OVERRUN_TARGET)\n    );\n    if (overrunTarget) {\n      const overrunEl = document.getElementById(overrunTarget);\n      if (overrunEl) {\n        rect = overrunEl.getBoundingClientRect();\n      } else {\n        throw new Error(\"did not find element with id \" + overrunTarget);\n      }\n    } else {\n      rect = this.el.getBoundingClientRect();\n    }\n    return rect;\n  }\n};\nvar hooks_default = Hooks;\n\n// js/phoenix_live_view/element_ref.js\nvar ElementRef = class {\n  static onUnlock(el, callback) {\n    if (!dom_default.isLocked(el) && !el.closest(`[${PHX_REF_LOCK}]`)) {\n      return callback();\n    }\n    const closestLock = el.closest(`[${PHX_REF_LOCK}]`);\n    const ref = closestLock.closest(`[${PHX_REF_LOCK}]`).getAttribute(PHX_REF_LOCK);\n    closestLock.addEventListener(\n      `phx:undo-lock:${ref}`,\n      () => {\n        callback();\n      },\n      { once: true }\n    );\n  }\n  constructor(el) {\n    this.el = el;\n    this.loadingRef = el.hasAttribute(PHX_REF_LOADING) ? parseInt(el.getAttribute(PHX_REF_LOADING), 10) : null;\n    this.lockRef = el.hasAttribute(PHX_REF_LOCK) ? parseInt(el.getAttribute(PHX_REF_LOCK), 10) : null;\n  }\n  // public\n  maybeUndo(ref, phxEvent, eachCloneCallback) {\n    if (!this.isWithin(ref)) {\n      dom_default.updatePrivate(this.el, PHX_PENDING_REFS, [], (pendingRefs) => {\n        pendingRefs.push(ref);\n        return pendingRefs;\n      });\n      return;\n    }\n    this.undoLocks(ref, phxEvent, eachCloneCallback);\n    this.undoLoading(ref, phxEvent);\n    dom_default.updatePrivate(this.el, PHX_PENDING_REFS, [], (pendingRefs) => {\n      return pendingRefs.filter((pendingRef) => {\n        let opts = {\n          detail: { ref: pendingRef, event: phxEvent },\n          bubbles: true,\n          cancelable: false\n        };\n        if (this.loadingRef && this.loadingRef > pendingRef) {\n          this.el.dispatchEvent(\n            new CustomEvent(`phx:undo-loading:${pendingRef}`, opts)\n          );\n        }\n        if (this.lockRef && this.lockRef > pendingRef) {\n          this.el.dispatchEvent(\n            new CustomEvent(`phx:undo-lock:${pendingRef}`, opts)\n          );\n        }\n        return pendingRef > ref;\n      });\n    });\n    if (this.isFullyResolvedBy(ref)) {\n      this.el.removeAttribute(PHX_REF_SRC);\n    }\n  }\n  // private\n  isWithin(ref) {\n    return !(this.loadingRef !== null && this.loadingRef > ref && this.lockRef !== null && this.lockRef > ref);\n  }\n  // Check for cloned PHX_REF_LOCK element that has been morphed behind\n  // the scenes while this element was locked in the DOM.\n  // When we apply the cloned tree to the active DOM element, we must\n  //\n  //   1. execute pending mounted hooks for nodes now in the DOM\n  //   2. undo any ref inside the cloned tree that has since been ack'd\n  undoLocks(ref, phxEvent, eachCloneCallback) {\n    if (!this.isLockUndoneBy(ref)) {\n      return;\n    }\n    const clonedTree = dom_default.private(this.el, PHX_REF_LOCK);\n    if (clonedTree) {\n      eachCloneCallback(clonedTree);\n      dom_default.deletePrivate(this.el, PHX_REF_LOCK);\n    }\n    this.el.removeAttribute(PHX_REF_LOCK);\n    const opts = {\n      detail: { ref, event: phxEvent },\n      bubbles: true,\n      cancelable: false\n    };\n    this.el.dispatchEvent(\n      new CustomEvent(`phx:undo-lock:${this.lockRef}`, opts)\n    );\n  }\n  undoLoading(ref, phxEvent) {\n    if (!this.isLoadingUndoneBy(ref)) {\n      if (this.canUndoLoading(ref) && this.el.classList.contains(\"phx-submit-loading\")) {\n        this.el.classList.remove(\"phx-change-loading\");\n      }\n      return;\n    }\n    if (this.canUndoLoading(ref)) {\n      this.el.removeAttribute(PHX_REF_LOADING);\n      const disabledVal = this.el.getAttribute(PHX_DISABLED);\n      const readOnlyVal = this.el.getAttribute(PHX_READONLY);\n      if (readOnlyVal !== null) {\n        this.el.readOnly = readOnlyVal === \"true\" ? true : false;\n        this.el.removeAttribute(PHX_READONLY);\n      }\n      if (disabledVal !== null) {\n        this.el.disabled = disabledVal === \"true\" ? true : false;\n        this.el.removeAttribute(PHX_DISABLED);\n      }\n      const disableRestore = this.el.getAttribute(PHX_DISABLE_WITH_RESTORE);\n      if (disableRestore !== null) {\n        this.el.textContent = disableRestore;\n        this.el.removeAttribute(PHX_DISABLE_WITH_RESTORE);\n      }\n      const opts = {\n        detail: { ref, event: phxEvent },\n        bubbles: true,\n        cancelable: false\n      };\n      this.el.dispatchEvent(\n        new CustomEvent(`phx:undo-loading:${this.loadingRef}`, opts)\n      );\n    }\n    PHX_EVENT_CLASSES.forEach((name) => {\n      if (name !== \"phx-submit-loading\" || this.canUndoLoading(ref)) {\n        dom_default.removeClass(this.el, name);\n      }\n    });\n  }\n  isLoadingUndoneBy(ref) {\n    return this.loadingRef === null ? false : this.loadingRef <= ref;\n  }\n  isLockUndoneBy(ref) {\n    return this.lockRef === null ? false : this.lockRef <= ref;\n  }\n  isFullyResolvedBy(ref) {\n    return (this.loadingRef === null || this.loadingRef <= ref) && (this.lockRef === null || this.lockRef <= ref);\n  }\n  // only remove the phx-submit-loading class if we are not locked\n  canUndoLoading(ref) {\n    return this.lockRef === null || this.lockRef <= ref;\n  }\n};\n\n// js/phoenix_live_view/dom_post_morph_restorer.js\nvar DOMPostMorphRestorer = class {\n  constructor(containerBefore, containerAfter, updateType) {\n    const idsBefore = /* @__PURE__ */ new Set();\n    const idsAfter = new Set(\n      [...containerAfter.children].map((child) => child.id)\n    );\n    const elementsToModify = [];\n    Array.from(containerBefore.children).forEach((child) => {\n      if (child.id) {\n        idsBefore.add(child.id);\n        if (idsAfter.has(child.id)) {\n          const previousElementId = child.previousElementSibling && child.previousElementSibling.id;\n          elementsToModify.push({\n            elementId: child.id,\n            previousElementId\n          });\n        }\n      }\n    });\n    this.containerId = containerAfter.id;\n    this.updateType = updateType;\n    this.elementsToModify = elementsToModify;\n    this.elementIdsToAdd = [...idsAfter].filter((id) => !idsBefore.has(id));\n  }\n  // We do the following to optimize append/prepend operations:\n  //   1) Track ids of modified elements & of new elements\n  //   2) All the modified elements are put back in the correct position in the DOM tree\n  //      by storing the id of their previous sibling\n  //   3) New elements are going to be put in the right place by morphdom during append.\n  //      For prepend, we move them to the first position in the container\n  perform() {\n    const container = dom_default.byId(this.containerId);\n    if (!container) {\n      return;\n    }\n    this.elementsToModify.forEach((elementToModify) => {\n      if (elementToModify.previousElementId) {\n        maybe(\n          document.getElementById(elementToModify.previousElementId),\n          (previousElem) => {\n            maybe(\n              document.getElementById(elementToModify.elementId),\n              (elem) => {\n                const isInRightPlace = elem.previousElementSibling && elem.previousElementSibling.id == previousElem.id;\n                if (!isInRightPlace) {\n                  previousElem.insertAdjacentElement(\"afterend\", elem);\n                }\n              }\n            );\n          }\n        );\n      } else {\n        maybe(document.getElementById(elementToModify.elementId), (elem) => {\n          const isInRightPlace = elem.previousElementSibling == null;\n          if (!isInRightPlace) {\n            container.insertAdjacentElement(\"afterbegin\", elem);\n          }\n        });\n      }\n    });\n    if (this.updateType == \"prepend\") {\n      this.elementIdsToAdd.reverse().forEach((elemId) => {\n        maybe(\n          document.getElementById(elemId),\n          (elem) => container.insertAdjacentElement(\"afterbegin\", elem)\n        );\n      });\n    }\n  }\n};\n\n// ../node_modules/morphdom/dist/morphdom-esm.js\nvar DOCUMENT_FRAGMENT_NODE = 11;\nfunction morphAttrs(fromNode, toNode) {\n  var toNodeAttrs = toNode.attributes;\n  var attr;\n  var attrName;\n  var attrNamespaceURI;\n  var attrValue;\n  var fromValue;\n  if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) {\n    return;\n  }\n  for (var i = toNodeAttrs.length - 1; i >= 0; i--) {\n    attr = toNodeAttrs[i];\n    attrName = attr.name;\n    attrNamespaceURI = attr.namespaceURI;\n    attrValue = attr.value;\n    if (attrNamespaceURI) {\n      attrName = attr.localName || attrName;\n      fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName);\n      if (fromValue !== attrValue) {\n        if (attr.prefix === \"xmlns\") {\n          attrName = attr.name;\n        }\n        fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue);\n      }\n    } else {\n      fromValue = fromNode.getAttribute(attrName);\n      if (fromValue !== attrValue) {\n        fromNode.setAttribute(attrName, attrValue);\n      }\n    }\n  }\n  var fromNodeAttrs = fromNode.attributes;\n  for (var d = fromNodeAttrs.length - 1; d >= 0; d--) {\n    attr = fromNodeAttrs[d];\n    attrName = attr.name;\n    attrNamespaceURI = attr.namespaceURI;\n    if (attrNamespaceURI) {\n      attrName = attr.localName || attrName;\n      if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) {\n        fromNode.removeAttributeNS(attrNamespaceURI, attrName);\n      }\n    } else {\n      if (!toNode.hasAttribute(attrName)) {\n        fromNode.removeAttribute(attrName);\n      }\n    }\n  }\n}\nvar range;\nvar NS_XHTML = \"http://www.w3.org/1999/xhtml\";\nvar doc = typeof document === \"undefined\" ? void 0 : document;\nvar HAS_TEMPLATE_SUPPORT = !!doc && \"content\" in doc.createElement(\"template\");\nvar HAS_RANGE_SUPPORT = !!doc && doc.createRange && \"createContextualFragment\" in doc.createRange();\nfunction createFragmentFromTemplate(str) {\n  var template = doc.createElement(\"template\");\n  template.innerHTML = str;\n  return template.content.childNodes[0];\n}\nfunction createFragmentFromRange(str) {\n  if (!range) {\n    range = doc.createRange();\n    range.selectNode(doc.body);\n  }\n  var fragment = range.createContextualFragment(str);\n  return fragment.childNodes[0];\n}\nfunction createFragmentFromWrap(str) {\n  var fragment = doc.createElement(\"body\");\n  fragment.innerHTML = str;\n  return fragment.childNodes[0];\n}\nfunction toElement(str) {\n  str = str.trim();\n  if (HAS_TEMPLATE_SUPPORT) {\n    return createFragmentFromTemplate(str);\n  } else if (HAS_RANGE_SUPPORT) {\n    return createFragmentFromRange(str);\n  }\n  return createFragmentFromWrap(str);\n}\nfunction compareNodeNames(fromEl, toEl) {\n  var fromNodeName = fromEl.nodeName;\n  var toNodeName = toEl.nodeName;\n  var fromCodeStart, toCodeStart;\n  if (fromNodeName === toNodeName) {\n    return true;\n  }\n  fromCodeStart = fromNodeName.charCodeAt(0);\n  toCodeStart = toNodeName.charCodeAt(0);\n  if (fromCodeStart <= 90 && toCodeStart >= 97) {\n    return fromNodeName === toNodeName.toUpperCase();\n  } else if (toCodeStart <= 90 && fromCodeStart >= 97) {\n    return toNodeName === fromNodeName.toUpperCase();\n  } else {\n    return false;\n  }\n}\nfunction createElementNS(name, namespaceURI) {\n  return !namespaceURI || namespaceURI === NS_XHTML ? doc.createElement(name) : doc.createElementNS(namespaceURI, name);\n}\nfunction moveChildren(fromEl, toEl) {\n  var curChild = fromEl.firstChild;\n  while (curChild) {\n    var nextChild = curChild.nextSibling;\n    toEl.appendChild(curChild);\n    curChild = nextChild;\n  }\n  return toEl;\n}\nfunction syncBooleanAttrProp(fromEl, toEl, name) {\n  if (fromEl[name] !== toEl[name]) {\n    fromEl[name] = toEl[name];\n    if (fromEl[name]) {\n      fromEl.setAttribute(name, \"\");\n    } else {\n      fromEl.removeAttribute(name);\n    }\n  }\n}\nvar specialElHandlers = {\n  OPTION: function(fromEl, toEl) {\n    var parentNode = fromEl.parentNode;\n    if (parentNode) {\n      var parentName = parentNode.nodeName.toUpperCase();\n      if (parentName === \"OPTGROUP\") {\n        parentNode = parentNode.parentNode;\n        parentName = parentNode && parentNode.nodeName.toUpperCase();\n      }\n      if (parentName === \"SELECT\" && !parentNode.hasAttribute(\"multiple\")) {\n        if (fromEl.hasAttribute(\"selected\") && !toEl.selected) {\n          fromEl.setAttribute(\"selected\", \"selected\");\n          fromEl.removeAttribute(\"selected\");\n        }\n        parentNode.selectedIndex = -1;\n      }\n    }\n    syncBooleanAttrProp(fromEl, toEl, \"selected\");\n  },\n  /**\n   * The \"value\" attribute is special for the <input> element since it sets\n   * the initial value. Changing the \"value\" attribute without changing the\n   * \"value\" property will have no effect since it is only used to the set the\n   * initial value.  Similar for the \"checked\" attribute, and \"disabled\".\n   */\n  INPUT: function(fromEl, toEl) {\n    syncBooleanAttrProp(fromEl, toEl, \"checked\");\n    syncBooleanAttrProp(fromEl, toEl, \"disabled\");\n    if (fromEl.value !== toEl.value) {\n      fromEl.value = toEl.value;\n    }\n    if (!toEl.hasAttribute(\"value\")) {\n      fromEl.removeAttribute(\"value\");\n    }\n  },\n  TEXTAREA: function(fromEl, toEl) {\n    var newValue = toEl.value;\n    if (fromEl.value !== newValue) {\n      fromEl.value = newValue;\n    }\n    var firstChild = fromEl.firstChild;\n    if (firstChild) {\n      var oldValue = firstChild.nodeValue;\n      if (oldValue == newValue || !newValue && oldValue == fromEl.placeholder) {\n        return;\n      }\n      firstChild.nodeValue = newValue;\n    }\n  },\n  SELECT: function(fromEl, toEl) {\n    if (!toEl.hasAttribute(\"multiple\")) {\n      var selectedIndex = -1;\n      var i = 0;\n      var curChild = fromEl.firstChild;\n      var optgroup;\n      var nodeName;\n      while (curChild) {\n        nodeName = curChild.nodeName && curChild.nodeName.toUpperCase();\n        if (nodeName === \"OPTGROUP\") {\n          optgroup = curChild;\n          curChild = optgroup.firstChild;\n          if (!curChild) {\n            curChild = optgroup.nextSibling;\n            optgroup = null;\n          }\n        } else {\n          if (nodeName === \"OPTION\") {\n            if (curChild.hasAttribute(\"selected\")) {\n              selectedIndex = i;\n              break;\n            }\n            i++;\n          }\n          curChild = curChild.nextSibling;\n          if (!curChild && optgroup) {\n            curChild = optgroup.nextSibling;\n            optgroup = null;\n          }\n        }\n      }\n      fromEl.selectedIndex = selectedIndex;\n    }\n  }\n};\nvar ELEMENT_NODE = 1;\nvar DOCUMENT_FRAGMENT_NODE$1 = 11;\nvar TEXT_NODE = 3;\nvar COMMENT_NODE = 8;\nfunction noop() {\n}\nfunction defaultGetNodeKey(node) {\n  if (node) {\n    return node.getAttribute && node.getAttribute(\"id\") || node.id;\n  }\n}\nfunction morphdomFactory(morphAttrs2) {\n  return function morphdom2(fromNode, toNode, options) {\n    if (!options) {\n      options = {};\n    }\n    if (typeof toNode === \"string\") {\n      if (fromNode.nodeName === \"#document\" || fromNode.nodeName === \"HTML\") {\n        var toNodeHtml = toNode;\n        toNode = doc.createElement(\"html\");\n        toNode.innerHTML = toNodeHtml;\n      } else if (fromNode.nodeName === \"BODY\") {\n        var toNodeBody = toNode;\n        toNode = doc.createElement(\"html\");\n        toNode.innerHTML = toNodeBody;\n        var bodyElement = toNode.querySelector(\"body\");\n        if (bodyElement) {\n          toNode = bodyElement;\n        }\n      } else {\n        toNode = toElement(toNode);\n      }\n    } else if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE$1) {\n      toNode = toNode.firstElementChild;\n    }\n    var getNodeKey = options.getNodeKey || defaultGetNodeKey;\n    var onBeforeNodeAdded = options.onBeforeNodeAdded || noop;\n    var onNodeAdded = options.onNodeAdded || noop;\n    var onBeforeElUpdated = options.onBeforeElUpdated || noop;\n    var onElUpdated = options.onElUpdated || noop;\n    var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop;\n    var onNodeDiscarded = options.onNodeDiscarded || noop;\n    var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop;\n    var skipFromChildren = options.skipFromChildren || noop;\n    var addChild = options.addChild || function(parent, child) {\n      return parent.appendChild(child);\n    };\n    var childrenOnly = options.childrenOnly === true;\n    var fromNodesLookup = /* @__PURE__ */ Object.create(null);\n    var keyedRemovalList = [];\n    function addKeyedRemoval(key) {\n      keyedRemovalList.push(key);\n    }\n    function walkDiscardedChildNodes(node, skipKeyedNodes) {\n      if (node.nodeType === ELEMENT_NODE) {\n        var curChild = node.firstChild;\n        while (curChild) {\n          var key = void 0;\n          if (skipKeyedNodes && (key = getNodeKey(curChild))) {\n            addKeyedRemoval(key);\n          } else {\n            onNodeDiscarded(curChild);\n            if (curChild.firstChild) {\n              walkDiscardedChildNodes(curChild, skipKeyedNodes);\n            }\n          }\n          curChild = curChild.nextSibling;\n        }\n      }\n    }\n    function removeNode(node, parentNode, skipKeyedNodes) {\n      if (onBeforeNodeDiscarded(node) === false) {\n        return;\n      }\n      if (parentNode) {\n        parentNode.removeChild(node);\n      }\n      onNodeDiscarded(node);\n      walkDiscardedChildNodes(node, skipKeyedNodes);\n    }\n    function indexTree(node) {\n      if (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE$1) {\n        var curChild = node.firstChild;\n        while (curChild) {\n          var key = getNodeKey(curChild);\n          if (key) {\n            fromNodesLookup[key] = curChild;\n          }\n          indexTree(curChild);\n          curChild = curChild.nextSibling;\n        }\n      }\n    }\n    indexTree(fromNode);\n    function handleNodeAdded(el) {\n      onNodeAdded(el);\n      var curChild = el.firstChild;\n      while (curChild) {\n        var nextSibling = curChild.nextSibling;\n        var key = getNodeKey(curChild);\n        if (key) {\n          var unmatchedFromEl = fromNodesLookup[key];\n          if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) {\n            curChild.parentNode.replaceChild(unmatchedFromEl, curChild);\n            morphEl(unmatchedFromEl, curChild);\n          } else {\n            handleNodeAdded(curChild);\n          }\n        } else {\n          handleNodeAdded(curChild);\n        }\n        curChild = nextSibling;\n      }\n    }\n    function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) {\n      while (curFromNodeChild) {\n        var fromNextSibling = curFromNodeChild.nextSibling;\n        if (curFromNodeKey = getNodeKey(curFromNodeChild)) {\n          addKeyedRemoval(curFromNodeKey);\n        } else {\n          removeNode(\n            curFromNodeChild,\n            fromEl,\n            true\n            /* skip keyed nodes */\n          );\n        }\n        curFromNodeChild = fromNextSibling;\n      }\n    }\n    function morphEl(fromEl, toEl, childrenOnly2) {\n      var toElKey = getNodeKey(toEl);\n      if (toElKey) {\n        delete fromNodesLookup[toElKey];\n      }\n      if (!childrenOnly2) {\n        var beforeUpdateResult = onBeforeElUpdated(fromEl, toEl);\n        if (beforeUpdateResult === false) {\n          return;\n        } else if (beforeUpdateResult instanceof HTMLElement) {\n          fromEl = beforeUpdateResult;\n          indexTree(fromEl);\n        }\n        morphAttrs2(fromEl, toEl);\n        onElUpdated(fromEl);\n        if (onBeforeElChildrenUpdated(fromEl, toEl) === false) {\n          return;\n        }\n      }\n      if (fromEl.nodeName !== \"TEXTAREA\") {\n        morphChildren(fromEl, toEl);\n      } else {\n        specialElHandlers.TEXTAREA(fromEl, toEl);\n      }\n    }\n    function morphChildren(fromEl, toEl) {\n      var skipFrom = skipFromChildren(fromEl, toEl);\n      var curToNodeChild = toEl.firstChild;\n      var curFromNodeChild = fromEl.firstChild;\n      var curToNodeKey;\n      var curFromNodeKey;\n      var fromNextSibling;\n      var toNextSibling;\n      var matchingFromEl;\n      outer:\n        while (curToNodeChild) {\n          toNextSibling = curToNodeChild.nextSibling;\n          curToNodeKey = getNodeKey(curToNodeChild);\n          while (!skipFrom && curFromNodeChild) {\n            fromNextSibling = curFromNodeChild.nextSibling;\n            if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) {\n              curToNodeChild = toNextSibling;\n              curFromNodeChild = fromNextSibling;\n              continue outer;\n            }\n            curFromNodeKey = getNodeKey(curFromNodeChild);\n            var curFromNodeType = curFromNodeChild.nodeType;\n            var isCompatible = void 0;\n            if (curFromNodeType === curToNodeChild.nodeType) {\n              if (curFromNodeType === ELEMENT_NODE) {\n                if (curToNodeKey) {\n                  if (curToNodeKey !== curFromNodeKey) {\n                    if (matchingFromEl = fromNodesLookup[curToNodeKey]) {\n                      if (fromNextSibling === matchingFromEl) {\n                        isCompatible = false;\n                      } else {\n                        fromEl.insertBefore(matchingFromEl, curFromNodeChild);\n                        if (curFromNodeKey) {\n                          addKeyedRemoval(curFromNodeKey);\n                        } else {\n                          removeNode(\n                            curFromNodeChild,\n                            fromEl,\n                            true\n                            /* skip keyed nodes */\n                          );\n                        }\n                        curFromNodeChild = matchingFromEl;\n                        curFromNodeKey = getNodeKey(curFromNodeChild);\n                      }\n                    } else {\n                      isCompatible = false;\n                    }\n                  }\n                } else if (curFromNodeKey) {\n                  isCompatible = false;\n                }\n                isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild);\n                if (isCompatible) {\n                  morphEl(curFromNodeChild, curToNodeChild);\n                }\n              } else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) {\n                isCompatible = true;\n                if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) {\n                  curFromNodeChild.nodeValue = curToNodeChild.nodeValue;\n                }\n              }\n            }\n            if (isCompatible) {\n              curToNodeChild = toNextSibling;\n              curFromNodeChild = fromNextSibling;\n              continue outer;\n            }\n            if (curFromNodeKey) {\n              addKeyedRemoval(curFromNodeKey);\n            } else {\n              removeNode(\n                curFromNodeChild,\n                fromEl,\n                true\n                /* skip keyed nodes */\n              );\n            }\n            curFromNodeChild = fromNextSibling;\n          }\n          if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) {\n            if (!skipFrom) {\n              addChild(fromEl, matchingFromEl);\n            }\n            morphEl(matchingFromEl, curToNodeChild);\n          } else {\n            var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild);\n            if (onBeforeNodeAddedResult !== false) {\n              if (onBeforeNodeAddedResult) {\n                curToNodeChild = onBeforeNodeAddedResult;\n              }\n              if (curToNodeChild.actualize) {\n                curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc);\n              }\n              addChild(fromEl, curToNodeChild);\n              handleNodeAdded(curToNodeChild);\n            }\n          }\n          curToNodeChild = toNextSibling;\n          curFromNodeChild = fromNextSibling;\n        }\n      cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey);\n      var specialElHandler = specialElHandlers[fromEl.nodeName];\n      if (specialElHandler) {\n        specialElHandler(fromEl, toEl);\n      }\n    }\n    var morphedNode = fromNode;\n    var morphedNodeType = morphedNode.nodeType;\n    var toNodeType = toNode.nodeType;\n    if (!childrenOnly) {\n      if (morphedNodeType === ELEMENT_NODE) {\n        if (toNodeType === ELEMENT_NODE) {\n          if (!compareNodeNames(fromNode, toNode)) {\n            onNodeDiscarded(fromNode);\n            morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI));\n          }\n        } else {\n          morphedNode = toNode;\n        }\n      } else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) {\n        if (toNodeType === morphedNodeType) {\n          if (morphedNode.nodeValue !== toNode.nodeValue) {\n            morphedNode.nodeValue = toNode.nodeValue;\n          }\n          return morphedNode;\n        } else {\n          morphedNode = toNode;\n        }\n      }\n    }\n    if (morphedNode === toNode) {\n      onNodeDiscarded(fromNode);\n    } else {\n      if (toNode.isSameNode && toNode.isSameNode(morphedNode)) {\n        return;\n      }\n      morphEl(morphedNode, toNode, childrenOnly);\n      if (keyedRemovalList) {\n        for (var i = 0, len = keyedRemovalList.length; i < len; i++) {\n          var elToRemove = fromNodesLookup[keyedRemovalList[i]];\n          if (elToRemove) {\n            removeNode(elToRemove, elToRemove.parentNode, false);\n          }\n        }\n      }\n    }\n    if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) {\n      if (morphedNode.actualize) {\n        morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc);\n      }\n      fromNode.parentNode.replaceChild(morphedNode, fromNode);\n    }\n    return morphedNode;\n  };\n}\nvar morphdom = morphdomFactory(morphAttrs);\nvar morphdom_esm_default = morphdom;\n\n// js/phoenix_live_view/dom_patch.js\nvar DOMPatch = class {\n  constructor(view, container, id, html, streams, targetCID, opts = {}) {\n    this.view = view;\n    this.liveSocket = view.liveSocket;\n    this.container = container;\n    this.id = id;\n    this.rootID = view.root.id;\n    this.html = html;\n    this.streams = streams;\n    this.streamInserts = {};\n    this.streamComponentRestore = {};\n    this.targetCID = targetCID;\n    this.cidPatch = isCid(this.targetCID);\n    this.pendingRemoves = [];\n    this.phxRemove = this.liveSocket.binding(\"remove\");\n    this.targetContainer = this.isCIDPatch() ? this.targetCIDContainer(html) : container;\n    this.callbacks = {\n      beforeadded: [],\n      beforeupdated: [],\n      beforephxChildAdded: [],\n      afteradded: [],\n      afterupdated: [],\n      afterdiscarded: [],\n      afterphxChildAdded: [],\n      aftertransitionsDiscarded: []\n    };\n    this.withChildren = opts.withChildren || opts.undoRef || false;\n    this.undoRef = opts.undoRef;\n  }\n  before(kind, callback) {\n    this.callbacks[`before${kind}`].push(callback);\n  }\n  after(kind, callback) {\n    this.callbacks[`after${kind}`].push(callback);\n  }\n  trackBefore(kind, ...args) {\n    this.callbacks[`before${kind}`].forEach((callback) => callback(...args));\n  }\n  trackAfter(kind, ...args) {\n    this.callbacks[`after${kind}`].forEach((callback) => callback(...args));\n  }\n  markPrunableContentForRemoval() {\n    const phxUpdate = this.liveSocket.binding(PHX_UPDATE);\n    dom_default.all(\n      this.container,\n      `[${phxUpdate}=append] > *, [${phxUpdate}=prepend] > *`,\n      (el) => {\n        el.setAttribute(PHX_PRUNE, \"\");\n      }\n    );\n  }\n  perform(isJoinPatch) {\n    const { view, liveSocket, html, container } = this;\n    let targetContainer = this.targetContainer;\n    if (this.isCIDPatch() && !this.targetContainer) {\n      return;\n    }\n    if (this.isCIDPatch()) {\n      const closestLock = targetContainer.closest(`[${PHX_REF_LOCK}]`);\n      if (closestLock && !closestLock.isSameNode(targetContainer)) {\n        const clonedTree = dom_default.private(closestLock, PHX_REF_LOCK);\n        if (clonedTree) {\n          targetContainer = clonedTree.querySelector(\n            `[data-phx-component=\"${this.targetCID}\"]`\n          );\n        }\n      }\n    }\n    const focused = liveSocket.getActiveElement();\n    const { selectionStart, selectionEnd } = focused && dom_default.hasSelectionRange(focused) ? focused : {};\n    const phxUpdate = liveSocket.binding(PHX_UPDATE);\n    const phxViewportTop = liveSocket.binding(PHX_VIEWPORT_TOP);\n    const phxViewportBottom = liveSocket.binding(PHX_VIEWPORT_BOTTOM);\n    const phxTriggerExternal = liveSocket.binding(PHX_TRIGGER_ACTION);\n    const added = [];\n    const updates = [];\n    const appendPrependUpdates = [];\n    let portalCallbacks = [];\n    let externalFormTriggered = null;\n    const morph = (targetContainer2, source, withChildren = this.withChildren) => {\n      const morphCallbacks = {\n        // normally, we are running with childrenOnly, as the patch HTML for a LV\n        // does not include the LV attrs (data-phx-session, etc.)\n        // when we are patching a live component, we do want to patch the root element as well;\n        // another case is the recursive patch of a stream item that was kept on reset (-> onBeforeNodeAdded)\n        childrenOnly: targetContainer2.getAttribute(PHX_COMPONENT) === null && !withChildren,\n        getNodeKey: (node) => {\n          if (dom_default.isPhxDestroyed(node)) {\n            return null;\n          }\n          if (isJoinPatch) {\n            return node.id;\n          }\n          return node.id || node.getAttribute && node.getAttribute(PHX_MAGIC_ID);\n        },\n        // skip indexing from children when container is stream\n        skipFromChildren: (from) => {\n          return from.getAttribute(phxUpdate) === PHX_STREAM;\n        },\n        // tell morphdom how to add a child\n        addChild: (parent, child) => {\n          const { ref, streamAt } = this.getStreamInsert(child);\n          if (ref === void 0) {\n            return parent.appendChild(child);\n          }\n          this.setStreamRef(child, ref);\n          if (streamAt === 0) {\n            parent.insertAdjacentElement(\"afterbegin\", child);\n          } else if (streamAt === -1) {\n            const lastChild = parent.lastElementChild;\n            if (lastChild && !lastChild.hasAttribute(PHX_STREAM_REF)) {\n              const nonStreamChild = Array.from(parent.children).find(\n                (c) => !c.hasAttribute(PHX_STREAM_REF)\n              );\n              parent.insertBefore(child, nonStreamChild);\n            } else {\n              parent.appendChild(child);\n            }\n          } else if (streamAt > 0) {\n            const sibling = Array.from(parent.children)[streamAt];\n            parent.insertBefore(child, sibling);\n          }\n        },\n        onBeforeNodeAdded: (el) => {\n          if (this.getStreamInsert(el)?.updateOnly && !this.streamComponentRestore[el.id]) {\n            return false;\n          }\n          dom_default.maintainPrivateHooks(el, el, phxViewportTop, phxViewportBottom);\n          this.trackBefore(\"added\", el);\n          let morphedEl = el;\n          if (this.streamComponentRestore[el.id]) {\n            morphedEl = this.streamComponentRestore[el.id];\n            delete this.streamComponentRestore[el.id];\n            morph(morphedEl, el, true);\n          }\n          return morphedEl;\n        },\n        onNodeAdded: (el) => {\n          if (el.getAttribute) {\n            this.maybeReOrderStream(el, true);\n          }\n          if (dom_default.isPortalTemplate(el)) {\n            portalCallbacks.push(() => this.teleport(el, morph));\n          }\n          if (el instanceof HTMLImageElement && el.srcset) {\n            el.srcset = el.srcset;\n          } else if (el instanceof HTMLVideoElement && el.autoplay) {\n            el.play();\n          }\n          if (dom_default.isNowTriggerFormExternal(el, phxTriggerExternal)) {\n            externalFormTriggered = el;\n          }\n          if (dom_default.isPhxChild(el) && view.ownsElement(el) || dom_default.isPhxSticky(el) && view.ownsElement(el.parentNode)) {\n            this.trackAfter(\"phxChildAdded\", el);\n          }\n          if (el.nodeName === \"SCRIPT\" && el.hasAttribute(PHX_RUNTIME_HOOK)) {\n            this.handleRuntimeHook(el, source);\n          }\n          added.push(el);\n        },\n        onNodeDiscarded: (el) => this.onNodeDiscarded(el),\n        onBeforeNodeDiscarded: (el) => {\n          if (el.getAttribute && el.getAttribute(PHX_PRUNE) !== null) {\n            return true;\n          }\n          if (el.parentElement !== null && el.id && dom_default.isPhxUpdate(el.parentElement, phxUpdate, [\n            PHX_STREAM,\n            \"append\",\n            \"prepend\"\n          ])) {\n            return false;\n          }\n          if (el.getAttribute && el.getAttribute(PHX_TELEPORTED_REF)) {\n            return false;\n          }\n          if (this.maybePendingRemove(el)) {\n            return false;\n          }\n          if (this.skipCIDSibling(el)) {\n            return false;\n          }\n          if (dom_default.isPortalTemplate(el)) {\n            const teleportedEl = document.getElementById(\n              el.content.firstElementChild.id\n            );\n            if (teleportedEl) {\n              teleportedEl.remove();\n              morphCallbacks.onNodeDiscarded(teleportedEl);\n              this.view.dropPortalElementId(teleportedEl.id);\n            }\n          }\n          return true;\n        },\n        onElUpdated: (el) => {\n          if (dom_default.isNowTriggerFormExternal(el, phxTriggerExternal)) {\n            externalFormTriggered = el;\n          }\n          updates.push(el);\n          this.maybeReOrderStream(el, false);\n        },\n        onBeforeElUpdated: (fromEl, toEl) => {\n          if (fromEl.id && fromEl.isSameNode(targetContainer2) && fromEl.id !== toEl.id) {\n            morphCallbacks.onNodeDiscarded(fromEl);\n            fromEl.replaceWith(toEl);\n            return morphCallbacks.onNodeAdded(toEl);\n          }\n          dom_default.syncPendingAttrs(fromEl, toEl);\n          dom_default.maintainPrivateHooks(\n            fromEl,\n            toEl,\n            phxViewportTop,\n            phxViewportBottom\n          );\n          dom_default.cleanChildNodes(toEl, phxUpdate);\n          if (this.skipCIDSibling(toEl)) {\n            this.maybeReOrderStream(fromEl);\n            return false;\n          }\n          if (dom_default.isPhxSticky(fromEl)) {\n            [PHX_SESSION, PHX_STATIC, PHX_ROOT_ID].map((attr) => [\n              attr,\n              fromEl.getAttribute(attr),\n              toEl.getAttribute(attr)\n            ]).forEach(([attr, fromVal, toVal]) => {\n              if (toVal && fromVal !== toVal) {\n                fromEl.setAttribute(attr, toVal);\n              }\n            });\n            return false;\n          }\n          if (dom_default.isIgnored(fromEl, phxUpdate) || fromEl.form && fromEl.form.isSameNode(externalFormTriggered)) {\n            this.trackBefore(\"updated\", fromEl, toEl);\n            dom_default.mergeAttrs(fromEl, toEl, {\n              isIgnored: dom_default.isIgnored(fromEl, phxUpdate)\n            });\n            updates.push(fromEl);\n            dom_default.applyStickyOperations(fromEl);\n            return false;\n          }\n          if (fromEl.type === \"number\" && fromEl.validity && fromEl.validity.badInput) {\n            return false;\n          }\n          const isFocusedFormEl = focused && fromEl.isSameNode(focused) && dom_default.isFormInput(fromEl);\n          const focusedSelectChanged = isFocusedFormEl && this.isChangedSelect(fromEl, toEl);\n          if (fromEl.hasAttribute(PHX_REF_SRC)) {\n            const ref = new ElementRef(fromEl);\n            if (ref.lockRef && (!this.undoRef || !ref.isLockUndoneBy(this.undoRef))) {\n              dom_default.applyStickyOperations(fromEl);\n              const isLocked = fromEl.hasAttribute(PHX_REF_LOCK);\n              const clone2 = isLocked ? dom_default.private(fromEl, PHX_REF_LOCK) || fromEl.cloneNode(true) : null;\n              if (clone2) {\n                dom_default.putPrivate(fromEl, PHX_REF_LOCK, clone2);\n                if (!isFocusedFormEl) {\n                  fromEl = clone2;\n                }\n              }\n            }\n          }\n          if (dom_default.isPhxChild(toEl)) {\n            const prevSession = fromEl.getAttribute(PHX_SESSION);\n            dom_default.mergeAttrs(fromEl, toEl, { exclude: [PHX_STATIC] });\n            if (prevSession !== \"\") {\n              fromEl.setAttribute(PHX_SESSION, prevSession);\n            }\n            fromEl.setAttribute(PHX_ROOT_ID, this.rootID);\n            dom_default.applyStickyOperations(fromEl);\n            return false;\n          }\n          if (this.undoRef && dom_default.private(toEl, PHX_REF_LOCK)) {\n            dom_default.putPrivate(\n              fromEl,\n              PHX_REF_LOCK,\n              dom_default.private(toEl, PHX_REF_LOCK)\n            );\n          }\n          dom_default.copyPrivates(toEl, fromEl);\n          if (dom_default.isPortalTemplate(toEl)) {\n            portalCallbacks.push(() => this.teleport(toEl, morph));\n            fromEl.content.replaceChildren(toEl.content.cloneNode(true));\n            return false;\n          }\n          if (isFocusedFormEl && fromEl.type !== \"hidden\" && !focusedSelectChanged) {\n            this.trackBefore(\"updated\", fromEl, toEl);\n            dom_default.mergeFocusedInput(fromEl, toEl);\n            dom_default.syncAttrsToProps(fromEl);\n            updates.push(fromEl);\n            dom_default.applyStickyOperations(fromEl);\n            return false;\n          } else {\n            if (focusedSelectChanged) {\n              fromEl.blur();\n            }\n            if (dom_default.isPhxUpdate(toEl, phxUpdate, [\"append\", \"prepend\"])) {\n              appendPrependUpdates.push(\n                new DOMPostMorphRestorer(\n                  fromEl,\n                  toEl,\n                  toEl.getAttribute(phxUpdate)\n                )\n              );\n            }\n            dom_default.syncAttrsToProps(toEl);\n            dom_default.applyStickyOperations(toEl);\n            this.trackBefore(\"updated\", fromEl, toEl);\n            return fromEl;\n          }\n        }\n      };\n      morphdom_esm_default(targetContainer2, source, morphCallbacks);\n    };\n    this.trackBefore(\"added\", container);\n    this.trackBefore(\"updated\", container, container);\n    liveSocket.time(\"morphdom\", () => {\n      this.streams.forEach(([ref, inserts, deleteIds, reset]) => {\n        inserts.forEach(([key, streamAt, limit, updateOnly]) => {\n          this.streamInserts[key] = { ref, streamAt, limit, reset, updateOnly };\n        });\n        if (reset !== void 0) {\n          dom_default.all(document, `[${PHX_STREAM_REF}=\"${ref}\"]`, (child) => {\n            this.removeStreamChildElement(child);\n          });\n        }\n        deleteIds.forEach((id) => {\n          const child = document.getElementById(id);\n          if (child) {\n            this.removeStreamChildElement(child);\n          }\n        });\n      });\n      if (isJoinPatch) {\n        dom_default.all(this.container, `[${phxUpdate}=${PHX_STREAM}]`).filter((el) => this.view.ownsElement(el)).forEach((el) => {\n          Array.from(el.children).forEach((child) => {\n            this.removeStreamChildElement(child, true);\n          });\n        });\n      }\n      morph(targetContainer, html);\n      let teleportCount = 0;\n      while (portalCallbacks.length > 0 && teleportCount < 5) {\n        const copy = portalCallbacks.slice();\n        portalCallbacks = [];\n        copy.forEach((callback) => callback());\n        teleportCount++;\n      }\n      this.view.portalElementIds.forEach((id) => {\n        const el = document.getElementById(id);\n        if (el) {\n          const source = document.getElementById(\n            el.getAttribute(PHX_TELEPORTED_SRC)\n          );\n          if (!source) {\n            el.remove();\n            this.onNodeDiscarded(el);\n            this.view.dropPortalElementId(id);\n          }\n        }\n      });\n    });\n    if (liveSocket.isDebugEnabled()) {\n      detectDuplicateIds();\n      detectInvalidStreamInserts(this.streamInserts);\n      Array.from(document.querySelectorAll(\"input[name=id]\")).forEach(\n        (node) => {\n          if (node instanceof HTMLInputElement && node.form) {\n            console.error(\n              'Detected an input with name=\"id\" inside a form! This will cause problems when patching the DOM.\\n',\n              node\n            );\n          }\n        }\n      );\n    }\n    if (appendPrependUpdates.length > 0) {\n      liveSocket.time(\"post-morph append/prepend restoration\", () => {\n        appendPrependUpdates.forEach((update) => update.perform());\n      });\n    }\n    liveSocket.silenceEvents(\n      () => dom_default.restoreFocus(focused, selectionStart, selectionEnd)\n    );\n    dom_default.dispatchEvent(document, \"phx:update\");\n    added.forEach((el) => this.trackAfter(\"added\", el));\n    updates.forEach((el) => this.trackAfter(\"updated\", el));\n    this.transitionPendingRemoves();\n    if (externalFormTriggered) {\n      liveSocket.unload();\n      const submitter = dom_default.private(externalFormTriggered, \"submitter\");\n      if (submitter && submitter.name && targetContainer.contains(submitter)) {\n        const input = document.createElement(\"input\");\n        input.type = \"hidden\";\n        const formId = submitter.getAttribute(\"form\");\n        if (formId) {\n          input.setAttribute(\"form\", formId);\n        }\n        input.name = submitter.name;\n        input.value = submitter.value;\n        submitter.parentElement.insertBefore(input, submitter);\n      }\n      Object.getPrototypeOf(externalFormTriggered).submit.call(\n        externalFormTriggered\n      );\n    }\n    return true;\n  }\n  onNodeDiscarded(el) {\n    if (dom_default.isPhxChild(el) || dom_default.isPhxSticky(el)) {\n      this.liveSocket.destroyViewByEl(el);\n    }\n    this.trackAfter(\"discarded\", el);\n  }\n  maybePendingRemove(node) {\n    if (node.getAttribute && node.getAttribute(this.phxRemove) !== null) {\n      this.pendingRemoves.push(node);\n      return true;\n    } else {\n      return false;\n    }\n  }\n  removeStreamChildElement(child, force = false) {\n    if (!force && !this.view.ownsElement(child)) {\n      return;\n    }\n    if (this.streamInserts[child.id]) {\n      this.streamComponentRestore[child.id] = child;\n      child.remove();\n    } else {\n      if (!this.maybePendingRemove(child)) {\n        child.remove();\n        this.onNodeDiscarded(child);\n      }\n    }\n  }\n  getStreamInsert(el) {\n    const insert = el.id ? this.streamInserts[el.id] : {};\n    return insert || {};\n  }\n  setStreamRef(el, ref) {\n    dom_default.putSticky(\n      el,\n      PHX_STREAM_REF,\n      (el2) => el2.setAttribute(PHX_STREAM_REF, ref)\n    );\n  }\n  maybeReOrderStream(el, isNew) {\n    const { ref, streamAt, reset } = this.getStreamInsert(el);\n    if (streamAt === void 0) {\n      return;\n    }\n    this.setStreamRef(el, ref);\n    if (!reset && !isNew) {\n      return;\n    }\n    if (!el.parentElement) {\n      return;\n    }\n    if (streamAt === 0) {\n      el.parentElement.insertBefore(el, el.parentElement.firstElementChild);\n    } else if (streamAt > 0) {\n      const children = Array.from(el.parentElement.children);\n      const oldIndex = children.indexOf(el);\n      if (streamAt >= children.length - 1) {\n        el.parentElement.appendChild(el);\n      } else {\n        const sibling = children[streamAt];\n        if (oldIndex > streamAt) {\n          el.parentElement.insertBefore(el, sibling);\n        } else {\n          el.parentElement.insertBefore(el, sibling.nextElementSibling);\n        }\n      }\n    }\n    this.maybeLimitStream(el);\n  }\n  maybeLimitStream(el) {\n    const { limit } = this.getStreamInsert(el);\n    const children = limit !== null && Array.from(el.parentElement.children);\n    if (limit && limit < 0 && children.length > limit * -1) {\n      children.slice(0, children.length + limit).forEach((child) => this.removeStreamChildElement(child));\n    } else if (limit && limit >= 0 && children.length > limit) {\n      children.slice(limit).forEach((child) => this.removeStreamChildElement(child));\n    }\n  }\n  transitionPendingRemoves() {\n    const { pendingRemoves, liveSocket } = this;\n    if (pendingRemoves.length > 0) {\n      liveSocket.transitionRemoves(pendingRemoves, () => {\n        pendingRemoves.forEach((el) => {\n          const child = dom_default.firstPhxChild(el);\n          if (child) {\n            liveSocket.destroyViewByEl(child);\n          }\n          el.remove();\n        });\n        this.trackAfter(\"transitionsDiscarded\", pendingRemoves);\n      });\n    }\n  }\n  isChangedSelect(fromEl, toEl) {\n    if (!(fromEl instanceof HTMLSelectElement) || fromEl.multiple) {\n      return false;\n    }\n    if (fromEl.options.length !== toEl.options.length) {\n      return true;\n    }\n    toEl.value = fromEl.value;\n    return !fromEl.isEqualNode(toEl);\n  }\n  isCIDPatch() {\n    return this.cidPatch;\n  }\n  skipCIDSibling(el) {\n    return el.nodeType === Node.ELEMENT_NODE && el.hasAttribute(PHX_SKIP);\n  }\n  targetCIDContainer(html) {\n    if (!this.isCIDPatch()) {\n      return;\n    }\n    const [first, ...rest] = dom_default.findComponentNodeList(\n      this.view.id,\n      this.targetCID\n    );\n    if (rest.length === 0 && dom_default.childNodeLength(html) === 1) {\n      return first;\n    } else {\n      return first && first.parentNode;\n    }\n  }\n  indexOf(parent, child) {\n    return Array.from(parent.children).indexOf(child);\n  }\n  teleport(el, morph) {\n    const targetSelector = el.getAttribute(PHX_PORTAL);\n    const portalContainer = document.querySelector(targetSelector);\n    if (!portalContainer) {\n      throw new Error(\n        \"portal target with selector \" + targetSelector + \" not found\"\n      );\n    }\n    const toTeleport = el.content.firstElementChild;\n    if (this.skipCIDSibling(toTeleport)) {\n      return;\n    }\n    if (!toTeleport?.id) {\n      throw new Error(\n        \"phx-portal template must have a single root element with ID!\"\n      );\n    }\n    const existing = document.getElementById(toTeleport.id);\n    let portalTarget;\n    if (existing) {\n      if (!portalContainer.contains(existing)) {\n        portalContainer.appendChild(existing);\n      }\n      portalTarget = existing;\n    } else {\n      portalTarget = document.createElement(toTeleport.tagName);\n      portalContainer.appendChild(portalTarget);\n    }\n    toTeleport.setAttribute(PHX_TELEPORTED_REF, this.view.id);\n    toTeleport.setAttribute(PHX_TELEPORTED_SRC, el.id);\n    morph(portalTarget, toTeleport, true);\n    toTeleport.removeAttribute(PHX_TELEPORTED_REF);\n    toTeleport.removeAttribute(PHX_TELEPORTED_SRC);\n    this.view.pushPortalElementId(toTeleport.id);\n  }\n  handleRuntimeHook(el, source) {\n    const name = el.getAttribute(PHX_RUNTIME_HOOK);\n    let nonce = el.hasAttribute(\"nonce\") ? el.getAttribute(\"nonce\") : null;\n    if (el.hasAttribute(\"nonce\")) {\n      const template = document.createElement(\"template\");\n      template.innerHTML = source;\n      nonce = template.content.querySelector(`script[${PHX_RUNTIME_HOOK}=\"${CSS.escape(name)}\"]`).getAttribute(\"nonce\");\n    }\n    const script = document.createElement(\"script\");\n    script.textContent = el.textContent;\n    dom_default.mergeAttrs(script, el, { isIgnored: false });\n    if (nonce) {\n      script.nonce = nonce;\n    }\n    el.replaceWith(script);\n    el = script;\n  }\n};\n\n// js/phoenix_live_view/rendered.js\nvar VOID_TAGS = /* @__PURE__ */ new Set([\n  \"area\",\n  \"base\",\n  \"br\",\n  \"col\",\n  \"command\",\n  \"embed\",\n  \"hr\",\n  \"img\",\n  \"input\",\n  \"keygen\",\n  \"link\",\n  \"meta\",\n  \"param\",\n  \"source\",\n  \"track\",\n  \"wbr\"\n]);\nvar quoteChars = /* @__PURE__ */ new Set([\"'\", '\"']);\nvar modifyRoot = (html, attrs, clearInnerHTML) => {\n  let i = 0;\n  let insideComment = false;\n  let beforeTag, afterTag, tag, tagNameEndsAt, id, newHTML;\n  const lookahead = html.match(/^(\\s*(?:<!--.*?-->\\s*)*)<([^\\s\\/>]+)/);\n  if (lookahead === null) {\n    throw new Error(`malformed html ${html}`);\n  }\n  i = lookahead[0].length;\n  beforeTag = lookahead[1];\n  tag = lookahead[2];\n  tagNameEndsAt = i;\n  for (i; i < html.length; i++) {\n    if (html.charAt(i) === \">\") {\n      break;\n    }\n    if (html.charAt(i) === \"=\") {\n      const isId = html.slice(i - 3, i) === \" id\";\n      i++;\n      const char = html.charAt(i);\n      if (quoteChars.has(char)) {\n        const attrStartsAt = i;\n        i++;\n        for (i; i < html.length; i++) {\n          if (html.charAt(i) === char) {\n            break;\n          }\n        }\n        if (isId) {\n          id = html.slice(attrStartsAt + 1, i);\n          break;\n        }\n      }\n    }\n  }\n  let closeAt = html.length - 1;\n  insideComment = false;\n  while (closeAt >= beforeTag.length + tag.length) {\n    const char = html.charAt(closeAt);\n    if (insideComment) {\n      if (char === \"-\" && html.slice(closeAt - 3, closeAt) === \"<!-\") {\n        insideComment = false;\n        closeAt -= 4;\n      } else {\n        closeAt -= 1;\n      }\n    } else if (char === \">\" && html.slice(closeAt - 2, closeAt) === \"--\") {\n      insideComment = true;\n      closeAt -= 3;\n    } else if (char === \">\") {\n      break;\n    } else {\n      closeAt -= 1;\n    }\n  }\n  afterTag = html.slice(closeAt + 1, html.length);\n  const attrsStr = Object.keys(attrs).map((attr) => attrs[attr] === true ? attr : `${attr}=\"${attrs[attr]}\"`).join(\" \");\n  if (clearInnerHTML) {\n    const idAttrStr = id ? ` id=\"${id}\"` : \"\";\n    if (VOID_TAGS.has(tag)) {\n      newHTML = `<${tag}${idAttrStr}${attrsStr === \"\" ? \"\" : \" \"}${attrsStr}/>`;\n    } else {\n      newHTML = `<${tag}${idAttrStr}${attrsStr === \"\" ? \"\" : \" \"}${attrsStr}></${tag}>`;\n    }\n  } else {\n    const rest = html.slice(tagNameEndsAt, closeAt + 1);\n    newHTML = `<${tag}${attrsStr === \"\" ? \"\" : \" \"}${attrsStr}${rest}`;\n  }\n  return [newHTML, beforeTag, afterTag];\n};\nvar Rendered = class {\n  static extract(diff) {\n    const { [REPLY]: reply, [EVENTS]: events, [TITLE]: title } = diff;\n    delete diff[REPLY];\n    delete diff[EVENTS];\n    delete diff[TITLE];\n    return { diff, title, reply: reply || null, events: events || [] };\n  }\n  constructor(viewId, rendered) {\n    this.viewId = viewId;\n    this.rendered = {};\n    this.magicId = 0;\n    this.mergeDiff(rendered);\n  }\n  parentViewId() {\n    return this.viewId;\n  }\n  toString(onlyCids) {\n    const { buffer: str, streams } = this.recursiveToString(\n      this.rendered,\n      this.rendered[COMPONENTS],\n      onlyCids,\n      true,\n      {}\n    );\n    return { buffer: str, streams };\n  }\n  recursiveToString(rendered, components = rendered[COMPONENTS], onlyCids, changeTracking, rootAttrs) {\n    onlyCids = onlyCids ? new Set(onlyCids) : null;\n    const output = {\n      buffer: \"\",\n      components,\n      onlyCids,\n      streams: /* @__PURE__ */ new Set()\n    };\n    this.toOutputBuffer(rendered, null, output, changeTracking, rootAttrs);\n    return { buffer: output.buffer, streams: output.streams };\n  }\n  componentCIDs(diff) {\n    return Object.keys(diff[COMPONENTS] || {}).map((i) => parseInt(i));\n  }\n  isComponentOnlyDiff(diff) {\n    if (!diff[COMPONENTS]) {\n      return false;\n    }\n    return Object.keys(diff).length === 1;\n  }\n  getComponent(diff, cid) {\n    return diff[COMPONENTS][cid];\n  }\n  resetRender(cid) {\n    if (this.rendered[COMPONENTS][cid]) {\n      this.rendered[COMPONENTS][cid].reset = true;\n    }\n  }\n  mergeDiff(diff) {\n    const newc = diff[COMPONENTS];\n    const cache = {};\n    delete diff[COMPONENTS];\n    this.rendered = this.mutableMerge(this.rendered, diff);\n    this.rendered[COMPONENTS] = this.rendered[COMPONENTS] || {};\n    if (newc) {\n      const oldc = this.rendered[COMPONENTS];\n      for (const cid in newc) {\n        newc[cid] = this.cachedFindComponent(cid, newc[cid], oldc, newc, cache);\n      }\n      for (const cid in newc) {\n        oldc[cid] = newc[cid];\n      }\n      diff[COMPONENTS] = newc;\n    }\n  }\n  cachedFindComponent(cid, cdiff, oldc, newc, cache) {\n    if (cache[cid]) {\n      return cache[cid];\n    } else {\n      let ndiff, stat, scid = cdiff[STATIC];\n      if (isCid(scid)) {\n        let tdiff;\n        if (scid > 0) {\n          tdiff = this.cachedFindComponent(scid, newc[scid], oldc, newc, cache);\n        } else {\n          tdiff = oldc[-scid];\n        }\n        stat = tdiff[STATIC];\n        ndiff = this.cloneMerge(tdiff, cdiff, true);\n        ndiff[STATIC] = stat;\n      } else {\n        ndiff = cdiff[STATIC] !== void 0 || oldc[cid] === void 0 ? cdiff : this.cloneMerge(oldc[cid], cdiff, false);\n      }\n      cache[cid] = ndiff;\n      return ndiff;\n    }\n  }\n  mutableMerge(target, source) {\n    if (source[STATIC] !== void 0) {\n      return source;\n    } else {\n      this.doMutableMerge(target, source);\n      return target;\n    }\n  }\n  doMutableMerge(target, source) {\n    if (source[KEYED]) {\n      this.mergeKeyed(target, source);\n    } else {\n      for (const key in source) {\n        const val = source[key];\n        const targetVal = target[key];\n        const isObjVal = isObject(val);\n        if (isObjVal && val[STATIC] === void 0 && isObject(targetVal)) {\n          this.doMutableMerge(targetVal, val);\n        } else {\n          target[key] = val;\n        }\n      }\n    }\n    if (target[ROOT]) {\n      target.newRender = true;\n    }\n  }\n  clone(diff) {\n    if (\"structuredClone\" in window) {\n      return structuredClone(diff);\n    } else {\n      return JSON.parse(JSON.stringify(diff));\n    }\n  }\n  // keyed comprehensions\n  mergeKeyed(target, source) {\n    const clonedTarget = this.clone(target);\n    Object.entries(source[KEYED]).forEach(([i, entry]) => {\n      if (i === KEYED_COUNT) {\n        return;\n      }\n      if (Array.isArray(entry)) {\n        const [old_idx, diff] = entry;\n        target[KEYED][i] = clonedTarget[KEYED][old_idx];\n        this.doMutableMerge(target[KEYED][i], diff);\n      } else if (typeof entry === \"number\") {\n        const old_idx = entry;\n        target[KEYED][i] = clonedTarget[KEYED][old_idx];\n      } else if (typeof entry === \"object\") {\n        if (!target[KEYED][i]) {\n          target[KEYED][i] = {};\n        }\n        this.doMutableMerge(target[KEYED][i], entry);\n      }\n    });\n    if (source[KEYED][KEYED_COUNT] < target[KEYED][KEYED_COUNT]) {\n      for (let i = source[KEYED][KEYED_COUNT]; i < target[KEYED][KEYED_COUNT]; i++) {\n        delete target[KEYED][i];\n      }\n    }\n    target[KEYED][KEYED_COUNT] = source[KEYED][KEYED_COUNT];\n    if (source[STREAM]) {\n      target[STREAM] = source[STREAM];\n    }\n    if (source[TEMPLATES]) {\n      target[TEMPLATES] = source[TEMPLATES];\n    }\n  }\n  // Merges cid trees together, copying statics from source tree.\n  //\n  // The `pruneMagicId` is passed to control pruning the magicId of the\n  // target. We must always prune the magicId when we are sharing statics\n  // from another component. If not pruning, we replicate the logic from\n  // mutableMerge, where we set newRender to true if there is a root\n  // (effectively forcing the new version to be rendered instead of skipped)\n  //\n  cloneMerge(target, source, pruneMagicId) {\n    let merged;\n    if (source[KEYED]) {\n      merged = this.clone(target);\n      this.mergeKeyed(merged, source);\n    } else {\n      merged = { ...target, ...source };\n      for (const key in merged) {\n        const val = source[key];\n        const targetVal = target[key];\n        if (isObject(val) && val[STATIC] === void 0 && isObject(targetVal)) {\n          merged[key] = this.cloneMerge(targetVal, val, pruneMagicId);\n        } else if (val === void 0 && isObject(targetVal)) {\n          merged[key] = this.cloneMerge(targetVal, {}, pruneMagicId);\n        }\n      }\n    }\n    if (pruneMagicId) {\n      delete merged.magicId;\n      delete merged.newRender;\n    } else if (target[ROOT]) {\n      merged.newRender = true;\n    }\n    return merged;\n  }\n  componentToString(cid) {\n    const { buffer: str, streams } = this.recursiveCIDToString(\n      this.rendered[COMPONENTS],\n      cid,\n      null\n    );\n    const [strippedHTML, _before, _after] = modifyRoot(str, {});\n    return { buffer: strippedHTML, streams };\n  }\n  pruneCIDs(cids) {\n    cids.forEach((cid) => delete this.rendered[COMPONENTS][cid]);\n  }\n  // private\n  get() {\n    return this.rendered;\n  }\n  isNewFingerprint(diff = {}) {\n    return !!diff[STATIC];\n  }\n  templateStatic(part, templates) {\n    if (typeof part === \"number\") {\n      return templates[part];\n    } else {\n      return part;\n    }\n  }\n  nextMagicID() {\n    this.magicId++;\n    return `m${this.magicId}-${this.parentViewId()}`;\n  }\n  // Converts rendered tree to output buffer.\n  //\n  // changeTracking controls if we can apply the PHX_SKIP optimization.\n  toOutputBuffer(rendered, templates, output, changeTracking, rootAttrs = {}) {\n    if (rendered[KEYED]) {\n      return this.comprehensionToBuffer(\n        rendered,\n        templates,\n        output,\n        changeTracking\n      );\n    }\n    if (rendered[TEMPLATES]) {\n      templates = rendered[TEMPLATES];\n      delete rendered[TEMPLATES];\n    }\n    let { [STATIC]: statics } = rendered;\n    statics = this.templateStatic(statics, templates);\n    rendered[STATIC] = statics;\n    const isRoot = rendered[ROOT];\n    const prevBuffer = output.buffer;\n    if (isRoot) {\n      output.buffer = \"\";\n    }\n    if (changeTracking && isRoot && !rendered.magicId) {\n      rendered.newRender = true;\n      rendered.magicId = this.nextMagicID();\n    }\n    output.buffer += statics[0];\n    for (let i = 1; i < statics.length; i++) {\n      this.dynamicToBuffer(rendered[i - 1], templates, output, changeTracking);\n      output.buffer += statics[i];\n    }\n    if (isRoot) {\n      let skip = false;\n      let attrs;\n      if (changeTracking || rendered.magicId) {\n        skip = changeTracking && !rendered.newRender;\n        attrs = { [PHX_MAGIC_ID]: rendered.magicId, ...rootAttrs };\n      } else {\n        attrs = rootAttrs;\n      }\n      if (skip) {\n        attrs[PHX_SKIP] = true;\n      }\n      const [newRoot, commentBefore, commentAfter] = modifyRoot(\n        output.buffer,\n        attrs,\n        skip\n      );\n      rendered.newRender = false;\n      output.buffer = prevBuffer + commentBefore + newRoot + commentAfter;\n    }\n  }\n  comprehensionToBuffer(rendered, templates, output, changeTracking) {\n    const keyedTemplates = templates || rendered[TEMPLATES];\n    const statics = this.templateStatic(rendered[STATIC], templates);\n    rendered[STATIC] = statics;\n    delete rendered[TEMPLATES];\n    for (let i = 0; i < rendered[KEYED][KEYED_COUNT]; i++) {\n      output.buffer += statics[0];\n      for (let j = 1; j < statics.length; j++) {\n        this.dynamicToBuffer(\n          rendered[KEYED][i][j - 1],\n          keyedTemplates,\n          output,\n          changeTracking\n        );\n        output.buffer += statics[j];\n      }\n    }\n    if (rendered[STREAM]) {\n      const stream = rendered[STREAM];\n      const [_ref, _inserts, deleteIds, reset] = stream || [null, {}, [], null];\n      if (stream !== void 0 && (rendered[KEYED][KEYED_COUNT] > 0 || deleteIds.length > 0 || reset)) {\n        delete rendered[STREAM];\n        rendered[KEYED] = {\n          [KEYED_COUNT]: 0\n        };\n        output.streams.add(stream);\n      }\n    }\n  }\n  dynamicToBuffer(rendered, templates, output, changeTracking) {\n    if (typeof rendered === \"number\") {\n      const { buffer: str, streams } = this.recursiveCIDToString(\n        output.components,\n        rendered,\n        output.onlyCids\n      );\n      output.buffer += str;\n      output.streams = /* @__PURE__ */ new Set([...output.streams, ...streams]);\n    } else if (isObject(rendered)) {\n      this.toOutputBuffer(rendered, templates, output, changeTracking, {});\n    } else {\n      output.buffer += rendered;\n    }\n  }\n  recursiveCIDToString(components, cid, onlyCids) {\n    const component = components[cid] || logError(`no component for CID ${cid}`, components);\n    const attrs = { [PHX_COMPONENT]: cid, [PHX_VIEW_REF]: this.viewId };\n    const skip = onlyCids && !onlyCids.has(cid);\n    component.newRender = !skip;\n    component.magicId = `c${cid}-${this.parentViewId()}`;\n    const changeTracking = !component.reset;\n    const { buffer: html, streams } = this.recursiveToString(\n      component,\n      components,\n      onlyCids,\n      changeTracking,\n      attrs\n    );\n    delete component.reset;\n    return { buffer: html, streams };\n  }\n};\n\n// js/phoenix_live_view/js.js\nvar focusStack = [];\nvar default_transition_time = 200;\nvar JS = {\n  // private\n  exec(e, eventType, phxEvent, view, sourceEl, defaults) {\n    const [defaultKind, defaultArgs] = defaults || [\n      null,\n      { callback: defaults && defaults.callback }\n    ];\n    const commands = Array.isArray(phxEvent) ? phxEvent : typeof phxEvent === \"string\" && phxEvent.startsWith(\"[\") ? JSON.parse(phxEvent) : [[defaultKind, defaultArgs]];\n    commands.forEach(([kind, args]) => {\n      if (kind === defaultKind) {\n        args = { ...defaultArgs, ...args };\n        args.callback = args.callback || defaultArgs.callback;\n      }\n      this.filterToEls(view.liveSocket, sourceEl, args).forEach((el) => {\n        this[`exec_${kind}`](e, eventType, phxEvent, view, sourceEl, el, args);\n      });\n    });\n  },\n  isVisible(el) {\n    return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length > 0);\n  },\n  // returns true if any part of the element is inside the viewport\n  isInViewport(el) {\n    const rect = el.getBoundingClientRect();\n    const windowHeight = window.innerHeight || document.documentElement.clientHeight;\n    const windowWidth = window.innerWidth || document.documentElement.clientWidth;\n    return rect.right > 0 && rect.bottom > 0 && rect.left < windowWidth && rect.top < windowHeight;\n  },\n  // private\n  // commands\n  exec_exec(e, eventType, phxEvent, view, sourceEl, el, { attr, to }) {\n    const encodedJS = el.getAttribute(attr);\n    if (!encodedJS) {\n      throw new Error(`expected ${attr} to contain JS command on \"${to}\"`);\n    }\n    view.liveSocket.execJS(el, encodedJS, eventType);\n  },\n  exec_dispatch(e, eventType, phxEvent, view, sourceEl, el, { event, detail, bubbles, blocking }) {\n    detail = detail || {};\n    detail.dispatcher = sourceEl;\n    if (blocking) {\n      const promise = new Promise((resolve, _reject) => {\n        detail.done = resolve;\n      });\n      view.liveSocket.asyncTransition(promise);\n    }\n    dom_default.dispatchEvent(el, event, { detail, bubbles });\n  },\n  exec_push(e, eventType, phxEvent, view, sourceEl, el, args) {\n    const {\n      event,\n      data,\n      target,\n      page_loading,\n      loading,\n      value,\n      dispatcher,\n      callback\n    } = args;\n    const pushOpts = {\n      loading,\n      value,\n      target,\n      page_loading: !!page_loading,\n      originalEvent: e\n    };\n    const targetSrc = eventType === \"change\" && dispatcher ? dispatcher : sourceEl;\n    const phxTarget = target || targetSrc.getAttribute(view.binding(\"target\")) || targetSrc;\n    const handler = (targetView, targetCtx) => {\n      if (!targetView.isConnected()) {\n        return;\n      }\n      if (eventType === \"change\") {\n        let { newCid, _target } = args;\n        _target = _target || (dom_default.isFormInput(sourceEl) ? sourceEl.name : void 0);\n        if (_target) {\n          pushOpts._target = _target;\n        }\n        targetView.pushInput(\n          sourceEl,\n          targetCtx,\n          newCid,\n          event || phxEvent,\n          pushOpts,\n          callback\n        );\n      } else if (eventType === \"submit\") {\n        const { submitter } = args;\n        targetView.submitForm(\n          sourceEl,\n          targetCtx,\n          event || phxEvent,\n          submitter,\n          pushOpts,\n          callback\n        );\n      } else {\n        targetView.pushEvent(\n          eventType,\n          sourceEl,\n          targetCtx,\n          event || phxEvent,\n          data,\n          pushOpts,\n          callback\n        );\n      }\n    };\n    if (args.targetView && args.targetCtx) {\n      handler(args.targetView, args.targetCtx);\n    } else {\n      view.withinTargets(phxTarget, handler);\n    }\n  },\n  exec_navigate(e, eventType, phxEvent, view, sourceEl, el, { href, replace }) {\n    view.liveSocket.historyRedirect(\n      e,\n      href,\n      replace ? \"replace\" : \"push\",\n      null,\n      sourceEl\n    );\n  },\n  exec_patch(e, eventType, phxEvent, view, sourceEl, el, { href, replace }) {\n    view.liveSocket.pushHistoryPatch(\n      e,\n      href,\n      replace ? \"replace\" : \"push\",\n      sourceEl\n    );\n  },\n  exec_focus(e, eventType, phxEvent, view, sourceEl, el) {\n    aria_default.attemptFocus(el);\n    window.requestAnimationFrame(() => {\n      window.requestAnimationFrame(() => aria_default.attemptFocus(el));\n    });\n  },\n  exec_focus_first(e, eventType, phxEvent, view, sourceEl, el) {\n    aria_default.focusFirstInteractive(el) || aria_default.focusFirst(el);\n    window.requestAnimationFrame(() => {\n      window.requestAnimationFrame(\n        () => aria_default.focusFirstInteractive(el) || aria_default.focusFirst(el)\n      );\n    });\n  },\n  exec_push_focus(e, eventType, phxEvent, view, sourceEl, el) {\n    focusStack.push(el || sourceEl);\n  },\n  exec_pop_focus(_e, _eventType, _phxEvent, _view, _sourceEl, _el) {\n    const el = focusStack.pop();\n    if (el) {\n      el.focus();\n      window.requestAnimationFrame(() => {\n        window.requestAnimationFrame(() => el.focus());\n      });\n    }\n  },\n  exec_add_class(e, eventType, phxEvent, view, sourceEl, el, { names, transition, time, blocking }) {\n    this.addOrRemoveClasses(el, names, [], transition, time, view, blocking);\n  },\n  exec_remove_class(e, eventType, phxEvent, view, sourceEl, el, { names, transition, time, blocking }) {\n    this.addOrRemoveClasses(el, [], names, transition, time, view, blocking);\n  },\n  exec_toggle_class(e, eventType, phxEvent, view, sourceEl, el, { names, transition, time, blocking }) {\n    this.toggleClasses(el, names, transition, time, view, blocking);\n  },\n  exec_toggle_attr(e, eventType, phxEvent, view, sourceEl, el, { attr: [attr, val1, val2] }) {\n    this.toggleAttr(el, attr, val1, val2);\n  },\n  exec_ignore_attrs(e, eventType, phxEvent, view, sourceEl, el, { attrs }) {\n    this.ignoreAttrs(el, attrs);\n  },\n  exec_transition(e, eventType, phxEvent, view, sourceEl, el, { time, transition, blocking }) {\n    this.addOrRemoveClasses(el, [], [], transition, time, view, blocking);\n  },\n  exec_toggle(e, eventType, phxEvent, view, sourceEl, el, { display, ins, outs, time, blocking }) {\n    this.toggle(eventType, view, el, display, ins, outs, time, blocking);\n  },\n  exec_show(e, eventType, phxEvent, view, sourceEl, el, { display, transition, time, blocking }) {\n    this.show(eventType, view, el, display, transition, time, blocking);\n  },\n  exec_hide(e, eventType, phxEvent, view, sourceEl, el, { display, transition, time, blocking }) {\n    this.hide(eventType, view, el, display, transition, time, blocking);\n  },\n  exec_set_attr(e, eventType, phxEvent, view, sourceEl, el, { attr: [attr, val] }) {\n    this.setOrRemoveAttrs(el, [[attr, val]], []);\n  },\n  exec_remove_attr(e, eventType, phxEvent, view, sourceEl, el, { attr }) {\n    this.setOrRemoveAttrs(el, [], [attr]);\n  },\n  ignoreAttrs(el, attrs) {\n    dom_default.putPrivate(el, \"JS:ignore_attrs\", {\n      apply: (fromEl, toEl) => {\n        let fromAttributes = Array.from(fromEl.attributes);\n        let fromAttributeNames = fromAttributes.map((attr) => attr.name);\n        Array.from(toEl.attributes).filter((attr) => {\n          return !fromAttributeNames.includes(attr.name);\n        }).forEach((attr) => {\n          if (dom_default.attributeIgnored(attr, attrs)) {\n            toEl.removeAttribute(attr.name);\n          }\n        });\n        fromAttributes.forEach((attr) => {\n          if (dom_default.attributeIgnored(attr, attrs)) {\n            toEl.setAttribute(attr.name, attr.value);\n          }\n        });\n      }\n    });\n  },\n  onBeforeElUpdated(fromEl, toEl) {\n    const ignoreAttrs = dom_default.private(fromEl, \"JS:ignore_attrs\");\n    if (ignoreAttrs) {\n      ignoreAttrs.apply(fromEl, toEl);\n    }\n  },\n  // utils for commands\n  show(eventType, view, el, display, transition, time, blocking) {\n    if (!this.isVisible(el)) {\n      this.toggle(\n        eventType,\n        view,\n        el,\n        display,\n        transition,\n        null,\n        time,\n        blocking\n      );\n    }\n  },\n  hide(eventType, view, el, display, transition, time, blocking) {\n    if (this.isVisible(el)) {\n      this.toggle(\n        eventType,\n        view,\n        el,\n        display,\n        null,\n        transition,\n        time,\n        blocking\n      );\n    }\n  },\n  toggle(eventType, view, el, display, ins, outs, time, blocking) {\n    time = time || default_transition_time;\n    const [inClasses, inStartClasses, inEndClasses] = ins || [[], [], []];\n    const [outClasses, outStartClasses, outEndClasses] = outs || [[], [], []];\n    if (inClasses.length > 0 || outClasses.length > 0) {\n      if (this.isVisible(el)) {\n        const onStart = () => {\n          this.addOrRemoveClasses(\n            el,\n            outStartClasses,\n            inClasses.concat(inStartClasses).concat(inEndClasses)\n          );\n          window.requestAnimationFrame(() => {\n            this.addOrRemoveClasses(el, outClasses, []);\n            window.requestAnimationFrame(\n              () => this.addOrRemoveClasses(el, outEndClasses, outStartClasses)\n            );\n          });\n        };\n        const onEnd = () => {\n          this.addOrRemoveClasses(el, [], outClasses.concat(outEndClasses));\n          dom_default.putSticky(\n            el,\n            \"toggle\",\n            (currentEl) => currentEl.style.display = \"none\"\n          );\n          el.dispatchEvent(new Event(\"phx:hide-end\"));\n        };\n        el.dispatchEvent(new Event(\"phx:hide-start\"));\n        if (blocking === false) {\n          onStart();\n          setTimeout(onEnd, time);\n        } else {\n          view.transition(time, onStart, onEnd);\n        }\n      } else {\n        if (eventType === \"remove\") {\n          return;\n        }\n        const onStart = () => {\n          this.addOrRemoveClasses(\n            el,\n            inStartClasses,\n            outClasses.concat(outStartClasses).concat(outEndClasses)\n          );\n          const stickyDisplay = display || this.defaultDisplay(el);\n          window.requestAnimationFrame(() => {\n            this.addOrRemoveClasses(el, inClasses, []);\n            window.requestAnimationFrame(() => {\n              dom_default.putSticky(\n                el,\n                \"toggle\",\n                (currentEl) => currentEl.style.display = stickyDisplay\n              );\n              this.addOrRemoveClasses(el, inEndClasses, inStartClasses);\n            });\n          });\n        };\n        const onEnd = () => {\n          this.addOrRemoveClasses(el, [], inClasses.concat(inEndClasses));\n          el.dispatchEvent(new Event(\"phx:show-end\"));\n        };\n        el.dispatchEvent(new Event(\"phx:show-start\"));\n        if (blocking === false) {\n          onStart();\n          setTimeout(onEnd, time);\n        } else {\n          view.transition(time, onStart, onEnd);\n        }\n      }\n    } else {\n      if (this.isVisible(el)) {\n        window.requestAnimationFrame(() => {\n          el.dispatchEvent(new Event(\"phx:hide-start\"));\n          dom_default.putSticky(\n            el,\n            \"toggle\",\n            (currentEl) => currentEl.style.display = \"none\"\n          );\n          el.dispatchEvent(new Event(\"phx:hide-end\"));\n        });\n      } else {\n        window.requestAnimationFrame(() => {\n          el.dispatchEvent(new Event(\"phx:show-start\"));\n          const stickyDisplay = display || this.defaultDisplay(el);\n          dom_default.putSticky(\n            el,\n            \"toggle\",\n            (currentEl) => currentEl.style.display = stickyDisplay\n          );\n          el.dispatchEvent(new Event(\"phx:show-end\"));\n        });\n      }\n    }\n  },\n  toggleClasses(el, classes, transition, time, view, blocking) {\n    window.requestAnimationFrame(() => {\n      const [prevAdds, prevRemoves] = dom_default.getSticky(el, \"classes\", [[], []]);\n      const newAdds = classes.filter(\n        (name) => prevAdds.indexOf(name) < 0 && !el.classList.contains(name)\n      );\n      const newRemoves = classes.filter(\n        (name) => prevRemoves.indexOf(name) < 0 && el.classList.contains(name)\n      );\n      this.addOrRemoveClasses(\n        el,\n        newAdds,\n        newRemoves,\n        transition,\n        time,\n        view,\n        blocking\n      );\n    });\n  },\n  toggleAttr(el, attr, val1, val2) {\n    if (el.hasAttribute(attr)) {\n      if (val2 !== void 0) {\n        if (el.getAttribute(attr) === val1) {\n          this.setOrRemoveAttrs(el, [[attr, val2]], []);\n        } else {\n          this.setOrRemoveAttrs(el, [[attr, val1]], []);\n        }\n      } else {\n        this.setOrRemoveAttrs(el, [], [attr]);\n      }\n    } else {\n      this.setOrRemoveAttrs(el, [[attr, val1]], []);\n    }\n  },\n  addOrRemoveClasses(el, adds, removes, transition, time, view, blocking) {\n    time = time || default_transition_time;\n    const [transitionRun, transitionStart, transitionEnd] = transition || [\n      [],\n      [],\n      []\n    ];\n    if (transitionRun.length > 0) {\n      const onStart = () => {\n        this.addOrRemoveClasses(\n          el,\n          transitionStart,\n          [].concat(transitionRun).concat(transitionEnd)\n        );\n        window.requestAnimationFrame(() => {\n          this.addOrRemoveClasses(el, transitionRun, []);\n          window.requestAnimationFrame(\n            () => this.addOrRemoveClasses(el, transitionEnd, transitionStart)\n          );\n        });\n      };\n      const onDone = () => this.addOrRemoveClasses(\n        el,\n        adds.concat(transitionEnd),\n        removes.concat(transitionRun).concat(transitionStart)\n      );\n      if (blocking === false) {\n        onStart();\n        setTimeout(onDone, time);\n      } else {\n        view.transition(time, onStart, onDone);\n      }\n      return;\n    }\n    window.requestAnimationFrame(() => {\n      const [prevAdds, prevRemoves] = dom_default.getSticky(el, \"classes\", [[], []]);\n      const keepAdds = adds.filter(\n        (name) => prevAdds.indexOf(name) < 0 && !el.classList.contains(name)\n      );\n      const keepRemoves = removes.filter(\n        (name) => prevRemoves.indexOf(name) < 0 && el.classList.contains(name)\n      );\n      const newAdds = prevAdds.filter((name) => removes.indexOf(name) < 0).concat(keepAdds);\n      const newRemoves = prevRemoves.filter((name) => adds.indexOf(name) < 0).concat(keepRemoves);\n      dom_default.putSticky(el, \"classes\", (currentEl) => {\n        currentEl.classList.remove(...newRemoves);\n        currentEl.classList.add(...newAdds);\n        return [newAdds, newRemoves];\n      });\n    });\n  },\n  setOrRemoveAttrs(el, sets, removes) {\n    const [prevSets, prevRemoves] = dom_default.getSticky(el, \"attrs\", [[], []]);\n    const alteredAttrs = sets.map(([attr, _val]) => attr).concat(removes);\n    const newSets = prevSets.filter(([attr, _val]) => !alteredAttrs.includes(attr)).concat(sets);\n    const newRemoves = prevRemoves.filter((attr) => !alteredAttrs.includes(attr)).concat(removes);\n    dom_default.putSticky(el, \"attrs\", (currentEl) => {\n      newRemoves.forEach((attr) => currentEl.removeAttribute(attr));\n      newSets.forEach(([attr, val]) => currentEl.setAttribute(attr, val));\n      return [newSets, newRemoves];\n    });\n  },\n  hasAllClasses(el, classes) {\n    return classes.every((name) => el.classList.contains(name));\n  },\n  isToggledOut(el, outClasses) {\n    return !this.isVisible(el) || this.hasAllClasses(el, outClasses);\n  },\n  filterToEls(liveSocket, sourceEl, { to }) {\n    const defaultQuery = () => {\n      if (typeof to === \"string\") {\n        return document.querySelectorAll(to);\n      } else if (to.closest) {\n        const toEl = sourceEl.closest(to.closest);\n        return toEl ? [toEl] : [];\n      } else if (to.inner) {\n        return sourceEl.querySelectorAll(to.inner);\n      }\n    };\n    return to ? liveSocket.jsQuerySelectorAll(sourceEl, to, defaultQuery) : [sourceEl];\n  },\n  defaultDisplay(el) {\n    return { tr: \"table-row\", td: \"table-cell\" }[el.tagName.toLowerCase()] || \"block\";\n  },\n  transitionClasses(val) {\n    if (!val) {\n      return null;\n    }\n    let [trans, tStart, tEnd] = Array.isArray(val) ? val : [val.split(\" \"), [], []];\n    trans = Array.isArray(trans) ? trans : trans.split(\" \");\n    tStart = Array.isArray(tStart) ? tStart : tStart.split(\" \");\n    tEnd = Array.isArray(tEnd) ? tEnd : tEnd.split(\" \");\n    return [trans, tStart, tEnd];\n  }\n};\nvar js_default = JS;\n\n// js/phoenix_live_view/js_commands.ts\nvar js_commands_default = (liveSocket, eventType) => {\n  return {\n    exec(el, encodedJS) {\n      liveSocket.execJS(el, encodedJS, eventType);\n    },\n    show(el, opts = {}) {\n      const owner = liveSocket.owner(el);\n      js_default.show(\n        eventType,\n        owner,\n        el,\n        opts.display,\n        js_default.transitionClasses(opts.transition),\n        opts.time,\n        opts.blocking\n      );\n    },\n    hide(el, opts = {}) {\n      const owner = liveSocket.owner(el);\n      js_default.hide(\n        eventType,\n        owner,\n        el,\n        null,\n        js_default.transitionClasses(opts.transition),\n        opts.time,\n        opts.blocking\n      );\n    },\n    toggle(el, opts = {}) {\n      const owner = liveSocket.owner(el);\n      const inTransition = js_default.transitionClasses(opts.in);\n      const outTransition = js_default.transitionClasses(opts.out);\n      js_default.toggle(\n        eventType,\n        owner,\n        el,\n        opts.display,\n        inTransition,\n        outTransition,\n        opts.time,\n        opts.blocking\n      );\n    },\n    addClass(el, names, opts = {}) {\n      const classNames = Array.isArray(names) ? names : names.split(\" \");\n      const owner = liveSocket.owner(el);\n      js_default.addOrRemoveClasses(\n        el,\n        classNames,\n        [],\n        js_default.transitionClasses(opts.transition),\n        opts.time,\n        owner,\n        opts.blocking\n      );\n    },\n    removeClass(el, names, opts = {}) {\n      const classNames = Array.isArray(names) ? names : names.split(\" \");\n      const owner = liveSocket.owner(el);\n      js_default.addOrRemoveClasses(\n        el,\n        [],\n        classNames,\n        js_default.transitionClasses(opts.transition),\n        opts.time,\n        owner,\n        opts.blocking\n      );\n    },\n    toggleClass(el, names, opts = {}) {\n      const classNames = Array.isArray(names) ? names : names.split(\" \");\n      const owner = liveSocket.owner(el);\n      js_default.toggleClasses(\n        el,\n        classNames,\n        js_default.transitionClasses(opts.transition),\n        opts.time,\n        owner,\n        opts.blocking\n      );\n    },\n    transition(el, transition, opts = {}) {\n      const owner = liveSocket.owner(el);\n      js_default.addOrRemoveClasses(\n        el,\n        [],\n        [],\n        js_default.transitionClasses(transition),\n        opts.time,\n        owner,\n        opts.blocking\n      );\n    },\n    setAttribute(el, attr, val) {\n      js_default.setOrRemoveAttrs(el, [[attr, val]], []);\n    },\n    removeAttribute(el, attr) {\n      js_default.setOrRemoveAttrs(el, [], [attr]);\n    },\n    toggleAttribute(el, attr, val1, val2) {\n      js_default.toggleAttr(el, attr, val1, val2);\n    },\n    push(el, type, opts = {}) {\n      liveSocket.withinOwners(el, (view) => {\n        const data = opts.value || {};\n        delete opts.value;\n        let e = new CustomEvent(\"phx:exec\", { detail: { sourceElement: el } });\n        js_default.exec(e, eventType, type, view, el, [\"push\", { data, ...opts }]);\n      });\n    },\n    navigate(href, opts = {}) {\n      const customEvent = new CustomEvent(\"phx:exec\");\n      liveSocket.historyRedirect(\n        customEvent,\n        href,\n        opts.replace ? \"replace\" : \"push\",\n        null,\n        null\n      );\n    },\n    patch(href, opts = {}) {\n      const customEvent = new CustomEvent(\"phx:exec\");\n      liveSocket.pushHistoryPatch(\n        customEvent,\n        href,\n        opts.replace ? \"replace\" : \"push\",\n        null\n      );\n    },\n    ignoreAttributes(el, attrs) {\n      js_default.ignoreAttrs(el, Array.isArray(attrs) ? attrs : [attrs]);\n    }\n  };\n};\n\n// js/phoenix_live_view/view_hook.ts\nvar HOOK_ID = \"hookId\";\nvar DEAD_HOOK = \"deadHook\";\nvar viewHookID = 1;\nvar ViewHook = class _ViewHook {\n  get liveSocket() {\n    return this.__liveSocket();\n  }\n  static makeID() {\n    return viewHookID++;\n  }\n  static elementID(el) {\n    return dom_default.private(el, HOOK_ID);\n  }\n  static deadHook(el) {\n    return dom_default.private(el, DEAD_HOOK) === true;\n  }\n  constructor(view, el, callbacks) {\n    this.el = el;\n    this.__attachView(view);\n    this.__listeners = /* @__PURE__ */ new Set();\n    this.__isDisconnected = false;\n    dom_default.putPrivate(this.el, HOOK_ID, _ViewHook.makeID());\n    if (view && view.isDead) {\n      dom_default.putPrivate(this.el, DEAD_HOOK, true);\n    }\n    if (callbacks) {\n      const protectedProps = /* @__PURE__ */ new Set([\n        \"el\",\n        \"liveSocket\",\n        \"__view\",\n        \"__listeners\",\n        \"__isDisconnected\",\n        \"constructor\",\n        // Standard object properties\n        // Core ViewHook API methods\n        \"js\",\n        \"pushEvent\",\n        \"pushEventTo\",\n        \"handleEvent\",\n        \"removeHandleEvent\",\n        \"upload\",\n        \"uploadTo\",\n        // Internal lifecycle callers\n        \"__mounted\",\n        \"__updated\",\n        \"__beforeUpdate\",\n        \"__destroyed\",\n        \"__reconnected\",\n        \"__disconnected\",\n        \"__cleanup__\"\n      ]);\n      for (const key in callbacks) {\n        if (Object.prototype.hasOwnProperty.call(callbacks, key)) {\n          this[key] = callbacks[key];\n          if (protectedProps.has(key)) {\n            console.warn(\n              `Hook object for element #${el.id} overwrites core property '${key}'!`\n            );\n          }\n        }\n      }\n      const lifecycleMethods = [\n        \"mounted\",\n        \"beforeUpdate\",\n        \"updated\",\n        \"destroyed\",\n        \"disconnected\",\n        \"reconnected\"\n      ];\n      lifecycleMethods.forEach((methodName) => {\n        if (callbacks[methodName] && typeof callbacks[methodName] === \"function\") {\n          this[methodName] = callbacks[methodName];\n        }\n      });\n    }\n  }\n  /** @internal */\n  __attachView(view) {\n    if (view) {\n      this.__view = () => view;\n      this.__liveSocket = () => view.liveSocket;\n    } else {\n      this.__view = () => {\n        throw new Error(\n          `hook not yet attached to a live view: ${this.el.outerHTML}`\n        );\n      };\n      this.__liveSocket = () => {\n        throw new Error(\n          `hook not yet attached to a live view: ${this.el.outerHTML}`\n        );\n      };\n    }\n  }\n  // Default lifecycle methods\n  mounted() {\n  }\n  beforeUpdate() {\n  }\n  updated() {\n  }\n  destroyed() {\n  }\n  disconnected() {\n  }\n  reconnected() {\n  }\n  // Internal lifecycle callers - called by the View\n  /** @internal */\n  __mounted() {\n    this.mounted();\n  }\n  /** @internal */\n  __updated() {\n    this.updated();\n  }\n  /** @internal */\n  __beforeUpdate() {\n    this.beforeUpdate();\n  }\n  /** @internal */\n  __destroyed() {\n    this.destroyed();\n    dom_default.deletePrivate(this.el, HOOK_ID);\n  }\n  /** @internal */\n  __reconnected() {\n    if (this.__isDisconnected) {\n      this.__isDisconnected = false;\n      this.reconnected();\n    }\n  }\n  /** @internal */\n  __disconnected() {\n    this.__isDisconnected = true;\n    this.disconnected();\n  }\n  js() {\n    return {\n      ...js_commands_default(this.__view().liveSocket, \"hook\"),\n      exec: (encodedJS) => {\n        this.__view().liveSocket.execJS(this.el, encodedJS, \"hook\");\n      }\n    };\n  }\n  pushEvent(event, payload, onReply) {\n    const promise = this.__view().pushHookEvent(\n      this.el,\n      null,\n      event,\n      payload || {}\n    );\n    if (onReply === void 0) {\n      return promise.then(({ reply }) => reply);\n    }\n    promise.then(\n      ({ reply, ref }) => onReply(reply, ref)\n    ).catch(() => {\n    });\n  }\n  pushEventTo(selectorOrTarget, event, payload, onReply) {\n    if (onReply === void 0) {\n      const targetPair = [];\n      this.__view().withinTargets(\n        selectorOrTarget,\n        (view, targetCtx) => {\n          targetPair.push({ view, targetCtx });\n        }\n      );\n      const promises = targetPair.map(({ view, targetCtx }) => {\n        return view.pushHookEvent(this.el, targetCtx, event, payload || {});\n      });\n      return Promise.allSettled(promises);\n    }\n    this.__view().withinTargets(\n      selectorOrTarget,\n      (view, targetCtx) => {\n        view.pushHookEvent(this.el, targetCtx, event, payload || {}).then(\n          ({ reply, ref }) => onReply(reply, ref)\n        ).catch(() => {\n        });\n      }\n    );\n  }\n  handleEvent(event, callback) {\n    const callbackRef = {\n      event,\n      callback: (customEvent) => callback(customEvent.detail)\n    };\n    window.addEventListener(\n      `phx:${event}`,\n      callbackRef.callback\n    );\n    this.__listeners.add(callbackRef);\n    return callbackRef;\n  }\n  removeHandleEvent(ref) {\n    window.removeEventListener(\n      `phx:${ref.event}`,\n      ref.callback\n    );\n    this.__listeners.delete(ref);\n  }\n  upload(name, files) {\n    return this.__view().dispatchUploads(null, name, files);\n  }\n  uploadTo(selectorOrTarget, name, files) {\n    return this.__view().withinTargets(\n      selectorOrTarget,\n      (view, targetCtx) => {\n        view.dispatchUploads(targetCtx, name, files);\n      }\n    );\n  }\n  /** @internal */\n  __cleanup__() {\n    this.__listeners.forEach(\n      (callbackRef) => this.removeHandleEvent(callbackRef)\n    );\n  }\n};\n\n// js/phoenix_live_view/view.js\nvar prependFormDataKey = (key, prefix) => {\n  const isArray = key.endsWith(\"[]\");\n  let baseKey = isArray ? key.slice(0, -2) : key;\n  baseKey = baseKey.replace(/([^\\[\\]]+)(\\]?$)/, `${prefix}$1$2`);\n  if (isArray) {\n    baseKey += \"[]\";\n  }\n  return baseKey;\n};\nvar View = class _View {\n  static closestView(el) {\n    const liveViewEl = el.closest(PHX_VIEW_SELECTOR);\n    return liveViewEl ? dom_default.private(liveViewEl, \"view\") : null;\n  }\n  constructor(el, liveSocket, parentView, flash, liveReferer) {\n    this.isDead = false;\n    this.liveSocket = liveSocket;\n    this.flash = flash;\n    this.parent = parentView;\n    this.root = parentView ? parentView.root : this;\n    this.el = el;\n    const boundView = dom_default.private(this.el, \"view\");\n    if (boundView !== void 0 && boundView.isDead !== true) {\n      logError(\n        `The DOM element for this view has already been bound to a view.\n\n        An element can only ever be associated with a single view!\n        Please ensure that you are not trying to initialize multiple LiveSockets on the same page.\n        This could happen if you're accidentally trying to render your root layout more than once.\n        Ensure that the template set on the LiveView is different than the root layout.\n      `,\n        { view: boundView }\n      );\n      throw new Error(\"Cannot bind multiple views to the same DOM element.\");\n    }\n    dom_default.putPrivate(this.el, \"view\", this);\n    this.id = this.el.id;\n    this.ref = 0;\n    this.lastAckRef = null;\n    this.childJoins = 0;\n    this.loaderTimer = null;\n    this.disconnectedTimer = null;\n    this.pendingDiffs = [];\n    this.pendingForms = /* @__PURE__ */ new Set();\n    this.redirect = false;\n    this.href = null;\n    this.joinCount = this.parent ? this.parent.joinCount - 1 : 0;\n    this.joinAttempts = 0;\n    this.joinPending = true;\n    this.destroyed = false;\n    this.joinCallback = function(onDone) {\n      onDone && onDone();\n    };\n    this.stopCallback = function() {\n    };\n    this.pendingJoinOps = [];\n    this.viewHooks = {};\n    this.formSubmits = [];\n    this.children = this.parent ? null : {};\n    this.root.children[this.id] = {};\n    this.formsForRecovery = {};\n    this.channel = this.liveSocket.channel(`lv:${this.id}`, () => {\n      const url = this.href && this.expandURL(this.href);\n      return {\n        redirect: this.redirect ? url : void 0,\n        url: this.redirect ? void 0 : url || void 0,\n        params: this.connectParams(liveReferer),\n        session: this.getSession(),\n        static: this.getStatic(),\n        flash: this.flash,\n        sticky: this.el.hasAttribute(PHX_STICKY)\n      };\n    });\n    this.portalElementIds = /* @__PURE__ */ new Set();\n  }\n  setHref(href) {\n    this.href = href;\n  }\n  setRedirect(href) {\n    this.redirect = true;\n    this.href = href;\n  }\n  isMain() {\n    return this.el.hasAttribute(PHX_MAIN);\n  }\n  connectParams(liveReferer) {\n    const params = this.liveSocket.params(this.el);\n    const manifest = dom_default.all(document, `[${this.binding(PHX_TRACK_STATIC)}]`).map((node) => node.src || node.href).filter((url) => typeof url === \"string\");\n    if (manifest.length > 0) {\n      params[\"_track_static\"] = manifest;\n    }\n    params[\"_mounts\"] = this.joinCount;\n    params[\"_mount_attempts\"] = this.joinAttempts;\n    params[\"_live_referer\"] = liveReferer;\n    this.joinAttempts++;\n    return params;\n  }\n  isConnected() {\n    return this.channel.canPush();\n  }\n  getSession() {\n    return this.el.getAttribute(PHX_SESSION);\n  }\n  getStatic() {\n    const val = this.el.getAttribute(PHX_STATIC);\n    return val === \"\" ? null : val;\n  }\n  destroy(callback = function() {\n  }) {\n    this.destroyAllChildren();\n    this.destroyPortalElements();\n    this.destroyed = true;\n    dom_default.deletePrivate(this.el, \"view\");\n    delete this.root.children[this.id];\n    if (this.parent) {\n      delete this.root.children[this.parent.id][this.id];\n    }\n    clearTimeout(this.loaderTimer);\n    const onFinished = () => {\n      callback();\n      for (const id in this.viewHooks) {\n        this.destroyHook(this.viewHooks[id]);\n      }\n    };\n    dom_default.markPhxChildDestroyed(this.el);\n    this.log(\"destroyed\", () => [\"the child has been removed from the parent\"]);\n    this.channel.leave().receive(\"ok\", onFinished).receive(\"error\", onFinished).receive(\"timeout\", onFinished);\n  }\n  setContainerClasses(...classes) {\n    this.el.classList.remove(\n      PHX_CONNECTED_CLASS,\n      PHX_LOADING_CLASS,\n      PHX_ERROR_CLASS,\n      PHX_CLIENT_ERROR_CLASS,\n      PHX_SERVER_ERROR_CLASS\n    );\n    this.el.classList.add(...classes);\n  }\n  showLoader(timeout) {\n    clearTimeout(this.loaderTimer);\n    if (timeout) {\n      this.loaderTimer = setTimeout(() => this.showLoader(), timeout);\n    } else {\n      for (const id in this.viewHooks) {\n        this.viewHooks[id].__disconnected();\n      }\n      this.setContainerClasses(PHX_LOADING_CLASS);\n    }\n  }\n  execAll(binding) {\n    dom_default.all(\n      this.el,\n      `[${binding}]`,\n      (el) => this.liveSocket.execJS(el, el.getAttribute(binding))\n    );\n  }\n  hideLoader() {\n    clearTimeout(this.loaderTimer);\n    clearTimeout(this.disconnectedTimer);\n    this.setContainerClasses(PHX_CONNECTED_CLASS);\n    this.execAll(this.binding(\"connected\"));\n  }\n  triggerReconnected() {\n    for (const id in this.viewHooks) {\n      this.viewHooks[id].__reconnected();\n    }\n  }\n  log(kind, msgCallback) {\n    this.liveSocket.log(this, kind, msgCallback);\n  }\n  transition(time, onStart, onDone = function() {\n  }) {\n    this.liveSocket.transition(time, onStart, onDone);\n  }\n  // calls the callback with the view and target element for the given phxTarget\n  // targets can be:\n  //  * an element itself, then it is simply passed to liveSocket.owner;\n  //  * a CID (Component ID), then we first search the component's element in the DOM\n  //  * a selector, then we search the selector in the DOM and call the callback\n  //    for each element found with the corresponding owner view\n  withinTargets(phxTarget, callback, dom = document) {\n    if (phxTarget instanceof HTMLElement || phxTarget instanceof SVGElement) {\n      return this.liveSocket.owner(\n        phxTarget,\n        (view) => callback(view, phxTarget)\n      );\n    }\n    if (isCid(phxTarget)) {\n      const targets = dom_default.findComponentNodeList(this.id, phxTarget, dom);\n      if (targets.length === 0) {\n        logError(`no component found matching phx-target of ${phxTarget}`);\n      } else {\n        callback(this, parseInt(phxTarget));\n      }\n    } else {\n      const targets = Array.from(dom.querySelectorAll(phxTarget));\n      if (targets.length === 0) {\n        logError(\n          `nothing found matching the phx-target selector \"${phxTarget}\"`\n        );\n      }\n      targets.forEach(\n        (target) => this.liveSocket.owner(target, (view) => callback(view, target))\n      );\n    }\n  }\n  applyDiff(type, rawDiff, callback) {\n    this.log(type, () => [\"\", clone(rawDiff)]);\n    const { diff, reply, events, title } = Rendered.extract(rawDiff);\n    const ev = events.reduce(\n      (acc, args) => {\n        if (args.length === 3 && args[2] == true) {\n          acc.pre.push(args.slice(0, -1));\n        } else {\n          acc.post.push(args);\n        }\n        return acc;\n      },\n      { pre: [], post: [] }\n    );\n    this.liveSocket.dispatchEvents(ev.pre);\n    const update = () => {\n      callback({ diff, reply, events: ev.post });\n      if (typeof title === \"string\" || type == \"mount\" && this.isMain()) {\n        window.requestAnimationFrame(() => dom_default.putTitle(title));\n      }\n    };\n    if (\"onDocumentPatch\" in this.liveSocket.domCallbacks) {\n      this.liveSocket.triggerDOM(\"onDocumentPatch\", [update]);\n    } else {\n      update();\n    }\n  }\n  onJoin(resp) {\n    const { rendered, container, liveview_version, pid } = resp;\n    if (container) {\n      const [tag, attrs] = container;\n      this.el = dom_default.replaceRootContainer(this.el, tag, attrs);\n    }\n    this.childJoins = 0;\n    this.joinPending = true;\n    this.flash = null;\n    if (this.root === this) {\n      this.formsForRecovery = this.getFormsForRecovery();\n    }\n    if (this.isMain() && window.history.state === null) {\n      browser_default.pushState(\"replace\", {\n        type: \"patch\",\n        id: this.id,\n        position: this.liveSocket.currentHistoryPosition\n      });\n    }\n    if (liveview_version !== this.liveSocket.version()) {\n      console.warn(\n        `LiveView asset version mismatch. JavaScript version ${this.liveSocket.version()} vs. server ${liveview_version}. To avoid issues, please ensure that your assets use the same version as the server.`\n      );\n    }\n    if (pid) {\n      this.el.setAttribute(PHX_LV_PID, pid);\n    }\n    browser_default.dropLocal(\n      this.liveSocket.localStorage,\n      window.location.pathname,\n      CONSECUTIVE_RELOADS\n    );\n    this.applyDiff(\"mount\", rendered, ({ diff, events }) => {\n      this.rendered = new Rendered(this.id, diff);\n      const [html, streams] = this.renderContainer(null, \"join\");\n      this.dropPendingRefs();\n      this.joinCount++;\n      this.joinAttempts = 0;\n      this.maybeRecoverForms(html, () => {\n        this.onJoinComplete(resp, html, streams, events);\n      });\n    });\n  }\n  dropPendingRefs() {\n    dom_default.all(document, `[${PHX_REF_SRC}=\"${this.refSrc()}\"]`, (el) => {\n      el.removeAttribute(PHX_REF_LOADING);\n      el.removeAttribute(PHX_REF_SRC);\n      el.removeAttribute(PHX_REF_LOCK);\n    });\n  }\n  onJoinComplete({ live_patch }, html, streams, events) {\n    if (this.joinCount > 1 || this.parent && !this.parent.isJoinPending()) {\n      return this.applyJoinPatch(live_patch, html, streams, events);\n    }\n    const newChildren = dom_default.findPhxChildrenInFragment(html, this.id).filter(\n      (toEl) => {\n        const fromEl = toEl.id && this.el.querySelector(`[id=\"${toEl.id}\"]`);\n        const phxStatic = fromEl && fromEl.getAttribute(PHX_STATIC);\n        if (phxStatic) {\n          toEl.setAttribute(PHX_STATIC, phxStatic);\n        }\n        if (fromEl) {\n          fromEl.setAttribute(PHX_ROOT_ID, this.root.id);\n        }\n        return this.joinChild(toEl);\n      }\n    );\n    if (newChildren.length === 0) {\n      if (this.parent) {\n        this.root.pendingJoinOps.push([\n          this,\n          () => this.applyJoinPatch(live_patch, html, streams, events)\n        ]);\n        this.parent.ackJoin(this);\n      } else {\n        this.onAllChildJoinsComplete();\n        this.applyJoinPatch(live_patch, html, streams, events);\n      }\n    } else {\n      this.root.pendingJoinOps.push([\n        this,\n        () => this.applyJoinPatch(live_patch, html, streams, events)\n      ]);\n    }\n  }\n  attachTrueDocEl() {\n    this.el = dom_default.byId(this.id);\n    this.el.setAttribute(PHX_ROOT_ID, this.root.id);\n  }\n  // this is invoked for dead and live views, so we must filter by\n  // by owner to ensure we aren't duplicating hooks across disconnect\n  // and connected states. This also handles cases where hooks exist\n  // in a root layout with a LV in the body\n  execNewMounted(parent = document) {\n    let phxViewportTop = this.binding(PHX_VIEWPORT_TOP);\n    let phxViewportBottom = this.binding(PHX_VIEWPORT_BOTTOM);\n    this.all(\n      parent,\n      `[${phxViewportTop}], [${phxViewportBottom}]`,\n      (hookEl) => {\n        dom_default.maintainPrivateHooks(\n          hookEl,\n          hookEl,\n          phxViewportTop,\n          phxViewportBottom\n        );\n        this.maybeAddNewHook(hookEl);\n      }\n    );\n    this.all(\n      parent,\n      `[${this.binding(PHX_HOOK)}], [data-phx-${PHX_HOOK}]`,\n      (hookEl) => {\n        this.maybeAddNewHook(hookEl);\n      }\n    );\n    this.all(parent, `[${this.binding(PHX_MOUNTED)}]`, (el) => {\n      this.maybeMounted(el);\n    });\n  }\n  all(parent, selector, callback) {\n    dom_default.all(parent, selector, (el) => {\n      if (this.ownsElement(el)) {\n        callback(el);\n      }\n    });\n  }\n  applyJoinPatch(live_patch, html, streams, events) {\n    if (this.joinCount > 1) {\n      if (this.pendingJoinOps.length) {\n        this.pendingJoinOps.forEach((cb) => typeof cb === \"function\" && cb());\n        this.pendingJoinOps = [];\n      }\n    }\n    this.attachTrueDocEl();\n    const patch = new DOMPatch(this, this.el, this.id, html, streams, null);\n    patch.markPrunableContentForRemoval();\n    this.performPatch(patch, false, true);\n    this.joinNewChildren();\n    this.execNewMounted();\n    this.joinPending = false;\n    this.liveSocket.dispatchEvents(events);\n    this.applyPendingUpdates();\n    if (live_patch) {\n      const { kind, to } = live_patch;\n      this.liveSocket.historyPatch(to, kind);\n    }\n    this.hideLoader();\n    if (this.joinCount > 1) {\n      this.triggerReconnected();\n    }\n    this.stopCallback();\n  }\n  triggerBeforeUpdateHook(fromEl, toEl) {\n    this.liveSocket.triggerDOM(\"onBeforeElUpdated\", [fromEl, toEl]);\n    const hook = this.getHook(fromEl);\n    const isIgnored = hook && dom_default.isIgnored(fromEl, this.binding(PHX_UPDATE));\n    if (hook && !fromEl.isEqualNode(toEl) && !(isIgnored && isEqualObj(fromEl.dataset, toEl.dataset))) {\n      hook.__beforeUpdate();\n      return hook;\n    }\n  }\n  maybeMounted(el) {\n    const phxMounted = el.getAttribute(this.binding(PHX_MOUNTED));\n    const hasBeenInvoked = phxMounted && dom_default.private(el, \"mounted\");\n    if (phxMounted && !hasBeenInvoked) {\n      this.liveSocket.execJS(el, phxMounted);\n      dom_default.putPrivate(el, \"mounted\", true);\n    }\n  }\n  maybeAddNewHook(el) {\n    const newHook = this.addHook(el);\n    if (newHook) {\n      newHook.__mounted();\n    }\n  }\n  performPatch(patch, pruneCids, isJoinPatch = false) {\n    const removedEls = [];\n    let phxChildrenAdded = false;\n    const updatedHookIds = /* @__PURE__ */ new Set();\n    this.liveSocket.triggerDOM(\"onPatchStart\", [patch.targetContainer]);\n    patch.after(\"added\", (el) => {\n      this.liveSocket.triggerDOM(\"onNodeAdded\", [el]);\n      const phxViewportTop = this.binding(PHX_VIEWPORT_TOP);\n      const phxViewportBottom = this.binding(PHX_VIEWPORT_BOTTOM);\n      dom_default.maintainPrivateHooks(el, el, phxViewportTop, phxViewportBottom);\n      this.maybeAddNewHook(el);\n      if (el.getAttribute) {\n        this.maybeMounted(el);\n      }\n    });\n    patch.after(\"phxChildAdded\", (el) => {\n      if (dom_default.isPhxSticky(el)) {\n        this.liveSocket.joinRootViews();\n      } else {\n        phxChildrenAdded = true;\n      }\n    });\n    patch.before(\"updated\", (fromEl, toEl) => {\n      const hook = this.triggerBeforeUpdateHook(fromEl, toEl);\n      if (hook) {\n        updatedHookIds.add(fromEl.id);\n      }\n      js_default.onBeforeElUpdated(fromEl, toEl);\n    });\n    patch.after(\"updated\", (el) => {\n      if (updatedHookIds.has(el.id)) {\n        this.getHook(el).__updated();\n      }\n    });\n    patch.after(\"discarded\", (el) => {\n      if (el.nodeType === Node.ELEMENT_NODE) {\n        removedEls.push(el);\n      }\n    });\n    patch.after(\n      \"transitionsDiscarded\",\n      (els) => this.afterElementsRemoved(els, pruneCids)\n    );\n    patch.perform(isJoinPatch);\n    this.afterElementsRemoved(removedEls, pruneCids);\n    this.liveSocket.triggerDOM(\"onPatchEnd\", [patch.targetContainer]);\n    return phxChildrenAdded;\n  }\n  afterElementsRemoved(elements, pruneCids) {\n    const destroyedCIDs = [];\n    elements.forEach((parent) => {\n      const components = dom_default.all(\n        parent,\n        `[${PHX_VIEW_REF}=\"${this.id}\"][${PHX_COMPONENT}]`\n      );\n      const hooks = dom_default.all(\n        parent,\n        `[${this.binding(PHX_HOOK)}], [data-phx-hook]`\n      );\n      components.concat(parent).forEach((el) => {\n        const cid = this.componentID(el);\n        if (isCid(cid) && destroyedCIDs.indexOf(cid) === -1 && el.getAttribute(PHX_VIEW_REF) === this.id) {\n          destroyedCIDs.push(cid);\n        }\n      });\n      hooks.concat(parent).forEach((hookEl) => {\n        const hook = this.getHook(hookEl);\n        hook && this.destroyHook(hook);\n      });\n    });\n    if (pruneCids) {\n      this.maybePushComponentsDestroyed(destroyedCIDs);\n    }\n  }\n  joinNewChildren() {\n    dom_default.findPhxChildren(document, this.id).forEach((el) => this.joinChild(el));\n  }\n  maybeRecoverForms(html, callback) {\n    const phxChange = this.binding(\"change\");\n    const oldForms = this.root.formsForRecovery;\n    const template = document.createElement(\"template\");\n    template.innerHTML = html;\n    dom_default.all(template.content, `[${PHX_PORTAL}]`).forEach((portalTemplate) => {\n      template.content.firstElementChild.appendChild(\n        portalTemplate.content.firstElementChild\n      );\n    });\n    const rootEl = template.content.firstElementChild;\n    rootEl.id = this.id;\n    rootEl.setAttribute(PHX_ROOT_ID, this.root.id);\n    rootEl.setAttribute(PHX_SESSION, this.getSession());\n    rootEl.setAttribute(PHX_STATIC, this.getStatic());\n    rootEl.setAttribute(PHX_PARENT_ID, this.parent ? this.parent.id : null);\n    const formsToRecover = (\n      // we go over all forms in the new DOM; because this is only the HTML for the current\n      // view, we can be sure that all forms are owned by this view:\n      dom_default.all(template.content, \"form\").filter((newForm) => newForm.id && oldForms[newForm.id]).filter((newForm) => !this.pendingForms.has(newForm.id)).filter(\n        (newForm) => oldForms[newForm.id].getAttribute(phxChange) === newForm.getAttribute(phxChange)\n      ).map((newForm) => {\n        return [oldForms[newForm.id], newForm];\n      })\n    );\n    if (formsToRecover.length === 0) {\n      return callback();\n    }\n    formsToRecover.forEach(([oldForm, newForm], i) => {\n      this.pendingForms.add(newForm.id);\n      this.pushFormRecovery(\n        oldForm,\n        newForm,\n        template.content.firstElementChild,\n        () => {\n          this.pendingForms.delete(newForm.id);\n          if (i === formsToRecover.length - 1) {\n            callback();\n          }\n        }\n      );\n    });\n  }\n  getChildById(id) {\n    return this.root.children[this.id][id];\n  }\n  getDescendentByEl(el) {\n    if (el.id === this.id) {\n      return this;\n    } else {\n      return this.children[el.getAttribute(PHX_PARENT_ID)]?.[el.id];\n    }\n  }\n  destroyDescendent(id) {\n    for (const parentId in this.root.children) {\n      for (const childId in this.root.children[parentId]) {\n        if (childId === id) {\n          return this.root.children[parentId][childId].destroy();\n        }\n      }\n    }\n  }\n  joinChild(el) {\n    const child = this.getChildById(el.id);\n    if (!child) {\n      const view = new _View(el, this.liveSocket, this);\n      this.root.children[this.id][view.id] = view;\n      view.join();\n      this.childJoins++;\n      return true;\n    }\n  }\n  isJoinPending() {\n    return this.joinPending;\n  }\n  ackJoin(_child) {\n    this.childJoins--;\n    if (this.childJoins === 0) {\n      if (this.parent) {\n        this.parent.ackJoin(this);\n      } else {\n        this.onAllChildJoinsComplete();\n      }\n    }\n  }\n  onAllChildJoinsComplete() {\n    this.pendingForms.clear();\n    this.formsForRecovery = {};\n    this.joinCallback(() => {\n      this.pendingJoinOps.forEach(([view, op]) => {\n        if (!view.isDestroyed()) {\n          op();\n        }\n      });\n      this.pendingJoinOps = [];\n    });\n  }\n  update(diff, events, isPending = false) {\n    if (this.isJoinPending() || this.liveSocket.hasPendingLink() && this.root.isMain()) {\n      if (!isPending) {\n        this.pendingDiffs.push({ diff, events });\n      }\n      return false;\n    }\n    this.rendered.mergeDiff(diff);\n    let phxChildrenAdded = false;\n    if (this.rendered.isComponentOnlyDiff(diff)) {\n      this.liveSocket.time(\"component patch complete\", () => {\n        const parentCids = dom_default.findExistingParentCIDs(\n          this.id,\n          this.rendered.componentCIDs(diff)\n        );\n        parentCids.forEach((parentCID) => {\n          if (this.componentPatch(\n            this.rendered.getComponent(diff, parentCID),\n            parentCID\n          )) {\n            phxChildrenAdded = true;\n          }\n        });\n      });\n    } else if (!isEmpty(diff)) {\n      this.liveSocket.time(\"full patch complete\", () => {\n        const [html, streams] = this.renderContainer(diff, \"update\");\n        const patch = new DOMPatch(this, this.el, this.id, html, streams, null);\n        phxChildrenAdded = this.performPatch(patch, true);\n      });\n    }\n    this.liveSocket.dispatchEvents(events);\n    if (phxChildrenAdded) {\n      this.joinNewChildren();\n    }\n    return true;\n  }\n  renderContainer(diff, kind) {\n    return this.liveSocket.time(`toString diff (${kind})`, () => {\n      const tag = this.el.tagName;\n      const cids = diff ? this.rendered.componentCIDs(diff) : null;\n      const { buffer: html, streams } = this.rendered.toString(cids);\n      return [`<${tag}>${html}</${tag}>`, streams];\n    });\n  }\n  componentPatch(diff, cid) {\n    if (isEmpty(diff))\n      return false;\n    const { buffer: html, streams } = this.rendered.componentToString(cid);\n    const patch = new DOMPatch(this, this.el, this.id, html, streams, cid);\n    const childrenAdded = this.performPatch(patch, true);\n    return childrenAdded;\n  }\n  getHook(el) {\n    return this.viewHooks[ViewHook.elementID(el)];\n  }\n  addHook(el) {\n    const hookElId = ViewHook.elementID(el);\n    if (el.getAttribute && !this.ownsElement(el)) {\n      return;\n    }\n    if (hookElId && !this.viewHooks[hookElId]) {\n      if (ViewHook.deadHook(el)) {\n        return;\n      }\n      const hook = dom_default.getCustomElHook(el) || logError(`no hook found for custom element: ${el.id}`);\n      this.viewHooks[hookElId] = hook;\n      hook.__attachView(this);\n      return hook;\n    } else if (hookElId || !el.getAttribute) {\n      return;\n    } else {\n      const hookName = el.getAttribute(`data-phx-${PHX_HOOK}`) || el.getAttribute(this.binding(PHX_HOOK));\n      if (!hookName) {\n        return;\n      }\n      const hookDefinition = this.liveSocket.getHookDefinition(hookName);\n      if (hookDefinition) {\n        if (!el.id) {\n          logError(\n            `no DOM ID for hook \"${hookName}\". Hooks require a unique ID on each element.`,\n            el\n          );\n          return;\n        }\n        let hookInstance;\n        try {\n          if (typeof hookDefinition === \"function\" && hookDefinition.prototype instanceof ViewHook) {\n            hookInstance = new hookDefinition(this, el);\n          } else if (typeof hookDefinition === \"object\" && hookDefinition !== null) {\n            hookInstance = new ViewHook(this, el, hookDefinition);\n          } else {\n            logError(\n              `Invalid hook definition for \"${hookName}\". Expected a class extending ViewHook or an object definition.`,\n              el\n            );\n            return;\n          }\n        } catch (e) {\n          const errorMessage = e instanceof Error ? e.message : String(e);\n          logError(`Failed to create hook \"${hookName}\": ${errorMessage}`, el);\n          return;\n        }\n        this.viewHooks[ViewHook.elementID(hookInstance.el)] = hookInstance;\n        return hookInstance;\n      } else if (hookName !== null) {\n        logError(`unknown hook found for \"${hookName}\"`, el);\n      }\n    }\n  }\n  destroyHook(hook) {\n    const hookId = ViewHook.elementID(hook.el);\n    hook.__destroyed();\n    hook.__cleanup__();\n    delete this.viewHooks[hookId];\n  }\n  applyPendingUpdates() {\n    this.pendingDiffs = this.pendingDiffs.filter(\n      ({ diff, events }) => !this.update(diff, events, true)\n    );\n    this.eachChild((child) => child.applyPendingUpdates());\n  }\n  eachChild(callback) {\n    const children = this.root.children[this.id] || {};\n    for (const id in children) {\n      callback(this.getChildById(id));\n    }\n  }\n  onChannel(event, cb) {\n    this.liveSocket.onChannel(this.channel, event, (resp) => {\n      if (this.isJoinPending()) {\n        if (this.joinCount > 1) {\n          this.pendingJoinOps.push(() => cb(resp));\n        } else {\n          this.root.pendingJoinOps.push([this, () => cb(resp)]);\n        }\n      } else {\n        this.liveSocket.requestDOMUpdate(() => cb(resp));\n      }\n    });\n  }\n  bindChannel() {\n    this.liveSocket.onChannel(this.channel, \"diff\", (rawDiff) => {\n      this.liveSocket.requestDOMUpdate(() => {\n        this.applyDiff(\n          \"update\",\n          rawDiff,\n          ({ diff, events }) => this.update(diff, events)\n        );\n      });\n    });\n    this.onChannel(\n      \"redirect\",\n      ({ to, flash }) => this.onRedirect({ to, flash })\n    );\n    this.onChannel(\"live_patch\", (redir) => this.onLivePatch(redir));\n    this.onChannel(\"live_redirect\", (redir) => this.onLiveRedirect(redir));\n    this.channel.onError((reason) => this.onError(reason));\n    this.channel.onClose((reason) => this.onClose(reason));\n  }\n  destroyAllChildren() {\n    this.eachChild((child) => child.destroy());\n  }\n  onLiveRedirect(redir) {\n    const { to, kind, flash } = redir;\n    const url = this.expandURL(to);\n    const e = new CustomEvent(\"phx:server-navigate\", {\n      detail: { to, kind, flash }\n    });\n    this.liveSocket.historyRedirect(e, url, kind, flash);\n  }\n  onLivePatch(redir) {\n    const { to, kind } = redir;\n    this.href = this.expandURL(to);\n    this.liveSocket.historyPatch(to, kind);\n  }\n  expandURL(to) {\n    return to.startsWith(\"/\") ? `${window.location.protocol}//${window.location.host}${to}` : to;\n  }\n  /**\n   * @param {{to: string, flash?: string, reloadToken?: string}} redirect\n   */\n  onRedirect({ to, flash, reloadToken }) {\n    this.liveSocket.redirect(to, flash, reloadToken);\n  }\n  isDestroyed() {\n    return this.destroyed;\n  }\n  joinDead() {\n    this.isDead = true;\n  }\n  joinPush() {\n    this.joinPush = this.joinPush || this.channel.join();\n    return this.joinPush;\n  }\n  join(callback) {\n    this.showLoader(this.liveSocket.loaderTimeout);\n    this.bindChannel();\n    if (this.isMain()) {\n      this.stopCallback = this.liveSocket.withPageLoading({\n        to: this.href,\n        kind: \"initial\"\n      });\n    }\n    this.joinCallback = (onDone) => {\n      onDone = onDone || function() {\n      };\n      callback ? callback(this.joinCount, onDone) : onDone();\n    };\n    this.wrapPush(() => this.channel.join(), {\n      ok: (resp) => this.liveSocket.requestDOMUpdate(() => this.onJoin(resp)),\n      error: (error) => this.onJoinError(error),\n      timeout: () => this.onJoinError({ reason: \"timeout\" })\n    });\n  }\n  onJoinError(resp) {\n    if (resp.reason === \"reload\") {\n      this.log(\"error\", () => [\n        `failed mount with ${resp.status}. Falling back to page reload`,\n        resp\n      ]);\n      this.onRedirect({\n        to: this.liveSocket.main.href,\n        reloadToken: resp.token\n      });\n      return;\n    } else if (resp.reason === \"unauthorized\" || resp.reason === \"stale\") {\n      this.log(\"error\", () => [\n        \"unauthorized live_redirect. Falling back to page request\",\n        resp\n      ]);\n      this.onRedirect({ to: this.liveSocket.main.href, flash: this.flash });\n      return;\n    }\n    if (resp.redirect || resp.live_redirect) {\n      this.joinPending = false;\n      this.channel.leave();\n    }\n    if (resp.redirect) {\n      return this.onRedirect(resp.redirect);\n    }\n    if (resp.live_redirect) {\n      return this.onLiveRedirect(resp.live_redirect);\n    }\n    this.log(\"error\", () => [\"unable to join\", resp]);\n    if (this.isMain()) {\n      this.displayError(\n        [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS],\n        { unstructuredError: resp, errorKind: \"server\" }\n      );\n      if (this.liveSocket.isConnected()) {\n        this.liveSocket.reloadWithJitter(this);\n      }\n    } else {\n      if (this.joinAttempts >= MAX_CHILD_JOIN_ATTEMPTS) {\n        this.root.displayError(\n          [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS],\n          { unstructuredError: resp, errorKind: \"server\" }\n        );\n        this.log(\"error\", () => [\n          `giving up trying to mount after ${MAX_CHILD_JOIN_ATTEMPTS} tries`,\n          resp\n        ]);\n        this.destroy();\n      }\n      const trueChildEl = dom_default.byId(this.el.id);\n      if (trueChildEl) {\n        dom_default.mergeAttrs(trueChildEl, this.el);\n        this.displayError(\n          [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS],\n          { unstructuredError: resp, errorKind: \"server\" }\n        );\n        this.el = trueChildEl;\n      } else {\n        this.destroy();\n      }\n    }\n  }\n  onClose(reason) {\n    if (this.isDestroyed()) {\n      return;\n    }\n    if (this.isMain() && this.liveSocket.hasPendingLink() && reason !== \"leave\") {\n      return this.liveSocket.reloadWithJitter(this);\n    }\n    this.destroyAllChildren();\n    this.liveSocket.dropActiveElement(this);\n    if (this.liveSocket.isUnloaded()) {\n      this.showLoader(BEFORE_UNLOAD_LOADER_TIMEOUT);\n    }\n  }\n  onError(reason) {\n    this.onClose(reason);\n    if (this.liveSocket.isConnected()) {\n      this.log(\"error\", () => [\"view crashed\", reason]);\n    }\n    if (!this.liveSocket.isUnloaded()) {\n      if (this.liveSocket.isConnected()) {\n        this.displayError(\n          [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS],\n          { unstructuredError: reason, errorKind: \"server\" }\n        );\n      } else {\n        this.displayError(\n          [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_CLIENT_ERROR_CLASS],\n          { unstructuredError: reason, errorKind: \"client\" }\n        );\n      }\n    }\n  }\n  displayError(classes, details = {}) {\n    if (this.isMain()) {\n      dom_default.dispatchEvent(window, \"phx:page-loading-start\", {\n        detail: { to: this.href, kind: \"error\", ...details }\n      });\n    }\n    this.showLoader();\n    this.setContainerClasses(...classes);\n    this.delayedDisconnected();\n  }\n  delayedDisconnected() {\n    this.disconnectedTimer = setTimeout(() => {\n      this.execAll(this.binding(\"disconnected\"));\n    }, this.liveSocket.disconnectedTimeout);\n  }\n  wrapPush(callerPush, receives) {\n    const latency = this.liveSocket.getLatencySim();\n    const withLatency = latency ? (cb) => setTimeout(() => !this.isDestroyed() && cb(), latency) : (cb) => !this.isDestroyed() && cb();\n    withLatency(() => {\n      callerPush().receive(\n        \"ok\",\n        (resp) => withLatency(() => receives.ok && receives.ok(resp))\n      ).receive(\n        \"error\",\n        (reason) => withLatency(() => receives.error && receives.error(reason))\n      ).receive(\n        \"timeout\",\n        () => withLatency(() => receives.timeout && receives.timeout())\n      );\n    });\n  }\n  pushWithReply(refGenerator, event, payload) {\n    if (!this.isConnected()) {\n      return Promise.reject(new Error(\"no connection\"));\n    }\n    const [ref, [el], opts] = refGenerator ? refGenerator({ payload }) : [null, [], {}];\n    const oldJoinCount = this.joinCount;\n    let onLoadingDone = function() {\n    };\n    if (opts.page_loading) {\n      onLoadingDone = this.liveSocket.withPageLoading({\n        kind: \"element\",\n        target: el\n      });\n    }\n    if (typeof payload.cid !== \"number\") {\n      delete payload.cid;\n    }\n    return new Promise((resolve, reject) => {\n      this.wrapPush(() => this.channel.push(event, payload, PUSH_TIMEOUT), {\n        ok: (resp) => {\n          if (ref !== null) {\n            this.lastAckRef = ref;\n          }\n          const finish = (hookReply) => {\n            if (resp.redirect) {\n              this.onRedirect(resp.redirect);\n            }\n            if (resp.live_patch) {\n              this.onLivePatch(resp.live_patch);\n            }\n            if (resp.live_redirect) {\n              this.onLiveRedirect(resp.live_redirect);\n            }\n            onLoadingDone();\n            resolve({ resp, reply: hookReply, ref });\n          };\n          if (resp.diff) {\n            this.liveSocket.requestDOMUpdate(() => {\n              this.applyDiff(\"update\", resp.diff, ({ diff, reply, events }) => {\n                if (ref !== null) {\n                  this.undoRefs(ref, payload.event);\n                }\n                this.update(diff, events);\n                finish(reply);\n              });\n            });\n          } else {\n            if (ref !== null) {\n              this.undoRefs(ref, payload.event);\n            }\n            finish(null);\n          }\n        },\n        error: (reason) => reject(new Error(`failed with reason: ${JSON.stringify(reason)}`)),\n        timeout: () => {\n          reject(new Error(\"timeout\"));\n          if (this.joinCount === oldJoinCount) {\n            this.liveSocket.reloadWithJitter(this, () => {\n              this.log(\"timeout\", () => [\n                \"received timeout while communicating with server. Falling back to hard refresh for recovery\"\n              ]);\n            });\n          }\n        }\n      });\n    });\n  }\n  undoRefs(ref, phxEvent, onlyEls) {\n    if (!this.isConnected()) {\n      return;\n    }\n    const selector = `[${PHX_REF_SRC}=\"${this.refSrc()}\"]`;\n    if (onlyEls) {\n      onlyEls = new Set(onlyEls);\n      dom_default.all(document, selector, (parent) => {\n        if (onlyEls && !onlyEls.has(parent)) {\n          return;\n        }\n        dom_default.all(\n          parent,\n          selector,\n          (child) => this.undoElRef(child, ref, phxEvent)\n        );\n        this.undoElRef(parent, ref, phxEvent);\n      });\n    } else {\n      dom_default.all(document, selector, (el) => this.undoElRef(el, ref, phxEvent));\n    }\n  }\n  undoElRef(el, ref, phxEvent) {\n    const elRef = new ElementRef(el);\n    elRef.maybeUndo(ref, phxEvent, (clonedTree) => {\n      const patch = new DOMPatch(this, el, this.id, clonedTree, [], null, {\n        undoRef: ref\n      });\n      const phxChildrenAdded = this.performPatch(patch, true);\n      dom_default.all(\n        el,\n        `[${PHX_REF_SRC}=\"${this.refSrc()}\"]`,\n        (child) => this.undoElRef(child, ref, phxEvent)\n      );\n      if (phxChildrenAdded) {\n        this.joinNewChildren();\n      }\n    });\n  }\n  refSrc() {\n    return this.el.id;\n  }\n  putRef(elements, phxEvent, eventType, opts = {}) {\n    const newRef = this.ref++;\n    const disableWith = this.binding(PHX_DISABLE_WITH);\n    if (opts.loading) {\n      const loadingEls = dom_default.all(document, opts.loading).map((el) => {\n        return { el, lock: true, loading: true };\n      });\n      elements = elements.concat(loadingEls);\n    }\n    for (const { el, lock, loading } of elements) {\n      if (!lock && !loading) {\n        throw new Error(\"putRef requires lock or loading\");\n      }\n      el.setAttribute(PHX_REF_SRC, this.refSrc());\n      if (loading) {\n        el.setAttribute(PHX_REF_LOADING, newRef);\n      }\n      if (lock) {\n        el.setAttribute(PHX_REF_LOCK, newRef);\n      }\n      if (!loading || opts.submitter && !(el === opts.submitter || el === opts.form)) {\n        continue;\n      }\n      const lockCompletePromise = new Promise((resolve) => {\n        el.addEventListener(`phx:undo-lock:${newRef}`, () => resolve(detail), {\n          once: true\n        });\n      });\n      const loadingCompletePromise = new Promise((resolve) => {\n        el.addEventListener(\n          `phx:undo-loading:${newRef}`,\n          () => resolve(detail),\n          { once: true }\n        );\n      });\n      el.classList.add(`phx-${eventType}-loading`);\n      const disableText = el.getAttribute(disableWith);\n      if (disableText !== null) {\n        if (!el.getAttribute(PHX_DISABLE_WITH_RESTORE)) {\n          el.setAttribute(PHX_DISABLE_WITH_RESTORE, el.textContent);\n        }\n        if (disableText !== \"\") {\n          el.textContent = disableText;\n        }\n        el.setAttribute(\n          PHX_DISABLED,\n          el.getAttribute(PHX_DISABLED) || el.disabled\n        );\n        el.setAttribute(\"disabled\", \"\");\n      }\n      const detail = {\n        event: phxEvent,\n        eventType,\n        ref: newRef,\n        isLoading: loading,\n        isLocked: lock,\n        lockElements: elements.filter(({ lock: lock2 }) => lock2).map(({ el: el2 }) => el2),\n        loadingElements: elements.filter(({ loading: loading2 }) => loading2).map(({ el: el2 }) => el2),\n        unlock: (els) => {\n          els = Array.isArray(els) ? els : [els];\n          this.undoRefs(newRef, phxEvent, els);\n        },\n        lockComplete: lockCompletePromise,\n        loadingComplete: loadingCompletePromise,\n        lock: (lockEl) => {\n          return new Promise((resolve) => {\n            if (this.isAcked(newRef)) {\n              return resolve(detail);\n            }\n            lockEl.setAttribute(PHX_REF_LOCK, newRef);\n            lockEl.setAttribute(PHX_REF_SRC, this.refSrc());\n            lockEl.addEventListener(\n              `phx:lock-stop:${newRef}`,\n              () => resolve(detail),\n              { once: true }\n            );\n          });\n        }\n      };\n      if (opts.payload) {\n        detail[\"payload\"] = opts.payload;\n      }\n      if (opts.target) {\n        detail[\"target\"] = opts.target;\n      }\n      if (opts.originalEvent) {\n        detail[\"originalEvent\"] = opts.originalEvent;\n      }\n      el.dispatchEvent(\n        new CustomEvent(\"phx:push\", {\n          detail,\n          bubbles: true,\n          cancelable: false\n        })\n      );\n      if (phxEvent) {\n        el.dispatchEvent(\n          new CustomEvent(`phx:push:${phxEvent}`, {\n            detail,\n            bubbles: true,\n            cancelable: false\n          })\n        );\n      }\n    }\n    return [newRef, elements.map(({ el }) => el), opts];\n  }\n  isAcked(ref) {\n    return this.lastAckRef !== null && this.lastAckRef >= ref;\n  }\n  componentID(el) {\n    const cid = el.getAttribute && el.getAttribute(PHX_COMPONENT);\n    return cid ? parseInt(cid) : null;\n  }\n  targetComponentID(target, targetCtx, opts = {}) {\n    if (isCid(targetCtx)) {\n      return targetCtx;\n    }\n    const cidOrSelector = opts.target || target.getAttribute(this.binding(\"target\"));\n    if (isCid(cidOrSelector)) {\n      return parseInt(cidOrSelector);\n    } else if (targetCtx && (cidOrSelector !== null || opts.target)) {\n      return this.closestComponentID(targetCtx);\n    } else {\n      return null;\n    }\n  }\n  closestComponentID(targetCtx) {\n    if (isCid(targetCtx)) {\n      return targetCtx;\n    } else if (targetCtx) {\n      return maybe(\n        // We either use the closest data-phx-component binding, or -\n        // in case of portals - continue with the portal source.\n        // This is necessary if teleporting an element outside of its LiveComponent.\n        targetCtx.closest(`[${PHX_COMPONENT}],[${PHX_TELEPORTED_SRC}]`),\n        (el) => {\n          if (el.hasAttribute(PHX_COMPONENT)) {\n            return this.ownsElement(el) && this.componentID(el);\n          }\n          if (el.hasAttribute(PHX_TELEPORTED_SRC)) {\n            const portalParent = dom_default.byId(el.getAttribute(PHX_TELEPORTED_SRC));\n            return this.closestComponentID(portalParent);\n          }\n        }\n      );\n    } else {\n      return null;\n    }\n  }\n  pushHookEvent(el, targetCtx, event, payload) {\n    if (!this.isConnected()) {\n      this.log(\"hook\", () => [\n        \"unable to push hook event. LiveView not connected\",\n        event,\n        payload\n      ]);\n      return Promise.reject(\n        new Error(\"unable to push hook event. LiveView not connected\")\n      );\n    }\n    const refGenerator = () => this.putRef([{ el, loading: true, lock: true }], event, \"hook\", {\n      payload,\n      target: targetCtx\n    });\n    return this.pushWithReply(refGenerator, \"event\", {\n      type: \"hook\",\n      event,\n      value: payload,\n      cid: this.closestComponentID(targetCtx)\n    }).then(({ resp: _resp, reply, ref }) => ({ reply, ref }));\n  }\n  extractMeta(el, meta, value) {\n    const prefix = this.binding(\"value-\");\n    for (let i = 0; i < el.attributes.length; i++) {\n      if (!meta) {\n        meta = {};\n      }\n      const name = el.attributes[i].name;\n      if (name.startsWith(prefix)) {\n        meta[name.replace(prefix, \"\")] = el.getAttribute(name);\n      }\n    }\n    if (el.value !== void 0 && !(el instanceof HTMLFormElement)) {\n      if (!meta) {\n        meta = {};\n      }\n      meta.value = el.value;\n      if (el.tagName === \"INPUT\" && CHECKABLE_INPUTS.indexOf(el.type) >= 0 && !el.checked) {\n        delete meta.value;\n      }\n    }\n    if (value) {\n      if (!meta) {\n        meta = {};\n      }\n      for (const key in value) {\n        meta[key] = value[key];\n      }\n    }\n    return meta;\n  }\n  serializeForm(form, opts, onlyNames = []) {\n    const { submitter } = opts;\n    let injectedElement;\n    if (submitter && submitter.name) {\n      const input = document.createElement(\"input\");\n      input.type = \"hidden\";\n      const formId = submitter.getAttribute(\"form\");\n      if (formId) {\n        input.setAttribute(\"form\", formId);\n      }\n      input.name = submitter.name;\n      input.value = submitter.value;\n      submitter.parentElement.insertBefore(input, submitter);\n      injectedElement = input;\n    }\n    const formData = new FormData(form);\n    const toRemove = [];\n    formData.forEach((val, key, _index) => {\n      if (val instanceof File) {\n        toRemove.push(key);\n      }\n    });\n    toRemove.forEach((key) => formData.delete(key));\n    const params = new URLSearchParams();\n    const { inputsUnused, onlyHiddenInputs } = Array.from(form.elements).reduce(\n      (acc, input) => {\n        const { inputsUnused: inputsUnused2, onlyHiddenInputs: onlyHiddenInputs2 } = acc;\n        const key = input.name;\n        if (!key) {\n          return acc;\n        }\n        if (inputsUnused2[key] === void 0) {\n          inputsUnused2[key] = true;\n        }\n        if (onlyHiddenInputs2[key] === void 0) {\n          onlyHiddenInputs2[key] = true;\n        }\n        const inputSkipUnusedField = input.hasAttribute(\n          this.binding(PHX_NO_UNUSED_FIELD)\n        );\n        const isUsed = dom_default.private(input, PHX_HAS_FOCUSED) || dom_default.private(input, PHX_HAS_SUBMITTED) || inputSkipUnusedField;\n        const isHidden = input.type === \"hidden\";\n        inputsUnused2[key] = inputsUnused2[key] && !isUsed;\n        onlyHiddenInputs2[key] = onlyHiddenInputs2[key] && isHidden;\n        return acc;\n      },\n      { inputsUnused: {}, onlyHiddenInputs: {} }\n    );\n    const formSkipUnusedFields = form.hasAttribute(\n      this.binding(PHX_NO_UNUSED_FIELD)\n    );\n    for (const [key, val] of formData.entries()) {\n      if (onlyNames.length === 0 || onlyNames.indexOf(key) >= 0) {\n        const isUnused = inputsUnused[key];\n        const hidden = onlyHiddenInputs[key];\n        const skipUnusedCheck = formSkipUnusedFields;\n        if (!skipUnusedCheck && isUnused && !(submitter && submitter.name == key) && !hidden) {\n          params.append(prependFormDataKey(key, \"_unused_\"), \"\");\n        }\n        if (typeof val === \"string\") {\n          params.append(key, val);\n        }\n      }\n    }\n    if (submitter && injectedElement) {\n      submitter.parentElement.removeChild(injectedElement);\n    }\n    return params.toString();\n  }\n  pushEvent(type, el, targetCtx, phxEvent, meta, opts = {}, onReply) {\n    this.pushWithReply(\n      (maybePayload) => this.putRef([{ el, loading: true, lock: true }], phxEvent, type, {\n        ...opts,\n        payload: maybePayload?.payload\n      }),\n      \"event\",\n      {\n        type,\n        event: phxEvent,\n        value: this.extractMeta(el, meta, opts.value),\n        cid: this.targetComponentID(el, targetCtx, opts)\n      }\n    ).then(({ reply }) => onReply && onReply(reply)).catch((error) => logError(\"Failed to push event\", error));\n  }\n  pushFileProgress(fileEl, entryRef, progress, onReply = function() {\n  }) {\n    this.liveSocket.withinOwners(fileEl.form, (view, targetCtx) => {\n      view.pushWithReply(null, \"progress\", {\n        event: fileEl.getAttribute(view.binding(PHX_PROGRESS)),\n        ref: fileEl.getAttribute(PHX_UPLOAD_REF),\n        entry_ref: entryRef,\n        progress,\n        cid: view.targetComponentID(fileEl.form, targetCtx)\n      }).then(() => onReply()).catch((error) => logError(\"Failed to push file progress\", error));\n    });\n  }\n  pushInput(inputEl, targetCtx, forceCid, phxEvent, opts, callback) {\n    if (!inputEl.form) {\n      throw new Error(\"form events require the input to be inside a form\");\n    }\n    let uploads;\n    const cid = isCid(forceCid) ? forceCid : this.targetComponentID(inputEl.form, targetCtx, opts);\n    const refGenerator = (maybePayload) => {\n      return this.putRef(\n        [\n          { el: inputEl, loading: true, lock: true },\n          { el: inputEl.form, loading: true, lock: true }\n        ],\n        phxEvent,\n        \"change\",\n        { ...opts, payload: maybePayload?.payload }\n      );\n    };\n    let formData;\n    const meta = this.extractMeta(inputEl.form, {}, opts.value);\n    const serializeOpts = {};\n    if (inputEl instanceof HTMLButtonElement) {\n      serializeOpts.submitter = inputEl;\n    }\n    if (inputEl.getAttribute(this.binding(\"change\"))) {\n      formData = this.serializeForm(inputEl.form, serializeOpts, [\n        inputEl.name\n      ]);\n    } else {\n      formData = this.serializeForm(inputEl.form, serializeOpts);\n    }\n    if (dom_default.isUploadInput(inputEl) && inputEl.files && inputEl.files.length > 0) {\n      LiveUploader.trackFiles(inputEl, Array.from(inputEl.files));\n    }\n    uploads = LiveUploader.serializeUploads(inputEl);\n    const event = {\n      type: \"form\",\n      event: phxEvent,\n      value: formData,\n      meta: {\n        // no target was implicitly sent as \"undefined\" in LV <= 1.0.5, therefore\n        // we have to keep it. In 1.0.6 we switched from passing meta as URL encoded data\n        // to passing it directly in the event, but the JSON encode would drop keys with\n        // undefined values.\n        _target: opts._target || \"undefined\",\n        ...meta\n      },\n      uploads,\n      cid\n    };\n    this.pushWithReply(refGenerator, \"event\", event).then(({ resp }) => {\n      if (dom_default.isUploadInput(inputEl) && dom_default.isAutoUpload(inputEl)) {\n        ElementRef.onUnlock(inputEl, () => {\n          if (LiveUploader.filesAwaitingPreflight(inputEl).length > 0) {\n            const [ref, _els] = refGenerator();\n            this.undoRefs(ref, phxEvent, [inputEl.form]);\n            this.uploadFiles(\n              inputEl.form,\n              phxEvent,\n              targetCtx,\n              ref,\n              cid,\n              (_uploads) => {\n                callback && callback(resp);\n                this.triggerAwaitingSubmit(inputEl.form, phxEvent);\n                this.undoRefs(ref, phxEvent);\n              }\n            );\n          }\n        });\n      } else {\n        callback && callback(resp);\n      }\n    }).catch((error) => logError(\"Failed to push input event\", error));\n  }\n  triggerAwaitingSubmit(formEl, phxEvent) {\n    const awaitingSubmit = this.getScheduledSubmit(formEl);\n    if (awaitingSubmit) {\n      const [_el, _ref, _opts, callback] = awaitingSubmit;\n      this.cancelSubmit(formEl, phxEvent);\n      callback();\n    }\n  }\n  getScheduledSubmit(formEl) {\n    return this.formSubmits.find(\n      ([el, _ref, _opts, _callback]) => el.isSameNode(formEl)\n    );\n  }\n  scheduleSubmit(formEl, ref, opts, callback) {\n    if (this.getScheduledSubmit(formEl)) {\n      return true;\n    }\n    this.formSubmits.push([formEl, ref, opts, callback]);\n  }\n  cancelSubmit(formEl, phxEvent) {\n    this.formSubmits = this.formSubmits.filter(\n      ([el, ref, _opts, _callback]) => {\n        if (el.isSameNode(formEl)) {\n          this.undoRefs(ref, phxEvent);\n          return false;\n        } else {\n          return true;\n        }\n      }\n    );\n  }\n  disableForm(formEl, phxEvent, opts = {}) {\n    const filterIgnored = (el) => {\n      const userIgnored = closestPhxBinding(\n        el,\n        `${this.binding(PHX_UPDATE)}=ignore`,\n        el.form\n      );\n      return !(userIgnored || closestPhxBinding(el, \"data-phx-update=ignore\", el.form));\n    };\n    const filterDisables = (el) => {\n      return el.hasAttribute(this.binding(PHX_DISABLE_WITH));\n    };\n    const filterButton = (el) => el.tagName == \"BUTTON\";\n    const filterInput = (el) => [\"INPUT\", \"TEXTAREA\", \"SELECT\"].includes(el.tagName);\n    const formElements = Array.from(formEl.elements);\n    const disables = formElements.filter(filterDisables);\n    const buttons = formElements.filter(filterButton).filter(filterIgnored);\n    const inputs = formElements.filter(filterInput).filter(filterIgnored);\n    buttons.forEach((button) => {\n      button.setAttribute(PHX_DISABLED, button.disabled);\n      button.disabled = true;\n    });\n    inputs.forEach((input) => {\n      input.setAttribute(PHX_READONLY, input.readOnly);\n      input.readOnly = true;\n      if (input.files) {\n        input.setAttribute(PHX_DISABLED, input.disabled);\n        input.disabled = true;\n      }\n    });\n    const formEls = disables.concat(buttons).concat(inputs).map((el) => {\n      return { el, loading: true, lock: true };\n    });\n    const els = [{ el: formEl, loading: true, lock: false }].concat(formEls).reverse();\n    return this.putRef(els, phxEvent, \"submit\", opts);\n  }\n  pushFormSubmit(formEl, targetCtx, phxEvent, submitter, opts, onReply) {\n    const refGenerator = (maybePayload) => this.disableForm(formEl, phxEvent, {\n      ...opts,\n      form: formEl,\n      payload: maybePayload?.payload,\n      submitter\n    });\n    dom_default.putPrivate(formEl, \"submitter\", submitter);\n    const cid = this.targetComponentID(formEl, targetCtx);\n    if (LiveUploader.hasUploadsInProgress(formEl)) {\n      const [ref, _els] = refGenerator();\n      const push = () => this.pushFormSubmit(\n        formEl,\n        targetCtx,\n        phxEvent,\n        submitter,\n        opts,\n        onReply\n      );\n      return this.scheduleSubmit(formEl, ref, opts, push);\n    } else if (LiveUploader.inputsAwaitingPreflight(formEl).length > 0) {\n      const [ref, els] = refGenerator();\n      const proxyRefGen = () => [ref, els, opts];\n      this.uploadFiles(formEl, phxEvent, targetCtx, ref, cid, (_uploads) => {\n        if (LiveUploader.inputsAwaitingPreflight(formEl).length > 0) {\n          return this.undoRefs(ref, phxEvent);\n        }\n        const meta = this.extractMeta(formEl, {}, opts.value);\n        const formData = this.serializeForm(formEl, { submitter });\n        this.pushWithReply(proxyRefGen, \"event\", {\n          type: \"form\",\n          event: phxEvent,\n          value: formData,\n          meta,\n          cid\n        }).then(({ resp }) => onReply(resp)).catch((error) => logError(\"Failed to push form submit\", error));\n      });\n    } else if (!(formEl.hasAttribute(PHX_REF_SRC) && formEl.classList.contains(\"phx-submit-loading\"))) {\n      const meta = this.extractMeta(formEl, {}, opts.value);\n      const formData = this.serializeForm(formEl, { submitter });\n      this.pushWithReply(refGenerator, \"event\", {\n        type: \"form\",\n        event: phxEvent,\n        value: formData,\n        meta,\n        cid\n      }).then(({ resp }) => onReply(resp)).catch((error) => logError(\"Failed to push form submit\", error));\n    }\n  }\n  uploadFiles(formEl, phxEvent, targetCtx, ref, cid, onComplete) {\n    const joinCountAtUpload = this.joinCount;\n    const inputEls = LiveUploader.activeFileInputs(formEl);\n    let numFileInputsInProgress = inputEls.length;\n    inputEls.forEach((inputEl) => {\n      const uploader = new LiveUploader(inputEl, this, () => {\n        numFileInputsInProgress--;\n        if (numFileInputsInProgress === 0) {\n          onComplete();\n        }\n      });\n      const entries = uploader.entries().map((entry) => entry.toPreflightPayload());\n      if (entries.length === 0) {\n        numFileInputsInProgress--;\n        return;\n      }\n      const payload = {\n        ref: inputEl.getAttribute(PHX_UPLOAD_REF),\n        entries,\n        cid: this.targetComponentID(inputEl.form, targetCtx)\n      };\n      this.log(\"upload\", () => [\"sending preflight request\", payload]);\n      this.pushWithReply(null, \"allow_upload\", payload).then(({ resp }) => {\n        this.log(\"upload\", () => [\"got preflight response\", resp]);\n        uploader.entries().forEach((entry) => {\n          if (resp.entries && !resp.entries[entry.ref]) {\n            this.handleFailedEntryPreflight(\n              entry.ref,\n              \"failed preflight\",\n              uploader\n            );\n          }\n        });\n        if (resp.error || Object.keys(resp.entries).length === 0) {\n          this.undoRefs(ref, phxEvent);\n          const errors = resp.error || [];\n          errors.map(([entry_ref, reason]) => {\n            this.handleFailedEntryPreflight(entry_ref, reason, uploader);\n          });\n        } else {\n          const onError = (callback) => {\n            this.channel.onError(() => {\n              if (this.joinCount === joinCountAtUpload) {\n                callback();\n              }\n            });\n          };\n          uploader.initAdapterUpload(resp, onError, this.liveSocket);\n        }\n      }).catch((error) => logError(\"Failed to push upload\", error));\n    });\n  }\n  handleFailedEntryPreflight(uploadRef, reason, uploader) {\n    if (uploader.isAutoUpload()) {\n      const entry = uploader.entries().find((entry2) => entry2.ref === uploadRef.toString());\n      if (entry) {\n        entry.cancel();\n      }\n    } else {\n      uploader.entries().map((entry) => entry.cancel());\n    }\n    this.log(\"upload\", () => [`error for entry ${uploadRef}`, reason]);\n  }\n  dispatchUploads(targetCtx, name, filesOrBlobs) {\n    const targetElement = this.targetCtxElement(targetCtx) || this.el;\n    const inputs = dom_default.findUploadInputs(targetElement).filter(\n      (el) => el.name === name\n    );\n    if (inputs.length === 0) {\n      logError(`no live file inputs found matching the name \"${name}\"`);\n    } else if (inputs.length > 1) {\n      logError(`duplicate live file inputs found matching the name \"${name}\"`);\n    } else {\n      dom_default.dispatchEvent(inputs[0], PHX_TRACK_UPLOADS, {\n        detail: { files: filesOrBlobs }\n      });\n    }\n  }\n  targetCtxElement(targetCtx) {\n    if (isCid(targetCtx)) {\n      const [target] = dom_default.findComponentNodeList(this.id, targetCtx);\n      return target;\n    } else if (targetCtx) {\n      return targetCtx;\n    } else {\n      return null;\n    }\n  }\n  pushFormRecovery(oldForm, newForm, templateDom, callback) {\n    const phxChange = this.binding(\"change\");\n    const phxTarget = newForm.getAttribute(this.binding(\"target\")) || newForm;\n    const phxEvent = newForm.getAttribute(this.binding(PHX_AUTO_RECOVER)) || newForm.getAttribute(this.binding(\"change\"));\n    const inputs = Array.from(oldForm.elements).filter(\n      (el) => dom_default.isFormInput(el) && el.name && !el.hasAttribute(phxChange)\n    );\n    if (inputs.length === 0) {\n      callback();\n      return;\n    }\n    inputs.forEach(\n      (input2) => input2.hasAttribute(PHX_UPLOAD_REF) && LiveUploader.clearFiles(input2)\n    );\n    const input = inputs.find((el) => el.type !== \"hidden\") || inputs[0];\n    let pending = 0;\n    this.withinTargets(\n      phxTarget,\n      (targetView, targetCtx) => {\n        const cid = this.targetComponentID(newForm, targetCtx);\n        pending++;\n        let e = new CustomEvent(\"phx:form-recovery\", {\n          detail: { sourceElement: oldForm }\n        });\n        js_default.exec(e, \"change\", phxEvent, this, input, [\n          \"push\",\n          {\n            _target: input.name,\n            targetView,\n            targetCtx,\n            newCid: cid,\n            callback: () => {\n              pending--;\n              if (pending === 0) {\n                callback();\n              }\n            }\n          }\n        ]);\n      },\n      templateDom\n    );\n  }\n  pushLinkPatch(e, href, targetEl, callback) {\n    const linkRef = this.liveSocket.setPendingLink(href);\n    const loading = e.isTrusted && e.type !== \"popstate\";\n    const refGen = targetEl ? () => this.putRef(\n      [{ el: targetEl, loading, lock: true }],\n      null,\n      \"click\"\n    ) : null;\n    const fallback = () => this.liveSocket.redirect(window.location.href);\n    const url = href.startsWith(\"/\") ? `${location.protocol}//${location.host}${href}` : href;\n    this.pushWithReply(refGen, \"live_patch\", { url }).then(\n      ({ resp }) => {\n        this.liveSocket.requestDOMUpdate(() => {\n          if (resp.link_redirect) {\n            this.liveSocket.replaceMain(href, null, callback, linkRef);\n          } else if (resp.redirect) {\n            return;\n          } else {\n            if (this.liveSocket.commitPendingLink(linkRef)) {\n              this.href = href;\n            }\n            this.applyPendingUpdates();\n            callback && callback(linkRef);\n          }\n        });\n      },\n      ({ error: _error, timeout: _timeout }) => fallback()\n    );\n  }\n  getFormsForRecovery() {\n    if (this.joinCount === 0) {\n      return {};\n    }\n    const phxChange = this.binding(\"change\");\n    return dom_default.all(\n      document,\n      `#${CSS.escape(this.id)} form[${phxChange}], [${PHX_TELEPORTED_REF}=\"${CSS.escape(this.id)}\"] form[${phxChange}]`\n    ).filter((form) => form.id).filter((form) => form.elements.length > 0).filter(\n      (form) => form.getAttribute(this.binding(PHX_AUTO_RECOVER)) !== \"ignore\"\n    ).map((form) => {\n      const clonedForm = form.cloneNode(true);\n      morphdom_esm_default(clonedForm, form, {\n        onBeforeElUpdated: (fromEl, toEl) => {\n          dom_default.copyPrivates(fromEl, toEl);\n          if (fromEl.getAttribute(\"form\") === form.id) {\n            fromEl.parentNode.removeChild(fromEl);\n            return false;\n          }\n          return true;\n        }\n      });\n      const externalElements = document.querySelectorAll(\n        `[form=\"${CSS.escape(form.id)}\"]`\n      );\n      Array.from(externalElements).forEach((el) => {\n        const clonedEl = (\n          /** @type {HTMLElement} */\n          el.cloneNode(true)\n        );\n        morphdom_esm_default(clonedEl, el);\n        dom_default.copyPrivates(clonedEl, el);\n        clonedEl.removeAttribute(\"form\");\n        clonedForm.appendChild(clonedEl);\n      });\n      return clonedForm;\n    }).reduce((acc, form) => {\n      acc[form.id] = form;\n      return acc;\n    }, {});\n  }\n  maybePushComponentsDestroyed(destroyedCIDs) {\n    let willDestroyCIDs = destroyedCIDs.filter((cid) => {\n      return dom_default.findComponentNodeList(this.id, cid).length === 0;\n    });\n    const onError = (error) => {\n      if (!this.isDestroyed()) {\n        logError(\"Failed to push components destroyed\", error);\n      }\n    };\n    if (willDestroyCIDs.length > 0) {\n      willDestroyCIDs.forEach((cid) => this.rendered.resetRender(cid));\n      this.pushWithReply(null, \"cids_will_destroy\", { cids: willDestroyCIDs }).then(() => {\n        this.liveSocket.requestDOMUpdate(() => {\n          let completelyDestroyCIDs = willDestroyCIDs.filter((cid) => {\n            return dom_default.findComponentNodeList(this.id, cid).length === 0;\n          });\n          if (completelyDestroyCIDs.length > 0) {\n            this.pushWithReply(null, \"cids_destroyed\", {\n              cids: completelyDestroyCIDs\n            }).then(({ resp }) => {\n              this.rendered.pruneCIDs(resp.cids);\n            }).catch(onError);\n          }\n        });\n      }).catch(onError);\n    }\n  }\n  ownsElement(el) {\n    let parentViewEl = dom_default.closestViewEl(el);\n    return el.getAttribute(PHX_PARENT_ID) === this.id || parentViewEl && parentViewEl.id === this.id || !parentViewEl && this.isDead;\n  }\n  submitForm(form, targetCtx, phxEvent, submitter, opts = {}) {\n    dom_default.putPrivate(form, PHX_HAS_SUBMITTED, true);\n    const inputs = Array.from(form.elements);\n    inputs.forEach((input) => dom_default.putPrivate(input, PHX_HAS_SUBMITTED, true));\n    this.liveSocket.blurActiveElement(this);\n    this.pushFormSubmit(form, targetCtx, phxEvent, submitter, opts, () => {\n      this.liveSocket.restorePreviouslyActiveFocus();\n    });\n  }\n  binding(kind) {\n    return this.liveSocket.binding(kind);\n  }\n  // phx-portal\n  pushPortalElementId(id) {\n    this.portalElementIds.add(id);\n  }\n  dropPortalElementId(id) {\n    this.portalElementIds.delete(id);\n  }\n  destroyPortalElements() {\n    if (!this.liveSocket.unloaded) {\n      this.portalElementIds.forEach((id) => {\n        const el = document.getElementById(id);\n        if (el) {\n          el.remove();\n        }\n      });\n    }\n  }\n};\n\n// js/phoenix_live_view/live_socket.js\nvar isUsedInput = (el) => dom_default.isUsedInput(el);\nvar LiveSocket = class {\n  constructor(url, phxSocket, opts = {}) {\n    this.unloaded = false;\n    if (!phxSocket || phxSocket.constructor.name === \"Object\") {\n      throw new Error(`\n      a phoenix Socket must be provided as the second argument to the LiveSocket constructor. For example:\n\n          import {Socket} from \"phoenix\"\n          import {LiveSocket} from \"phoenix_live_view\"\n          let liveSocket = new LiveSocket(\"/live\", Socket, {...})\n      `);\n    }\n    this.socket = new phxSocket(url, opts);\n    this.bindingPrefix = opts.bindingPrefix || BINDING_PREFIX;\n    this.opts = opts;\n    this.params = closure(opts.params || {});\n    this.viewLogger = opts.viewLogger;\n    this.metadataCallbacks = opts.metadata || {};\n    this.defaults = Object.assign(clone(DEFAULTS), opts.defaults || {});\n    this.prevActive = null;\n    this.silenced = false;\n    this.main = null;\n    this.outgoingMainEl = null;\n    this.clickStartedAtTarget = null;\n    this.linkRef = 1;\n    this.roots = {};\n    this.href = window.location.href;\n    this.pendingLink = null;\n    this.currentLocation = clone(window.location);\n    this.hooks = opts.hooks || {};\n    this.uploaders = opts.uploaders || {};\n    this.loaderTimeout = opts.loaderTimeout || LOADER_TIMEOUT;\n    this.disconnectedTimeout = opts.disconnectedTimeout || DISCONNECTED_TIMEOUT;\n    this.reloadWithJitterTimer = null;\n    this.maxReloads = opts.maxReloads || MAX_RELOADS;\n    this.reloadJitterMin = opts.reloadJitterMin || RELOAD_JITTER_MIN;\n    this.reloadJitterMax = opts.reloadJitterMax || RELOAD_JITTER_MAX;\n    this.failsafeJitter = opts.failsafeJitter || FAILSAFE_JITTER;\n    this.localStorage = opts.localStorage || window.localStorage;\n    this.sessionStorage = opts.sessionStorage || window.sessionStorage;\n    this.boundTopLevelEvents = false;\n    this.boundEventNames = /* @__PURE__ */ new Set();\n    this.blockPhxChangeWhileComposing = opts.blockPhxChangeWhileComposing || false;\n    this.serverCloseRef = null;\n    this.domCallbacks = Object.assign(\n      {\n        jsQuerySelectorAll: null,\n        onPatchStart: closure(),\n        onPatchEnd: closure(),\n        onNodeAdded: closure(),\n        onBeforeElUpdated: closure()\n      },\n      opts.dom || {}\n    );\n    this.transitions = new TransitionSet();\n    this.currentHistoryPosition = parseInt(this.sessionStorage.getItem(PHX_LV_HISTORY_POSITION)) || 0;\n    window.addEventListener(\"pagehide\", (_e) => {\n      this.unloaded = true;\n    });\n    this.socket.onOpen(() => {\n      if (this.isUnloaded()) {\n        window.location.reload();\n      }\n    });\n  }\n  // public\n  version() {\n    return \"1.2.0-dev\";\n  }\n  isProfileEnabled() {\n    return this.sessionStorage.getItem(PHX_LV_PROFILE) === \"true\";\n  }\n  isDebugEnabled() {\n    return this.sessionStorage.getItem(PHX_LV_DEBUG) === \"true\";\n  }\n  isDebugDisabled() {\n    return this.sessionStorage.getItem(PHX_LV_DEBUG) === \"false\";\n  }\n  enableDebug() {\n    this.sessionStorage.setItem(PHX_LV_DEBUG, \"true\");\n  }\n  enableProfiling() {\n    this.sessionStorage.setItem(PHX_LV_PROFILE, \"true\");\n  }\n  disableDebug() {\n    this.sessionStorage.setItem(PHX_LV_DEBUG, \"false\");\n  }\n  disableProfiling() {\n    this.sessionStorage.removeItem(PHX_LV_PROFILE);\n  }\n  enableLatencySim(upperBoundMs) {\n    this.enableDebug();\n    console.log(\n      \"latency simulator enabled for the duration of this browser session. Call disableLatencySim() to disable\"\n    );\n    this.sessionStorage.setItem(PHX_LV_LATENCY_SIM, upperBoundMs);\n  }\n  disableLatencySim() {\n    this.sessionStorage.removeItem(PHX_LV_LATENCY_SIM);\n  }\n  getLatencySim() {\n    const str = this.sessionStorage.getItem(PHX_LV_LATENCY_SIM);\n    return str ? parseInt(str) : null;\n  }\n  getSocket() {\n    return this.socket;\n  }\n  connect() {\n    if (window.location.hostname === \"localhost\" && !this.isDebugDisabled()) {\n      this.enableDebug();\n    }\n    const doConnect = () => {\n      this.resetReloadStatus();\n      if (this.joinRootViews()) {\n        this.bindTopLevelEvents();\n        this.socket.connect();\n      } else if (this.main) {\n        this.socket.connect();\n      } else {\n        this.bindTopLevelEvents({ dead: true });\n      }\n      this.joinDeadView();\n    };\n    if ([\"complete\", \"loaded\", \"interactive\"].indexOf(document.readyState) >= 0) {\n      doConnect();\n    } else {\n      document.addEventListener(\"DOMContentLoaded\", () => doConnect());\n    }\n  }\n  disconnect(callback) {\n    clearTimeout(this.reloadWithJitterTimer);\n    if (this.serverCloseRef) {\n      this.socket.off(this.serverCloseRef);\n      this.serverCloseRef = null;\n    }\n    this.socket.disconnect(callback);\n  }\n  replaceTransport(transport) {\n    clearTimeout(this.reloadWithJitterTimer);\n    this.socket.replaceTransport(transport);\n    this.connect();\n  }\n  /**\n   * @param {HTMLElement} el\n   * @param {import(\"./js_commands\").EncodedJS} encodedJS\n   * @param {string | null} [eventType]\n   */\n  execJS(el, encodedJS, eventType = null) {\n    const e = new CustomEvent(\"phx:exec\", { detail: { sourceElement: el } });\n    this.owner(el, (view) => js_default.exec(e, eventType, encodedJS, view, el));\n  }\n  /**\n   * Returns an object with methods to manipulate the DOM and execute JavaScript.\n   * The applied changes integrate with server DOM patching.\n   *\n   * @returns {import(\"./js_commands\").LiveSocketJSCommands}\n   */\n  js() {\n    return js_commands_default(this, \"js\");\n  }\n  // private\n  unload() {\n    if (this.unloaded) {\n      return;\n    }\n    if (this.main && this.isConnected()) {\n      this.log(this.main, \"socket\", () => [\"disconnect for page nav\"]);\n    }\n    this.unloaded = true;\n    this.destroyAllViews();\n    this.disconnect();\n  }\n  triggerDOM(kind, args) {\n    this.domCallbacks[kind](...args);\n  }\n  time(name, func) {\n    if (!this.isProfileEnabled() || !console.time) {\n      return func();\n    }\n    console.time(name);\n    const result = func();\n    console.timeEnd(name);\n    return result;\n  }\n  log(view, kind, msgCallback) {\n    if (this.viewLogger) {\n      const [msg, obj] = msgCallback();\n      this.viewLogger(view, kind, msg, obj);\n    } else if (this.isDebugEnabled()) {\n      const [msg, obj] = msgCallback();\n      debug(view, kind, msg, obj);\n    }\n  }\n  requestDOMUpdate(callback) {\n    this.transitions.after(callback);\n  }\n  asyncTransition(promise) {\n    this.transitions.addAsyncTransition(promise);\n  }\n  transition(time, onStart, onDone = function() {\n  }) {\n    this.transitions.addTransition(time, onStart, onDone);\n  }\n  onChannel(channel, event, cb) {\n    channel.on(event, (data) => {\n      const latency = this.getLatencySim();\n      if (!latency) {\n        cb(data);\n      } else {\n        setTimeout(() => cb(data), latency);\n      }\n    });\n  }\n  reloadWithJitter(view, log) {\n    clearTimeout(this.reloadWithJitterTimer);\n    this.disconnect();\n    const minMs = this.reloadJitterMin;\n    const maxMs = this.reloadJitterMax;\n    let afterMs = Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs;\n    const tries = browser_default.updateLocal(\n      this.localStorage,\n      window.location.pathname,\n      CONSECUTIVE_RELOADS,\n      0,\n      (count) => count + 1\n    );\n    if (tries >= this.maxReloads) {\n      afterMs = this.failsafeJitter;\n    }\n    this.reloadWithJitterTimer = setTimeout(() => {\n      if (view.isDestroyed() || view.isConnected()) {\n        return;\n      }\n      view.destroy();\n      log ? log() : this.log(view, \"join\", () => [\n        `encountered ${tries} consecutive reloads`\n      ]);\n      if (tries >= this.maxReloads) {\n        this.log(view, \"join\", () => [\n          `exceeded ${this.maxReloads} consecutive reloads. Entering failsafe mode`\n        ]);\n      }\n      if (this.hasPendingLink()) {\n        window.location = this.pendingLink;\n      } else {\n        window.location.reload();\n      }\n    }, afterMs);\n  }\n  getHookDefinition(name) {\n    if (!name) {\n      return;\n    }\n    return this.maybeInternalHook(name) || this.hooks[name] || this.maybeRuntimeHook(name);\n  }\n  maybeInternalHook(name) {\n    return name && name.startsWith(\"Phoenix.\") && hooks_default[name.split(\".\")[1]];\n  }\n  maybeRuntimeHook(name) {\n    const runtimeHook = document.querySelector(\n      `script[${PHX_RUNTIME_HOOK}=\"${CSS.escape(name)}\"]`\n    );\n    if (!runtimeHook) {\n      return;\n    }\n    let callbacks = window[`phx_hook_${name}`];\n    if (!callbacks || typeof callbacks !== \"function\") {\n      logError(\"a runtime hook must be a function\", runtimeHook);\n      return;\n    }\n    const hookDefiniton = callbacks();\n    if (hookDefiniton && (typeof hookDefiniton === \"object\" || typeof hookDefiniton === \"function\")) {\n      return hookDefiniton;\n    }\n    logError(\n      \"runtime hook must return an object with hook callbacks or an instance of ViewHook\",\n      runtimeHook\n    );\n  }\n  isUnloaded() {\n    return this.unloaded;\n  }\n  isConnected() {\n    return this.socket.isConnected();\n  }\n  getBindingPrefix() {\n    return this.bindingPrefix;\n  }\n  binding(kind) {\n    return `${this.getBindingPrefix()}${kind}`;\n  }\n  channel(topic, params) {\n    return this.socket.channel(topic, params);\n  }\n  joinDeadView() {\n    const body = document.body;\n    if (body && !this.isPhxView(body) && !this.isPhxView(document.firstElementChild)) {\n      const view = this.newRootView(body);\n      view.setHref(this.getHref());\n      view.joinDead();\n      if (!this.main) {\n        this.main = view;\n      }\n      window.requestAnimationFrame(() => {\n        view.execNewMounted();\n        this.maybeScroll(history.state?.scroll);\n      });\n    }\n  }\n  joinRootViews() {\n    let rootsFound = false;\n    dom_default.all(\n      document,\n      `${PHX_VIEW_SELECTOR}:not([${PHX_PARENT_ID}])`,\n      (rootEl) => {\n        if (!this.getRootById(rootEl.id)) {\n          const view = this.newRootView(rootEl);\n          if (!dom_default.isPhxSticky(rootEl)) {\n            view.setHref(this.getHref());\n          }\n          view.join();\n          if (rootEl.hasAttribute(PHX_MAIN)) {\n            this.main = view;\n          }\n        }\n        rootsFound = true;\n      }\n    );\n    return rootsFound;\n  }\n  redirect(to, flash, reloadToken) {\n    if (reloadToken) {\n      browser_default.setCookie(PHX_RELOAD_STATUS, reloadToken, 60);\n    }\n    this.unload();\n    browser_default.redirect(to, flash);\n  }\n  replaceMain(href, flash, callback = null, linkRef = this.setPendingLink(href)) {\n    const liveReferer = this.currentLocation.href;\n    this.outgoingMainEl = this.outgoingMainEl || this.main.el;\n    const stickies = dom_default.findPhxSticky(document) || [];\n    const removeEls = dom_default.all(\n      this.outgoingMainEl,\n      `[${this.binding(\"remove\")}]`\n    ).filter((el) => !dom_default.isChildOfAny(el, stickies));\n    const newMainEl = dom_default.cloneNode(this.outgoingMainEl, \"\");\n    this.main.showLoader(this.loaderTimeout);\n    this.main.destroy();\n    this.main = this.newRootView(newMainEl, flash, liveReferer);\n    this.main.setRedirect(href);\n    this.transitionRemoves(removeEls);\n    this.main.join((joinCount, onDone) => {\n      if (joinCount === 1 && this.commitPendingLink(linkRef)) {\n        this.requestDOMUpdate(() => {\n          removeEls.forEach((el) => el.remove());\n          stickies.forEach((el) => newMainEl.appendChild(el));\n          this.outgoingMainEl.replaceWith(newMainEl);\n          this.outgoingMainEl = null;\n          callback && callback(linkRef);\n          onDone();\n        });\n      }\n    });\n  }\n  transitionRemoves(elements, callback) {\n    const removeAttr = this.binding(\"remove\");\n    const silenceEvents = (e) => {\n      e.preventDefault();\n      e.stopImmediatePropagation();\n    };\n    elements.forEach((el) => {\n      for (const event of this.boundEventNames) {\n        el.addEventListener(event, silenceEvents, true);\n      }\n      this.execJS(el, el.getAttribute(removeAttr), \"remove\");\n    });\n    this.requestDOMUpdate(() => {\n      elements.forEach((el) => {\n        for (const event of this.boundEventNames) {\n          el.removeEventListener(event, silenceEvents, true);\n        }\n      });\n      callback && callback();\n    });\n  }\n  isPhxView(el) {\n    return el.getAttribute && el.getAttribute(PHX_SESSION) !== null;\n  }\n  newRootView(el, flash, liveReferer) {\n    const view = new View(el, this, null, flash, liveReferer);\n    this.roots[view.id] = view;\n    return view;\n  }\n  owner(childEl, callback) {\n    let view;\n    const viewEl = dom_default.closestViewEl(childEl);\n    if (viewEl) {\n      view = this.getViewByEl(viewEl);\n    } else {\n      if (!childEl.isConnected) {\n        return null;\n      }\n      view = this.main;\n    }\n    return view && callback ? callback(view) : view;\n  }\n  withinOwners(childEl, callback) {\n    this.owner(childEl, (view) => callback(view, childEl));\n  }\n  getViewByEl(el) {\n    const rootId = el.getAttribute(PHX_ROOT_ID);\n    return maybe(\n      this.getRootById(rootId),\n      (root) => root.getDescendentByEl(el)\n    );\n  }\n  getRootById(id) {\n    return this.roots[id];\n  }\n  destroyAllViews() {\n    for (const id in this.roots) {\n      this.roots[id].destroy();\n      delete this.roots[id];\n    }\n    this.main = null;\n  }\n  destroyViewByEl(el) {\n    const root = this.getRootById(el.getAttribute(PHX_ROOT_ID));\n    if (root && root.id === el.id) {\n      root.destroy();\n      delete this.roots[root.id];\n    } else if (root) {\n      root.destroyDescendent(el.id);\n    }\n  }\n  getActiveElement() {\n    return document.activeElement;\n  }\n  dropActiveElement(view) {\n    if (this.prevActive && view.ownsElement(this.prevActive)) {\n      this.prevActive = null;\n    }\n  }\n  restorePreviouslyActiveFocus() {\n    if (this.prevActive && this.prevActive !== document.body && this.prevActive instanceof HTMLElement) {\n      this.prevActive.focus();\n    }\n  }\n  blurActiveElement() {\n    this.prevActive = this.getActiveElement();\n    if (this.prevActive !== document.body && this.prevActive instanceof HTMLElement) {\n      this.prevActive.blur();\n    }\n  }\n  /**\n   * @param {{dead?: boolean}} [options={}]\n   */\n  bindTopLevelEvents({ dead } = {}) {\n    if (this.boundTopLevelEvents) {\n      return;\n    }\n    this.boundTopLevelEvents = true;\n    this.serverCloseRef = this.socket.onClose((event) => {\n      if (event && event.code === 1e3 && this.main) {\n        return this.reloadWithJitter(this.main);\n      }\n    });\n    document.body.addEventListener(\"click\", function() {\n    });\n    window.addEventListener(\n      \"pageshow\",\n      (e) => {\n        if (e.persisted) {\n          this.getSocket().disconnect();\n          this.withPageLoading({ to: window.location.href, kind: \"redirect\" });\n          window.location.reload();\n        }\n      },\n      true\n    );\n    if (!dead) {\n      this.bindNav();\n    }\n    this.bindClicks();\n    if (!dead) {\n      this.bindForms();\n    }\n    this.bind(\n      { keyup: \"keyup\", keydown: \"keydown\" },\n      (e, type, view, targetEl, phxEvent, _phxTarget) => {\n        const matchKey = targetEl.getAttribute(this.binding(PHX_KEY));\n        const pressedKey = e.key && e.key.toLowerCase();\n        if (matchKey && matchKey.toLowerCase() !== pressedKey) {\n          return;\n        }\n        const data = { key: e.key, ...this.eventMeta(type, e, targetEl) };\n        js_default.exec(e, type, phxEvent, view, targetEl, [\"push\", { data }]);\n      }\n    );\n    this.bind(\n      { blur: \"focusout\", focus: \"focusin\" },\n      (e, type, view, targetEl, phxEvent, phxTarget) => {\n        if (!phxTarget) {\n          const data = { key: e.key, ...this.eventMeta(type, e, targetEl) };\n          js_default.exec(e, type, phxEvent, view, targetEl, [\"push\", { data }]);\n        }\n      }\n    );\n    this.bind(\n      { blur: \"blur\", focus: \"focus\" },\n      (e, type, view, targetEl, phxEvent, phxTarget) => {\n        if (phxTarget === \"window\") {\n          const data = this.eventMeta(type, e, targetEl);\n          js_default.exec(e, type, phxEvent, view, targetEl, [\"push\", { data }]);\n        }\n      }\n    );\n    this.on(\"dragover\", (e) => e.preventDefault());\n    this.on(\"dragenter\", (e) => {\n      const dropzone = closestPhxBinding(\n        e.target,\n        this.binding(PHX_DROP_TARGET)\n      );\n      if (!dropzone || !(dropzone instanceof HTMLElement)) {\n        return;\n      }\n      if (eventContainsFiles(e)) {\n        this.js().addClass(dropzone, PHX_DROP_TARGET_ACTIVE_CLASS);\n      }\n    });\n    this.on(\"dragleave\", (e) => {\n      const dropzone = closestPhxBinding(\n        e.target,\n        this.binding(PHX_DROP_TARGET)\n      );\n      if (!dropzone || !(dropzone instanceof HTMLElement)) {\n        return;\n      }\n      const rect = dropzone.getBoundingClientRect();\n      if (e.clientX <= rect.left || e.clientX >= rect.right || e.clientY <= rect.top || e.clientY >= rect.bottom) {\n        this.js().removeClass(dropzone, PHX_DROP_TARGET_ACTIVE_CLASS);\n      }\n    });\n    this.on(\"drop\", (e) => {\n      e.preventDefault();\n      const dropzone = closestPhxBinding(\n        e.target,\n        this.binding(PHX_DROP_TARGET)\n      );\n      if (!dropzone || !(dropzone instanceof HTMLElement)) {\n        return;\n      }\n      this.js().removeClass(dropzone, PHX_DROP_TARGET_ACTIVE_CLASS);\n      const dropTargetId = dropzone.getAttribute(this.binding(PHX_DROP_TARGET));\n      const dropTarget = dropTargetId && document.getElementById(dropTargetId);\n      const files = Array.from(e.dataTransfer.files || []);\n      if (!dropTarget || !(dropTarget instanceof HTMLInputElement) || dropTarget.disabled || files.length === 0 || !(dropTarget.files instanceof FileList)) {\n        return;\n      }\n      LiveUploader.trackFiles(dropTarget, files, e.dataTransfer);\n      dropTarget.dispatchEvent(new Event(\"input\", { bubbles: true }));\n    });\n    this.on(PHX_TRACK_UPLOADS, (e) => {\n      const uploadTarget = e.target;\n      if (!dom_default.isUploadInput(uploadTarget)) {\n        return;\n      }\n      const files = Array.from(e.detail.files || []).filter(\n        (f) => f instanceof File || f instanceof Blob\n      );\n      LiveUploader.trackFiles(uploadTarget, files);\n      uploadTarget.dispatchEvent(new Event(\"input\", { bubbles: true }));\n    });\n  }\n  eventMeta(eventName, e, targetEl) {\n    const callback = this.metadataCallbacks[eventName];\n    return callback ? callback(e, targetEl) : {};\n  }\n  setPendingLink(href) {\n    this.linkRef++;\n    this.pendingLink = href;\n    this.resetReloadStatus();\n    return this.linkRef;\n  }\n  // anytime we are navigating or connecting, drop reload cookie in case\n  // we issue the cookie but the next request was interrupted and the server never dropped it\n  resetReloadStatus() {\n    browser_default.deleteCookie(PHX_RELOAD_STATUS);\n  }\n  commitPendingLink(linkRef) {\n    if (this.linkRef !== linkRef) {\n      return false;\n    } else {\n      this.href = this.pendingLink;\n      this.pendingLink = null;\n      return true;\n    }\n  }\n  getHref() {\n    return this.href;\n  }\n  hasPendingLink() {\n    return !!this.pendingLink;\n  }\n  bind(events, callback) {\n    for (const event in events) {\n      const browserEventName = events[event];\n      this.on(browserEventName, (e) => {\n        const binding = this.binding(event);\n        const windowBinding = this.binding(`window-${event}`);\n        const targetPhxEvent = e.target.getAttribute && e.target.getAttribute(binding);\n        if (targetPhxEvent) {\n          this.debounce(e.target, e, browserEventName, () => {\n            this.withinOwners(e.target, (view) => {\n              callback(e, event, view, e.target, targetPhxEvent, null);\n            });\n          });\n        } else {\n          dom_default.all(document, `[${windowBinding}]`, (el) => {\n            const phxEvent = el.getAttribute(windowBinding);\n            this.debounce(el, e, browserEventName, () => {\n              this.withinOwners(el, (view) => {\n                callback(e, event, view, el, phxEvent, \"window\");\n              });\n            });\n          });\n        }\n      });\n    }\n  }\n  bindClicks() {\n    this.on(\"mousedown\", (e) => this.clickStartedAtTarget = e.target);\n    this.bindClick(\"click\", \"click\");\n  }\n  bindClick(eventName, bindingName) {\n    const click = this.binding(bindingName);\n    window.addEventListener(\n      eventName,\n      (e) => {\n        let target = null;\n        if (e.detail === 0)\n          this.clickStartedAtTarget = e.target;\n        const clickStartedAtTarget = this.clickStartedAtTarget || e.target;\n        target = closestPhxBinding(e.target, click);\n        this.dispatchClickAway(e, clickStartedAtTarget);\n        this.clickStartedAtTarget = null;\n        const phxEvent = target && target.getAttribute(click);\n        if (!phxEvent) {\n          if (dom_default.isNewPageClick(e, window.location)) {\n            this.unload();\n          }\n          return;\n        }\n        if (target.getAttribute(\"href\") === \"#\") {\n          e.preventDefault();\n        }\n        if (target.hasAttribute(PHX_REF_SRC)) {\n          return;\n        }\n        this.debounce(target, e, \"click\", () => {\n          this.withinOwners(target, (view) => {\n            js_default.exec(e, \"click\", phxEvent, view, target, [\n              \"push\",\n              { data: this.eventMeta(\"click\", e, target) }\n            ]);\n          });\n        });\n      },\n      false\n    );\n  }\n  dispatchClickAway(e, clickStartedAt) {\n    const phxClickAway = this.binding(\"click-away\");\n    const portal = clickStartedAt.closest(`[${PHX_TELEPORTED_SRC}]`);\n    const portalStartedAt = portal && dom_default.byId(portal.getAttribute(PHX_TELEPORTED_SRC));\n    dom_default.all(document, `[${phxClickAway}]`, (el) => {\n      let startedAt = clickStartedAt;\n      if (portal && !portal.contains(el)) {\n        startedAt = portalStartedAt;\n      }\n      if (!(el.isSameNode(startedAt) || el.contains(startedAt) || // When clicking a link with custom method,\n      // phoenix_html triggers a click on a submit button\n      // of a hidden form appended to the body. For such cases\n      // where the clicked target is hidden, we skip click-away.\n      //\n      // Also, when we have a portal, we don't want to check the visibility\n      // of the portal source, as it's a <template> that is always not visible.\n      // Instead, check the visibility of the original click target.\n      !js_default.isVisible(clickStartedAt))) {\n        this.withinOwners(el, (view) => {\n          const phxEvent = el.getAttribute(phxClickAway);\n          if (js_default.isVisible(el) && js_default.isInViewport(el)) {\n            js_default.exec(e, \"click\", phxEvent, view, el, [\n              \"push\",\n              { data: this.eventMeta(\"click\", e, e.target) }\n            ]);\n          }\n        });\n      }\n    });\n  }\n  bindNav() {\n    if (!browser_default.canPushState()) {\n      return;\n    }\n    if (history.scrollRestoration) {\n      history.scrollRestoration = \"manual\";\n    }\n    let scrollTimer = null;\n    window.addEventListener(\"scroll\", (_e) => {\n      clearTimeout(scrollTimer);\n      scrollTimer = setTimeout(() => {\n        browser_default.updateCurrentState(\n          (state) => Object.assign(state, { scroll: window.scrollY })\n        );\n      }, 100);\n    });\n    window.addEventListener(\n      \"popstate\",\n      (event) => {\n        if (!this.registerNewLocation(window.location)) {\n          return;\n        }\n        const { type, backType, id, scroll, position } = event.state || {};\n        const href = window.location.href;\n        const isForward = position > this.currentHistoryPosition;\n        const navType = isForward ? type : backType || type;\n        this.currentHistoryPosition = position || 0;\n        this.sessionStorage.setItem(\n          PHX_LV_HISTORY_POSITION,\n          this.currentHistoryPosition.toString()\n        );\n        dom_default.dispatchEvent(window, \"phx:navigate\", {\n          detail: {\n            href,\n            patch: navType === \"patch\",\n            pop: true,\n            direction: isForward ? \"forward\" : \"backward\"\n          }\n        });\n        this.requestDOMUpdate(() => {\n          const callback = () => {\n            this.maybeScroll(scroll);\n          };\n          if (this.main.isConnected() && navType === \"patch\" && id === this.main.id) {\n            this.main.pushLinkPatch(event, href, null, callback);\n          } else {\n            this.replaceMain(href, null, callback);\n          }\n        });\n      },\n      false\n    );\n    window.addEventListener(\n      \"click\",\n      (e) => {\n        const target = closestPhxBinding(e.target, PHX_LIVE_LINK);\n        const type = target && target.getAttribute(PHX_LIVE_LINK);\n        if (!type || !this.isConnected() || !this.main || dom_default.wantsNewTab(e)) {\n          return;\n        }\n        const href = target.href instanceof SVGAnimatedString ? target.href.baseVal : target.href;\n        const linkState = target.getAttribute(PHX_LINK_STATE);\n        e.preventDefault();\n        e.stopImmediatePropagation();\n        if (this.pendingLink === href) {\n          return;\n        }\n        this.requestDOMUpdate(() => {\n          if (type === \"patch\") {\n            this.pushHistoryPatch(e, href, linkState, target);\n          } else if (type === \"redirect\") {\n            this.historyRedirect(e, href, linkState, null, target);\n          } else {\n            throw new Error(\n              `expected ${PHX_LIVE_LINK} to be \"patch\" or \"redirect\", got: ${type}`\n            );\n          }\n          const phxClick = target.getAttribute(this.binding(\"click\"));\n          if (phxClick) {\n            this.requestDOMUpdate(() => this.execJS(target, phxClick, \"click\"));\n          }\n        });\n      },\n      false\n    );\n  }\n  maybeScroll(scroll) {\n    if (typeof scroll === \"number\") {\n      requestAnimationFrame(() => {\n        window.scrollTo(0, scroll);\n      });\n    }\n  }\n  dispatchEvent(event, payload = {}) {\n    dom_default.dispatchEvent(window, `phx:${event}`, { detail: payload });\n  }\n  dispatchEvents(events) {\n    events.forEach(([event, payload]) => this.dispatchEvent(event, payload));\n  }\n  withPageLoading(info, callback) {\n    dom_default.dispatchEvent(window, \"phx:page-loading-start\", { detail: info });\n    const done = () => dom_default.dispatchEvent(window, \"phx:page-loading-stop\", { detail: info });\n    return callback ? callback(done) : done;\n  }\n  pushHistoryPatch(e, href, linkState, targetEl) {\n    if (!this.isConnected() || !this.main.isMain()) {\n      return browser_default.redirect(href);\n    }\n    this.withPageLoading({ to: href, kind: \"patch\" }, (done) => {\n      this.main.pushLinkPatch(e, href, targetEl, (linkRef) => {\n        this.historyPatch(href, linkState, linkRef);\n        done();\n      });\n    });\n  }\n  historyPatch(href, linkState, linkRef = this.setPendingLink(href)) {\n    if (!this.commitPendingLink(linkRef)) {\n      return;\n    }\n    this.currentHistoryPosition++;\n    this.sessionStorage.setItem(\n      PHX_LV_HISTORY_POSITION,\n      this.currentHistoryPosition.toString()\n    );\n    browser_default.updateCurrentState((state) => ({ ...state, backType: \"patch\" }));\n    browser_default.pushState(\n      linkState,\n      {\n        type: \"patch\",\n        id: this.main.id,\n        position: this.currentHistoryPosition\n      },\n      href\n    );\n    dom_default.dispatchEvent(window, \"phx:navigate\", {\n      detail: { patch: true, href, pop: false, direction: \"forward\" }\n    });\n    this.registerNewLocation(window.location);\n  }\n  historyRedirect(e, href, linkState, flash, targetEl) {\n    const clickLoading = targetEl && e.isTrusted && e.type !== \"popstate\";\n    if (clickLoading) {\n      targetEl.classList.add(\"phx-click-loading\");\n    }\n    if (!this.isConnected() || !this.main.isMain()) {\n      return browser_default.redirect(href, flash);\n    }\n    if (/^\\/$|^\\/[^\\/]+.*$/.test(href)) {\n      const { protocol, host } = window.location;\n      href = `${protocol}//${host}${href}`;\n    }\n    const scroll = window.scrollY;\n    this.withPageLoading({ to: href, kind: \"redirect\" }, (done) => {\n      this.replaceMain(href, flash, (linkRef) => {\n        if (linkRef === this.linkRef) {\n          this.currentHistoryPosition++;\n          this.sessionStorage.setItem(\n            PHX_LV_HISTORY_POSITION,\n            this.currentHistoryPosition.toString()\n          );\n          browser_default.updateCurrentState((state) => ({\n            ...state,\n            backType: \"redirect\"\n          }));\n          browser_default.pushState(\n            linkState,\n            {\n              type: \"redirect\",\n              id: this.main.id,\n              scroll,\n              position: this.currentHistoryPosition\n            },\n            href\n          );\n          dom_default.dispatchEvent(window, \"phx:navigate\", {\n            detail: { href, patch: false, pop: false, direction: \"forward\" }\n          });\n          this.registerNewLocation(window.location);\n        }\n        if (clickLoading) {\n          targetEl.classList.remove(\"phx-click-loading\");\n        }\n        done();\n      });\n    });\n  }\n  registerNewLocation(newLocation) {\n    const { pathname, search } = this.currentLocation;\n    if (pathname + search === newLocation.pathname + newLocation.search) {\n      return false;\n    } else {\n      this.currentLocation = clone(newLocation);\n      return true;\n    }\n  }\n  bindForms() {\n    let iterations = 0;\n    let externalFormSubmitted = false;\n    this.on(\"submit\", (e) => {\n      const phxSubmit = e.target.getAttribute(this.binding(\"submit\"));\n      const phxChange = e.target.getAttribute(this.binding(\"change\"));\n      if (!externalFormSubmitted && phxChange && !phxSubmit) {\n        externalFormSubmitted = true;\n        e.preventDefault();\n        this.withinOwners(e.target, (view) => {\n          view.disableForm(e.target);\n          window.requestAnimationFrame(() => {\n            if (dom_default.isUnloadableFormSubmit(e)) {\n              this.unload();\n            }\n            e.target.submit();\n          });\n        });\n      }\n    });\n    this.on(\"submit\", (e) => {\n      const phxEvent = e.target.getAttribute(this.binding(\"submit\"));\n      if (!phxEvent) {\n        if (dom_default.isUnloadableFormSubmit(e)) {\n          this.unload();\n        }\n        return;\n      }\n      e.preventDefault();\n      e.target.disabled = true;\n      this.withinOwners(e.target, (view) => {\n        js_default.exec(e, \"submit\", phxEvent, view, e.target, [\n          \"push\",\n          { submitter: e.submitter }\n        ]);\n      });\n    });\n    for (const type of [\"change\", \"input\"]) {\n      this.on(type, (e) => {\n        if (e instanceof CustomEvent && (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement || e.target instanceof HTMLTextAreaElement) && e.target.form === void 0) {\n          if (e.detail && e.detail.dispatcher) {\n            throw new Error(\n              `dispatching a custom ${type} event is only supported on input elements inside a form`\n            );\n          }\n          return;\n        }\n        const phxChange = this.binding(\"change\");\n        const input = e.target;\n        if (this.blockPhxChangeWhileComposing && e.isComposing) {\n          const key = `composition-listener-${type}`;\n          if (!dom_default.private(input, key)) {\n            dom_default.putPrivate(input, key, true);\n            input.addEventListener(\n              \"compositionend\",\n              () => {\n                input.dispatchEvent(new Event(type, { bubbles: true }));\n                dom_default.deletePrivate(input, key);\n              },\n              { once: true }\n            );\n          }\n          return;\n        }\n        const inputEvent = input.getAttribute(phxChange);\n        const formEvent = input.form && input.form.getAttribute(phxChange);\n        const phxEvent = inputEvent || formEvent;\n        if (!phxEvent) {\n          return;\n        }\n        if (input.type === \"number\" && input.validity && input.validity.badInput) {\n          return;\n        }\n        const dispatcher = inputEvent ? input : input.form;\n        const currentIterations = iterations;\n        iterations++;\n        const { at, type: lastType } = dom_default.private(input, \"prev-iteration\") || {};\n        if (at === currentIterations - 1 && type === \"change\" && lastType === \"input\") {\n          return;\n        }\n        dom_default.putPrivate(input, \"prev-iteration\", {\n          at: currentIterations,\n          type\n        });\n        this.debounce(input, e, type, () => {\n          this.withinOwners(dispatcher, (view) => {\n            dom_default.putPrivate(input, PHX_HAS_FOCUSED, true);\n            js_default.exec(e, \"change\", phxEvent, view, input, [\n              \"push\",\n              { _target: e.target.name, dispatcher }\n            ]);\n          });\n        });\n      });\n    }\n    this.on(\"reset\", (e) => {\n      const form = e.target;\n      dom_default.resetForm(form);\n      const input = Array.from(form.elements).find((el) => el.type === \"reset\");\n      if (input) {\n        window.requestAnimationFrame(() => {\n          input.dispatchEvent(\n            new Event(\"input\", { bubbles: true, cancelable: false })\n          );\n        });\n      }\n    });\n  }\n  debounce(el, event, eventType, callback) {\n    if (eventType === \"blur\" || eventType === \"focusout\") {\n      return callback();\n    }\n    const phxDebounce = this.binding(PHX_DEBOUNCE);\n    const phxThrottle = this.binding(PHX_THROTTLE);\n    const defaultDebounce = this.defaults.debounce.toString();\n    const defaultThrottle = this.defaults.throttle.toString();\n    this.withinOwners(el, (view) => {\n      const asyncFilter = () => !view.isDestroyed() && document.body.contains(el);\n      dom_default.debounce(\n        el,\n        event,\n        phxDebounce,\n        defaultDebounce,\n        phxThrottle,\n        defaultThrottle,\n        asyncFilter,\n        () => {\n          callback();\n        }\n      );\n    });\n  }\n  silenceEvents(callback) {\n    this.silenced = true;\n    callback();\n    this.silenced = false;\n  }\n  on(event, callback) {\n    this.boundEventNames.add(event);\n    window.addEventListener(event, (e) => {\n      if (!this.silenced) {\n        callback(e);\n      }\n    });\n  }\n  jsQuerySelectorAll(sourceEl, query, defaultQuery) {\n    const all = this.domCallbacks.jsQuerySelectorAll;\n    return all ? all(sourceEl, query, defaultQuery) : defaultQuery();\n  }\n};\nvar TransitionSet = class {\n  constructor() {\n    this.transitions = /* @__PURE__ */ new Set();\n    this.promises = /* @__PURE__ */ new Set();\n    this.pendingOps = [];\n  }\n  reset() {\n    this.transitions.forEach((timer) => {\n      clearTimeout(timer);\n      this.transitions.delete(timer);\n    });\n    this.promises.clear();\n    this.flushPendingOps();\n  }\n  after(callback) {\n    if (this.size() === 0) {\n      callback();\n    } else {\n      this.pushPendingOp(callback);\n    }\n  }\n  addTransition(time, onStart, onDone) {\n    onStart();\n    const timer = setTimeout(() => {\n      this.transitions.delete(timer);\n      onDone();\n      this.flushPendingOps();\n    }, time);\n    this.transitions.add(timer);\n  }\n  addAsyncTransition(promise) {\n    this.promises.add(promise);\n    promise.then(() => {\n      this.promises.delete(promise);\n      this.flushPendingOps();\n    });\n  }\n  pushPendingOp(op) {\n    this.pendingOps.push(op);\n  }\n  size() {\n    return this.transitions.size + this.promises.size;\n  }\n  flushPendingOps() {\n    if (this.size() > 0) {\n      return;\n    }\n    const op = this.pendingOps.shift();\n    if (op) {\n      op();\n      this.flushPendingOps();\n    }\n  }\n};\n\n// js/phoenix_live_view/index.ts\nvar LiveSocket2 = LiveSocket;\nfunction createHook(el, callbacks) {\n  let existingHook = dom_default.getCustomElHook(el);\n  if (existingHook) {\n    return existingHook;\n  }\n  if (!el.hasAttribute(\"id\")) {\n    logError(\n      \"Elements passed to createHook need to have a unique id attribute\",\n      el\n    );\n  }\n  let hook = new ViewHook(View.closestView(el), el, callbacks);\n  dom_default.putCustomElHook(el, hook);\n  return hook;\n}\n//# sourceMappingURL=phoenix_live_view.cjs.js.map\n"
  },
  {
    "path": "priv/static/phoenix_live_view.esm.js",
    "content": "// js/phoenix_live_view/constants.js\nvar CONSECUTIVE_RELOADS = \"consecutive-reloads\";\nvar MAX_RELOADS = 10;\nvar RELOAD_JITTER_MIN = 5e3;\nvar RELOAD_JITTER_MAX = 1e4;\nvar FAILSAFE_JITTER = 3e4;\nvar PHX_EVENT_CLASSES = [\n  \"phx-click-loading\",\n  \"phx-change-loading\",\n  \"phx-submit-loading\",\n  \"phx-keydown-loading\",\n  \"phx-keyup-loading\",\n  \"phx-blur-loading\",\n  \"phx-focus-loading\",\n  \"phx-hook-loading\"\n];\nvar PHX_DROP_TARGET_ACTIVE_CLASS = \"phx-drop-target-active\";\nvar PHX_COMPONENT = \"data-phx-component\";\nvar PHX_VIEW_REF = \"data-phx-view\";\nvar PHX_LIVE_LINK = \"data-phx-link\";\nvar PHX_TRACK_STATIC = \"track-static\";\nvar PHX_LINK_STATE = \"data-phx-link-state\";\nvar PHX_REF_LOADING = \"data-phx-ref-loading\";\nvar PHX_REF_SRC = \"data-phx-ref-src\";\nvar PHX_REF_LOCK = \"data-phx-ref-lock\";\nvar PHX_PENDING_REFS = \"phx-pending-refs\";\nvar PHX_TRACK_UPLOADS = \"track-uploads\";\nvar PHX_UPLOAD_REF = \"data-phx-upload-ref\";\nvar PHX_PREFLIGHTED_REFS = \"data-phx-preflighted-refs\";\nvar PHX_DONE_REFS = \"data-phx-done-refs\";\nvar PHX_DROP_TARGET = \"drop-target\";\nvar PHX_ACTIVE_ENTRY_REFS = \"data-phx-active-refs\";\nvar PHX_LIVE_FILE_UPDATED = \"phx:live-file:updated\";\nvar PHX_SKIP = \"data-phx-skip\";\nvar PHX_MAGIC_ID = \"data-phx-id\";\nvar PHX_PRUNE = \"data-phx-prune\";\nvar PHX_CONNECTED_CLASS = \"phx-connected\";\nvar PHX_LOADING_CLASS = \"phx-loading\";\nvar PHX_ERROR_CLASS = \"phx-error\";\nvar PHX_CLIENT_ERROR_CLASS = \"phx-client-error\";\nvar PHX_SERVER_ERROR_CLASS = \"phx-server-error\";\nvar PHX_PARENT_ID = \"data-phx-parent-id\";\nvar PHX_MAIN = \"data-phx-main\";\nvar PHX_ROOT_ID = \"data-phx-root-id\";\nvar PHX_VIEWPORT_TOP = \"viewport-top\";\nvar PHX_VIEWPORT_BOTTOM = \"viewport-bottom\";\nvar PHX_VIEWPORT_OVERRUN_TARGET = \"viewport-overrun-target\";\nvar PHX_TRIGGER_ACTION = \"trigger-action\";\nvar PHX_HAS_FOCUSED = \"phx-has-focused\";\nvar FOCUSABLE_INPUTS = [\n  \"text\",\n  \"textarea\",\n  \"number\",\n  \"email\",\n  \"password\",\n  \"search\",\n  \"tel\",\n  \"url\",\n  \"date\",\n  \"time\",\n  \"datetime-local\",\n  \"color\",\n  \"range\"\n];\nvar CHECKABLE_INPUTS = [\"checkbox\", \"radio\"];\nvar PHX_HAS_SUBMITTED = \"phx-has-submitted\";\nvar PHX_SESSION = \"data-phx-session\";\nvar PHX_VIEW_SELECTOR = `[${PHX_SESSION}]`;\nvar PHX_STICKY = \"data-phx-sticky\";\nvar PHX_STATIC = \"data-phx-static\";\nvar PHX_READONLY = \"data-phx-readonly\";\nvar PHX_DISABLED = \"data-phx-disabled\";\nvar PHX_DISABLE_WITH = \"disable-with\";\nvar PHX_DISABLE_WITH_RESTORE = \"data-phx-disable-with-restore\";\nvar PHX_HOOK = \"hook\";\nvar PHX_DEBOUNCE = \"debounce\";\nvar PHX_THROTTLE = \"throttle\";\nvar PHX_UPDATE = \"update\";\nvar PHX_STREAM = \"stream\";\nvar PHX_STREAM_REF = \"data-phx-stream\";\nvar PHX_PORTAL = \"data-phx-portal\";\nvar PHX_TELEPORTED_REF = \"data-phx-teleported\";\nvar PHX_TELEPORTED_SRC = \"data-phx-teleported-src\";\nvar PHX_RUNTIME_HOOK = \"data-phx-runtime-hook\";\nvar PHX_LV_PID = \"data-phx-pid\";\nvar PHX_KEY = \"key\";\nvar PHX_PRIVATE = \"phxPrivate\";\nvar PHX_AUTO_RECOVER = \"auto-recover\";\nvar PHX_NO_UNUSED_FIELD = \"no-unused-field\";\nvar PHX_LV_DEBUG = \"phx:live-socket:debug\";\nvar PHX_LV_PROFILE = \"phx:live-socket:profiling\";\nvar PHX_LV_LATENCY_SIM = \"phx:live-socket:latency-sim\";\nvar PHX_LV_HISTORY_POSITION = \"phx:nav-history-position\";\nvar PHX_PROGRESS = \"progress\";\nvar PHX_MOUNTED = \"mounted\";\nvar PHX_RELOAD_STATUS = \"__phoenix_reload_status__\";\nvar LOADER_TIMEOUT = 1;\nvar MAX_CHILD_JOIN_ATTEMPTS = 3;\nvar BEFORE_UNLOAD_LOADER_TIMEOUT = 200;\nvar DISCONNECTED_TIMEOUT = 500;\nvar BINDING_PREFIX = \"phx-\";\nvar PUSH_TIMEOUT = 3e4;\nvar DEBOUNCE_TRIGGER = \"debounce-trigger\";\nvar THROTTLED = \"throttled\";\nvar DEBOUNCE_PREV_KEY = \"debounce-prev-key\";\nvar DEFAULTS = {\n  debounce: 300,\n  throttle: 300\n};\nvar PHX_PENDING_ATTRS = [PHX_REF_LOADING, PHX_REF_SRC, PHX_REF_LOCK];\nvar STATIC = \"s\";\nvar ROOT = \"r\";\nvar COMPONENTS = \"c\";\nvar KEYED = \"k\";\nvar KEYED_COUNT = \"kc\";\nvar EVENTS = \"e\";\nvar REPLY = \"r\";\nvar TITLE = \"t\";\nvar TEMPLATES = \"p\";\nvar STREAM = \"stream\";\n\n// js/phoenix_live_view/entry_uploader.js\nvar EntryUploader = class {\n  constructor(entry, config, liveSocket) {\n    const { chunk_size, chunk_timeout } = config;\n    this.liveSocket = liveSocket;\n    this.entry = entry;\n    this.offset = 0;\n    this.chunkSize = chunk_size;\n    this.chunkTimeout = chunk_timeout;\n    this.chunkTimer = null;\n    this.errored = false;\n    this.uploadChannel = liveSocket.channel(`lvu:${entry.ref}`, {\n      token: entry.metadata()\n    });\n  }\n  error(reason) {\n    if (this.errored) {\n      return;\n    }\n    this.uploadChannel.leave();\n    this.errored = true;\n    clearTimeout(this.chunkTimer);\n    this.entry.error(reason);\n  }\n  upload() {\n    this.uploadChannel.onError((reason) => this.error(reason));\n    this.uploadChannel.join().receive(\"ok\", (_data) => this.readNextChunk()).receive(\"error\", (reason) => this.error(reason));\n  }\n  isDone() {\n    return this.offset >= this.entry.file.size;\n  }\n  readNextChunk() {\n    const reader = new window.FileReader();\n    const blob = this.entry.file.slice(\n      this.offset,\n      this.chunkSize + this.offset\n    );\n    reader.onload = (e) => {\n      if (e.target.error === null) {\n        this.offset += /** @type {ArrayBuffer} */\n        e.target.result.byteLength;\n        this.pushChunk(\n          /** @type {ArrayBuffer} */\n          e.target.result\n        );\n      } else {\n        return logError(\"Read error: \" + e.target.error);\n      }\n    };\n    reader.readAsArrayBuffer(blob);\n  }\n  pushChunk(chunk) {\n    if (!this.uploadChannel.isJoined()) {\n      return;\n    }\n    this.uploadChannel.push(\"chunk\", chunk, this.chunkTimeout).receive(\"ok\", () => {\n      this.entry.progress(this.offset / this.entry.file.size * 100);\n      if (!this.isDone()) {\n        this.chunkTimer = setTimeout(\n          () => this.readNextChunk(),\n          this.liveSocket.getLatencySim() || 0\n        );\n      }\n    }).receive(\"error\", ({ reason }) => this.error(reason));\n  }\n};\n\n// js/phoenix_live_view/utils.js\nvar logError = (msg, obj) => console.error && console.error(msg, obj);\nvar isCid = (cid) => {\n  const type = typeof cid;\n  return type === \"number\" || type === \"string\" && /^(0|[1-9]\\d*)$/.test(cid);\n};\nfunction detectDuplicateIds() {\n  const ids = /* @__PURE__ */ new Set();\n  const elems = document.querySelectorAll(\"*[id]\");\n  for (let i = 0, len = elems.length; i < len; i++) {\n    if (ids.has(elems[i].id)) {\n      console.error(\n        `Multiple IDs detected: ${elems[i].id}. Ensure unique element ids.`\n      );\n    } else {\n      ids.add(elems[i].id);\n    }\n  }\n}\nfunction detectInvalidStreamInserts(inserts) {\n  const errors = /* @__PURE__ */ new Set();\n  Object.keys(inserts).forEach((id) => {\n    const streamEl = document.getElementById(id);\n    if (streamEl && streamEl.parentElement && streamEl.parentElement.getAttribute(\"phx-update\") !== \"stream\") {\n      errors.add(\n        `The stream container with id \"${streamEl.parentElement.id}\" is missing the phx-update=\"stream\" attribute. Ensure it is set for streams to work properly.`\n      );\n    }\n  });\n  errors.forEach((error) => console.error(error));\n}\nvar debug = (view, kind, msg, obj) => {\n  if (view.liveSocket.isDebugEnabled()) {\n    console.log(`${view.id} ${kind}: ${msg} - `, obj);\n  }\n};\nvar closure = (val) => typeof val === \"function\" ? val : function() {\n  return val;\n};\nvar clone = (obj) => {\n  return JSON.parse(JSON.stringify(obj));\n};\nvar closestPhxBinding = (el, binding, borderEl) => {\n  do {\n    if (el.matches(`[${binding}]`) && !el.disabled) {\n      return el;\n    }\n    el = el.parentElement || el.parentNode;\n  } while (el !== null && el.nodeType === 1 && !(borderEl && borderEl.isSameNode(el) || el.matches(PHX_VIEW_SELECTOR)));\n  return null;\n};\nvar isObject = (obj) => {\n  return obj !== null && typeof obj === \"object\" && !(obj instanceof Array);\n};\nvar isEqualObj = (obj1, obj2) => JSON.stringify(obj1) === JSON.stringify(obj2);\nvar isEmpty = (obj) => {\n  for (const x in obj) {\n    return false;\n  }\n  return true;\n};\nvar maybe = (el, callback) => el && callback(el);\nvar channelUploader = function(entries, onError, resp, liveSocket) {\n  entries.forEach((entry) => {\n    const entryUploader = new EntryUploader(entry, resp.config, liveSocket);\n    entryUploader.upload();\n  });\n};\nvar eventContainsFiles = (e) => {\n  if (e.dataTransfer.types) {\n    for (let i = 0; i < e.dataTransfer.types.length; i++) {\n      if (e.dataTransfer.types[i] === \"Files\") {\n        return true;\n      }\n    }\n  }\n  return false;\n};\n\n// js/phoenix_live_view/browser.js\nvar Browser = {\n  canPushState() {\n    return typeof history.pushState !== \"undefined\";\n  },\n  dropLocal(localStorage, namespace, subkey) {\n    return localStorage.removeItem(this.localKey(namespace, subkey));\n  },\n  updateLocal(localStorage, namespace, subkey, initial, func) {\n    const current = this.getLocal(localStorage, namespace, subkey);\n    const key = this.localKey(namespace, subkey);\n    const newVal = current === null ? initial : func(current);\n    localStorage.setItem(key, JSON.stringify(newVal));\n    return newVal;\n  },\n  getLocal(localStorage, namespace, subkey) {\n    return JSON.parse(localStorage.getItem(this.localKey(namespace, subkey)));\n  },\n  updateCurrentState(callback) {\n    if (!this.canPushState()) {\n      return;\n    }\n    history.replaceState(\n      callback(history.state || {}),\n      \"\",\n      window.location.href\n    );\n  },\n  pushState(kind, meta, to) {\n    if (this.canPushState()) {\n      if (to !== window.location.href) {\n        if (meta.type == \"redirect\" && meta.scroll) {\n          const currentState = history.state || {};\n          currentState.scroll = meta.scroll;\n          history.replaceState(currentState, \"\", window.location.href);\n        }\n        delete meta.scroll;\n        history[kind + \"State\"](meta, \"\", to || null);\n        window.requestAnimationFrame(() => {\n          const hashEl = this.getHashTargetEl(window.location.hash);\n          if (hashEl) {\n            hashEl.scrollIntoView();\n          } else if (meta.type === \"redirect\") {\n            window.scroll(0, 0);\n          }\n        });\n      }\n    } else {\n      this.redirect(to);\n    }\n  },\n  setCookie(name, value, maxAgeSeconds) {\n    const expires = typeof maxAgeSeconds === \"number\" ? ` max-age=${maxAgeSeconds};` : \"\";\n    document.cookie = `${name}=${value};${expires} path=/`;\n  },\n  getCookie(name) {\n    return document.cookie.replace(\n      new RegExp(`(?:(?:^|.*;s*)${name}s*=s*([^;]*).*$)|^.*$`),\n      \"$1\"\n    );\n  },\n  deleteCookie(name) {\n    document.cookie = `${name}=; max-age=-1; path=/`;\n  },\n  redirect(toURL, flash, navigate = (url) => {\n    window.location.href = url;\n  }) {\n    if (flash) {\n      this.setCookie(\"__phoenix_flash__\", flash, 60);\n    }\n    navigate(toURL);\n  },\n  localKey(namespace, subkey) {\n    return `${namespace}-${subkey}`;\n  },\n  getHashTargetEl(maybeHash) {\n    const hash = maybeHash.toString().substring(1);\n    if (hash === \"\") {\n      return;\n    }\n    return document.getElementById(hash) || document.querySelector(`a[name=\"${hash}\"]`);\n  }\n};\nvar browser_default = Browser;\n\n// js/phoenix_live_view/dom.js\nvar DOM = {\n  byId(id) {\n    return document.getElementById(id) || logError(`no id found for ${id}`);\n  },\n  removeClass(el, className) {\n    el.classList.remove(className);\n    if (el.classList.length === 0) {\n      el.removeAttribute(\"class\");\n    }\n  },\n  all(node, query, callback) {\n    if (!node) {\n      return [];\n    }\n    const array = Array.from(node.querySelectorAll(query));\n    if (callback) {\n      array.forEach(callback);\n    }\n    return array;\n  },\n  childNodeLength(html) {\n    const template = document.createElement(\"template\");\n    template.innerHTML = html;\n    return template.content.childElementCount;\n  },\n  isUploadInput(el) {\n    return el.type === \"file\" && el.getAttribute(PHX_UPLOAD_REF) !== null;\n  },\n  isAutoUpload(inputEl) {\n    return inputEl.hasAttribute(\"data-phx-auto-upload\");\n  },\n  findUploadInputs(node) {\n    const formId = node.id;\n    const inputsOutsideForm = this.all(\n      document,\n      `input[type=\"file\"][${PHX_UPLOAD_REF}][form=\"${formId}\"]`\n    );\n    return this.all(node, `input[type=\"file\"][${PHX_UPLOAD_REF}]`).concat(\n      inputsOutsideForm\n    );\n  },\n  findComponentNodeList(viewId, cid, doc2 = document) {\n    return this.all(\n      doc2,\n      `[${PHX_VIEW_REF}=\"${viewId}\"][${PHX_COMPONENT}=\"${cid}\"]`\n    );\n  },\n  isPhxDestroyed(node) {\n    return node.id && DOM.private(node, \"destroyed\") ? true : false;\n  },\n  wantsNewTab(e) {\n    const wantsNewTab = e.ctrlKey || e.shiftKey || e.metaKey || e.button && e.button === 1;\n    const isDownload = e.target instanceof HTMLAnchorElement && e.target.hasAttribute(\"download\");\n    const isTargetBlank = e.target.hasAttribute(\"target\") && e.target.getAttribute(\"target\").toLowerCase() === \"_blank\";\n    const isTargetNamedTab = e.target.hasAttribute(\"target\") && !e.target.getAttribute(\"target\").startsWith(\"_\");\n    return wantsNewTab || isTargetBlank || isDownload || isTargetNamedTab;\n  },\n  isUnloadableFormSubmit(e) {\n    const isDialogSubmit = e.target && e.target.getAttribute(\"method\") === \"dialog\" || e.submitter && e.submitter.getAttribute(\"formmethod\") === \"dialog\";\n    if (isDialogSubmit) {\n      return false;\n    } else {\n      return !e.defaultPrevented && !this.wantsNewTab(e);\n    }\n  },\n  isNewPageClick(e, currentLocation) {\n    const href = e.target instanceof HTMLAnchorElement ? e.target.getAttribute(\"href\") : null;\n    let url;\n    if (e.defaultPrevented || href === null || this.wantsNewTab(e)) {\n      return false;\n    }\n    if (href.startsWith(\"mailto:\") || href.startsWith(\"tel:\")) {\n      return false;\n    }\n    if (e.target.isContentEditable) {\n      return false;\n    }\n    try {\n      url = new URL(href);\n    } catch {\n      try {\n        url = new URL(href, currentLocation);\n      } catch {\n        return true;\n      }\n    }\n    if (url.host === currentLocation.host && url.protocol === currentLocation.protocol) {\n      if (url.pathname === currentLocation.pathname && url.search === currentLocation.search) {\n        return url.hash === \"\" && !url.href.endsWith(\"#\");\n      }\n    }\n    return url.protocol.startsWith(\"http\");\n  },\n  markPhxChildDestroyed(el) {\n    if (this.isPhxChild(el)) {\n      el.setAttribute(PHX_SESSION, \"\");\n    }\n    this.putPrivate(el, \"destroyed\", true);\n  },\n  findPhxChildrenInFragment(html, parentId) {\n    const template = document.createElement(\"template\");\n    template.innerHTML = html;\n    return this.findPhxChildren(template.content, parentId);\n  },\n  isIgnored(el, phxUpdate) {\n    return (el.getAttribute(phxUpdate) || el.getAttribute(\"data-phx-update\")) === \"ignore\";\n  },\n  isPhxUpdate(el, phxUpdate, updateTypes) {\n    return el.getAttribute && updateTypes.indexOf(el.getAttribute(phxUpdate)) >= 0;\n  },\n  findPhxSticky(el) {\n    return this.all(el, `[${PHX_STICKY}]`);\n  },\n  findPhxChildren(el, parentId) {\n    return this.all(el, `${PHX_VIEW_SELECTOR}[${PHX_PARENT_ID}=\"${parentId}\"]`);\n  },\n  findExistingParentCIDs(viewId, cids) {\n    const parentCids = /* @__PURE__ */ new Set();\n    const childrenCids = /* @__PURE__ */ new Set();\n    cids.forEach((cid) => {\n      this.all(\n        document,\n        `[${PHX_VIEW_REF}=\"${viewId}\"][${PHX_COMPONENT}=\"${cid}\"]`\n      ).forEach((parent) => {\n        parentCids.add(cid);\n        this.all(parent, `[${PHX_VIEW_REF}=\"${viewId}\"][${PHX_COMPONENT}]`).map((el) => parseInt(el.getAttribute(PHX_COMPONENT))).forEach((childCID) => childrenCids.add(childCID));\n      });\n    });\n    childrenCids.forEach((childCid) => parentCids.delete(childCid));\n    return parentCids;\n  },\n  private(el, key) {\n    return el[PHX_PRIVATE] && el[PHX_PRIVATE][key];\n  },\n  deletePrivate(el, key) {\n    el[PHX_PRIVATE] && delete el[PHX_PRIVATE][key];\n  },\n  putPrivate(el, key, value) {\n    if (!el[PHX_PRIVATE]) {\n      el[PHX_PRIVATE] = {};\n    }\n    el[PHX_PRIVATE][key] = value;\n  },\n  updatePrivate(el, key, defaultVal, updateFunc) {\n    const existing = this.private(el, key);\n    if (existing === void 0) {\n      this.putPrivate(el, key, updateFunc(defaultVal));\n    } else {\n      this.putPrivate(el, key, updateFunc(existing));\n    }\n  },\n  syncPendingAttrs(fromEl, toEl) {\n    if (!fromEl.hasAttribute(PHX_REF_SRC)) {\n      return;\n    }\n    PHX_EVENT_CLASSES.forEach((className) => {\n      fromEl.classList.contains(className) && toEl.classList.add(className);\n    });\n    PHX_PENDING_ATTRS.filter((attr) => fromEl.hasAttribute(attr)).forEach(\n      (attr) => {\n        toEl.setAttribute(attr, fromEl.getAttribute(attr));\n      }\n    );\n  },\n  copyPrivates(target, source) {\n    if (source[PHX_PRIVATE]) {\n      target[PHX_PRIVATE] = source[PHX_PRIVATE];\n    }\n  },\n  putTitle(str) {\n    const titleEl = document.querySelector(\"title\");\n    if (titleEl) {\n      const { prefix, suffix, default: defaultTitle } = titleEl.dataset;\n      const isEmpty2 = typeof str !== \"string\" || str.trim() === \"\";\n      if (isEmpty2 && typeof defaultTitle !== \"string\") {\n        return;\n      }\n      const inner = isEmpty2 ? defaultTitle : str;\n      document.title = `${prefix || \"\"}${inner || \"\"}${suffix || \"\"}`;\n    } else {\n      document.title = str;\n    }\n  },\n  debounce(el, event, phxDebounce, defaultDebounce, phxThrottle, defaultThrottle, asyncFilter, callback) {\n    let debounce = el.getAttribute(phxDebounce);\n    let throttle = el.getAttribute(phxThrottle);\n    if (debounce === \"\") {\n      debounce = defaultDebounce;\n    }\n    if (throttle === \"\") {\n      throttle = defaultThrottle;\n    }\n    const value = debounce || throttle;\n    switch (value) {\n      case null:\n        return callback();\n      case \"blur\":\n        this.incCycle(el, \"debounce-blur-cycle\", () => {\n          if (asyncFilter()) {\n            callback();\n          }\n        });\n        if (this.once(el, \"debounce-blur\")) {\n          el.addEventListener(\n            \"blur\",\n            () => this.triggerCycle(el, \"debounce-blur-cycle\")\n          );\n        }\n        return;\n      default:\n        const timeout = parseInt(value);\n        const trigger = () => throttle ? this.deletePrivate(el, THROTTLED) : callback();\n        const currentCycle = this.incCycle(el, DEBOUNCE_TRIGGER, trigger);\n        if (isNaN(timeout)) {\n          return logError(`invalid throttle/debounce value: ${value}`);\n        }\n        if (throttle) {\n          let newKeyDown = false;\n          if (event.type === \"keydown\") {\n            const prevKey = this.private(el, DEBOUNCE_PREV_KEY);\n            this.putPrivate(el, DEBOUNCE_PREV_KEY, event.key);\n            newKeyDown = prevKey !== event.key;\n          }\n          if (!newKeyDown && this.private(el, THROTTLED)) {\n            return false;\n          } else {\n            callback();\n            const t = setTimeout(() => {\n              if (asyncFilter()) {\n                this.triggerCycle(el, DEBOUNCE_TRIGGER);\n              }\n            }, timeout);\n            this.putPrivate(el, THROTTLED, t);\n          }\n        } else {\n          setTimeout(() => {\n            if (asyncFilter()) {\n              this.triggerCycle(el, DEBOUNCE_TRIGGER, currentCycle);\n            }\n          }, timeout);\n        }\n        const form = el.form;\n        if (form && this.once(form, \"bind-debounce\")) {\n          form.addEventListener(\"submit\", () => {\n            Array.from(new FormData(form).entries(), ([name]) => {\n              const namedItem = form.elements.namedItem(name);\n              const input = namedItem instanceof RadioNodeList ? namedItem[0] : namedItem;\n              if (input) {\n                this.incCycle(input, DEBOUNCE_TRIGGER);\n                this.deletePrivate(input, THROTTLED);\n              }\n            });\n          });\n        }\n        if (this.once(el, \"bind-debounce\")) {\n          el.addEventListener(\"blur\", () => {\n            clearTimeout(this.private(el, THROTTLED));\n            this.triggerCycle(el, DEBOUNCE_TRIGGER);\n          });\n        }\n    }\n  },\n  triggerCycle(el, key, currentCycle) {\n    const [cycle, trigger] = this.private(el, key);\n    if (!currentCycle) {\n      currentCycle = cycle;\n    }\n    if (currentCycle === cycle) {\n      this.incCycle(el, key);\n      trigger();\n    }\n  },\n  once(el, key) {\n    if (this.private(el, key) === true) {\n      return false;\n    }\n    this.putPrivate(el, key, true);\n    return true;\n  },\n  incCycle(el, key, trigger = function() {\n  }) {\n    let [currentCycle] = this.private(el, key) || [0, trigger];\n    currentCycle++;\n    this.putPrivate(el, key, [currentCycle, trigger]);\n    return currentCycle;\n  },\n  // maintains or adds privately used hook information\n  // fromEl and toEl can be the same element in the case of a newly added node\n  // fromEl and toEl can be any HTML node type, so we need to check if it's an element node\n  maintainPrivateHooks(fromEl, toEl, phxViewportTop, phxViewportBottom) {\n    if (fromEl.hasAttribute && fromEl.hasAttribute(\"data-phx-hook\") && !toEl.hasAttribute(\"data-phx-hook\")) {\n      toEl.setAttribute(\"data-phx-hook\", fromEl.getAttribute(\"data-phx-hook\"));\n    }\n    if (toEl.hasAttribute && (toEl.hasAttribute(phxViewportTop) || toEl.hasAttribute(phxViewportBottom))) {\n      toEl.setAttribute(\"data-phx-hook\", \"Phoenix.InfiniteScroll\");\n    }\n  },\n  putCustomElHook(el, hook) {\n    if (el.isConnected) {\n      el.setAttribute(\"data-phx-hook\", \"\");\n    } else {\n      console.error(`\n        hook attached to non-connected DOM element\n        ensure you are calling createHook within your connectedCallback. ${el.outerHTML}\n      `);\n    }\n    this.putPrivate(el, \"custom-el-hook\", hook);\n  },\n  getCustomElHook(el) {\n    return this.private(el, \"custom-el-hook\");\n  },\n  isUsedInput(el) {\n    return el.nodeType === Node.ELEMENT_NODE && (this.private(el, PHX_HAS_FOCUSED) || this.private(el, PHX_HAS_SUBMITTED));\n  },\n  resetForm(form) {\n    Array.from(form.elements).forEach((input) => {\n      this.deletePrivate(input, PHX_HAS_FOCUSED);\n      this.deletePrivate(input, PHX_HAS_SUBMITTED);\n    });\n  },\n  isPhxChild(node) {\n    return node.getAttribute && node.getAttribute(PHX_PARENT_ID);\n  },\n  isPhxSticky(node) {\n    return node.getAttribute && node.getAttribute(PHX_STICKY) !== null;\n  },\n  isChildOfAny(el, parents) {\n    return !!parents.find((parent) => parent.contains(el));\n  },\n  firstPhxChild(el) {\n    return this.isPhxChild(el) ? el : this.all(el, `[${PHX_PARENT_ID}]`)[0];\n  },\n  isPortalTemplate(el) {\n    return el.tagName === \"TEMPLATE\" && el.hasAttribute(PHX_PORTAL);\n  },\n  closestViewEl(el) {\n    const portalOrViewEl = el.closest(\n      `[${PHX_TELEPORTED_REF}],${PHX_VIEW_SELECTOR}`\n    );\n    if (!portalOrViewEl) {\n      return null;\n    }\n    if (portalOrViewEl.hasAttribute(PHX_TELEPORTED_REF)) {\n      return this.byId(portalOrViewEl.getAttribute(PHX_TELEPORTED_REF));\n    } else if (portalOrViewEl.hasAttribute(PHX_SESSION)) {\n      return portalOrViewEl;\n    }\n    return null;\n  },\n  dispatchEvent(target, name, opts = {}) {\n    let defaultBubble = true;\n    const isUploadTarget = target.nodeName === \"INPUT\" && target.type === \"file\";\n    if (isUploadTarget && name === \"click\") {\n      defaultBubble = false;\n    }\n    const bubbles = opts.bubbles === void 0 ? defaultBubble : !!opts.bubbles;\n    const eventOpts = {\n      bubbles,\n      cancelable: true,\n      detail: opts.detail || {}\n    };\n    const event = name === \"click\" ? new MouseEvent(\"click\", eventOpts) : new CustomEvent(name, eventOpts);\n    target.dispatchEvent(event);\n  },\n  cloneNode(node, html) {\n    if (typeof html === \"undefined\") {\n      return node.cloneNode(true);\n    } else {\n      const cloned = node.cloneNode(false);\n      cloned.innerHTML = html;\n      return cloned;\n    }\n  },\n  // merge attributes from source to target\n  // if an element is ignored, we only merge data attributes\n  // including removing data attributes that are no longer in the source\n  mergeAttrs(target, source, opts = {}) {\n    const exclude = new Set(opts.exclude || []);\n    const isIgnored = opts.isIgnored;\n    const sourceAttrs = source.attributes;\n    for (let i = sourceAttrs.length - 1; i >= 0; i--) {\n      const name = sourceAttrs[i].name;\n      if (!exclude.has(name)) {\n        const sourceValue = source.getAttribute(name);\n        if (target.getAttribute(name) !== sourceValue && (!isIgnored || isIgnored && name.startsWith(\"data-\"))) {\n          target.setAttribute(name, sourceValue);\n        }\n      } else {\n        if (name === \"value\") {\n          const sourceValue = source.value ?? source.getAttribute(name);\n          if (target.value === sourceValue) {\n            target.setAttribute(\"value\", source.getAttribute(name));\n          }\n        }\n      }\n    }\n    const targetAttrs = target.attributes;\n    for (let i = targetAttrs.length - 1; i >= 0; i--) {\n      const name = targetAttrs[i].name;\n      if (isIgnored) {\n        if (name.startsWith(\"data-\") && !source.hasAttribute(name) && !PHX_PENDING_ATTRS.includes(name)) {\n          target.removeAttribute(name);\n        }\n      } else {\n        if (!source.hasAttribute(name)) {\n          target.removeAttribute(name);\n        }\n      }\n    }\n  },\n  mergeFocusedInput(target, source) {\n    if (!(target instanceof HTMLSelectElement)) {\n      DOM.mergeAttrs(target, source, { exclude: [\"value\"] });\n    }\n    if (source.readOnly) {\n      target.setAttribute(\"readonly\", true);\n    } else {\n      target.removeAttribute(\"readonly\");\n    }\n  },\n  hasSelectionRange(el) {\n    return el.setSelectionRange && (el.type === \"text\" || el.type === \"textarea\");\n  },\n  restoreFocus(focused, selectionStart, selectionEnd) {\n    if (focused instanceof HTMLSelectElement) {\n      focused.focus();\n    }\n    if (!DOM.isTextualInput(focused)) {\n      return;\n    }\n    const wasFocused = focused.matches(\":focus\");\n    if (!wasFocused) {\n      focused.focus();\n    }\n    if (this.hasSelectionRange(focused)) {\n      focused.setSelectionRange(selectionStart, selectionEnd);\n    }\n  },\n  isFormInput(el) {\n    if (el.localName && customElements.get(el.localName)) {\n      return customElements.get(el.localName)[`formAssociated`];\n    }\n    return /^(?:input|select|textarea)$/i.test(el.tagName) && el.type !== \"button\";\n  },\n  syncAttrsToProps(el) {\n    if (el instanceof HTMLInputElement && CHECKABLE_INPUTS.indexOf(el.type.toLocaleLowerCase()) >= 0) {\n      el.checked = el.getAttribute(\"checked\") !== null;\n    }\n  },\n  isTextualInput(el) {\n    return FOCUSABLE_INPUTS.indexOf(el.type) >= 0;\n  },\n  isNowTriggerFormExternal(el, phxTriggerExternal) {\n    return el.getAttribute && el.getAttribute(phxTriggerExternal) !== null && document.body.contains(el);\n  },\n  cleanChildNodes(container, phxUpdate) {\n    if (DOM.isPhxUpdate(container, phxUpdate, [\"append\", \"prepend\", PHX_STREAM])) {\n      const toRemove = [];\n      container.childNodes.forEach((childNode) => {\n        if (!childNode.id) {\n          const isEmptyTextNode = childNode.nodeType === Node.TEXT_NODE && childNode.nodeValue.trim() === \"\";\n          if (!isEmptyTextNode && childNode.nodeType !== Node.COMMENT_NODE) {\n            logError(\n              `only HTML element tags with an id are allowed inside containers with phx-update.\n\nremoving illegal node: \"${(childNode.outerHTML || childNode.nodeValue).trim()}\"\n\n`\n            );\n          }\n          toRemove.push(childNode);\n        }\n      });\n      toRemove.forEach((childNode) => childNode.remove());\n    }\n  },\n  replaceRootContainer(container, tagName, attrs) {\n    const retainedAttrs = /* @__PURE__ */ new Set([\n      \"id\",\n      PHX_SESSION,\n      PHX_STATIC,\n      PHX_MAIN,\n      PHX_ROOT_ID\n    ]);\n    if (container.tagName.toLowerCase() === tagName.toLowerCase()) {\n      Array.from(container.attributes).filter((attr) => !retainedAttrs.has(attr.name.toLowerCase())).forEach((attr) => container.removeAttribute(attr.name));\n      Object.keys(attrs).filter((name) => !retainedAttrs.has(name.toLowerCase())).forEach((attr) => container.setAttribute(attr, attrs[attr]));\n      return container;\n    } else {\n      const newContainer = document.createElement(tagName);\n      Object.keys(attrs).forEach(\n        (attr) => newContainer.setAttribute(attr, attrs[attr])\n      );\n      retainedAttrs.forEach(\n        (attr) => newContainer.setAttribute(attr, container.getAttribute(attr))\n      );\n      newContainer.innerHTML = container.innerHTML;\n      container.replaceWith(newContainer);\n      return newContainer;\n    }\n  },\n  getSticky(el, name, defaultVal) {\n    const op = (DOM.private(el, \"sticky\") || []).find(\n      ([existingName]) => name === existingName\n    );\n    if (op) {\n      const [_name, _op, stashedResult] = op;\n      return stashedResult;\n    } else {\n      return typeof defaultVal === \"function\" ? defaultVal() : defaultVal;\n    }\n  },\n  deleteSticky(el, name) {\n    this.updatePrivate(el, \"sticky\", [], (ops) => {\n      return ops.filter(([existingName, _]) => existingName !== name);\n    });\n  },\n  putSticky(el, name, op) {\n    const stashedResult = op(el);\n    this.updatePrivate(el, \"sticky\", [], (ops) => {\n      const existingIndex = ops.findIndex(\n        ([existingName]) => name === existingName\n      );\n      if (existingIndex >= 0) {\n        ops[existingIndex] = [name, op, stashedResult];\n      } else {\n        ops.push([name, op, stashedResult]);\n      }\n      return ops;\n    });\n  },\n  applyStickyOperations(el) {\n    const ops = DOM.private(el, \"sticky\");\n    if (!ops) {\n      return;\n    }\n    ops.forEach(([name, op, _stashed]) => this.putSticky(el, name, op));\n  },\n  isLocked(el) {\n    return el.hasAttribute && el.hasAttribute(PHX_REF_LOCK);\n  },\n  attributeIgnored(attribute, ignoredAttributes) {\n    return ignoredAttributes.some(\n      (toIgnore) => attribute.name == toIgnore || toIgnore === \"*\" || toIgnore.includes(\"*\") && attribute.name.match(toIgnore) != null\n    );\n  }\n};\nvar dom_default = DOM;\n\n// js/phoenix_live_view/upload_entry.js\nvar UploadEntry = class {\n  static isActive(fileEl, file) {\n    const isNew = file._phxRef === void 0;\n    const activeRefs = fileEl.getAttribute(PHX_ACTIVE_ENTRY_REFS).split(\",\");\n    const isActive = activeRefs.indexOf(LiveUploader.genFileRef(file)) >= 0;\n    return file.size > 0 && (isNew || isActive);\n  }\n  static isPreflighted(fileEl, file) {\n    const preflightedRefs = fileEl.getAttribute(PHX_PREFLIGHTED_REFS).split(\",\");\n    const isPreflighted = preflightedRefs.indexOf(LiveUploader.genFileRef(file)) >= 0;\n    return isPreflighted && this.isActive(fileEl, file);\n  }\n  static isPreflightInProgress(file) {\n    return file._preflightInProgress === true;\n  }\n  static markPreflightInProgress(file) {\n    file._preflightInProgress = true;\n  }\n  constructor(fileEl, file, view, autoUpload) {\n    this.ref = LiveUploader.genFileRef(file);\n    this.fileEl = fileEl;\n    this.file = file;\n    this.view = view;\n    this.meta = null;\n    this._isCancelled = false;\n    this._isDone = false;\n    this._progress = 0;\n    this._lastProgressSent = -1;\n    this._onDone = function() {\n    };\n    this._onElUpdated = this.onElUpdated.bind(this);\n    this.fileEl.addEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated);\n    this.autoUpload = autoUpload;\n  }\n  metadata() {\n    return this.meta;\n  }\n  progress(progress) {\n    this._progress = Math.floor(progress);\n    if (this._progress > this._lastProgressSent) {\n      if (this._progress >= 100) {\n        this._progress = 100;\n        this._lastProgressSent = 100;\n        this._isDone = true;\n        this.view.pushFileProgress(this.fileEl, this.ref, 100, () => {\n          LiveUploader.untrackFile(this.fileEl, this.file);\n          this._onDone();\n        });\n      } else {\n        this._lastProgressSent = this._progress;\n        this.view.pushFileProgress(this.fileEl, this.ref, this._progress);\n      }\n    }\n  }\n  isCancelled() {\n    return this._isCancelled;\n  }\n  cancel() {\n    this.file._preflightInProgress = false;\n    this._isCancelled = true;\n    this._isDone = true;\n    this._onDone();\n  }\n  isDone() {\n    return this._isDone;\n  }\n  error(reason = \"failed\") {\n    this.fileEl.removeEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated);\n    this.view.pushFileProgress(this.fileEl, this.ref, { error: reason });\n    if (!this.isAutoUpload()) {\n      LiveUploader.clearFiles(this.fileEl);\n    }\n  }\n  isAutoUpload() {\n    return this.autoUpload;\n  }\n  //private\n  onDone(callback) {\n    this._onDone = () => {\n      this.fileEl.removeEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated);\n      callback();\n    };\n  }\n  onElUpdated() {\n    const activeRefs = this.fileEl.getAttribute(PHX_ACTIVE_ENTRY_REFS).split(\",\");\n    if (activeRefs.indexOf(this.ref) === -1) {\n      LiveUploader.untrackFile(this.fileEl, this.file);\n      this.cancel();\n    }\n  }\n  toPreflightPayload() {\n    return {\n      last_modified: this.file.lastModified,\n      name: this.file.name,\n      relative_path: this.file.webkitRelativePath,\n      size: this.file.size,\n      type: this.file.type,\n      ref: this.ref,\n      meta: typeof this.file.meta === \"function\" ? this.file.meta() : void 0\n    };\n  }\n  uploader(uploaders) {\n    if (this.meta.uploader) {\n      const callback = uploaders[this.meta.uploader] || logError(`no uploader configured for ${this.meta.uploader}`);\n      return { name: this.meta.uploader, callback };\n    } else {\n      return { name: \"channel\", callback: channelUploader };\n    }\n  }\n  zipPostFlight(resp) {\n    this.meta = resp.entries[this.ref];\n    if (!this.meta) {\n      logError(`no preflight upload response returned with ref ${this.ref}`, {\n        input: this.fileEl,\n        response: resp\n      });\n    }\n  }\n};\n\n// js/phoenix_live_view/live_uploader.js\nvar liveUploaderFileRef = 0;\nvar LiveUploader = class _LiveUploader {\n  static genFileRef(file) {\n    const ref = file._phxRef;\n    if (ref !== void 0) {\n      return ref;\n    } else {\n      file._phxRef = (liveUploaderFileRef++).toString();\n      return file._phxRef;\n    }\n  }\n  static getEntryDataURL(inputEl, ref, callback) {\n    const file = this.activeFiles(inputEl).find(\n      (file2) => this.genFileRef(file2) === ref\n    );\n    callback(URL.createObjectURL(file));\n  }\n  static hasUploadsInProgress(formEl) {\n    let active = 0;\n    dom_default.findUploadInputs(formEl).forEach((input) => {\n      if (input.getAttribute(PHX_PREFLIGHTED_REFS) !== input.getAttribute(PHX_DONE_REFS)) {\n        active++;\n      }\n    });\n    return active > 0;\n  }\n  static serializeUploads(inputEl) {\n    const files = this.activeFiles(inputEl);\n    const fileData = {};\n    files.forEach((file) => {\n      const entry = { path: inputEl.name };\n      const uploadRef = inputEl.getAttribute(PHX_UPLOAD_REF);\n      fileData[uploadRef] = fileData[uploadRef] || [];\n      entry.ref = this.genFileRef(file);\n      entry.last_modified = file.lastModified;\n      entry.name = file.name || entry.ref;\n      entry.relative_path = file.webkitRelativePath;\n      entry.type = file.type;\n      entry.size = file.size;\n      if (typeof file.meta === \"function\") {\n        entry.meta = file.meta();\n      }\n      fileData[uploadRef].push(entry);\n    });\n    return fileData;\n  }\n  static clearFiles(inputEl) {\n    inputEl.value = null;\n    inputEl.removeAttribute(PHX_UPLOAD_REF);\n    dom_default.putPrivate(inputEl, \"files\", []);\n  }\n  static untrackFile(inputEl, file) {\n    dom_default.putPrivate(\n      inputEl,\n      \"files\",\n      dom_default.private(inputEl, \"files\").filter((f) => !Object.is(f, file))\n    );\n  }\n  /**\n   * @param {HTMLInputElement} inputEl\n   * @param {Array<File|Blob>} files\n   * @param {DataTransfer} [dataTransfer]\n   */\n  static trackFiles(inputEl, files, dataTransfer) {\n    if (inputEl.getAttribute(\"multiple\") !== null) {\n      const newFiles = files.filter(\n        (file) => !this.activeFiles(inputEl).find((f) => Object.is(f, file))\n      );\n      dom_default.updatePrivate(\n        inputEl,\n        \"files\",\n        [],\n        (existing) => existing.concat(newFiles)\n      );\n      inputEl.value = null;\n    } else {\n      if (dataTransfer && dataTransfer.files.length > 0) {\n        inputEl.files = dataTransfer.files;\n      }\n      dom_default.putPrivate(inputEl, \"files\", files);\n    }\n  }\n  static activeFileInputs(formEl) {\n    const fileInputs = dom_default.findUploadInputs(formEl);\n    return Array.from(fileInputs).filter(\n      (el) => el.files && this.activeFiles(el).length > 0\n    );\n  }\n  static activeFiles(input) {\n    return (dom_default.private(input, \"files\") || []).filter(\n      (f) => UploadEntry.isActive(input, f)\n    );\n  }\n  static inputsAwaitingPreflight(formEl) {\n    const fileInputs = dom_default.findUploadInputs(formEl);\n    return Array.from(fileInputs).filter(\n      (input) => this.filesAwaitingPreflight(input).length > 0\n    );\n  }\n  static filesAwaitingPreflight(input) {\n    return this.activeFiles(input).filter(\n      (f) => !UploadEntry.isPreflighted(input, f) && !UploadEntry.isPreflightInProgress(f)\n    );\n  }\n  static markPreflightInProgress(entries) {\n    entries.forEach((entry) => UploadEntry.markPreflightInProgress(entry.file));\n  }\n  constructor(inputEl, view, onComplete) {\n    this.autoUpload = dom_default.isAutoUpload(inputEl);\n    this.view = view;\n    this.onComplete = onComplete;\n    this._entries = Array.from(\n      _LiveUploader.filesAwaitingPreflight(inputEl) || []\n    ).map((file) => new UploadEntry(inputEl, file, view, this.autoUpload));\n    _LiveUploader.markPreflightInProgress(this._entries);\n    this.numEntriesInProgress = this._entries.length;\n  }\n  isAutoUpload() {\n    return this.autoUpload;\n  }\n  entries() {\n    return this._entries;\n  }\n  initAdapterUpload(resp, onError, liveSocket) {\n    this._entries = this._entries.map((entry) => {\n      if (entry.isCancelled()) {\n        this.numEntriesInProgress--;\n        if (this.numEntriesInProgress === 0) {\n          this.onComplete();\n        }\n      } else {\n        entry.zipPostFlight(resp);\n        entry.onDone(() => {\n          this.numEntriesInProgress--;\n          if (this.numEntriesInProgress === 0) {\n            this.onComplete();\n          }\n        });\n      }\n      return entry;\n    });\n    const groupedEntries = this._entries.reduce((acc, entry) => {\n      if (!entry.meta) {\n        return acc;\n      }\n      const { name, callback } = entry.uploader(liveSocket.uploaders);\n      acc[name] = acc[name] || { callback, entries: [] };\n      acc[name].entries.push(entry);\n      return acc;\n    }, {});\n    for (const name in groupedEntries) {\n      const { callback, entries } = groupedEntries[name];\n      callback(entries, onError, resp, liveSocket);\n    }\n  }\n};\n\n// js/phoenix_live_view/aria.js\nvar ARIA = {\n  anyOf(instance, classes) {\n    return classes.find((name) => instance instanceof name);\n  },\n  isFocusable(el, interactiveOnly) {\n    return el instanceof HTMLAnchorElement && el.rel !== \"ignore\" || el instanceof HTMLAreaElement && el.href !== void 0 || !el.disabled && this.anyOf(el, [\n      HTMLInputElement,\n      HTMLSelectElement,\n      HTMLTextAreaElement,\n      HTMLButtonElement\n    ]) || el instanceof HTMLIFrameElement || el.tabIndex >= 0 && el.getAttribute(\"aria-hidden\") !== \"true\" || !interactiveOnly && el.getAttribute(\"tabindex\") !== null && el.getAttribute(\"aria-hidden\") !== \"true\";\n  },\n  attemptFocus(el, interactiveOnly) {\n    if (this.isFocusable(el, interactiveOnly)) {\n      try {\n        el.focus();\n      } catch {\n      }\n    }\n    return !!document.activeElement && document.activeElement.isSameNode(el);\n  },\n  focusFirstInteractive(el) {\n    let child = el.firstElementChild;\n    while (child) {\n      if (this.attemptFocus(child, true) || this.focusFirstInteractive(child)) {\n        return true;\n      }\n      child = child.nextElementSibling;\n    }\n  },\n  focusFirst(el) {\n    let child = el.firstElementChild;\n    while (child) {\n      if (this.attemptFocus(child) || this.focusFirst(child)) {\n        return true;\n      }\n      child = child.nextElementSibling;\n    }\n  },\n  focusLast(el) {\n    let child = el.lastElementChild;\n    while (child) {\n      if (this.attemptFocus(child) || this.focusLast(child)) {\n        return true;\n      }\n      child = child.previousElementSibling;\n    }\n  }\n};\nvar aria_default = ARIA;\n\n// js/phoenix_live_view/hooks.js\nvar Hooks = {\n  LiveFileUpload: {\n    activeRefs() {\n      return this.el.getAttribute(PHX_ACTIVE_ENTRY_REFS);\n    },\n    preflightedRefs() {\n      return this.el.getAttribute(PHX_PREFLIGHTED_REFS);\n    },\n    mounted() {\n      this.js().ignoreAttributes(this.el, [\"value\"]);\n      this.preflightedWas = this.preflightedRefs();\n    },\n    updated() {\n      const newPreflights = this.preflightedRefs();\n      if (this.preflightedWas !== newPreflights) {\n        this.preflightedWas = newPreflights;\n        if (newPreflights === \"\") {\n          this.__view().cancelSubmit(this.el.form);\n        }\n      }\n      if (this.activeRefs() === \"\") {\n        this.el.value = null;\n      }\n      this.el.dispatchEvent(new CustomEvent(PHX_LIVE_FILE_UPDATED));\n    }\n  },\n  LiveImgPreview: {\n    mounted() {\n      this.ref = this.el.getAttribute(\"data-phx-entry-ref\");\n      this.inputEl = document.getElementById(\n        this.el.getAttribute(PHX_UPLOAD_REF)\n      );\n      LiveUploader.getEntryDataURL(this.inputEl, this.ref, (url) => {\n        this.url = url;\n        this.el.src = url;\n      });\n    },\n    destroyed() {\n      URL.revokeObjectURL(this.url);\n    }\n  },\n  FocusWrap: {\n    mounted() {\n      this.focusStart = this.el.firstElementChild;\n      this.focusEnd = this.el.lastElementChild;\n      this.focusStart.addEventListener(\"focus\", (e) => {\n        if (!e.relatedTarget || !this.el.contains(e.relatedTarget)) {\n          const nextFocus = e.target.nextElementSibling;\n          aria_default.attemptFocus(nextFocus) || aria_default.focusFirst(nextFocus);\n        } else {\n          aria_default.focusLast(this.el);\n        }\n      });\n      this.focusEnd.addEventListener(\"focus\", (e) => {\n        if (!e.relatedTarget || !this.el.contains(e.relatedTarget)) {\n          const nextFocus = e.target.previousElementSibling;\n          aria_default.attemptFocus(nextFocus) || aria_default.focusLast(nextFocus);\n        } else {\n          aria_default.focusFirst(this.el);\n        }\n      });\n      if (!this.el.contains(document.activeElement)) {\n        this.el.addEventListener(\"phx:show-end\", () => this.el.focus());\n        if (window.getComputedStyle(this.el).display !== \"none\") {\n          aria_default.focusFirst(this.el);\n        }\n      }\n    }\n  }\n};\nvar findScrollContainer = (el) => {\n  if ([\"HTML\", \"BODY\"].indexOf(el.nodeName.toUpperCase()) >= 0)\n    return null;\n  if ([\"scroll\", \"auto\"].indexOf(getComputedStyle(el).overflowY) >= 0)\n    return el;\n  return findScrollContainer(el.parentElement);\n};\nvar scrollTop = (scrollContainer) => {\n  if (scrollContainer) {\n    return scrollContainer.scrollTop;\n  } else {\n    return document.documentElement.scrollTop || document.body.scrollTop;\n  }\n};\nvar bottom = (scrollContainer) => {\n  if (scrollContainer) {\n    return scrollContainer.getBoundingClientRect().bottom;\n  } else {\n    return window.innerHeight || document.documentElement.clientHeight;\n  }\n};\nvar top = (scrollContainer) => {\n  if (scrollContainer) {\n    return scrollContainer.getBoundingClientRect().top;\n  } else {\n    return 0;\n  }\n};\nvar isAtViewportTop = (el, scrollContainer) => {\n  const rect = el.getBoundingClientRect();\n  return Math.ceil(rect.top) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.top) <= bottom(scrollContainer);\n};\nvar isAtViewportBottom = (el, scrollContainer) => {\n  const rect = el.getBoundingClientRect();\n  return Math.ceil(rect.bottom) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.bottom) <= bottom(scrollContainer);\n};\nvar isWithinViewport = (el, scrollContainer) => {\n  const rect = el.getBoundingClientRect();\n  return Math.ceil(rect.top) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.top) <= bottom(scrollContainer);\n};\nHooks.InfiniteScroll = {\n  mounted() {\n    this.scrollContainer = findScrollContainer(this.el);\n    let scrollBefore = scrollTop(this.scrollContainer);\n    let topOverran = false;\n    const throttleInterval = 500;\n    let pendingOp = null;\n    const onTopOverrun = this.throttle(\n      throttleInterval,\n      (topEvent, firstChild) => {\n        pendingOp = () => true;\n        this.liveSocket.js().push(this.el, topEvent, {\n          value: { id: firstChild.id, _overran: true },\n          callback: () => {\n            pendingOp = null;\n          }\n        });\n      }\n    );\n    const onFirstChildAtTop = this.throttle(\n      throttleInterval,\n      (topEvent, firstChild) => {\n        pendingOp = () => firstChild.scrollIntoView({ block: \"start\" });\n        this.liveSocket.js().push(this.el, topEvent, {\n          value: { id: firstChild.id },\n          callback: () => {\n            pendingOp = null;\n            window.requestAnimationFrame(() => {\n              if (!isWithinViewport(firstChild, this.scrollContainer)) {\n                firstChild.scrollIntoView({ block: \"start\" });\n              }\n            });\n          }\n        });\n      }\n    );\n    const onLastChildAtBottom = this.throttle(\n      throttleInterval,\n      (bottomEvent, lastChild) => {\n        pendingOp = () => lastChild.scrollIntoView({ block: \"end\" });\n        this.liveSocket.js().push(this.el, bottomEvent, {\n          value: { id: lastChild.id },\n          callback: () => {\n            pendingOp = null;\n            window.requestAnimationFrame(() => {\n              if (!isWithinViewport(lastChild, this.scrollContainer)) {\n                lastChild.scrollIntoView({ block: \"end\" });\n              }\n            });\n          }\n        });\n      }\n    );\n    this.onScroll = (_e) => {\n      const scrollNow = scrollTop(this.scrollContainer);\n      if (pendingOp) {\n        scrollBefore = scrollNow;\n        return pendingOp();\n      }\n      const rect = this.findOverrunTarget();\n      const topEvent = this.el.getAttribute(\n        this.liveSocket.binding(\"viewport-top\")\n      );\n      const bottomEvent = this.el.getAttribute(\n        this.liveSocket.binding(\"viewport-bottom\")\n      );\n      const lastChild = this.el.lastElementChild;\n      const firstChild = this.el.firstElementChild;\n      const isScrollingUp = scrollNow < scrollBefore;\n      const isScrollingDown = scrollNow > scrollBefore;\n      if (isScrollingUp && topEvent && !topOverran && rect.top >= 0) {\n        topOverran = true;\n        onTopOverrun(topEvent, firstChild);\n      } else if (isScrollingDown && topOverran && rect.top <= 0) {\n        topOverran = false;\n      }\n      if (topEvent && isScrollingUp && isAtViewportTop(firstChild, this.scrollContainer)) {\n        onFirstChildAtTop(topEvent, firstChild);\n      } else if (bottomEvent && isScrollingDown && isAtViewportBottom(lastChild, this.scrollContainer)) {\n        onLastChildAtBottom(bottomEvent, lastChild);\n      }\n      scrollBefore = scrollNow;\n    };\n    if (this.scrollContainer) {\n      this.scrollContainer.addEventListener(\"scroll\", this.onScroll);\n    } else {\n      window.addEventListener(\"scroll\", this.onScroll);\n    }\n  },\n  destroyed() {\n    if (this.scrollContainer) {\n      this.scrollContainer.removeEventListener(\"scroll\", this.onScroll);\n    } else {\n      window.removeEventListener(\"scroll\", this.onScroll);\n    }\n  },\n  throttle(interval, callback) {\n    let lastCallAt = 0;\n    let timer;\n    return (...args) => {\n      const now = Date.now();\n      const remainingTime = interval - (now - lastCallAt);\n      if (remainingTime <= 0 || remainingTime > interval) {\n        if (timer) {\n          clearTimeout(timer);\n          timer = null;\n        }\n        lastCallAt = now;\n        callback(...args);\n      } else if (!timer) {\n        timer = setTimeout(() => {\n          lastCallAt = Date.now();\n          timer = null;\n          callback(...args);\n        }, remainingTime);\n      }\n    };\n  },\n  findOverrunTarget() {\n    let rect;\n    const overrunTarget = this.el.getAttribute(\n      this.liveSocket.binding(PHX_VIEWPORT_OVERRUN_TARGET)\n    );\n    if (overrunTarget) {\n      const overrunEl = document.getElementById(overrunTarget);\n      if (overrunEl) {\n        rect = overrunEl.getBoundingClientRect();\n      } else {\n        throw new Error(\"did not find element with id \" + overrunTarget);\n      }\n    } else {\n      rect = this.el.getBoundingClientRect();\n    }\n    return rect;\n  }\n};\nvar hooks_default = Hooks;\n\n// js/phoenix_live_view/element_ref.js\nvar ElementRef = class {\n  static onUnlock(el, callback) {\n    if (!dom_default.isLocked(el) && !el.closest(`[${PHX_REF_LOCK}]`)) {\n      return callback();\n    }\n    const closestLock = el.closest(`[${PHX_REF_LOCK}]`);\n    const ref = closestLock.closest(`[${PHX_REF_LOCK}]`).getAttribute(PHX_REF_LOCK);\n    closestLock.addEventListener(\n      `phx:undo-lock:${ref}`,\n      () => {\n        callback();\n      },\n      { once: true }\n    );\n  }\n  constructor(el) {\n    this.el = el;\n    this.loadingRef = el.hasAttribute(PHX_REF_LOADING) ? parseInt(el.getAttribute(PHX_REF_LOADING), 10) : null;\n    this.lockRef = el.hasAttribute(PHX_REF_LOCK) ? parseInt(el.getAttribute(PHX_REF_LOCK), 10) : null;\n  }\n  // public\n  maybeUndo(ref, phxEvent, eachCloneCallback) {\n    if (!this.isWithin(ref)) {\n      dom_default.updatePrivate(this.el, PHX_PENDING_REFS, [], (pendingRefs) => {\n        pendingRefs.push(ref);\n        return pendingRefs;\n      });\n      return;\n    }\n    this.undoLocks(ref, phxEvent, eachCloneCallback);\n    this.undoLoading(ref, phxEvent);\n    dom_default.updatePrivate(this.el, PHX_PENDING_REFS, [], (pendingRefs) => {\n      return pendingRefs.filter((pendingRef) => {\n        let opts = {\n          detail: { ref: pendingRef, event: phxEvent },\n          bubbles: true,\n          cancelable: false\n        };\n        if (this.loadingRef && this.loadingRef > pendingRef) {\n          this.el.dispatchEvent(\n            new CustomEvent(`phx:undo-loading:${pendingRef}`, opts)\n          );\n        }\n        if (this.lockRef && this.lockRef > pendingRef) {\n          this.el.dispatchEvent(\n            new CustomEvent(`phx:undo-lock:${pendingRef}`, opts)\n          );\n        }\n        return pendingRef > ref;\n      });\n    });\n    if (this.isFullyResolvedBy(ref)) {\n      this.el.removeAttribute(PHX_REF_SRC);\n    }\n  }\n  // private\n  isWithin(ref) {\n    return !(this.loadingRef !== null && this.loadingRef > ref && this.lockRef !== null && this.lockRef > ref);\n  }\n  // Check for cloned PHX_REF_LOCK element that has been morphed behind\n  // the scenes while this element was locked in the DOM.\n  // When we apply the cloned tree to the active DOM element, we must\n  //\n  //   1. execute pending mounted hooks for nodes now in the DOM\n  //   2. undo any ref inside the cloned tree that has since been ack'd\n  undoLocks(ref, phxEvent, eachCloneCallback) {\n    if (!this.isLockUndoneBy(ref)) {\n      return;\n    }\n    const clonedTree = dom_default.private(this.el, PHX_REF_LOCK);\n    if (clonedTree) {\n      eachCloneCallback(clonedTree);\n      dom_default.deletePrivate(this.el, PHX_REF_LOCK);\n    }\n    this.el.removeAttribute(PHX_REF_LOCK);\n    const opts = {\n      detail: { ref, event: phxEvent },\n      bubbles: true,\n      cancelable: false\n    };\n    this.el.dispatchEvent(\n      new CustomEvent(`phx:undo-lock:${this.lockRef}`, opts)\n    );\n  }\n  undoLoading(ref, phxEvent) {\n    if (!this.isLoadingUndoneBy(ref)) {\n      if (this.canUndoLoading(ref) && this.el.classList.contains(\"phx-submit-loading\")) {\n        this.el.classList.remove(\"phx-change-loading\");\n      }\n      return;\n    }\n    if (this.canUndoLoading(ref)) {\n      this.el.removeAttribute(PHX_REF_LOADING);\n      const disabledVal = this.el.getAttribute(PHX_DISABLED);\n      const readOnlyVal = this.el.getAttribute(PHX_READONLY);\n      if (readOnlyVal !== null) {\n        this.el.readOnly = readOnlyVal === \"true\" ? true : false;\n        this.el.removeAttribute(PHX_READONLY);\n      }\n      if (disabledVal !== null) {\n        this.el.disabled = disabledVal === \"true\" ? true : false;\n        this.el.removeAttribute(PHX_DISABLED);\n      }\n      const disableRestore = this.el.getAttribute(PHX_DISABLE_WITH_RESTORE);\n      if (disableRestore !== null) {\n        this.el.textContent = disableRestore;\n        this.el.removeAttribute(PHX_DISABLE_WITH_RESTORE);\n      }\n      const opts = {\n        detail: { ref, event: phxEvent },\n        bubbles: true,\n        cancelable: false\n      };\n      this.el.dispatchEvent(\n        new CustomEvent(`phx:undo-loading:${this.loadingRef}`, opts)\n      );\n    }\n    PHX_EVENT_CLASSES.forEach((name) => {\n      if (name !== \"phx-submit-loading\" || this.canUndoLoading(ref)) {\n        dom_default.removeClass(this.el, name);\n      }\n    });\n  }\n  isLoadingUndoneBy(ref) {\n    return this.loadingRef === null ? false : this.loadingRef <= ref;\n  }\n  isLockUndoneBy(ref) {\n    return this.lockRef === null ? false : this.lockRef <= ref;\n  }\n  isFullyResolvedBy(ref) {\n    return (this.loadingRef === null || this.loadingRef <= ref) && (this.lockRef === null || this.lockRef <= ref);\n  }\n  // only remove the phx-submit-loading class if we are not locked\n  canUndoLoading(ref) {\n    return this.lockRef === null || this.lockRef <= ref;\n  }\n};\n\n// js/phoenix_live_view/dom_post_morph_restorer.js\nvar DOMPostMorphRestorer = class {\n  constructor(containerBefore, containerAfter, updateType) {\n    const idsBefore = /* @__PURE__ */ new Set();\n    const idsAfter = new Set(\n      [...containerAfter.children].map((child) => child.id)\n    );\n    const elementsToModify = [];\n    Array.from(containerBefore.children).forEach((child) => {\n      if (child.id) {\n        idsBefore.add(child.id);\n        if (idsAfter.has(child.id)) {\n          const previousElementId = child.previousElementSibling && child.previousElementSibling.id;\n          elementsToModify.push({\n            elementId: child.id,\n            previousElementId\n          });\n        }\n      }\n    });\n    this.containerId = containerAfter.id;\n    this.updateType = updateType;\n    this.elementsToModify = elementsToModify;\n    this.elementIdsToAdd = [...idsAfter].filter((id) => !idsBefore.has(id));\n  }\n  // We do the following to optimize append/prepend operations:\n  //   1) Track ids of modified elements & of new elements\n  //   2) All the modified elements are put back in the correct position in the DOM tree\n  //      by storing the id of their previous sibling\n  //   3) New elements are going to be put in the right place by morphdom during append.\n  //      For prepend, we move them to the first position in the container\n  perform() {\n    const container = dom_default.byId(this.containerId);\n    if (!container) {\n      return;\n    }\n    this.elementsToModify.forEach((elementToModify) => {\n      if (elementToModify.previousElementId) {\n        maybe(\n          document.getElementById(elementToModify.previousElementId),\n          (previousElem) => {\n            maybe(\n              document.getElementById(elementToModify.elementId),\n              (elem) => {\n                const isInRightPlace = elem.previousElementSibling && elem.previousElementSibling.id == previousElem.id;\n                if (!isInRightPlace) {\n                  previousElem.insertAdjacentElement(\"afterend\", elem);\n                }\n              }\n            );\n          }\n        );\n      } else {\n        maybe(document.getElementById(elementToModify.elementId), (elem) => {\n          const isInRightPlace = elem.previousElementSibling == null;\n          if (!isInRightPlace) {\n            container.insertAdjacentElement(\"afterbegin\", elem);\n          }\n        });\n      }\n    });\n    if (this.updateType == \"prepend\") {\n      this.elementIdsToAdd.reverse().forEach((elemId) => {\n        maybe(\n          document.getElementById(elemId),\n          (elem) => container.insertAdjacentElement(\"afterbegin\", elem)\n        );\n      });\n    }\n  }\n};\n\n// ../node_modules/morphdom/dist/morphdom-esm.js\nvar DOCUMENT_FRAGMENT_NODE = 11;\nfunction morphAttrs(fromNode, toNode) {\n  var toNodeAttrs = toNode.attributes;\n  var attr;\n  var attrName;\n  var attrNamespaceURI;\n  var attrValue;\n  var fromValue;\n  if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) {\n    return;\n  }\n  for (var i = toNodeAttrs.length - 1; i >= 0; i--) {\n    attr = toNodeAttrs[i];\n    attrName = attr.name;\n    attrNamespaceURI = attr.namespaceURI;\n    attrValue = attr.value;\n    if (attrNamespaceURI) {\n      attrName = attr.localName || attrName;\n      fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName);\n      if (fromValue !== attrValue) {\n        if (attr.prefix === \"xmlns\") {\n          attrName = attr.name;\n        }\n        fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue);\n      }\n    } else {\n      fromValue = fromNode.getAttribute(attrName);\n      if (fromValue !== attrValue) {\n        fromNode.setAttribute(attrName, attrValue);\n      }\n    }\n  }\n  var fromNodeAttrs = fromNode.attributes;\n  for (var d = fromNodeAttrs.length - 1; d >= 0; d--) {\n    attr = fromNodeAttrs[d];\n    attrName = attr.name;\n    attrNamespaceURI = attr.namespaceURI;\n    if (attrNamespaceURI) {\n      attrName = attr.localName || attrName;\n      if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) {\n        fromNode.removeAttributeNS(attrNamespaceURI, attrName);\n      }\n    } else {\n      if (!toNode.hasAttribute(attrName)) {\n        fromNode.removeAttribute(attrName);\n      }\n    }\n  }\n}\nvar range;\nvar NS_XHTML = \"http://www.w3.org/1999/xhtml\";\nvar doc = typeof document === \"undefined\" ? void 0 : document;\nvar HAS_TEMPLATE_SUPPORT = !!doc && \"content\" in doc.createElement(\"template\");\nvar HAS_RANGE_SUPPORT = !!doc && doc.createRange && \"createContextualFragment\" in doc.createRange();\nfunction createFragmentFromTemplate(str) {\n  var template = doc.createElement(\"template\");\n  template.innerHTML = str;\n  return template.content.childNodes[0];\n}\nfunction createFragmentFromRange(str) {\n  if (!range) {\n    range = doc.createRange();\n    range.selectNode(doc.body);\n  }\n  var fragment = range.createContextualFragment(str);\n  return fragment.childNodes[0];\n}\nfunction createFragmentFromWrap(str) {\n  var fragment = doc.createElement(\"body\");\n  fragment.innerHTML = str;\n  return fragment.childNodes[0];\n}\nfunction toElement(str) {\n  str = str.trim();\n  if (HAS_TEMPLATE_SUPPORT) {\n    return createFragmentFromTemplate(str);\n  } else if (HAS_RANGE_SUPPORT) {\n    return createFragmentFromRange(str);\n  }\n  return createFragmentFromWrap(str);\n}\nfunction compareNodeNames(fromEl, toEl) {\n  var fromNodeName = fromEl.nodeName;\n  var toNodeName = toEl.nodeName;\n  var fromCodeStart, toCodeStart;\n  if (fromNodeName === toNodeName) {\n    return true;\n  }\n  fromCodeStart = fromNodeName.charCodeAt(0);\n  toCodeStart = toNodeName.charCodeAt(0);\n  if (fromCodeStart <= 90 && toCodeStart >= 97) {\n    return fromNodeName === toNodeName.toUpperCase();\n  } else if (toCodeStart <= 90 && fromCodeStart >= 97) {\n    return toNodeName === fromNodeName.toUpperCase();\n  } else {\n    return false;\n  }\n}\nfunction createElementNS(name, namespaceURI) {\n  return !namespaceURI || namespaceURI === NS_XHTML ? doc.createElement(name) : doc.createElementNS(namespaceURI, name);\n}\nfunction moveChildren(fromEl, toEl) {\n  var curChild = fromEl.firstChild;\n  while (curChild) {\n    var nextChild = curChild.nextSibling;\n    toEl.appendChild(curChild);\n    curChild = nextChild;\n  }\n  return toEl;\n}\nfunction syncBooleanAttrProp(fromEl, toEl, name) {\n  if (fromEl[name] !== toEl[name]) {\n    fromEl[name] = toEl[name];\n    if (fromEl[name]) {\n      fromEl.setAttribute(name, \"\");\n    } else {\n      fromEl.removeAttribute(name);\n    }\n  }\n}\nvar specialElHandlers = {\n  OPTION: function(fromEl, toEl) {\n    var parentNode = fromEl.parentNode;\n    if (parentNode) {\n      var parentName = parentNode.nodeName.toUpperCase();\n      if (parentName === \"OPTGROUP\") {\n        parentNode = parentNode.parentNode;\n        parentName = parentNode && parentNode.nodeName.toUpperCase();\n      }\n      if (parentName === \"SELECT\" && !parentNode.hasAttribute(\"multiple\")) {\n        if (fromEl.hasAttribute(\"selected\") && !toEl.selected) {\n          fromEl.setAttribute(\"selected\", \"selected\");\n          fromEl.removeAttribute(\"selected\");\n        }\n        parentNode.selectedIndex = -1;\n      }\n    }\n    syncBooleanAttrProp(fromEl, toEl, \"selected\");\n  },\n  /**\n   * The \"value\" attribute is special for the <input> element since it sets\n   * the initial value. Changing the \"value\" attribute without changing the\n   * \"value\" property will have no effect since it is only used to the set the\n   * initial value.  Similar for the \"checked\" attribute, and \"disabled\".\n   */\n  INPUT: function(fromEl, toEl) {\n    syncBooleanAttrProp(fromEl, toEl, \"checked\");\n    syncBooleanAttrProp(fromEl, toEl, \"disabled\");\n    if (fromEl.value !== toEl.value) {\n      fromEl.value = toEl.value;\n    }\n    if (!toEl.hasAttribute(\"value\")) {\n      fromEl.removeAttribute(\"value\");\n    }\n  },\n  TEXTAREA: function(fromEl, toEl) {\n    var newValue = toEl.value;\n    if (fromEl.value !== newValue) {\n      fromEl.value = newValue;\n    }\n    var firstChild = fromEl.firstChild;\n    if (firstChild) {\n      var oldValue = firstChild.nodeValue;\n      if (oldValue == newValue || !newValue && oldValue == fromEl.placeholder) {\n        return;\n      }\n      firstChild.nodeValue = newValue;\n    }\n  },\n  SELECT: function(fromEl, toEl) {\n    if (!toEl.hasAttribute(\"multiple\")) {\n      var selectedIndex = -1;\n      var i = 0;\n      var curChild = fromEl.firstChild;\n      var optgroup;\n      var nodeName;\n      while (curChild) {\n        nodeName = curChild.nodeName && curChild.nodeName.toUpperCase();\n        if (nodeName === \"OPTGROUP\") {\n          optgroup = curChild;\n          curChild = optgroup.firstChild;\n          if (!curChild) {\n            curChild = optgroup.nextSibling;\n            optgroup = null;\n          }\n        } else {\n          if (nodeName === \"OPTION\") {\n            if (curChild.hasAttribute(\"selected\")) {\n              selectedIndex = i;\n              break;\n            }\n            i++;\n          }\n          curChild = curChild.nextSibling;\n          if (!curChild && optgroup) {\n            curChild = optgroup.nextSibling;\n            optgroup = null;\n          }\n        }\n      }\n      fromEl.selectedIndex = selectedIndex;\n    }\n  }\n};\nvar ELEMENT_NODE = 1;\nvar DOCUMENT_FRAGMENT_NODE$1 = 11;\nvar TEXT_NODE = 3;\nvar COMMENT_NODE = 8;\nfunction noop() {\n}\nfunction defaultGetNodeKey(node) {\n  if (node) {\n    return node.getAttribute && node.getAttribute(\"id\") || node.id;\n  }\n}\nfunction morphdomFactory(morphAttrs2) {\n  return function morphdom2(fromNode, toNode, options) {\n    if (!options) {\n      options = {};\n    }\n    if (typeof toNode === \"string\") {\n      if (fromNode.nodeName === \"#document\" || fromNode.nodeName === \"HTML\") {\n        var toNodeHtml = toNode;\n        toNode = doc.createElement(\"html\");\n        toNode.innerHTML = toNodeHtml;\n      } else if (fromNode.nodeName === \"BODY\") {\n        var toNodeBody = toNode;\n        toNode = doc.createElement(\"html\");\n        toNode.innerHTML = toNodeBody;\n        var bodyElement = toNode.querySelector(\"body\");\n        if (bodyElement) {\n          toNode = bodyElement;\n        }\n      } else {\n        toNode = toElement(toNode);\n      }\n    } else if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE$1) {\n      toNode = toNode.firstElementChild;\n    }\n    var getNodeKey = options.getNodeKey || defaultGetNodeKey;\n    var onBeforeNodeAdded = options.onBeforeNodeAdded || noop;\n    var onNodeAdded = options.onNodeAdded || noop;\n    var onBeforeElUpdated = options.onBeforeElUpdated || noop;\n    var onElUpdated = options.onElUpdated || noop;\n    var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop;\n    var onNodeDiscarded = options.onNodeDiscarded || noop;\n    var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop;\n    var skipFromChildren = options.skipFromChildren || noop;\n    var addChild = options.addChild || function(parent, child) {\n      return parent.appendChild(child);\n    };\n    var childrenOnly = options.childrenOnly === true;\n    var fromNodesLookup = /* @__PURE__ */ Object.create(null);\n    var keyedRemovalList = [];\n    function addKeyedRemoval(key) {\n      keyedRemovalList.push(key);\n    }\n    function walkDiscardedChildNodes(node, skipKeyedNodes) {\n      if (node.nodeType === ELEMENT_NODE) {\n        var curChild = node.firstChild;\n        while (curChild) {\n          var key = void 0;\n          if (skipKeyedNodes && (key = getNodeKey(curChild))) {\n            addKeyedRemoval(key);\n          } else {\n            onNodeDiscarded(curChild);\n            if (curChild.firstChild) {\n              walkDiscardedChildNodes(curChild, skipKeyedNodes);\n            }\n          }\n          curChild = curChild.nextSibling;\n        }\n      }\n    }\n    function removeNode(node, parentNode, skipKeyedNodes) {\n      if (onBeforeNodeDiscarded(node) === false) {\n        return;\n      }\n      if (parentNode) {\n        parentNode.removeChild(node);\n      }\n      onNodeDiscarded(node);\n      walkDiscardedChildNodes(node, skipKeyedNodes);\n    }\n    function indexTree(node) {\n      if (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE$1) {\n        var curChild = node.firstChild;\n        while (curChild) {\n          var key = getNodeKey(curChild);\n          if (key) {\n            fromNodesLookup[key] = curChild;\n          }\n          indexTree(curChild);\n          curChild = curChild.nextSibling;\n        }\n      }\n    }\n    indexTree(fromNode);\n    function handleNodeAdded(el) {\n      onNodeAdded(el);\n      var curChild = el.firstChild;\n      while (curChild) {\n        var nextSibling = curChild.nextSibling;\n        var key = getNodeKey(curChild);\n        if (key) {\n          var unmatchedFromEl = fromNodesLookup[key];\n          if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) {\n            curChild.parentNode.replaceChild(unmatchedFromEl, curChild);\n            morphEl(unmatchedFromEl, curChild);\n          } else {\n            handleNodeAdded(curChild);\n          }\n        } else {\n          handleNodeAdded(curChild);\n        }\n        curChild = nextSibling;\n      }\n    }\n    function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) {\n      while (curFromNodeChild) {\n        var fromNextSibling = curFromNodeChild.nextSibling;\n        if (curFromNodeKey = getNodeKey(curFromNodeChild)) {\n          addKeyedRemoval(curFromNodeKey);\n        } else {\n          removeNode(\n            curFromNodeChild,\n            fromEl,\n            true\n            /* skip keyed nodes */\n          );\n        }\n        curFromNodeChild = fromNextSibling;\n      }\n    }\n    function morphEl(fromEl, toEl, childrenOnly2) {\n      var toElKey = getNodeKey(toEl);\n      if (toElKey) {\n        delete fromNodesLookup[toElKey];\n      }\n      if (!childrenOnly2) {\n        var beforeUpdateResult = onBeforeElUpdated(fromEl, toEl);\n        if (beforeUpdateResult === false) {\n          return;\n        } else if (beforeUpdateResult instanceof HTMLElement) {\n          fromEl = beforeUpdateResult;\n          indexTree(fromEl);\n        }\n        morphAttrs2(fromEl, toEl);\n        onElUpdated(fromEl);\n        if (onBeforeElChildrenUpdated(fromEl, toEl) === false) {\n          return;\n        }\n      }\n      if (fromEl.nodeName !== \"TEXTAREA\") {\n        morphChildren(fromEl, toEl);\n      } else {\n        specialElHandlers.TEXTAREA(fromEl, toEl);\n      }\n    }\n    function morphChildren(fromEl, toEl) {\n      var skipFrom = skipFromChildren(fromEl, toEl);\n      var curToNodeChild = toEl.firstChild;\n      var curFromNodeChild = fromEl.firstChild;\n      var curToNodeKey;\n      var curFromNodeKey;\n      var fromNextSibling;\n      var toNextSibling;\n      var matchingFromEl;\n      outer:\n        while (curToNodeChild) {\n          toNextSibling = curToNodeChild.nextSibling;\n          curToNodeKey = getNodeKey(curToNodeChild);\n          while (!skipFrom && curFromNodeChild) {\n            fromNextSibling = curFromNodeChild.nextSibling;\n            if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) {\n              curToNodeChild = toNextSibling;\n              curFromNodeChild = fromNextSibling;\n              continue outer;\n            }\n            curFromNodeKey = getNodeKey(curFromNodeChild);\n            var curFromNodeType = curFromNodeChild.nodeType;\n            var isCompatible = void 0;\n            if (curFromNodeType === curToNodeChild.nodeType) {\n              if (curFromNodeType === ELEMENT_NODE) {\n                if (curToNodeKey) {\n                  if (curToNodeKey !== curFromNodeKey) {\n                    if (matchingFromEl = fromNodesLookup[curToNodeKey]) {\n                      if (fromNextSibling === matchingFromEl) {\n                        isCompatible = false;\n                      } else {\n                        fromEl.insertBefore(matchingFromEl, curFromNodeChild);\n                        if (curFromNodeKey) {\n                          addKeyedRemoval(curFromNodeKey);\n                        } else {\n                          removeNode(\n                            curFromNodeChild,\n                            fromEl,\n                            true\n                            /* skip keyed nodes */\n                          );\n                        }\n                        curFromNodeChild = matchingFromEl;\n                        curFromNodeKey = getNodeKey(curFromNodeChild);\n                      }\n                    } else {\n                      isCompatible = false;\n                    }\n                  }\n                } else if (curFromNodeKey) {\n                  isCompatible = false;\n                }\n                isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild);\n                if (isCompatible) {\n                  morphEl(curFromNodeChild, curToNodeChild);\n                }\n              } else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) {\n                isCompatible = true;\n                if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) {\n                  curFromNodeChild.nodeValue = curToNodeChild.nodeValue;\n                }\n              }\n            }\n            if (isCompatible) {\n              curToNodeChild = toNextSibling;\n              curFromNodeChild = fromNextSibling;\n              continue outer;\n            }\n            if (curFromNodeKey) {\n              addKeyedRemoval(curFromNodeKey);\n            } else {\n              removeNode(\n                curFromNodeChild,\n                fromEl,\n                true\n                /* skip keyed nodes */\n              );\n            }\n            curFromNodeChild = fromNextSibling;\n          }\n          if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) {\n            if (!skipFrom) {\n              addChild(fromEl, matchingFromEl);\n            }\n            morphEl(matchingFromEl, curToNodeChild);\n          } else {\n            var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild);\n            if (onBeforeNodeAddedResult !== false) {\n              if (onBeforeNodeAddedResult) {\n                curToNodeChild = onBeforeNodeAddedResult;\n              }\n              if (curToNodeChild.actualize) {\n                curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc);\n              }\n              addChild(fromEl, curToNodeChild);\n              handleNodeAdded(curToNodeChild);\n            }\n          }\n          curToNodeChild = toNextSibling;\n          curFromNodeChild = fromNextSibling;\n        }\n      cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey);\n      var specialElHandler = specialElHandlers[fromEl.nodeName];\n      if (specialElHandler) {\n        specialElHandler(fromEl, toEl);\n      }\n    }\n    var morphedNode = fromNode;\n    var morphedNodeType = morphedNode.nodeType;\n    var toNodeType = toNode.nodeType;\n    if (!childrenOnly) {\n      if (morphedNodeType === ELEMENT_NODE) {\n        if (toNodeType === ELEMENT_NODE) {\n          if (!compareNodeNames(fromNode, toNode)) {\n            onNodeDiscarded(fromNode);\n            morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI));\n          }\n        } else {\n          morphedNode = toNode;\n        }\n      } else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) {\n        if (toNodeType === morphedNodeType) {\n          if (morphedNode.nodeValue !== toNode.nodeValue) {\n            morphedNode.nodeValue = toNode.nodeValue;\n          }\n          return morphedNode;\n        } else {\n          morphedNode = toNode;\n        }\n      }\n    }\n    if (morphedNode === toNode) {\n      onNodeDiscarded(fromNode);\n    } else {\n      if (toNode.isSameNode && toNode.isSameNode(morphedNode)) {\n        return;\n      }\n      morphEl(morphedNode, toNode, childrenOnly);\n      if (keyedRemovalList) {\n        for (var i = 0, len = keyedRemovalList.length; i < len; i++) {\n          var elToRemove = fromNodesLookup[keyedRemovalList[i]];\n          if (elToRemove) {\n            removeNode(elToRemove, elToRemove.parentNode, false);\n          }\n        }\n      }\n    }\n    if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) {\n      if (morphedNode.actualize) {\n        morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc);\n      }\n      fromNode.parentNode.replaceChild(morphedNode, fromNode);\n    }\n    return morphedNode;\n  };\n}\nvar morphdom = morphdomFactory(morphAttrs);\nvar morphdom_esm_default = morphdom;\n\n// js/phoenix_live_view/dom_patch.js\nvar DOMPatch = class {\n  constructor(view, container, id, html, streams, targetCID, opts = {}) {\n    this.view = view;\n    this.liveSocket = view.liveSocket;\n    this.container = container;\n    this.id = id;\n    this.rootID = view.root.id;\n    this.html = html;\n    this.streams = streams;\n    this.streamInserts = {};\n    this.streamComponentRestore = {};\n    this.targetCID = targetCID;\n    this.cidPatch = isCid(this.targetCID);\n    this.pendingRemoves = [];\n    this.phxRemove = this.liveSocket.binding(\"remove\");\n    this.targetContainer = this.isCIDPatch() ? this.targetCIDContainer(html) : container;\n    this.callbacks = {\n      beforeadded: [],\n      beforeupdated: [],\n      beforephxChildAdded: [],\n      afteradded: [],\n      afterupdated: [],\n      afterdiscarded: [],\n      afterphxChildAdded: [],\n      aftertransitionsDiscarded: []\n    };\n    this.withChildren = opts.withChildren || opts.undoRef || false;\n    this.undoRef = opts.undoRef;\n  }\n  before(kind, callback) {\n    this.callbacks[`before${kind}`].push(callback);\n  }\n  after(kind, callback) {\n    this.callbacks[`after${kind}`].push(callback);\n  }\n  trackBefore(kind, ...args) {\n    this.callbacks[`before${kind}`].forEach((callback) => callback(...args));\n  }\n  trackAfter(kind, ...args) {\n    this.callbacks[`after${kind}`].forEach((callback) => callback(...args));\n  }\n  markPrunableContentForRemoval() {\n    const phxUpdate = this.liveSocket.binding(PHX_UPDATE);\n    dom_default.all(\n      this.container,\n      `[${phxUpdate}=append] > *, [${phxUpdate}=prepend] > *`,\n      (el) => {\n        el.setAttribute(PHX_PRUNE, \"\");\n      }\n    );\n  }\n  perform(isJoinPatch) {\n    const { view, liveSocket, html, container } = this;\n    let targetContainer = this.targetContainer;\n    if (this.isCIDPatch() && !this.targetContainer) {\n      return;\n    }\n    if (this.isCIDPatch()) {\n      const closestLock = targetContainer.closest(`[${PHX_REF_LOCK}]`);\n      if (closestLock && !closestLock.isSameNode(targetContainer)) {\n        const clonedTree = dom_default.private(closestLock, PHX_REF_LOCK);\n        if (clonedTree) {\n          targetContainer = clonedTree.querySelector(\n            `[data-phx-component=\"${this.targetCID}\"]`\n          );\n        }\n      }\n    }\n    const focused = liveSocket.getActiveElement();\n    const { selectionStart, selectionEnd } = focused && dom_default.hasSelectionRange(focused) ? focused : {};\n    const phxUpdate = liveSocket.binding(PHX_UPDATE);\n    const phxViewportTop = liveSocket.binding(PHX_VIEWPORT_TOP);\n    const phxViewportBottom = liveSocket.binding(PHX_VIEWPORT_BOTTOM);\n    const phxTriggerExternal = liveSocket.binding(PHX_TRIGGER_ACTION);\n    const added = [];\n    const updates = [];\n    const appendPrependUpdates = [];\n    let portalCallbacks = [];\n    let externalFormTriggered = null;\n    const morph = (targetContainer2, source, withChildren = this.withChildren) => {\n      const morphCallbacks = {\n        // normally, we are running with childrenOnly, as the patch HTML for a LV\n        // does not include the LV attrs (data-phx-session, etc.)\n        // when we are patching a live component, we do want to patch the root element as well;\n        // another case is the recursive patch of a stream item that was kept on reset (-> onBeforeNodeAdded)\n        childrenOnly: targetContainer2.getAttribute(PHX_COMPONENT) === null && !withChildren,\n        getNodeKey: (node) => {\n          if (dom_default.isPhxDestroyed(node)) {\n            return null;\n          }\n          if (isJoinPatch) {\n            return node.id;\n          }\n          return node.id || node.getAttribute && node.getAttribute(PHX_MAGIC_ID);\n        },\n        // skip indexing from children when container is stream\n        skipFromChildren: (from) => {\n          return from.getAttribute(phxUpdate) === PHX_STREAM;\n        },\n        // tell morphdom how to add a child\n        addChild: (parent, child) => {\n          const { ref, streamAt } = this.getStreamInsert(child);\n          if (ref === void 0) {\n            return parent.appendChild(child);\n          }\n          this.setStreamRef(child, ref);\n          if (streamAt === 0) {\n            parent.insertAdjacentElement(\"afterbegin\", child);\n          } else if (streamAt === -1) {\n            const lastChild = parent.lastElementChild;\n            if (lastChild && !lastChild.hasAttribute(PHX_STREAM_REF)) {\n              const nonStreamChild = Array.from(parent.children).find(\n                (c) => !c.hasAttribute(PHX_STREAM_REF)\n              );\n              parent.insertBefore(child, nonStreamChild);\n            } else {\n              parent.appendChild(child);\n            }\n          } else if (streamAt > 0) {\n            const sibling = Array.from(parent.children)[streamAt];\n            parent.insertBefore(child, sibling);\n          }\n        },\n        onBeforeNodeAdded: (el) => {\n          if (this.getStreamInsert(el)?.updateOnly && !this.streamComponentRestore[el.id]) {\n            return false;\n          }\n          dom_default.maintainPrivateHooks(el, el, phxViewportTop, phxViewportBottom);\n          this.trackBefore(\"added\", el);\n          let morphedEl = el;\n          if (this.streamComponentRestore[el.id]) {\n            morphedEl = this.streamComponentRestore[el.id];\n            delete this.streamComponentRestore[el.id];\n            morph(morphedEl, el, true);\n          }\n          return morphedEl;\n        },\n        onNodeAdded: (el) => {\n          if (el.getAttribute) {\n            this.maybeReOrderStream(el, true);\n          }\n          if (dom_default.isPortalTemplate(el)) {\n            portalCallbacks.push(() => this.teleport(el, morph));\n          }\n          if (el instanceof HTMLImageElement && el.srcset) {\n            el.srcset = el.srcset;\n          } else if (el instanceof HTMLVideoElement && el.autoplay) {\n            el.play();\n          }\n          if (dom_default.isNowTriggerFormExternal(el, phxTriggerExternal)) {\n            externalFormTriggered = el;\n          }\n          if (dom_default.isPhxChild(el) && view.ownsElement(el) || dom_default.isPhxSticky(el) && view.ownsElement(el.parentNode)) {\n            this.trackAfter(\"phxChildAdded\", el);\n          }\n          if (el.nodeName === \"SCRIPT\" && el.hasAttribute(PHX_RUNTIME_HOOK)) {\n            this.handleRuntimeHook(el, source);\n          }\n          added.push(el);\n        },\n        onNodeDiscarded: (el) => this.onNodeDiscarded(el),\n        onBeforeNodeDiscarded: (el) => {\n          if (el.getAttribute && el.getAttribute(PHX_PRUNE) !== null) {\n            return true;\n          }\n          if (el.parentElement !== null && el.id && dom_default.isPhxUpdate(el.parentElement, phxUpdate, [\n            PHX_STREAM,\n            \"append\",\n            \"prepend\"\n          ])) {\n            return false;\n          }\n          if (el.getAttribute && el.getAttribute(PHX_TELEPORTED_REF)) {\n            return false;\n          }\n          if (this.maybePendingRemove(el)) {\n            return false;\n          }\n          if (this.skipCIDSibling(el)) {\n            return false;\n          }\n          if (dom_default.isPortalTemplate(el)) {\n            const teleportedEl = document.getElementById(\n              el.content.firstElementChild.id\n            );\n            if (teleportedEl) {\n              teleportedEl.remove();\n              morphCallbacks.onNodeDiscarded(teleportedEl);\n              this.view.dropPortalElementId(teleportedEl.id);\n            }\n          }\n          return true;\n        },\n        onElUpdated: (el) => {\n          if (dom_default.isNowTriggerFormExternal(el, phxTriggerExternal)) {\n            externalFormTriggered = el;\n          }\n          updates.push(el);\n          this.maybeReOrderStream(el, false);\n        },\n        onBeforeElUpdated: (fromEl, toEl) => {\n          if (fromEl.id && fromEl.isSameNode(targetContainer2) && fromEl.id !== toEl.id) {\n            morphCallbacks.onNodeDiscarded(fromEl);\n            fromEl.replaceWith(toEl);\n            return morphCallbacks.onNodeAdded(toEl);\n          }\n          dom_default.syncPendingAttrs(fromEl, toEl);\n          dom_default.maintainPrivateHooks(\n            fromEl,\n            toEl,\n            phxViewportTop,\n            phxViewportBottom\n          );\n          dom_default.cleanChildNodes(toEl, phxUpdate);\n          if (this.skipCIDSibling(toEl)) {\n            this.maybeReOrderStream(fromEl);\n            return false;\n          }\n          if (dom_default.isPhxSticky(fromEl)) {\n            [PHX_SESSION, PHX_STATIC, PHX_ROOT_ID].map((attr) => [\n              attr,\n              fromEl.getAttribute(attr),\n              toEl.getAttribute(attr)\n            ]).forEach(([attr, fromVal, toVal]) => {\n              if (toVal && fromVal !== toVal) {\n                fromEl.setAttribute(attr, toVal);\n              }\n            });\n            return false;\n          }\n          if (dom_default.isIgnored(fromEl, phxUpdate) || fromEl.form && fromEl.form.isSameNode(externalFormTriggered)) {\n            this.trackBefore(\"updated\", fromEl, toEl);\n            dom_default.mergeAttrs(fromEl, toEl, {\n              isIgnored: dom_default.isIgnored(fromEl, phxUpdate)\n            });\n            updates.push(fromEl);\n            dom_default.applyStickyOperations(fromEl);\n            return false;\n          }\n          if (fromEl.type === \"number\" && fromEl.validity && fromEl.validity.badInput) {\n            return false;\n          }\n          const isFocusedFormEl = focused && fromEl.isSameNode(focused) && dom_default.isFormInput(fromEl);\n          const focusedSelectChanged = isFocusedFormEl && this.isChangedSelect(fromEl, toEl);\n          if (fromEl.hasAttribute(PHX_REF_SRC)) {\n            const ref = new ElementRef(fromEl);\n            if (ref.lockRef && (!this.undoRef || !ref.isLockUndoneBy(this.undoRef))) {\n              dom_default.applyStickyOperations(fromEl);\n              const isLocked = fromEl.hasAttribute(PHX_REF_LOCK);\n              const clone2 = isLocked ? dom_default.private(fromEl, PHX_REF_LOCK) || fromEl.cloneNode(true) : null;\n              if (clone2) {\n                dom_default.putPrivate(fromEl, PHX_REF_LOCK, clone2);\n                if (!isFocusedFormEl) {\n                  fromEl = clone2;\n                }\n              }\n            }\n          }\n          if (dom_default.isPhxChild(toEl)) {\n            const prevSession = fromEl.getAttribute(PHX_SESSION);\n            dom_default.mergeAttrs(fromEl, toEl, { exclude: [PHX_STATIC] });\n            if (prevSession !== \"\") {\n              fromEl.setAttribute(PHX_SESSION, prevSession);\n            }\n            fromEl.setAttribute(PHX_ROOT_ID, this.rootID);\n            dom_default.applyStickyOperations(fromEl);\n            return false;\n          }\n          if (this.undoRef && dom_default.private(toEl, PHX_REF_LOCK)) {\n            dom_default.putPrivate(\n              fromEl,\n              PHX_REF_LOCK,\n              dom_default.private(toEl, PHX_REF_LOCK)\n            );\n          }\n          dom_default.copyPrivates(toEl, fromEl);\n          if (dom_default.isPortalTemplate(toEl)) {\n            portalCallbacks.push(() => this.teleport(toEl, morph));\n            fromEl.content.replaceChildren(toEl.content.cloneNode(true));\n            return false;\n          }\n          if (isFocusedFormEl && fromEl.type !== \"hidden\" && !focusedSelectChanged) {\n            this.trackBefore(\"updated\", fromEl, toEl);\n            dom_default.mergeFocusedInput(fromEl, toEl);\n            dom_default.syncAttrsToProps(fromEl);\n            updates.push(fromEl);\n            dom_default.applyStickyOperations(fromEl);\n            return false;\n          } else {\n            if (focusedSelectChanged) {\n              fromEl.blur();\n            }\n            if (dom_default.isPhxUpdate(toEl, phxUpdate, [\"append\", \"prepend\"])) {\n              appendPrependUpdates.push(\n                new DOMPostMorphRestorer(\n                  fromEl,\n                  toEl,\n                  toEl.getAttribute(phxUpdate)\n                )\n              );\n            }\n            dom_default.syncAttrsToProps(toEl);\n            dom_default.applyStickyOperations(toEl);\n            this.trackBefore(\"updated\", fromEl, toEl);\n            return fromEl;\n          }\n        }\n      };\n      morphdom_esm_default(targetContainer2, source, morphCallbacks);\n    };\n    this.trackBefore(\"added\", container);\n    this.trackBefore(\"updated\", container, container);\n    liveSocket.time(\"morphdom\", () => {\n      this.streams.forEach(([ref, inserts, deleteIds, reset]) => {\n        inserts.forEach(([key, streamAt, limit, updateOnly]) => {\n          this.streamInserts[key] = { ref, streamAt, limit, reset, updateOnly };\n        });\n        if (reset !== void 0) {\n          dom_default.all(document, `[${PHX_STREAM_REF}=\"${ref}\"]`, (child) => {\n            this.removeStreamChildElement(child);\n          });\n        }\n        deleteIds.forEach((id) => {\n          const child = document.getElementById(id);\n          if (child) {\n            this.removeStreamChildElement(child);\n          }\n        });\n      });\n      if (isJoinPatch) {\n        dom_default.all(this.container, `[${phxUpdate}=${PHX_STREAM}]`).filter((el) => this.view.ownsElement(el)).forEach((el) => {\n          Array.from(el.children).forEach((child) => {\n            this.removeStreamChildElement(child, true);\n          });\n        });\n      }\n      morph(targetContainer, html);\n      let teleportCount = 0;\n      while (portalCallbacks.length > 0 && teleportCount < 5) {\n        const copy = portalCallbacks.slice();\n        portalCallbacks = [];\n        copy.forEach((callback) => callback());\n        teleportCount++;\n      }\n      this.view.portalElementIds.forEach((id) => {\n        const el = document.getElementById(id);\n        if (el) {\n          const source = document.getElementById(\n            el.getAttribute(PHX_TELEPORTED_SRC)\n          );\n          if (!source) {\n            el.remove();\n            this.onNodeDiscarded(el);\n            this.view.dropPortalElementId(id);\n          }\n        }\n      });\n    });\n    if (liveSocket.isDebugEnabled()) {\n      detectDuplicateIds();\n      detectInvalidStreamInserts(this.streamInserts);\n      Array.from(document.querySelectorAll(\"input[name=id]\")).forEach(\n        (node) => {\n          if (node instanceof HTMLInputElement && node.form) {\n            console.error(\n              'Detected an input with name=\"id\" inside a form! This will cause problems when patching the DOM.\\n',\n              node\n            );\n          }\n        }\n      );\n    }\n    if (appendPrependUpdates.length > 0) {\n      liveSocket.time(\"post-morph append/prepend restoration\", () => {\n        appendPrependUpdates.forEach((update) => update.perform());\n      });\n    }\n    liveSocket.silenceEvents(\n      () => dom_default.restoreFocus(focused, selectionStart, selectionEnd)\n    );\n    dom_default.dispatchEvent(document, \"phx:update\");\n    added.forEach((el) => this.trackAfter(\"added\", el));\n    updates.forEach((el) => this.trackAfter(\"updated\", el));\n    this.transitionPendingRemoves();\n    if (externalFormTriggered) {\n      liveSocket.unload();\n      const submitter = dom_default.private(externalFormTriggered, \"submitter\");\n      if (submitter && submitter.name && targetContainer.contains(submitter)) {\n        const input = document.createElement(\"input\");\n        input.type = \"hidden\";\n        const formId = submitter.getAttribute(\"form\");\n        if (formId) {\n          input.setAttribute(\"form\", formId);\n        }\n        input.name = submitter.name;\n        input.value = submitter.value;\n        submitter.parentElement.insertBefore(input, submitter);\n      }\n      Object.getPrototypeOf(externalFormTriggered).submit.call(\n        externalFormTriggered\n      );\n    }\n    return true;\n  }\n  onNodeDiscarded(el) {\n    if (dom_default.isPhxChild(el) || dom_default.isPhxSticky(el)) {\n      this.liveSocket.destroyViewByEl(el);\n    }\n    this.trackAfter(\"discarded\", el);\n  }\n  maybePendingRemove(node) {\n    if (node.getAttribute && node.getAttribute(this.phxRemove) !== null) {\n      this.pendingRemoves.push(node);\n      return true;\n    } else {\n      return false;\n    }\n  }\n  removeStreamChildElement(child, force = false) {\n    if (!force && !this.view.ownsElement(child)) {\n      return;\n    }\n    if (this.streamInserts[child.id]) {\n      this.streamComponentRestore[child.id] = child;\n      child.remove();\n    } else {\n      if (!this.maybePendingRemove(child)) {\n        child.remove();\n        this.onNodeDiscarded(child);\n      }\n    }\n  }\n  getStreamInsert(el) {\n    const insert = el.id ? this.streamInserts[el.id] : {};\n    return insert || {};\n  }\n  setStreamRef(el, ref) {\n    dom_default.putSticky(\n      el,\n      PHX_STREAM_REF,\n      (el2) => el2.setAttribute(PHX_STREAM_REF, ref)\n    );\n  }\n  maybeReOrderStream(el, isNew) {\n    const { ref, streamAt, reset } = this.getStreamInsert(el);\n    if (streamAt === void 0) {\n      return;\n    }\n    this.setStreamRef(el, ref);\n    if (!reset && !isNew) {\n      return;\n    }\n    if (!el.parentElement) {\n      return;\n    }\n    if (streamAt === 0) {\n      el.parentElement.insertBefore(el, el.parentElement.firstElementChild);\n    } else if (streamAt > 0) {\n      const children = Array.from(el.parentElement.children);\n      const oldIndex = children.indexOf(el);\n      if (streamAt >= children.length - 1) {\n        el.parentElement.appendChild(el);\n      } else {\n        const sibling = children[streamAt];\n        if (oldIndex > streamAt) {\n          el.parentElement.insertBefore(el, sibling);\n        } else {\n          el.parentElement.insertBefore(el, sibling.nextElementSibling);\n        }\n      }\n    }\n    this.maybeLimitStream(el);\n  }\n  maybeLimitStream(el) {\n    const { limit } = this.getStreamInsert(el);\n    const children = limit !== null && Array.from(el.parentElement.children);\n    if (limit && limit < 0 && children.length > limit * -1) {\n      children.slice(0, children.length + limit).forEach((child) => this.removeStreamChildElement(child));\n    } else if (limit && limit >= 0 && children.length > limit) {\n      children.slice(limit).forEach((child) => this.removeStreamChildElement(child));\n    }\n  }\n  transitionPendingRemoves() {\n    const { pendingRemoves, liveSocket } = this;\n    if (pendingRemoves.length > 0) {\n      liveSocket.transitionRemoves(pendingRemoves, () => {\n        pendingRemoves.forEach((el) => {\n          const child = dom_default.firstPhxChild(el);\n          if (child) {\n            liveSocket.destroyViewByEl(child);\n          }\n          el.remove();\n        });\n        this.trackAfter(\"transitionsDiscarded\", pendingRemoves);\n      });\n    }\n  }\n  isChangedSelect(fromEl, toEl) {\n    if (!(fromEl instanceof HTMLSelectElement) || fromEl.multiple) {\n      return false;\n    }\n    if (fromEl.options.length !== toEl.options.length) {\n      return true;\n    }\n    toEl.value = fromEl.value;\n    return !fromEl.isEqualNode(toEl);\n  }\n  isCIDPatch() {\n    return this.cidPatch;\n  }\n  skipCIDSibling(el) {\n    return el.nodeType === Node.ELEMENT_NODE && el.hasAttribute(PHX_SKIP);\n  }\n  targetCIDContainer(html) {\n    if (!this.isCIDPatch()) {\n      return;\n    }\n    const [first, ...rest] = dom_default.findComponentNodeList(\n      this.view.id,\n      this.targetCID\n    );\n    if (rest.length === 0 && dom_default.childNodeLength(html) === 1) {\n      return first;\n    } else {\n      return first && first.parentNode;\n    }\n  }\n  indexOf(parent, child) {\n    return Array.from(parent.children).indexOf(child);\n  }\n  teleport(el, morph) {\n    const targetSelector = el.getAttribute(PHX_PORTAL);\n    const portalContainer = document.querySelector(targetSelector);\n    if (!portalContainer) {\n      throw new Error(\n        \"portal target with selector \" + targetSelector + \" not found\"\n      );\n    }\n    const toTeleport = el.content.firstElementChild;\n    if (this.skipCIDSibling(toTeleport)) {\n      return;\n    }\n    if (!toTeleport?.id) {\n      throw new Error(\n        \"phx-portal template must have a single root element with ID!\"\n      );\n    }\n    const existing = document.getElementById(toTeleport.id);\n    let portalTarget;\n    if (existing) {\n      if (!portalContainer.contains(existing)) {\n        portalContainer.appendChild(existing);\n      }\n      portalTarget = existing;\n    } else {\n      portalTarget = document.createElement(toTeleport.tagName);\n      portalContainer.appendChild(portalTarget);\n    }\n    toTeleport.setAttribute(PHX_TELEPORTED_REF, this.view.id);\n    toTeleport.setAttribute(PHX_TELEPORTED_SRC, el.id);\n    morph(portalTarget, toTeleport, true);\n    toTeleport.removeAttribute(PHX_TELEPORTED_REF);\n    toTeleport.removeAttribute(PHX_TELEPORTED_SRC);\n    this.view.pushPortalElementId(toTeleport.id);\n  }\n  handleRuntimeHook(el, source) {\n    const name = el.getAttribute(PHX_RUNTIME_HOOK);\n    let nonce = el.hasAttribute(\"nonce\") ? el.getAttribute(\"nonce\") : null;\n    if (el.hasAttribute(\"nonce\")) {\n      const template = document.createElement(\"template\");\n      template.innerHTML = source;\n      nonce = template.content.querySelector(`script[${PHX_RUNTIME_HOOK}=\"${CSS.escape(name)}\"]`).getAttribute(\"nonce\");\n    }\n    const script = document.createElement(\"script\");\n    script.textContent = el.textContent;\n    dom_default.mergeAttrs(script, el, { isIgnored: false });\n    if (nonce) {\n      script.nonce = nonce;\n    }\n    el.replaceWith(script);\n    el = script;\n  }\n};\n\n// js/phoenix_live_view/rendered.js\nvar VOID_TAGS = /* @__PURE__ */ new Set([\n  \"area\",\n  \"base\",\n  \"br\",\n  \"col\",\n  \"command\",\n  \"embed\",\n  \"hr\",\n  \"img\",\n  \"input\",\n  \"keygen\",\n  \"link\",\n  \"meta\",\n  \"param\",\n  \"source\",\n  \"track\",\n  \"wbr\"\n]);\nvar quoteChars = /* @__PURE__ */ new Set([\"'\", '\"']);\nvar modifyRoot = (html, attrs, clearInnerHTML) => {\n  let i = 0;\n  let insideComment = false;\n  let beforeTag, afterTag, tag, tagNameEndsAt, id, newHTML;\n  const lookahead = html.match(/^(\\s*(?:<!--.*?-->\\s*)*)<([^\\s\\/>]+)/);\n  if (lookahead === null) {\n    throw new Error(`malformed html ${html}`);\n  }\n  i = lookahead[0].length;\n  beforeTag = lookahead[1];\n  tag = lookahead[2];\n  tagNameEndsAt = i;\n  for (i; i < html.length; i++) {\n    if (html.charAt(i) === \">\") {\n      break;\n    }\n    if (html.charAt(i) === \"=\") {\n      const isId = html.slice(i - 3, i) === \" id\";\n      i++;\n      const char = html.charAt(i);\n      if (quoteChars.has(char)) {\n        const attrStartsAt = i;\n        i++;\n        for (i; i < html.length; i++) {\n          if (html.charAt(i) === char) {\n            break;\n          }\n        }\n        if (isId) {\n          id = html.slice(attrStartsAt + 1, i);\n          break;\n        }\n      }\n    }\n  }\n  let closeAt = html.length - 1;\n  insideComment = false;\n  while (closeAt >= beforeTag.length + tag.length) {\n    const char = html.charAt(closeAt);\n    if (insideComment) {\n      if (char === \"-\" && html.slice(closeAt - 3, closeAt) === \"<!-\") {\n        insideComment = false;\n        closeAt -= 4;\n      } else {\n        closeAt -= 1;\n      }\n    } else if (char === \">\" && html.slice(closeAt - 2, closeAt) === \"--\") {\n      insideComment = true;\n      closeAt -= 3;\n    } else if (char === \">\") {\n      break;\n    } else {\n      closeAt -= 1;\n    }\n  }\n  afterTag = html.slice(closeAt + 1, html.length);\n  const attrsStr = Object.keys(attrs).map((attr) => attrs[attr] === true ? attr : `${attr}=\"${attrs[attr]}\"`).join(\" \");\n  if (clearInnerHTML) {\n    const idAttrStr = id ? ` id=\"${id}\"` : \"\";\n    if (VOID_TAGS.has(tag)) {\n      newHTML = `<${tag}${idAttrStr}${attrsStr === \"\" ? \"\" : \" \"}${attrsStr}/>`;\n    } else {\n      newHTML = `<${tag}${idAttrStr}${attrsStr === \"\" ? \"\" : \" \"}${attrsStr}></${tag}>`;\n    }\n  } else {\n    const rest = html.slice(tagNameEndsAt, closeAt + 1);\n    newHTML = `<${tag}${attrsStr === \"\" ? \"\" : \" \"}${attrsStr}${rest}`;\n  }\n  return [newHTML, beforeTag, afterTag];\n};\nvar Rendered = class {\n  static extract(diff) {\n    const { [REPLY]: reply, [EVENTS]: events, [TITLE]: title } = diff;\n    delete diff[REPLY];\n    delete diff[EVENTS];\n    delete diff[TITLE];\n    return { diff, title, reply: reply || null, events: events || [] };\n  }\n  constructor(viewId, rendered) {\n    this.viewId = viewId;\n    this.rendered = {};\n    this.magicId = 0;\n    this.mergeDiff(rendered);\n  }\n  parentViewId() {\n    return this.viewId;\n  }\n  toString(onlyCids) {\n    const { buffer: str, streams } = this.recursiveToString(\n      this.rendered,\n      this.rendered[COMPONENTS],\n      onlyCids,\n      true,\n      {}\n    );\n    return { buffer: str, streams };\n  }\n  recursiveToString(rendered, components = rendered[COMPONENTS], onlyCids, changeTracking, rootAttrs) {\n    onlyCids = onlyCids ? new Set(onlyCids) : null;\n    const output = {\n      buffer: \"\",\n      components,\n      onlyCids,\n      streams: /* @__PURE__ */ new Set()\n    };\n    this.toOutputBuffer(rendered, null, output, changeTracking, rootAttrs);\n    return { buffer: output.buffer, streams: output.streams };\n  }\n  componentCIDs(diff) {\n    return Object.keys(diff[COMPONENTS] || {}).map((i) => parseInt(i));\n  }\n  isComponentOnlyDiff(diff) {\n    if (!diff[COMPONENTS]) {\n      return false;\n    }\n    return Object.keys(diff).length === 1;\n  }\n  getComponent(diff, cid) {\n    return diff[COMPONENTS][cid];\n  }\n  resetRender(cid) {\n    if (this.rendered[COMPONENTS][cid]) {\n      this.rendered[COMPONENTS][cid].reset = true;\n    }\n  }\n  mergeDiff(diff) {\n    const newc = diff[COMPONENTS];\n    const cache = {};\n    delete diff[COMPONENTS];\n    this.rendered = this.mutableMerge(this.rendered, diff);\n    this.rendered[COMPONENTS] = this.rendered[COMPONENTS] || {};\n    if (newc) {\n      const oldc = this.rendered[COMPONENTS];\n      for (const cid in newc) {\n        newc[cid] = this.cachedFindComponent(cid, newc[cid], oldc, newc, cache);\n      }\n      for (const cid in newc) {\n        oldc[cid] = newc[cid];\n      }\n      diff[COMPONENTS] = newc;\n    }\n  }\n  cachedFindComponent(cid, cdiff, oldc, newc, cache) {\n    if (cache[cid]) {\n      return cache[cid];\n    } else {\n      let ndiff, stat, scid = cdiff[STATIC];\n      if (isCid(scid)) {\n        let tdiff;\n        if (scid > 0) {\n          tdiff = this.cachedFindComponent(scid, newc[scid], oldc, newc, cache);\n        } else {\n          tdiff = oldc[-scid];\n        }\n        stat = tdiff[STATIC];\n        ndiff = this.cloneMerge(tdiff, cdiff, true);\n        ndiff[STATIC] = stat;\n      } else {\n        ndiff = cdiff[STATIC] !== void 0 || oldc[cid] === void 0 ? cdiff : this.cloneMerge(oldc[cid], cdiff, false);\n      }\n      cache[cid] = ndiff;\n      return ndiff;\n    }\n  }\n  mutableMerge(target, source) {\n    if (source[STATIC] !== void 0) {\n      return source;\n    } else {\n      this.doMutableMerge(target, source);\n      return target;\n    }\n  }\n  doMutableMerge(target, source) {\n    if (source[KEYED]) {\n      this.mergeKeyed(target, source);\n    } else {\n      for (const key in source) {\n        const val = source[key];\n        const targetVal = target[key];\n        const isObjVal = isObject(val);\n        if (isObjVal && val[STATIC] === void 0 && isObject(targetVal)) {\n          this.doMutableMerge(targetVal, val);\n        } else {\n          target[key] = val;\n        }\n      }\n    }\n    if (target[ROOT]) {\n      target.newRender = true;\n    }\n  }\n  clone(diff) {\n    if (\"structuredClone\" in window) {\n      return structuredClone(diff);\n    } else {\n      return JSON.parse(JSON.stringify(diff));\n    }\n  }\n  // keyed comprehensions\n  mergeKeyed(target, source) {\n    const clonedTarget = this.clone(target);\n    Object.entries(source[KEYED]).forEach(([i, entry]) => {\n      if (i === KEYED_COUNT) {\n        return;\n      }\n      if (Array.isArray(entry)) {\n        const [old_idx, diff] = entry;\n        target[KEYED][i] = clonedTarget[KEYED][old_idx];\n        this.doMutableMerge(target[KEYED][i], diff);\n      } else if (typeof entry === \"number\") {\n        const old_idx = entry;\n        target[KEYED][i] = clonedTarget[KEYED][old_idx];\n      } else if (typeof entry === \"object\") {\n        if (!target[KEYED][i]) {\n          target[KEYED][i] = {};\n        }\n        this.doMutableMerge(target[KEYED][i], entry);\n      }\n    });\n    if (source[KEYED][KEYED_COUNT] < target[KEYED][KEYED_COUNT]) {\n      for (let i = source[KEYED][KEYED_COUNT]; i < target[KEYED][KEYED_COUNT]; i++) {\n        delete target[KEYED][i];\n      }\n    }\n    target[KEYED][KEYED_COUNT] = source[KEYED][KEYED_COUNT];\n    if (source[STREAM]) {\n      target[STREAM] = source[STREAM];\n    }\n    if (source[TEMPLATES]) {\n      target[TEMPLATES] = source[TEMPLATES];\n    }\n  }\n  // Merges cid trees together, copying statics from source tree.\n  //\n  // The `pruneMagicId` is passed to control pruning the magicId of the\n  // target. We must always prune the magicId when we are sharing statics\n  // from another component. If not pruning, we replicate the logic from\n  // mutableMerge, where we set newRender to true if there is a root\n  // (effectively forcing the new version to be rendered instead of skipped)\n  //\n  cloneMerge(target, source, pruneMagicId) {\n    let merged;\n    if (source[KEYED]) {\n      merged = this.clone(target);\n      this.mergeKeyed(merged, source);\n    } else {\n      merged = { ...target, ...source };\n      for (const key in merged) {\n        const val = source[key];\n        const targetVal = target[key];\n        if (isObject(val) && val[STATIC] === void 0 && isObject(targetVal)) {\n          merged[key] = this.cloneMerge(targetVal, val, pruneMagicId);\n        } else if (val === void 0 && isObject(targetVal)) {\n          merged[key] = this.cloneMerge(targetVal, {}, pruneMagicId);\n        }\n      }\n    }\n    if (pruneMagicId) {\n      delete merged.magicId;\n      delete merged.newRender;\n    } else if (target[ROOT]) {\n      merged.newRender = true;\n    }\n    return merged;\n  }\n  componentToString(cid) {\n    const { buffer: str, streams } = this.recursiveCIDToString(\n      this.rendered[COMPONENTS],\n      cid,\n      null\n    );\n    const [strippedHTML, _before, _after] = modifyRoot(str, {});\n    return { buffer: strippedHTML, streams };\n  }\n  pruneCIDs(cids) {\n    cids.forEach((cid) => delete this.rendered[COMPONENTS][cid]);\n  }\n  // private\n  get() {\n    return this.rendered;\n  }\n  isNewFingerprint(diff = {}) {\n    return !!diff[STATIC];\n  }\n  templateStatic(part, templates) {\n    if (typeof part === \"number\") {\n      return templates[part];\n    } else {\n      return part;\n    }\n  }\n  nextMagicID() {\n    this.magicId++;\n    return `m${this.magicId}-${this.parentViewId()}`;\n  }\n  // Converts rendered tree to output buffer.\n  //\n  // changeTracking controls if we can apply the PHX_SKIP optimization.\n  toOutputBuffer(rendered, templates, output, changeTracking, rootAttrs = {}) {\n    if (rendered[KEYED]) {\n      return this.comprehensionToBuffer(\n        rendered,\n        templates,\n        output,\n        changeTracking\n      );\n    }\n    if (rendered[TEMPLATES]) {\n      templates = rendered[TEMPLATES];\n      delete rendered[TEMPLATES];\n    }\n    let { [STATIC]: statics } = rendered;\n    statics = this.templateStatic(statics, templates);\n    rendered[STATIC] = statics;\n    const isRoot = rendered[ROOT];\n    const prevBuffer = output.buffer;\n    if (isRoot) {\n      output.buffer = \"\";\n    }\n    if (changeTracking && isRoot && !rendered.magicId) {\n      rendered.newRender = true;\n      rendered.magicId = this.nextMagicID();\n    }\n    output.buffer += statics[0];\n    for (let i = 1; i < statics.length; i++) {\n      this.dynamicToBuffer(rendered[i - 1], templates, output, changeTracking);\n      output.buffer += statics[i];\n    }\n    if (isRoot) {\n      let skip = false;\n      let attrs;\n      if (changeTracking || rendered.magicId) {\n        skip = changeTracking && !rendered.newRender;\n        attrs = { [PHX_MAGIC_ID]: rendered.magicId, ...rootAttrs };\n      } else {\n        attrs = rootAttrs;\n      }\n      if (skip) {\n        attrs[PHX_SKIP] = true;\n      }\n      const [newRoot, commentBefore, commentAfter] = modifyRoot(\n        output.buffer,\n        attrs,\n        skip\n      );\n      rendered.newRender = false;\n      output.buffer = prevBuffer + commentBefore + newRoot + commentAfter;\n    }\n  }\n  comprehensionToBuffer(rendered, templates, output, changeTracking) {\n    const keyedTemplates = templates || rendered[TEMPLATES];\n    const statics = this.templateStatic(rendered[STATIC], templates);\n    rendered[STATIC] = statics;\n    delete rendered[TEMPLATES];\n    for (let i = 0; i < rendered[KEYED][KEYED_COUNT]; i++) {\n      output.buffer += statics[0];\n      for (let j = 1; j < statics.length; j++) {\n        this.dynamicToBuffer(\n          rendered[KEYED][i][j - 1],\n          keyedTemplates,\n          output,\n          changeTracking\n        );\n        output.buffer += statics[j];\n      }\n    }\n    if (rendered[STREAM]) {\n      const stream = rendered[STREAM];\n      const [_ref, _inserts, deleteIds, reset] = stream || [null, {}, [], null];\n      if (stream !== void 0 && (rendered[KEYED][KEYED_COUNT] > 0 || deleteIds.length > 0 || reset)) {\n        delete rendered[STREAM];\n        rendered[KEYED] = {\n          [KEYED_COUNT]: 0\n        };\n        output.streams.add(stream);\n      }\n    }\n  }\n  dynamicToBuffer(rendered, templates, output, changeTracking) {\n    if (typeof rendered === \"number\") {\n      const { buffer: str, streams } = this.recursiveCIDToString(\n        output.components,\n        rendered,\n        output.onlyCids\n      );\n      output.buffer += str;\n      output.streams = /* @__PURE__ */ new Set([...output.streams, ...streams]);\n    } else if (isObject(rendered)) {\n      this.toOutputBuffer(rendered, templates, output, changeTracking, {});\n    } else {\n      output.buffer += rendered;\n    }\n  }\n  recursiveCIDToString(components, cid, onlyCids) {\n    const component = components[cid] || logError(`no component for CID ${cid}`, components);\n    const attrs = { [PHX_COMPONENT]: cid, [PHX_VIEW_REF]: this.viewId };\n    const skip = onlyCids && !onlyCids.has(cid);\n    component.newRender = !skip;\n    component.magicId = `c${cid}-${this.parentViewId()}`;\n    const changeTracking = !component.reset;\n    const { buffer: html, streams } = this.recursiveToString(\n      component,\n      components,\n      onlyCids,\n      changeTracking,\n      attrs\n    );\n    delete component.reset;\n    return { buffer: html, streams };\n  }\n};\n\n// js/phoenix_live_view/js.js\nvar focusStack = [];\nvar default_transition_time = 200;\nvar JS = {\n  // private\n  exec(e, eventType, phxEvent, view, sourceEl, defaults) {\n    const [defaultKind, defaultArgs] = defaults || [\n      null,\n      { callback: defaults && defaults.callback }\n    ];\n    const commands = Array.isArray(phxEvent) ? phxEvent : typeof phxEvent === \"string\" && phxEvent.startsWith(\"[\") ? JSON.parse(phxEvent) : [[defaultKind, defaultArgs]];\n    commands.forEach(([kind, args]) => {\n      if (kind === defaultKind) {\n        args = { ...defaultArgs, ...args };\n        args.callback = args.callback || defaultArgs.callback;\n      }\n      this.filterToEls(view.liveSocket, sourceEl, args).forEach((el) => {\n        this[`exec_${kind}`](e, eventType, phxEvent, view, sourceEl, el, args);\n      });\n    });\n  },\n  isVisible(el) {\n    return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length > 0);\n  },\n  // returns true if any part of the element is inside the viewport\n  isInViewport(el) {\n    const rect = el.getBoundingClientRect();\n    const windowHeight = window.innerHeight || document.documentElement.clientHeight;\n    const windowWidth = window.innerWidth || document.documentElement.clientWidth;\n    return rect.right > 0 && rect.bottom > 0 && rect.left < windowWidth && rect.top < windowHeight;\n  },\n  // private\n  // commands\n  exec_exec(e, eventType, phxEvent, view, sourceEl, el, { attr, to }) {\n    const encodedJS = el.getAttribute(attr);\n    if (!encodedJS) {\n      throw new Error(`expected ${attr} to contain JS command on \"${to}\"`);\n    }\n    view.liveSocket.execJS(el, encodedJS, eventType);\n  },\n  exec_dispatch(e, eventType, phxEvent, view, sourceEl, el, { event, detail, bubbles, blocking }) {\n    detail = detail || {};\n    detail.dispatcher = sourceEl;\n    if (blocking) {\n      const promise = new Promise((resolve, _reject) => {\n        detail.done = resolve;\n      });\n      view.liveSocket.asyncTransition(promise);\n    }\n    dom_default.dispatchEvent(el, event, { detail, bubbles });\n  },\n  exec_push(e, eventType, phxEvent, view, sourceEl, el, args) {\n    const {\n      event,\n      data,\n      target,\n      page_loading,\n      loading,\n      value,\n      dispatcher,\n      callback\n    } = args;\n    const pushOpts = {\n      loading,\n      value,\n      target,\n      page_loading: !!page_loading,\n      originalEvent: e\n    };\n    const targetSrc = eventType === \"change\" && dispatcher ? dispatcher : sourceEl;\n    const phxTarget = target || targetSrc.getAttribute(view.binding(\"target\")) || targetSrc;\n    const handler = (targetView, targetCtx) => {\n      if (!targetView.isConnected()) {\n        return;\n      }\n      if (eventType === \"change\") {\n        let { newCid, _target } = args;\n        _target = _target || (dom_default.isFormInput(sourceEl) ? sourceEl.name : void 0);\n        if (_target) {\n          pushOpts._target = _target;\n        }\n        targetView.pushInput(\n          sourceEl,\n          targetCtx,\n          newCid,\n          event || phxEvent,\n          pushOpts,\n          callback\n        );\n      } else if (eventType === \"submit\") {\n        const { submitter } = args;\n        targetView.submitForm(\n          sourceEl,\n          targetCtx,\n          event || phxEvent,\n          submitter,\n          pushOpts,\n          callback\n        );\n      } else {\n        targetView.pushEvent(\n          eventType,\n          sourceEl,\n          targetCtx,\n          event || phxEvent,\n          data,\n          pushOpts,\n          callback\n        );\n      }\n    };\n    if (args.targetView && args.targetCtx) {\n      handler(args.targetView, args.targetCtx);\n    } else {\n      view.withinTargets(phxTarget, handler);\n    }\n  },\n  exec_navigate(e, eventType, phxEvent, view, sourceEl, el, { href, replace }) {\n    view.liveSocket.historyRedirect(\n      e,\n      href,\n      replace ? \"replace\" : \"push\",\n      null,\n      sourceEl\n    );\n  },\n  exec_patch(e, eventType, phxEvent, view, sourceEl, el, { href, replace }) {\n    view.liveSocket.pushHistoryPatch(\n      e,\n      href,\n      replace ? \"replace\" : \"push\",\n      sourceEl\n    );\n  },\n  exec_focus(e, eventType, phxEvent, view, sourceEl, el) {\n    aria_default.attemptFocus(el);\n    window.requestAnimationFrame(() => {\n      window.requestAnimationFrame(() => aria_default.attemptFocus(el));\n    });\n  },\n  exec_focus_first(e, eventType, phxEvent, view, sourceEl, el) {\n    aria_default.focusFirstInteractive(el) || aria_default.focusFirst(el);\n    window.requestAnimationFrame(() => {\n      window.requestAnimationFrame(\n        () => aria_default.focusFirstInteractive(el) || aria_default.focusFirst(el)\n      );\n    });\n  },\n  exec_push_focus(e, eventType, phxEvent, view, sourceEl, el) {\n    focusStack.push(el || sourceEl);\n  },\n  exec_pop_focus(_e, _eventType, _phxEvent, _view, _sourceEl, _el) {\n    const el = focusStack.pop();\n    if (el) {\n      el.focus();\n      window.requestAnimationFrame(() => {\n        window.requestAnimationFrame(() => el.focus());\n      });\n    }\n  },\n  exec_add_class(e, eventType, phxEvent, view, sourceEl, el, { names, transition, time, blocking }) {\n    this.addOrRemoveClasses(el, names, [], transition, time, view, blocking);\n  },\n  exec_remove_class(e, eventType, phxEvent, view, sourceEl, el, { names, transition, time, blocking }) {\n    this.addOrRemoveClasses(el, [], names, transition, time, view, blocking);\n  },\n  exec_toggle_class(e, eventType, phxEvent, view, sourceEl, el, { names, transition, time, blocking }) {\n    this.toggleClasses(el, names, transition, time, view, blocking);\n  },\n  exec_toggle_attr(e, eventType, phxEvent, view, sourceEl, el, { attr: [attr, val1, val2] }) {\n    this.toggleAttr(el, attr, val1, val2);\n  },\n  exec_ignore_attrs(e, eventType, phxEvent, view, sourceEl, el, { attrs }) {\n    this.ignoreAttrs(el, attrs);\n  },\n  exec_transition(e, eventType, phxEvent, view, sourceEl, el, { time, transition, blocking }) {\n    this.addOrRemoveClasses(el, [], [], transition, time, view, blocking);\n  },\n  exec_toggle(e, eventType, phxEvent, view, sourceEl, el, { display, ins, outs, time, blocking }) {\n    this.toggle(eventType, view, el, display, ins, outs, time, blocking);\n  },\n  exec_show(e, eventType, phxEvent, view, sourceEl, el, { display, transition, time, blocking }) {\n    this.show(eventType, view, el, display, transition, time, blocking);\n  },\n  exec_hide(e, eventType, phxEvent, view, sourceEl, el, { display, transition, time, blocking }) {\n    this.hide(eventType, view, el, display, transition, time, blocking);\n  },\n  exec_set_attr(e, eventType, phxEvent, view, sourceEl, el, { attr: [attr, val] }) {\n    this.setOrRemoveAttrs(el, [[attr, val]], []);\n  },\n  exec_remove_attr(e, eventType, phxEvent, view, sourceEl, el, { attr }) {\n    this.setOrRemoveAttrs(el, [], [attr]);\n  },\n  ignoreAttrs(el, attrs) {\n    dom_default.putPrivate(el, \"JS:ignore_attrs\", {\n      apply: (fromEl, toEl) => {\n        let fromAttributes = Array.from(fromEl.attributes);\n        let fromAttributeNames = fromAttributes.map((attr) => attr.name);\n        Array.from(toEl.attributes).filter((attr) => {\n          return !fromAttributeNames.includes(attr.name);\n        }).forEach((attr) => {\n          if (dom_default.attributeIgnored(attr, attrs)) {\n            toEl.removeAttribute(attr.name);\n          }\n        });\n        fromAttributes.forEach((attr) => {\n          if (dom_default.attributeIgnored(attr, attrs)) {\n            toEl.setAttribute(attr.name, attr.value);\n          }\n        });\n      }\n    });\n  },\n  onBeforeElUpdated(fromEl, toEl) {\n    const ignoreAttrs = dom_default.private(fromEl, \"JS:ignore_attrs\");\n    if (ignoreAttrs) {\n      ignoreAttrs.apply(fromEl, toEl);\n    }\n  },\n  // utils for commands\n  show(eventType, view, el, display, transition, time, blocking) {\n    if (!this.isVisible(el)) {\n      this.toggle(\n        eventType,\n        view,\n        el,\n        display,\n        transition,\n        null,\n        time,\n        blocking\n      );\n    }\n  },\n  hide(eventType, view, el, display, transition, time, blocking) {\n    if (this.isVisible(el)) {\n      this.toggle(\n        eventType,\n        view,\n        el,\n        display,\n        null,\n        transition,\n        time,\n        blocking\n      );\n    }\n  },\n  toggle(eventType, view, el, display, ins, outs, time, blocking) {\n    time = time || default_transition_time;\n    const [inClasses, inStartClasses, inEndClasses] = ins || [[], [], []];\n    const [outClasses, outStartClasses, outEndClasses] = outs || [[], [], []];\n    if (inClasses.length > 0 || outClasses.length > 0) {\n      if (this.isVisible(el)) {\n        const onStart = () => {\n          this.addOrRemoveClasses(\n            el,\n            outStartClasses,\n            inClasses.concat(inStartClasses).concat(inEndClasses)\n          );\n          window.requestAnimationFrame(() => {\n            this.addOrRemoveClasses(el, outClasses, []);\n            window.requestAnimationFrame(\n              () => this.addOrRemoveClasses(el, outEndClasses, outStartClasses)\n            );\n          });\n        };\n        const onEnd = () => {\n          this.addOrRemoveClasses(el, [], outClasses.concat(outEndClasses));\n          dom_default.putSticky(\n            el,\n            \"toggle\",\n            (currentEl) => currentEl.style.display = \"none\"\n          );\n          el.dispatchEvent(new Event(\"phx:hide-end\"));\n        };\n        el.dispatchEvent(new Event(\"phx:hide-start\"));\n        if (blocking === false) {\n          onStart();\n          setTimeout(onEnd, time);\n        } else {\n          view.transition(time, onStart, onEnd);\n        }\n      } else {\n        if (eventType === \"remove\") {\n          return;\n        }\n        const onStart = () => {\n          this.addOrRemoveClasses(\n            el,\n            inStartClasses,\n            outClasses.concat(outStartClasses).concat(outEndClasses)\n          );\n          const stickyDisplay = display || this.defaultDisplay(el);\n          window.requestAnimationFrame(() => {\n            this.addOrRemoveClasses(el, inClasses, []);\n            window.requestAnimationFrame(() => {\n              dom_default.putSticky(\n                el,\n                \"toggle\",\n                (currentEl) => currentEl.style.display = stickyDisplay\n              );\n              this.addOrRemoveClasses(el, inEndClasses, inStartClasses);\n            });\n          });\n        };\n        const onEnd = () => {\n          this.addOrRemoveClasses(el, [], inClasses.concat(inEndClasses));\n          el.dispatchEvent(new Event(\"phx:show-end\"));\n        };\n        el.dispatchEvent(new Event(\"phx:show-start\"));\n        if (blocking === false) {\n          onStart();\n          setTimeout(onEnd, time);\n        } else {\n          view.transition(time, onStart, onEnd);\n        }\n      }\n    } else {\n      if (this.isVisible(el)) {\n        window.requestAnimationFrame(() => {\n          el.dispatchEvent(new Event(\"phx:hide-start\"));\n          dom_default.putSticky(\n            el,\n            \"toggle\",\n            (currentEl) => currentEl.style.display = \"none\"\n          );\n          el.dispatchEvent(new Event(\"phx:hide-end\"));\n        });\n      } else {\n        window.requestAnimationFrame(() => {\n          el.dispatchEvent(new Event(\"phx:show-start\"));\n          const stickyDisplay = display || this.defaultDisplay(el);\n          dom_default.putSticky(\n            el,\n            \"toggle\",\n            (currentEl) => currentEl.style.display = stickyDisplay\n          );\n          el.dispatchEvent(new Event(\"phx:show-end\"));\n        });\n      }\n    }\n  },\n  toggleClasses(el, classes, transition, time, view, blocking) {\n    window.requestAnimationFrame(() => {\n      const [prevAdds, prevRemoves] = dom_default.getSticky(el, \"classes\", [[], []]);\n      const newAdds = classes.filter(\n        (name) => prevAdds.indexOf(name) < 0 && !el.classList.contains(name)\n      );\n      const newRemoves = classes.filter(\n        (name) => prevRemoves.indexOf(name) < 0 && el.classList.contains(name)\n      );\n      this.addOrRemoveClasses(\n        el,\n        newAdds,\n        newRemoves,\n        transition,\n        time,\n        view,\n        blocking\n      );\n    });\n  },\n  toggleAttr(el, attr, val1, val2) {\n    if (el.hasAttribute(attr)) {\n      if (val2 !== void 0) {\n        if (el.getAttribute(attr) === val1) {\n          this.setOrRemoveAttrs(el, [[attr, val2]], []);\n        } else {\n          this.setOrRemoveAttrs(el, [[attr, val1]], []);\n        }\n      } else {\n        this.setOrRemoveAttrs(el, [], [attr]);\n      }\n    } else {\n      this.setOrRemoveAttrs(el, [[attr, val1]], []);\n    }\n  },\n  addOrRemoveClasses(el, adds, removes, transition, time, view, blocking) {\n    time = time || default_transition_time;\n    const [transitionRun, transitionStart, transitionEnd] = transition || [\n      [],\n      [],\n      []\n    ];\n    if (transitionRun.length > 0) {\n      const onStart = () => {\n        this.addOrRemoveClasses(\n          el,\n          transitionStart,\n          [].concat(transitionRun).concat(transitionEnd)\n        );\n        window.requestAnimationFrame(() => {\n          this.addOrRemoveClasses(el, transitionRun, []);\n          window.requestAnimationFrame(\n            () => this.addOrRemoveClasses(el, transitionEnd, transitionStart)\n          );\n        });\n      };\n      const onDone = () => this.addOrRemoveClasses(\n        el,\n        adds.concat(transitionEnd),\n        removes.concat(transitionRun).concat(transitionStart)\n      );\n      if (blocking === false) {\n        onStart();\n        setTimeout(onDone, time);\n      } else {\n        view.transition(time, onStart, onDone);\n      }\n      return;\n    }\n    window.requestAnimationFrame(() => {\n      const [prevAdds, prevRemoves] = dom_default.getSticky(el, \"classes\", [[], []]);\n      const keepAdds = adds.filter(\n        (name) => prevAdds.indexOf(name) < 0 && !el.classList.contains(name)\n      );\n      const keepRemoves = removes.filter(\n        (name) => prevRemoves.indexOf(name) < 0 && el.classList.contains(name)\n      );\n      const newAdds = prevAdds.filter((name) => removes.indexOf(name) < 0).concat(keepAdds);\n      const newRemoves = prevRemoves.filter((name) => adds.indexOf(name) < 0).concat(keepRemoves);\n      dom_default.putSticky(el, \"classes\", (currentEl) => {\n        currentEl.classList.remove(...newRemoves);\n        currentEl.classList.add(...newAdds);\n        return [newAdds, newRemoves];\n      });\n    });\n  },\n  setOrRemoveAttrs(el, sets, removes) {\n    const [prevSets, prevRemoves] = dom_default.getSticky(el, \"attrs\", [[], []]);\n    const alteredAttrs = sets.map(([attr, _val]) => attr).concat(removes);\n    const newSets = prevSets.filter(([attr, _val]) => !alteredAttrs.includes(attr)).concat(sets);\n    const newRemoves = prevRemoves.filter((attr) => !alteredAttrs.includes(attr)).concat(removes);\n    dom_default.putSticky(el, \"attrs\", (currentEl) => {\n      newRemoves.forEach((attr) => currentEl.removeAttribute(attr));\n      newSets.forEach(([attr, val]) => currentEl.setAttribute(attr, val));\n      return [newSets, newRemoves];\n    });\n  },\n  hasAllClasses(el, classes) {\n    return classes.every((name) => el.classList.contains(name));\n  },\n  isToggledOut(el, outClasses) {\n    return !this.isVisible(el) || this.hasAllClasses(el, outClasses);\n  },\n  filterToEls(liveSocket, sourceEl, { to }) {\n    const defaultQuery = () => {\n      if (typeof to === \"string\") {\n        return document.querySelectorAll(to);\n      } else if (to.closest) {\n        const toEl = sourceEl.closest(to.closest);\n        return toEl ? [toEl] : [];\n      } else if (to.inner) {\n        return sourceEl.querySelectorAll(to.inner);\n      }\n    };\n    return to ? liveSocket.jsQuerySelectorAll(sourceEl, to, defaultQuery) : [sourceEl];\n  },\n  defaultDisplay(el) {\n    return { tr: \"table-row\", td: \"table-cell\" }[el.tagName.toLowerCase()] || \"block\";\n  },\n  transitionClasses(val) {\n    if (!val) {\n      return null;\n    }\n    let [trans, tStart, tEnd] = Array.isArray(val) ? val : [val.split(\" \"), [], []];\n    trans = Array.isArray(trans) ? trans : trans.split(\" \");\n    tStart = Array.isArray(tStart) ? tStart : tStart.split(\" \");\n    tEnd = Array.isArray(tEnd) ? tEnd : tEnd.split(\" \");\n    return [trans, tStart, tEnd];\n  }\n};\nvar js_default = JS;\n\n// js/phoenix_live_view/js_commands.ts\nvar js_commands_default = (liveSocket, eventType) => {\n  return {\n    exec(el, encodedJS) {\n      liveSocket.execJS(el, encodedJS, eventType);\n    },\n    show(el, opts = {}) {\n      const owner = liveSocket.owner(el);\n      js_default.show(\n        eventType,\n        owner,\n        el,\n        opts.display,\n        js_default.transitionClasses(opts.transition),\n        opts.time,\n        opts.blocking\n      );\n    },\n    hide(el, opts = {}) {\n      const owner = liveSocket.owner(el);\n      js_default.hide(\n        eventType,\n        owner,\n        el,\n        null,\n        js_default.transitionClasses(opts.transition),\n        opts.time,\n        opts.blocking\n      );\n    },\n    toggle(el, opts = {}) {\n      const owner = liveSocket.owner(el);\n      const inTransition = js_default.transitionClasses(opts.in);\n      const outTransition = js_default.transitionClasses(opts.out);\n      js_default.toggle(\n        eventType,\n        owner,\n        el,\n        opts.display,\n        inTransition,\n        outTransition,\n        opts.time,\n        opts.blocking\n      );\n    },\n    addClass(el, names, opts = {}) {\n      const classNames = Array.isArray(names) ? names : names.split(\" \");\n      const owner = liveSocket.owner(el);\n      js_default.addOrRemoveClasses(\n        el,\n        classNames,\n        [],\n        js_default.transitionClasses(opts.transition),\n        opts.time,\n        owner,\n        opts.blocking\n      );\n    },\n    removeClass(el, names, opts = {}) {\n      const classNames = Array.isArray(names) ? names : names.split(\" \");\n      const owner = liveSocket.owner(el);\n      js_default.addOrRemoveClasses(\n        el,\n        [],\n        classNames,\n        js_default.transitionClasses(opts.transition),\n        opts.time,\n        owner,\n        opts.blocking\n      );\n    },\n    toggleClass(el, names, opts = {}) {\n      const classNames = Array.isArray(names) ? names : names.split(\" \");\n      const owner = liveSocket.owner(el);\n      js_default.toggleClasses(\n        el,\n        classNames,\n        js_default.transitionClasses(opts.transition),\n        opts.time,\n        owner,\n        opts.blocking\n      );\n    },\n    transition(el, transition, opts = {}) {\n      const owner = liveSocket.owner(el);\n      js_default.addOrRemoveClasses(\n        el,\n        [],\n        [],\n        js_default.transitionClasses(transition),\n        opts.time,\n        owner,\n        opts.blocking\n      );\n    },\n    setAttribute(el, attr, val) {\n      js_default.setOrRemoveAttrs(el, [[attr, val]], []);\n    },\n    removeAttribute(el, attr) {\n      js_default.setOrRemoveAttrs(el, [], [attr]);\n    },\n    toggleAttribute(el, attr, val1, val2) {\n      js_default.toggleAttr(el, attr, val1, val2);\n    },\n    push(el, type, opts = {}) {\n      liveSocket.withinOwners(el, (view) => {\n        const data = opts.value || {};\n        delete opts.value;\n        let e = new CustomEvent(\"phx:exec\", { detail: { sourceElement: el } });\n        js_default.exec(e, eventType, type, view, el, [\"push\", { data, ...opts }]);\n      });\n    },\n    navigate(href, opts = {}) {\n      const customEvent = new CustomEvent(\"phx:exec\");\n      liveSocket.historyRedirect(\n        customEvent,\n        href,\n        opts.replace ? \"replace\" : \"push\",\n        null,\n        null\n      );\n    },\n    patch(href, opts = {}) {\n      const customEvent = new CustomEvent(\"phx:exec\");\n      liveSocket.pushHistoryPatch(\n        customEvent,\n        href,\n        opts.replace ? \"replace\" : \"push\",\n        null\n      );\n    },\n    ignoreAttributes(el, attrs) {\n      js_default.ignoreAttrs(el, Array.isArray(attrs) ? attrs : [attrs]);\n    }\n  };\n};\n\n// js/phoenix_live_view/view_hook.ts\nvar HOOK_ID = \"hookId\";\nvar DEAD_HOOK = \"deadHook\";\nvar viewHookID = 1;\nvar ViewHook = class _ViewHook {\n  get liveSocket() {\n    return this.__liveSocket();\n  }\n  static makeID() {\n    return viewHookID++;\n  }\n  static elementID(el) {\n    return dom_default.private(el, HOOK_ID);\n  }\n  static deadHook(el) {\n    return dom_default.private(el, DEAD_HOOK) === true;\n  }\n  constructor(view, el, callbacks) {\n    this.el = el;\n    this.__attachView(view);\n    this.__listeners = /* @__PURE__ */ new Set();\n    this.__isDisconnected = false;\n    dom_default.putPrivate(this.el, HOOK_ID, _ViewHook.makeID());\n    if (view && view.isDead) {\n      dom_default.putPrivate(this.el, DEAD_HOOK, true);\n    }\n    if (callbacks) {\n      const protectedProps = /* @__PURE__ */ new Set([\n        \"el\",\n        \"liveSocket\",\n        \"__view\",\n        \"__listeners\",\n        \"__isDisconnected\",\n        \"constructor\",\n        // Standard object properties\n        // Core ViewHook API methods\n        \"js\",\n        \"pushEvent\",\n        \"pushEventTo\",\n        \"handleEvent\",\n        \"removeHandleEvent\",\n        \"upload\",\n        \"uploadTo\",\n        // Internal lifecycle callers\n        \"__mounted\",\n        \"__updated\",\n        \"__beforeUpdate\",\n        \"__destroyed\",\n        \"__reconnected\",\n        \"__disconnected\",\n        \"__cleanup__\"\n      ]);\n      for (const key in callbacks) {\n        if (Object.prototype.hasOwnProperty.call(callbacks, key)) {\n          this[key] = callbacks[key];\n          if (protectedProps.has(key)) {\n            console.warn(\n              `Hook object for element #${el.id} overwrites core property '${key}'!`\n            );\n          }\n        }\n      }\n      const lifecycleMethods = [\n        \"mounted\",\n        \"beforeUpdate\",\n        \"updated\",\n        \"destroyed\",\n        \"disconnected\",\n        \"reconnected\"\n      ];\n      lifecycleMethods.forEach((methodName) => {\n        if (callbacks[methodName] && typeof callbacks[methodName] === \"function\") {\n          this[methodName] = callbacks[methodName];\n        }\n      });\n    }\n  }\n  /** @internal */\n  __attachView(view) {\n    if (view) {\n      this.__view = () => view;\n      this.__liveSocket = () => view.liveSocket;\n    } else {\n      this.__view = () => {\n        throw new Error(\n          `hook not yet attached to a live view: ${this.el.outerHTML}`\n        );\n      };\n      this.__liveSocket = () => {\n        throw new Error(\n          `hook not yet attached to a live view: ${this.el.outerHTML}`\n        );\n      };\n    }\n  }\n  // Default lifecycle methods\n  mounted() {\n  }\n  beforeUpdate() {\n  }\n  updated() {\n  }\n  destroyed() {\n  }\n  disconnected() {\n  }\n  reconnected() {\n  }\n  // Internal lifecycle callers - called by the View\n  /** @internal */\n  __mounted() {\n    this.mounted();\n  }\n  /** @internal */\n  __updated() {\n    this.updated();\n  }\n  /** @internal */\n  __beforeUpdate() {\n    this.beforeUpdate();\n  }\n  /** @internal */\n  __destroyed() {\n    this.destroyed();\n    dom_default.deletePrivate(this.el, HOOK_ID);\n  }\n  /** @internal */\n  __reconnected() {\n    if (this.__isDisconnected) {\n      this.__isDisconnected = false;\n      this.reconnected();\n    }\n  }\n  /** @internal */\n  __disconnected() {\n    this.__isDisconnected = true;\n    this.disconnected();\n  }\n  js() {\n    return {\n      ...js_commands_default(this.__view().liveSocket, \"hook\"),\n      exec: (encodedJS) => {\n        this.__view().liveSocket.execJS(this.el, encodedJS, \"hook\");\n      }\n    };\n  }\n  pushEvent(event, payload, onReply) {\n    const promise = this.__view().pushHookEvent(\n      this.el,\n      null,\n      event,\n      payload || {}\n    );\n    if (onReply === void 0) {\n      return promise.then(({ reply }) => reply);\n    }\n    promise.then(\n      ({ reply, ref }) => onReply(reply, ref)\n    ).catch(() => {\n    });\n  }\n  pushEventTo(selectorOrTarget, event, payload, onReply) {\n    if (onReply === void 0) {\n      const targetPair = [];\n      this.__view().withinTargets(\n        selectorOrTarget,\n        (view, targetCtx) => {\n          targetPair.push({ view, targetCtx });\n        }\n      );\n      const promises = targetPair.map(({ view, targetCtx }) => {\n        return view.pushHookEvent(this.el, targetCtx, event, payload || {});\n      });\n      return Promise.allSettled(promises);\n    }\n    this.__view().withinTargets(\n      selectorOrTarget,\n      (view, targetCtx) => {\n        view.pushHookEvent(this.el, targetCtx, event, payload || {}).then(\n          ({ reply, ref }) => onReply(reply, ref)\n        ).catch(() => {\n        });\n      }\n    );\n  }\n  handleEvent(event, callback) {\n    const callbackRef = {\n      event,\n      callback: (customEvent) => callback(customEvent.detail)\n    };\n    window.addEventListener(\n      `phx:${event}`,\n      callbackRef.callback\n    );\n    this.__listeners.add(callbackRef);\n    return callbackRef;\n  }\n  removeHandleEvent(ref) {\n    window.removeEventListener(\n      `phx:${ref.event}`,\n      ref.callback\n    );\n    this.__listeners.delete(ref);\n  }\n  upload(name, files) {\n    return this.__view().dispatchUploads(null, name, files);\n  }\n  uploadTo(selectorOrTarget, name, files) {\n    return this.__view().withinTargets(\n      selectorOrTarget,\n      (view, targetCtx) => {\n        view.dispatchUploads(targetCtx, name, files);\n      }\n    );\n  }\n  /** @internal */\n  __cleanup__() {\n    this.__listeners.forEach(\n      (callbackRef) => this.removeHandleEvent(callbackRef)\n    );\n  }\n};\n\n// js/phoenix_live_view/view.js\nvar prependFormDataKey = (key, prefix) => {\n  const isArray = key.endsWith(\"[]\");\n  let baseKey = isArray ? key.slice(0, -2) : key;\n  baseKey = baseKey.replace(/([^\\[\\]]+)(\\]?$)/, `${prefix}$1$2`);\n  if (isArray) {\n    baseKey += \"[]\";\n  }\n  return baseKey;\n};\nvar View = class _View {\n  static closestView(el) {\n    const liveViewEl = el.closest(PHX_VIEW_SELECTOR);\n    return liveViewEl ? dom_default.private(liveViewEl, \"view\") : null;\n  }\n  constructor(el, liveSocket, parentView, flash, liveReferer) {\n    this.isDead = false;\n    this.liveSocket = liveSocket;\n    this.flash = flash;\n    this.parent = parentView;\n    this.root = parentView ? parentView.root : this;\n    this.el = el;\n    const boundView = dom_default.private(this.el, \"view\");\n    if (boundView !== void 0 && boundView.isDead !== true) {\n      logError(\n        `The DOM element for this view has already been bound to a view.\n\n        An element can only ever be associated with a single view!\n        Please ensure that you are not trying to initialize multiple LiveSockets on the same page.\n        This could happen if you're accidentally trying to render your root layout more than once.\n        Ensure that the template set on the LiveView is different than the root layout.\n      `,\n        { view: boundView }\n      );\n      throw new Error(\"Cannot bind multiple views to the same DOM element.\");\n    }\n    dom_default.putPrivate(this.el, \"view\", this);\n    this.id = this.el.id;\n    this.ref = 0;\n    this.lastAckRef = null;\n    this.childJoins = 0;\n    this.loaderTimer = null;\n    this.disconnectedTimer = null;\n    this.pendingDiffs = [];\n    this.pendingForms = /* @__PURE__ */ new Set();\n    this.redirect = false;\n    this.href = null;\n    this.joinCount = this.parent ? this.parent.joinCount - 1 : 0;\n    this.joinAttempts = 0;\n    this.joinPending = true;\n    this.destroyed = false;\n    this.joinCallback = function(onDone) {\n      onDone && onDone();\n    };\n    this.stopCallback = function() {\n    };\n    this.pendingJoinOps = [];\n    this.viewHooks = {};\n    this.formSubmits = [];\n    this.children = this.parent ? null : {};\n    this.root.children[this.id] = {};\n    this.formsForRecovery = {};\n    this.channel = this.liveSocket.channel(`lv:${this.id}`, () => {\n      const url = this.href && this.expandURL(this.href);\n      return {\n        redirect: this.redirect ? url : void 0,\n        url: this.redirect ? void 0 : url || void 0,\n        params: this.connectParams(liveReferer),\n        session: this.getSession(),\n        static: this.getStatic(),\n        flash: this.flash,\n        sticky: this.el.hasAttribute(PHX_STICKY)\n      };\n    });\n    this.portalElementIds = /* @__PURE__ */ new Set();\n  }\n  setHref(href) {\n    this.href = href;\n  }\n  setRedirect(href) {\n    this.redirect = true;\n    this.href = href;\n  }\n  isMain() {\n    return this.el.hasAttribute(PHX_MAIN);\n  }\n  connectParams(liveReferer) {\n    const params = this.liveSocket.params(this.el);\n    const manifest = dom_default.all(document, `[${this.binding(PHX_TRACK_STATIC)}]`).map((node) => node.src || node.href).filter((url) => typeof url === \"string\");\n    if (manifest.length > 0) {\n      params[\"_track_static\"] = manifest;\n    }\n    params[\"_mounts\"] = this.joinCount;\n    params[\"_mount_attempts\"] = this.joinAttempts;\n    params[\"_live_referer\"] = liveReferer;\n    this.joinAttempts++;\n    return params;\n  }\n  isConnected() {\n    return this.channel.canPush();\n  }\n  getSession() {\n    return this.el.getAttribute(PHX_SESSION);\n  }\n  getStatic() {\n    const val = this.el.getAttribute(PHX_STATIC);\n    return val === \"\" ? null : val;\n  }\n  destroy(callback = function() {\n  }) {\n    this.destroyAllChildren();\n    this.destroyPortalElements();\n    this.destroyed = true;\n    dom_default.deletePrivate(this.el, \"view\");\n    delete this.root.children[this.id];\n    if (this.parent) {\n      delete this.root.children[this.parent.id][this.id];\n    }\n    clearTimeout(this.loaderTimer);\n    const onFinished = () => {\n      callback();\n      for (const id in this.viewHooks) {\n        this.destroyHook(this.viewHooks[id]);\n      }\n    };\n    dom_default.markPhxChildDestroyed(this.el);\n    this.log(\"destroyed\", () => [\"the child has been removed from the parent\"]);\n    this.channel.leave().receive(\"ok\", onFinished).receive(\"error\", onFinished).receive(\"timeout\", onFinished);\n  }\n  setContainerClasses(...classes) {\n    this.el.classList.remove(\n      PHX_CONNECTED_CLASS,\n      PHX_LOADING_CLASS,\n      PHX_ERROR_CLASS,\n      PHX_CLIENT_ERROR_CLASS,\n      PHX_SERVER_ERROR_CLASS\n    );\n    this.el.classList.add(...classes);\n  }\n  showLoader(timeout) {\n    clearTimeout(this.loaderTimer);\n    if (timeout) {\n      this.loaderTimer = setTimeout(() => this.showLoader(), timeout);\n    } else {\n      for (const id in this.viewHooks) {\n        this.viewHooks[id].__disconnected();\n      }\n      this.setContainerClasses(PHX_LOADING_CLASS);\n    }\n  }\n  execAll(binding) {\n    dom_default.all(\n      this.el,\n      `[${binding}]`,\n      (el) => this.liveSocket.execJS(el, el.getAttribute(binding))\n    );\n  }\n  hideLoader() {\n    clearTimeout(this.loaderTimer);\n    clearTimeout(this.disconnectedTimer);\n    this.setContainerClasses(PHX_CONNECTED_CLASS);\n    this.execAll(this.binding(\"connected\"));\n  }\n  triggerReconnected() {\n    for (const id in this.viewHooks) {\n      this.viewHooks[id].__reconnected();\n    }\n  }\n  log(kind, msgCallback) {\n    this.liveSocket.log(this, kind, msgCallback);\n  }\n  transition(time, onStart, onDone = function() {\n  }) {\n    this.liveSocket.transition(time, onStart, onDone);\n  }\n  // calls the callback with the view and target element for the given phxTarget\n  // targets can be:\n  //  * an element itself, then it is simply passed to liveSocket.owner;\n  //  * a CID (Component ID), then we first search the component's element in the DOM\n  //  * a selector, then we search the selector in the DOM and call the callback\n  //    for each element found with the corresponding owner view\n  withinTargets(phxTarget, callback, dom = document) {\n    if (phxTarget instanceof HTMLElement || phxTarget instanceof SVGElement) {\n      return this.liveSocket.owner(\n        phxTarget,\n        (view) => callback(view, phxTarget)\n      );\n    }\n    if (isCid(phxTarget)) {\n      const targets = dom_default.findComponentNodeList(this.id, phxTarget, dom);\n      if (targets.length === 0) {\n        logError(`no component found matching phx-target of ${phxTarget}`);\n      } else {\n        callback(this, parseInt(phxTarget));\n      }\n    } else {\n      const targets = Array.from(dom.querySelectorAll(phxTarget));\n      if (targets.length === 0) {\n        logError(\n          `nothing found matching the phx-target selector \"${phxTarget}\"`\n        );\n      }\n      targets.forEach(\n        (target) => this.liveSocket.owner(target, (view) => callback(view, target))\n      );\n    }\n  }\n  applyDiff(type, rawDiff, callback) {\n    this.log(type, () => [\"\", clone(rawDiff)]);\n    const { diff, reply, events, title } = Rendered.extract(rawDiff);\n    const ev = events.reduce(\n      (acc, args) => {\n        if (args.length === 3 && args[2] == true) {\n          acc.pre.push(args.slice(0, -1));\n        } else {\n          acc.post.push(args);\n        }\n        return acc;\n      },\n      { pre: [], post: [] }\n    );\n    this.liveSocket.dispatchEvents(ev.pre);\n    const update = () => {\n      callback({ diff, reply, events: ev.post });\n      if (typeof title === \"string\" || type == \"mount\" && this.isMain()) {\n        window.requestAnimationFrame(() => dom_default.putTitle(title));\n      }\n    };\n    if (\"onDocumentPatch\" in this.liveSocket.domCallbacks) {\n      this.liveSocket.triggerDOM(\"onDocumentPatch\", [update]);\n    } else {\n      update();\n    }\n  }\n  onJoin(resp) {\n    const { rendered, container, liveview_version, pid } = resp;\n    if (container) {\n      const [tag, attrs] = container;\n      this.el = dom_default.replaceRootContainer(this.el, tag, attrs);\n    }\n    this.childJoins = 0;\n    this.joinPending = true;\n    this.flash = null;\n    if (this.root === this) {\n      this.formsForRecovery = this.getFormsForRecovery();\n    }\n    if (this.isMain() && window.history.state === null) {\n      browser_default.pushState(\"replace\", {\n        type: \"patch\",\n        id: this.id,\n        position: this.liveSocket.currentHistoryPosition\n      });\n    }\n    if (liveview_version !== this.liveSocket.version()) {\n      console.warn(\n        `LiveView asset version mismatch. JavaScript version ${this.liveSocket.version()} vs. server ${liveview_version}. To avoid issues, please ensure that your assets use the same version as the server.`\n      );\n    }\n    if (pid) {\n      this.el.setAttribute(PHX_LV_PID, pid);\n    }\n    browser_default.dropLocal(\n      this.liveSocket.localStorage,\n      window.location.pathname,\n      CONSECUTIVE_RELOADS\n    );\n    this.applyDiff(\"mount\", rendered, ({ diff, events }) => {\n      this.rendered = new Rendered(this.id, diff);\n      const [html, streams] = this.renderContainer(null, \"join\");\n      this.dropPendingRefs();\n      this.joinCount++;\n      this.joinAttempts = 0;\n      this.maybeRecoverForms(html, () => {\n        this.onJoinComplete(resp, html, streams, events);\n      });\n    });\n  }\n  dropPendingRefs() {\n    dom_default.all(document, `[${PHX_REF_SRC}=\"${this.refSrc()}\"]`, (el) => {\n      el.removeAttribute(PHX_REF_LOADING);\n      el.removeAttribute(PHX_REF_SRC);\n      el.removeAttribute(PHX_REF_LOCK);\n    });\n  }\n  onJoinComplete({ live_patch }, html, streams, events) {\n    if (this.joinCount > 1 || this.parent && !this.parent.isJoinPending()) {\n      return this.applyJoinPatch(live_patch, html, streams, events);\n    }\n    const newChildren = dom_default.findPhxChildrenInFragment(html, this.id).filter(\n      (toEl) => {\n        const fromEl = toEl.id && this.el.querySelector(`[id=\"${toEl.id}\"]`);\n        const phxStatic = fromEl && fromEl.getAttribute(PHX_STATIC);\n        if (phxStatic) {\n          toEl.setAttribute(PHX_STATIC, phxStatic);\n        }\n        if (fromEl) {\n          fromEl.setAttribute(PHX_ROOT_ID, this.root.id);\n        }\n        return this.joinChild(toEl);\n      }\n    );\n    if (newChildren.length === 0) {\n      if (this.parent) {\n        this.root.pendingJoinOps.push([\n          this,\n          () => this.applyJoinPatch(live_patch, html, streams, events)\n        ]);\n        this.parent.ackJoin(this);\n      } else {\n        this.onAllChildJoinsComplete();\n        this.applyJoinPatch(live_patch, html, streams, events);\n      }\n    } else {\n      this.root.pendingJoinOps.push([\n        this,\n        () => this.applyJoinPatch(live_patch, html, streams, events)\n      ]);\n    }\n  }\n  attachTrueDocEl() {\n    this.el = dom_default.byId(this.id);\n    this.el.setAttribute(PHX_ROOT_ID, this.root.id);\n  }\n  // this is invoked for dead and live views, so we must filter by\n  // by owner to ensure we aren't duplicating hooks across disconnect\n  // and connected states. This also handles cases where hooks exist\n  // in a root layout with a LV in the body\n  execNewMounted(parent = document) {\n    let phxViewportTop = this.binding(PHX_VIEWPORT_TOP);\n    let phxViewportBottom = this.binding(PHX_VIEWPORT_BOTTOM);\n    this.all(\n      parent,\n      `[${phxViewportTop}], [${phxViewportBottom}]`,\n      (hookEl) => {\n        dom_default.maintainPrivateHooks(\n          hookEl,\n          hookEl,\n          phxViewportTop,\n          phxViewportBottom\n        );\n        this.maybeAddNewHook(hookEl);\n      }\n    );\n    this.all(\n      parent,\n      `[${this.binding(PHX_HOOK)}], [data-phx-${PHX_HOOK}]`,\n      (hookEl) => {\n        this.maybeAddNewHook(hookEl);\n      }\n    );\n    this.all(parent, `[${this.binding(PHX_MOUNTED)}]`, (el) => {\n      this.maybeMounted(el);\n    });\n  }\n  all(parent, selector, callback) {\n    dom_default.all(parent, selector, (el) => {\n      if (this.ownsElement(el)) {\n        callback(el);\n      }\n    });\n  }\n  applyJoinPatch(live_patch, html, streams, events) {\n    if (this.joinCount > 1) {\n      if (this.pendingJoinOps.length) {\n        this.pendingJoinOps.forEach((cb) => typeof cb === \"function\" && cb());\n        this.pendingJoinOps = [];\n      }\n    }\n    this.attachTrueDocEl();\n    const patch = new DOMPatch(this, this.el, this.id, html, streams, null);\n    patch.markPrunableContentForRemoval();\n    this.performPatch(patch, false, true);\n    this.joinNewChildren();\n    this.execNewMounted();\n    this.joinPending = false;\n    this.liveSocket.dispatchEvents(events);\n    this.applyPendingUpdates();\n    if (live_patch) {\n      const { kind, to } = live_patch;\n      this.liveSocket.historyPatch(to, kind);\n    }\n    this.hideLoader();\n    if (this.joinCount > 1) {\n      this.triggerReconnected();\n    }\n    this.stopCallback();\n  }\n  triggerBeforeUpdateHook(fromEl, toEl) {\n    this.liveSocket.triggerDOM(\"onBeforeElUpdated\", [fromEl, toEl]);\n    const hook = this.getHook(fromEl);\n    const isIgnored = hook && dom_default.isIgnored(fromEl, this.binding(PHX_UPDATE));\n    if (hook && !fromEl.isEqualNode(toEl) && !(isIgnored && isEqualObj(fromEl.dataset, toEl.dataset))) {\n      hook.__beforeUpdate();\n      return hook;\n    }\n  }\n  maybeMounted(el) {\n    const phxMounted = el.getAttribute(this.binding(PHX_MOUNTED));\n    const hasBeenInvoked = phxMounted && dom_default.private(el, \"mounted\");\n    if (phxMounted && !hasBeenInvoked) {\n      this.liveSocket.execJS(el, phxMounted);\n      dom_default.putPrivate(el, \"mounted\", true);\n    }\n  }\n  maybeAddNewHook(el) {\n    const newHook = this.addHook(el);\n    if (newHook) {\n      newHook.__mounted();\n    }\n  }\n  performPatch(patch, pruneCids, isJoinPatch = false) {\n    const removedEls = [];\n    let phxChildrenAdded = false;\n    const updatedHookIds = /* @__PURE__ */ new Set();\n    this.liveSocket.triggerDOM(\"onPatchStart\", [patch.targetContainer]);\n    patch.after(\"added\", (el) => {\n      this.liveSocket.triggerDOM(\"onNodeAdded\", [el]);\n      const phxViewportTop = this.binding(PHX_VIEWPORT_TOP);\n      const phxViewportBottom = this.binding(PHX_VIEWPORT_BOTTOM);\n      dom_default.maintainPrivateHooks(el, el, phxViewportTop, phxViewportBottom);\n      this.maybeAddNewHook(el);\n      if (el.getAttribute) {\n        this.maybeMounted(el);\n      }\n    });\n    patch.after(\"phxChildAdded\", (el) => {\n      if (dom_default.isPhxSticky(el)) {\n        this.liveSocket.joinRootViews();\n      } else {\n        phxChildrenAdded = true;\n      }\n    });\n    patch.before(\"updated\", (fromEl, toEl) => {\n      const hook = this.triggerBeforeUpdateHook(fromEl, toEl);\n      if (hook) {\n        updatedHookIds.add(fromEl.id);\n      }\n      js_default.onBeforeElUpdated(fromEl, toEl);\n    });\n    patch.after(\"updated\", (el) => {\n      if (updatedHookIds.has(el.id)) {\n        this.getHook(el).__updated();\n      }\n    });\n    patch.after(\"discarded\", (el) => {\n      if (el.nodeType === Node.ELEMENT_NODE) {\n        removedEls.push(el);\n      }\n    });\n    patch.after(\n      \"transitionsDiscarded\",\n      (els) => this.afterElementsRemoved(els, pruneCids)\n    );\n    patch.perform(isJoinPatch);\n    this.afterElementsRemoved(removedEls, pruneCids);\n    this.liveSocket.triggerDOM(\"onPatchEnd\", [patch.targetContainer]);\n    return phxChildrenAdded;\n  }\n  afterElementsRemoved(elements, pruneCids) {\n    const destroyedCIDs = [];\n    elements.forEach((parent) => {\n      const components = dom_default.all(\n        parent,\n        `[${PHX_VIEW_REF}=\"${this.id}\"][${PHX_COMPONENT}]`\n      );\n      const hooks = dom_default.all(\n        parent,\n        `[${this.binding(PHX_HOOK)}], [data-phx-hook]`\n      );\n      components.concat(parent).forEach((el) => {\n        const cid = this.componentID(el);\n        if (isCid(cid) && destroyedCIDs.indexOf(cid) === -1 && el.getAttribute(PHX_VIEW_REF) === this.id) {\n          destroyedCIDs.push(cid);\n        }\n      });\n      hooks.concat(parent).forEach((hookEl) => {\n        const hook = this.getHook(hookEl);\n        hook && this.destroyHook(hook);\n      });\n    });\n    if (pruneCids) {\n      this.maybePushComponentsDestroyed(destroyedCIDs);\n    }\n  }\n  joinNewChildren() {\n    dom_default.findPhxChildren(document, this.id).forEach((el) => this.joinChild(el));\n  }\n  maybeRecoverForms(html, callback) {\n    const phxChange = this.binding(\"change\");\n    const oldForms = this.root.formsForRecovery;\n    const template = document.createElement(\"template\");\n    template.innerHTML = html;\n    dom_default.all(template.content, `[${PHX_PORTAL}]`).forEach((portalTemplate) => {\n      template.content.firstElementChild.appendChild(\n        portalTemplate.content.firstElementChild\n      );\n    });\n    const rootEl = template.content.firstElementChild;\n    rootEl.id = this.id;\n    rootEl.setAttribute(PHX_ROOT_ID, this.root.id);\n    rootEl.setAttribute(PHX_SESSION, this.getSession());\n    rootEl.setAttribute(PHX_STATIC, this.getStatic());\n    rootEl.setAttribute(PHX_PARENT_ID, this.parent ? this.parent.id : null);\n    const formsToRecover = (\n      // we go over all forms in the new DOM; because this is only the HTML for the current\n      // view, we can be sure that all forms are owned by this view:\n      dom_default.all(template.content, \"form\").filter((newForm) => newForm.id && oldForms[newForm.id]).filter((newForm) => !this.pendingForms.has(newForm.id)).filter(\n        (newForm) => oldForms[newForm.id].getAttribute(phxChange) === newForm.getAttribute(phxChange)\n      ).map((newForm) => {\n        return [oldForms[newForm.id], newForm];\n      })\n    );\n    if (formsToRecover.length === 0) {\n      return callback();\n    }\n    formsToRecover.forEach(([oldForm, newForm], i) => {\n      this.pendingForms.add(newForm.id);\n      this.pushFormRecovery(\n        oldForm,\n        newForm,\n        template.content.firstElementChild,\n        () => {\n          this.pendingForms.delete(newForm.id);\n          if (i === formsToRecover.length - 1) {\n            callback();\n          }\n        }\n      );\n    });\n  }\n  getChildById(id) {\n    return this.root.children[this.id][id];\n  }\n  getDescendentByEl(el) {\n    if (el.id === this.id) {\n      return this;\n    } else {\n      return this.children[el.getAttribute(PHX_PARENT_ID)]?.[el.id];\n    }\n  }\n  destroyDescendent(id) {\n    for (const parentId in this.root.children) {\n      for (const childId in this.root.children[parentId]) {\n        if (childId === id) {\n          return this.root.children[parentId][childId].destroy();\n        }\n      }\n    }\n  }\n  joinChild(el) {\n    const child = this.getChildById(el.id);\n    if (!child) {\n      const view = new _View(el, this.liveSocket, this);\n      this.root.children[this.id][view.id] = view;\n      view.join();\n      this.childJoins++;\n      return true;\n    }\n  }\n  isJoinPending() {\n    return this.joinPending;\n  }\n  ackJoin(_child) {\n    this.childJoins--;\n    if (this.childJoins === 0) {\n      if (this.parent) {\n        this.parent.ackJoin(this);\n      } else {\n        this.onAllChildJoinsComplete();\n      }\n    }\n  }\n  onAllChildJoinsComplete() {\n    this.pendingForms.clear();\n    this.formsForRecovery = {};\n    this.joinCallback(() => {\n      this.pendingJoinOps.forEach(([view, op]) => {\n        if (!view.isDestroyed()) {\n          op();\n        }\n      });\n      this.pendingJoinOps = [];\n    });\n  }\n  update(diff, events, isPending = false) {\n    if (this.isJoinPending() || this.liveSocket.hasPendingLink() && this.root.isMain()) {\n      if (!isPending) {\n        this.pendingDiffs.push({ diff, events });\n      }\n      return false;\n    }\n    this.rendered.mergeDiff(diff);\n    let phxChildrenAdded = false;\n    if (this.rendered.isComponentOnlyDiff(diff)) {\n      this.liveSocket.time(\"component patch complete\", () => {\n        const parentCids = dom_default.findExistingParentCIDs(\n          this.id,\n          this.rendered.componentCIDs(diff)\n        );\n        parentCids.forEach((parentCID) => {\n          if (this.componentPatch(\n            this.rendered.getComponent(diff, parentCID),\n            parentCID\n          )) {\n            phxChildrenAdded = true;\n          }\n        });\n      });\n    } else if (!isEmpty(diff)) {\n      this.liveSocket.time(\"full patch complete\", () => {\n        const [html, streams] = this.renderContainer(diff, \"update\");\n        const patch = new DOMPatch(this, this.el, this.id, html, streams, null);\n        phxChildrenAdded = this.performPatch(patch, true);\n      });\n    }\n    this.liveSocket.dispatchEvents(events);\n    if (phxChildrenAdded) {\n      this.joinNewChildren();\n    }\n    return true;\n  }\n  renderContainer(diff, kind) {\n    return this.liveSocket.time(`toString diff (${kind})`, () => {\n      const tag = this.el.tagName;\n      const cids = diff ? this.rendered.componentCIDs(diff) : null;\n      const { buffer: html, streams } = this.rendered.toString(cids);\n      return [`<${tag}>${html}</${tag}>`, streams];\n    });\n  }\n  componentPatch(diff, cid) {\n    if (isEmpty(diff))\n      return false;\n    const { buffer: html, streams } = this.rendered.componentToString(cid);\n    const patch = new DOMPatch(this, this.el, this.id, html, streams, cid);\n    const childrenAdded = this.performPatch(patch, true);\n    return childrenAdded;\n  }\n  getHook(el) {\n    return this.viewHooks[ViewHook.elementID(el)];\n  }\n  addHook(el) {\n    const hookElId = ViewHook.elementID(el);\n    if (el.getAttribute && !this.ownsElement(el)) {\n      return;\n    }\n    if (hookElId && !this.viewHooks[hookElId]) {\n      if (ViewHook.deadHook(el)) {\n        return;\n      }\n      const hook = dom_default.getCustomElHook(el) || logError(`no hook found for custom element: ${el.id}`);\n      this.viewHooks[hookElId] = hook;\n      hook.__attachView(this);\n      return hook;\n    } else if (hookElId || !el.getAttribute) {\n      return;\n    } else {\n      const hookName = el.getAttribute(`data-phx-${PHX_HOOK}`) || el.getAttribute(this.binding(PHX_HOOK));\n      if (!hookName) {\n        return;\n      }\n      const hookDefinition = this.liveSocket.getHookDefinition(hookName);\n      if (hookDefinition) {\n        if (!el.id) {\n          logError(\n            `no DOM ID for hook \"${hookName}\". Hooks require a unique ID on each element.`,\n            el\n          );\n          return;\n        }\n        let hookInstance;\n        try {\n          if (typeof hookDefinition === \"function\" && hookDefinition.prototype instanceof ViewHook) {\n            hookInstance = new hookDefinition(this, el);\n          } else if (typeof hookDefinition === \"object\" && hookDefinition !== null) {\n            hookInstance = new ViewHook(this, el, hookDefinition);\n          } else {\n            logError(\n              `Invalid hook definition for \"${hookName}\". Expected a class extending ViewHook or an object definition.`,\n              el\n            );\n            return;\n          }\n        } catch (e) {\n          const errorMessage = e instanceof Error ? e.message : String(e);\n          logError(`Failed to create hook \"${hookName}\": ${errorMessage}`, el);\n          return;\n        }\n        this.viewHooks[ViewHook.elementID(hookInstance.el)] = hookInstance;\n        return hookInstance;\n      } else if (hookName !== null) {\n        logError(`unknown hook found for \"${hookName}\"`, el);\n      }\n    }\n  }\n  destroyHook(hook) {\n    const hookId = ViewHook.elementID(hook.el);\n    hook.__destroyed();\n    hook.__cleanup__();\n    delete this.viewHooks[hookId];\n  }\n  applyPendingUpdates() {\n    this.pendingDiffs = this.pendingDiffs.filter(\n      ({ diff, events }) => !this.update(diff, events, true)\n    );\n    this.eachChild((child) => child.applyPendingUpdates());\n  }\n  eachChild(callback) {\n    const children = this.root.children[this.id] || {};\n    for (const id in children) {\n      callback(this.getChildById(id));\n    }\n  }\n  onChannel(event, cb) {\n    this.liveSocket.onChannel(this.channel, event, (resp) => {\n      if (this.isJoinPending()) {\n        if (this.joinCount > 1) {\n          this.pendingJoinOps.push(() => cb(resp));\n        } else {\n          this.root.pendingJoinOps.push([this, () => cb(resp)]);\n        }\n      } else {\n        this.liveSocket.requestDOMUpdate(() => cb(resp));\n      }\n    });\n  }\n  bindChannel() {\n    this.liveSocket.onChannel(this.channel, \"diff\", (rawDiff) => {\n      this.liveSocket.requestDOMUpdate(() => {\n        this.applyDiff(\n          \"update\",\n          rawDiff,\n          ({ diff, events }) => this.update(diff, events)\n        );\n      });\n    });\n    this.onChannel(\n      \"redirect\",\n      ({ to, flash }) => this.onRedirect({ to, flash })\n    );\n    this.onChannel(\"live_patch\", (redir) => this.onLivePatch(redir));\n    this.onChannel(\"live_redirect\", (redir) => this.onLiveRedirect(redir));\n    this.channel.onError((reason) => this.onError(reason));\n    this.channel.onClose((reason) => this.onClose(reason));\n  }\n  destroyAllChildren() {\n    this.eachChild((child) => child.destroy());\n  }\n  onLiveRedirect(redir) {\n    const { to, kind, flash } = redir;\n    const url = this.expandURL(to);\n    const e = new CustomEvent(\"phx:server-navigate\", {\n      detail: { to, kind, flash }\n    });\n    this.liveSocket.historyRedirect(e, url, kind, flash);\n  }\n  onLivePatch(redir) {\n    const { to, kind } = redir;\n    this.href = this.expandURL(to);\n    this.liveSocket.historyPatch(to, kind);\n  }\n  expandURL(to) {\n    return to.startsWith(\"/\") ? `${window.location.protocol}//${window.location.host}${to}` : to;\n  }\n  /**\n   * @param {{to: string, flash?: string, reloadToken?: string}} redirect\n   */\n  onRedirect({ to, flash, reloadToken }) {\n    this.liveSocket.redirect(to, flash, reloadToken);\n  }\n  isDestroyed() {\n    return this.destroyed;\n  }\n  joinDead() {\n    this.isDead = true;\n  }\n  joinPush() {\n    this.joinPush = this.joinPush || this.channel.join();\n    return this.joinPush;\n  }\n  join(callback) {\n    this.showLoader(this.liveSocket.loaderTimeout);\n    this.bindChannel();\n    if (this.isMain()) {\n      this.stopCallback = this.liveSocket.withPageLoading({\n        to: this.href,\n        kind: \"initial\"\n      });\n    }\n    this.joinCallback = (onDone) => {\n      onDone = onDone || function() {\n      };\n      callback ? callback(this.joinCount, onDone) : onDone();\n    };\n    this.wrapPush(() => this.channel.join(), {\n      ok: (resp) => this.liveSocket.requestDOMUpdate(() => this.onJoin(resp)),\n      error: (error) => this.onJoinError(error),\n      timeout: () => this.onJoinError({ reason: \"timeout\" })\n    });\n  }\n  onJoinError(resp) {\n    if (resp.reason === \"reload\") {\n      this.log(\"error\", () => [\n        `failed mount with ${resp.status}. Falling back to page reload`,\n        resp\n      ]);\n      this.onRedirect({\n        to: this.liveSocket.main.href,\n        reloadToken: resp.token\n      });\n      return;\n    } else if (resp.reason === \"unauthorized\" || resp.reason === \"stale\") {\n      this.log(\"error\", () => [\n        \"unauthorized live_redirect. Falling back to page request\",\n        resp\n      ]);\n      this.onRedirect({ to: this.liveSocket.main.href, flash: this.flash });\n      return;\n    }\n    if (resp.redirect || resp.live_redirect) {\n      this.joinPending = false;\n      this.channel.leave();\n    }\n    if (resp.redirect) {\n      return this.onRedirect(resp.redirect);\n    }\n    if (resp.live_redirect) {\n      return this.onLiveRedirect(resp.live_redirect);\n    }\n    this.log(\"error\", () => [\"unable to join\", resp]);\n    if (this.isMain()) {\n      this.displayError(\n        [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS],\n        { unstructuredError: resp, errorKind: \"server\" }\n      );\n      if (this.liveSocket.isConnected()) {\n        this.liveSocket.reloadWithJitter(this);\n      }\n    } else {\n      if (this.joinAttempts >= MAX_CHILD_JOIN_ATTEMPTS) {\n        this.root.displayError(\n          [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS],\n          { unstructuredError: resp, errorKind: \"server\" }\n        );\n        this.log(\"error\", () => [\n          `giving up trying to mount after ${MAX_CHILD_JOIN_ATTEMPTS} tries`,\n          resp\n        ]);\n        this.destroy();\n      }\n      const trueChildEl = dom_default.byId(this.el.id);\n      if (trueChildEl) {\n        dom_default.mergeAttrs(trueChildEl, this.el);\n        this.displayError(\n          [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS],\n          { unstructuredError: resp, errorKind: \"server\" }\n        );\n        this.el = trueChildEl;\n      } else {\n        this.destroy();\n      }\n    }\n  }\n  onClose(reason) {\n    if (this.isDestroyed()) {\n      return;\n    }\n    if (this.isMain() && this.liveSocket.hasPendingLink() && reason !== \"leave\") {\n      return this.liveSocket.reloadWithJitter(this);\n    }\n    this.destroyAllChildren();\n    this.liveSocket.dropActiveElement(this);\n    if (this.liveSocket.isUnloaded()) {\n      this.showLoader(BEFORE_UNLOAD_LOADER_TIMEOUT);\n    }\n  }\n  onError(reason) {\n    this.onClose(reason);\n    if (this.liveSocket.isConnected()) {\n      this.log(\"error\", () => [\"view crashed\", reason]);\n    }\n    if (!this.liveSocket.isUnloaded()) {\n      if (this.liveSocket.isConnected()) {\n        this.displayError(\n          [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS],\n          { unstructuredError: reason, errorKind: \"server\" }\n        );\n      } else {\n        this.displayError(\n          [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_CLIENT_ERROR_CLASS],\n          { unstructuredError: reason, errorKind: \"client\" }\n        );\n      }\n    }\n  }\n  displayError(classes, details = {}) {\n    if (this.isMain()) {\n      dom_default.dispatchEvent(window, \"phx:page-loading-start\", {\n        detail: { to: this.href, kind: \"error\", ...details }\n      });\n    }\n    this.showLoader();\n    this.setContainerClasses(...classes);\n    this.delayedDisconnected();\n  }\n  delayedDisconnected() {\n    this.disconnectedTimer = setTimeout(() => {\n      this.execAll(this.binding(\"disconnected\"));\n    }, this.liveSocket.disconnectedTimeout);\n  }\n  wrapPush(callerPush, receives) {\n    const latency = this.liveSocket.getLatencySim();\n    const withLatency = latency ? (cb) => setTimeout(() => !this.isDestroyed() && cb(), latency) : (cb) => !this.isDestroyed() && cb();\n    withLatency(() => {\n      callerPush().receive(\n        \"ok\",\n        (resp) => withLatency(() => receives.ok && receives.ok(resp))\n      ).receive(\n        \"error\",\n        (reason) => withLatency(() => receives.error && receives.error(reason))\n      ).receive(\n        \"timeout\",\n        () => withLatency(() => receives.timeout && receives.timeout())\n      );\n    });\n  }\n  pushWithReply(refGenerator, event, payload) {\n    if (!this.isConnected()) {\n      return Promise.reject(new Error(\"no connection\"));\n    }\n    const [ref, [el], opts] = refGenerator ? refGenerator({ payload }) : [null, [], {}];\n    const oldJoinCount = this.joinCount;\n    let onLoadingDone = function() {\n    };\n    if (opts.page_loading) {\n      onLoadingDone = this.liveSocket.withPageLoading({\n        kind: \"element\",\n        target: el\n      });\n    }\n    if (typeof payload.cid !== \"number\") {\n      delete payload.cid;\n    }\n    return new Promise((resolve, reject) => {\n      this.wrapPush(() => this.channel.push(event, payload, PUSH_TIMEOUT), {\n        ok: (resp) => {\n          if (ref !== null) {\n            this.lastAckRef = ref;\n          }\n          const finish = (hookReply) => {\n            if (resp.redirect) {\n              this.onRedirect(resp.redirect);\n            }\n            if (resp.live_patch) {\n              this.onLivePatch(resp.live_patch);\n            }\n            if (resp.live_redirect) {\n              this.onLiveRedirect(resp.live_redirect);\n            }\n            onLoadingDone();\n            resolve({ resp, reply: hookReply, ref });\n          };\n          if (resp.diff) {\n            this.liveSocket.requestDOMUpdate(() => {\n              this.applyDiff(\"update\", resp.diff, ({ diff, reply, events }) => {\n                if (ref !== null) {\n                  this.undoRefs(ref, payload.event);\n                }\n                this.update(diff, events);\n                finish(reply);\n              });\n            });\n          } else {\n            if (ref !== null) {\n              this.undoRefs(ref, payload.event);\n            }\n            finish(null);\n          }\n        },\n        error: (reason) => reject(new Error(`failed with reason: ${JSON.stringify(reason)}`)),\n        timeout: () => {\n          reject(new Error(\"timeout\"));\n          if (this.joinCount === oldJoinCount) {\n            this.liveSocket.reloadWithJitter(this, () => {\n              this.log(\"timeout\", () => [\n                \"received timeout while communicating with server. Falling back to hard refresh for recovery\"\n              ]);\n            });\n          }\n        }\n      });\n    });\n  }\n  undoRefs(ref, phxEvent, onlyEls) {\n    if (!this.isConnected()) {\n      return;\n    }\n    const selector = `[${PHX_REF_SRC}=\"${this.refSrc()}\"]`;\n    if (onlyEls) {\n      onlyEls = new Set(onlyEls);\n      dom_default.all(document, selector, (parent) => {\n        if (onlyEls && !onlyEls.has(parent)) {\n          return;\n        }\n        dom_default.all(\n          parent,\n          selector,\n          (child) => this.undoElRef(child, ref, phxEvent)\n        );\n        this.undoElRef(parent, ref, phxEvent);\n      });\n    } else {\n      dom_default.all(document, selector, (el) => this.undoElRef(el, ref, phxEvent));\n    }\n  }\n  undoElRef(el, ref, phxEvent) {\n    const elRef = new ElementRef(el);\n    elRef.maybeUndo(ref, phxEvent, (clonedTree) => {\n      const patch = new DOMPatch(this, el, this.id, clonedTree, [], null, {\n        undoRef: ref\n      });\n      const phxChildrenAdded = this.performPatch(patch, true);\n      dom_default.all(\n        el,\n        `[${PHX_REF_SRC}=\"${this.refSrc()}\"]`,\n        (child) => this.undoElRef(child, ref, phxEvent)\n      );\n      if (phxChildrenAdded) {\n        this.joinNewChildren();\n      }\n    });\n  }\n  refSrc() {\n    return this.el.id;\n  }\n  putRef(elements, phxEvent, eventType, opts = {}) {\n    const newRef = this.ref++;\n    const disableWith = this.binding(PHX_DISABLE_WITH);\n    if (opts.loading) {\n      const loadingEls = dom_default.all(document, opts.loading).map((el) => {\n        return { el, lock: true, loading: true };\n      });\n      elements = elements.concat(loadingEls);\n    }\n    for (const { el, lock, loading } of elements) {\n      if (!lock && !loading) {\n        throw new Error(\"putRef requires lock or loading\");\n      }\n      el.setAttribute(PHX_REF_SRC, this.refSrc());\n      if (loading) {\n        el.setAttribute(PHX_REF_LOADING, newRef);\n      }\n      if (lock) {\n        el.setAttribute(PHX_REF_LOCK, newRef);\n      }\n      if (!loading || opts.submitter && !(el === opts.submitter || el === opts.form)) {\n        continue;\n      }\n      const lockCompletePromise = new Promise((resolve) => {\n        el.addEventListener(`phx:undo-lock:${newRef}`, () => resolve(detail), {\n          once: true\n        });\n      });\n      const loadingCompletePromise = new Promise((resolve) => {\n        el.addEventListener(\n          `phx:undo-loading:${newRef}`,\n          () => resolve(detail),\n          { once: true }\n        );\n      });\n      el.classList.add(`phx-${eventType}-loading`);\n      const disableText = el.getAttribute(disableWith);\n      if (disableText !== null) {\n        if (!el.getAttribute(PHX_DISABLE_WITH_RESTORE)) {\n          el.setAttribute(PHX_DISABLE_WITH_RESTORE, el.textContent);\n        }\n        if (disableText !== \"\") {\n          el.textContent = disableText;\n        }\n        el.setAttribute(\n          PHX_DISABLED,\n          el.getAttribute(PHX_DISABLED) || el.disabled\n        );\n        el.setAttribute(\"disabled\", \"\");\n      }\n      const detail = {\n        event: phxEvent,\n        eventType,\n        ref: newRef,\n        isLoading: loading,\n        isLocked: lock,\n        lockElements: elements.filter(({ lock: lock2 }) => lock2).map(({ el: el2 }) => el2),\n        loadingElements: elements.filter(({ loading: loading2 }) => loading2).map(({ el: el2 }) => el2),\n        unlock: (els) => {\n          els = Array.isArray(els) ? els : [els];\n          this.undoRefs(newRef, phxEvent, els);\n        },\n        lockComplete: lockCompletePromise,\n        loadingComplete: loadingCompletePromise,\n        lock: (lockEl) => {\n          return new Promise((resolve) => {\n            if (this.isAcked(newRef)) {\n              return resolve(detail);\n            }\n            lockEl.setAttribute(PHX_REF_LOCK, newRef);\n            lockEl.setAttribute(PHX_REF_SRC, this.refSrc());\n            lockEl.addEventListener(\n              `phx:lock-stop:${newRef}`,\n              () => resolve(detail),\n              { once: true }\n            );\n          });\n        }\n      };\n      if (opts.payload) {\n        detail[\"payload\"] = opts.payload;\n      }\n      if (opts.target) {\n        detail[\"target\"] = opts.target;\n      }\n      if (opts.originalEvent) {\n        detail[\"originalEvent\"] = opts.originalEvent;\n      }\n      el.dispatchEvent(\n        new CustomEvent(\"phx:push\", {\n          detail,\n          bubbles: true,\n          cancelable: false\n        })\n      );\n      if (phxEvent) {\n        el.dispatchEvent(\n          new CustomEvent(`phx:push:${phxEvent}`, {\n            detail,\n            bubbles: true,\n            cancelable: false\n          })\n        );\n      }\n    }\n    return [newRef, elements.map(({ el }) => el), opts];\n  }\n  isAcked(ref) {\n    return this.lastAckRef !== null && this.lastAckRef >= ref;\n  }\n  componentID(el) {\n    const cid = el.getAttribute && el.getAttribute(PHX_COMPONENT);\n    return cid ? parseInt(cid) : null;\n  }\n  targetComponentID(target, targetCtx, opts = {}) {\n    if (isCid(targetCtx)) {\n      return targetCtx;\n    }\n    const cidOrSelector = opts.target || target.getAttribute(this.binding(\"target\"));\n    if (isCid(cidOrSelector)) {\n      return parseInt(cidOrSelector);\n    } else if (targetCtx && (cidOrSelector !== null || opts.target)) {\n      return this.closestComponentID(targetCtx);\n    } else {\n      return null;\n    }\n  }\n  closestComponentID(targetCtx) {\n    if (isCid(targetCtx)) {\n      return targetCtx;\n    } else if (targetCtx) {\n      return maybe(\n        // We either use the closest data-phx-component binding, or -\n        // in case of portals - continue with the portal source.\n        // This is necessary if teleporting an element outside of its LiveComponent.\n        targetCtx.closest(`[${PHX_COMPONENT}],[${PHX_TELEPORTED_SRC}]`),\n        (el) => {\n          if (el.hasAttribute(PHX_COMPONENT)) {\n            return this.ownsElement(el) && this.componentID(el);\n          }\n          if (el.hasAttribute(PHX_TELEPORTED_SRC)) {\n            const portalParent = dom_default.byId(el.getAttribute(PHX_TELEPORTED_SRC));\n            return this.closestComponentID(portalParent);\n          }\n        }\n      );\n    } else {\n      return null;\n    }\n  }\n  pushHookEvent(el, targetCtx, event, payload) {\n    if (!this.isConnected()) {\n      this.log(\"hook\", () => [\n        \"unable to push hook event. LiveView not connected\",\n        event,\n        payload\n      ]);\n      return Promise.reject(\n        new Error(\"unable to push hook event. LiveView not connected\")\n      );\n    }\n    const refGenerator = () => this.putRef([{ el, loading: true, lock: true }], event, \"hook\", {\n      payload,\n      target: targetCtx\n    });\n    return this.pushWithReply(refGenerator, \"event\", {\n      type: \"hook\",\n      event,\n      value: payload,\n      cid: this.closestComponentID(targetCtx)\n    }).then(({ resp: _resp, reply, ref }) => ({ reply, ref }));\n  }\n  extractMeta(el, meta, value) {\n    const prefix = this.binding(\"value-\");\n    for (let i = 0; i < el.attributes.length; i++) {\n      if (!meta) {\n        meta = {};\n      }\n      const name = el.attributes[i].name;\n      if (name.startsWith(prefix)) {\n        meta[name.replace(prefix, \"\")] = el.getAttribute(name);\n      }\n    }\n    if (el.value !== void 0 && !(el instanceof HTMLFormElement)) {\n      if (!meta) {\n        meta = {};\n      }\n      meta.value = el.value;\n      if (el.tagName === \"INPUT\" && CHECKABLE_INPUTS.indexOf(el.type) >= 0 && !el.checked) {\n        delete meta.value;\n      }\n    }\n    if (value) {\n      if (!meta) {\n        meta = {};\n      }\n      for (const key in value) {\n        meta[key] = value[key];\n      }\n    }\n    return meta;\n  }\n  serializeForm(form, opts, onlyNames = []) {\n    const { submitter } = opts;\n    let injectedElement;\n    if (submitter && submitter.name) {\n      const input = document.createElement(\"input\");\n      input.type = \"hidden\";\n      const formId = submitter.getAttribute(\"form\");\n      if (formId) {\n        input.setAttribute(\"form\", formId);\n      }\n      input.name = submitter.name;\n      input.value = submitter.value;\n      submitter.parentElement.insertBefore(input, submitter);\n      injectedElement = input;\n    }\n    const formData = new FormData(form);\n    const toRemove = [];\n    formData.forEach((val, key, _index) => {\n      if (val instanceof File) {\n        toRemove.push(key);\n      }\n    });\n    toRemove.forEach((key) => formData.delete(key));\n    const params = new URLSearchParams();\n    const { inputsUnused, onlyHiddenInputs } = Array.from(form.elements).reduce(\n      (acc, input) => {\n        const { inputsUnused: inputsUnused2, onlyHiddenInputs: onlyHiddenInputs2 } = acc;\n        const key = input.name;\n        if (!key) {\n          return acc;\n        }\n        if (inputsUnused2[key] === void 0) {\n          inputsUnused2[key] = true;\n        }\n        if (onlyHiddenInputs2[key] === void 0) {\n          onlyHiddenInputs2[key] = true;\n        }\n        const inputSkipUnusedField = input.hasAttribute(\n          this.binding(PHX_NO_UNUSED_FIELD)\n        );\n        const isUsed = dom_default.private(input, PHX_HAS_FOCUSED) || dom_default.private(input, PHX_HAS_SUBMITTED) || inputSkipUnusedField;\n        const isHidden = input.type === \"hidden\";\n        inputsUnused2[key] = inputsUnused2[key] && !isUsed;\n        onlyHiddenInputs2[key] = onlyHiddenInputs2[key] && isHidden;\n        return acc;\n      },\n      { inputsUnused: {}, onlyHiddenInputs: {} }\n    );\n    const formSkipUnusedFields = form.hasAttribute(\n      this.binding(PHX_NO_UNUSED_FIELD)\n    );\n    for (const [key, val] of formData.entries()) {\n      if (onlyNames.length === 0 || onlyNames.indexOf(key) >= 0) {\n        const isUnused = inputsUnused[key];\n        const hidden = onlyHiddenInputs[key];\n        const skipUnusedCheck = formSkipUnusedFields;\n        if (!skipUnusedCheck && isUnused && !(submitter && submitter.name == key) && !hidden) {\n          params.append(prependFormDataKey(key, \"_unused_\"), \"\");\n        }\n        if (typeof val === \"string\") {\n          params.append(key, val);\n        }\n      }\n    }\n    if (submitter && injectedElement) {\n      submitter.parentElement.removeChild(injectedElement);\n    }\n    return params.toString();\n  }\n  pushEvent(type, el, targetCtx, phxEvent, meta, opts = {}, onReply) {\n    this.pushWithReply(\n      (maybePayload) => this.putRef([{ el, loading: true, lock: true }], phxEvent, type, {\n        ...opts,\n        payload: maybePayload?.payload\n      }),\n      \"event\",\n      {\n        type,\n        event: phxEvent,\n        value: this.extractMeta(el, meta, opts.value),\n        cid: this.targetComponentID(el, targetCtx, opts)\n      }\n    ).then(({ reply }) => onReply && onReply(reply)).catch((error) => logError(\"Failed to push event\", error));\n  }\n  pushFileProgress(fileEl, entryRef, progress, onReply = function() {\n  }) {\n    this.liveSocket.withinOwners(fileEl.form, (view, targetCtx) => {\n      view.pushWithReply(null, \"progress\", {\n        event: fileEl.getAttribute(view.binding(PHX_PROGRESS)),\n        ref: fileEl.getAttribute(PHX_UPLOAD_REF),\n        entry_ref: entryRef,\n        progress,\n        cid: view.targetComponentID(fileEl.form, targetCtx)\n      }).then(() => onReply()).catch((error) => logError(\"Failed to push file progress\", error));\n    });\n  }\n  pushInput(inputEl, targetCtx, forceCid, phxEvent, opts, callback) {\n    if (!inputEl.form) {\n      throw new Error(\"form events require the input to be inside a form\");\n    }\n    let uploads;\n    const cid = isCid(forceCid) ? forceCid : this.targetComponentID(inputEl.form, targetCtx, opts);\n    const refGenerator = (maybePayload) => {\n      return this.putRef(\n        [\n          { el: inputEl, loading: true, lock: true },\n          { el: inputEl.form, loading: true, lock: true }\n        ],\n        phxEvent,\n        \"change\",\n        { ...opts, payload: maybePayload?.payload }\n      );\n    };\n    let formData;\n    const meta = this.extractMeta(inputEl.form, {}, opts.value);\n    const serializeOpts = {};\n    if (inputEl instanceof HTMLButtonElement) {\n      serializeOpts.submitter = inputEl;\n    }\n    if (inputEl.getAttribute(this.binding(\"change\"))) {\n      formData = this.serializeForm(inputEl.form, serializeOpts, [\n        inputEl.name\n      ]);\n    } else {\n      formData = this.serializeForm(inputEl.form, serializeOpts);\n    }\n    if (dom_default.isUploadInput(inputEl) && inputEl.files && inputEl.files.length > 0) {\n      LiveUploader.trackFiles(inputEl, Array.from(inputEl.files));\n    }\n    uploads = LiveUploader.serializeUploads(inputEl);\n    const event = {\n      type: \"form\",\n      event: phxEvent,\n      value: formData,\n      meta: {\n        // no target was implicitly sent as \"undefined\" in LV <= 1.0.5, therefore\n        // we have to keep it. In 1.0.6 we switched from passing meta as URL encoded data\n        // to passing it directly in the event, but the JSON encode would drop keys with\n        // undefined values.\n        _target: opts._target || \"undefined\",\n        ...meta\n      },\n      uploads,\n      cid\n    };\n    this.pushWithReply(refGenerator, \"event\", event).then(({ resp }) => {\n      if (dom_default.isUploadInput(inputEl) && dom_default.isAutoUpload(inputEl)) {\n        ElementRef.onUnlock(inputEl, () => {\n          if (LiveUploader.filesAwaitingPreflight(inputEl).length > 0) {\n            const [ref, _els] = refGenerator();\n            this.undoRefs(ref, phxEvent, [inputEl.form]);\n            this.uploadFiles(\n              inputEl.form,\n              phxEvent,\n              targetCtx,\n              ref,\n              cid,\n              (_uploads) => {\n                callback && callback(resp);\n                this.triggerAwaitingSubmit(inputEl.form, phxEvent);\n                this.undoRefs(ref, phxEvent);\n              }\n            );\n          }\n        });\n      } else {\n        callback && callback(resp);\n      }\n    }).catch((error) => logError(\"Failed to push input event\", error));\n  }\n  triggerAwaitingSubmit(formEl, phxEvent) {\n    const awaitingSubmit = this.getScheduledSubmit(formEl);\n    if (awaitingSubmit) {\n      const [_el, _ref, _opts, callback] = awaitingSubmit;\n      this.cancelSubmit(formEl, phxEvent);\n      callback();\n    }\n  }\n  getScheduledSubmit(formEl) {\n    return this.formSubmits.find(\n      ([el, _ref, _opts, _callback]) => el.isSameNode(formEl)\n    );\n  }\n  scheduleSubmit(formEl, ref, opts, callback) {\n    if (this.getScheduledSubmit(formEl)) {\n      return true;\n    }\n    this.formSubmits.push([formEl, ref, opts, callback]);\n  }\n  cancelSubmit(formEl, phxEvent) {\n    this.formSubmits = this.formSubmits.filter(\n      ([el, ref, _opts, _callback]) => {\n        if (el.isSameNode(formEl)) {\n          this.undoRefs(ref, phxEvent);\n          return false;\n        } else {\n          return true;\n        }\n      }\n    );\n  }\n  disableForm(formEl, phxEvent, opts = {}) {\n    const filterIgnored = (el) => {\n      const userIgnored = closestPhxBinding(\n        el,\n        `${this.binding(PHX_UPDATE)}=ignore`,\n        el.form\n      );\n      return !(userIgnored || closestPhxBinding(el, \"data-phx-update=ignore\", el.form));\n    };\n    const filterDisables = (el) => {\n      return el.hasAttribute(this.binding(PHX_DISABLE_WITH));\n    };\n    const filterButton = (el) => el.tagName == \"BUTTON\";\n    const filterInput = (el) => [\"INPUT\", \"TEXTAREA\", \"SELECT\"].includes(el.tagName);\n    const formElements = Array.from(formEl.elements);\n    const disables = formElements.filter(filterDisables);\n    const buttons = formElements.filter(filterButton).filter(filterIgnored);\n    const inputs = formElements.filter(filterInput).filter(filterIgnored);\n    buttons.forEach((button) => {\n      button.setAttribute(PHX_DISABLED, button.disabled);\n      button.disabled = true;\n    });\n    inputs.forEach((input) => {\n      input.setAttribute(PHX_READONLY, input.readOnly);\n      input.readOnly = true;\n      if (input.files) {\n        input.setAttribute(PHX_DISABLED, input.disabled);\n        input.disabled = true;\n      }\n    });\n    const formEls = disables.concat(buttons).concat(inputs).map((el) => {\n      return { el, loading: true, lock: true };\n    });\n    const els = [{ el: formEl, loading: true, lock: false }].concat(formEls).reverse();\n    return this.putRef(els, phxEvent, \"submit\", opts);\n  }\n  pushFormSubmit(formEl, targetCtx, phxEvent, submitter, opts, onReply) {\n    const refGenerator = (maybePayload) => this.disableForm(formEl, phxEvent, {\n      ...opts,\n      form: formEl,\n      payload: maybePayload?.payload,\n      submitter\n    });\n    dom_default.putPrivate(formEl, \"submitter\", submitter);\n    const cid = this.targetComponentID(formEl, targetCtx);\n    if (LiveUploader.hasUploadsInProgress(formEl)) {\n      const [ref, _els] = refGenerator();\n      const push = () => this.pushFormSubmit(\n        formEl,\n        targetCtx,\n        phxEvent,\n        submitter,\n        opts,\n        onReply\n      );\n      return this.scheduleSubmit(formEl, ref, opts, push);\n    } else if (LiveUploader.inputsAwaitingPreflight(formEl).length > 0) {\n      const [ref, els] = refGenerator();\n      const proxyRefGen = () => [ref, els, opts];\n      this.uploadFiles(formEl, phxEvent, targetCtx, ref, cid, (_uploads) => {\n        if (LiveUploader.inputsAwaitingPreflight(formEl).length > 0) {\n          return this.undoRefs(ref, phxEvent);\n        }\n        const meta = this.extractMeta(formEl, {}, opts.value);\n        const formData = this.serializeForm(formEl, { submitter });\n        this.pushWithReply(proxyRefGen, \"event\", {\n          type: \"form\",\n          event: phxEvent,\n          value: formData,\n          meta,\n          cid\n        }).then(({ resp }) => onReply(resp)).catch((error) => logError(\"Failed to push form submit\", error));\n      });\n    } else if (!(formEl.hasAttribute(PHX_REF_SRC) && formEl.classList.contains(\"phx-submit-loading\"))) {\n      const meta = this.extractMeta(formEl, {}, opts.value);\n      const formData = this.serializeForm(formEl, { submitter });\n      this.pushWithReply(refGenerator, \"event\", {\n        type: \"form\",\n        event: phxEvent,\n        value: formData,\n        meta,\n        cid\n      }).then(({ resp }) => onReply(resp)).catch((error) => logError(\"Failed to push form submit\", error));\n    }\n  }\n  uploadFiles(formEl, phxEvent, targetCtx, ref, cid, onComplete) {\n    const joinCountAtUpload = this.joinCount;\n    const inputEls = LiveUploader.activeFileInputs(formEl);\n    let numFileInputsInProgress = inputEls.length;\n    inputEls.forEach((inputEl) => {\n      const uploader = new LiveUploader(inputEl, this, () => {\n        numFileInputsInProgress--;\n        if (numFileInputsInProgress === 0) {\n          onComplete();\n        }\n      });\n      const entries = uploader.entries().map((entry) => entry.toPreflightPayload());\n      if (entries.length === 0) {\n        numFileInputsInProgress--;\n        return;\n      }\n      const payload = {\n        ref: inputEl.getAttribute(PHX_UPLOAD_REF),\n        entries,\n        cid: this.targetComponentID(inputEl.form, targetCtx)\n      };\n      this.log(\"upload\", () => [\"sending preflight request\", payload]);\n      this.pushWithReply(null, \"allow_upload\", payload).then(({ resp }) => {\n        this.log(\"upload\", () => [\"got preflight response\", resp]);\n        uploader.entries().forEach((entry) => {\n          if (resp.entries && !resp.entries[entry.ref]) {\n            this.handleFailedEntryPreflight(\n              entry.ref,\n              \"failed preflight\",\n              uploader\n            );\n          }\n        });\n        if (resp.error || Object.keys(resp.entries).length === 0) {\n          this.undoRefs(ref, phxEvent);\n          const errors = resp.error || [];\n          errors.map(([entry_ref, reason]) => {\n            this.handleFailedEntryPreflight(entry_ref, reason, uploader);\n          });\n        } else {\n          const onError = (callback) => {\n            this.channel.onError(() => {\n              if (this.joinCount === joinCountAtUpload) {\n                callback();\n              }\n            });\n          };\n          uploader.initAdapterUpload(resp, onError, this.liveSocket);\n        }\n      }).catch((error) => logError(\"Failed to push upload\", error));\n    });\n  }\n  handleFailedEntryPreflight(uploadRef, reason, uploader) {\n    if (uploader.isAutoUpload()) {\n      const entry = uploader.entries().find((entry2) => entry2.ref === uploadRef.toString());\n      if (entry) {\n        entry.cancel();\n      }\n    } else {\n      uploader.entries().map((entry) => entry.cancel());\n    }\n    this.log(\"upload\", () => [`error for entry ${uploadRef}`, reason]);\n  }\n  dispatchUploads(targetCtx, name, filesOrBlobs) {\n    const targetElement = this.targetCtxElement(targetCtx) || this.el;\n    const inputs = dom_default.findUploadInputs(targetElement).filter(\n      (el) => el.name === name\n    );\n    if (inputs.length === 0) {\n      logError(`no live file inputs found matching the name \"${name}\"`);\n    } else if (inputs.length > 1) {\n      logError(`duplicate live file inputs found matching the name \"${name}\"`);\n    } else {\n      dom_default.dispatchEvent(inputs[0], PHX_TRACK_UPLOADS, {\n        detail: { files: filesOrBlobs }\n      });\n    }\n  }\n  targetCtxElement(targetCtx) {\n    if (isCid(targetCtx)) {\n      const [target] = dom_default.findComponentNodeList(this.id, targetCtx);\n      return target;\n    } else if (targetCtx) {\n      return targetCtx;\n    } else {\n      return null;\n    }\n  }\n  pushFormRecovery(oldForm, newForm, templateDom, callback) {\n    const phxChange = this.binding(\"change\");\n    const phxTarget = newForm.getAttribute(this.binding(\"target\")) || newForm;\n    const phxEvent = newForm.getAttribute(this.binding(PHX_AUTO_RECOVER)) || newForm.getAttribute(this.binding(\"change\"));\n    const inputs = Array.from(oldForm.elements).filter(\n      (el) => dom_default.isFormInput(el) && el.name && !el.hasAttribute(phxChange)\n    );\n    if (inputs.length === 0) {\n      callback();\n      return;\n    }\n    inputs.forEach(\n      (input2) => input2.hasAttribute(PHX_UPLOAD_REF) && LiveUploader.clearFiles(input2)\n    );\n    const input = inputs.find((el) => el.type !== \"hidden\") || inputs[0];\n    let pending = 0;\n    this.withinTargets(\n      phxTarget,\n      (targetView, targetCtx) => {\n        const cid = this.targetComponentID(newForm, targetCtx);\n        pending++;\n        let e = new CustomEvent(\"phx:form-recovery\", {\n          detail: { sourceElement: oldForm }\n        });\n        js_default.exec(e, \"change\", phxEvent, this, input, [\n          \"push\",\n          {\n            _target: input.name,\n            targetView,\n            targetCtx,\n            newCid: cid,\n            callback: () => {\n              pending--;\n              if (pending === 0) {\n                callback();\n              }\n            }\n          }\n        ]);\n      },\n      templateDom\n    );\n  }\n  pushLinkPatch(e, href, targetEl, callback) {\n    const linkRef = this.liveSocket.setPendingLink(href);\n    const loading = e.isTrusted && e.type !== \"popstate\";\n    const refGen = targetEl ? () => this.putRef(\n      [{ el: targetEl, loading, lock: true }],\n      null,\n      \"click\"\n    ) : null;\n    const fallback = () => this.liveSocket.redirect(window.location.href);\n    const url = href.startsWith(\"/\") ? `${location.protocol}//${location.host}${href}` : href;\n    this.pushWithReply(refGen, \"live_patch\", { url }).then(\n      ({ resp }) => {\n        this.liveSocket.requestDOMUpdate(() => {\n          if (resp.link_redirect) {\n            this.liveSocket.replaceMain(href, null, callback, linkRef);\n          } else if (resp.redirect) {\n            return;\n          } else {\n            if (this.liveSocket.commitPendingLink(linkRef)) {\n              this.href = href;\n            }\n            this.applyPendingUpdates();\n            callback && callback(linkRef);\n          }\n        });\n      },\n      ({ error: _error, timeout: _timeout }) => fallback()\n    );\n  }\n  getFormsForRecovery() {\n    if (this.joinCount === 0) {\n      return {};\n    }\n    const phxChange = this.binding(\"change\");\n    return dom_default.all(\n      document,\n      `#${CSS.escape(this.id)} form[${phxChange}], [${PHX_TELEPORTED_REF}=\"${CSS.escape(this.id)}\"] form[${phxChange}]`\n    ).filter((form) => form.id).filter((form) => form.elements.length > 0).filter(\n      (form) => form.getAttribute(this.binding(PHX_AUTO_RECOVER)) !== \"ignore\"\n    ).map((form) => {\n      const clonedForm = form.cloneNode(true);\n      morphdom_esm_default(clonedForm, form, {\n        onBeforeElUpdated: (fromEl, toEl) => {\n          dom_default.copyPrivates(fromEl, toEl);\n          if (fromEl.getAttribute(\"form\") === form.id) {\n            fromEl.parentNode.removeChild(fromEl);\n            return false;\n          }\n          return true;\n        }\n      });\n      const externalElements = document.querySelectorAll(\n        `[form=\"${CSS.escape(form.id)}\"]`\n      );\n      Array.from(externalElements).forEach((el) => {\n        const clonedEl = (\n          /** @type {HTMLElement} */\n          el.cloneNode(true)\n        );\n        morphdom_esm_default(clonedEl, el);\n        dom_default.copyPrivates(clonedEl, el);\n        clonedEl.removeAttribute(\"form\");\n        clonedForm.appendChild(clonedEl);\n      });\n      return clonedForm;\n    }).reduce((acc, form) => {\n      acc[form.id] = form;\n      return acc;\n    }, {});\n  }\n  maybePushComponentsDestroyed(destroyedCIDs) {\n    let willDestroyCIDs = destroyedCIDs.filter((cid) => {\n      return dom_default.findComponentNodeList(this.id, cid).length === 0;\n    });\n    const onError = (error) => {\n      if (!this.isDestroyed()) {\n        logError(\"Failed to push components destroyed\", error);\n      }\n    };\n    if (willDestroyCIDs.length > 0) {\n      willDestroyCIDs.forEach((cid) => this.rendered.resetRender(cid));\n      this.pushWithReply(null, \"cids_will_destroy\", { cids: willDestroyCIDs }).then(() => {\n        this.liveSocket.requestDOMUpdate(() => {\n          let completelyDestroyCIDs = willDestroyCIDs.filter((cid) => {\n            return dom_default.findComponentNodeList(this.id, cid).length === 0;\n          });\n          if (completelyDestroyCIDs.length > 0) {\n            this.pushWithReply(null, \"cids_destroyed\", {\n              cids: completelyDestroyCIDs\n            }).then(({ resp }) => {\n              this.rendered.pruneCIDs(resp.cids);\n            }).catch(onError);\n          }\n        });\n      }).catch(onError);\n    }\n  }\n  ownsElement(el) {\n    let parentViewEl = dom_default.closestViewEl(el);\n    return el.getAttribute(PHX_PARENT_ID) === this.id || parentViewEl && parentViewEl.id === this.id || !parentViewEl && this.isDead;\n  }\n  submitForm(form, targetCtx, phxEvent, submitter, opts = {}) {\n    dom_default.putPrivate(form, PHX_HAS_SUBMITTED, true);\n    const inputs = Array.from(form.elements);\n    inputs.forEach((input) => dom_default.putPrivate(input, PHX_HAS_SUBMITTED, true));\n    this.liveSocket.blurActiveElement(this);\n    this.pushFormSubmit(form, targetCtx, phxEvent, submitter, opts, () => {\n      this.liveSocket.restorePreviouslyActiveFocus();\n    });\n  }\n  binding(kind) {\n    return this.liveSocket.binding(kind);\n  }\n  // phx-portal\n  pushPortalElementId(id) {\n    this.portalElementIds.add(id);\n  }\n  dropPortalElementId(id) {\n    this.portalElementIds.delete(id);\n  }\n  destroyPortalElements() {\n    if (!this.liveSocket.unloaded) {\n      this.portalElementIds.forEach((id) => {\n        const el = document.getElementById(id);\n        if (el) {\n          el.remove();\n        }\n      });\n    }\n  }\n};\n\n// js/phoenix_live_view/live_socket.js\nvar isUsedInput = (el) => dom_default.isUsedInput(el);\nvar LiveSocket = class {\n  constructor(url, phxSocket, opts = {}) {\n    this.unloaded = false;\n    if (!phxSocket || phxSocket.constructor.name === \"Object\") {\n      throw new Error(`\n      a phoenix Socket must be provided as the second argument to the LiveSocket constructor. For example:\n\n          import {Socket} from \"phoenix\"\n          import {LiveSocket} from \"phoenix_live_view\"\n          let liveSocket = new LiveSocket(\"/live\", Socket, {...})\n      `);\n    }\n    this.socket = new phxSocket(url, opts);\n    this.bindingPrefix = opts.bindingPrefix || BINDING_PREFIX;\n    this.opts = opts;\n    this.params = closure(opts.params || {});\n    this.viewLogger = opts.viewLogger;\n    this.metadataCallbacks = opts.metadata || {};\n    this.defaults = Object.assign(clone(DEFAULTS), opts.defaults || {});\n    this.prevActive = null;\n    this.silenced = false;\n    this.main = null;\n    this.outgoingMainEl = null;\n    this.clickStartedAtTarget = null;\n    this.linkRef = 1;\n    this.roots = {};\n    this.href = window.location.href;\n    this.pendingLink = null;\n    this.currentLocation = clone(window.location);\n    this.hooks = opts.hooks || {};\n    this.uploaders = opts.uploaders || {};\n    this.loaderTimeout = opts.loaderTimeout || LOADER_TIMEOUT;\n    this.disconnectedTimeout = opts.disconnectedTimeout || DISCONNECTED_TIMEOUT;\n    this.reloadWithJitterTimer = null;\n    this.maxReloads = opts.maxReloads || MAX_RELOADS;\n    this.reloadJitterMin = opts.reloadJitterMin || RELOAD_JITTER_MIN;\n    this.reloadJitterMax = opts.reloadJitterMax || RELOAD_JITTER_MAX;\n    this.failsafeJitter = opts.failsafeJitter || FAILSAFE_JITTER;\n    this.localStorage = opts.localStorage || window.localStorage;\n    this.sessionStorage = opts.sessionStorage || window.sessionStorage;\n    this.boundTopLevelEvents = false;\n    this.boundEventNames = /* @__PURE__ */ new Set();\n    this.blockPhxChangeWhileComposing = opts.blockPhxChangeWhileComposing || false;\n    this.serverCloseRef = null;\n    this.domCallbacks = Object.assign(\n      {\n        jsQuerySelectorAll: null,\n        onPatchStart: closure(),\n        onPatchEnd: closure(),\n        onNodeAdded: closure(),\n        onBeforeElUpdated: closure()\n      },\n      opts.dom || {}\n    );\n    this.transitions = new TransitionSet();\n    this.currentHistoryPosition = parseInt(this.sessionStorage.getItem(PHX_LV_HISTORY_POSITION)) || 0;\n    window.addEventListener(\"pagehide\", (_e) => {\n      this.unloaded = true;\n    });\n    this.socket.onOpen(() => {\n      if (this.isUnloaded()) {\n        window.location.reload();\n      }\n    });\n  }\n  // public\n  version() {\n    return \"1.2.0-dev\";\n  }\n  isProfileEnabled() {\n    return this.sessionStorage.getItem(PHX_LV_PROFILE) === \"true\";\n  }\n  isDebugEnabled() {\n    return this.sessionStorage.getItem(PHX_LV_DEBUG) === \"true\";\n  }\n  isDebugDisabled() {\n    return this.sessionStorage.getItem(PHX_LV_DEBUG) === \"false\";\n  }\n  enableDebug() {\n    this.sessionStorage.setItem(PHX_LV_DEBUG, \"true\");\n  }\n  enableProfiling() {\n    this.sessionStorage.setItem(PHX_LV_PROFILE, \"true\");\n  }\n  disableDebug() {\n    this.sessionStorage.setItem(PHX_LV_DEBUG, \"false\");\n  }\n  disableProfiling() {\n    this.sessionStorage.removeItem(PHX_LV_PROFILE);\n  }\n  enableLatencySim(upperBoundMs) {\n    this.enableDebug();\n    console.log(\n      \"latency simulator enabled for the duration of this browser session. Call disableLatencySim() to disable\"\n    );\n    this.sessionStorage.setItem(PHX_LV_LATENCY_SIM, upperBoundMs);\n  }\n  disableLatencySim() {\n    this.sessionStorage.removeItem(PHX_LV_LATENCY_SIM);\n  }\n  getLatencySim() {\n    const str = this.sessionStorage.getItem(PHX_LV_LATENCY_SIM);\n    return str ? parseInt(str) : null;\n  }\n  getSocket() {\n    return this.socket;\n  }\n  connect() {\n    if (window.location.hostname === \"localhost\" && !this.isDebugDisabled()) {\n      this.enableDebug();\n    }\n    const doConnect = () => {\n      this.resetReloadStatus();\n      if (this.joinRootViews()) {\n        this.bindTopLevelEvents();\n        this.socket.connect();\n      } else if (this.main) {\n        this.socket.connect();\n      } else {\n        this.bindTopLevelEvents({ dead: true });\n      }\n      this.joinDeadView();\n    };\n    if ([\"complete\", \"loaded\", \"interactive\"].indexOf(document.readyState) >= 0) {\n      doConnect();\n    } else {\n      document.addEventListener(\"DOMContentLoaded\", () => doConnect());\n    }\n  }\n  disconnect(callback) {\n    clearTimeout(this.reloadWithJitterTimer);\n    if (this.serverCloseRef) {\n      this.socket.off(this.serverCloseRef);\n      this.serverCloseRef = null;\n    }\n    this.socket.disconnect(callback);\n  }\n  replaceTransport(transport) {\n    clearTimeout(this.reloadWithJitterTimer);\n    this.socket.replaceTransport(transport);\n    this.connect();\n  }\n  /**\n   * @param {HTMLElement} el\n   * @param {import(\"./js_commands\").EncodedJS} encodedJS\n   * @param {string | null} [eventType]\n   */\n  execJS(el, encodedJS, eventType = null) {\n    const e = new CustomEvent(\"phx:exec\", { detail: { sourceElement: el } });\n    this.owner(el, (view) => js_default.exec(e, eventType, encodedJS, view, el));\n  }\n  /**\n   * Returns an object with methods to manipulate the DOM and execute JavaScript.\n   * The applied changes integrate with server DOM patching.\n   *\n   * @returns {import(\"./js_commands\").LiveSocketJSCommands}\n   */\n  js() {\n    return js_commands_default(this, \"js\");\n  }\n  // private\n  unload() {\n    if (this.unloaded) {\n      return;\n    }\n    if (this.main && this.isConnected()) {\n      this.log(this.main, \"socket\", () => [\"disconnect for page nav\"]);\n    }\n    this.unloaded = true;\n    this.destroyAllViews();\n    this.disconnect();\n  }\n  triggerDOM(kind, args) {\n    this.domCallbacks[kind](...args);\n  }\n  time(name, func) {\n    if (!this.isProfileEnabled() || !console.time) {\n      return func();\n    }\n    console.time(name);\n    const result = func();\n    console.timeEnd(name);\n    return result;\n  }\n  log(view, kind, msgCallback) {\n    if (this.viewLogger) {\n      const [msg, obj] = msgCallback();\n      this.viewLogger(view, kind, msg, obj);\n    } else if (this.isDebugEnabled()) {\n      const [msg, obj] = msgCallback();\n      debug(view, kind, msg, obj);\n    }\n  }\n  requestDOMUpdate(callback) {\n    this.transitions.after(callback);\n  }\n  asyncTransition(promise) {\n    this.transitions.addAsyncTransition(promise);\n  }\n  transition(time, onStart, onDone = function() {\n  }) {\n    this.transitions.addTransition(time, onStart, onDone);\n  }\n  onChannel(channel, event, cb) {\n    channel.on(event, (data) => {\n      const latency = this.getLatencySim();\n      if (!latency) {\n        cb(data);\n      } else {\n        setTimeout(() => cb(data), latency);\n      }\n    });\n  }\n  reloadWithJitter(view, log) {\n    clearTimeout(this.reloadWithJitterTimer);\n    this.disconnect();\n    const minMs = this.reloadJitterMin;\n    const maxMs = this.reloadJitterMax;\n    let afterMs = Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs;\n    const tries = browser_default.updateLocal(\n      this.localStorage,\n      window.location.pathname,\n      CONSECUTIVE_RELOADS,\n      0,\n      (count) => count + 1\n    );\n    if (tries >= this.maxReloads) {\n      afterMs = this.failsafeJitter;\n    }\n    this.reloadWithJitterTimer = setTimeout(() => {\n      if (view.isDestroyed() || view.isConnected()) {\n        return;\n      }\n      view.destroy();\n      log ? log() : this.log(view, \"join\", () => [\n        `encountered ${tries} consecutive reloads`\n      ]);\n      if (tries >= this.maxReloads) {\n        this.log(view, \"join\", () => [\n          `exceeded ${this.maxReloads} consecutive reloads. Entering failsafe mode`\n        ]);\n      }\n      if (this.hasPendingLink()) {\n        window.location = this.pendingLink;\n      } else {\n        window.location.reload();\n      }\n    }, afterMs);\n  }\n  getHookDefinition(name) {\n    if (!name) {\n      return;\n    }\n    return this.maybeInternalHook(name) || this.hooks[name] || this.maybeRuntimeHook(name);\n  }\n  maybeInternalHook(name) {\n    return name && name.startsWith(\"Phoenix.\") && hooks_default[name.split(\".\")[1]];\n  }\n  maybeRuntimeHook(name) {\n    const runtimeHook = document.querySelector(\n      `script[${PHX_RUNTIME_HOOK}=\"${CSS.escape(name)}\"]`\n    );\n    if (!runtimeHook) {\n      return;\n    }\n    let callbacks = window[`phx_hook_${name}`];\n    if (!callbacks || typeof callbacks !== \"function\") {\n      logError(\"a runtime hook must be a function\", runtimeHook);\n      return;\n    }\n    const hookDefiniton = callbacks();\n    if (hookDefiniton && (typeof hookDefiniton === \"object\" || typeof hookDefiniton === \"function\")) {\n      return hookDefiniton;\n    }\n    logError(\n      \"runtime hook must return an object with hook callbacks or an instance of ViewHook\",\n      runtimeHook\n    );\n  }\n  isUnloaded() {\n    return this.unloaded;\n  }\n  isConnected() {\n    return this.socket.isConnected();\n  }\n  getBindingPrefix() {\n    return this.bindingPrefix;\n  }\n  binding(kind) {\n    return `${this.getBindingPrefix()}${kind}`;\n  }\n  channel(topic, params) {\n    return this.socket.channel(topic, params);\n  }\n  joinDeadView() {\n    const body = document.body;\n    if (body && !this.isPhxView(body) && !this.isPhxView(document.firstElementChild)) {\n      const view = this.newRootView(body);\n      view.setHref(this.getHref());\n      view.joinDead();\n      if (!this.main) {\n        this.main = view;\n      }\n      window.requestAnimationFrame(() => {\n        view.execNewMounted();\n        this.maybeScroll(history.state?.scroll);\n      });\n    }\n  }\n  joinRootViews() {\n    let rootsFound = false;\n    dom_default.all(\n      document,\n      `${PHX_VIEW_SELECTOR}:not([${PHX_PARENT_ID}])`,\n      (rootEl) => {\n        if (!this.getRootById(rootEl.id)) {\n          const view = this.newRootView(rootEl);\n          if (!dom_default.isPhxSticky(rootEl)) {\n            view.setHref(this.getHref());\n          }\n          view.join();\n          if (rootEl.hasAttribute(PHX_MAIN)) {\n            this.main = view;\n          }\n        }\n        rootsFound = true;\n      }\n    );\n    return rootsFound;\n  }\n  redirect(to, flash, reloadToken) {\n    if (reloadToken) {\n      browser_default.setCookie(PHX_RELOAD_STATUS, reloadToken, 60);\n    }\n    this.unload();\n    browser_default.redirect(to, flash);\n  }\n  replaceMain(href, flash, callback = null, linkRef = this.setPendingLink(href)) {\n    const liveReferer = this.currentLocation.href;\n    this.outgoingMainEl = this.outgoingMainEl || this.main.el;\n    const stickies = dom_default.findPhxSticky(document) || [];\n    const removeEls = dom_default.all(\n      this.outgoingMainEl,\n      `[${this.binding(\"remove\")}]`\n    ).filter((el) => !dom_default.isChildOfAny(el, stickies));\n    const newMainEl = dom_default.cloneNode(this.outgoingMainEl, \"\");\n    this.main.showLoader(this.loaderTimeout);\n    this.main.destroy();\n    this.main = this.newRootView(newMainEl, flash, liveReferer);\n    this.main.setRedirect(href);\n    this.transitionRemoves(removeEls);\n    this.main.join((joinCount, onDone) => {\n      if (joinCount === 1 && this.commitPendingLink(linkRef)) {\n        this.requestDOMUpdate(() => {\n          removeEls.forEach((el) => el.remove());\n          stickies.forEach((el) => newMainEl.appendChild(el));\n          this.outgoingMainEl.replaceWith(newMainEl);\n          this.outgoingMainEl = null;\n          callback && callback(linkRef);\n          onDone();\n        });\n      }\n    });\n  }\n  transitionRemoves(elements, callback) {\n    const removeAttr = this.binding(\"remove\");\n    const silenceEvents = (e) => {\n      e.preventDefault();\n      e.stopImmediatePropagation();\n    };\n    elements.forEach((el) => {\n      for (const event of this.boundEventNames) {\n        el.addEventListener(event, silenceEvents, true);\n      }\n      this.execJS(el, el.getAttribute(removeAttr), \"remove\");\n    });\n    this.requestDOMUpdate(() => {\n      elements.forEach((el) => {\n        for (const event of this.boundEventNames) {\n          el.removeEventListener(event, silenceEvents, true);\n        }\n      });\n      callback && callback();\n    });\n  }\n  isPhxView(el) {\n    return el.getAttribute && el.getAttribute(PHX_SESSION) !== null;\n  }\n  newRootView(el, flash, liveReferer) {\n    const view = new View(el, this, null, flash, liveReferer);\n    this.roots[view.id] = view;\n    return view;\n  }\n  owner(childEl, callback) {\n    let view;\n    const viewEl = dom_default.closestViewEl(childEl);\n    if (viewEl) {\n      view = this.getViewByEl(viewEl);\n    } else {\n      if (!childEl.isConnected) {\n        return null;\n      }\n      view = this.main;\n    }\n    return view && callback ? callback(view) : view;\n  }\n  withinOwners(childEl, callback) {\n    this.owner(childEl, (view) => callback(view, childEl));\n  }\n  getViewByEl(el) {\n    const rootId = el.getAttribute(PHX_ROOT_ID);\n    return maybe(\n      this.getRootById(rootId),\n      (root) => root.getDescendentByEl(el)\n    );\n  }\n  getRootById(id) {\n    return this.roots[id];\n  }\n  destroyAllViews() {\n    for (const id in this.roots) {\n      this.roots[id].destroy();\n      delete this.roots[id];\n    }\n    this.main = null;\n  }\n  destroyViewByEl(el) {\n    const root = this.getRootById(el.getAttribute(PHX_ROOT_ID));\n    if (root && root.id === el.id) {\n      root.destroy();\n      delete this.roots[root.id];\n    } else if (root) {\n      root.destroyDescendent(el.id);\n    }\n  }\n  getActiveElement() {\n    return document.activeElement;\n  }\n  dropActiveElement(view) {\n    if (this.prevActive && view.ownsElement(this.prevActive)) {\n      this.prevActive = null;\n    }\n  }\n  restorePreviouslyActiveFocus() {\n    if (this.prevActive && this.prevActive !== document.body && this.prevActive instanceof HTMLElement) {\n      this.prevActive.focus();\n    }\n  }\n  blurActiveElement() {\n    this.prevActive = this.getActiveElement();\n    if (this.prevActive !== document.body && this.prevActive instanceof HTMLElement) {\n      this.prevActive.blur();\n    }\n  }\n  /**\n   * @param {{dead?: boolean}} [options={}]\n   */\n  bindTopLevelEvents({ dead } = {}) {\n    if (this.boundTopLevelEvents) {\n      return;\n    }\n    this.boundTopLevelEvents = true;\n    this.serverCloseRef = this.socket.onClose((event) => {\n      if (event && event.code === 1e3 && this.main) {\n        return this.reloadWithJitter(this.main);\n      }\n    });\n    document.body.addEventListener(\"click\", function() {\n    });\n    window.addEventListener(\n      \"pageshow\",\n      (e) => {\n        if (e.persisted) {\n          this.getSocket().disconnect();\n          this.withPageLoading({ to: window.location.href, kind: \"redirect\" });\n          window.location.reload();\n        }\n      },\n      true\n    );\n    if (!dead) {\n      this.bindNav();\n    }\n    this.bindClicks();\n    if (!dead) {\n      this.bindForms();\n    }\n    this.bind(\n      { keyup: \"keyup\", keydown: \"keydown\" },\n      (e, type, view, targetEl, phxEvent, _phxTarget) => {\n        const matchKey = targetEl.getAttribute(this.binding(PHX_KEY));\n        const pressedKey = e.key && e.key.toLowerCase();\n        if (matchKey && matchKey.toLowerCase() !== pressedKey) {\n          return;\n        }\n        const data = { key: e.key, ...this.eventMeta(type, e, targetEl) };\n        js_default.exec(e, type, phxEvent, view, targetEl, [\"push\", { data }]);\n      }\n    );\n    this.bind(\n      { blur: \"focusout\", focus: \"focusin\" },\n      (e, type, view, targetEl, phxEvent, phxTarget) => {\n        if (!phxTarget) {\n          const data = { key: e.key, ...this.eventMeta(type, e, targetEl) };\n          js_default.exec(e, type, phxEvent, view, targetEl, [\"push\", { data }]);\n        }\n      }\n    );\n    this.bind(\n      { blur: \"blur\", focus: \"focus\" },\n      (e, type, view, targetEl, phxEvent, phxTarget) => {\n        if (phxTarget === \"window\") {\n          const data = this.eventMeta(type, e, targetEl);\n          js_default.exec(e, type, phxEvent, view, targetEl, [\"push\", { data }]);\n        }\n      }\n    );\n    this.on(\"dragover\", (e) => e.preventDefault());\n    this.on(\"dragenter\", (e) => {\n      const dropzone = closestPhxBinding(\n        e.target,\n        this.binding(PHX_DROP_TARGET)\n      );\n      if (!dropzone || !(dropzone instanceof HTMLElement)) {\n        return;\n      }\n      if (eventContainsFiles(e)) {\n        this.js().addClass(dropzone, PHX_DROP_TARGET_ACTIVE_CLASS);\n      }\n    });\n    this.on(\"dragleave\", (e) => {\n      const dropzone = closestPhxBinding(\n        e.target,\n        this.binding(PHX_DROP_TARGET)\n      );\n      if (!dropzone || !(dropzone instanceof HTMLElement)) {\n        return;\n      }\n      const rect = dropzone.getBoundingClientRect();\n      if (e.clientX <= rect.left || e.clientX >= rect.right || e.clientY <= rect.top || e.clientY >= rect.bottom) {\n        this.js().removeClass(dropzone, PHX_DROP_TARGET_ACTIVE_CLASS);\n      }\n    });\n    this.on(\"drop\", (e) => {\n      e.preventDefault();\n      const dropzone = closestPhxBinding(\n        e.target,\n        this.binding(PHX_DROP_TARGET)\n      );\n      if (!dropzone || !(dropzone instanceof HTMLElement)) {\n        return;\n      }\n      this.js().removeClass(dropzone, PHX_DROP_TARGET_ACTIVE_CLASS);\n      const dropTargetId = dropzone.getAttribute(this.binding(PHX_DROP_TARGET));\n      const dropTarget = dropTargetId && document.getElementById(dropTargetId);\n      const files = Array.from(e.dataTransfer.files || []);\n      if (!dropTarget || !(dropTarget instanceof HTMLInputElement) || dropTarget.disabled || files.length === 0 || !(dropTarget.files instanceof FileList)) {\n        return;\n      }\n      LiveUploader.trackFiles(dropTarget, files, e.dataTransfer);\n      dropTarget.dispatchEvent(new Event(\"input\", { bubbles: true }));\n    });\n    this.on(PHX_TRACK_UPLOADS, (e) => {\n      const uploadTarget = e.target;\n      if (!dom_default.isUploadInput(uploadTarget)) {\n        return;\n      }\n      const files = Array.from(e.detail.files || []).filter(\n        (f) => f instanceof File || f instanceof Blob\n      );\n      LiveUploader.trackFiles(uploadTarget, files);\n      uploadTarget.dispatchEvent(new Event(\"input\", { bubbles: true }));\n    });\n  }\n  eventMeta(eventName, e, targetEl) {\n    const callback = this.metadataCallbacks[eventName];\n    return callback ? callback(e, targetEl) : {};\n  }\n  setPendingLink(href) {\n    this.linkRef++;\n    this.pendingLink = href;\n    this.resetReloadStatus();\n    return this.linkRef;\n  }\n  // anytime we are navigating or connecting, drop reload cookie in case\n  // we issue the cookie but the next request was interrupted and the server never dropped it\n  resetReloadStatus() {\n    browser_default.deleteCookie(PHX_RELOAD_STATUS);\n  }\n  commitPendingLink(linkRef) {\n    if (this.linkRef !== linkRef) {\n      return false;\n    } else {\n      this.href = this.pendingLink;\n      this.pendingLink = null;\n      return true;\n    }\n  }\n  getHref() {\n    return this.href;\n  }\n  hasPendingLink() {\n    return !!this.pendingLink;\n  }\n  bind(events, callback) {\n    for (const event in events) {\n      const browserEventName = events[event];\n      this.on(browserEventName, (e) => {\n        const binding = this.binding(event);\n        const windowBinding = this.binding(`window-${event}`);\n        const targetPhxEvent = e.target.getAttribute && e.target.getAttribute(binding);\n        if (targetPhxEvent) {\n          this.debounce(e.target, e, browserEventName, () => {\n            this.withinOwners(e.target, (view) => {\n              callback(e, event, view, e.target, targetPhxEvent, null);\n            });\n          });\n        } else {\n          dom_default.all(document, `[${windowBinding}]`, (el) => {\n            const phxEvent = el.getAttribute(windowBinding);\n            this.debounce(el, e, browserEventName, () => {\n              this.withinOwners(el, (view) => {\n                callback(e, event, view, el, phxEvent, \"window\");\n              });\n            });\n          });\n        }\n      });\n    }\n  }\n  bindClicks() {\n    this.on(\"mousedown\", (e) => this.clickStartedAtTarget = e.target);\n    this.bindClick(\"click\", \"click\");\n  }\n  bindClick(eventName, bindingName) {\n    const click = this.binding(bindingName);\n    window.addEventListener(\n      eventName,\n      (e) => {\n        let target = null;\n        if (e.detail === 0)\n          this.clickStartedAtTarget = e.target;\n        const clickStartedAtTarget = this.clickStartedAtTarget || e.target;\n        target = closestPhxBinding(e.target, click);\n        this.dispatchClickAway(e, clickStartedAtTarget);\n        this.clickStartedAtTarget = null;\n        const phxEvent = target && target.getAttribute(click);\n        if (!phxEvent) {\n          if (dom_default.isNewPageClick(e, window.location)) {\n            this.unload();\n          }\n          return;\n        }\n        if (target.getAttribute(\"href\") === \"#\") {\n          e.preventDefault();\n        }\n        if (target.hasAttribute(PHX_REF_SRC)) {\n          return;\n        }\n        this.debounce(target, e, \"click\", () => {\n          this.withinOwners(target, (view) => {\n            js_default.exec(e, \"click\", phxEvent, view, target, [\n              \"push\",\n              { data: this.eventMeta(\"click\", e, target) }\n            ]);\n          });\n        });\n      },\n      false\n    );\n  }\n  dispatchClickAway(e, clickStartedAt) {\n    const phxClickAway = this.binding(\"click-away\");\n    const portal = clickStartedAt.closest(`[${PHX_TELEPORTED_SRC}]`);\n    const portalStartedAt = portal && dom_default.byId(portal.getAttribute(PHX_TELEPORTED_SRC));\n    dom_default.all(document, `[${phxClickAway}]`, (el) => {\n      let startedAt = clickStartedAt;\n      if (portal && !portal.contains(el)) {\n        startedAt = portalStartedAt;\n      }\n      if (!(el.isSameNode(startedAt) || el.contains(startedAt) || // When clicking a link with custom method,\n      // phoenix_html triggers a click on a submit button\n      // of a hidden form appended to the body. For such cases\n      // where the clicked target is hidden, we skip click-away.\n      //\n      // Also, when we have a portal, we don't want to check the visibility\n      // of the portal source, as it's a <template> that is always not visible.\n      // Instead, check the visibility of the original click target.\n      !js_default.isVisible(clickStartedAt))) {\n        this.withinOwners(el, (view) => {\n          const phxEvent = el.getAttribute(phxClickAway);\n          if (js_default.isVisible(el) && js_default.isInViewport(el)) {\n            js_default.exec(e, \"click\", phxEvent, view, el, [\n              \"push\",\n              { data: this.eventMeta(\"click\", e, e.target) }\n            ]);\n          }\n        });\n      }\n    });\n  }\n  bindNav() {\n    if (!browser_default.canPushState()) {\n      return;\n    }\n    if (history.scrollRestoration) {\n      history.scrollRestoration = \"manual\";\n    }\n    let scrollTimer = null;\n    window.addEventListener(\"scroll\", (_e) => {\n      clearTimeout(scrollTimer);\n      scrollTimer = setTimeout(() => {\n        browser_default.updateCurrentState(\n          (state) => Object.assign(state, { scroll: window.scrollY })\n        );\n      }, 100);\n    });\n    window.addEventListener(\n      \"popstate\",\n      (event) => {\n        if (!this.registerNewLocation(window.location)) {\n          return;\n        }\n        const { type, backType, id, scroll, position } = event.state || {};\n        const href = window.location.href;\n        const isForward = position > this.currentHistoryPosition;\n        const navType = isForward ? type : backType || type;\n        this.currentHistoryPosition = position || 0;\n        this.sessionStorage.setItem(\n          PHX_LV_HISTORY_POSITION,\n          this.currentHistoryPosition.toString()\n        );\n        dom_default.dispatchEvent(window, \"phx:navigate\", {\n          detail: {\n            href,\n            patch: navType === \"patch\",\n            pop: true,\n            direction: isForward ? \"forward\" : \"backward\"\n          }\n        });\n        this.requestDOMUpdate(() => {\n          const callback = () => {\n            this.maybeScroll(scroll);\n          };\n          if (this.main.isConnected() && navType === \"patch\" && id === this.main.id) {\n            this.main.pushLinkPatch(event, href, null, callback);\n          } else {\n            this.replaceMain(href, null, callback);\n          }\n        });\n      },\n      false\n    );\n    window.addEventListener(\n      \"click\",\n      (e) => {\n        const target = closestPhxBinding(e.target, PHX_LIVE_LINK);\n        const type = target && target.getAttribute(PHX_LIVE_LINK);\n        if (!type || !this.isConnected() || !this.main || dom_default.wantsNewTab(e)) {\n          return;\n        }\n        const href = target.href instanceof SVGAnimatedString ? target.href.baseVal : target.href;\n        const linkState = target.getAttribute(PHX_LINK_STATE);\n        e.preventDefault();\n        e.stopImmediatePropagation();\n        if (this.pendingLink === href) {\n          return;\n        }\n        this.requestDOMUpdate(() => {\n          if (type === \"patch\") {\n            this.pushHistoryPatch(e, href, linkState, target);\n          } else if (type === \"redirect\") {\n            this.historyRedirect(e, href, linkState, null, target);\n          } else {\n            throw new Error(\n              `expected ${PHX_LIVE_LINK} to be \"patch\" or \"redirect\", got: ${type}`\n            );\n          }\n          const phxClick = target.getAttribute(this.binding(\"click\"));\n          if (phxClick) {\n            this.requestDOMUpdate(() => this.execJS(target, phxClick, \"click\"));\n          }\n        });\n      },\n      false\n    );\n  }\n  maybeScroll(scroll) {\n    if (typeof scroll === \"number\") {\n      requestAnimationFrame(() => {\n        window.scrollTo(0, scroll);\n      });\n    }\n  }\n  dispatchEvent(event, payload = {}) {\n    dom_default.dispatchEvent(window, `phx:${event}`, { detail: payload });\n  }\n  dispatchEvents(events) {\n    events.forEach(([event, payload]) => this.dispatchEvent(event, payload));\n  }\n  withPageLoading(info, callback) {\n    dom_default.dispatchEvent(window, \"phx:page-loading-start\", { detail: info });\n    const done = () => dom_default.dispatchEvent(window, \"phx:page-loading-stop\", { detail: info });\n    return callback ? callback(done) : done;\n  }\n  pushHistoryPatch(e, href, linkState, targetEl) {\n    if (!this.isConnected() || !this.main.isMain()) {\n      return browser_default.redirect(href);\n    }\n    this.withPageLoading({ to: href, kind: \"patch\" }, (done) => {\n      this.main.pushLinkPatch(e, href, targetEl, (linkRef) => {\n        this.historyPatch(href, linkState, linkRef);\n        done();\n      });\n    });\n  }\n  historyPatch(href, linkState, linkRef = this.setPendingLink(href)) {\n    if (!this.commitPendingLink(linkRef)) {\n      return;\n    }\n    this.currentHistoryPosition++;\n    this.sessionStorage.setItem(\n      PHX_LV_HISTORY_POSITION,\n      this.currentHistoryPosition.toString()\n    );\n    browser_default.updateCurrentState((state) => ({ ...state, backType: \"patch\" }));\n    browser_default.pushState(\n      linkState,\n      {\n        type: \"patch\",\n        id: this.main.id,\n        position: this.currentHistoryPosition\n      },\n      href\n    );\n    dom_default.dispatchEvent(window, \"phx:navigate\", {\n      detail: { patch: true, href, pop: false, direction: \"forward\" }\n    });\n    this.registerNewLocation(window.location);\n  }\n  historyRedirect(e, href, linkState, flash, targetEl) {\n    const clickLoading = targetEl && e.isTrusted && e.type !== \"popstate\";\n    if (clickLoading) {\n      targetEl.classList.add(\"phx-click-loading\");\n    }\n    if (!this.isConnected() || !this.main.isMain()) {\n      return browser_default.redirect(href, flash);\n    }\n    if (/^\\/$|^\\/[^\\/]+.*$/.test(href)) {\n      const { protocol, host } = window.location;\n      href = `${protocol}//${host}${href}`;\n    }\n    const scroll = window.scrollY;\n    this.withPageLoading({ to: href, kind: \"redirect\" }, (done) => {\n      this.replaceMain(href, flash, (linkRef) => {\n        if (linkRef === this.linkRef) {\n          this.currentHistoryPosition++;\n          this.sessionStorage.setItem(\n            PHX_LV_HISTORY_POSITION,\n            this.currentHistoryPosition.toString()\n          );\n          browser_default.updateCurrentState((state) => ({\n            ...state,\n            backType: \"redirect\"\n          }));\n          browser_default.pushState(\n            linkState,\n            {\n              type: \"redirect\",\n              id: this.main.id,\n              scroll,\n              position: this.currentHistoryPosition\n            },\n            href\n          );\n          dom_default.dispatchEvent(window, \"phx:navigate\", {\n            detail: { href, patch: false, pop: false, direction: \"forward\" }\n          });\n          this.registerNewLocation(window.location);\n        }\n        if (clickLoading) {\n          targetEl.classList.remove(\"phx-click-loading\");\n        }\n        done();\n      });\n    });\n  }\n  registerNewLocation(newLocation) {\n    const { pathname, search } = this.currentLocation;\n    if (pathname + search === newLocation.pathname + newLocation.search) {\n      return false;\n    } else {\n      this.currentLocation = clone(newLocation);\n      return true;\n    }\n  }\n  bindForms() {\n    let iterations = 0;\n    let externalFormSubmitted = false;\n    this.on(\"submit\", (e) => {\n      const phxSubmit = e.target.getAttribute(this.binding(\"submit\"));\n      const phxChange = e.target.getAttribute(this.binding(\"change\"));\n      if (!externalFormSubmitted && phxChange && !phxSubmit) {\n        externalFormSubmitted = true;\n        e.preventDefault();\n        this.withinOwners(e.target, (view) => {\n          view.disableForm(e.target);\n          window.requestAnimationFrame(() => {\n            if (dom_default.isUnloadableFormSubmit(e)) {\n              this.unload();\n            }\n            e.target.submit();\n          });\n        });\n      }\n    });\n    this.on(\"submit\", (e) => {\n      const phxEvent = e.target.getAttribute(this.binding(\"submit\"));\n      if (!phxEvent) {\n        if (dom_default.isUnloadableFormSubmit(e)) {\n          this.unload();\n        }\n        return;\n      }\n      e.preventDefault();\n      e.target.disabled = true;\n      this.withinOwners(e.target, (view) => {\n        js_default.exec(e, \"submit\", phxEvent, view, e.target, [\n          \"push\",\n          { submitter: e.submitter }\n        ]);\n      });\n    });\n    for (const type of [\"change\", \"input\"]) {\n      this.on(type, (e) => {\n        if (e instanceof CustomEvent && (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement || e.target instanceof HTMLTextAreaElement) && e.target.form === void 0) {\n          if (e.detail && e.detail.dispatcher) {\n            throw new Error(\n              `dispatching a custom ${type} event is only supported on input elements inside a form`\n            );\n          }\n          return;\n        }\n        const phxChange = this.binding(\"change\");\n        const input = e.target;\n        if (this.blockPhxChangeWhileComposing && e.isComposing) {\n          const key = `composition-listener-${type}`;\n          if (!dom_default.private(input, key)) {\n            dom_default.putPrivate(input, key, true);\n            input.addEventListener(\n              \"compositionend\",\n              () => {\n                input.dispatchEvent(new Event(type, { bubbles: true }));\n                dom_default.deletePrivate(input, key);\n              },\n              { once: true }\n            );\n          }\n          return;\n        }\n        const inputEvent = input.getAttribute(phxChange);\n        const formEvent = input.form && input.form.getAttribute(phxChange);\n        const phxEvent = inputEvent || formEvent;\n        if (!phxEvent) {\n          return;\n        }\n        if (input.type === \"number\" && input.validity && input.validity.badInput) {\n          return;\n        }\n        const dispatcher = inputEvent ? input : input.form;\n        const currentIterations = iterations;\n        iterations++;\n        const { at, type: lastType } = dom_default.private(input, \"prev-iteration\") || {};\n        if (at === currentIterations - 1 && type === \"change\" && lastType === \"input\") {\n          return;\n        }\n        dom_default.putPrivate(input, \"prev-iteration\", {\n          at: currentIterations,\n          type\n        });\n        this.debounce(input, e, type, () => {\n          this.withinOwners(dispatcher, (view) => {\n            dom_default.putPrivate(input, PHX_HAS_FOCUSED, true);\n            js_default.exec(e, \"change\", phxEvent, view, input, [\n              \"push\",\n              { _target: e.target.name, dispatcher }\n            ]);\n          });\n        });\n      });\n    }\n    this.on(\"reset\", (e) => {\n      const form = e.target;\n      dom_default.resetForm(form);\n      const input = Array.from(form.elements).find((el) => el.type === \"reset\");\n      if (input) {\n        window.requestAnimationFrame(() => {\n          input.dispatchEvent(\n            new Event(\"input\", { bubbles: true, cancelable: false })\n          );\n        });\n      }\n    });\n  }\n  debounce(el, event, eventType, callback) {\n    if (eventType === \"blur\" || eventType === \"focusout\") {\n      return callback();\n    }\n    const phxDebounce = this.binding(PHX_DEBOUNCE);\n    const phxThrottle = this.binding(PHX_THROTTLE);\n    const defaultDebounce = this.defaults.debounce.toString();\n    const defaultThrottle = this.defaults.throttle.toString();\n    this.withinOwners(el, (view) => {\n      const asyncFilter = () => !view.isDestroyed() && document.body.contains(el);\n      dom_default.debounce(\n        el,\n        event,\n        phxDebounce,\n        defaultDebounce,\n        phxThrottle,\n        defaultThrottle,\n        asyncFilter,\n        () => {\n          callback();\n        }\n      );\n    });\n  }\n  silenceEvents(callback) {\n    this.silenced = true;\n    callback();\n    this.silenced = false;\n  }\n  on(event, callback) {\n    this.boundEventNames.add(event);\n    window.addEventListener(event, (e) => {\n      if (!this.silenced) {\n        callback(e);\n      }\n    });\n  }\n  jsQuerySelectorAll(sourceEl, query, defaultQuery) {\n    const all = this.domCallbacks.jsQuerySelectorAll;\n    return all ? all(sourceEl, query, defaultQuery) : defaultQuery();\n  }\n};\nvar TransitionSet = class {\n  constructor() {\n    this.transitions = /* @__PURE__ */ new Set();\n    this.promises = /* @__PURE__ */ new Set();\n    this.pendingOps = [];\n  }\n  reset() {\n    this.transitions.forEach((timer) => {\n      clearTimeout(timer);\n      this.transitions.delete(timer);\n    });\n    this.promises.clear();\n    this.flushPendingOps();\n  }\n  after(callback) {\n    if (this.size() === 0) {\n      callback();\n    } else {\n      this.pushPendingOp(callback);\n    }\n  }\n  addTransition(time, onStart, onDone) {\n    onStart();\n    const timer = setTimeout(() => {\n      this.transitions.delete(timer);\n      onDone();\n      this.flushPendingOps();\n    }, time);\n    this.transitions.add(timer);\n  }\n  addAsyncTransition(promise) {\n    this.promises.add(promise);\n    promise.then(() => {\n      this.promises.delete(promise);\n      this.flushPendingOps();\n    });\n  }\n  pushPendingOp(op) {\n    this.pendingOps.push(op);\n  }\n  size() {\n    return this.transitions.size + this.promises.size;\n  }\n  flushPendingOps() {\n    if (this.size() > 0) {\n      return;\n    }\n    const op = this.pendingOps.shift();\n    if (op) {\n      op();\n      this.flushPendingOps();\n    }\n  }\n};\n\n// js/phoenix_live_view/index.ts\nvar LiveSocket2 = LiveSocket;\nfunction createHook(el, callbacks) {\n  let existingHook = dom_default.getCustomElHook(el);\n  if (existingHook) {\n    return existingHook;\n  }\n  if (!el.hasAttribute(\"id\")) {\n    logError(\n      \"Elements passed to createHook need to have a unique id attribute\",\n      el\n    );\n  }\n  let hook = new ViewHook(View.closestView(el), el, callbacks);\n  dom_default.putCustomElHook(el, hook);\n  return hook;\n}\nexport {\n  LiveSocket2 as LiveSocket,\n  ViewHook,\n  createHook,\n  isUsedInput\n};\n//# sourceMappingURL=phoenix_live_view.esm.js.map\n"
  },
  {
    "path": "priv/static/phoenix_live_view.js",
    "content": "var LiveView = (() => {\n  var __defProp = Object.defineProperty;\n  var __defProps = Object.defineProperties;\n  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;\n  var __getOwnPropDescs = Object.getOwnPropertyDescriptors;\n  var __getOwnPropNames = Object.getOwnPropertyNames;\n  var __getOwnPropSymbols = Object.getOwnPropertySymbols;\n  var __hasOwnProp = Object.prototype.hasOwnProperty;\n  var __propIsEnum = Object.prototype.propertyIsEnumerable;\n  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;\n  var __spreadValues = (a, b) => {\n    for (var prop in b || (b = {}))\n      if (__hasOwnProp.call(b, prop))\n        __defNormalProp(a, prop, b[prop]);\n    if (__getOwnPropSymbols)\n      for (var prop of __getOwnPropSymbols(b)) {\n        if (__propIsEnum.call(b, prop))\n          __defNormalProp(a, prop, b[prop]);\n      }\n    return a;\n  };\n  var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));\n  var __export = (target, all) => {\n    for (var name in all)\n      __defProp(target, name, { get: all[name], enumerable: true });\n  };\n  var __copyProps = (to, from, except, desc) => {\n    if (from && typeof from === \"object\" || typeof from === \"function\") {\n      for (let key of __getOwnPropNames(from))\n        if (!__hasOwnProp.call(to, key) && key !== except)\n          __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });\n    }\n    return to;\n  };\n  var __toCommonJS = (mod) => __copyProps(__defProp({}, \"__esModule\", { value: true }), mod);\n\n  // js/phoenix_live_view/index.ts\n  var phoenix_live_view_exports = {};\n  __export(phoenix_live_view_exports, {\n    LiveSocket: () => LiveSocket2,\n    ViewHook: () => ViewHook,\n    createHook: () => createHook,\n    isUsedInput: () => isUsedInput\n  });\n\n  // js/phoenix_live_view/constants.js\n  var CONSECUTIVE_RELOADS = \"consecutive-reloads\";\n  var MAX_RELOADS = 10;\n  var RELOAD_JITTER_MIN = 5e3;\n  var RELOAD_JITTER_MAX = 1e4;\n  var FAILSAFE_JITTER = 3e4;\n  var PHX_EVENT_CLASSES = [\n    \"phx-click-loading\",\n    \"phx-change-loading\",\n    \"phx-submit-loading\",\n    \"phx-keydown-loading\",\n    \"phx-keyup-loading\",\n    \"phx-blur-loading\",\n    \"phx-focus-loading\",\n    \"phx-hook-loading\"\n  ];\n  var PHX_DROP_TARGET_ACTIVE_CLASS = \"phx-drop-target-active\";\n  var PHX_COMPONENT = \"data-phx-component\";\n  var PHX_VIEW_REF = \"data-phx-view\";\n  var PHX_LIVE_LINK = \"data-phx-link\";\n  var PHX_TRACK_STATIC = \"track-static\";\n  var PHX_LINK_STATE = \"data-phx-link-state\";\n  var PHX_REF_LOADING = \"data-phx-ref-loading\";\n  var PHX_REF_SRC = \"data-phx-ref-src\";\n  var PHX_REF_LOCK = \"data-phx-ref-lock\";\n  var PHX_PENDING_REFS = \"phx-pending-refs\";\n  var PHX_TRACK_UPLOADS = \"track-uploads\";\n  var PHX_UPLOAD_REF = \"data-phx-upload-ref\";\n  var PHX_PREFLIGHTED_REFS = \"data-phx-preflighted-refs\";\n  var PHX_DONE_REFS = \"data-phx-done-refs\";\n  var PHX_DROP_TARGET = \"drop-target\";\n  var PHX_ACTIVE_ENTRY_REFS = \"data-phx-active-refs\";\n  var PHX_LIVE_FILE_UPDATED = \"phx:live-file:updated\";\n  var PHX_SKIP = \"data-phx-skip\";\n  var PHX_MAGIC_ID = \"data-phx-id\";\n  var PHX_PRUNE = \"data-phx-prune\";\n  var PHX_CONNECTED_CLASS = \"phx-connected\";\n  var PHX_LOADING_CLASS = \"phx-loading\";\n  var PHX_ERROR_CLASS = \"phx-error\";\n  var PHX_CLIENT_ERROR_CLASS = \"phx-client-error\";\n  var PHX_SERVER_ERROR_CLASS = \"phx-server-error\";\n  var PHX_PARENT_ID = \"data-phx-parent-id\";\n  var PHX_MAIN = \"data-phx-main\";\n  var PHX_ROOT_ID = \"data-phx-root-id\";\n  var PHX_VIEWPORT_TOP = \"viewport-top\";\n  var PHX_VIEWPORT_BOTTOM = \"viewport-bottom\";\n  var PHX_VIEWPORT_OVERRUN_TARGET = \"viewport-overrun-target\";\n  var PHX_TRIGGER_ACTION = \"trigger-action\";\n  var PHX_HAS_FOCUSED = \"phx-has-focused\";\n  var FOCUSABLE_INPUTS = [\n    \"text\",\n    \"textarea\",\n    \"number\",\n    \"email\",\n    \"password\",\n    \"search\",\n    \"tel\",\n    \"url\",\n    \"date\",\n    \"time\",\n    \"datetime-local\",\n    \"color\",\n    \"range\"\n  ];\n  var CHECKABLE_INPUTS = [\"checkbox\", \"radio\"];\n  var PHX_HAS_SUBMITTED = \"phx-has-submitted\";\n  var PHX_SESSION = \"data-phx-session\";\n  var PHX_VIEW_SELECTOR = `[${PHX_SESSION}]`;\n  var PHX_STICKY = \"data-phx-sticky\";\n  var PHX_STATIC = \"data-phx-static\";\n  var PHX_READONLY = \"data-phx-readonly\";\n  var PHX_DISABLED = \"data-phx-disabled\";\n  var PHX_DISABLE_WITH = \"disable-with\";\n  var PHX_DISABLE_WITH_RESTORE = \"data-phx-disable-with-restore\";\n  var PHX_HOOK = \"hook\";\n  var PHX_DEBOUNCE = \"debounce\";\n  var PHX_THROTTLE = \"throttle\";\n  var PHX_UPDATE = \"update\";\n  var PHX_STREAM = \"stream\";\n  var PHX_STREAM_REF = \"data-phx-stream\";\n  var PHX_PORTAL = \"data-phx-portal\";\n  var PHX_TELEPORTED_REF = \"data-phx-teleported\";\n  var PHX_TELEPORTED_SRC = \"data-phx-teleported-src\";\n  var PHX_RUNTIME_HOOK = \"data-phx-runtime-hook\";\n  var PHX_LV_PID = \"data-phx-pid\";\n  var PHX_KEY = \"key\";\n  var PHX_PRIVATE = \"phxPrivate\";\n  var PHX_AUTO_RECOVER = \"auto-recover\";\n  var PHX_NO_UNUSED_FIELD = \"no-unused-field\";\n  var PHX_LV_DEBUG = \"phx:live-socket:debug\";\n  var PHX_LV_PROFILE = \"phx:live-socket:profiling\";\n  var PHX_LV_LATENCY_SIM = \"phx:live-socket:latency-sim\";\n  var PHX_LV_HISTORY_POSITION = \"phx:nav-history-position\";\n  var PHX_PROGRESS = \"progress\";\n  var PHX_MOUNTED = \"mounted\";\n  var PHX_RELOAD_STATUS = \"__phoenix_reload_status__\";\n  var LOADER_TIMEOUT = 1;\n  var MAX_CHILD_JOIN_ATTEMPTS = 3;\n  var BEFORE_UNLOAD_LOADER_TIMEOUT = 200;\n  var DISCONNECTED_TIMEOUT = 500;\n  var BINDING_PREFIX = \"phx-\";\n  var PUSH_TIMEOUT = 3e4;\n  var DEBOUNCE_TRIGGER = \"debounce-trigger\";\n  var THROTTLED = \"throttled\";\n  var DEBOUNCE_PREV_KEY = \"debounce-prev-key\";\n  var DEFAULTS = {\n    debounce: 300,\n    throttle: 300\n  };\n  var PHX_PENDING_ATTRS = [PHX_REF_LOADING, PHX_REF_SRC, PHX_REF_LOCK];\n  var STATIC = \"s\";\n  var ROOT = \"r\";\n  var COMPONENTS = \"c\";\n  var KEYED = \"k\";\n  var KEYED_COUNT = \"kc\";\n  var EVENTS = \"e\";\n  var REPLY = \"r\";\n  var TITLE = \"t\";\n  var TEMPLATES = \"p\";\n  var STREAM = \"stream\";\n\n  // js/phoenix_live_view/entry_uploader.js\n  var EntryUploader = class {\n    constructor(entry, config, liveSocket) {\n      const { chunk_size, chunk_timeout } = config;\n      this.liveSocket = liveSocket;\n      this.entry = entry;\n      this.offset = 0;\n      this.chunkSize = chunk_size;\n      this.chunkTimeout = chunk_timeout;\n      this.chunkTimer = null;\n      this.errored = false;\n      this.uploadChannel = liveSocket.channel(`lvu:${entry.ref}`, {\n        token: entry.metadata()\n      });\n    }\n    error(reason) {\n      if (this.errored) {\n        return;\n      }\n      this.uploadChannel.leave();\n      this.errored = true;\n      clearTimeout(this.chunkTimer);\n      this.entry.error(reason);\n    }\n    upload() {\n      this.uploadChannel.onError((reason) => this.error(reason));\n      this.uploadChannel.join().receive(\"ok\", (_data) => this.readNextChunk()).receive(\"error\", (reason) => this.error(reason));\n    }\n    isDone() {\n      return this.offset >= this.entry.file.size;\n    }\n    readNextChunk() {\n      const reader = new window.FileReader();\n      const blob = this.entry.file.slice(\n        this.offset,\n        this.chunkSize + this.offset\n      );\n      reader.onload = (e) => {\n        if (e.target.error === null) {\n          this.offset += /** @type {ArrayBuffer} */\n          e.target.result.byteLength;\n          this.pushChunk(\n            /** @type {ArrayBuffer} */\n            e.target.result\n          );\n        } else {\n          return logError(\"Read error: \" + e.target.error);\n        }\n      };\n      reader.readAsArrayBuffer(blob);\n    }\n    pushChunk(chunk) {\n      if (!this.uploadChannel.isJoined()) {\n        return;\n      }\n      this.uploadChannel.push(\"chunk\", chunk, this.chunkTimeout).receive(\"ok\", () => {\n        this.entry.progress(this.offset / this.entry.file.size * 100);\n        if (!this.isDone()) {\n          this.chunkTimer = setTimeout(\n            () => this.readNextChunk(),\n            this.liveSocket.getLatencySim() || 0\n          );\n        }\n      }).receive(\"error\", ({ reason }) => this.error(reason));\n    }\n  };\n\n  // js/phoenix_live_view/utils.js\n  var logError = (msg, obj) => console.error && console.error(msg, obj);\n  var isCid = (cid) => {\n    const type = typeof cid;\n    return type === \"number\" || type === \"string\" && /^(0|[1-9]\\d*)$/.test(cid);\n  };\n  function detectDuplicateIds() {\n    const ids = /* @__PURE__ */ new Set();\n    const elems = document.querySelectorAll(\"*[id]\");\n    for (let i = 0, len = elems.length; i < len; i++) {\n      if (ids.has(elems[i].id)) {\n        console.error(\n          `Multiple IDs detected: ${elems[i].id}. Ensure unique element ids.`\n        );\n      } else {\n        ids.add(elems[i].id);\n      }\n    }\n  }\n  function detectInvalidStreamInserts(inserts) {\n    const errors = /* @__PURE__ */ new Set();\n    Object.keys(inserts).forEach((id) => {\n      const streamEl = document.getElementById(id);\n      if (streamEl && streamEl.parentElement && streamEl.parentElement.getAttribute(\"phx-update\") !== \"stream\") {\n        errors.add(\n          `The stream container with id \"${streamEl.parentElement.id}\" is missing the phx-update=\"stream\" attribute. Ensure it is set for streams to work properly.`\n        );\n      }\n    });\n    errors.forEach((error) => console.error(error));\n  }\n  var debug = (view, kind, msg, obj) => {\n    if (view.liveSocket.isDebugEnabled()) {\n      console.log(`${view.id} ${kind}: ${msg} - `, obj);\n    }\n  };\n  var closure = (val) => typeof val === \"function\" ? val : function() {\n    return val;\n  };\n  var clone = (obj) => {\n    return JSON.parse(JSON.stringify(obj));\n  };\n  var closestPhxBinding = (el, binding, borderEl) => {\n    do {\n      if (el.matches(`[${binding}]`) && !el.disabled) {\n        return el;\n      }\n      el = el.parentElement || el.parentNode;\n    } while (el !== null && el.nodeType === 1 && !(borderEl && borderEl.isSameNode(el) || el.matches(PHX_VIEW_SELECTOR)));\n    return null;\n  };\n  var isObject = (obj) => {\n    return obj !== null && typeof obj === \"object\" && !(obj instanceof Array);\n  };\n  var isEqualObj = (obj1, obj2) => JSON.stringify(obj1) === JSON.stringify(obj2);\n  var isEmpty = (obj) => {\n    for (const x in obj) {\n      return false;\n    }\n    return true;\n  };\n  var maybe = (el, callback) => el && callback(el);\n  var channelUploader = function(entries, onError, resp, liveSocket) {\n    entries.forEach((entry) => {\n      const entryUploader = new EntryUploader(entry, resp.config, liveSocket);\n      entryUploader.upload();\n    });\n  };\n  var eventContainsFiles = (e) => {\n    if (e.dataTransfer.types) {\n      for (let i = 0; i < e.dataTransfer.types.length; i++) {\n        if (e.dataTransfer.types[i] === \"Files\") {\n          return true;\n        }\n      }\n    }\n    return false;\n  };\n\n  // js/phoenix_live_view/browser.js\n  var Browser = {\n    canPushState() {\n      return typeof history.pushState !== \"undefined\";\n    },\n    dropLocal(localStorage, namespace, subkey) {\n      return localStorage.removeItem(this.localKey(namespace, subkey));\n    },\n    updateLocal(localStorage, namespace, subkey, initial, func) {\n      const current = this.getLocal(localStorage, namespace, subkey);\n      const key = this.localKey(namespace, subkey);\n      const newVal = current === null ? initial : func(current);\n      localStorage.setItem(key, JSON.stringify(newVal));\n      return newVal;\n    },\n    getLocal(localStorage, namespace, subkey) {\n      return JSON.parse(localStorage.getItem(this.localKey(namespace, subkey)));\n    },\n    updateCurrentState(callback) {\n      if (!this.canPushState()) {\n        return;\n      }\n      history.replaceState(\n        callback(history.state || {}),\n        \"\",\n        window.location.href\n      );\n    },\n    pushState(kind, meta, to) {\n      if (this.canPushState()) {\n        if (to !== window.location.href) {\n          if (meta.type == \"redirect\" && meta.scroll) {\n            const currentState = history.state || {};\n            currentState.scroll = meta.scroll;\n            history.replaceState(currentState, \"\", window.location.href);\n          }\n          delete meta.scroll;\n          history[kind + \"State\"](meta, \"\", to || null);\n          window.requestAnimationFrame(() => {\n            const hashEl = this.getHashTargetEl(window.location.hash);\n            if (hashEl) {\n              hashEl.scrollIntoView();\n            } else if (meta.type === \"redirect\") {\n              window.scroll(0, 0);\n            }\n          });\n        }\n      } else {\n        this.redirect(to);\n      }\n    },\n    setCookie(name, value, maxAgeSeconds) {\n      const expires = typeof maxAgeSeconds === \"number\" ? ` max-age=${maxAgeSeconds};` : \"\";\n      document.cookie = `${name}=${value};${expires} path=/`;\n    },\n    getCookie(name) {\n      return document.cookie.replace(\n        new RegExp(`(?:(?:^|.*;s*)${name}s*=s*([^;]*).*$)|^.*$`),\n        \"$1\"\n      );\n    },\n    deleteCookie(name) {\n      document.cookie = `${name}=; max-age=-1; path=/`;\n    },\n    redirect(toURL, flash, navigate = (url) => {\n      window.location.href = url;\n    }) {\n      if (flash) {\n        this.setCookie(\"__phoenix_flash__\", flash, 60);\n      }\n      navigate(toURL);\n    },\n    localKey(namespace, subkey) {\n      return `${namespace}-${subkey}`;\n    },\n    getHashTargetEl(maybeHash) {\n      const hash = maybeHash.toString().substring(1);\n      if (hash === \"\") {\n        return;\n      }\n      return document.getElementById(hash) || document.querySelector(`a[name=\"${hash}\"]`);\n    }\n  };\n  var browser_default = Browser;\n\n  // js/phoenix_live_view/dom.js\n  var DOM = {\n    byId(id) {\n      return document.getElementById(id) || logError(`no id found for ${id}`);\n    },\n    removeClass(el, className) {\n      el.classList.remove(className);\n      if (el.classList.length === 0) {\n        el.removeAttribute(\"class\");\n      }\n    },\n    all(node, query, callback) {\n      if (!node) {\n        return [];\n      }\n      const array = Array.from(node.querySelectorAll(query));\n      if (callback) {\n        array.forEach(callback);\n      }\n      return array;\n    },\n    childNodeLength(html) {\n      const template = document.createElement(\"template\");\n      template.innerHTML = html;\n      return template.content.childElementCount;\n    },\n    isUploadInput(el) {\n      return el.type === \"file\" && el.getAttribute(PHX_UPLOAD_REF) !== null;\n    },\n    isAutoUpload(inputEl) {\n      return inputEl.hasAttribute(\"data-phx-auto-upload\");\n    },\n    findUploadInputs(node) {\n      const formId = node.id;\n      const inputsOutsideForm = this.all(\n        document,\n        `input[type=\"file\"][${PHX_UPLOAD_REF}][form=\"${formId}\"]`\n      );\n      return this.all(node, `input[type=\"file\"][${PHX_UPLOAD_REF}]`).concat(\n        inputsOutsideForm\n      );\n    },\n    findComponentNodeList(viewId, cid, doc2 = document) {\n      return this.all(\n        doc2,\n        `[${PHX_VIEW_REF}=\"${viewId}\"][${PHX_COMPONENT}=\"${cid}\"]`\n      );\n    },\n    isPhxDestroyed(node) {\n      return node.id && DOM.private(node, \"destroyed\") ? true : false;\n    },\n    wantsNewTab(e) {\n      const wantsNewTab = e.ctrlKey || e.shiftKey || e.metaKey || e.button && e.button === 1;\n      const isDownload = e.target instanceof HTMLAnchorElement && e.target.hasAttribute(\"download\");\n      const isTargetBlank = e.target.hasAttribute(\"target\") && e.target.getAttribute(\"target\").toLowerCase() === \"_blank\";\n      const isTargetNamedTab = e.target.hasAttribute(\"target\") && !e.target.getAttribute(\"target\").startsWith(\"_\");\n      return wantsNewTab || isTargetBlank || isDownload || isTargetNamedTab;\n    },\n    isUnloadableFormSubmit(e) {\n      const isDialogSubmit = e.target && e.target.getAttribute(\"method\") === \"dialog\" || e.submitter && e.submitter.getAttribute(\"formmethod\") === \"dialog\";\n      if (isDialogSubmit) {\n        return false;\n      } else {\n        return !e.defaultPrevented && !this.wantsNewTab(e);\n      }\n    },\n    isNewPageClick(e, currentLocation) {\n      const href = e.target instanceof HTMLAnchorElement ? e.target.getAttribute(\"href\") : null;\n      let url;\n      if (e.defaultPrevented || href === null || this.wantsNewTab(e)) {\n        return false;\n      }\n      if (href.startsWith(\"mailto:\") || href.startsWith(\"tel:\")) {\n        return false;\n      }\n      if (e.target.isContentEditable) {\n        return false;\n      }\n      try {\n        url = new URL(href);\n      } catch (e2) {\n        try {\n          url = new URL(href, currentLocation);\n        } catch (e3) {\n          return true;\n        }\n      }\n      if (url.host === currentLocation.host && url.protocol === currentLocation.protocol) {\n        if (url.pathname === currentLocation.pathname && url.search === currentLocation.search) {\n          return url.hash === \"\" && !url.href.endsWith(\"#\");\n        }\n      }\n      return url.protocol.startsWith(\"http\");\n    },\n    markPhxChildDestroyed(el) {\n      if (this.isPhxChild(el)) {\n        el.setAttribute(PHX_SESSION, \"\");\n      }\n      this.putPrivate(el, \"destroyed\", true);\n    },\n    findPhxChildrenInFragment(html, parentId) {\n      const template = document.createElement(\"template\");\n      template.innerHTML = html;\n      return this.findPhxChildren(template.content, parentId);\n    },\n    isIgnored(el, phxUpdate) {\n      return (el.getAttribute(phxUpdate) || el.getAttribute(\"data-phx-update\")) === \"ignore\";\n    },\n    isPhxUpdate(el, phxUpdate, updateTypes) {\n      return el.getAttribute && updateTypes.indexOf(el.getAttribute(phxUpdate)) >= 0;\n    },\n    findPhxSticky(el) {\n      return this.all(el, `[${PHX_STICKY}]`);\n    },\n    findPhxChildren(el, parentId) {\n      return this.all(el, `${PHX_VIEW_SELECTOR}[${PHX_PARENT_ID}=\"${parentId}\"]`);\n    },\n    findExistingParentCIDs(viewId, cids) {\n      const parentCids = /* @__PURE__ */ new Set();\n      const childrenCids = /* @__PURE__ */ new Set();\n      cids.forEach((cid) => {\n        this.all(\n          document,\n          `[${PHX_VIEW_REF}=\"${viewId}\"][${PHX_COMPONENT}=\"${cid}\"]`\n        ).forEach((parent) => {\n          parentCids.add(cid);\n          this.all(parent, `[${PHX_VIEW_REF}=\"${viewId}\"][${PHX_COMPONENT}]`).map((el) => parseInt(el.getAttribute(PHX_COMPONENT))).forEach((childCID) => childrenCids.add(childCID));\n        });\n      });\n      childrenCids.forEach((childCid) => parentCids.delete(childCid));\n      return parentCids;\n    },\n    private(el, key) {\n      return el[PHX_PRIVATE] && el[PHX_PRIVATE][key];\n    },\n    deletePrivate(el, key) {\n      el[PHX_PRIVATE] && delete el[PHX_PRIVATE][key];\n    },\n    putPrivate(el, key, value) {\n      if (!el[PHX_PRIVATE]) {\n        el[PHX_PRIVATE] = {};\n      }\n      el[PHX_PRIVATE][key] = value;\n    },\n    updatePrivate(el, key, defaultVal, updateFunc) {\n      const existing = this.private(el, key);\n      if (existing === void 0) {\n        this.putPrivate(el, key, updateFunc(defaultVal));\n      } else {\n        this.putPrivate(el, key, updateFunc(existing));\n      }\n    },\n    syncPendingAttrs(fromEl, toEl) {\n      if (!fromEl.hasAttribute(PHX_REF_SRC)) {\n        return;\n      }\n      PHX_EVENT_CLASSES.forEach((className) => {\n        fromEl.classList.contains(className) && toEl.classList.add(className);\n      });\n      PHX_PENDING_ATTRS.filter((attr) => fromEl.hasAttribute(attr)).forEach(\n        (attr) => {\n          toEl.setAttribute(attr, fromEl.getAttribute(attr));\n        }\n      );\n    },\n    copyPrivates(target, source) {\n      if (source[PHX_PRIVATE]) {\n        target[PHX_PRIVATE] = source[PHX_PRIVATE];\n      }\n    },\n    putTitle(str) {\n      const titleEl = document.querySelector(\"title\");\n      if (titleEl) {\n        const { prefix, suffix, default: defaultTitle } = titleEl.dataset;\n        const isEmpty2 = typeof str !== \"string\" || str.trim() === \"\";\n        if (isEmpty2 && typeof defaultTitle !== \"string\") {\n          return;\n        }\n        const inner = isEmpty2 ? defaultTitle : str;\n        document.title = `${prefix || \"\"}${inner || \"\"}${suffix || \"\"}`;\n      } else {\n        document.title = str;\n      }\n    },\n    debounce(el, event, phxDebounce, defaultDebounce, phxThrottle, defaultThrottle, asyncFilter, callback) {\n      let debounce = el.getAttribute(phxDebounce);\n      let throttle = el.getAttribute(phxThrottle);\n      if (debounce === \"\") {\n        debounce = defaultDebounce;\n      }\n      if (throttle === \"\") {\n        throttle = defaultThrottle;\n      }\n      const value = debounce || throttle;\n      switch (value) {\n        case null:\n          return callback();\n        case \"blur\":\n          this.incCycle(el, \"debounce-blur-cycle\", () => {\n            if (asyncFilter()) {\n              callback();\n            }\n          });\n          if (this.once(el, \"debounce-blur\")) {\n            el.addEventListener(\n              \"blur\",\n              () => this.triggerCycle(el, \"debounce-blur-cycle\")\n            );\n          }\n          return;\n        default:\n          const timeout = parseInt(value);\n          const trigger = () => throttle ? this.deletePrivate(el, THROTTLED) : callback();\n          const currentCycle = this.incCycle(el, DEBOUNCE_TRIGGER, trigger);\n          if (isNaN(timeout)) {\n            return logError(`invalid throttle/debounce value: ${value}`);\n          }\n          if (throttle) {\n            let newKeyDown = false;\n            if (event.type === \"keydown\") {\n              const prevKey = this.private(el, DEBOUNCE_PREV_KEY);\n              this.putPrivate(el, DEBOUNCE_PREV_KEY, event.key);\n              newKeyDown = prevKey !== event.key;\n            }\n            if (!newKeyDown && this.private(el, THROTTLED)) {\n              return false;\n            } else {\n              callback();\n              const t = setTimeout(() => {\n                if (asyncFilter()) {\n                  this.triggerCycle(el, DEBOUNCE_TRIGGER);\n                }\n              }, timeout);\n              this.putPrivate(el, THROTTLED, t);\n            }\n          } else {\n            setTimeout(() => {\n              if (asyncFilter()) {\n                this.triggerCycle(el, DEBOUNCE_TRIGGER, currentCycle);\n              }\n            }, timeout);\n          }\n          const form = el.form;\n          if (form && this.once(form, \"bind-debounce\")) {\n            form.addEventListener(\"submit\", () => {\n              Array.from(new FormData(form).entries(), ([name]) => {\n                const namedItem = form.elements.namedItem(name);\n                const input = namedItem instanceof RadioNodeList ? namedItem[0] : namedItem;\n                if (input) {\n                  this.incCycle(input, DEBOUNCE_TRIGGER);\n                  this.deletePrivate(input, THROTTLED);\n                }\n              });\n            });\n          }\n          if (this.once(el, \"bind-debounce\")) {\n            el.addEventListener(\"blur\", () => {\n              clearTimeout(this.private(el, THROTTLED));\n              this.triggerCycle(el, DEBOUNCE_TRIGGER);\n            });\n          }\n      }\n    },\n    triggerCycle(el, key, currentCycle) {\n      const [cycle, trigger] = this.private(el, key);\n      if (!currentCycle) {\n        currentCycle = cycle;\n      }\n      if (currentCycle === cycle) {\n        this.incCycle(el, key);\n        trigger();\n      }\n    },\n    once(el, key) {\n      if (this.private(el, key) === true) {\n        return false;\n      }\n      this.putPrivate(el, key, true);\n      return true;\n    },\n    incCycle(el, key, trigger = function() {\n    }) {\n      let [currentCycle] = this.private(el, key) || [0, trigger];\n      currentCycle++;\n      this.putPrivate(el, key, [currentCycle, trigger]);\n      return currentCycle;\n    },\n    // maintains or adds privately used hook information\n    // fromEl and toEl can be the same element in the case of a newly added node\n    // fromEl and toEl can be any HTML node type, so we need to check if it's an element node\n    maintainPrivateHooks(fromEl, toEl, phxViewportTop, phxViewportBottom) {\n      if (fromEl.hasAttribute && fromEl.hasAttribute(\"data-phx-hook\") && !toEl.hasAttribute(\"data-phx-hook\")) {\n        toEl.setAttribute(\"data-phx-hook\", fromEl.getAttribute(\"data-phx-hook\"));\n      }\n      if (toEl.hasAttribute && (toEl.hasAttribute(phxViewportTop) || toEl.hasAttribute(phxViewportBottom))) {\n        toEl.setAttribute(\"data-phx-hook\", \"Phoenix.InfiniteScroll\");\n      }\n    },\n    putCustomElHook(el, hook) {\n      if (el.isConnected) {\n        el.setAttribute(\"data-phx-hook\", \"\");\n      } else {\n        console.error(`\n        hook attached to non-connected DOM element\n        ensure you are calling createHook within your connectedCallback. ${el.outerHTML}\n      `);\n      }\n      this.putPrivate(el, \"custom-el-hook\", hook);\n    },\n    getCustomElHook(el) {\n      return this.private(el, \"custom-el-hook\");\n    },\n    isUsedInput(el) {\n      return el.nodeType === Node.ELEMENT_NODE && (this.private(el, PHX_HAS_FOCUSED) || this.private(el, PHX_HAS_SUBMITTED));\n    },\n    resetForm(form) {\n      Array.from(form.elements).forEach((input) => {\n        this.deletePrivate(input, PHX_HAS_FOCUSED);\n        this.deletePrivate(input, PHX_HAS_SUBMITTED);\n      });\n    },\n    isPhxChild(node) {\n      return node.getAttribute && node.getAttribute(PHX_PARENT_ID);\n    },\n    isPhxSticky(node) {\n      return node.getAttribute && node.getAttribute(PHX_STICKY) !== null;\n    },\n    isChildOfAny(el, parents) {\n      return !!parents.find((parent) => parent.contains(el));\n    },\n    firstPhxChild(el) {\n      return this.isPhxChild(el) ? el : this.all(el, `[${PHX_PARENT_ID}]`)[0];\n    },\n    isPortalTemplate(el) {\n      return el.tagName === \"TEMPLATE\" && el.hasAttribute(PHX_PORTAL);\n    },\n    closestViewEl(el) {\n      const portalOrViewEl = el.closest(\n        `[${PHX_TELEPORTED_REF}],${PHX_VIEW_SELECTOR}`\n      );\n      if (!portalOrViewEl) {\n        return null;\n      }\n      if (portalOrViewEl.hasAttribute(PHX_TELEPORTED_REF)) {\n        return this.byId(portalOrViewEl.getAttribute(PHX_TELEPORTED_REF));\n      } else if (portalOrViewEl.hasAttribute(PHX_SESSION)) {\n        return portalOrViewEl;\n      }\n      return null;\n    },\n    dispatchEvent(target, name, opts = {}) {\n      let defaultBubble = true;\n      const isUploadTarget = target.nodeName === \"INPUT\" && target.type === \"file\";\n      if (isUploadTarget && name === \"click\") {\n        defaultBubble = false;\n      }\n      const bubbles = opts.bubbles === void 0 ? defaultBubble : !!opts.bubbles;\n      const eventOpts = {\n        bubbles,\n        cancelable: true,\n        detail: opts.detail || {}\n      };\n      const event = name === \"click\" ? new MouseEvent(\"click\", eventOpts) : new CustomEvent(name, eventOpts);\n      target.dispatchEvent(event);\n    },\n    cloneNode(node, html) {\n      if (typeof html === \"undefined\") {\n        return node.cloneNode(true);\n      } else {\n        const cloned = node.cloneNode(false);\n        cloned.innerHTML = html;\n        return cloned;\n      }\n    },\n    // merge attributes from source to target\n    // if an element is ignored, we only merge data attributes\n    // including removing data attributes that are no longer in the source\n    mergeAttrs(target, source, opts = {}) {\n      var _a;\n      const exclude = new Set(opts.exclude || []);\n      const isIgnored = opts.isIgnored;\n      const sourceAttrs = source.attributes;\n      for (let i = sourceAttrs.length - 1; i >= 0; i--) {\n        const name = sourceAttrs[i].name;\n        if (!exclude.has(name)) {\n          const sourceValue = source.getAttribute(name);\n          if (target.getAttribute(name) !== sourceValue && (!isIgnored || isIgnored && name.startsWith(\"data-\"))) {\n            target.setAttribute(name, sourceValue);\n          }\n        } else {\n          if (name === \"value\") {\n            const sourceValue = (_a = source.value) != null ? _a : source.getAttribute(name);\n            if (target.value === sourceValue) {\n              target.setAttribute(\"value\", source.getAttribute(name));\n            }\n          }\n        }\n      }\n      const targetAttrs = target.attributes;\n      for (let i = targetAttrs.length - 1; i >= 0; i--) {\n        const name = targetAttrs[i].name;\n        if (isIgnored) {\n          if (name.startsWith(\"data-\") && !source.hasAttribute(name) && !PHX_PENDING_ATTRS.includes(name)) {\n            target.removeAttribute(name);\n          }\n        } else {\n          if (!source.hasAttribute(name)) {\n            target.removeAttribute(name);\n          }\n        }\n      }\n    },\n    mergeFocusedInput(target, source) {\n      if (!(target instanceof HTMLSelectElement)) {\n        DOM.mergeAttrs(target, source, { exclude: [\"value\"] });\n      }\n      if (source.readOnly) {\n        target.setAttribute(\"readonly\", true);\n      } else {\n        target.removeAttribute(\"readonly\");\n      }\n    },\n    hasSelectionRange(el) {\n      return el.setSelectionRange && (el.type === \"text\" || el.type === \"textarea\");\n    },\n    restoreFocus(focused, selectionStart, selectionEnd) {\n      if (focused instanceof HTMLSelectElement) {\n        focused.focus();\n      }\n      if (!DOM.isTextualInput(focused)) {\n        return;\n      }\n      const wasFocused = focused.matches(\":focus\");\n      if (!wasFocused) {\n        focused.focus();\n      }\n      if (this.hasSelectionRange(focused)) {\n        focused.setSelectionRange(selectionStart, selectionEnd);\n      }\n    },\n    isFormInput(el) {\n      if (el.localName && customElements.get(el.localName)) {\n        return customElements.get(el.localName)[`formAssociated`];\n      }\n      return /^(?:input|select|textarea)$/i.test(el.tagName) && el.type !== \"button\";\n    },\n    syncAttrsToProps(el) {\n      if (el instanceof HTMLInputElement && CHECKABLE_INPUTS.indexOf(el.type.toLocaleLowerCase()) >= 0) {\n        el.checked = el.getAttribute(\"checked\") !== null;\n      }\n    },\n    isTextualInput(el) {\n      return FOCUSABLE_INPUTS.indexOf(el.type) >= 0;\n    },\n    isNowTriggerFormExternal(el, phxTriggerExternal) {\n      return el.getAttribute && el.getAttribute(phxTriggerExternal) !== null && document.body.contains(el);\n    },\n    cleanChildNodes(container, phxUpdate) {\n      if (DOM.isPhxUpdate(container, phxUpdate, [\"append\", \"prepend\", PHX_STREAM])) {\n        const toRemove = [];\n        container.childNodes.forEach((childNode) => {\n          if (!childNode.id) {\n            const isEmptyTextNode = childNode.nodeType === Node.TEXT_NODE && childNode.nodeValue.trim() === \"\";\n            if (!isEmptyTextNode && childNode.nodeType !== Node.COMMENT_NODE) {\n              logError(\n                `only HTML element tags with an id are allowed inside containers with phx-update.\n\nremoving illegal node: \"${(childNode.outerHTML || childNode.nodeValue).trim()}\"\n\n`\n              );\n            }\n            toRemove.push(childNode);\n          }\n        });\n        toRemove.forEach((childNode) => childNode.remove());\n      }\n    },\n    replaceRootContainer(container, tagName, attrs) {\n      const retainedAttrs = /* @__PURE__ */ new Set([\n        \"id\",\n        PHX_SESSION,\n        PHX_STATIC,\n        PHX_MAIN,\n        PHX_ROOT_ID\n      ]);\n      if (container.tagName.toLowerCase() === tagName.toLowerCase()) {\n        Array.from(container.attributes).filter((attr) => !retainedAttrs.has(attr.name.toLowerCase())).forEach((attr) => container.removeAttribute(attr.name));\n        Object.keys(attrs).filter((name) => !retainedAttrs.has(name.toLowerCase())).forEach((attr) => container.setAttribute(attr, attrs[attr]));\n        return container;\n      } else {\n        const newContainer = document.createElement(tagName);\n        Object.keys(attrs).forEach(\n          (attr) => newContainer.setAttribute(attr, attrs[attr])\n        );\n        retainedAttrs.forEach(\n          (attr) => newContainer.setAttribute(attr, container.getAttribute(attr))\n        );\n        newContainer.innerHTML = container.innerHTML;\n        container.replaceWith(newContainer);\n        return newContainer;\n      }\n    },\n    getSticky(el, name, defaultVal) {\n      const op = (DOM.private(el, \"sticky\") || []).find(\n        ([existingName]) => name === existingName\n      );\n      if (op) {\n        const [_name, _op, stashedResult] = op;\n        return stashedResult;\n      } else {\n        return typeof defaultVal === \"function\" ? defaultVal() : defaultVal;\n      }\n    },\n    deleteSticky(el, name) {\n      this.updatePrivate(el, \"sticky\", [], (ops) => {\n        return ops.filter(([existingName, _]) => existingName !== name);\n      });\n    },\n    putSticky(el, name, op) {\n      const stashedResult = op(el);\n      this.updatePrivate(el, \"sticky\", [], (ops) => {\n        const existingIndex = ops.findIndex(\n          ([existingName]) => name === existingName\n        );\n        if (existingIndex >= 0) {\n          ops[existingIndex] = [name, op, stashedResult];\n        } else {\n          ops.push([name, op, stashedResult]);\n        }\n        return ops;\n      });\n    },\n    applyStickyOperations(el) {\n      const ops = DOM.private(el, \"sticky\");\n      if (!ops) {\n        return;\n      }\n      ops.forEach(([name, op, _stashed]) => this.putSticky(el, name, op));\n    },\n    isLocked(el) {\n      return el.hasAttribute && el.hasAttribute(PHX_REF_LOCK);\n    },\n    attributeIgnored(attribute, ignoredAttributes) {\n      return ignoredAttributes.some(\n        (toIgnore) => attribute.name == toIgnore || toIgnore === \"*\" || toIgnore.includes(\"*\") && attribute.name.match(toIgnore) != null\n      );\n    }\n  };\n  var dom_default = DOM;\n\n  // js/phoenix_live_view/upload_entry.js\n  var UploadEntry = class {\n    static isActive(fileEl, file) {\n      const isNew = file._phxRef === void 0;\n      const activeRefs = fileEl.getAttribute(PHX_ACTIVE_ENTRY_REFS).split(\",\");\n      const isActive = activeRefs.indexOf(LiveUploader.genFileRef(file)) >= 0;\n      return file.size > 0 && (isNew || isActive);\n    }\n    static isPreflighted(fileEl, file) {\n      const preflightedRefs = fileEl.getAttribute(PHX_PREFLIGHTED_REFS).split(\",\");\n      const isPreflighted = preflightedRefs.indexOf(LiveUploader.genFileRef(file)) >= 0;\n      return isPreflighted && this.isActive(fileEl, file);\n    }\n    static isPreflightInProgress(file) {\n      return file._preflightInProgress === true;\n    }\n    static markPreflightInProgress(file) {\n      file._preflightInProgress = true;\n    }\n    constructor(fileEl, file, view, autoUpload) {\n      this.ref = LiveUploader.genFileRef(file);\n      this.fileEl = fileEl;\n      this.file = file;\n      this.view = view;\n      this.meta = null;\n      this._isCancelled = false;\n      this._isDone = false;\n      this._progress = 0;\n      this._lastProgressSent = -1;\n      this._onDone = function() {\n      };\n      this._onElUpdated = this.onElUpdated.bind(this);\n      this.fileEl.addEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated);\n      this.autoUpload = autoUpload;\n    }\n    metadata() {\n      return this.meta;\n    }\n    progress(progress) {\n      this._progress = Math.floor(progress);\n      if (this._progress > this._lastProgressSent) {\n        if (this._progress >= 100) {\n          this._progress = 100;\n          this._lastProgressSent = 100;\n          this._isDone = true;\n          this.view.pushFileProgress(this.fileEl, this.ref, 100, () => {\n            LiveUploader.untrackFile(this.fileEl, this.file);\n            this._onDone();\n          });\n        } else {\n          this._lastProgressSent = this._progress;\n          this.view.pushFileProgress(this.fileEl, this.ref, this._progress);\n        }\n      }\n    }\n    isCancelled() {\n      return this._isCancelled;\n    }\n    cancel() {\n      this.file._preflightInProgress = false;\n      this._isCancelled = true;\n      this._isDone = true;\n      this._onDone();\n    }\n    isDone() {\n      return this._isDone;\n    }\n    error(reason = \"failed\") {\n      this.fileEl.removeEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated);\n      this.view.pushFileProgress(this.fileEl, this.ref, { error: reason });\n      if (!this.isAutoUpload()) {\n        LiveUploader.clearFiles(this.fileEl);\n      }\n    }\n    isAutoUpload() {\n      return this.autoUpload;\n    }\n    //private\n    onDone(callback) {\n      this._onDone = () => {\n        this.fileEl.removeEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated);\n        callback();\n      };\n    }\n    onElUpdated() {\n      const activeRefs = this.fileEl.getAttribute(PHX_ACTIVE_ENTRY_REFS).split(\",\");\n      if (activeRefs.indexOf(this.ref) === -1) {\n        LiveUploader.untrackFile(this.fileEl, this.file);\n        this.cancel();\n      }\n    }\n    toPreflightPayload() {\n      return {\n        last_modified: this.file.lastModified,\n        name: this.file.name,\n        relative_path: this.file.webkitRelativePath,\n        size: this.file.size,\n        type: this.file.type,\n        ref: this.ref,\n        meta: typeof this.file.meta === \"function\" ? this.file.meta() : void 0\n      };\n    }\n    uploader(uploaders) {\n      if (this.meta.uploader) {\n        const callback = uploaders[this.meta.uploader] || logError(`no uploader configured for ${this.meta.uploader}`);\n        return { name: this.meta.uploader, callback };\n      } else {\n        return { name: \"channel\", callback: channelUploader };\n      }\n    }\n    zipPostFlight(resp) {\n      this.meta = resp.entries[this.ref];\n      if (!this.meta) {\n        logError(`no preflight upload response returned with ref ${this.ref}`, {\n          input: this.fileEl,\n          response: resp\n        });\n      }\n    }\n  };\n\n  // js/phoenix_live_view/live_uploader.js\n  var liveUploaderFileRef = 0;\n  var LiveUploader = class _LiveUploader {\n    static genFileRef(file) {\n      const ref = file._phxRef;\n      if (ref !== void 0) {\n        return ref;\n      } else {\n        file._phxRef = (liveUploaderFileRef++).toString();\n        return file._phxRef;\n      }\n    }\n    static getEntryDataURL(inputEl, ref, callback) {\n      const file = this.activeFiles(inputEl).find(\n        (file2) => this.genFileRef(file2) === ref\n      );\n      callback(URL.createObjectURL(file));\n    }\n    static hasUploadsInProgress(formEl) {\n      let active = 0;\n      dom_default.findUploadInputs(formEl).forEach((input) => {\n        if (input.getAttribute(PHX_PREFLIGHTED_REFS) !== input.getAttribute(PHX_DONE_REFS)) {\n          active++;\n        }\n      });\n      return active > 0;\n    }\n    static serializeUploads(inputEl) {\n      const files = this.activeFiles(inputEl);\n      const fileData = {};\n      files.forEach((file) => {\n        const entry = { path: inputEl.name };\n        const uploadRef = inputEl.getAttribute(PHX_UPLOAD_REF);\n        fileData[uploadRef] = fileData[uploadRef] || [];\n        entry.ref = this.genFileRef(file);\n        entry.last_modified = file.lastModified;\n        entry.name = file.name || entry.ref;\n        entry.relative_path = file.webkitRelativePath;\n        entry.type = file.type;\n        entry.size = file.size;\n        if (typeof file.meta === \"function\") {\n          entry.meta = file.meta();\n        }\n        fileData[uploadRef].push(entry);\n      });\n      return fileData;\n    }\n    static clearFiles(inputEl) {\n      inputEl.value = null;\n      inputEl.removeAttribute(PHX_UPLOAD_REF);\n      dom_default.putPrivate(inputEl, \"files\", []);\n    }\n    static untrackFile(inputEl, file) {\n      dom_default.putPrivate(\n        inputEl,\n        \"files\",\n        dom_default.private(inputEl, \"files\").filter((f) => !Object.is(f, file))\n      );\n    }\n    /**\n     * @param {HTMLInputElement} inputEl\n     * @param {Array<File|Blob>} files\n     * @param {DataTransfer} [dataTransfer]\n     */\n    static trackFiles(inputEl, files, dataTransfer) {\n      if (inputEl.getAttribute(\"multiple\") !== null) {\n        const newFiles = files.filter(\n          (file) => !this.activeFiles(inputEl).find((f) => Object.is(f, file))\n        );\n        dom_default.updatePrivate(\n          inputEl,\n          \"files\",\n          [],\n          (existing) => existing.concat(newFiles)\n        );\n        inputEl.value = null;\n      } else {\n        if (dataTransfer && dataTransfer.files.length > 0) {\n          inputEl.files = dataTransfer.files;\n        }\n        dom_default.putPrivate(inputEl, \"files\", files);\n      }\n    }\n    static activeFileInputs(formEl) {\n      const fileInputs = dom_default.findUploadInputs(formEl);\n      return Array.from(fileInputs).filter(\n        (el) => el.files && this.activeFiles(el).length > 0\n      );\n    }\n    static activeFiles(input) {\n      return (dom_default.private(input, \"files\") || []).filter(\n        (f) => UploadEntry.isActive(input, f)\n      );\n    }\n    static inputsAwaitingPreflight(formEl) {\n      const fileInputs = dom_default.findUploadInputs(formEl);\n      return Array.from(fileInputs).filter(\n        (input) => this.filesAwaitingPreflight(input).length > 0\n      );\n    }\n    static filesAwaitingPreflight(input) {\n      return this.activeFiles(input).filter(\n        (f) => !UploadEntry.isPreflighted(input, f) && !UploadEntry.isPreflightInProgress(f)\n      );\n    }\n    static markPreflightInProgress(entries) {\n      entries.forEach((entry) => UploadEntry.markPreflightInProgress(entry.file));\n    }\n    constructor(inputEl, view, onComplete) {\n      this.autoUpload = dom_default.isAutoUpload(inputEl);\n      this.view = view;\n      this.onComplete = onComplete;\n      this._entries = Array.from(\n        _LiveUploader.filesAwaitingPreflight(inputEl) || []\n      ).map((file) => new UploadEntry(inputEl, file, view, this.autoUpload));\n      _LiveUploader.markPreflightInProgress(this._entries);\n      this.numEntriesInProgress = this._entries.length;\n    }\n    isAutoUpload() {\n      return this.autoUpload;\n    }\n    entries() {\n      return this._entries;\n    }\n    initAdapterUpload(resp, onError, liveSocket) {\n      this._entries = this._entries.map((entry) => {\n        if (entry.isCancelled()) {\n          this.numEntriesInProgress--;\n          if (this.numEntriesInProgress === 0) {\n            this.onComplete();\n          }\n        } else {\n          entry.zipPostFlight(resp);\n          entry.onDone(() => {\n            this.numEntriesInProgress--;\n            if (this.numEntriesInProgress === 0) {\n              this.onComplete();\n            }\n          });\n        }\n        return entry;\n      });\n      const groupedEntries = this._entries.reduce((acc, entry) => {\n        if (!entry.meta) {\n          return acc;\n        }\n        const { name, callback } = entry.uploader(liveSocket.uploaders);\n        acc[name] = acc[name] || { callback, entries: [] };\n        acc[name].entries.push(entry);\n        return acc;\n      }, {});\n      for (const name in groupedEntries) {\n        const { callback, entries } = groupedEntries[name];\n        callback(entries, onError, resp, liveSocket);\n      }\n    }\n  };\n\n  // js/phoenix_live_view/aria.js\n  var ARIA = {\n    anyOf(instance, classes) {\n      return classes.find((name) => instance instanceof name);\n    },\n    isFocusable(el, interactiveOnly) {\n      return el instanceof HTMLAnchorElement && el.rel !== \"ignore\" || el instanceof HTMLAreaElement && el.href !== void 0 || !el.disabled && this.anyOf(el, [\n        HTMLInputElement,\n        HTMLSelectElement,\n        HTMLTextAreaElement,\n        HTMLButtonElement\n      ]) || el instanceof HTMLIFrameElement || el.tabIndex >= 0 && el.getAttribute(\"aria-hidden\") !== \"true\" || !interactiveOnly && el.getAttribute(\"tabindex\") !== null && el.getAttribute(\"aria-hidden\") !== \"true\";\n    },\n    attemptFocus(el, interactiveOnly) {\n      if (this.isFocusable(el, interactiveOnly)) {\n        try {\n          el.focus();\n        } catch (e) {\n        }\n      }\n      return !!document.activeElement && document.activeElement.isSameNode(el);\n    },\n    focusFirstInteractive(el) {\n      let child = el.firstElementChild;\n      while (child) {\n        if (this.attemptFocus(child, true) || this.focusFirstInteractive(child)) {\n          return true;\n        }\n        child = child.nextElementSibling;\n      }\n    },\n    focusFirst(el) {\n      let child = el.firstElementChild;\n      while (child) {\n        if (this.attemptFocus(child) || this.focusFirst(child)) {\n          return true;\n        }\n        child = child.nextElementSibling;\n      }\n    },\n    focusLast(el) {\n      let child = el.lastElementChild;\n      while (child) {\n        if (this.attemptFocus(child) || this.focusLast(child)) {\n          return true;\n        }\n        child = child.previousElementSibling;\n      }\n    }\n  };\n  var aria_default = ARIA;\n\n  // js/phoenix_live_view/hooks.js\n  var Hooks = {\n    LiveFileUpload: {\n      activeRefs() {\n        return this.el.getAttribute(PHX_ACTIVE_ENTRY_REFS);\n      },\n      preflightedRefs() {\n        return this.el.getAttribute(PHX_PREFLIGHTED_REFS);\n      },\n      mounted() {\n        this.js().ignoreAttributes(this.el, [\"value\"]);\n        this.preflightedWas = this.preflightedRefs();\n      },\n      updated() {\n        const newPreflights = this.preflightedRefs();\n        if (this.preflightedWas !== newPreflights) {\n          this.preflightedWas = newPreflights;\n          if (newPreflights === \"\") {\n            this.__view().cancelSubmit(this.el.form);\n          }\n        }\n        if (this.activeRefs() === \"\") {\n          this.el.value = null;\n        }\n        this.el.dispatchEvent(new CustomEvent(PHX_LIVE_FILE_UPDATED));\n      }\n    },\n    LiveImgPreview: {\n      mounted() {\n        this.ref = this.el.getAttribute(\"data-phx-entry-ref\");\n        this.inputEl = document.getElementById(\n          this.el.getAttribute(PHX_UPLOAD_REF)\n        );\n        LiveUploader.getEntryDataURL(this.inputEl, this.ref, (url) => {\n          this.url = url;\n          this.el.src = url;\n        });\n      },\n      destroyed() {\n        URL.revokeObjectURL(this.url);\n      }\n    },\n    FocusWrap: {\n      mounted() {\n        this.focusStart = this.el.firstElementChild;\n        this.focusEnd = this.el.lastElementChild;\n        this.focusStart.addEventListener(\"focus\", (e) => {\n          if (!e.relatedTarget || !this.el.contains(e.relatedTarget)) {\n            const nextFocus = e.target.nextElementSibling;\n            aria_default.attemptFocus(nextFocus) || aria_default.focusFirst(nextFocus);\n          } else {\n            aria_default.focusLast(this.el);\n          }\n        });\n        this.focusEnd.addEventListener(\"focus\", (e) => {\n          if (!e.relatedTarget || !this.el.contains(e.relatedTarget)) {\n            const nextFocus = e.target.previousElementSibling;\n            aria_default.attemptFocus(nextFocus) || aria_default.focusLast(nextFocus);\n          } else {\n            aria_default.focusFirst(this.el);\n          }\n        });\n        if (!this.el.contains(document.activeElement)) {\n          this.el.addEventListener(\"phx:show-end\", () => this.el.focus());\n          if (window.getComputedStyle(this.el).display !== \"none\") {\n            aria_default.focusFirst(this.el);\n          }\n        }\n      }\n    }\n  };\n  var findScrollContainer = (el) => {\n    if ([\"HTML\", \"BODY\"].indexOf(el.nodeName.toUpperCase()) >= 0)\n      return null;\n    if ([\"scroll\", \"auto\"].indexOf(getComputedStyle(el).overflowY) >= 0)\n      return el;\n    return findScrollContainer(el.parentElement);\n  };\n  var scrollTop = (scrollContainer) => {\n    if (scrollContainer) {\n      return scrollContainer.scrollTop;\n    } else {\n      return document.documentElement.scrollTop || document.body.scrollTop;\n    }\n  };\n  var bottom = (scrollContainer) => {\n    if (scrollContainer) {\n      return scrollContainer.getBoundingClientRect().bottom;\n    } else {\n      return window.innerHeight || document.documentElement.clientHeight;\n    }\n  };\n  var top = (scrollContainer) => {\n    if (scrollContainer) {\n      return scrollContainer.getBoundingClientRect().top;\n    } else {\n      return 0;\n    }\n  };\n  var isAtViewportTop = (el, scrollContainer) => {\n    const rect = el.getBoundingClientRect();\n    return Math.ceil(rect.top) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.top) <= bottom(scrollContainer);\n  };\n  var isAtViewportBottom = (el, scrollContainer) => {\n    const rect = el.getBoundingClientRect();\n    return Math.ceil(rect.bottom) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.bottom) <= bottom(scrollContainer);\n  };\n  var isWithinViewport = (el, scrollContainer) => {\n    const rect = el.getBoundingClientRect();\n    return Math.ceil(rect.top) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.top) <= bottom(scrollContainer);\n  };\n  Hooks.InfiniteScroll = {\n    mounted() {\n      this.scrollContainer = findScrollContainer(this.el);\n      let scrollBefore = scrollTop(this.scrollContainer);\n      let topOverran = false;\n      const throttleInterval = 500;\n      let pendingOp = null;\n      const onTopOverrun = this.throttle(\n        throttleInterval,\n        (topEvent, firstChild) => {\n          pendingOp = () => true;\n          this.liveSocket.js().push(this.el, topEvent, {\n            value: { id: firstChild.id, _overran: true },\n            callback: () => {\n              pendingOp = null;\n            }\n          });\n        }\n      );\n      const onFirstChildAtTop = this.throttle(\n        throttleInterval,\n        (topEvent, firstChild) => {\n          pendingOp = () => firstChild.scrollIntoView({ block: \"start\" });\n          this.liveSocket.js().push(this.el, topEvent, {\n            value: { id: firstChild.id },\n            callback: () => {\n              pendingOp = null;\n              window.requestAnimationFrame(() => {\n                if (!isWithinViewport(firstChild, this.scrollContainer)) {\n                  firstChild.scrollIntoView({ block: \"start\" });\n                }\n              });\n            }\n          });\n        }\n      );\n      const onLastChildAtBottom = this.throttle(\n        throttleInterval,\n        (bottomEvent, lastChild) => {\n          pendingOp = () => lastChild.scrollIntoView({ block: \"end\" });\n          this.liveSocket.js().push(this.el, bottomEvent, {\n            value: { id: lastChild.id },\n            callback: () => {\n              pendingOp = null;\n              window.requestAnimationFrame(() => {\n                if (!isWithinViewport(lastChild, this.scrollContainer)) {\n                  lastChild.scrollIntoView({ block: \"end\" });\n                }\n              });\n            }\n          });\n        }\n      );\n      this.onScroll = (_e) => {\n        const scrollNow = scrollTop(this.scrollContainer);\n        if (pendingOp) {\n          scrollBefore = scrollNow;\n          return pendingOp();\n        }\n        const rect = this.findOverrunTarget();\n        const topEvent = this.el.getAttribute(\n          this.liveSocket.binding(\"viewport-top\")\n        );\n        const bottomEvent = this.el.getAttribute(\n          this.liveSocket.binding(\"viewport-bottom\")\n        );\n        const lastChild = this.el.lastElementChild;\n        const firstChild = this.el.firstElementChild;\n        const isScrollingUp = scrollNow < scrollBefore;\n        const isScrollingDown = scrollNow > scrollBefore;\n        if (isScrollingUp && topEvent && !topOverran && rect.top >= 0) {\n          topOverran = true;\n          onTopOverrun(topEvent, firstChild);\n        } else if (isScrollingDown && topOverran && rect.top <= 0) {\n          topOverran = false;\n        }\n        if (topEvent && isScrollingUp && isAtViewportTop(firstChild, this.scrollContainer)) {\n          onFirstChildAtTop(topEvent, firstChild);\n        } else if (bottomEvent && isScrollingDown && isAtViewportBottom(lastChild, this.scrollContainer)) {\n          onLastChildAtBottom(bottomEvent, lastChild);\n        }\n        scrollBefore = scrollNow;\n      };\n      if (this.scrollContainer) {\n        this.scrollContainer.addEventListener(\"scroll\", this.onScroll);\n      } else {\n        window.addEventListener(\"scroll\", this.onScroll);\n      }\n    },\n    destroyed() {\n      if (this.scrollContainer) {\n        this.scrollContainer.removeEventListener(\"scroll\", this.onScroll);\n      } else {\n        window.removeEventListener(\"scroll\", this.onScroll);\n      }\n    },\n    throttle(interval, callback) {\n      let lastCallAt = 0;\n      let timer;\n      return (...args) => {\n        const now = Date.now();\n        const remainingTime = interval - (now - lastCallAt);\n        if (remainingTime <= 0 || remainingTime > interval) {\n          if (timer) {\n            clearTimeout(timer);\n            timer = null;\n          }\n          lastCallAt = now;\n          callback(...args);\n        } else if (!timer) {\n          timer = setTimeout(() => {\n            lastCallAt = Date.now();\n            timer = null;\n            callback(...args);\n          }, remainingTime);\n        }\n      };\n    },\n    findOverrunTarget() {\n      let rect;\n      const overrunTarget = this.el.getAttribute(\n        this.liveSocket.binding(PHX_VIEWPORT_OVERRUN_TARGET)\n      );\n      if (overrunTarget) {\n        const overrunEl = document.getElementById(overrunTarget);\n        if (overrunEl) {\n          rect = overrunEl.getBoundingClientRect();\n        } else {\n          throw new Error(\"did not find element with id \" + overrunTarget);\n        }\n      } else {\n        rect = this.el.getBoundingClientRect();\n      }\n      return rect;\n    }\n  };\n  var hooks_default = Hooks;\n\n  // js/phoenix_live_view/element_ref.js\n  var ElementRef = class {\n    static onUnlock(el, callback) {\n      if (!dom_default.isLocked(el) && !el.closest(`[${PHX_REF_LOCK}]`)) {\n        return callback();\n      }\n      const closestLock = el.closest(`[${PHX_REF_LOCK}]`);\n      const ref = closestLock.closest(`[${PHX_REF_LOCK}]`).getAttribute(PHX_REF_LOCK);\n      closestLock.addEventListener(\n        `phx:undo-lock:${ref}`,\n        () => {\n          callback();\n        },\n        { once: true }\n      );\n    }\n    constructor(el) {\n      this.el = el;\n      this.loadingRef = el.hasAttribute(PHX_REF_LOADING) ? parseInt(el.getAttribute(PHX_REF_LOADING), 10) : null;\n      this.lockRef = el.hasAttribute(PHX_REF_LOCK) ? parseInt(el.getAttribute(PHX_REF_LOCK), 10) : null;\n    }\n    // public\n    maybeUndo(ref, phxEvent, eachCloneCallback) {\n      if (!this.isWithin(ref)) {\n        dom_default.updatePrivate(this.el, PHX_PENDING_REFS, [], (pendingRefs) => {\n          pendingRefs.push(ref);\n          return pendingRefs;\n        });\n        return;\n      }\n      this.undoLocks(ref, phxEvent, eachCloneCallback);\n      this.undoLoading(ref, phxEvent);\n      dom_default.updatePrivate(this.el, PHX_PENDING_REFS, [], (pendingRefs) => {\n        return pendingRefs.filter((pendingRef) => {\n          let opts = {\n            detail: { ref: pendingRef, event: phxEvent },\n            bubbles: true,\n            cancelable: false\n          };\n          if (this.loadingRef && this.loadingRef > pendingRef) {\n            this.el.dispatchEvent(\n              new CustomEvent(`phx:undo-loading:${pendingRef}`, opts)\n            );\n          }\n          if (this.lockRef && this.lockRef > pendingRef) {\n            this.el.dispatchEvent(\n              new CustomEvent(`phx:undo-lock:${pendingRef}`, opts)\n            );\n          }\n          return pendingRef > ref;\n        });\n      });\n      if (this.isFullyResolvedBy(ref)) {\n        this.el.removeAttribute(PHX_REF_SRC);\n      }\n    }\n    // private\n    isWithin(ref) {\n      return !(this.loadingRef !== null && this.loadingRef > ref && this.lockRef !== null && this.lockRef > ref);\n    }\n    // Check for cloned PHX_REF_LOCK element that has been morphed behind\n    // the scenes while this element was locked in the DOM.\n    // When we apply the cloned tree to the active DOM element, we must\n    //\n    //   1. execute pending mounted hooks for nodes now in the DOM\n    //   2. undo any ref inside the cloned tree that has since been ack'd\n    undoLocks(ref, phxEvent, eachCloneCallback) {\n      if (!this.isLockUndoneBy(ref)) {\n        return;\n      }\n      const clonedTree = dom_default.private(this.el, PHX_REF_LOCK);\n      if (clonedTree) {\n        eachCloneCallback(clonedTree);\n        dom_default.deletePrivate(this.el, PHX_REF_LOCK);\n      }\n      this.el.removeAttribute(PHX_REF_LOCK);\n      const opts = {\n        detail: { ref, event: phxEvent },\n        bubbles: true,\n        cancelable: false\n      };\n      this.el.dispatchEvent(\n        new CustomEvent(`phx:undo-lock:${this.lockRef}`, opts)\n      );\n    }\n    undoLoading(ref, phxEvent) {\n      if (!this.isLoadingUndoneBy(ref)) {\n        if (this.canUndoLoading(ref) && this.el.classList.contains(\"phx-submit-loading\")) {\n          this.el.classList.remove(\"phx-change-loading\");\n        }\n        return;\n      }\n      if (this.canUndoLoading(ref)) {\n        this.el.removeAttribute(PHX_REF_LOADING);\n        const disabledVal = this.el.getAttribute(PHX_DISABLED);\n        const readOnlyVal = this.el.getAttribute(PHX_READONLY);\n        if (readOnlyVal !== null) {\n          this.el.readOnly = readOnlyVal === \"true\" ? true : false;\n          this.el.removeAttribute(PHX_READONLY);\n        }\n        if (disabledVal !== null) {\n          this.el.disabled = disabledVal === \"true\" ? true : false;\n          this.el.removeAttribute(PHX_DISABLED);\n        }\n        const disableRestore = this.el.getAttribute(PHX_DISABLE_WITH_RESTORE);\n        if (disableRestore !== null) {\n          this.el.textContent = disableRestore;\n          this.el.removeAttribute(PHX_DISABLE_WITH_RESTORE);\n        }\n        const opts = {\n          detail: { ref, event: phxEvent },\n          bubbles: true,\n          cancelable: false\n        };\n        this.el.dispatchEvent(\n          new CustomEvent(`phx:undo-loading:${this.loadingRef}`, opts)\n        );\n      }\n      PHX_EVENT_CLASSES.forEach((name) => {\n        if (name !== \"phx-submit-loading\" || this.canUndoLoading(ref)) {\n          dom_default.removeClass(this.el, name);\n        }\n      });\n    }\n    isLoadingUndoneBy(ref) {\n      return this.loadingRef === null ? false : this.loadingRef <= ref;\n    }\n    isLockUndoneBy(ref) {\n      return this.lockRef === null ? false : this.lockRef <= ref;\n    }\n    isFullyResolvedBy(ref) {\n      return (this.loadingRef === null || this.loadingRef <= ref) && (this.lockRef === null || this.lockRef <= ref);\n    }\n    // only remove the phx-submit-loading class if we are not locked\n    canUndoLoading(ref) {\n      return this.lockRef === null || this.lockRef <= ref;\n    }\n  };\n\n  // js/phoenix_live_view/dom_post_morph_restorer.js\n  var DOMPostMorphRestorer = class {\n    constructor(containerBefore, containerAfter, updateType) {\n      const idsBefore = /* @__PURE__ */ new Set();\n      const idsAfter = new Set(\n        [...containerAfter.children].map((child) => child.id)\n      );\n      const elementsToModify = [];\n      Array.from(containerBefore.children).forEach((child) => {\n        if (child.id) {\n          idsBefore.add(child.id);\n          if (idsAfter.has(child.id)) {\n            const previousElementId = child.previousElementSibling && child.previousElementSibling.id;\n            elementsToModify.push({\n              elementId: child.id,\n              previousElementId\n            });\n          }\n        }\n      });\n      this.containerId = containerAfter.id;\n      this.updateType = updateType;\n      this.elementsToModify = elementsToModify;\n      this.elementIdsToAdd = [...idsAfter].filter((id) => !idsBefore.has(id));\n    }\n    // We do the following to optimize append/prepend operations:\n    //   1) Track ids of modified elements & of new elements\n    //   2) All the modified elements are put back in the correct position in the DOM tree\n    //      by storing the id of their previous sibling\n    //   3) New elements are going to be put in the right place by morphdom during append.\n    //      For prepend, we move them to the first position in the container\n    perform() {\n      const container = dom_default.byId(this.containerId);\n      if (!container) {\n        return;\n      }\n      this.elementsToModify.forEach((elementToModify) => {\n        if (elementToModify.previousElementId) {\n          maybe(\n            document.getElementById(elementToModify.previousElementId),\n            (previousElem) => {\n              maybe(\n                document.getElementById(elementToModify.elementId),\n                (elem) => {\n                  const isInRightPlace = elem.previousElementSibling && elem.previousElementSibling.id == previousElem.id;\n                  if (!isInRightPlace) {\n                    previousElem.insertAdjacentElement(\"afterend\", elem);\n                  }\n                }\n              );\n            }\n          );\n        } else {\n          maybe(document.getElementById(elementToModify.elementId), (elem) => {\n            const isInRightPlace = elem.previousElementSibling == null;\n            if (!isInRightPlace) {\n              container.insertAdjacentElement(\"afterbegin\", elem);\n            }\n          });\n        }\n      });\n      if (this.updateType == \"prepend\") {\n        this.elementIdsToAdd.reverse().forEach((elemId) => {\n          maybe(\n            document.getElementById(elemId),\n            (elem) => container.insertAdjacentElement(\"afterbegin\", elem)\n          );\n        });\n      }\n    }\n  };\n\n  // ../node_modules/morphdom/dist/morphdom-esm.js\n  var DOCUMENT_FRAGMENT_NODE = 11;\n  function morphAttrs(fromNode, toNode) {\n    var toNodeAttrs = toNode.attributes;\n    var attr;\n    var attrName;\n    var attrNamespaceURI;\n    var attrValue;\n    var fromValue;\n    if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) {\n      return;\n    }\n    for (var i = toNodeAttrs.length - 1; i >= 0; i--) {\n      attr = toNodeAttrs[i];\n      attrName = attr.name;\n      attrNamespaceURI = attr.namespaceURI;\n      attrValue = attr.value;\n      if (attrNamespaceURI) {\n        attrName = attr.localName || attrName;\n        fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName);\n        if (fromValue !== attrValue) {\n          if (attr.prefix === \"xmlns\") {\n            attrName = attr.name;\n          }\n          fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue);\n        }\n      } else {\n        fromValue = fromNode.getAttribute(attrName);\n        if (fromValue !== attrValue) {\n          fromNode.setAttribute(attrName, attrValue);\n        }\n      }\n    }\n    var fromNodeAttrs = fromNode.attributes;\n    for (var d = fromNodeAttrs.length - 1; d >= 0; d--) {\n      attr = fromNodeAttrs[d];\n      attrName = attr.name;\n      attrNamespaceURI = attr.namespaceURI;\n      if (attrNamespaceURI) {\n        attrName = attr.localName || attrName;\n        if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) {\n          fromNode.removeAttributeNS(attrNamespaceURI, attrName);\n        }\n      } else {\n        if (!toNode.hasAttribute(attrName)) {\n          fromNode.removeAttribute(attrName);\n        }\n      }\n    }\n  }\n  var range;\n  var NS_XHTML = \"http://www.w3.org/1999/xhtml\";\n  var doc = typeof document === \"undefined\" ? void 0 : document;\n  var HAS_TEMPLATE_SUPPORT = !!doc && \"content\" in doc.createElement(\"template\");\n  var HAS_RANGE_SUPPORT = !!doc && doc.createRange && \"createContextualFragment\" in doc.createRange();\n  function createFragmentFromTemplate(str) {\n    var template = doc.createElement(\"template\");\n    template.innerHTML = str;\n    return template.content.childNodes[0];\n  }\n  function createFragmentFromRange(str) {\n    if (!range) {\n      range = doc.createRange();\n      range.selectNode(doc.body);\n    }\n    var fragment = range.createContextualFragment(str);\n    return fragment.childNodes[0];\n  }\n  function createFragmentFromWrap(str) {\n    var fragment = doc.createElement(\"body\");\n    fragment.innerHTML = str;\n    return fragment.childNodes[0];\n  }\n  function toElement(str) {\n    str = str.trim();\n    if (HAS_TEMPLATE_SUPPORT) {\n      return createFragmentFromTemplate(str);\n    } else if (HAS_RANGE_SUPPORT) {\n      return createFragmentFromRange(str);\n    }\n    return createFragmentFromWrap(str);\n  }\n  function compareNodeNames(fromEl, toEl) {\n    var fromNodeName = fromEl.nodeName;\n    var toNodeName = toEl.nodeName;\n    var fromCodeStart, toCodeStart;\n    if (fromNodeName === toNodeName) {\n      return true;\n    }\n    fromCodeStart = fromNodeName.charCodeAt(0);\n    toCodeStart = toNodeName.charCodeAt(0);\n    if (fromCodeStart <= 90 && toCodeStart >= 97) {\n      return fromNodeName === toNodeName.toUpperCase();\n    } else if (toCodeStart <= 90 && fromCodeStart >= 97) {\n      return toNodeName === fromNodeName.toUpperCase();\n    } else {\n      return false;\n    }\n  }\n  function createElementNS(name, namespaceURI) {\n    return !namespaceURI || namespaceURI === NS_XHTML ? doc.createElement(name) : doc.createElementNS(namespaceURI, name);\n  }\n  function moveChildren(fromEl, toEl) {\n    var curChild = fromEl.firstChild;\n    while (curChild) {\n      var nextChild = curChild.nextSibling;\n      toEl.appendChild(curChild);\n      curChild = nextChild;\n    }\n    return toEl;\n  }\n  function syncBooleanAttrProp(fromEl, toEl, name) {\n    if (fromEl[name] !== toEl[name]) {\n      fromEl[name] = toEl[name];\n      if (fromEl[name]) {\n        fromEl.setAttribute(name, \"\");\n      } else {\n        fromEl.removeAttribute(name);\n      }\n    }\n  }\n  var specialElHandlers = {\n    OPTION: function(fromEl, toEl) {\n      var parentNode = fromEl.parentNode;\n      if (parentNode) {\n        var parentName = parentNode.nodeName.toUpperCase();\n        if (parentName === \"OPTGROUP\") {\n          parentNode = parentNode.parentNode;\n          parentName = parentNode && parentNode.nodeName.toUpperCase();\n        }\n        if (parentName === \"SELECT\" && !parentNode.hasAttribute(\"multiple\")) {\n          if (fromEl.hasAttribute(\"selected\") && !toEl.selected) {\n            fromEl.setAttribute(\"selected\", \"selected\");\n            fromEl.removeAttribute(\"selected\");\n          }\n          parentNode.selectedIndex = -1;\n        }\n      }\n      syncBooleanAttrProp(fromEl, toEl, \"selected\");\n    },\n    /**\n     * The \"value\" attribute is special for the <input> element since it sets\n     * the initial value. Changing the \"value\" attribute without changing the\n     * \"value\" property will have no effect since it is only used to the set the\n     * initial value.  Similar for the \"checked\" attribute, and \"disabled\".\n     */\n    INPUT: function(fromEl, toEl) {\n      syncBooleanAttrProp(fromEl, toEl, \"checked\");\n      syncBooleanAttrProp(fromEl, toEl, \"disabled\");\n      if (fromEl.value !== toEl.value) {\n        fromEl.value = toEl.value;\n      }\n      if (!toEl.hasAttribute(\"value\")) {\n        fromEl.removeAttribute(\"value\");\n      }\n    },\n    TEXTAREA: function(fromEl, toEl) {\n      var newValue = toEl.value;\n      if (fromEl.value !== newValue) {\n        fromEl.value = newValue;\n      }\n      var firstChild = fromEl.firstChild;\n      if (firstChild) {\n        var oldValue = firstChild.nodeValue;\n        if (oldValue == newValue || !newValue && oldValue == fromEl.placeholder) {\n          return;\n        }\n        firstChild.nodeValue = newValue;\n      }\n    },\n    SELECT: function(fromEl, toEl) {\n      if (!toEl.hasAttribute(\"multiple\")) {\n        var selectedIndex = -1;\n        var i = 0;\n        var curChild = fromEl.firstChild;\n        var optgroup;\n        var nodeName;\n        while (curChild) {\n          nodeName = curChild.nodeName && curChild.nodeName.toUpperCase();\n          if (nodeName === \"OPTGROUP\") {\n            optgroup = curChild;\n            curChild = optgroup.firstChild;\n            if (!curChild) {\n              curChild = optgroup.nextSibling;\n              optgroup = null;\n            }\n          } else {\n            if (nodeName === \"OPTION\") {\n              if (curChild.hasAttribute(\"selected\")) {\n                selectedIndex = i;\n                break;\n              }\n              i++;\n            }\n            curChild = curChild.nextSibling;\n            if (!curChild && optgroup) {\n              curChild = optgroup.nextSibling;\n              optgroup = null;\n            }\n          }\n        }\n        fromEl.selectedIndex = selectedIndex;\n      }\n    }\n  };\n  var ELEMENT_NODE = 1;\n  var DOCUMENT_FRAGMENT_NODE$1 = 11;\n  var TEXT_NODE = 3;\n  var COMMENT_NODE = 8;\n  function noop() {\n  }\n  function defaultGetNodeKey(node) {\n    if (node) {\n      return node.getAttribute && node.getAttribute(\"id\") || node.id;\n    }\n  }\n  function morphdomFactory(morphAttrs2) {\n    return function morphdom2(fromNode, toNode, options) {\n      if (!options) {\n        options = {};\n      }\n      if (typeof toNode === \"string\") {\n        if (fromNode.nodeName === \"#document\" || fromNode.nodeName === \"HTML\") {\n          var toNodeHtml = toNode;\n          toNode = doc.createElement(\"html\");\n          toNode.innerHTML = toNodeHtml;\n        } else if (fromNode.nodeName === \"BODY\") {\n          var toNodeBody = toNode;\n          toNode = doc.createElement(\"html\");\n          toNode.innerHTML = toNodeBody;\n          var bodyElement = toNode.querySelector(\"body\");\n          if (bodyElement) {\n            toNode = bodyElement;\n          }\n        } else {\n          toNode = toElement(toNode);\n        }\n      } else if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE$1) {\n        toNode = toNode.firstElementChild;\n      }\n      var getNodeKey = options.getNodeKey || defaultGetNodeKey;\n      var onBeforeNodeAdded = options.onBeforeNodeAdded || noop;\n      var onNodeAdded = options.onNodeAdded || noop;\n      var onBeforeElUpdated = options.onBeforeElUpdated || noop;\n      var onElUpdated = options.onElUpdated || noop;\n      var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop;\n      var onNodeDiscarded = options.onNodeDiscarded || noop;\n      var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop;\n      var skipFromChildren = options.skipFromChildren || noop;\n      var addChild = options.addChild || function(parent, child) {\n        return parent.appendChild(child);\n      };\n      var childrenOnly = options.childrenOnly === true;\n      var fromNodesLookup = /* @__PURE__ */ Object.create(null);\n      var keyedRemovalList = [];\n      function addKeyedRemoval(key) {\n        keyedRemovalList.push(key);\n      }\n      function walkDiscardedChildNodes(node, skipKeyedNodes) {\n        if (node.nodeType === ELEMENT_NODE) {\n          var curChild = node.firstChild;\n          while (curChild) {\n            var key = void 0;\n            if (skipKeyedNodes && (key = getNodeKey(curChild))) {\n              addKeyedRemoval(key);\n            } else {\n              onNodeDiscarded(curChild);\n              if (curChild.firstChild) {\n                walkDiscardedChildNodes(curChild, skipKeyedNodes);\n              }\n            }\n            curChild = curChild.nextSibling;\n          }\n        }\n      }\n      function removeNode(node, parentNode, skipKeyedNodes) {\n        if (onBeforeNodeDiscarded(node) === false) {\n          return;\n        }\n        if (parentNode) {\n          parentNode.removeChild(node);\n        }\n        onNodeDiscarded(node);\n        walkDiscardedChildNodes(node, skipKeyedNodes);\n      }\n      function indexTree(node) {\n        if (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE$1) {\n          var curChild = node.firstChild;\n          while (curChild) {\n            var key = getNodeKey(curChild);\n            if (key) {\n              fromNodesLookup[key] = curChild;\n            }\n            indexTree(curChild);\n            curChild = curChild.nextSibling;\n          }\n        }\n      }\n      indexTree(fromNode);\n      function handleNodeAdded(el) {\n        onNodeAdded(el);\n        var curChild = el.firstChild;\n        while (curChild) {\n          var nextSibling = curChild.nextSibling;\n          var key = getNodeKey(curChild);\n          if (key) {\n            var unmatchedFromEl = fromNodesLookup[key];\n            if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) {\n              curChild.parentNode.replaceChild(unmatchedFromEl, curChild);\n              morphEl(unmatchedFromEl, curChild);\n            } else {\n              handleNodeAdded(curChild);\n            }\n          } else {\n            handleNodeAdded(curChild);\n          }\n          curChild = nextSibling;\n        }\n      }\n      function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) {\n        while (curFromNodeChild) {\n          var fromNextSibling = curFromNodeChild.nextSibling;\n          if (curFromNodeKey = getNodeKey(curFromNodeChild)) {\n            addKeyedRemoval(curFromNodeKey);\n          } else {\n            removeNode(\n              curFromNodeChild,\n              fromEl,\n              true\n              /* skip keyed nodes */\n            );\n          }\n          curFromNodeChild = fromNextSibling;\n        }\n      }\n      function morphEl(fromEl, toEl, childrenOnly2) {\n        var toElKey = getNodeKey(toEl);\n        if (toElKey) {\n          delete fromNodesLookup[toElKey];\n        }\n        if (!childrenOnly2) {\n          var beforeUpdateResult = onBeforeElUpdated(fromEl, toEl);\n          if (beforeUpdateResult === false) {\n            return;\n          } else if (beforeUpdateResult instanceof HTMLElement) {\n            fromEl = beforeUpdateResult;\n            indexTree(fromEl);\n          }\n          morphAttrs2(fromEl, toEl);\n          onElUpdated(fromEl);\n          if (onBeforeElChildrenUpdated(fromEl, toEl) === false) {\n            return;\n          }\n        }\n        if (fromEl.nodeName !== \"TEXTAREA\") {\n          morphChildren(fromEl, toEl);\n        } else {\n          specialElHandlers.TEXTAREA(fromEl, toEl);\n        }\n      }\n      function morphChildren(fromEl, toEl) {\n        var skipFrom = skipFromChildren(fromEl, toEl);\n        var curToNodeChild = toEl.firstChild;\n        var curFromNodeChild = fromEl.firstChild;\n        var curToNodeKey;\n        var curFromNodeKey;\n        var fromNextSibling;\n        var toNextSibling;\n        var matchingFromEl;\n        outer:\n          while (curToNodeChild) {\n            toNextSibling = curToNodeChild.nextSibling;\n            curToNodeKey = getNodeKey(curToNodeChild);\n            while (!skipFrom && curFromNodeChild) {\n              fromNextSibling = curFromNodeChild.nextSibling;\n              if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) {\n                curToNodeChild = toNextSibling;\n                curFromNodeChild = fromNextSibling;\n                continue outer;\n              }\n              curFromNodeKey = getNodeKey(curFromNodeChild);\n              var curFromNodeType = curFromNodeChild.nodeType;\n              var isCompatible = void 0;\n              if (curFromNodeType === curToNodeChild.nodeType) {\n                if (curFromNodeType === ELEMENT_NODE) {\n                  if (curToNodeKey) {\n                    if (curToNodeKey !== curFromNodeKey) {\n                      if (matchingFromEl = fromNodesLookup[curToNodeKey]) {\n                        if (fromNextSibling === matchingFromEl) {\n                          isCompatible = false;\n                        } else {\n                          fromEl.insertBefore(matchingFromEl, curFromNodeChild);\n                          if (curFromNodeKey) {\n                            addKeyedRemoval(curFromNodeKey);\n                          } else {\n                            removeNode(\n                              curFromNodeChild,\n                              fromEl,\n                              true\n                              /* skip keyed nodes */\n                            );\n                          }\n                          curFromNodeChild = matchingFromEl;\n                          curFromNodeKey = getNodeKey(curFromNodeChild);\n                        }\n                      } else {\n                        isCompatible = false;\n                      }\n                    }\n                  } else if (curFromNodeKey) {\n                    isCompatible = false;\n                  }\n                  isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild);\n                  if (isCompatible) {\n                    morphEl(curFromNodeChild, curToNodeChild);\n                  }\n                } else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) {\n                  isCompatible = true;\n                  if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) {\n                    curFromNodeChild.nodeValue = curToNodeChild.nodeValue;\n                  }\n                }\n              }\n              if (isCompatible) {\n                curToNodeChild = toNextSibling;\n                curFromNodeChild = fromNextSibling;\n                continue outer;\n              }\n              if (curFromNodeKey) {\n                addKeyedRemoval(curFromNodeKey);\n              } else {\n                removeNode(\n                  curFromNodeChild,\n                  fromEl,\n                  true\n                  /* skip keyed nodes */\n                );\n              }\n              curFromNodeChild = fromNextSibling;\n            }\n            if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) {\n              if (!skipFrom) {\n                addChild(fromEl, matchingFromEl);\n              }\n              morphEl(matchingFromEl, curToNodeChild);\n            } else {\n              var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild);\n              if (onBeforeNodeAddedResult !== false) {\n                if (onBeforeNodeAddedResult) {\n                  curToNodeChild = onBeforeNodeAddedResult;\n                }\n                if (curToNodeChild.actualize) {\n                  curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc);\n                }\n                addChild(fromEl, curToNodeChild);\n                handleNodeAdded(curToNodeChild);\n              }\n            }\n            curToNodeChild = toNextSibling;\n            curFromNodeChild = fromNextSibling;\n          }\n        cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey);\n        var specialElHandler = specialElHandlers[fromEl.nodeName];\n        if (specialElHandler) {\n          specialElHandler(fromEl, toEl);\n        }\n      }\n      var morphedNode = fromNode;\n      var morphedNodeType = morphedNode.nodeType;\n      var toNodeType = toNode.nodeType;\n      if (!childrenOnly) {\n        if (morphedNodeType === ELEMENT_NODE) {\n          if (toNodeType === ELEMENT_NODE) {\n            if (!compareNodeNames(fromNode, toNode)) {\n              onNodeDiscarded(fromNode);\n              morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI));\n            }\n          } else {\n            morphedNode = toNode;\n          }\n        } else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) {\n          if (toNodeType === morphedNodeType) {\n            if (morphedNode.nodeValue !== toNode.nodeValue) {\n              morphedNode.nodeValue = toNode.nodeValue;\n            }\n            return morphedNode;\n          } else {\n            morphedNode = toNode;\n          }\n        }\n      }\n      if (morphedNode === toNode) {\n        onNodeDiscarded(fromNode);\n      } else {\n        if (toNode.isSameNode && toNode.isSameNode(morphedNode)) {\n          return;\n        }\n        morphEl(morphedNode, toNode, childrenOnly);\n        if (keyedRemovalList) {\n          for (var i = 0, len = keyedRemovalList.length; i < len; i++) {\n            var elToRemove = fromNodesLookup[keyedRemovalList[i]];\n            if (elToRemove) {\n              removeNode(elToRemove, elToRemove.parentNode, false);\n            }\n          }\n        }\n      }\n      if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) {\n        if (morphedNode.actualize) {\n          morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc);\n        }\n        fromNode.parentNode.replaceChild(morphedNode, fromNode);\n      }\n      return morphedNode;\n    };\n  }\n  var morphdom = morphdomFactory(morphAttrs);\n  var morphdom_esm_default = morphdom;\n\n  // js/phoenix_live_view/dom_patch.js\n  var DOMPatch = class {\n    constructor(view, container, id, html, streams, targetCID, opts = {}) {\n      this.view = view;\n      this.liveSocket = view.liveSocket;\n      this.container = container;\n      this.id = id;\n      this.rootID = view.root.id;\n      this.html = html;\n      this.streams = streams;\n      this.streamInserts = {};\n      this.streamComponentRestore = {};\n      this.targetCID = targetCID;\n      this.cidPatch = isCid(this.targetCID);\n      this.pendingRemoves = [];\n      this.phxRemove = this.liveSocket.binding(\"remove\");\n      this.targetContainer = this.isCIDPatch() ? this.targetCIDContainer(html) : container;\n      this.callbacks = {\n        beforeadded: [],\n        beforeupdated: [],\n        beforephxChildAdded: [],\n        afteradded: [],\n        afterupdated: [],\n        afterdiscarded: [],\n        afterphxChildAdded: [],\n        aftertransitionsDiscarded: []\n      };\n      this.withChildren = opts.withChildren || opts.undoRef || false;\n      this.undoRef = opts.undoRef;\n    }\n    before(kind, callback) {\n      this.callbacks[`before${kind}`].push(callback);\n    }\n    after(kind, callback) {\n      this.callbacks[`after${kind}`].push(callback);\n    }\n    trackBefore(kind, ...args) {\n      this.callbacks[`before${kind}`].forEach((callback) => callback(...args));\n    }\n    trackAfter(kind, ...args) {\n      this.callbacks[`after${kind}`].forEach((callback) => callback(...args));\n    }\n    markPrunableContentForRemoval() {\n      const phxUpdate = this.liveSocket.binding(PHX_UPDATE);\n      dom_default.all(\n        this.container,\n        `[${phxUpdate}=append] > *, [${phxUpdate}=prepend] > *`,\n        (el) => {\n          el.setAttribute(PHX_PRUNE, \"\");\n        }\n      );\n    }\n    perform(isJoinPatch) {\n      const { view, liveSocket, html, container } = this;\n      let targetContainer = this.targetContainer;\n      if (this.isCIDPatch() && !this.targetContainer) {\n        return;\n      }\n      if (this.isCIDPatch()) {\n        const closestLock = targetContainer.closest(`[${PHX_REF_LOCK}]`);\n        if (closestLock && !closestLock.isSameNode(targetContainer)) {\n          const clonedTree = dom_default.private(closestLock, PHX_REF_LOCK);\n          if (clonedTree) {\n            targetContainer = clonedTree.querySelector(\n              `[data-phx-component=\"${this.targetCID}\"]`\n            );\n          }\n        }\n      }\n      const focused = liveSocket.getActiveElement();\n      const { selectionStart, selectionEnd } = focused && dom_default.hasSelectionRange(focused) ? focused : {};\n      const phxUpdate = liveSocket.binding(PHX_UPDATE);\n      const phxViewportTop = liveSocket.binding(PHX_VIEWPORT_TOP);\n      const phxViewportBottom = liveSocket.binding(PHX_VIEWPORT_BOTTOM);\n      const phxTriggerExternal = liveSocket.binding(PHX_TRIGGER_ACTION);\n      const added = [];\n      const updates = [];\n      const appendPrependUpdates = [];\n      let portalCallbacks = [];\n      let externalFormTriggered = null;\n      const morph = (targetContainer2, source, withChildren = this.withChildren) => {\n        const morphCallbacks = {\n          // normally, we are running with childrenOnly, as the patch HTML for a LV\n          // does not include the LV attrs (data-phx-session, etc.)\n          // when we are patching a live component, we do want to patch the root element as well;\n          // another case is the recursive patch of a stream item that was kept on reset (-> onBeforeNodeAdded)\n          childrenOnly: targetContainer2.getAttribute(PHX_COMPONENT) === null && !withChildren,\n          getNodeKey: (node) => {\n            if (dom_default.isPhxDestroyed(node)) {\n              return null;\n            }\n            if (isJoinPatch) {\n              return node.id;\n            }\n            return node.id || node.getAttribute && node.getAttribute(PHX_MAGIC_ID);\n          },\n          // skip indexing from children when container is stream\n          skipFromChildren: (from) => {\n            return from.getAttribute(phxUpdate) === PHX_STREAM;\n          },\n          // tell morphdom how to add a child\n          addChild: (parent, child) => {\n            const { ref, streamAt } = this.getStreamInsert(child);\n            if (ref === void 0) {\n              return parent.appendChild(child);\n            }\n            this.setStreamRef(child, ref);\n            if (streamAt === 0) {\n              parent.insertAdjacentElement(\"afterbegin\", child);\n            } else if (streamAt === -1) {\n              const lastChild = parent.lastElementChild;\n              if (lastChild && !lastChild.hasAttribute(PHX_STREAM_REF)) {\n                const nonStreamChild = Array.from(parent.children).find(\n                  (c) => !c.hasAttribute(PHX_STREAM_REF)\n                );\n                parent.insertBefore(child, nonStreamChild);\n              } else {\n                parent.appendChild(child);\n              }\n            } else if (streamAt > 0) {\n              const sibling = Array.from(parent.children)[streamAt];\n              parent.insertBefore(child, sibling);\n            }\n          },\n          onBeforeNodeAdded: (el) => {\n            var _a;\n            if (((_a = this.getStreamInsert(el)) == null ? void 0 : _a.updateOnly) && !this.streamComponentRestore[el.id]) {\n              return false;\n            }\n            dom_default.maintainPrivateHooks(el, el, phxViewportTop, phxViewportBottom);\n            this.trackBefore(\"added\", el);\n            let morphedEl = el;\n            if (this.streamComponentRestore[el.id]) {\n              morphedEl = this.streamComponentRestore[el.id];\n              delete this.streamComponentRestore[el.id];\n              morph(morphedEl, el, true);\n            }\n            return morphedEl;\n          },\n          onNodeAdded: (el) => {\n            if (el.getAttribute) {\n              this.maybeReOrderStream(el, true);\n            }\n            if (dom_default.isPortalTemplate(el)) {\n              portalCallbacks.push(() => this.teleport(el, morph));\n            }\n            if (el instanceof HTMLImageElement && el.srcset) {\n              el.srcset = el.srcset;\n            } else if (el instanceof HTMLVideoElement && el.autoplay) {\n              el.play();\n            }\n            if (dom_default.isNowTriggerFormExternal(el, phxTriggerExternal)) {\n              externalFormTriggered = el;\n            }\n            if (dom_default.isPhxChild(el) && view.ownsElement(el) || dom_default.isPhxSticky(el) && view.ownsElement(el.parentNode)) {\n              this.trackAfter(\"phxChildAdded\", el);\n            }\n            if (el.nodeName === \"SCRIPT\" && el.hasAttribute(PHX_RUNTIME_HOOK)) {\n              this.handleRuntimeHook(el, source);\n            }\n            added.push(el);\n          },\n          onNodeDiscarded: (el) => this.onNodeDiscarded(el),\n          onBeforeNodeDiscarded: (el) => {\n            if (el.getAttribute && el.getAttribute(PHX_PRUNE) !== null) {\n              return true;\n            }\n            if (el.parentElement !== null && el.id && dom_default.isPhxUpdate(el.parentElement, phxUpdate, [\n              PHX_STREAM,\n              \"append\",\n              \"prepend\"\n            ])) {\n              return false;\n            }\n            if (el.getAttribute && el.getAttribute(PHX_TELEPORTED_REF)) {\n              return false;\n            }\n            if (this.maybePendingRemove(el)) {\n              return false;\n            }\n            if (this.skipCIDSibling(el)) {\n              return false;\n            }\n            if (dom_default.isPortalTemplate(el)) {\n              const teleportedEl = document.getElementById(\n                el.content.firstElementChild.id\n              );\n              if (teleportedEl) {\n                teleportedEl.remove();\n                morphCallbacks.onNodeDiscarded(teleportedEl);\n                this.view.dropPortalElementId(teleportedEl.id);\n              }\n            }\n            return true;\n          },\n          onElUpdated: (el) => {\n            if (dom_default.isNowTriggerFormExternal(el, phxTriggerExternal)) {\n              externalFormTriggered = el;\n            }\n            updates.push(el);\n            this.maybeReOrderStream(el, false);\n          },\n          onBeforeElUpdated: (fromEl, toEl) => {\n            if (fromEl.id && fromEl.isSameNode(targetContainer2) && fromEl.id !== toEl.id) {\n              morphCallbacks.onNodeDiscarded(fromEl);\n              fromEl.replaceWith(toEl);\n              return morphCallbacks.onNodeAdded(toEl);\n            }\n            dom_default.syncPendingAttrs(fromEl, toEl);\n            dom_default.maintainPrivateHooks(\n              fromEl,\n              toEl,\n              phxViewportTop,\n              phxViewportBottom\n            );\n            dom_default.cleanChildNodes(toEl, phxUpdate);\n            if (this.skipCIDSibling(toEl)) {\n              this.maybeReOrderStream(fromEl);\n              return false;\n            }\n            if (dom_default.isPhxSticky(fromEl)) {\n              [PHX_SESSION, PHX_STATIC, PHX_ROOT_ID].map((attr) => [\n                attr,\n                fromEl.getAttribute(attr),\n                toEl.getAttribute(attr)\n              ]).forEach(([attr, fromVal, toVal]) => {\n                if (toVal && fromVal !== toVal) {\n                  fromEl.setAttribute(attr, toVal);\n                }\n              });\n              return false;\n            }\n            if (dom_default.isIgnored(fromEl, phxUpdate) || fromEl.form && fromEl.form.isSameNode(externalFormTriggered)) {\n              this.trackBefore(\"updated\", fromEl, toEl);\n              dom_default.mergeAttrs(fromEl, toEl, {\n                isIgnored: dom_default.isIgnored(fromEl, phxUpdate)\n              });\n              updates.push(fromEl);\n              dom_default.applyStickyOperations(fromEl);\n              return false;\n            }\n            if (fromEl.type === \"number\" && fromEl.validity && fromEl.validity.badInput) {\n              return false;\n            }\n            const isFocusedFormEl = focused && fromEl.isSameNode(focused) && dom_default.isFormInput(fromEl);\n            const focusedSelectChanged = isFocusedFormEl && this.isChangedSelect(fromEl, toEl);\n            if (fromEl.hasAttribute(PHX_REF_SRC)) {\n              const ref = new ElementRef(fromEl);\n              if (ref.lockRef && (!this.undoRef || !ref.isLockUndoneBy(this.undoRef))) {\n                dom_default.applyStickyOperations(fromEl);\n                const isLocked = fromEl.hasAttribute(PHX_REF_LOCK);\n                const clone2 = isLocked ? dom_default.private(fromEl, PHX_REF_LOCK) || fromEl.cloneNode(true) : null;\n                if (clone2) {\n                  dom_default.putPrivate(fromEl, PHX_REF_LOCK, clone2);\n                  if (!isFocusedFormEl) {\n                    fromEl = clone2;\n                  }\n                }\n              }\n            }\n            if (dom_default.isPhxChild(toEl)) {\n              const prevSession = fromEl.getAttribute(PHX_SESSION);\n              dom_default.mergeAttrs(fromEl, toEl, { exclude: [PHX_STATIC] });\n              if (prevSession !== \"\") {\n                fromEl.setAttribute(PHX_SESSION, prevSession);\n              }\n              fromEl.setAttribute(PHX_ROOT_ID, this.rootID);\n              dom_default.applyStickyOperations(fromEl);\n              return false;\n            }\n            if (this.undoRef && dom_default.private(toEl, PHX_REF_LOCK)) {\n              dom_default.putPrivate(\n                fromEl,\n                PHX_REF_LOCK,\n                dom_default.private(toEl, PHX_REF_LOCK)\n              );\n            }\n            dom_default.copyPrivates(toEl, fromEl);\n            if (dom_default.isPortalTemplate(toEl)) {\n              portalCallbacks.push(() => this.teleport(toEl, morph));\n              fromEl.content.replaceChildren(toEl.content.cloneNode(true));\n              return false;\n            }\n            if (isFocusedFormEl && fromEl.type !== \"hidden\" && !focusedSelectChanged) {\n              this.trackBefore(\"updated\", fromEl, toEl);\n              dom_default.mergeFocusedInput(fromEl, toEl);\n              dom_default.syncAttrsToProps(fromEl);\n              updates.push(fromEl);\n              dom_default.applyStickyOperations(fromEl);\n              return false;\n            } else {\n              if (focusedSelectChanged) {\n                fromEl.blur();\n              }\n              if (dom_default.isPhxUpdate(toEl, phxUpdate, [\"append\", \"prepend\"])) {\n                appendPrependUpdates.push(\n                  new DOMPostMorphRestorer(\n                    fromEl,\n                    toEl,\n                    toEl.getAttribute(phxUpdate)\n                  )\n                );\n              }\n              dom_default.syncAttrsToProps(toEl);\n              dom_default.applyStickyOperations(toEl);\n              this.trackBefore(\"updated\", fromEl, toEl);\n              return fromEl;\n            }\n          }\n        };\n        morphdom_esm_default(targetContainer2, source, morphCallbacks);\n      };\n      this.trackBefore(\"added\", container);\n      this.trackBefore(\"updated\", container, container);\n      liveSocket.time(\"morphdom\", () => {\n        this.streams.forEach(([ref, inserts, deleteIds, reset]) => {\n          inserts.forEach(([key, streamAt, limit, updateOnly]) => {\n            this.streamInserts[key] = { ref, streamAt, limit, reset, updateOnly };\n          });\n          if (reset !== void 0) {\n            dom_default.all(document, `[${PHX_STREAM_REF}=\"${ref}\"]`, (child) => {\n              this.removeStreamChildElement(child);\n            });\n          }\n          deleteIds.forEach((id) => {\n            const child = document.getElementById(id);\n            if (child) {\n              this.removeStreamChildElement(child);\n            }\n          });\n        });\n        if (isJoinPatch) {\n          dom_default.all(this.container, `[${phxUpdate}=${PHX_STREAM}]`).filter((el) => this.view.ownsElement(el)).forEach((el) => {\n            Array.from(el.children).forEach((child) => {\n              this.removeStreamChildElement(child, true);\n            });\n          });\n        }\n        morph(targetContainer, html);\n        let teleportCount = 0;\n        while (portalCallbacks.length > 0 && teleportCount < 5) {\n          const copy = portalCallbacks.slice();\n          portalCallbacks = [];\n          copy.forEach((callback) => callback());\n          teleportCount++;\n        }\n        this.view.portalElementIds.forEach((id) => {\n          const el = document.getElementById(id);\n          if (el) {\n            const source = document.getElementById(\n              el.getAttribute(PHX_TELEPORTED_SRC)\n            );\n            if (!source) {\n              el.remove();\n              this.onNodeDiscarded(el);\n              this.view.dropPortalElementId(id);\n            }\n          }\n        });\n      });\n      if (liveSocket.isDebugEnabled()) {\n        detectDuplicateIds();\n        detectInvalidStreamInserts(this.streamInserts);\n        Array.from(document.querySelectorAll(\"input[name=id]\")).forEach(\n          (node) => {\n            if (node instanceof HTMLInputElement && node.form) {\n              console.error(\n                'Detected an input with name=\"id\" inside a form! This will cause problems when patching the DOM.\\n',\n                node\n              );\n            }\n          }\n        );\n      }\n      if (appendPrependUpdates.length > 0) {\n        liveSocket.time(\"post-morph append/prepend restoration\", () => {\n          appendPrependUpdates.forEach((update) => update.perform());\n        });\n      }\n      liveSocket.silenceEvents(\n        () => dom_default.restoreFocus(focused, selectionStart, selectionEnd)\n      );\n      dom_default.dispatchEvent(document, \"phx:update\");\n      added.forEach((el) => this.trackAfter(\"added\", el));\n      updates.forEach((el) => this.trackAfter(\"updated\", el));\n      this.transitionPendingRemoves();\n      if (externalFormTriggered) {\n        liveSocket.unload();\n        const submitter = dom_default.private(externalFormTriggered, \"submitter\");\n        if (submitter && submitter.name && targetContainer.contains(submitter)) {\n          const input = document.createElement(\"input\");\n          input.type = \"hidden\";\n          const formId = submitter.getAttribute(\"form\");\n          if (formId) {\n            input.setAttribute(\"form\", formId);\n          }\n          input.name = submitter.name;\n          input.value = submitter.value;\n          submitter.parentElement.insertBefore(input, submitter);\n        }\n        Object.getPrototypeOf(externalFormTriggered).submit.call(\n          externalFormTriggered\n        );\n      }\n      return true;\n    }\n    onNodeDiscarded(el) {\n      if (dom_default.isPhxChild(el) || dom_default.isPhxSticky(el)) {\n        this.liveSocket.destroyViewByEl(el);\n      }\n      this.trackAfter(\"discarded\", el);\n    }\n    maybePendingRemove(node) {\n      if (node.getAttribute && node.getAttribute(this.phxRemove) !== null) {\n        this.pendingRemoves.push(node);\n        return true;\n      } else {\n        return false;\n      }\n    }\n    removeStreamChildElement(child, force = false) {\n      if (!force && !this.view.ownsElement(child)) {\n        return;\n      }\n      if (this.streamInserts[child.id]) {\n        this.streamComponentRestore[child.id] = child;\n        child.remove();\n      } else {\n        if (!this.maybePendingRemove(child)) {\n          child.remove();\n          this.onNodeDiscarded(child);\n        }\n      }\n    }\n    getStreamInsert(el) {\n      const insert = el.id ? this.streamInserts[el.id] : {};\n      return insert || {};\n    }\n    setStreamRef(el, ref) {\n      dom_default.putSticky(\n        el,\n        PHX_STREAM_REF,\n        (el2) => el2.setAttribute(PHX_STREAM_REF, ref)\n      );\n    }\n    maybeReOrderStream(el, isNew) {\n      const { ref, streamAt, reset } = this.getStreamInsert(el);\n      if (streamAt === void 0) {\n        return;\n      }\n      this.setStreamRef(el, ref);\n      if (!reset && !isNew) {\n        return;\n      }\n      if (!el.parentElement) {\n        return;\n      }\n      if (streamAt === 0) {\n        el.parentElement.insertBefore(el, el.parentElement.firstElementChild);\n      } else if (streamAt > 0) {\n        const children = Array.from(el.parentElement.children);\n        const oldIndex = children.indexOf(el);\n        if (streamAt >= children.length - 1) {\n          el.parentElement.appendChild(el);\n        } else {\n          const sibling = children[streamAt];\n          if (oldIndex > streamAt) {\n            el.parentElement.insertBefore(el, sibling);\n          } else {\n            el.parentElement.insertBefore(el, sibling.nextElementSibling);\n          }\n        }\n      }\n      this.maybeLimitStream(el);\n    }\n    maybeLimitStream(el) {\n      const { limit } = this.getStreamInsert(el);\n      const children = limit !== null && Array.from(el.parentElement.children);\n      if (limit && limit < 0 && children.length > limit * -1) {\n        children.slice(0, children.length + limit).forEach((child) => this.removeStreamChildElement(child));\n      } else if (limit && limit >= 0 && children.length > limit) {\n        children.slice(limit).forEach((child) => this.removeStreamChildElement(child));\n      }\n    }\n    transitionPendingRemoves() {\n      const { pendingRemoves, liveSocket } = this;\n      if (pendingRemoves.length > 0) {\n        liveSocket.transitionRemoves(pendingRemoves, () => {\n          pendingRemoves.forEach((el) => {\n            const child = dom_default.firstPhxChild(el);\n            if (child) {\n              liveSocket.destroyViewByEl(child);\n            }\n            el.remove();\n          });\n          this.trackAfter(\"transitionsDiscarded\", pendingRemoves);\n        });\n      }\n    }\n    isChangedSelect(fromEl, toEl) {\n      if (!(fromEl instanceof HTMLSelectElement) || fromEl.multiple) {\n        return false;\n      }\n      if (fromEl.options.length !== toEl.options.length) {\n        return true;\n      }\n      toEl.value = fromEl.value;\n      return !fromEl.isEqualNode(toEl);\n    }\n    isCIDPatch() {\n      return this.cidPatch;\n    }\n    skipCIDSibling(el) {\n      return el.nodeType === Node.ELEMENT_NODE && el.hasAttribute(PHX_SKIP);\n    }\n    targetCIDContainer(html) {\n      if (!this.isCIDPatch()) {\n        return;\n      }\n      const [first, ...rest] = dom_default.findComponentNodeList(\n        this.view.id,\n        this.targetCID\n      );\n      if (rest.length === 0 && dom_default.childNodeLength(html) === 1) {\n        return first;\n      } else {\n        return first && first.parentNode;\n      }\n    }\n    indexOf(parent, child) {\n      return Array.from(parent.children).indexOf(child);\n    }\n    teleport(el, morph) {\n      const targetSelector = el.getAttribute(PHX_PORTAL);\n      const portalContainer = document.querySelector(targetSelector);\n      if (!portalContainer) {\n        throw new Error(\n          \"portal target with selector \" + targetSelector + \" not found\"\n        );\n      }\n      const toTeleport = el.content.firstElementChild;\n      if (this.skipCIDSibling(toTeleport)) {\n        return;\n      }\n      if (!(toTeleport == null ? void 0 : toTeleport.id)) {\n        throw new Error(\n          \"phx-portal template must have a single root element with ID!\"\n        );\n      }\n      const existing = document.getElementById(toTeleport.id);\n      let portalTarget;\n      if (existing) {\n        if (!portalContainer.contains(existing)) {\n          portalContainer.appendChild(existing);\n        }\n        portalTarget = existing;\n      } else {\n        portalTarget = document.createElement(toTeleport.tagName);\n        portalContainer.appendChild(portalTarget);\n      }\n      toTeleport.setAttribute(PHX_TELEPORTED_REF, this.view.id);\n      toTeleport.setAttribute(PHX_TELEPORTED_SRC, el.id);\n      morph(portalTarget, toTeleport, true);\n      toTeleport.removeAttribute(PHX_TELEPORTED_REF);\n      toTeleport.removeAttribute(PHX_TELEPORTED_SRC);\n      this.view.pushPortalElementId(toTeleport.id);\n    }\n    handleRuntimeHook(el, source) {\n      const name = el.getAttribute(PHX_RUNTIME_HOOK);\n      let nonce = el.hasAttribute(\"nonce\") ? el.getAttribute(\"nonce\") : null;\n      if (el.hasAttribute(\"nonce\")) {\n        const template = document.createElement(\"template\");\n        template.innerHTML = source;\n        nonce = template.content.querySelector(`script[${PHX_RUNTIME_HOOK}=\"${CSS.escape(name)}\"]`).getAttribute(\"nonce\");\n      }\n      const script = document.createElement(\"script\");\n      script.textContent = el.textContent;\n      dom_default.mergeAttrs(script, el, { isIgnored: false });\n      if (nonce) {\n        script.nonce = nonce;\n      }\n      el.replaceWith(script);\n      el = script;\n    }\n  };\n\n  // js/phoenix_live_view/rendered.js\n  var VOID_TAGS = /* @__PURE__ */ new Set([\n    \"area\",\n    \"base\",\n    \"br\",\n    \"col\",\n    \"command\",\n    \"embed\",\n    \"hr\",\n    \"img\",\n    \"input\",\n    \"keygen\",\n    \"link\",\n    \"meta\",\n    \"param\",\n    \"source\",\n    \"track\",\n    \"wbr\"\n  ]);\n  var quoteChars = /* @__PURE__ */ new Set([\"'\", '\"']);\n  var modifyRoot = (html, attrs, clearInnerHTML) => {\n    let i = 0;\n    let insideComment = false;\n    let beforeTag, afterTag, tag, tagNameEndsAt, id, newHTML;\n    const lookahead = html.match(/^(\\s*(?:<!--.*?-->\\s*)*)<([^\\s\\/>]+)/);\n    if (lookahead === null) {\n      throw new Error(`malformed html ${html}`);\n    }\n    i = lookahead[0].length;\n    beforeTag = lookahead[1];\n    tag = lookahead[2];\n    tagNameEndsAt = i;\n    for (i; i < html.length; i++) {\n      if (html.charAt(i) === \">\") {\n        break;\n      }\n      if (html.charAt(i) === \"=\") {\n        const isId = html.slice(i - 3, i) === \" id\";\n        i++;\n        const char = html.charAt(i);\n        if (quoteChars.has(char)) {\n          const attrStartsAt = i;\n          i++;\n          for (i; i < html.length; i++) {\n            if (html.charAt(i) === char) {\n              break;\n            }\n          }\n          if (isId) {\n            id = html.slice(attrStartsAt + 1, i);\n            break;\n          }\n        }\n      }\n    }\n    let closeAt = html.length - 1;\n    insideComment = false;\n    while (closeAt >= beforeTag.length + tag.length) {\n      const char = html.charAt(closeAt);\n      if (insideComment) {\n        if (char === \"-\" && html.slice(closeAt - 3, closeAt) === \"<!-\") {\n          insideComment = false;\n          closeAt -= 4;\n        } else {\n          closeAt -= 1;\n        }\n      } else if (char === \">\" && html.slice(closeAt - 2, closeAt) === \"--\") {\n        insideComment = true;\n        closeAt -= 3;\n      } else if (char === \">\") {\n        break;\n      } else {\n        closeAt -= 1;\n      }\n    }\n    afterTag = html.slice(closeAt + 1, html.length);\n    const attrsStr = Object.keys(attrs).map((attr) => attrs[attr] === true ? attr : `${attr}=\"${attrs[attr]}\"`).join(\" \");\n    if (clearInnerHTML) {\n      const idAttrStr = id ? ` id=\"${id}\"` : \"\";\n      if (VOID_TAGS.has(tag)) {\n        newHTML = `<${tag}${idAttrStr}${attrsStr === \"\" ? \"\" : \" \"}${attrsStr}/>`;\n      } else {\n        newHTML = `<${tag}${idAttrStr}${attrsStr === \"\" ? \"\" : \" \"}${attrsStr}></${tag}>`;\n      }\n    } else {\n      const rest = html.slice(tagNameEndsAt, closeAt + 1);\n      newHTML = `<${tag}${attrsStr === \"\" ? \"\" : \" \"}${attrsStr}${rest}`;\n    }\n    return [newHTML, beforeTag, afterTag];\n  };\n  var Rendered = class {\n    static extract(diff) {\n      const { [REPLY]: reply, [EVENTS]: events, [TITLE]: title } = diff;\n      delete diff[REPLY];\n      delete diff[EVENTS];\n      delete diff[TITLE];\n      return { diff, title, reply: reply || null, events: events || [] };\n    }\n    constructor(viewId, rendered) {\n      this.viewId = viewId;\n      this.rendered = {};\n      this.magicId = 0;\n      this.mergeDiff(rendered);\n    }\n    parentViewId() {\n      return this.viewId;\n    }\n    toString(onlyCids) {\n      const { buffer: str, streams } = this.recursiveToString(\n        this.rendered,\n        this.rendered[COMPONENTS],\n        onlyCids,\n        true,\n        {}\n      );\n      return { buffer: str, streams };\n    }\n    recursiveToString(rendered, components = rendered[COMPONENTS], onlyCids, changeTracking, rootAttrs) {\n      onlyCids = onlyCids ? new Set(onlyCids) : null;\n      const output = {\n        buffer: \"\",\n        components,\n        onlyCids,\n        streams: /* @__PURE__ */ new Set()\n      };\n      this.toOutputBuffer(rendered, null, output, changeTracking, rootAttrs);\n      return { buffer: output.buffer, streams: output.streams };\n    }\n    componentCIDs(diff) {\n      return Object.keys(diff[COMPONENTS] || {}).map((i) => parseInt(i));\n    }\n    isComponentOnlyDiff(diff) {\n      if (!diff[COMPONENTS]) {\n        return false;\n      }\n      return Object.keys(diff).length === 1;\n    }\n    getComponent(diff, cid) {\n      return diff[COMPONENTS][cid];\n    }\n    resetRender(cid) {\n      if (this.rendered[COMPONENTS][cid]) {\n        this.rendered[COMPONENTS][cid].reset = true;\n      }\n    }\n    mergeDiff(diff) {\n      const newc = diff[COMPONENTS];\n      const cache = {};\n      delete diff[COMPONENTS];\n      this.rendered = this.mutableMerge(this.rendered, diff);\n      this.rendered[COMPONENTS] = this.rendered[COMPONENTS] || {};\n      if (newc) {\n        const oldc = this.rendered[COMPONENTS];\n        for (const cid in newc) {\n          newc[cid] = this.cachedFindComponent(cid, newc[cid], oldc, newc, cache);\n        }\n        for (const cid in newc) {\n          oldc[cid] = newc[cid];\n        }\n        diff[COMPONENTS] = newc;\n      }\n    }\n    cachedFindComponent(cid, cdiff, oldc, newc, cache) {\n      if (cache[cid]) {\n        return cache[cid];\n      } else {\n        let ndiff, stat, scid = cdiff[STATIC];\n        if (isCid(scid)) {\n          let tdiff;\n          if (scid > 0) {\n            tdiff = this.cachedFindComponent(scid, newc[scid], oldc, newc, cache);\n          } else {\n            tdiff = oldc[-scid];\n          }\n          stat = tdiff[STATIC];\n          ndiff = this.cloneMerge(tdiff, cdiff, true);\n          ndiff[STATIC] = stat;\n        } else {\n          ndiff = cdiff[STATIC] !== void 0 || oldc[cid] === void 0 ? cdiff : this.cloneMerge(oldc[cid], cdiff, false);\n        }\n        cache[cid] = ndiff;\n        return ndiff;\n      }\n    }\n    mutableMerge(target, source) {\n      if (source[STATIC] !== void 0) {\n        return source;\n      } else {\n        this.doMutableMerge(target, source);\n        return target;\n      }\n    }\n    doMutableMerge(target, source) {\n      if (source[KEYED]) {\n        this.mergeKeyed(target, source);\n      } else {\n        for (const key in source) {\n          const val = source[key];\n          const targetVal = target[key];\n          const isObjVal = isObject(val);\n          if (isObjVal && val[STATIC] === void 0 && isObject(targetVal)) {\n            this.doMutableMerge(targetVal, val);\n          } else {\n            target[key] = val;\n          }\n        }\n      }\n      if (target[ROOT]) {\n        target.newRender = true;\n      }\n    }\n    clone(diff) {\n      if (\"structuredClone\" in window) {\n        return structuredClone(diff);\n      } else {\n        return JSON.parse(JSON.stringify(diff));\n      }\n    }\n    // keyed comprehensions\n    mergeKeyed(target, source) {\n      const clonedTarget = this.clone(target);\n      Object.entries(source[KEYED]).forEach(([i, entry]) => {\n        if (i === KEYED_COUNT) {\n          return;\n        }\n        if (Array.isArray(entry)) {\n          const [old_idx, diff] = entry;\n          target[KEYED][i] = clonedTarget[KEYED][old_idx];\n          this.doMutableMerge(target[KEYED][i], diff);\n        } else if (typeof entry === \"number\") {\n          const old_idx = entry;\n          target[KEYED][i] = clonedTarget[KEYED][old_idx];\n        } else if (typeof entry === \"object\") {\n          if (!target[KEYED][i]) {\n            target[KEYED][i] = {};\n          }\n          this.doMutableMerge(target[KEYED][i], entry);\n        }\n      });\n      if (source[KEYED][KEYED_COUNT] < target[KEYED][KEYED_COUNT]) {\n        for (let i = source[KEYED][KEYED_COUNT]; i < target[KEYED][KEYED_COUNT]; i++) {\n          delete target[KEYED][i];\n        }\n      }\n      target[KEYED][KEYED_COUNT] = source[KEYED][KEYED_COUNT];\n      if (source[STREAM]) {\n        target[STREAM] = source[STREAM];\n      }\n      if (source[TEMPLATES]) {\n        target[TEMPLATES] = source[TEMPLATES];\n      }\n    }\n    // Merges cid trees together, copying statics from source tree.\n    //\n    // The `pruneMagicId` is passed to control pruning the magicId of the\n    // target. We must always prune the magicId when we are sharing statics\n    // from another component. If not pruning, we replicate the logic from\n    // mutableMerge, where we set newRender to true if there is a root\n    // (effectively forcing the new version to be rendered instead of skipped)\n    //\n    cloneMerge(target, source, pruneMagicId) {\n      let merged;\n      if (source[KEYED]) {\n        merged = this.clone(target);\n        this.mergeKeyed(merged, source);\n      } else {\n        merged = __spreadValues(__spreadValues({}, target), source);\n        for (const key in merged) {\n          const val = source[key];\n          const targetVal = target[key];\n          if (isObject(val) && val[STATIC] === void 0 && isObject(targetVal)) {\n            merged[key] = this.cloneMerge(targetVal, val, pruneMagicId);\n          } else if (val === void 0 && isObject(targetVal)) {\n            merged[key] = this.cloneMerge(targetVal, {}, pruneMagicId);\n          }\n        }\n      }\n      if (pruneMagicId) {\n        delete merged.magicId;\n        delete merged.newRender;\n      } else if (target[ROOT]) {\n        merged.newRender = true;\n      }\n      return merged;\n    }\n    componentToString(cid) {\n      const { buffer: str, streams } = this.recursiveCIDToString(\n        this.rendered[COMPONENTS],\n        cid,\n        null\n      );\n      const [strippedHTML, _before, _after] = modifyRoot(str, {});\n      return { buffer: strippedHTML, streams };\n    }\n    pruneCIDs(cids) {\n      cids.forEach((cid) => delete this.rendered[COMPONENTS][cid]);\n    }\n    // private\n    get() {\n      return this.rendered;\n    }\n    isNewFingerprint(diff = {}) {\n      return !!diff[STATIC];\n    }\n    templateStatic(part, templates) {\n      if (typeof part === \"number\") {\n        return templates[part];\n      } else {\n        return part;\n      }\n    }\n    nextMagicID() {\n      this.magicId++;\n      return `m${this.magicId}-${this.parentViewId()}`;\n    }\n    // Converts rendered tree to output buffer.\n    //\n    // changeTracking controls if we can apply the PHX_SKIP optimization.\n    toOutputBuffer(rendered, templates, output, changeTracking, rootAttrs = {}) {\n      if (rendered[KEYED]) {\n        return this.comprehensionToBuffer(\n          rendered,\n          templates,\n          output,\n          changeTracking\n        );\n      }\n      if (rendered[TEMPLATES]) {\n        templates = rendered[TEMPLATES];\n        delete rendered[TEMPLATES];\n      }\n      let { [STATIC]: statics } = rendered;\n      statics = this.templateStatic(statics, templates);\n      rendered[STATIC] = statics;\n      const isRoot = rendered[ROOT];\n      const prevBuffer = output.buffer;\n      if (isRoot) {\n        output.buffer = \"\";\n      }\n      if (changeTracking && isRoot && !rendered.magicId) {\n        rendered.newRender = true;\n        rendered.magicId = this.nextMagicID();\n      }\n      output.buffer += statics[0];\n      for (let i = 1; i < statics.length; i++) {\n        this.dynamicToBuffer(rendered[i - 1], templates, output, changeTracking);\n        output.buffer += statics[i];\n      }\n      if (isRoot) {\n        let skip = false;\n        let attrs;\n        if (changeTracking || rendered.magicId) {\n          skip = changeTracking && !rendered.newRender;\n          attrs = __spreadValues({ [PHX_MAGIC_ID]: rendered.magicId }, rootAttrs);\n        } else {\n          attrs = rootAttrs;\n        }\n        if (skip) {\n          attrs[PHX_SKIP] = true;\n        }\n        const [newRoot, commentBefore, commentAfter] = modifyRoot(\n          output.buffer,\n          attrs,\n          skip\n        );\n        rendered.newRender = false;\n        output.buffer = prevBuffer + commentBefore + newRoot + commentAfter;\n      }\n    }\n    comprehensionToBuffer(rendered, templates, output, changeTracking) {\n      const keyedTemplates = templates || rendered[TEMPLATES];\n      const statics = this.templateStatic(rendered[STATIC], templates);\n      rendered[STATIC] = statics;\n      delete rendered[TEMPLATES];\n      for (let i = 0; i < rendered[KEYED][KEYED_COUNT]; i++) {\n        output.buffer += statics[0];\n        for (let j = 1; j < statics.length; j++) {\n          this.dynamicToBuffer(\n            rendered[KEYED][i][j - 1],\n            keyedTemplates,\n            output,\n            changeTracking\n          );\n          output.buffer += statics[j];\n        }\n      }\n      if (rendered[STREAM]) {\n        const stream = rendered[STREAM];\n        const [_ref, _inserts, deleteIds, reset] = stream || [null, {}, [], null];\n        if (stream !== void 0 && (rendered[KEYED][KEYED_COUNT] > 0 || deleteIds.length > 0 || reset)) {\n          delete rendered[STREAM];\n          rendered[KEYED] = {\n            [KEYED_COUNT]: 0\n          };\n          output.streams.add(stream);\n        }\n      }\n    }\n    dynamicToBuffer(rendered, templates, output, changeTracking) {\n      if (typeof rendered === \"number\") {\n        const { buffer: str, streams } = this.recursiveCIDToString(\n          output.components,\n          rendered,\n          output.onlyCids\n        );\n        output.buffer += str;\n        output.streams = /* @__PURE__ */ new Set([...output.streams, ...streams]);\n      } else if (isObject(rendered)) {\n        this.toOutputBuffer(rendered, templates, output, changeTracking, {});\n      } else {\n        output.buffer += rendered;\n      }\n    }\n    recursiveCIDToString(components, cid, onlyCids) {\n      const component = components[cid] || logError(`no component for CID ${cid}`, components);\n      const attrs = { [PHX_COMPONENT]: cid, [PHX_VIEW_REF]: this.viewId };\n      const skip = onlyCids && !onlyCids.has(cid);\n      component.newRender = !skip;\n      component.magicId = `c${cid}-${this.parentViewId()}`;\n      const changeTracking = !component.reset;\n      const { buffer: html, streams } = this.recursiveToString(\n        component,\n        components,\n        onlyCids,\n        changeTracking,\n        attrs\n      );\n      delete component.reset;\n      return { buffer: html, streams };\n    }\n  };\n\n  // js/phoenix_live_view/js.js\n  var focusStack = [];\n  var default_transition_time = 200;\n  var JS = {\n    // private\n    exec(e, eventType, phxEvent, view, sourceEl, defaults) {\n      const [defaultKind, defaultArgs] = defaults || [\n        null,\n        { callback: defaults && defaults.callback }\n      ];\n      const commands = Array.isArray(phxEvent) ? phxEvent : typeof phxEvent === \"string\" && phxEvent.startsWith(\"[\") ? JSON.parse(phxEvent) : [[defaultKind, defaultArgs]];\n      commands.forEach(([kind, args]) => {\n        if (kind === defaultKind) {\n          args = __spreadValues(__spreadValues({}, defaultArgs), args);\n          args.callback = args.callback || defaultArgs.callback;\n        }\n        this.filterToEls(view.liveSocket, sourceEl, args).forEach((el) => {\n          this[`exec_${kind}`](e, eventType, phxEvent, view, sourceEl, el, args);\n        });\n      });\n    },\n    isVisible(el) {\n      return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length > 0);\n    },\n    // returns true if any part of the element is inside the viewport\n    isInViewport(el) {\n      const rect = el.getBoundingClientRect();\n      const windowHeight = window.innerHeight || document.documentElement.clientHeight;\n      const windowWidth = window.innerWidth || document.documentElement.clientWidth;\n      return rect.right > 0 && rect.bottom > 0 && rect.left < windowWidth && rect.top < windowHeight;\n    },\n    // private\n    // commands\n    exec_exec(e, eventType, phxEvent, view, sourceEl, el, { attr, to }) {\n      const encodedJS = el.getAttribute(attr);\n      if (!encodedJS) {\n        throw new Error(`expected ${attr} to contain JS command on \"${to}\"`);\n      }\n      view.liveSocket.execJS(el, encodedJS, eventType);\n    },\n    exec_dispatch(e, eventType, phxEvent, view, sourceEl, el, { event, detail, bubbles, blocking }) {\n      detail = detail || {};\n      detail.dispatcher = sourceEl;\n      if (blocking) {\n        const promise = new Promise((resolve, _reject) => {\n          detail.done = resolve;\n        });\n        view.liveSocket.asyncTransition(promise);\n      }\n      dom_default.dispatchEvent(el, event, { detail, bubbles });\n    },\n    exec_push(e, eventType, phxEvent, view, sourceEl, el, args) {\n      const {\n        event,\n        data,\n        target,\n        page_loading,\n        loading,\n        value,\n        dispatcher,\n        callback\n      } = args;\n      const pushOpts = {\n        loading,\n        value,\n        target,\n        page_loading: !!page_loading,\n        originalEvent: e\n      };\n      const targetSrc = eventType === \"change\" && dispatcher ? dispatcher : sourceEl;\n      const phxTarget = target || targetSrc.getAttribute(view.binding(\"target\")) || targetSrc;\n      const handler = (targetView, targetCtx) => {\n        if (!targetView.isConnected()) {\n          return;\n        }\n        if (eventType === \"change\") {\n          let { newCid, _target } = args;\n          _target = _target || (dom_default.isFormInput(sourceEl) ? sourceEl.name : void 0);\n          if (_target) {\n            pushOpts._target = _target;\n          }\n          targetView.pushInput(\n            sourceEl,\n            targetCtx,\n            newCid,\n            event || phxEvent,\n            pushOpts,\n            callback\n          );\n        } else if (eventType === \"submit\") {\n          const { submitter } = args;\n          targetView.submitForm(\n            sourceEl,\n            targetCtx,\n            event || phxEvent,\n            submitter,\n            pushOpts,\n            callback\n          );\n        } else {\n          targetView.pushEvent(\n            eventType,\n            sourceEl,\n            targetCtx,\n            event || phxEvent,\n            data,\n            pushOpts,\n            callback\n          );\n        }\n      };\n      if (args.targetView && args.targetCtx) {\n        handler(args.targetView, args.targetCtx);\n      } else {\n        view.withinTargets(phxTarget, handler);\n      }\n    },\n    exec_navigate(e, eventType, phxEvent, view, sourceEl, el, { href, replace }) {\n      view.liveSocket.historyRedirect(\n        e,\n        href,\n        replace ? \"replace\" : \"push\",\n        null,\n        sourceEl\n      );\n    },\n    exec_patch(e, eventType, phxEvent, view, sourceEl, el, { href, replace }) {\n      view.liveSocket.pushHistoryPatch(\n        e,\n        href,\n        replace ? \"replace\" : \"push\",\n        sourceEl\n      );\n    },\n    exec_focus(e, eventType, phxEvent, view, sourceEl, el) {\n      aria_default.attemptFocus(el);\n      window.requestAnimationFrame(() => {\n        window.requestAnimationFrame(() => aria_default.attemptFocus(el));\n      });\n    },\n    exec_focus_first(e, eventType, phxEvent, view, sourceEl, el) {\n      aria_default.focusFirstInteractive(el) || aria_default.focusFirst(el);\n      window.requestAnimationFrame(() => {\n        window.requestAnimationFrame(\n          () => aria_default.focusFirstInteractive(el) || aria_default.focusFirst(el)\n        );\n      });\n    },\n    exec_push_focus(e, eventType, phxEvent, view, sourceEl, el) {\n      focusStack.push(el || sourceEl);\n    },\n    exec_pop_focus(_e, _eventType, _phxEvent, _view, _sourceEl, _el) {\n      const el = focusStack.pop();\n      if (el) {\n        el.focus();\n        window.requestAnimationFrame(() => {\n          window.requestAnimationFrame(() => el.focus());\n        });\n      }\n    },\n    exec_add_class(e, eventType, phxEvent, view, sourceEl, el, { names, transition, time, blocking }) {\n      this.addOrRemoveClasses(el, names, [], transition, time, view, blocking);\n    },\n    exec_remove_class(e, eventType, phxEvent, view, sourceEl, el, { names, transition, time, blocking }) {\n      this.addOrRemoveClasses(el, [], names, transition, time, view, blocking);\n    },\n    exec_toggle_class(e, eventType, phxEvent, view, sourceEl, el, { names, transition, time, blocking }) {\n      this.toggleClasses(el, names, transition, time, view, blocking);\n    },\n    exec_toggle_attr(e, eventType, phxEvent, view, sourceEl, el, { attr: [attr, val1, val2] }) {\n      this.toggleAttr(el, attr, val1, val2);\n    },\n    exec_ignore_attrs(e, eventType, phxEvent, view, sourceEl, el, { attrs }) {\n      this.ignoreAttrs(el, attrs);\n    },\n    exec_transition(e, eventType, phxEvent, view, sourceEl, el, { time, transition, blocking }) {\n      this.addOrRemoveClasses(el, [], [], transition, time, view, blocking);\n    },\n    exec_toggle(e, eventType, phxEvent, view, sourceEl, el, { display, ins, outs, time, blocking }) {\n      this.toggle(eventType, view, el, display, ins, outs, time, blocking);\n    },\n    exec_show(e, eventType, phxEvent, view, sourceEl, el, { display, transition, time, blocking }) {\n      this.show(eventType, view, el, display, transition, time, blocking);\n    },\n    exec_hide(e, eventType, phxEvent, view, sourceEl, el, { display, transition, time, blocking }) {\n      this.hide(eventType, view, el, display, transition, time, blocking);\n    },\n    exec_set_attr(e, eventType, phxEvent, view, sourceEl, el, { attr: [attr, val] }) {\n      this.setOrRemoveAttrs(el, [[attr, val]], []);\n    },\n    exec_remove_attr(e, eventType, phxEvent, view, sourceEl, el, { attr }) {\n      this.setOrRemoveAttrs(el, [], [attr]);\n    },\n    ignoreAttrs(el, attrs) {\n      dom_default.putPrivate(el, \"JS:ignore_attrs\", {\n        apply: (fromEl, toEl) => {\n          let fromAttributes = Array.from(fromEl.attributes);\n          let fromAttributeNames = fromAttributes.map((attr) => attr.name);\n          Array.from(toEl.attributes).filter((attr) => {\n            return !fromAttributeNames.includes(attr.name);\n          }).forEach((attr) => {\n            if (dom_default.attributeIgnored(attr, attrs)) {\n              toEl.removeAttribute(attr.name);\n            }\n          });\n          fromAttributes.forEach((attr) => {\n            if (dom_default.attributeIgnored(attr, attrs)) {\n              toEl.setAttribute(attr.name, attr.value);\n            }\n          });\n        }\n      });\n    },\n    onBeforeElUpdated(fromEl, toEl) {\n      const ignoreAttrs = dom_default.private(fromEl, \"JS:ignore_attrs\");\n      if (ignoreAttrs) {\n        ignoreAttrs.apply(fromEl, toEl);\n      }\n    },\n    // utils for commands\n    show(eventType, view, el, display, transition, time, blocking) {\n      if (!this.isVisible(el)) {\n        this.toggle(\n          eventType,\n          view,\n          el,\n          display,\n          transition,\n          null,\n          time,\n          blocking\n        );\n      }\n    },\n    hide(eventType, view, el, display, transition, time, blocking) {\n      if (this.isVisible(el)) {\n        this.toggle(\n          eventType,\n          view,\n          el,\n          display,\n          null,\n          transition,\n          time,\n          blocking\n        );\n      }\n    },\n    toggle(eventType, view, el, display, ins, outs, time, blocking) {\n      time = time || default_transition_time;\n      const [inClasses, inStartClasses, inEndClasses] = ins || [[], [], []];\n      const [outClasses, outStartClasses, outEndClasses] = outs || [[], [], []];\n      if (inClasses.length > 0 || outClasses.length > 0) {\n        if (this.isVisible(el)) {\n          const onStart = () => {\n            this.addOrRemoveClasses(\n              el,\n              outStartClasses,\n              inClasses.concat(inStartClasses).concat(inEndClasses)\n            );\n            window.requestAnimationFrame(() => {\n              this.addOrRemoveClasses(el, outClasses, []);\n              window.requestAnimationFrame(\n                () => this.addOrRemoveClasses(el, outEndClasses, outStartClasses)\n              );\n            });\n          };\n          const onEnd = () => {\n            this.addOrRemoveClasses(el, [], outClasses.concat(outEndClasses));\n            dom_default.putSticky(\n              el,\n              \"toggle\",\n              (currentEl) => currentEl.style.display = \"none\"\n            );\n            el.dispatchEvent(new Event(\"phx:hide-end\"));\n          };\n          el.dispatchEvent(new Event(\"phx:hide-start\"));\n          if (blocking === false) {\n            onStart();\n            setTimeout(onEnd, time);\n          } else {\n            view.transition(time, onStart, onEnd);\n          }\n        } else {\n          if (eventType === \"remove\") {\n            return;\n          }\n          const onStart = () => {\n            this.addOrRemoveClasses(\n              el,\n              inStartClasses,\n              outClasses.concat(outStartClasses).concat(outEndClasses)\n            );\n            const stickyDisplay = display || this.defaultDisplay(el);\n            window.requestAnimationFrame(() => {\n              this.addOrRemoveClasses(el, inClasses, []);\n              window.requestAnimationFrame(() => {\n                dom_default.putSticky(\n                  el,\n                  \"toggle\",\n                  (currentEl) => currentEl.style.display = stickyDisplay\n                );\n                this.addOrRemoveClasses(el, inEndClasses, inStartClasses);\n              });\n            });\n          };\n          const onEnd = () => {\n            this.addOrRemoveClasses(el, [], inClasses.concat(inEndClasses));\n            el.dispatchEvent(new Event(\"phx:show-end\"));\n          };\n          el.dispatchEvent(new Event(\"phx:show-start\"));\n          if (blocking === false) {\n            onStart();\n            setTimeout(onEnd, time);\n          } else {\n            view.transition(time, onStart, onEnd);\n          }\n        }\n      } else {\n        if (this.isVisible(el)) {\n          window.requestAnimationFrame(() => {\n            el.dispatchEvent(new Event(\"phx:hide-start\"));\n            dom_default.putSticky(\n              el,\n              \"toggle\",\n              (currentEl) => currentEl.style.display = \"none\"\n            );\n            el.dispatchEvent(new Event(\"phx:hide-end\"));\n          });\n        } else {\n          window.requestAnimationFrame(() => {\n            el.dispatchEvent(new Event(\"phx:show-start\"));\n            const stickyDisplay = display || this.defaultDisplay(el);\n            dom_default.putSticky(\n              el,\n              \"toggle\",\n              (currentEl) => currentEl.style.display = stickyDisplay\n            );\n            el.dispatchEvent(new Event(\"phx:show-end\"));\n          });\n        }\n      }\n    },\n    toggleClasses(el, classes, transition, time, view, blocking) {\n      window.requestAnimationFrame(() => {\n        const [prevAdds, prevRemoves] = dom_default.getSticky(el, \"classes\", [[], []]);\n        const newAdds = classes.filter(\n          (name) => prevAdds.indexOf(name) < 0 && !el.classList.contains(name)\n        );\n        const newRemoves = classes.filter(\n          (name) => prevRemoves.indexOf(name) < 0 && el.classList.contains(name)\n        );\n        this.addOrRemoveClasses(\n          el,\n          newAdds,\n          newRemoves,\n          transition,\n          time,\n          view,\n          blocking\n        );\n      });\n    },\n    toggleAttr(el, attr, val1, val2) {\n      if (el.hasAttribute(attr)) {\n        if (val2 !== void 0) {\n          if (el.getAttribute(attr) === val1) {\n            this.setOrRemoveAttrs(el, [[attr, val2]], []);\n          } else {\n            this.setOrRemoveAttrs(el, [[attr, val1]], []);\n          }\n        } else {\n          this.setOrRemoveAttrs(el, [], [attr]);\n        }\n      } else {\n        this.setOrRemoveAttrs(el, [[attr, val1]], []);\n      }\n    },\n    addOrRemoveClasses(el, adds, removes, transition, time, view, blocking) {\n      time = time || default_transition_time;\n      const [transitionRun, transitionStart, transitionEnd] = transition || [\n        [],\n        [],\n        []\n      ];\n      if (transitionRun.length > 0) {\n        const onStart = () => {\n          this.addOrRemoveClasses(\n            el,\n            transitionStart,\n            [].concat(transitionRun).concat(transitionEnd)\n          );\n          window.requestAnimationFrame(() => {\n            this.addOrRemoveClasses(el, transitionRun, []);\n            window.requestAnimationFrame(\n              () => this.addOrRemoveClasses(el, transitionEnd, transitionStart)\n            );\n          });\n        };\n        const onDone = () => this.addOrRemoveClasses(\n          el,\n          adds.concat(transitionEnd),\n          removes.concat(transitionRun).concat(transitionStart)\n        );\n        if (blocking === false) {\n          onStart();\n          setTimeout(onDone, time);\n        } else {\n          view.transition(time, onStart, onDone);\n        }\n        return;\n      }\n      window.requestAnimationFrame(() => {\n        const [prevAdds, prevRemoves] = dom_default.getSticky(el, \"classes\", [[], []]);\n        const keepAdds = adds.filter(\n          (name) => prevAdds.indexOf(name) < 0 && !el.classList.contains(name)\n        );\n        const keepRemoves = removes.filter(\n          (name) => prevRemoves.indexOf(name) < 0 && el.classList.contains(name)\n        );\n        const newAdds = prevAdds.filter((name) => removes.indexOf(name) < 0).concat(keepAdds);\n        const newRemoves = prevRemoves.filter((name) => adds.indexOf(name) < 0).concat(keepRemoves);\n        dom_default.putSticky(el, \"classes\", (currentEl) => {\n          currentEl.classList.remove(...newRemoves);\n          currentEl.classList.add(...newAdds);\n          return [newAdds, newRemoves];\n        });\n      });\n    },\n    setOrRemoveAttrs(el, sets, removes) {\n      const [prevSets, prevRemoves] = dom_default.getSticky(el, \"attrs\", [[], []]);\n      const alteredAttrs = sets.map(([attr, _val]) => attr).concat(removes);\n      const newSets = prevSets.filter(([attr, _val]) => !alteredAttrs.includes(attr)).concat(sets);\n      const newRemoves = prevRemoves.filter((attr) => !alteredAttrs.includes(attr)).concat(removes);\n      dom_default.putSticky(el, \"attrs\", (currentEl) => {\n        newRemoves.forEach((attr) => currentEl.removeAttribute(attr));\n        newSets.forEach(([attr, val]) => currentEl.setAttribute(attr, val));\n        return [newSets, newRemoves];\n      });\n    },\n    hasAllClasses(el, classes) {\n      return classes.every((name) => el.classList.contains(name));\n    },\n    isToggledOut(el, outClasses) {\n      return !this.isVisible(el) || this.hasAllClasses(el, outClasses);\n    },\n    filterToEls(liveSocket, sourceEl, { to }) {\n      const defaultQuery = () => {\n        if (typeof to === \"string\") {\n          return document.querySelectorAll(to);\n        } else if (to.closest) {\n          const toEl = sourceEl.closest(to.closest);\n          return toEl ? [toEl] : [];\n        } else if (to.inner) {\n          return sourceEl.querySelectorAll(to.inner);\n        }\n      };\n      return to ? liveSocket.jsQuerySelectorAll(sourceEl, to, defaultQuery) : [sourceEl];\n    },\n    defaultDisplay(el) {\n      return { tr: \"table-row\", td: \"table-cell\" }[el.tagName.toLowerCase()] || \"block\";\n    },\n    transitionClasses(val) {\n      if (!val) {\n        return null;\n      }\n      let [trans, tStart, tEnd] = Array.isArray(val) ? val : [val.split(\" \"), [], []];\n      trans = Array.isArray(trans) ? trans : trans.split(\" \");\n      tStart = Array.isArray(tStart) ? tStart : tStart.split(\" \");\n      tEnd = Array.isArray(tEnd) ? tEnd : tEnd.split(\" \");\n      return [trans, tStart, tEnd];\n    }\n  };\n  var js_default = JS;\n\n  // js/phoenix_live_view/js_commands.ts\n  var js_commands_default = (liveSocket, eventType) => {\n    return {\n      exec(el, encodedJS) {\n        liveSocket.execJS(el, encodedJS, eventType);\n      },\n      show(el, opts = {}) {\n        const owner = liveSocket.owner(el);\n        js_default.show(\n          eventType,\n          owner,\n          el,\n          opts.display,\n          js_default.transitionClasses(opts.transition),\n          opts.time,\n          opts.blocking\n        );\n      },\n      hide(el, opts = {}) {\n        const owner = liveSocket.owner(el);\n        js_default.hide(\n          eventType,\n          owner,\n          el,\n          null,\n          js_default.transitionClasses(opts.transition),\n          opts.time,\n          opts.blocking\n        );\n      },\n      toggle(el, opts = {}) {\n        const owner = liveSocket.owner(el);\n        const inTransition = js_default.transitionClasses(opts.in);\n        const outTransition = js_default.transitionClasses(opts.out);\n        js_default.toggle(\n          eventType,\n          owner,\n          el,\n          opts.display,\n          inTransition,\n          outTransition,\n          opts.time,\n          opts.blocking\n        );\n      },\n      addClass(el, names, opts = {}) {\n        const classNames = Array.isArray(names) ? names : names.split(\" \");\n        const owner = liveSocket.owner(el);\n        js_default.addOrRemoveClasses(\n          el,\n          classNames,\n          [],\n          js_default.transitionClasses(opts.transition),\n          opts.time,\n          owner,\n          opts.blocking\n        );\n      },\n      removeClass(el, names, opts = {}) {\n        const classNames = Array.isArray(names) ? names : names.split(\" \");\n        const owner = liveSocket.owner(el);\n        js_default.addOrRemoveClasses(\n          el,\n          [],\n          classNames,\n          js_default.transitionClasses(opts.transition),\n          opts.time,\n          owner,\n          opts.blocking\n        );\n      },\n      toggleClass(el, names, opts = {}) {\n        const classNames = Array.isArray(names) ? names : names.split(\" \");\n        const owner = liveSocket.owner(el);\n        js_default.toggleClasses(\n          el,\n          classNames,\n          js_default.transitionClasses(opts.transition),\n          opts.time,\n          owner,\n          opts.blocking\n        );\n      },\n      transition(el, transition, opts = {}) {\n        const owner = liveSocket.owner(el);\n        js_default.addOrRemoveClasses(\n          el,\n          [],\n          [],\n          js_default.transitionClasses(transition),\n          opts.time,\n          owner,\n          opts.blocking\n        );\n      },\n      setAttribute(el, attr, val) {\n        js_default.setOrRemoveAttrs(el, [[attr, val]], []);\n      },\n      removeAttribute(el, attr) {\n        js_default.setOrRemoveAttrs(el, [], [attr]);\n      },\n      toggleAttribute(el, attr, val1, val2) {\n        js_default.toggleAttr(el, attr, val1, val2);\n      },\n      push(el, type, opts = {}) {\n        liveSocket.withinOwners(el, (view) => {\n          const data = opts.value || {};\n          delete opts.value;\n          let e = new CustomEvent(\"phx:exec\", { detail: { sourceElement: el } });\n          js_default.exec(e, eventType, type, view, el, [\"push\", __spreadValues({ data }, opts)]);\n        });\n      },\n      navigate(href, opts = {}) {\n        const customEvent = new CustomEvent(\"phx:exec\");\n        liveSocket.historyRedirect(\n          customEvent,\n          href,\n          opts.replace ? \"replace\" : \"push\",\n          null,\n          null\n        );\n      },\n      patch(href, opts = {}) {\n        const customEvent = new CustomEvent(\"phx:exec\");\n        liveSocket.pushHistoryPatch(\n          customEvent,\n          href,\n          opts.replace ? \"replace\" : \"push\",\n          null\n        );\n      },\n      ignoreAttributes(el, attrs) {\n        js_default.ignoreAttrs(el, Array.isArray(attrs) ? attrs : [attrs]);\n      }\n    };\n  };\n\n  // js/phoenix_live_view/view_hook.ts\n  var HOOK_ID = \"hookId\";\n  var DEAD_HOOK = \"deadHook\";\n  var viewHookID = 1;\n  var ViewHook = class _ViewHook {\n    get liveSocket() {\n      return this.__liveSocket();\n    }\n    static makeID() {\n      return viewHookID++;\n    }\n    static elementID(el) {\n      return dom_default.private(el, HOOK_ID);\n    }\n    static deadHook(el) {\n      return dom_default.private(el, DEAD_HOOK) === true;\n    }\n    constructor(view, el, callbacks) {\n      this.el = el;\n      this.__attachView(view);\n      this.__listeners = /* @__PURE__ */ new Set();\n      this.__isDisconnected = false;\n      dom_default.putPrivate(this.el, HOOK_ID, _ViewHook.makeID());\n      if (view && view.isDead) {\n        dom_default.putPrivate(this.el, DEAD_HOOK, true);\n      }\n      if (callbacks) {\n        const protectedProps = /* @__PURE__ */ new Set([\n          \"el\",\n          \"liveSocket\",\n          \"__view\",\n          \"__listeners\",\n          \"__isDisconnected\",\n          \"constructor\",\n          // Standard object properties\n          // Core ViewHook API methods\n          \"js\",\n          \"pushEvent\",\n          \"pushEventTo\",\n          \"handleEvent\",\n          \"removeHandleEvent\",\n          \"upload\",\n          \"uploadTo\",\n          // Internal lifecycle callers\n          \"__mounted\",\n          \"__updated\",\n          \"__beforeUpdate\",\n          \"__destroyed\",\n          \"__reconnected\",\n          \"__disconnected\",\n          \"__cleanup__\"\n        ]);\n        for (const key in callbacks) {\n          if (Object.prototype.hasOwnProperty.call(callbacks, key)) {\n            this[key] = callbacks[key];\n            if (protectedProps.has(key)) {\n              console.warn(\n                `Hook object for element #${el.id} overwrites core property '${key}'!`\n              );\n            }\n          }\n        }\n        const lifecycleMethods = [\n          \"mounted\",\n          \"beforeUpdate\",\n          \"updated\",\n          \"destroyed\",\n          \"disconnected\",\n          \"reconnected\"\n        ];\n        lifecycleMethods.forEach((methodName) => {\n          if (callbacks[methodName] && typeof callbacks[methodName] === \"function\") {\n            this[methodName] = callbacks[methodName];\n          }\n        });\n      }\n    }\n    /** @internal */\n    __attachView(view) {\n      if (view) {\n        this.__view = () => view;\n        this.__liveSocket = () => view.liveSocket;\n      } else {\n        this.__view = () => {\n          throw new Error(\n            `hook not yet attached to a live view: ${this.el.outerHTML}`\n          );\n        };\n        this.__liveSocket = () => {\n          throw new Error(\n            `hook not yet attached to a live view: ${this.el.outerHTML}`\n          );\n        };\n      }\n    }\n    // Default lifecycle methods\n    mounted() {\n    }\n    beforeUpdate() {\n    }\n    updated() {\n    }\n    destroyed() {\n    }\n    disconnected() {\n    }\n    reconnected() {\n    }\n    // Internal lifecycle callers - called by the View\n    /** @internal */\n    __mounted() {\n      this.mounted();\n    }\n    /** @internal */\n    __updated() {\n      this.updated();\n    }\n    /** @internal */\n    __beforeUpdate() {\n      this.beforeUpdate();\n    }\n    /** @internal */\n    __destroyed() {\n      this.destroyed();\n      dom_default.deletePrivate(this.el, HOOK_ID);\n    }\n    /** @internal */\n    __reconnected() {\n      if (this.__isDisconnected) {\n        this.__isDisconnected = false;\n        this.reconnected();\n      }\n    }\n    /** @internal */\n    __disconnected() {\n      this.__isDisconnected = true;\n      this.disconnected();\n    }\n    js() {\n      return __spreadProps(__spreadValues({}, js_commands_default(this.__view().liveSocket, \"hook\")), {\n        exec: (encodedJS) => {\n          this.__view().liveSocket.execJS(this.el, encodedJS, \"hook\");\n        }\n      });\n    }\n    pushEvent(event, payload, onReply) {\n      const promise = this.__view().pushHookEvent(\n        this.el,\n        null,\n        event,\n        payload || {}\n      );\n      if (onReply === void 0) {\n        return promise.then(({ reply }) => reply);\n      }\n      promise.then(\n        ({ reply, ref }) => onReply(reply, ref)\n      ).catch(() => {\n      });\n    }\n    pushEventTo(selectorOrTarget, event, payload, onReply) {\n      if (onReply === void 0) {\n        const targetPair = [];\n        this.__view().withinTargets(\n          selectorOrTarget,\n          (view, targetCtx) => {\n            targetPair.push({ view, targetCtx });\n          }\n        );\n        const promises = targetPair.map(({ view, targetCtx }) => {\n          return view.pushHookEvent(this.el, targetCtx, event, payload || {});\n        });\n        return Promise.allSettled(promises);\n      }\n      this.__view().withinTargets(\n        selectorOrTarget,\n        (view, targetCtx) => {\n          view.pushHookEvent(this.el, targetCtx, event, payload || {}).then(\n            ({ reply, ref }) => onReply(reply, ref)\n          ).catch(() => {\n          });\n        }\n      );\n    }\n    handleEvent(event, callback) {\n      const callbackRef = {\n        event,\n        callback: (customEvent) => callback(customEvent.detail)\n      };\n      window.addEventListener(\n        `phx:${event}`,\n        callbackRef.callback\n      );\n      this.__listeners.add(callbackRef);\n      return callbackRef;\n    }\n    removeHandleEvent(ref) {\n      window.removeEventListener(\n        `phx:${ref.event}`,\n        ref.callback\n      );\n      this.__listeners.delete(ref);\n    }\n    upload(name, files) {\n      return this.__view().dispatchUploads(null, name, files);\n    }\n    uploadTo(selectorOrTarget, name, files) {\n      return this.__view().withinTargets(\n        selectorOrTarget,\n        (view, targetCtx) => {\n          view.dispatchUploads(targetCtx, name, files);\n        }\n      );\n    }\n    /** @internal */\n    __cleanup__() {\n      this.__listeners.forEach(\n        (callbackRef) => this.removeHandleEvent(callbackRef)\n      );\n    }\n  };\n\n  // js/phoenix_live_view/view.js\n  var prependFormDataKey = (key, prefix) => {\n    const isArray = key.endsWith(\"[]\");\n    let baseKey = isArray ? key.slice(0, -2) : key;\n    baseKey = baseKey.replace(/([^\\[\\]]+)(\\]?$)/, `${prefix}$1$2`);\n    if (isArray) {\n      baseKey += \"[]\";\n    }\n    return baseKey;\n  };\n  var View = class _View {\n    static closestView(el) {\n      const liveViewEl = el.closest(PHX_VIEW_SELECTOR);\n      return liveViewEl ? dom_default.private(liveViewEl, \"view\") : null;\n    }\n    constructor(el, liveSocket, parentView, flash, liveReferer) {\n      this.isDead = false;\n      this.liveSocket = liveSocket;\n      this.flash = flash;\n      this.parent = parentView;\n      this.root = parentView ? parentView.root : this;\n      this.el = el;\n      const boundView = dom_default.private(this.el, \"view\");\n      if (boundView !== void 0 && boundView.isDead !== true) {\n        logError(\n          `The DOM element for this view has already been bound to a view.\n\n        An element can only ever be associated with a single view!\n        Please ensure that you are not trying to initialize multiple LiveSockets on the same page.\n        This could happen if you're accidentally trying to render your root layout more than once.\n        Ensure that the template set on the LiveView is different than the root layout.\n      `,\n          { view: boundView }\n        );\n        throw new Error(\"Cannot bind multiple views to the same DOM element.\");\n      }\n      dom_default.putPrivate(this.el, \"view\", this);\n      this.id = this.el.id;\n      this.ref = 0;\n      this.lastAckRef = null;\n      this.childJoins = 0;\n      this.loaderTimer = null;\n      this.disconnectedTimer = null;\n      this.pendingDiffs = [];\n      this.pendingForms = /* @__PURE__ */ new Set();\n      this.redirect = false;\n      this.href = null;\n      this.joinCount = this.parent ? this.parent.joinCount - 1 : 0;\n      this.joinAttempts = 0;\n      this.joinPending = true;\n      this.destroyed = false;\n      this.joinCallback = function(onDone) {\n        onDone && onDone();\n      };\n      this.stopCallback = function() {\n      };\n      this.pendingJoinOps = [];\n      this.viewHooks = {};\n      this.formSubmits = [];\n      this.children = this.parent ? null : {};\n      this.root.children[this.id] = {};\n      this.formsForRecovery = {};\n      this.channel = this.liveSocket.channel(`lv:${this.id}`, () => {\n        const url = this.href && this.expandURL(this.href);\n        return {\n          redirect: this.redirect ? url : void 0,\n          url: this.redirect ? void 0 : url || void 0,\n          params: this.connectParams(liveReferer),\n          session: this.getSession(),\n          static: this.getStatic(),\n          flash: this.flash,\n          sticky: this.el.hasAttribute(PHX_STICKY)\n        };\n      });\n      this.portalElementIds = /* @__PURE__ */ new Set();\n    }\n    setHref(href) {\n      this.href = href;\n    }\n    setRedirect(href) {\n      this.redirect = true;\n      this.href = href;\n    }\n    isMain() {\n      return this.el.hasAttribute(PHX_MAIN);\n    }\n    connectParams(liveReferer) {\n      const params = this.liveSocket.params(this.el);\n      const manifest = dom_default.all(document, `[${this.binding(PHX_TRACK_STATIC)}]`).map((node) => node.src || node.href).filter((url) => typeof url === \"string\");\n      if (manifest.length > 0) {\n        params[\"_track_static\"] = manifest;\n      }\n      params[\"_mounts\"] = this.joinCount;\n      params[\"_mount_attempts\"] = this.joinAttempts;\n      params[\"_live_referer\"] = liveReferer;\n      this.joinAttempts++;\n      return params;\n    }\n    isConnected() {\n      return this.channel.canPush();\n    }\n    getSession() {\n      return this.el.getAttribute(PHX_SESSION);\n    }\n    getStatic() {\n      const val = this.el.getAttribute(PHX_STATIC);\n      return val === \"\" ? null : val;\n    }\n    destroy(callback = function() {\n    }) {\n      this.destroyAllChildren();\n      this.destroyPortalElements();\n      this.destroyed = true;\n      dom_default.deletePrivate(this.el, \"view\");\n      delete this.root.children[this.id];\n      if (this.parent) {\n        delete this.root.children[this.parent.id][this.id];\n      }\n      clearTimeout(this.loaderTimer);\n      const onFinished = () => {\n        callback();\n        for (const id in this.viewHooks) {\n          this.destroyHook(this.viewHooks[id]);\n        }\n      };\n      dom_default.markPhxChildDestroyed(this.el);\n      this.log(\"destroyed\", () => [\"the child has been removed from the parent\"]);\n      this.channel.leave().receive(\"ok\", onFinished).receive(\"error\", onFinished).receive(\"timeout\", onFinished);\n    }\n    setContainerClasses(...classes) {\n      this.el.classList.remove(\n        PHX_CONNECTED_CLASS,\n        PHX_LOADING_CLASS,\n        PHX_ERROR_CLASS,\n        PHX_CLIENT_ERROR_CLASS,\n        PHX_SERVER_ERROR_CLASS\n      );\n      this.el.classList.add(...classes);\n    }\n    showLoader(timeout) {\n      clearTimeout(this.loaderTimer);\n      if (timeout) {\n        this.loaderTimer = setTimeout(() => this.showLoader(), timeout);\n      } else {\n        for (const id in this.viewHooks) {\n          this.viewHooks[id].__disconnected();\n        }\n        this.setContainerClasses(PHX_LOADING_CLASS);\n      }\n    }\n    execAll(binding) {\n      dom_default.all(\n        this.el,\n        `[${binding}]`,\n        (el) => this.liveSocket.execJS(el, el.getAttribute(binding))\n      );\n    }\n    hideLoader() {\n      clearTimeout(this.loaderTimer);\n      clearTimeout(this.disconnectedTimer);\n      this.setContainerClasses(PHX_CONNECTED_CLASS);\n      this.execAll(this.binding(\"connected\"));\n    }\n    triggerReconnected() {\n      for (const id in this.viewHooks) {\n        this.viewHooks[id].__reconnected();\n      }\n    }\n    log(kind, msgCallback) {\n      this.liveSocket.log(this, kind, msgCallback);\n    }\n    transition(time, onStart, onDone = function() {\n    }) {\n      this.liveSocket.transition(time, onStart, onDone);\n    }\n    // calls the callback with the view and target element for the given phxTarget\n    // targets can be:\n    //  * an element itself, then it is simply passed to liveSocket.owner;\n    //  * a CID (Component ID), then we first search the component's element in the DOM\n    //  * a selector, then we search the selector in the DOM and call the callback\n    //    for each element found with the corresponding owner view\n    withinTargets(phxTarget, callback, dom = document) {\n      if (phxTarget instanceof HTMLElement || phxTarget instanceof SVGElement) {\n        return this.liveSocket.owner(\n          phxTarget,\n          (view) => callback(view, phxTarget)\n        );\n      }\n      if (isCid(phxTarget)) {\n        const targets = dom_default.findComponentNodeList(this.id, phxTarget, dom);\n        if (targets.length === 0) {\n          logError(`no component found matching phx-target of ${phxTarget}`);\n        } else {\n          callback(this, parseInt(phxTarget));\n        }\n      } else {\n        const targets = Array.from(dom.querySelectorAll(phxTarget));\n        if (targets.length === 0) {\n          logError(\n            `nothing found matching the phx-target selector \"${phxTarget}\"`\n          );\n        }\n        targets.forEach(\n          (target) => this.liveSocket.owner(target, (view) => callback(view, target))\n        );\n      }\n    }\n    applyDiff(type, rawDiff, callback) {\n      this.log(type, () => [\"\", clone(rawDiff)]);\n      const { diff, reply, events, title } = Rendered.extract(rawDiff);\n      const ev = events.reduce(\n        (acc, args) => {\n          if (args.length === 3 && args[2] == true) {\n            acc.pre.push(args.slice(0, -1));\n          } else {\n            acc.post.push(args);\n          }\n          return acc;\n        },\n        { pre: [], post: [] }\n      );\n      this.liveSocket.dispatchEvents(ev.pre);\n      const update = () => {\n        callback({ diff, reply, events: ev.post });\n        if (typeof title === \"string\" || type == \"mount\" && this.isMain()) {\n          window.requestAnimationFrame(() => dom_default.putTitle(title));\n        }\n      };\n      if (\"onDocumentPatch\" in this.liveSocket.domCallbacks) {\n        this.liveSocket.triggerDOM(\"onDocumentPatch\", [update]);\n      } else {\n        update();\n      }\n    }\n    onJoin(resp) {\n      const { rendered, container, liveview_version, pid } = resp;\n      if (container) {\n        const [tag, attrs] = container;\n        this.el = dom_default.replaceRootContainer(this.el, tag, attrs);\n      }\n      this.childJoins = 0;\n      this.joinPending = true;\n      this.flash = null;\n      if (this.root === this) {\n        this.formsForRecovery = this.getFormsForRecovery();\n      }\n      if (this.isMain() && window.history.state === null) {\n        browser_default.pushState(\"replace\", {\n          type: \"patch\",\n          id: this.id,\n          position: this.liveSocket.currentHistoryPosition\n        });\n      }\n      if (liveview_version !== this.liveSocket.version()) {\n        console.warn(\n          `LiveView asset version mismatch. JavaScript version ${this.liveSocket.version()} vs. server ${liveview_version}. To avoid issues, please ensure that your assets use the same version as the server.`\n        );\n      }\n      if (pid) {\n        this.el.setAttribute(PHX_LV_PID, pid);\n      }\n      browser_default.dropLocal(\n        this.liveSocket.localStorage,\n        window.location.pathname,\n        CONSECUTIVE_RELOADS\n      );\n      this.applyDiff(\"mount\", rendered, ({ diff, events }) => {\n        this.rendered = new Rendered(this.id, diff);\n        const [html, streams] = this.renderContainer(null, \"join\");\n        this.dropPendingRefs();\n        this.joinCount++;\n        this.joinAttempts = 0;\n        this.maybeRecoverForms(html, () => {\n          this.onJoinComplete(resp, html, streams, events);\n        });\n      });\n    }\n    dropPendingRefs() {\n      dom_default.all(document, `[${PHX_REF_SRC}=\"${this.refSrc()}\"]`, (el) => {\n        el.removeAttribute(PHX_REF_LOADING);\n        el.removeAttribute(PHX_REF_SRC);\n        el.removeAttribute(PHX_REF_LOCK);\n      });\n    }\n    onJoinComplete({ live_patch }, html, streams, events) {\n      if (this.joinCount > 1 || this.parent && !this.parent.isJoinPending()) {\n        return this.applyJoinPatch(live_patch, html, streams, events);\n      }\n      const newChildren = dom_default.findPhxChildrenInFragment(html, this.id).filter(\n        (toEl) => {\n          const fromEl = toEl.id && this.el.querySelector(`[id=\"${toEl.id}\"]`);\n          const phxStatic = fromEl && fromEl.getAttribute(PHX_STATIC);\n          if (phxStatic) {\n            toEl.setAttribute(PHX_STATIC, phxStatic);\n          }\n          if (fromEl) {\n            fromEl.setAttribute(PHX_ROOT_ID, this.root.id);\n          }\n          return this.joinChild(toEl);\n        }\n      );\n      if (newChildren.length === 0) {\n        if (this.parent) {\n          this.root.pendingJoinOps.push([\n            this,\n            () => this.applyJoinPatch(live_patch, html, streams, events)\n          ]);\n          this.parent.ackJoin(this);\n        } else {\n          this.onAllChildJoinsComplete();\n          this.applyJoinPatch(live_patch, html, streams, events);\n        }\n      } else {\n        this.root.pendingJoinOps.push([\n          this,\n          () => this.applyJoinPatch(live_patch, html, streams, events)\n        ]);\n      }\n    }\n    attachTrueDocEl() {\n      this.el = dom_default.byId(this.id);\n      this.el.setAttribute(PHX_ROOT_ID, this.root.id);\n    }\n    // this is invoked for dead and live views, so we must filter by\n    // by owner to ensure we aren't duplicating hooks across disconnect\n    // and connected states. This also handles cases where hooks exist\n    // in a root layout with a LV in the body\n    execNewMounted(parent = document) {\n      let phxViewportTop = this.binding(PHX_VIEWPORT_TOP);\n      let phxViewportBottom = this.binding(PHX_VIEWPORT_BOTTOM);\n      this.all(\n        parent,\n        `[${phxViewportTop}], [${phxViewportBottom}]`,\n        (hookEl) => {\n          dom_default.maintainPrivateHooks(\n            hookEl,\n            hookEl,\n            phxViewportTop,\n            phxViewportBottom\n          );\n          this.maybeAddNewHook(hookEl);\n        }\n      );\n      this.all(\n        parent,\n        `[${this.binding(PHX_HOOK)}], [data-phx-${PHX_HOOK}]`,\n        (hookEl) => {\n          this.maybeAddNewHook(hookEl);\n        }\n      );\n      this.all(parent, `[${this.binding(PHX_MOUNTED)}]`, (el) => {\n        this.maybeMounted(el);\n      });\n    }\n    all(parent, selector, callback) {\n      dom_default.all(parent, selector, (el) => {\n        if (this.ownsElement(el)) {\n          callback(el);\n        }\n      });\n    }\n    applyJoinPatch(live_patch, html, streams, events) {\n      if (this.joinCount > 1) {\n        if (this.pendingJoinOps.length) {\n          this.pendingJoinOps.forEach((cb) => typeof cb === \"function\" && cb());\n          this.pendingJoinOps = [];\n        }\n      }\n      this.attachTrueDocEl();\n      const patch = new DOMPatch(this, this.el, this.id, html, streams, null);\n      patch.markPrunableContentForRemoval();\n      this.performPatch(patch, false, true);\n      this.joinNewChildren();\n      this.execNewMounted();\n      this.joinPending = false;\n      this.liveSocket.dispatchEvents(events);\n      this.applyPendingUpdates();\n      if (live_patch) {\n        const { kind, to } = live_patch;\n        this.liveSocket.historyPatch(to, kind);\n      }\n      this.hideLoader();\n      if (this.joinCount > 1) {\n        this.triggerReconnected();\n      }\n      this.stopCallback();\n    }\n    triggerBeforeUpdateHook(fromEl, toEl) {\n      this.liveSocket.triggerDOM(\"onBeforeElUpdated\", [fromEl, toEl]);\n      const hook = this.getHook(fromEl);\n      const isIgnored = hook && dom_default.isIgnored(fromEl, this.binding(PHX_UPDATE));\n      if (hook && !fromEl.isEqualNode(toEl) && !(isIgnored && isEqualObj(fromEl.dataset, toEl.dataset))) {\n        hook.__beforeUpdate();\n        return hook;\n      }\n    }\n    maybeMounted(el) {\n      const phxMounted = el.getAttribute(this.binding(PHX_MOUNTED));\n      const hasBeenInvoked = phxMounted && dom_default.private(el, \"mounted\");\n      if (phxMounted && !hasBeenInvoked) {\n        this.liveSocket.execJS(el, phxMounted);\n        dom_default.putPrivate(el, \"mounted\", true);\n      }\n    }\n    maybeAddNewHook(el) {\n      const newHook = this.addHook(el);\n      if (newHook) {\n        newHook.__mounted();\n      }\n    }\n    performPatch(patch, pruneCids, isJoinPatch = false) {\n      const removedEls = [];\n      let phxChildrenAdded = false;\n      const updatedHookIds = /* @__PURE__ */ new Set();\n      this.liveSocket.triggerDOM(\"onPatchStart\", [patch.targetContainer]);\n      patch.after(\"added\", (el) => {\n        this.liveSocket.triggerDOM(\"onNodeAdded\", [el]);\n        const phxViewportTop = this.binding(PHX_VIEWPORT_TOP);\n        const phxViewportBottom = this.binding(PHX_VIEWPORT_BOTTOM);\n        dom_default.maintainPrivateHooks(el, el, phxViewportTop, phxViewportBottom);\n        this.maybeAddNewHook(el);\n        if (el.getAttribute) {\n          this.maybeMounted(el);\n        }\n      });\n      patch.after(\"phxChildAdded\", (el) => {\n        if (dom_default.isPhxSticky(el)) {\n          this.liveSocket.joinRootViews();\n        } else {\n          phxChildrenAdded = true;\n        }\n      });\n      patch.before(\"updated\", (fromEl, toEl) => {\n        const hook = this.triggerBeforeUpdateHook(fromEl, toEl);\n        if (hook) {\n          updatedHookIds.add(fromEl.id);\n        }\n        js_default.onBeforeElUpdated(fromEl, toEl);\n      });\n      patch.after(\"updated\", (el) => {\n        if (updatedHookIds.has(el.id)) {\n          this.getHook(el).__updated();\n        }\n      });\n      patch.after(\"discarded\", (el) => {\n        if (el.nodeType === Node.ELEMENT_NODE) {\n          removedEls.push(el);\n        }\n      });\n      patch.after(\n        \"transitionsDiscarded\",\n        (els) => this.afterElementsRemoved(els, pruneCids)\n      );\n      patch.perform(isJoinPatch);\n      this.afterElementsRemoved(removedEls, pruneCids);\n      this.liveSocket.triggerDOM(\"onPatchEnd\", [patch.targetContainer]);\n      return phxChildrenAdded;\n    }\n    afterElementsRemoved(elements, pruneCids) {\n      const destroyedCIDs = [];\n      elements.forEach((parent) => {\n        const components = dom_default.all(\n          parent,\n          `[${PHX_VIEW_REF}=\"${this.id}\"][${PHX_COMPONENT}]`\n        );\n        const hooks = dom_default.all(\n          parent,\n          `[${this.binding(PHX_HOOK)}], [data-phx-hook]`\n        );\n        components.concat(parent).forEach((el) => {\n          const cid = this.componentID(el);\n          if (isCid(cid) && destroyedCIDs.indexOf(cid) === -1 && el.getAttribute(PHX_VIEW_REF) === this.id) {\n            destroyedCIDs.push(cid);\n          }\n        });\n        hooks.concat(parent).forEach((hookEl) => {\n          const hook = this.getHook(hookEl);\n          hook && this.destroyHook(hook);\n        });\n      });\n      if (pruneCids) {\n        this.maybePushComponentsDestroyed(destroyedCIDs);\n      }\n    }\n    joinNewChildren() {\n      dom_default.findPhxChildren(document, this.id).forEach((el) => this.joinChild(el));\n    }\n    maybeRecoverForms(html, callback) {\n      const phxChange = this.binding(\"change\");\n      const oldForms = this.root.formsForRecovery;\n      const template = document.createElement(\"template\");\n      template.innerHTML = html;\n      dom_default.all(template.content, `[${PHX_PORTAL}]`).forEach((portalTemplate) => {\n        template.content.firstElementChild.appendChild(\n          portalTemplate.content.firstElementChild\n        );\n      });\n      const rootEl = template.content.firstElementChild;\n      rootEl.id = this.id;\n      rootEl.setAttribute(PHX_ROOT_ID, this.root.id);\n      rootEl.setAttribute(PHX_SESSION, this.getSession());\n      rootEl.setAttribute(PHX_STATIC, this.getStatic());\n      rootEl.setAttribute(PHX_PARENT_ID, this.parent ? this.parent.id : null);\n      const formsToRecover = (\n        // we go over all forms in the new DOM; because this is only the HTML for the current\n        // view, we can be sure that all forms are owned by this view:\n        dom_default.all(template.content, \"form\").filter((newForm) => newForm.id && oldForms[newForm.id]).filter((newForm) => !this.pendingForms.has(newForm.id)).filter(\n          (newForm) => oldForms[newForm.id].getAttribute(phxChange) === newForm.getAttribute(phxChange)\n        ).map((newForm) => {\n          return [oldForms[newForm.id], newForm];\n        })\n      );\n      if (formsToRecover.length === 0) {\n        return callback();\n      }\n      formsToRecover.forEach(([oldForm, newForm], i) => {\n        this.pendingForms.add(newForm.id);\n        this.pushFormRecovery(\n          oldForm,\n          newForm,\n          template.content.firstElementChild,\n          () => {\n            this.pendingForms.delete(newForm.id);\n            if (i === formsToRecover.length - 1) {\n              callback();\n            }\n          }\n        );\n      });\n    }\n    getChildById(id) {\n      return this.root.children[this.id][id];\n    }\n    getDescendentByEl(el) {\n      var _a;\n      if (el.id === this.id) {\n        return this;\n      } else {\n        return (_a = this.children[el.getAttribute(PHX_PARENT_ID)]) == null ? void 0 : _a[el.id];\n      }\n    }\n    destroyDescendent(id) {\n      for (const parentId in this.root.children) {\n        for (const childId in this.root.children[parentId]) {\n          if (childId === id) {\n            return this.root.children[parentId][childId].destroy();\n          }\n        }\n      }\n    }\n    joinChild(el) {\n      const child = this.getChildById(el.id);\n      if (!child) {\n        const view = new _View(el, this.liveSocket, this);\n        this.root.children[this.id][view.id] = view;\n        view.join();\n        this.childJoins++;\n        return true;\n      }\n    }\n    isJoinPending() {\n      return this.joinPending;\n    }\n    ackJoin(_child) {\n      this.childJoins--;\n      if (this.childJoins === 0) {\n        if (this.parent) {\n          this.parent.ackJoin(this);\n        } else {\n          this.onAllChildJoinsComplete();\n        }\n      }\n    }\n    onAllChildJoinsComplete() {\n      this.pendingForms.clear();\n      this.formsForRecovery = {};\n      this.joinCallback(() => {\n        this.pendingJoinOps.forEach(([view, op]) => {\n          if (!view.isDestroyed()) {\n            op();\n          }\n        });\n        this.pendingJoinOps = [];\n      });\n    }\n    update(diff, events, isPending = false) {\n      if (this.isJoinPending() || this.liveSocket.hasPendingLink() && this.root.isMain()) {\n        if (!isPending) {\n          this.pendingDiffs.push({ diff, events });\n        }\n        return false;\n      }\n      this.rendered.mergeDiff(diff);\n      let phxChildrenAdded = false;\n      if (this.rendered.isComponentOnlyDiff(diff)) {\n        this.liveSocket.time(\"component patch complete\", () => {\n          const parentCids = dom_default.findExistingParentCIDs(\n            this.id,\n            this.rendered.componentCIDs(diff)\n          );\n          parentCids.forEach((parentCID) => {\n            if (this.componentPatch(\n              this.rendered.getComponent(diff, parentCID),\n              parentCID\n            )) {\n              phxChildrenAdded = true;\n            }\n          });\n        });\n      } else if (!isEmpty(diff)) {\n        this.liveSocket.time(\"full patch complete\", () => {\n          const [html, streams] = this.renderContainer(diff, \"update\");\n          const patch = new DOMPatch(this, this.el, this.id, html, streams, null);\n          phxChildrenAdded = this.performPatch(patch, true);\n        });\n      }\n      this.liveSocket.dispatchEvents(events);\n      if (phxChildrenAdded) {\n        this.joinNewChildren();\n      }\n      return true;\n    }\n    renderContainer(diff, kind) {\n      return this.liveSocket.time(`toString diff (${kind})`, () => {\n        const tag = this.el.tagName;\n        const cids = diff ? this.rendered.componentCIDs(diff) : null;\n        const { buffer: html, streams } = this.rendered.toString(cids);\n        return [`<${tag}>${html}</${tag}>`, streams];\n      });\n    }\n    componentPatch(diff, cid) {\n      if (isEmpty(diff))\n        return false;\n      const { buffer: html, streams } = this.rendered.componentToString(cid);\n      const patch = new DOMPatch(this, this.el, this.id, html, streams, cid);\n      const childrenAdded = this.performPatch(patch, true);\n      return childrenAdded;\n    }\n    getHook(el) {\n      return this.viewHooks[ViewHook.elementID(el)];\n    }\n    addHook(el) {\n      const hookElId = ViewHook.elementID(el);\n      if (el.getAttribute && !this.ownsElement(el)) {\n        return;\n      }\n      if (hookElId && !this.viewHooks[hookElId]) {\n        if (ViewHook.deadHook(el)) {\n          return;\n        }\n        const hook = dom_default.getCustomElHook(el) || logError(`no hook found for custom element: ${el.id}`);\n        this.viewHooks[hookElId] = hook;\n        hook.__attachView(this);\n        return hook;\n      } else if (hookElId || !el.getAttribute) {\n        return;\n      } else {\n        const hookName = el.getAttribute(`data-phx-${PHX_HOOK}`) || el.getAttribute(this.binding(PHX_HOOK));\n        if (!hookName) {\n          return;\n        }\n        const hookDefinition = this.liveSocket.getHookDefinition(hookName);\n        if (hookDefinition) {\n          if (!el.id) {\n            logError(\n              `no DOM ID for hook \"${hookName}\". Hooks require a unique ID on each element.`,\n              el\n            );\n            return;\n          }\n          let hookInstance;\n          try {\n            if (typeof hookDefinition === \"function\" && hookDefinition.prototype instanceof ViewHook) {\n              hookInstance = new hookDefinition(this, el);\n            } else if (typeof hookDefinition === \"object\" && hookDefinition !== null) {\n              hookInstance = new ViewHook(this, el, hookDefinition);\n            } else {\n              logError(\n                `Invalid hook definition for \"${hookName}\". Expected a class extending ViewHook or an object definition.`,\n                el\n              );\n              return;\n            }\n          } catch (e) {\n            const errorMessage = e instanceof Error ? e.message : String(e);\n            logError(`Failed to create hook \"${hookName}\": ${errorMessage}`, el);\n            return;\n          }\n          this.viewHooks[ViewHook.elementID(hookInstance.el)] = hookInstance;\n          return hookInstance;\n        } else if (hookName !== null) {\n          logError(`unknown hook found for \"${hookName}\"`, el);\n        }\n      }\n    }\n    destroyHook(hook) {\n      const hookId = ViewHook.elementID(hook.el);\n      hook.__destroyed();\n      hook.__cleanup__();\n      delete this.viewHooks[hookId];\n    }\n    applyPendingUpdates() {\n      this.pendingDiffs = this.pendingDiffs.filter(\n        ({ diff, events }) => !this.update(diff, events, true)\n      );\n      this.eachChild((child) => child.applyPendingUpdates());\n    }\n    eachChild(callback) {\n      const children = this.root.children[this.id] || {};\n      for (const id in children) {\n        callback(this.getChildById(id));\n      }\n    }\n    onChannel(event, cb) {\n      this.liveSocket.onChannel(this.channel, event, (resp) => {\n        if (this.isJoinPending()) {\n          if (this.joinCount > 1) {\n            this.pendingJoinOps.push(() => cb(resp));\n          } else {\n            this.root.pendingJoinOps.push([this, () => cb(resp)]);\n          }\n        } else {\n          this.liveSocket.requestDOMUpdate(() => cb(resp));\n        }\n      });\n    }\n    bindChannel() {\n      this.liveSocket.onChannel(this.channel, \"diff\", (rawDiff) => {\n        this.liveSocket.requestDOMUpdate(() => {\n          this.applyDiff(\n            \"update\",\n            rawDiff,\n            ({ diff, events }) => this.update(diff, events)\n          );\n        });\n      });\n      this.onChannel(\n        \"redirect\",\n        ({ to, flash }) => this.onRedirect({ to, flash })\n      );\n      this.onChannel(\"live_patch\", (redir) => this.onLivePatch(redir));\n      this.onChannel(\"live_redirect\", (redir) => this.onLiveRedirect(redir));\n      this.channel.onError((reason) => this.onError(reason));\n      this.channel.onClose((reason) => this.onClose(reason));\n    }\n    destroyAllChildren() {\n      this.eachChild((child) => child.destroy());\n    }\n    onLiveRedirect(redir) {\n      const { to, kind, flash } = redir;\n      const url = this.expandURL(to);\n      const e = new CustomEvent(\"phx:server-navigate\", {\n        detail: { to, kind, flash }\n      });\n      this.liveSocket.historyRedirect(e, url, kind, flash);\n    }\n    onLivePatch(redir) {\n      const { to, kind } = redir;\n      this.href = this.expandURL(to);\n      this.liveSocket.historyPatch(to, kind);\n    }\n    expandURL(to) {\n      return to.startsWith(\"/\") ? `${window.location.protocol}//${window.location.host}${to}` : to;\n    }\n    /**\n     * @param {{to: string, flash?: string, reloadToken?: string}} redirect\n     */\n    onRedirect({ to, flash, reloadToken }) {\n      this.liveSocket.redirect(to, flash, reloadToken);\n    }\n    isDestroyed() {\n      return this.destroyed;\n    }\n    joinDead() {\n      this.isDead = true;\n    }\n    joinPush() {\n      this.joinPush = this.joinPush || this.channel.join();\n      return this.joinPush;\n    }\n    join(callback) {\n      this.showLoader(this.liveSocket.loaderTimeout);\n      this.bindChannel();\n      if (this.isMain()) {\n        this.stopCallback = this.liveSocket.withPageLoading({\n          to: this.href,\n          kind: \"initial\"\n        });\n      }\n      this.joinCallback = (onDone) => {\n        onDone = onDone || function() {\n        };\n        callback ? callback(this.joinCount, onDone) : onDone();\n      };\n      this.wrapPush(() => this.channel.join(), {\n        ok: (resp) => this.liveSocket.requestDOMUpdate(() => this.onJoin(resp)),\n        error: (error) => this.onJoinError(error),\n        timeout: () => this.onJoinError({ reason: \"timeout\" })\n      });\n    }\n    onJoinError(resp) {\n      if (resp.reason === \"reload\") {\n        this.log(\"error\", () => [\n          `failed mount with ${resp.status}. Falling back to page reload`,\n          resp\n        ]);\n        this.onRedirect({\n          to: this.liveSocket.main.href,\n          reloadToken: resp.token\n        });\n        return;\n      } else if (resp.reason === \"unauthorized\" || resp.reason === \"stale\") {\n        this.log(\"error\", () => [\n          \"unauthorized live_redirect. Falling back to page request\",\n          resp\n        ]);\n        this.onRedirect({ to: this.liveSocket.main.href, flash: this.flash });\n        return;\n      }\n      if (resp.redirect || resp.live_redirect) {\n        this.joinPending = false;\n        this.channel.leave();\n      }\n      if (resp.redirect) {\n        return this.onRedirect(resp.redirect);\n      }\n      if (resp.live_redirect) {\n        return this.onLiveRedirect(resp.live_redirect);\n      }\n      this.log(\"error\", () => [\"unable to join\", resp]);\n      if (this.isMain()) {\n        this.displayError(\n          [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS],\n          { unstructuredError: resp, errorKind: \"server\" }\n        );\n        if (this.liveSocket.isConnected()) {\n          this.liveSocket.reloadWithJitter(this);\n        }\n      } else {\n        if (this.joinAttempts >= MAX_CHILD_JOIN_ATTEMPTS) {\n          this.root.displayError(\n            [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS],\n            { unstructuredError: resp, errorKind: \"server\" }\n          );\n          this.log(\"error\", () => [\n            `giving up trying to mount after ${MAX_CHILD_JOIN_ATTEMPTS} tries`,\n            resp\n          ]);\n          this.destroy();\n        }\n        const trueChildEl = dom_default.byId(this.el.id);\n        if (trueChildEl) {\n          dom_default.mergeAttrs(trueChildEl, this.el);\n          this.displayError(\n            [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS],\n            { unstructuredError: resp, errorKind: \"server\" }\n          );\n          this.el = trueChildEl;\n        } else {\n          this.destroy();\n        }\n      }\n    }\n    onClose(reason) {\n      if (this.isDestroyed()) {\n        return;\n      }\n      if (this.isMain() && this.liveSocket.hasPendingLink() && reason !== \"leave\") {\n        return this.liveSocket.reloadWithJitter(this);\n      }\n      this.destroyAllChildren();\n      this.liveSocket.dropActiveElement(this);\n      if (this.liveSocket.isUnloaded()) {\n        this.showLoader(BEFORE_UNLOAD_LOADER_TIMEOUT);\n      }\n    }\n    onError(reason) {\n      this.onClose(reason);\n      if (this.liveSocket.isConnected()) {\n        this.log(\"error\", () => [\"view crashed\", reason]);\n      }\n      if (!this.liveSocket.isUnloaded()) {\n        if (this.liveSocket.isConnected()) {\n          this.displayError(\n            [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS],\n            { unstructuredError: reason, errorKind: \"server\" }\n          );\n        } else {\n          this.displayError(\n            [PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_CLIENT_ERROR_CLASS],\n            { unstructuredError: reason, errorKind: \"client\" }\n          );\n        }\n      }\n    }\n    displayError(classes, details = {}) {\n      if (this.isMain()) {\n        dom_default.dispatchEvent(window, \"phx:page-loading-start\", {\n          detail: __spreadValues({ to: this.href, kind: \"error\" }, details)\n        });\n      }\n      this.showLoader();\n      this.setContainerClasses(...classes);\n      this.delayedDisconnected();\n    }\n    delayedDisconnected() {\n      this.disconnectedTimer = setTimeout(() => {\n        this.execAll(this.binding(\"disconnected\"));\n      }, this.liveSocket.disconnectedTimeout);\n    }\n    wrapPush(callerPush, receives) {\n      const latency = this.liveSocket.getLatencySim();\n      const withLatency = latency ? (cb) => setTimeout(() => !this.isDestroyed() && cb(), latency) : (cb) => !this.isDestroyed() && cb();\n      withLatency(() => {\n        callerPush().receive(\n          \"ok\",\n          (resp) => withLatency(() => receives.ok && receives.ok(resp))\n        ).receive(\n          \"error\",\n          (reason) => withLatency(() => receives.error && receives.error(reason))\n        ).receive(\n          \"timeout\",\n          () => withLatency(() => receives.timeout && receives.timeout())\n        );\n      });\n    }\n    pushWithReply(refGenerator, event, payload) {\n      if (!this.isConnected()) {\n        return Promise.reject(new Error(\"no connection\"));\n      }\n      const [ref, [el], opts] = refGenerator ? refGenerator({ payload }) : [null, [], {}];\n      const oldJoinCount = this.joinCount;\n      let onLoadingDone = function() {\n      };\n      if (opts.page_loading) {\n        onLoadingDone = this.liveSocket.withPageLoading({\n          kind: \"element\",\n          target: el\n        });\n      }\n      if (typeof payload.cid !== \"number\") {\n        delete payload.cid;\n      }\n      return new Promise((resolve, reject) => {\n        this.wrapPush(() => this.channel.push(event, payload, PUSH_TIMEOUT), {\n          ok: (resp) => {\n            if (ref !== null) {\n              this.lastAckRef = ref;\n            }\n            const finish = (hookReply) => {\n              if (resp.redirect) {\n                this.onRedirect(resp.redirect);\n              }\n              if (resp.live_patch) {\n                this.onLivePatch(resp.live_patch);\n              }\n              if (resp.live_redirect) {\n                this.onLiveRedirect(resp.live_redirect);\n              }\n              onLoadingDone();\n              resolve({ resp, reply: hookReply, ref });\n            };\n            if (resp.diff) {\n              this.liveSocket.requestDOMUpdate(() => {\n                this.applyDiff(\"update\", resp.diff, ({ diff, reply, events }) => {\n                  if (ref !== null) {\n                    this.undoRefs(ref, payload.event);\n                  }\n                  this.update(diff, events);\n                  finish(reply);\n                });\n              });\n            } else {\n              if (ref !== null) {\n                this.undoRefs(ref, payload.event);\n              }\n              finish(null);\n            }\n          },\n          error: (reason) => reject(new Error(`failed with reason: ${JSON.stringify(reason)}`)),\n          timeout: () => {\n            reject(new Error(\"timeout\"));\n            if (this.joinCount === oldJoinCount) {\n              this.liveSocket.reloadWithJitter(this, () => {\n                this.log(\"timeout\", () => [\n                  \"received timeout while communicating with server. Falling back to hard refresh for recovery\"\n                ]);\n              });\n            }\n          }\n        });\n      });\n    }\n    undoRefs(ref, phxEvent, onlyEls) {\n      if (!this.isConnected()) {\n        return;\n      }\n      const selector = `[${PHX_REF_SRC}=\"${this.refSrc()}\"]`;\n      if (onlyEls) {\n        onlyEls = new Set(onlyEls);\n        dom_default.all(document, selector, (parent) => {\n          if (onlyEls && !onlyEls.has(parent)) {\n            return;\n          }\n          dom_default.all(\n            parent,\n            selector,\n            (child) => this.undoElRef(child, ref, phxEvent)\n          );\n          this.undoElRef(parent, ref, phxEvent);\n        });\n      } else {\n        dom_default.all(document, selector, (el) => this.undoElRef(el, ref, phxEvent));\n      }\n    }\n    undoElRef(el, ref, phxEvent) {\n      const elRef = new ElementRef(el);\n      elRef.maybeUndo(ref, phxEvent, (clonedTree) => {\n        const patch = new DOMPatch(this, el, this.id, clonedTree, [], null, {\n          undoRef: ref\n        });\n        const phxChildrenAdded = this.performPatch(patch, true);\n        dom_default.all(\n          el,\n          `[${PHX_REF_SRC}=\"${this.refSrc()}\"]`,\n          (child) => this.undoElRef(child, ref, phxEvent)\n        );\n        if (phxChildrenAdded) {\n          this.joinNewChildren();\n        }\n      });\n    }\n    refSrc() {\n      return this.el.id;\n    }\n    putRef(elements, phxEvent, eventType, opts = {}) {\n      const newRef = this.ref++;\n      const disableWith = this.binding(PHX_DISABLE_WITH);\n      if (opts.loading) {\n        const loadingEls = dom_default.all(document, opts.loading).map((el) => {\n          return { el, lock: true, loading: true };\n        });\n        elements = elements.concat(loadingEls);\n      }\n      for (const { el, lock, loading } of elements) {\n        if (!lock && !loading) {\n          throw new Error(\"putRef requires lock or loading\");\n        }\n        el.setAttribute(PHX_REF_SRC, this.refSrc());\n        if (loading) {\n          el.setAttribute(PHX_REF_LOADING, newRef);\n        }\n        if (lock) {\n          el.setAttribute(PHX_REF_LOCK, newRef);\n        }\n        if (!loading || opts.submitter && !(el === opts.submitter || el === opts.form)) {\n          continue;\n        }\n        const lockCompletePromise = new Promise((resolve) => {\n          el.addEventListener(`phx:undo-lock:${newRef}`, () => resolve(detail), {\n            once: true\n          });\n        });\n        const loadingCompletePromise = new Promise((resolve) => {\n          el.addEventListener(\n            `phx:undo-loading:${newRef}`,\n            () => resolve(detail),\n            { once: true }\n          );\n        });\n        el.classList.add(`phx-${eventType}-loading`);\n        const disableText = el.getAttribute(disableWith);\n        if (disableText !== null) {\n          if (!el.getAttribute(PHX_DISABLE_WITH_RESTORE)) {\n            el.setAttribute(PHX_DISABLE_WITH_RESTORE, el.textContent);\n          }\n          if (disableText !== \"\") {\n            el.textContent = disableText;\n          }\n          el.setAttribute(\n            PHX_DISABLED,\n            el.getAttribute(PHX_DISABLED) || el.disabled\n          );\n          el.setAttribute(\"disabled\", \"\");\n        }\n        const detail = {\n          event: phxEvent,\n          eventType,\n          ref: newRef,\n          isLoading: loading,\n          isLocked: lock,\n          lockElements: elements.filter(({ lock: lock2 }) => lock2).map(({ el: el2 }) => el2),\n          loadingElements: elements.filter(({ loading: loading2 }) => loading2).map(({ el: el2 }) => el2),\n          unlock: (els) => {\n            els = Array.isArray(els) ? els : [els];\n            this.undoRefs(newRef, phxEvent, els);\n          },\n          lockComplete: lockCompletePromise,\n          loadingComplete: loadingCompletePromise,\n          lock: (lockEl) => {\n            return new Promise((resolve) => {\n              if (this.isAcked(newRef)) {\n                return resolve(detail);\n              }\n              lockEl.setAttribute(PHX_REF_LOCK, newRef);\n              lockEl.setAttribute(PHX_REF_SRC, this.refSrc());\n              lockEl.addEventListener(\n                `phx:lock-stop:${newRef}`,\n                () => resolve(detail),\n                { once: true }\n              );\n            });\n          }\n        };\n        if (opts.payload) {\n          detail[\"payload\"] = opts.payload;\n        }\n        if (opts.target) {\n          detail[\"target\"] = opts.target;\n        }\n        if (opts.originalEvent) {\n          detail[\"originalEvent\"] = opts.originalEvent;\n        }\n        el.dispatchEvent(\n          new CustomEvent(\"phx:push\", {\n            detail,\n            bubbles: true,\n            cancelable: false\n          })\n        );\n        if (phxEvent) {\n          el.dispatchEvent(\n            new CustomEvent(`phx:push:${phxEvent}`, {\n              detail,\n              bubbles: true,\n              cancelable: false\n            })\n          );\n        }\n      }\n      return [newRef, elements.map(({ el }) => el), opts];\n    }\n    isAcked(ref) {\n      return this.lastAckRef !== null && this.lastAckRef >= ref;\n    }\n    componentID(el) {\n      const cid = el.getAttribute && el.getAttribute(PHX_COMPONENT);\n      return cid ? parseInt(cid) : null;\n    }\n    targetComponentID(target, targetCtx, opts = {}) {\n      if (isCid(targetCtx)) {\n        return targetCtx;\n      }\n      const cidOrSelector = opts.target || target.getAttribute(this.binding(\"target\"));\n      if (isCid(cidOrSelector)) {\n        return parseInt(cidOrSelector);\n      } else if (targetCtx && (cidOrSelector !== null || opts.target)) {\n        return this.closestComponentID(targetCtx);\n      } else {\n        return null;\n      }\n    }\n    closestComponentID(targetCtx) {\n      if (isCid(targetCtx)) {\n        return targetCtx;\n      } else if (targetCtx) {\n        return maybe(\n          // We either use the closest data-phx-component binding, or -\n          // in case of portals - continue with the portal source.\n          // This is necessary if teleporting an element outside of its LiveComponent.\n          targetCtx.closest(`[${PHX_COMPONENT}],[${PHX_TELEPORTED_SRC}]`),\n          (el) => {\n            if (el.hasAttribute(PHX_COMPONENT)) {\n              return this.ownsElement(el) && this.componentID(el);\n            }\n            if (el.hasAttribute(PHX_TELEPORTED_SRC)) {\n              const portalParent = dom_default.byId(el.getAttribute(PHX_TELEPORTED_SRC));\n              return this.closestComponentID(portalParent);\n            }\n          }\n        );\n      } else {\n        return null;\n      }\n    }\n    pushHookEvent(el, targetCtx, event, payload) {\n      if (!this.isConnected()) {\n        this.log(\"hook\", () => [\n          \"unable to push hook event. LiveView not connected\",\n          event,\n          payload\n        ]);\n        return Promise.reject(\n          new Error(\"unable to push hook event. LiveView not connected\")\n        );\n      }\n      const refGenerator = () => this.putRef([{ el, loading: true, lock: true }], event, \"hook\", {\n        payload,\n        target: targetCtx\n      });\n      return this.pushWithReply(refGenerator, \"event\", {\n        type: \"hook\",\n        event,\n        value: payload,\n        cid: this.closestComponentID(targetCtx)\n      }).then(({ resp: _resp, reply, ref }) => ({ reply, ref }));\n    }\n    extractMeta(el, meta, value) {\n      const prefix = this.binding(\"value-\");\n      for (let i = 0; i < el.attributes.length; i++) {\n        if (!meta) {\n          meta = {};\n        }\n        const name = el.attributes[i].name;\n        if (name.startsWith(prefix)) {\n          meta[name.replace(prefix, \"\")] = el.getAttribute(name);\n        }\n      }\n      if (el.value !== void 0 && !(el instanceof HTMLFormElement)) {\n        if (!meta) {\n          meta = {};\n        }\n        meta.value = el.value;\n        if (el.tagName === \"INPUT\" && CHECKABLE_INPUTS.indexOf(el.type) >= 0 && !el.checked) {\n          delete meta.value;\n        }\n      }\n      if (value) {\n        if (!meta) {\n          meta = {};\n        }\n        for (const key in value) {\n          meta[key] = value[key];\n        }\n      }\n      return meta;\n    }\n    serializeForm(form, opts, onlyNames = []) {\n      const { submitter } = opts;\n      let injectedElement;\n      if (submitter && submitter.name) {\n        const input = document.createElement(\"input\");\n        input.type = \"hidden\";\n        const formId = submitter.getAttribute(\"form\");\n        if (formId) {\n          input.setAttribute(\"form\", formId);\n        }\n        input.name = submitter.name;\n        input.value = submitter.value;\n        submitter.parentElement.insertBefore(input, submitter);\n        injectedElement = input;\n      }\n      const formData = new FormData(form);\n      const toRemove = [];\n      formData.forEach((val, key, _index) => {\n        if (val instanceof File) {\n          toRemove.push(key);\n        }\n      });\n      toRemove.forEach((key) => formData.delete(key));\n      const params = new URLSearchParams();\n      const { inputsUnused, onlyHiddenInputs } = Array.from(form.elements).reduce(\n        (acc, input) => {\n          const { inputsUnused: inputsUnused2, onlyHiddenInputs: onlyHiddenInputs2 } = acc;\n          const key = input.name;\n          if (!key) {\n            return acc;\n          }\n          if (inputsUnused2[key] === void 0) {\n            inputsUnused2[key] = true;\n          }\n          if (onlyHiddenInputs2[key] === void 0) {\n            onlyHiddenInputs2[key] = true;\n          }\n          const inputSkipUnusedField = input.hasAttribute(\n            this.binding(PHX_NO_UNUSED_FIELD)\n          );\n          const isUsed = dom_default.private(input, PHX_HAS_FOCUSED) || dom_default.private(input, PHX_HAS_SUBMITTED) || inputSkipUnusedField;\n          const isHidden = input.type === \"hidden\";\n          inputsUnused2[key] = inputsUnused2[key] && !isUsed;\n          onlyHiddenInputs2[key] = onlyHiddenInputs2[key] && isHidden;\n          return acc;\n        },\n        { inputsUnused: {}, onlyHiddenInputs: {} }\n      );\n      const formSkipUnusedFields = form.hasAttribute(\n        this.binding(PHX_NO_UNUSED_FIELD)\n      );\n      for (const [key, val] of formData.entries()) {\n        if (onlyNames.length === 0 || onlyNames.indexOf(key) >= 0) {\n          const isUnused = inputsUnused[key];\n          const hidden = onlyHiddenInputs[key];\n          const skipUnusedCheck = formSkipUnusedFields;\n          if (!skipUnusedCheck && isUnused && !(submitter && submitter.name == key) && !hidden) {\n            params.append(prependFormDataKey(key, \"_unused_\"), \"\");\n          }\n          if (typeof val === \"string\") {\n            params.append(key, val);\n          }\n        }\n      }\n      if (submitter && injectedElement) {\n        submitter.parentElement.removeChild(injectedElement);\n      }\n      return params.toString();\n    }\n    pushEvent(type, el, targetCtx, phxEvent, meta, opts = {}, onReply) {\n      this.pushWithReply(\n        (maybePayload) => this.putRef([{ el, loading: true, lock: true }], phxEvent, type, __spreadProps(__spreadValues({}, opts), {\n          payload: maybePayload == null ? void 0 : maybePayload.payload\n        })),\n        \"event\",\n        {\n          type,\n          event: phxEvent,\n          value: this.extractMeta(el, meta, opts.value),\n          cid: this.targetComponentID(el, targetCtx, opts)\n        }\n      ).then(({ reply }) => onReply && onReply(reply)).catch((error) => logError(\"Failed to push event\", error));\n    }\n    pushFileProgress(fileEl, entryRef, progress, onReply = function() {\n    }) {\n      this.liveSocket.withinOwners(fileEl.form, (view, targetCtx) => {\n        view.pushWithReply(null, \"progress\", {\n          event: fileEl.getAttribute(view.binding(PHX_PROGRESS)),\n          ref: fileEl.getAttribute(PHX_UPLOAD_REF),\n          entry_ref: entryRef,\n          progress,\n          cid: view.targetComponentID(fileEl.form, targetCtx)\n        }).then(() => onReply()).catch((error) => logError(\"Failed to push file progress\", error));\n      });\n    }\n    pushInput(inputEl, targetCtx, forceCid, phxEvent, opts, callback) {\n      if (!inputEl.form) {\n        throw new Error(\"form events require the input to be inside a form\");\n      }\n      let uploads;\n      const cid = isCid(forceCid) ? forceCid : this.targetComponentID(inputEl.form, targetCtx, opts);\n      const refGenerator = (maybePayload) => {\n        return this.putRef(\n          [\n            { el: inputEl, loading: true, lock: true },\n            { el: inputEl.form, loading: true, lock: true }\n          ],\n          phxEvent,\n          \"change\",\n          __spreadProps(__spreadValues({}, opts), { payload: maybePayload == null ? void 0 : maybePayload.payload })\n        );\n      };\n      let formData;\n      const meta = this.extractMeta(inputEl.form, {}, opts.value);\n      const serializeOpts = {};\n      if (inputEl instanceof HTMLButtonElement) {\n        serializeOpts.submitter = inputEl;\n      }\n      if (inputEl.getAttribute(this.binding(\"change\"))) {\n        formData = this.serializeForm(inputEl.form, serializeOpts, [\n          inputEl.name\n        ]);\n      } else {\n        formData = this.serializeForm(inputEl.form, serializeOpts);\n      }\n      if (dom_default.isUploadInput(inputEl) && inputEl.files && inputEl.files.length > 0) {\n        LiveUploader.trackFiles(inputEl, Array.from(inputEl.files));\n      }\n      uploads = LiveUploader.serializeUploads(inputEl);\n      const event = {\n        type: \"form\",\n        event: phxEvent,\n        value: formData,\n        meta: __spreadValues({\n          // no target was implicitly sent as \"undefined\" in LV <= 1.0.5, therefore\n          // we have to keep it. In 1.0.6 we switched from passing meta as URL encoded data\n          // to passing it directly in the event, but the JSON encode would drop keys with\n          // undefined values.\n          _target: opts._target || \"undefined\"\n        }, meta),\n        uploads,\n        cid\n      };\n      this.pushWithReply(refGenerator, \"event\", event).then(({ resp }) => {\n        if (dom_default.isUploadInput(inputEl) && dom_default.isAutoUpload(inputEl)) {\n          ElementRef.onUnlock(inputEl, () => {\n            if (LiveUploader.filesAwaitingPreflight(inputEl).length > 0) {\n              const [ref, _els] = refGenerator();\n              this.undoRefs(ref, phxEvent, [inputEl.form]);\n              this.uploadFiles(\n                inputEl.form,\n                phxEvent,\n                targetCtx,\n                ref,\n                cid,\n                (_uploads) => {\n                  callback && callback(resp);\n                  this.triggerAwaitingSubmit(inputEl.form, phxEvent);\n                  this.undoRefs(ref, phxEvent);\n                }\n              );\n            }\n          });\n        } else {\n          callback && callback(resp);\n        }\n      }).catch((error) => logError(\"Failed to push input event\", error));\n    }\n    triggerAwaitingSubmit(formEl, phxEvent) {\n      const awaitingSubmit = this.getScheduledSubmit(formEl);\n      if (awaitingSubmit) {\n        const [_el, _ref, _opts, callback] = awaitingSubmit;\n        this.cancelSubmit(formEl, phxEvent);\n        callback();\n      }\n    }\n    getScheduledSubmit(formEl) {\n      return this.formSubmits.find(\n        ([el, _ref, _opts, _callback]) => el.isSameNode(formEl)\n      );\n    }\n    scheduleSubmit(formEl, ref, opts, callback) {\n      if (this.getScheduledSubmit(formEl)) {\n        return true;\n      }\n      this.formSubmits.push([formEl, ref, opts, callback]);\n    }\n    cancelSubmit(formEl, phxEvent) {\n      this.formSubmits = this.formSubmits.filter(\n        ([el, ref, _opts, _callback]) => {\n          if (el.isSameNode(formEl)) {\n            this.undoRefs(ref, phxEvent);\n            return false;\n          } else {\n            return true;\n          }\n        }\n      );\n    }\n    disableForm(formEl, phxEvent, opts = {}) {\n      const filterIgnored = (el) => {\n        const userIgnored = closestPhxBinding(\n          el,\n          `${this.binding(PHX_UPDATE)}=ignore`,\n          el.form\n        );\n        return !(userIgnored || closestPhxBinding(el, \"data-phx-update=ignore\", el.form));\n      };\n      const filterDisables = (el) => {\n        return el.hasAttribute(this.binding(PHX_DISABLE_WITH));\n      };\n      const filterButton = (el) => el.tagName == \"BUTTON\";\n      const filterInput = (el) => [\"INPUT\", \"TEXTAREA\", \"SELECT\"].includes(el.tagName);\n      const formElements = Array.from(formEl.elements);\n      const disables = formElements.filter(filterDisables);\n      const buttons = formElements.filter(filterButton).filter(filterIgnored);\n      const inputs = formElements.filter(filterInput).filter(filterIgnored);\n      buttons.forEach((button) => {\n        button.setAttribute(PHX_DISABLED, button.disabled);\n        button.disabled = true;\n      });\n      inputs.forEach((input) => {\n        input.setAttribute(PHX_READONLY, input.readOnly);\n        input.readOnly = true;\n        if (input.files) {\n          input.setAttribute(PHX_DISABLED, input.disabled);\n          input.disabled = true;\n        }\n      });\n      const formEls = disables.concat(buttons).concat(inputs).map((el) => {\n        return { el, loading: true, lock: true };\n      });\n      const els = [{ el: formEl, loading: true, lock: false }].concat(formEls).reverse();\n      return this.putRef(els, phxEvent, \"submit\", opts);\n    }\n    pushFormSubmit(formEl, targetCtx, phxEvent, submitter, opts, onReply) {\n      const refGenerator = (maybePayload) => this.disableForm(formEl, phxEvent, __spreadProps(__spreadValues({}, opts), {\n        form: formEl,\n        payload: maybePayload == null ? void 0 : maybePayload.payload,\n        submitter\n      }));\n      dom_default.putPrivate(formEl, \"submitter\", submitter);\n      const cid = this.targetComponentID(formEl, targetCtx);\n      if (LiveUploader.hasUploadsInProgress(formEl)) {\n        const [ref, _els] = refGenerator();\n        const push = () => this.pushFormSubmit(\n          formEl,\n          targetCtx,\n          phxEvent,\n          submitter,\n          opts,\n          onReply\n        );\n        return this.scheduleSubmit(formEl, ref, opts, push);\n      } else if (LiveUploader.inputsAwaitingPreflight(formEl).length > 0) {\n        const [ref, els] = refGenerator();\n        const proxyRefGen = () => [ref, els, opts];\n        this.uploadFiles(formEl, phxEvent, targetCtx, ref, cid, (_uploads) => {\n          if (LiveUploader.inputsAwaitingPreflight(formEl).length > 0) {\n            return this.undoRefs(ref, phxEvent);\n          }\n          const meta = this.extractMeta(formEl, {}, opts.value);\n          const formData = this.serializeForm(formEl, { submitter });\n          this.pushWithReply(proxyRefGen, \"event\", {\n            type: \"form\",\n            event: phxEvent,\n            value: formData,\n            meta,\n            cid\n          }).then(({ resp }) => onReply(resp)).catch((error) => logError(\"Failed to push form submit\", error));\n        });\n      } else if (!(formEl.hasAttribute(PHX_REF_SRC) && formEl.classList.contains(\"phx-submit-loading\"))) {\n        const meta = this.extractMeta(formEl, {}, opts.value);\n        const formData = this.serializeForm(formEl, { submitter });\n        this.pushWithReply(refGenerator, \"event\", {\n          type: \"form\",\n          event: phxEvent,\n          value: formData,\n          meta,\n          cid\n        }).then(({ resp }) => onReply(resp)).catch((error) => logError(\"Failed to push form submit\", error));\n      }\n    }\n    uploadFiles(formEl, phxEvent, targetCtx, ref, cid, onComplete) {\n      const joinCountAtUpload = this.joinCount;\n      const inputEls = LiveUploader.activeFileInputs(formEl);\n      let numFileInputsInProgress = inputEls.length;\n      inputEls.forEach((inputEl) => {\n        const uploader = new LiveUploader(inputEl, this, () => {\n          numFileInputsInProgress--;\n          if (numFileInputsInProgress === 0) {\n            onComplete();\n          }\n        });\n        const entries = uploader.entries().map((entry) => entry.toPreflightPayload());\n        if (entries.length === 0) {\n          numFileInputsInProgress--;\n          return;\n        }\n        const payload = {\n          ref: inputEl.getAttribute(PHX_UPLOAD_REF),\n          entries,\n          cid: this.targetComponentID(inputEl.form, targetCtx)\n        };\n        this.log(\"upload\", () => [\"sending preflight request\", payload]);\n        this.pushWithReply(null, \"allow_upload\", payload).then(({ resp }) => {\n          this.log(\"upload\", () => [\"got preflight response\", resp]);\n          uploader.entries().forEach((entry) => {\n            if (resp.entries && !resp.entries[entry.ref]) {\n              this.handleFailedEntryPreflight(\n                entry.ref,\n                \"failed preflight\",\n                uploader\n              );\n            }\n          });\n          if (resp.error || Object.keys(resp.entries).length === 0) {\n            this.undoRefs(ref, phxEvent);\n            const errors = resp.error || [];\n            errors.map(([entry_ref, reason]) => {\n              this.handleFailedEntryPreflight(entry_ref, reason, uploader);\n            });\n          } else {\n            const onError = (callback) => {\n              this.channel.onError(() => {\n                if (this.joinCount === joinCountAtUpload) {\n                  callback();\n                }\n              });\n            };\n            uploader.initAdapterUpload(resp, onError, this.liveSocket);\n          }\n        }).catch((error) => logError(\"Failed to push upload\", error));\n      });\n    }\n    handleFailedEntryPreflight(uploadRef, reason, uploader) {\n      if (uploader.isAutoUpload()) {\n        const entry = uploader.entries().find((entry2) => entry2.ref === uploadRef.toString());\n        if (entry) {\n          entry.cancel();\n        }\n      } else {\n        uploader.entries().map((entry) => entry.cancel());\n      }\n      this.log(\"upload\", () => [`error for entry ${uploadRef}`, reason]);\n    }\n    dispatchUploads(targetCtx, name, filesOrBlobs) {\n      const targetElement = this.targetCtxElement(targetCtx) || this.el;\n      const inputs = dom_default.findUploadInputs(targetElement).filter(\n        (el) => el.name === name\n      );\n      if (inputs.length === 0) {\n        logError(`no live file inputs found matching the name \"${name}\"`);\n      } else if (inputs.length > 1) {\n        logError(`duplicate live file inputs found matching the name \"${name}\"`);\n      } else {\n        dom_default.dispatchEvent(inputs[0], PHX_TRACK_UPLOADS, {\n          detail: { files: filesOrBlobs }\n        });\n      }\n    }\n    targetCtxElement(targetCtx) {\n      if (isCid(targetCtx)) {\n        const [target] = dom_default.findComponentNodeList(this.id, targetCtx);\n        return target;\n      } else if (targetCtx) {\n        return targetCtx;\n      } else {\n        return null;\n      }\n    }\n    pushFormRecovery(oldForm, newForm, templateDom, callback) {\n      const phxChange = this.binding(\"change\");\n      const phxTarget = newForm.getAttribute(this.binding(\"target\")) || newForm;\n      const phxEvent = newForm.getAttribute(this.binding(PHX_AUTO_RECOVER)) || newForm.getAttribute(this.binding(\"change\"));\n      const inputs = Array.from(oldForm.elements).filter(\n        (el) => dom_default.isFormInput(el) && el.name && !el.hasAttribute(phxChange)\n      );\n      if (inputs.length === 0) {\n        callback();\n        return;\n      }\n      inputs.forEach(\n        (input2) => input2.hasAttribute(PHX_UPLOAD_REF) && LiveUploader.clearFiles(input2)\n      );\n      const input = inputs.find((el) => el.type !== \"hidden\") || inputs[0];\n      let pending = 0;\n      this.withinTargets(\n        phxTarget,\n        (targetView, targetCtx) => {\n          const cid = this.targetComponentID(newForm, targetCtx);\n          pending++;\n          let e = new CustomEvent(\"phx:form-recovery\", {\n            detail: { sourceElement: oldForm }\n          });\n          js_default.exec(e, \"change\", phxEvent, this, input, [\n            \"push\",\n            {\n              _target: input.name,\n              targetView,\n              targetCtx,\n              newCid: cid,\n              callback: () => {\n                pending--;\n                if (pending === 0) {\n                  callback();\n                }\n              }\n            }\n          ]);\n        },\n        templateDom\n      );\n    }\n    pushLinkPatch(e, href, targetEl, callback) {\n      const linkRef = this.liveSocket.setPendingLink(href);\n      const loading = e.isTrusted && e.type !== \"popstate\";\n      const refGen = targetEl ? () => this.putRef(\n        [{ el: targetEl, loading, lock: true }],\n        null,\n        \"click\"\n      ) : null;\n      const fallback = () => this.liveSocket.redirect(window.location.href);\n      const url = href.startsWith(\"/\") ? `${location.protocol}//${location.host}${href}` : href;\n      this.pushWithReply(refGen, \"live_patch\", { url }).then(\n        ({ resp }) => {\n          this.liveSocket.requestDOMUpdate(() => {\n            if (resp.link_redirect) {\n              this.liveSocket.replaceMain(href, null, callback, linkRef);\n            } else if (resp.redirect) {\n              return;\n            } else {\n              if (this.liveSocket.commitPendingLink(linkRef)) {\n                this.href = href;\n              }\n              this.applyPendingUpdates();\n              callback && callback(linkRef);\n            }\n          });\n        },\n        ({ error: _error, timeout: _timeout }) => fallback()\n      );\n    }\n    getFormsForRecovery() {\n      if (this.joinCount === 0) {\n        return {};\n      }\n      const phxChange = this.binding(\"change\");\n      return dom_default.all(\n        document,\n        `#${CSS.escape(this.id)} form[${phxChange}], [${PHX_TELEPORTED_REF}=\"${CSS.escape(this.id)}\"] form[${phxChange}]`\n      ).filter((form) => form.id).filter((form) => form.elements.length > 0).filter(\n        (form) => form.getAttribute(this.binding(PHX_AUTO_RECOVER)) !== \"ignore\"\n      ).map((form) => {\n        const clonedForm = form.cloneNode(true);\n        morphdom_esm_default(clonedForm, form, {\n          onBeforeElUpdated: (fromEl, toEl) => {\n            dom_default.copyPrivates(fromEl, toEl);\n            if (fromEl.getAttribute(\"form\") === form.id) {\n              fromEl.parentNode.removeChild(fromEl);\n              return false;\n            }\n            return true;\n          }\n        });\n        const externalElements = document.querySelectorAll(\n          `[form=\"${CSS.escape(form.id)}\"]`\n        );\n        Array.from(externalElements).forEach((el) => {\n          const clonedEl = (\n            /** @type {HTMLElement} */\n            el.cloneNode(true)\n          );\n          morphdom_esm_default(clonedEl, el);\n          dom_default.copyPrivates(clonedEl, el);\n          clonedEl.removeAttribute(\"form\");\n          clonedForm.appendChild(clonedEl);\n        });\n        return clonedForm;\n      }).reduce((acc, form) => {\n        acc[form.id] = form;\n        return acc;\n      }, {});\n    }\n    maybePushComponentsDestroyed(destroyedCIDs) {\n      let willDestroyCIDs = destroyedCIDs.filter((cid) => {\n        return dom_default.findComponentNodeList(this.id, cid).length === 0;\n      });\n      const onError = (error) => {\n        if (!this.isDestroyed()) {\n          logError(\"Failed to push components destroyed\", error);\n        }\n      };\n      if (willDestroyCIDs.length > 0) {\n        willDestroyCIDs.forEach((cid) => this.rendered.resetRender(cid));\n        this.pushWithReply(null, \"cids_will_destroy\", { cids: willDestroyCIDs }).then(() => {\n          this.liveSocket.requestDOMUpdate(() => {\n            let completelyDestroyCIDs = willDestroyCIDs.filter((cid) => {\n              return dom_default.findComponentNodeList(this.id, cid).length === 0;\n            });\n            if (completelyDestroyCIDs.length > 0) {\n              this.pushWithReply(null, \"cids_destroyed\", {\n                cids: completelyDestroyCIDs\n              }).then(({ resp }) => {\n                this.rendered.pruneCIDs(resp.cids);\n              }).catch(onError);\n            }\n          });\n        }).catch(onError);\n      }\n    }\n    ownsElement(el) {\n      let parentViewEl = dom_default.closestViewEl(el);\n      return el.getAttribute(PHX_PARENT_ID) === this.id || parentViewEl && parentViewEl.id === this.id || !parentViewEl && this.isDead;\n    }\n    submitForm(form, targetCtx, phxEvent, submitter, opts = {}) {\n      dom_default.putPrivate(form, PHX_HAS_SUBMITTED, true);\n      const inputs = Array.from(form.elements);\n      inputs.forEach((input) => dom_default.putPrivate(input, PHX_HAS_SUBMITTED, true));\n      this.liveSocket.blurActiveElement(this);\n      this.pushFormSubmit(form, targetCtx, phxEvent, submitter, opts, () => {\n        this.liveSocket.restorePreviouslyActiveFocus();\n      });\n    }\n    binding(kind) {\n      return this.liveSocket.binding(kind);\n    }\n    // phx-portal\n    pushPortalElementId(id) {\n      this.portalElementIds.add(id);\n    }\n    dropPortalElementId(id) {\n      this.portalElementIds.delete(id);\n    }\n    destroyPortalElements() {\n      if (!this.liveSocket.unloaded) {\n        this.portalElementIds.forEach((id) => {\n          const el = document.getElementById(id);\n          if (el) {\n            el.remove();\n          }\n        });\n      }\n    }\n  };\n\n  // js/phoenix_live_view/live_socket.js\n  var isUsedInput = (el) => dom_default.isUsedInput(el);\n  var LiveSocket = class {\n    constructor(url, phxSocket, opts = {}) {\n      this.unloaded = false;\n      if (!phxSocket || phxSocket.constructor.name === \"Object\") {\n        throw new Error(`\n      a phoenix Socket must be provided as the second argument to the LiveSocket constructor. For example:\n\n          import {Socket} from \"phoenix\"\n          import {LiveSocket} from \"phoenix_live_view\"\n          let liveSocket = new LiveSocket(\"/live\", Socket, {...})\n      `);\n      }\n      this.socket = new phxSocket(url, opts);\n      this.bindingPrefix = opts.bindingPrefix || BINDING_PREFIX;\n      this.opts = opts;\n      this.params = closure(opts.params || {});\n      this.viewLogger = opts.viewLogger;\n      this.metadataCallbacks = opts.metadata || {};\n      this.defaults = Object.assign(clone(DEFAULTS), opts.defaults || {});\n      this.prevActive = null;\n      this.silenced = false;\n      this.main = null;\n      this.outgoingMainEl = null;\n      this.clickStartedAtTarget = null;\n      this.linkRef = 1;\n      this.roots = {};\n      this.href = window.location.href;\n      this.pendingLink = null;\n      this.currentLocation = clone(window.location);\n      this.hooks = opts.hooks || {};\n      this.uploaders = opts.uploaders || {};\n      this.loaderTimeout = opts.loaderTimeout || LOADER_TIMEOUT;\n      this.disconnectedTimeout = opts.disconnectedTimeout || DISCONNECTED_TIMEOUT;\n      this.reloadWithJitterTimer = null;\n      this.maxReloads = opts.maxReloads || MAX_RELOADS;\n      this.reloadJitterMin = opts.reloadJitterMin || RELOAD_JITTER_MIN;\n      this.reloadJitterMax = opts.reloadJitterMax || RELOAD_JITTER_MAX;\n      this.failsafeJitter = opts.failsafeJitter || FAILSAFE_JITTER;\n      this.localStorage = opts.localStorage || window.localStorage;\n      this.sessionStorage = opts.sessionStorage || window.sessionStorage;\n      this.boundTopLevelEvents = false;\n      this.boundEventNames = /* @__PURE__ */ new Set();\n      this.blockPhxChangeWhileComposing = opts.blockPhxChangeWhileComposing || false;\n      this.serverCloseRef = null;\n      this.domCallbacks = Object.assign(\n        {\n          jsQuerySelectorAll: null,\n          onPatchStart: closure(),\n          onPatchEnd: closure(),\n          onNodeAdded: closure(),\n          onBeforeElUpdated: closure()\n        },\n        opts.dom || {}\n      );\n      this.transitions = new TransitionSet();\n      this.currentHistoryPosition = parseInt(this.sessionStorage.getItem(PHX_LV_HISTORY_POSITION)) || 0;\n      window.addEventListener(\"pagehide\", (_e) => {\n        this.unloaded = true;\n      });\n      this.socket.onOpen(() => {\n        if (this.isUnloaded()) {\n          window.location.reload();\n        }\n      });\n    }\n    // public\n    version() {\n      return \"1.2.0-dev\";\n    }\n    isProfileEnabled() {\n      return this.sessionStorage.getItem(PHX_LV_PROFILE) === \"true\";\n    }\n    isDebugEnabled() {\n      return this.sessionStorage.getItem(PHX_LV_DEBUG) === \"true\";\n    }\n    isDebugDisabled() {\n      return this.sessionStorage.getItem(PHX_LV_DEBUG) === \"false\";\n    }\n    enableDebug() {\n      this.sessionStorage.setItem(PHX_LV_DEBUG, \"true\");\n    }\n    enableProfiling() {\n      this.sessionStorage.setItem(PHX_LV_PROFILE, \"true\");\n    }\n    disableDebug() {\n      this.sessionStorage.setItem(PHX_LV_DEBUG, \"false\");\n    }\n    disableProfiling() {\n      this.sessionStorage.removeItem(PHX_LV_PROFILE);\n    }\n    enableLatencySim(upperBoundMs) {\n      this.enableDebug();\n      console.log(\n        \"latency simulator enabled for the duration of this browser session. Call disableLatencySim() to disable\"\n      );\n      this.sessionStorage.setItem(PHX_LV_LATENCY_SIM, upperBoundMs);\n    }\n    disableLatencySim() {\n      this.sessionStorage.removeItem(PHX_LV_LATENCY_SIM);\n    }\n    getLatencySim() {\n      const str = this.sessionStorage.getItem(PHX_LV_LATENCY_SIM);\n      return str ? parseInt(str) : null;\n    }\n    getSocket() {\n      return this.socket;\n    }\n    connect() {\n      if (window.location.hostname === \"localhost\" && !this.isDebugDisabled()) {\n        this.enableDebug();\n      }\n      const doConnect = () => {\n        this.resetReloadStatus();\n        if (this.joinRootViews()) {\n          this.bindTopLevelEvents();\n          this.socket.connect();\n        } else if (this.main) {\n          this.socket.connect();\n        } else {\n          this.bindTopLevelEvents({ dead: true });\n        }\n        this.joinDeadView();\n      };\n      if ([\"complete\", \"loaded\", \"interactive\"].indexOf(document.readyState) >= 0) {\n        doConnect();\n      } else {\n        document.addEventListener(\"DOMContentLoaded\", () => doConnect());\n      }\n    }\n    disconnect(callback) {\n      clearTimeout(this.reloadWithJitterTimer);\n      if (this.serverCloseRef) {\n        this.socket.off(this.serverCloseRef);\n        this.serverCloseRef = null;\n      }\n      this.socket.disconnect(callback);\n    }\n    replaceTransport(transport) {\n      clearTimeout(this.reloadWithJitterTimer);\n      this.socket.replaceTransport(transport);\n      this.connect();\n    }\n    /**\n     * @param {HTMLElement} el\n     * @param {import(\"./js_commands\").EncodedJS} encodedJS\n     * @param {string | null} [eventType]\n     */\n    execJS(el, encodedJS, eventType = null) {\n      const e = new CustomEvent(\"phx:exec\", { detail: { sourceElement: el } });\n      this.owner(el, (view) => js_default.exec(e, eventType, encodedJS, view, el));\n    }\n    /**\n     * Returns an object with methods to manipulate the DOM and execute JavaScript.\n     * The applied changes integrate with server DOM patching.\n     *\n     * @returns {import(\"./js_commands\").LiveSocketJSCommands}\n     */\n    js() {\n      return js_commands_default(this, \"js\");\n    }\n    // private\n    unload() {\n      if (this.unloaded) {\n        return;\n      }\n      if (this.main && this.isConnected()) {\n        this.log(this.main, \"socket\", () => [\"disconnect for page nav\"]);\n      }\n      this.unloaded = true;\n      this.destroyAllViews();\n      this.disconnect();\n    }\n    triggerDOM(kind, args) {\n      this.domCallbacks[kind](...args);\n    }\n    time(name, func) {\n      if (!this.isProfileEnabled() || !console.time) {\n        return func();\n      }\n      console.time(name);\n      const result = func();\n      console.timeEnd(name);\n      return result;\n    }\n    log(view, kind, msgCallback) {\n      if (this.viewLogger) {\n        const [msg, obj] = msgCallback();\n        this.viewLogger(view, kind, msg, obj);\n      } else if (this.isDebugEnabled()) {\n        const [msg, obj] = msgCallback();\n        debug(view, kind, msg, obj);\n      }\n    }\n    requestDOMUpdate(callback) {\n      this.transitions.after(callback);\n    }\n    asyncTransition(promise) {\n      this.transitions.addAsyncTransition(promise);\n    }\n    transition(time, onStart, onDone = function() {\n    }) {\n      this.transitions.addTransition(time, onStart, onDone);\n    }\n    onChannel(channel, event, cb) {\n      channel.on(event, (data) => {\n        const latency = this.getLatencySim();\n        if (!latency) {\n          cb(data);\n        } else {\n          setTimeout(() => cb(data), latency);\n        }\n      });\n    }\n    reloadWithJitter(view, log) {\n      clearTimeout(this.reloadWithJitterTimer);\n      this.disconnect();\n      const minMs = this.reloadJitterMin;\n      const maxMs = this.reloadJitterMax;\n      let afterMs = Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs;\n      const tries = browser_default.updateLocal(\n        this.localStorage,\n        window.location.pathname,\n        CONSECUTIVE_RELOADS,\n        0,\n        (count) => count + 1\n      );\n      if (tries >= this.maxReloads) {\n        afterMs = this.failsafeJitter;\n      }\n      this.reloadWithJitterTimer = setTimeout(() => {\n        if (view.isDestroyed() || view.isConnected()) {\n          return;\n        }\n        view.destroy();\n        log ? log() : this.log(view, \"join\", () => [\n          `encountered ${tries} consecutive reloads`\n        ]);\n        if (tries >= this.maxReloads) {\n          this.log(view, \"join\", () => [\n            `exceeded ${this.maxReloads} consecutive reloads. Entering failsafe mode`\n          ]);\n        }\n        if (this.hasPendingLink()) {\n          window.location = this.pendingLink;\n        } else {\n          window.location.reload();\n        }\n      }, afterMs);\n    }\n    getHookDefinition(name) {\n      if (!name) {\n        return;\n      }\n      return this.maybeInternalHook(name) || this.hooks[name] || this.maybeRuntimeHook(name);\n    }\n    maybeInternalHook(name) {\n      return name && name.startsWith(\"Phoenix.\") && hooks_default[name.split(\".\")[1]];\n    }\n    maybeRuntimeHook(name) {\n      const runtimeHook = document.querySelector(\n        `script[${PHX_RUNTIME_HOOK}=\"${CSS.escape(name)}\"]`\n      );\n      if (!runtimeHook) {\n        return;\n      }\n      let callbacks = window[`phx_hook_${name}`];\n      if (!callbacks || typeof callbacks !== \"function\") {\n        logError(\"a runtime hook must be a function\", runtimeHook);\n        return;\n      }\n      const hookDefiniton = callbacks();\n      if (hookDefiniton && (typeof hookDefiniton === \"object\" || typeof hookDefiniton === \"function\")) {\n        return hookDefiniton;\n      }\n      logError(\n        \"runtime hook must return an object with hook callbacks or an instance of ViewHook\",\n        runtimeHook\n      );\n    }\n    isUnloaded() {\n      return this.unloaded;\n    }\n    isConnected() {\n      return this.socket.isConnected();\n    }\n    getBindingPrefix() {\n      return this.bindingPrefix;\n    }\n    binding(kind) {\n      return `${this.getBindingPrefix()}${kind}`;\n    }\n    channel(topic, params) {\n      return this.socket.channel(topic, params);\n    }\n    joinDeadView() {\n      const body = document.body;\n      if (body && !this.isPhxView(body) && !this.isPhxView(document.firstElementChild)) {\n        const view = this.newRootView(body);\n        view.setHref(this.getHref());\n        view.joinDead();\n        if (!this.main) {\n          this.main = view;\n        }\n        window.requestAnimationFrame(() => {\n          var _a;\n          view.execNewMounted();\n          this.maybeScroll((_a = history.state) == null ? void 0 : _a.scroll);\n        });\n      }\n    }\n    joinRootViews() {\n      let rootsFound = false;\n      dom_default.all(\n        document,\n        `${PHX_VIEW_SELECTOR}:not([${PHX_PARENT_ID}])`,\n        (rootEl) => {\n          if (!this.getRootById(rootEl.id)) {\n            const view = this.newRootView(rootEl);\n            if (!dom_default.isPhxSticky(rootEl)) {\n              view.setHref(this.getHref());\n            }\n            view.join();\n            if (rootEl.hasAttribute(PHX_MAIN)) {\n              this.main = view;\n            }\n          }\n          rootsFound = true;\n        }\n      );\n      return rootsFound;\n    }\n    redirect(to, flash, reloadToken) {\n      if (reloadToken) {\n        browser_default.setCookie(PHX_RELOAD_STATUS, reloadToken, 60);\n      }\n      this.unload();\n      browser_default.redirect(to, flash);\n    }\n    replaceMain(href, flash, callback = null, linkRef = this.setPendingLink(href)) {\n      const liveReferer = this.currentLocation.href;\n      this.outgoingMainEl = this.outgoingMainEl || this.main.el;\n      const stickies = dom_default.findPhxSticky(document) || [];\n      const removeEls = dom_default.all(\n        this.outgoingMainEl,\n        `[${this.binding(\"remove\")}]`\n      ).filter((el) => !dom_default.isChildOfAny(el, stickies));\n      const newMainEl = dom_default.cloneNode(this.outgoingMainEl, \"\");\n      this.main.showLoader(this.loaderTimeout);\n      this.main.destroy();\n      this.main = this.newRootView(newMainEl, flash, liveReferer);\n      this.main.setRedirect(href);\n      this.transitionRemoves(removeEls);\n      this.main.join((joinCount, onDone) => {\n        if (joinCount === 1 && this.commitPendingLink(linkRef)) {\n          this.requestDOMUpdate(() => {\n            removeEls.forEach((el) => el.remove());\n            stickies.forEach((el) => newMainEl.appendChild(el));\n            this.outgoingMainEl.replaceWith(newMainEl);\n            this.outgoingMainEl = null;\n            callback && callback(linkRef);\n            onDone();\n          });\n        }\n      });\n    }\n    transitionRemoves(elements, callback) {\n      const removeAttr = this.binding(\"remove\");\n      const silenceEvents = (e) => {\n        e.preventDefault();\n        e.stopImmediatePropagation();\n      };\n      elements.forEach((el) => {\n        for (const event of this.boundEventNames) {\n          el.addEventListener(event, silenceEvents, true);\n        }\n        this.execJS(el, el.getAttribute(removeAttr), \"remove\");\n      });\n      this.requestDOMUpdate(() => {\n        elements.forEach((el) => {\n          for (const event of this.boundEventNames) {\n            el.removeEventListener(event, silenceEvents, true);\n          }\n        });\n        callback && callback();\n      });\n    }\n    isPhxView(el) {\n      return el.getAttribute && el.getAttribute(PHX_SESSION) !== null;\n    }\n    newRootView(el, flash, liveReferer) {\n      const view = new View(el, this, null, flash, liveReferer);\n      this.roots[view.id] = view;\n      return view;\n    }\n    owner(childEl, callback) {\n      let view;\n      const viewEl = dom_default.closestViewEl(childEl);\n      if (viewEl) {\n        view = this.getViewByEl(viewEl);\n      } else {\n        if (!childEl.isConnected) {\n          return null;\n        }\n        view = this.main;\n      }\n      return view && callback ? callback(view) : view;\n    }\n    withinOwners(childEl, callback) {\n      this.owner(childEl, (view) => callback(view, childEl));\n    }\n    getViewByEl(el) {\n      const rootId = el.getAttribute(PHX_ROOT_ID);\n      return maybe(\n        this.getRootById(rootId),\n        (root) => root.getDescendentByEl(el)\n      );\n    }\n    getRootById(id) {\n      return this.roots[id];\n    }\n    destroyAllViews() {\n      for (const id in this.roots) {\n        this.roots[id].destroy();\n        delete this.roots[id];\n      }\n      this.main = null;\n    }\n    destroyViewByEl(el) {\n      const root = this.getRootById(el.getAttribute(PHX_ROOT_ID));\n      if (root && root.id === el.id) {\n        root.destroy();\n        delete this.roots[root.id];\n      } else if (root) {\n        root.destroyDescendent(el.id);\n      }\n    }\n    getActiveElement() {\n      return document.activeElement;\n    }\n    dropActiveElement(view) {\n      if (this.prevActive && view.ownsElement(this.prevActive)) {\n        this.prevActive = null;\n      }\n    }\n    restorePreviouslyActiveFocus() {\n      if (this.prevActive && this.prevActive !== document.body && this.prevActive instanceof HTMLElement) {\n        this.prevActive.focus();\n      }\n    }\n    blurActiveElement() {\n      this.prevActive = this.getActiveElement();\n      if (this.prevActive !== document.body && this.prevActive instanceof HTMLElement) {\n        this.prevActive.blur();\n      }\n    }\n    /**\n     * @param {{dead?: boolean}} [options={}]\n     */\n    bindTopLevelEvents({ dead } = {}) {\n      if (this.boundTopLevelEvents) {\n        return;\n      }\n      this.boundTopLevelEvents = true;\n      this.serverCloseRef = this.socket.onClose((event) => {\n        if (event && event.code === 1e3 && this.main) {\n          return this.reloadWithJitter(this.main);\n        }\n      });\n      document.body.addEventListener(\"click\", function() {\n      });\n      window.addEventListener(\n        \"pageshow\",\n        (e) => {\n          if (e.persisted) {\n            this.getSocket().disconnect();\n            this.withPageLoading({ to: window.location.href, kind: \"redirect\" });\n            window.location.reload();\n          }\n        },\n        true\n      );\n      if (!dead) {\n        this.bindNav();\n      }\n      this.bindClicks();\n      if (!dead) {\n        this.bindForms();\n      }\n      this.bind(\n        { keyup: \"keyup\", keydown: \"keydown\" },\n        (e, type, view, targetEl, phxEvent, _phxTarget) => {\n          const matchKey = targetEl.getAttribute(this.binding(PHX_KEY));\n          const pressedKey = e.key && e.key.toLowerCase();\n          if (matchKey && matchKey.toLowerCase() !== pressedKey) {\n            return;\n          }\n          const data = __spreadValues({ key: e.key }, this.eventMeta(type, e, targetEl));\n          js_default.exec(e, type, phxEvent, view, targetEl, [\"push\", { data }]);\n        }\n      );\n      this.bind(\n        { blur: \"focusout\", focus: \"focusin\" },\n        (e, type, view, targetEl, phxEvent, phxTarget) => {\n          if (!phxTarget) {\n            const data = __spreadValues({ key: e.key }, this.eventMeta(type, e, targetEl));\n            js_default.exec(e, type, phxEvent, view, targetEl, [\"push\", { data }]);\n          }\n        }\n      );\n      this.bind(\n        { blur: \"blur\", focus: \"focus\" },\n        (e, type, view, targetEl, phxEvent, phxTarget) => {\n          if (phxTarget === \"window\") {\n            const data = this.eventMeta(type, e, targetEl);\n            js_default.exec(e, type, phxEvent, view, targetEl, [\"push\", { data }]);\n          }\n        }\n      );\n      this.on(\"dragover\", (e) => e.preventDefault());\n      this.on(\"dragenter\", (e) => {\n        const dropzone = closestPhxBinding(\n          e.target,\n          this.binding(PHX_DROP_TARGET)\n        );\n        if (!dropzone || !(dropzone instanceof HTMLElement)) {\n          return;\n        }\n        if (eventContainsFiles(e)) {\n          this.js().addClass(dropzone, PHX_DROP_TARGET_ACTIVE_CLASS);\n        }\n      });\n      this.on(\"dragleave\", (e) => {\n        const dropzone = closestPhxBinding(\n          e.target,\n          this.binding(PHX_DROP_TARGET)\n        );\n        if (!dropzone || !(dropzone instanceof HTMLElement)) {\n          return;\n        }\n        const rect = dropzone.getBoundingClientRect();\n        if (e.clientX <= rect.left || e.clientX >= rect.right || e.clientY <= rect.top || e.clientY >= rect.bottom) {\n          this.js().removeClass(dropzone, PHX_DROP_TARGET_ACTIVE_CLASS);\n        }\n      });\n      this.on(\"drop\", (e) => {\n        e.preventDefault();\n        const dropzone = closestPhxBinding(\n          e.target,\n          this.binding(PHX_DROP_TARGET)\n        );\n        if (!dropzone || !(dropzone instanceof HTMLElement)) {\n          return;\n        }\n        this.js().removeClass(dropzone, PHX_DROP_TARGET_ACTIVE_CLASS);\n        const dropTargetId = dropzone.getAttribute(this.binding(PHX_DROP_TARGET));\n        const dropTarget = dropTargetId && document.getElementById(dropTargetId);\n        const files = Array.from(e.dataTransfer.files || []);\n        if (!dropTarget || !(dropTarget instanceof HTMLInputElement) || dropTarget.disabled || files.length === 0 || !(dropTarget.files instanceof FileList)) {\n          return;\n        }\n        LiveUploader.trackFiles(dropTarget, files, e.dataTransfer);\n        dropTarget.dispatchEvent(new Event(\"input\", { bubbles: true }));\n      });\n      this.on(PHX_TRACK_UPLOADS, (e) => {\n        const uploadTarget = e.target;\n        if (!dom_default.isUploadInput(uploadTarget)) {\n          return;\n        }\n        const files = Array.from(e.detail.files || []).filter(\n          (f) => f instanceof File || f instanceof Blob\n        );\n        LiveUploader.trackFiles(uploadTarget, files);\n        uploadTarget.dispatchEvent(new Event(\"input\", { bubbles: true }));\n      });\n    }\n    eventMeta(eventName, e, targetEl) {\n      const callback = this.metadataCallbacks[eventName];\n      return callback ? callback(e, targetEl) : {};\n    }\n    setPendingLink(href) {\n      this.linkRef++;\n      this.pendingLink = href;\n      this.resetReloadStatus();\n      return this.linkRef;\n    }\n    // anytime we are navigating or connecting, drop reload cookie in case\n    // we issue the cookie but the next request was interrupted and the server never dropped it\n    resetReloadStatus() {\n      browser_default.deleteCookie(PHX_RELOAD_STATUS);\n    }\n    commitPendingLink(linkRef) {\n      if (this.linkRef !== linkRef) {\n        return false;\n      } else {\n        this.href = this.pendingLink;\n        this.pendingLink = null;\n        return true;\n      }\n    }\n    getHref() {\n      return this.href;\n    }\n    hasPendingLink() {\n      return !!this.pendingLink;\n    }\n    bind(events, callback) {\n      for (const event in events) {\n        const browserEventName = events[event];\n        this.on(browserEventName, (e) => {\n          const binding = this.binding(event);\n          const windowBinding = this.binding(`window-${event}`);\n          const targetPhxEvent = e.target.getAttribute && e.target.getAttribute(binding);\n          if (targetPhxEvent) {\n            this.debounce(e.target, e, browserEventName, () => {\n              this.withinOwners(e.target, (view) => {\n                callback(e, event, view, e.target, targetPhxEvent, null);\n              });\n            });\n          } else {\n            dom_default.all(document, `[${windowBinding}]`, (el) => {\n              const phxEvent = el.getAttribute(windowBinding);\n              this.debounce(el, e, browserEventName, () => {\n                this.withinOwners(el, (view) => {\n                  callback(e, event, view, el, phxEvent, \"window\");\n                });\n              });\n            });\n          }\n        });\n      }\n    }\n    bindClicks() {\n      this.on(\"mousedown\", (e) => this.clickStartedAtTarget = e.target);\n      this.bindClick(\"click\", \"click\");\n    }\n    bindClick(eventName, bindingName) {\n      const click = this.binding(bindingName);\n      window.addEventListener(\n        eventName,\n        (e) => {\n          let target = null;\n          if (e.detail === 0)\n            this.clickStartedAtTarget = e.target;\n          const clickStartedAtTarget = this.clickStartedAtTarget || e.target;\n          target = closestPhxBinding(e.target, click);\n          this.dispatchClickAway(e, clickStartedAtTarget);\n          this.clickStartedAtTarget = null;\n          const phxEvent = target && target.getAttribute(click);\n          if (!phxEvent) {\n            if (dom_default.isNewPageClick(e, window.location)) {\n              this.unload();\n            }\n            return;\n          }\n          if (target.getAttribute(\"href\") === \"#\") {\n            e.preventDefault();\n          }\n          if (target.hasAttribute(PHX_REF_SRC)) {\n            return;\n          }\n          this.debounce(target, e, \"click\", () => {\n            this.withinOwners(target, (view) => {\n              js_default.exec(e, \"click\", phxEvent, view, target, [\n                \"push\",\n                { data: this.eventMeta(\"click\", e, target) }\n              ]);\n            });\n          });\n        },\n        false\n      );\n    }\n    dispatchClickAway(e, clickStartedAt) {\n      const phxClickAway = this.binding(\"click-away\");\n      const portal = clickStartedAt.closest(`[${PHX_TELEPORTED_SRC}]`);\n      const portalStartedAt = portal && dom_default.byId(portal.getAttribute(PHX_TELEPORTED_SRC));\n      dom_default.all(document, `[${phxClickAway}]`, (el) => {\n        let startedAt = clickStartedAt;\n        if (portal && !portal.contains(el)) {\n          startedAt = portalStartedAt;\n        }\n        if (!(el.isSameNode(startedAt) || el.contains(startedAt) || // When clicking a link with custom method,\n        // phoenix_html triggers a click on a submit button\n        // of a hidden form appended to the body. For such cases\n        // where the clicked target is hidden, we skip click-away.\n        //\n        // Also, when we have a portal, we don't want to check the visibility\n        // of the portal source, as it's a <template> that is always not visible.\n        // Instead, check the visibility of the original click target.\n        !js_default.isVisible(clickStartedAt))) {\n          this.withinOwners(el, (view) => {\n            const phxEvent = el.getAttribute(phxClickAway);\n            if (js_default.isVisible(el) && js_default.isInViewport(el)) {\n              js_default.exec(e, \"click\", phxEvent, view, el, [\n                \"push\",\n                { data: this.eventMeta(\"click\", e, e.target) }\n              ]);\n            }\n          });\n        }\n      });\n    }\n    bindNav() {\n      if (!browser_default.canPushState()) {\n        return;\n      }\n      if (history.scrollRestoration) {\n        history.scrollRestoration = \"manual\";\n      }\n      let scrollTimer = null;\n      window.addEventListener(\"scroll\", (_e) => {\n        clearTimeout(scrollTimer);\n        scrollTimer = setTimeout(() => {\n          browser_default.updateCurrentState(\n            (state) => Object.assign(state, { scroll: window.scrollY })\n          );\n        }, 100);\n      });\n      window.addEventListener(\n        \"popstate\",\n        (event) => {\n          if (!this.registerNewLocation(window.location)) {\n            return;\n          }\n          const { type, backType, id, scroll, position } = event.state || {};\n          const href = window.location.href;\n          const isForward = position > this.currentHistoryPosition;\n          const navType = isForward ? type : backType || type;\n          this.currentHistoryPosition = position || 0;\n          this.sessionStorage.setItem(\n            PHX_LV_HISTORY_POSITION,\n            this.currentHistoryPosition.toString()\n          );\n          dom_default.dispatchEvent(window, \"phx:navigate\", {\n            detail: {\n              href,\n              patch: navType === \"patch\",\n              pop: true,\n              direction: isForward ? \"forward\" : \"backward\"\n            }\n          });\n          this.requestDOMUpdate(() => {\n            const callback = () => {\n              this.maybeScroll(scroll);\n            };\n            if (this.main.isConnected() && navType === \"patch\" && id === this.main.id) {\n              this.main.pushLinkPatch(event, href, null, callback);\n            } else {\n              this.replaceMain(href, null, callback);\n            }\n          });\n        },\n        false\n      );\n      window.addEventListener(\n        \"click\",\n        (e) => {\n          const target = closestPhxBinding(e.target, PHX_LIVE_LINK);\n          const type = target && target.getAttribute(PHX_LIVE_LINK);\n          if (!type || !this.isConnected() || !this.main || dom_default.wantsNewTab(e)) {\n            return;\n          }\n          const href = target.href instanceof SVGAnimatedString ? target.href.baseVal : target.href;\n          const linkState = target.getAttribute(PHX_LINK_STATE);\n          e.preventDefault();\n          e.stopImmediatePropagation();\n          if (this.pendingLink === href) {\n            return;\n          }\n          this.requestDOMUpdate(() => {\n            if (type === \"patch\") {\n              this.pushHistoryPatch(e, href, linkState, target);\n            } else if (type === \"redirect\") {\n              this.historyRedirect(e, href, linkState, null, target);\n            } else {\n              throw new Error(\n                `expected ${PHX_LIVE_LINK} to be \"patch\" or \"redirect\", got: ${type}`\n              );\n            }\n            const phxClick = target.getAttribute(this.binding(\"click\"));\n            if (phxClick) {\n              this.requestDOMUpdate(() => this.execJS(target, phxClick, \"click\"));\n            }\n          });\n        },\n        false\n      );\n    }\n    maybeScroll(scroll) {\n      if (typeof scroll === \"number\") {\n        requestAnimationFrame(() => {\n          window.scrollTo(0, scroll);\n        });\n      }\n    }\n    dispatchEvent(event, payload = {}) {\n      dom_default.dispatchEvent(window, `phx:${event}`, { detail: payload });\n    }\n    dispatchEvents(events) {\n      events.forEach(([event, payload]) => this.dispatchEvent(event, payload));\n    }\n    withPageLoading(info, callback) {\n      dom_default.dispatchEvent(window, \"phx:page-loading-start\", { detail: info });\n      const done = () => dom_default.dispatchEvent(window, \"phx:page-loading-stop\", { detail: info });\n      return callback ? callback(done) : done;\n    }\n    pushHistoryPatch(e, href, linkState, targetEl) {\n      if (!this.isConnected() || !this.main.isMain()) {\n        return browser_default.redirect(href);\n      }\n      this.withPageLoading({ to: href, kind: \"patch\" }, (done) => {\n        this.main.pushLinkPatch(e, href, targetEl, (linkRef) => {\n          this.historyPatch(href, linkState, linkRef);\n          done();\n        });\n      });\n    }\n    historyPatch(href, linkState, linkRef = this.setPendingLink(href)) {\n      if (!this.commitPendingLink(linkRef)) {\n        return;\n      }\n      this.currentHistoryPosition++;\n      this.sessionStorage.setItem(\n        PHX_LV_HISTORY_POSITION,\n        this.currentHistoryPosition.toString()\n      );\n      browser_default.updateCurrentState((state) => __spreadProps(__spreadValues({}, state), { backType: \"patch\" }));\n      browser_default.pushState(\n        linkState,\n        {\n          type: \"patch\",\n          id: this.main.id,\n          position: this.currentHistoryPosition\n        },\n        href\n      );\n      dom_default.dispatchEvent(window, \"phx:navigate\", {\n        detail: { patch: true, href, pop: false, direction: \"forward\" }\n      });\n      this.registerNewLocation(window.location);\n    }\n    historyRedirect(e, href, linkState, flash, targetEl) {\n      const clickLoading = targetEl && e.isTrusted && e.type !== \"popstate\";\n      if (clickLoading) {\n        targetEl.classList.add(\"phx-click-loading\");\n      }\n      if (!this.isConnected() || !this.main.isMain()) {\n        return browser_default.redirect(href, flash);\n      }\n      if (/^\\/$|^\\/[^\\/]+.*$/.test(href)) {\n        const { protocol, host } = window.location;\n        href = `${protocol}//${host}${href}`;\n      }\n      const scroll = window.scrollY;\n      this.withPageLoading({ to: href, kind: \"redirect\" }, (done) => {\n        this.replaceMain(href, flash, (linkRef) => {\n          if (linkRef === this.linkRef) {\n            this.currentHistoryPosition++;\n            this.sessionStorage.setItem(\n              PHX_LV_HISTORY_POSITION,\n              this.currentHistoryPosition.toString()\n            );\n            browser_default.updateCurrentState((state) => __spreadProps(__spreadValues({}, state), {\n              backType: \"redirect\"\n            }));\n            browser_default.pushState(\n              linkState,\n              {\n                type: \"redirect\",\n                id: this.main.id,\n                scroll,\n                position: this.currentHistoryPosition\n              },\n              href\n            );\n            dom_default.dispatchEvent(window, \"phx:navigate\", {\n              detail: { href, patch: false, pop: false, direction: \"forward\" }\n            });\n            this.registerNewLocation(window.location);\n          }\n          if (clickLoading) {\n            targetEl.classList.remove(\"phx-click-loading\");\n          }\n          done();\n        });\n      });\n    }\n    registerNewLocation(newLocation) {\n      const { pathname, search } = this.currentLocation;\n      if (pathname + search === newLocation.pathname + newLocation.search) {\n        return false;\n      } else {\n        this.currentLocation = clone(newLocation);\n        return true;\n      }\n    }\n    bindForms() {\n      let iterations = 0;\n      let externalFormSubmitted = false;\n      this.on(\"submit\", (e) => {\n        const phxSubmit = e.target.getAttribute(this.binding(\"submit\"));\n        const phxChange = e.target.getAttribute(this.binding(\"change\"));\n        if (!externalFormSubmitted && phxChange && !phxSubmit) {\n          externalFormSubmitted = true;\n          e.preventDefault();\n          this.withinOwners(e.target, (view) => {\n            view.disableForm(e.target);\n            window.requestAnimationFrame(() => {\n              if (dom_default.isUnloadableFormSubmit(e)) {\n                this.unload();\n              }\n              e.target.submit();\n            });\n          });\n        }\n      });\n      this.on(\"submit\", (e) => {\n        const phxEvent = e.target.getAttribute(this.binding(\"submit\"));\n        if (!phxEvent) {\n          if (dom_default.isUnloadableFormSubmit(e)) {\n            this.unload();\n          }\n          return;\n        }\n        e.preventDefault();\n        e.target.disabled = true;\n        this.withinOwners(e.target, (view) => {\n          js_default.exec(e, \"submit\", phxEvent, view, e.target, [\n            \"push\",\n            { submitter: e.submitter }\n          ]);\n        });\n      });\n      for (const type of [\"change\", \"input\"]) {\n        this.on(type, (e) => {\n          if (e instanceof CustomEvent && (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement || e.target instanceof HTMLTextAreaElement) && e.target.form === void 0) {\n            if (e.detail && e.detail.dispatcher) {\n              throw new Error(\n                `dispatching a custom ${type} event is only supported on input elements inside a form`\n              );\n            }\n            return;\n          }\n          const phxChange = this.binding(\"change\");\n          const input = e.target;\n          if (this.blockPhxChangeWhileComposing && e.isComposing) {\n            const key = `composition-listener-${type}`;\n            if (!dom_default.private(input, key)) {\n              dom_default.putPrivate(input, key, true);\n              input.addEventListener(\n                \"compositionend\",\n                () => {\n                  input.dispatchEvent(new Event(type, { bubbles: true }));\n                  dom_default.deletePrivate(input, key);\n                },\n                { once: true }\n              );\n            }\n            return;\n          }\n          const inputEvent = input.getAttribute(phxChange);\n          const formEvent = input.form && input.form.getAttribute(phxChange);\n          const phxEvent = inputEvent || formEvent;\n          if (!phxEvent) {\n            return;\n          }\n          if (input.type === \"number\" && input.validity && input.validity.badInput) {\n            return;\n          }\n          const dispatcher = inputEvent ? input : input.form;\n          const currentIterations = iterations;\n          iterations++;\n          const { at, type: lastType } = dom_default.private(input, \"prev-iteration\") || {};\n          if (at === currentIterations - 1 && type === \"change\" && lastType === \"input\") {\n            return;\n          }\n          dom_default.putPrivate(input, \"prev-iteration\", {\n            at: currentIterations,\n            type\n          });\n          this.debounce(input, e, type, () => {\n            this.withinOwners(dispatcher, (view) => {\n              dom_default.putPrivate(input, PHX_HAS_FOCUSED, true);\n              js_default.exec(e, \"change\", phxEvent, view, input, [\n                \"push\",\n                { _target: e.target.name, dispatcher }\n              ]);\n            });\n          });\n        });\n      }\n      this.on(\"reset\", (e) => {\n        const form = e.target;\n        dom_default.resetForm(form);\n        const input = Array.from(form.elements).find((el) => el.type === \"reset\");\n        if (input) {\n          window.requestAnimationFrame(() => {\n            input.dispatchEvent(\n              new Event(\"input\", { bubbles: true, cancelable: false })\n            );\n          });\n        }\n      });\n    }\n    debounce(el, event, eventType, callback) {\n      if (eventType === \"blur\" || eventType === \"focusout\") {\n        return callback();\n      }\n      const phxDebounce = this.binding(PHX_DEBOUNCE);\n      const phxThrottle = this.binding(PHX_THROTTLE);\n      const defaultDebounce = this.defaults.debounce.toString();\n      const defaultThrottle = this.defaults.throttle.toString();\n      this.withinOwners(el, (view) => {\n        const asyncFilter = () => !view.isDestroyed() && document.body.contains(el);\n        dom_default.debounce(\n          el,\n          event,\n          phxDebounce,\n          defaultDebounce,\n          phxThrottle,\n          defaultThrottle,\n          asyncFilter,\n          () => {\n            callback();\n          }\n        );\n      });\n    }\n    silenceEvents(callback) {\n      this.silenced = true;\n      callback();\n      this.silenced = false;\n    }\n    on(event, callback) {\n      this.boundEventNames.add(event);\n      window.addEventListener(event, (e) => {\n        if (!this.silenced) {\n          callback(e);\n        }\n      });\n    }\n    jsQuerySelectorAll(sourceEl, query, defaultQuery) {\n      const all = this.domCallbacks.jsQuerySelectorAll;\n      return all ? all(sourceEl, query, defaultQuery) : defaultQuery();\n    }\n  };\n  var TransitionSet = class {\n    constructor() {\n      this.transitions = /* @__PURE__ */ new Set();\n      this.promises = /* @__PURE__ */ new Set();\n      this.pendingOps = [];\n    }\n    reset() {\n      this.transitions.forEach((timer) => {\n        clearTimeout(timer);\n        this.transitions.delete(timer);\n      });\n      this.promises.clear();\n      this.flushPendingOps();\n    }\n    after(callback) {\n      if (this.size() === 0) {\n        callback();\n      } else {\n        this.pushPendingOp(callback);\n      }\n    }\n    addTransition(time, onStart, onDone) {\n      onStart();\n      const timer = setTimeout(() => {\n        this.transitions.delete(timer);\n        onDone();\n        this.flushPendingOps();\n      }, time);\n      this.transitions.add(timer);\n    }\n    addAsyncTransition(promise) {\n      this.promises.add(promise);\n      promise.then(() => {\n        this.promises.delete(promise);\n        this.flushPendingOps();\n      });\n    }\n    pushPendingOp(op) {\n      this.pendingOps.push(op);\n    }\n    size() {\n      return this.transitions.size + this.promises.size;\n    }\n    flushPendingOps() {\n      if (this.size() > 0) {\n        return;\n      }\n      const op = this.pendingOps.shift();\n      if (op) {\n        op();\n        this.flushPendingOps();\n      }\n    }\n  };\n\n  // js/phoenix_live_view/index.ts\n  var LiveSocket2 = LiveSocket;\n  function createHook(el, callbacks) {\n    let existingHook = dom_default.getCustomElHook(el);\n    if (existingHook) {\n      return existingHook;\n    }\n    if (!el.hasAttribute(\"id\")) {\n      logError(\n        \"Elements passed to createHook need to have a unique id attribute\",\n        el\n      );\n    }\n    let hook = new ViewHook(View.closestView(el), el, callbacks);\n    dom_default.putCustomElHook(el, hook);\n    return hook;\n  }\n  return __toCommonJS(phoenix_live_view_exports);\n})();\n"
  },
  {
    "path": "setupTests.js",
    "content": "import \"css.escape\";\n"
  },
  {
    "path": "test/e2e/.prettierignore",
    "content": "test-results/\n"
  },
  {
    "path": "test/e2e/README.md",
    "content": "# End-to-end tests\n\nThis directory contains end-to-end tests that use the [Playwright](https://playwright.dev/)\ntest framework.\nThese tests use all three web engines (Chromium, Firefox, Webkit) and test the interaction\nwith an actual LiveView server.\n\n## Running the tests\n\nTo run the tests, ensure that the npm dependencies are installed by running `npm install`, followed by `npx playwright install` in\nthe root of the repository. Then, run `npm run e2e:test` to run the tests.\n\nThis will execute the `npx playwright test` command in the `test/e2e` directory. Playwright\nwill start a LiveView server using the `MIX_ENV=e2e mix run test/e2e/test_helper.exs` command.\n\nPlaywright supports an [interactive UI mode](https://playwright.dev/docs/test-ui-mode) that\ncan be used to debug the tests. To run the tests in this mode, run `npm run e2e:test -- --ui`.\n\nTests can also be run in headed mode by passing the `--headed` flag. This is especially useful\nin combination with running only specific tests, for example:\n\n```bash\nnpm run e2e:test -- tests/streams.spec.js:9 --project chromium --headed\n```\n\nTo step through a single test, pass `--debug`, which will automatically run the test in headed\nmode:\n\n```bash\nnpm run e2e:test -- tests/streams.spec.js:9 --project chromium --debug\n```\n"
  },
  {
    "path": "test/e2e/merge-coverage.js",
    "content": "import { CoverageReport } from \"monocart-coverage-reports\";\n\nconst coverageOptions = {\n  name: \"Phoenix LiveView JS Coverage\",\n  inputDir: [\"./coverage/raw\", \"./test/e2e/test-results/coverage/raw\"],\n  outputDir: \"./cover/merged-js\",\n  reports: [[\"v8\"], [\"console-summary\"]],\n  sourcePath: (filePath) => {\n    if (!filePath.startsWith(\"assets\")) {\n      return \"assets/js/phoenix_live_view/\" + filePath;\n    } else {\n      return filePath;\n    }\n  },\n};\nawait new CoverageReport(coverageOptions).generate();\n"
  },
  {
    "path": "test/e2e/playwright.config.js",
    "content": "// playwright.config.js\n// @ts-check\nimport { devices } from \"@playwright/test\";\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n/** @type {import(\"@playwright/test\").ReporterDescription} */\nconst monocartReporter = [\n  \"monocart-reporter\",\n  {\n    name: \"Phoenix LiveView\",\n    outputFile: \"./test-results/report.html\",\n    coverage: {\n      reports: [[\"raw\", { outputDir: \"./raw\" }], [\"v8\"]],\n      entryFilter: (entry) =>\n        entry.url.indexOf(\"phoenix_live_view.esm.js\") !== -1,\n    },\n  },\n];\n\n/** @type {import(\"@playwright/test\").PlaywrightTestConfig} */\nconst config = {\n  forbidOnly: !!process.env.CI,\n  retries: process.env.CI ? 2 : 0,\n  reporter: process.env.CI\n    ? [[\"github\"], [\"html\"], [\"dot\"], monocartReporter]\n    : [[\"list\"], monocartReporter],\n  use: {\n    trace: \"retain-on-failure\",\n    screenshot: \"only-on-failure\",\n    baseURL: \"http://localhost:4004/\",\n    ignoreHTTPSErrors: true,\n  },\n  webServer: {\n    command: \"npm run e2e:server\",\n    url: \"http://127.0.0.1:4004/health\",\n    reuseExistingServer: !process.env.CI,\n    stdout: \"pipe\",\n    stderr: \"pipe\",\n  },\n  projects: [\n    {\n      name: \"chromium\",\n      use: { ...devices[\"Desktop Chrome\"] },\n    },\n    {\n      name: \"firefox\",\n      use: { ...devices[\"Desktop Firefox\"] },\n    },\n    {\n      name: \"webkit\",\n      use: { ...devices[\"Desktop Safari\"] },\n    },\n  ],\n  outputDir: \"test-results\",\n  globalTeardown: resolve(__dirname, \"./teardown.js\"),\n};\n\nexport default config;\n"
  },
  {
    "path": "test/e2e/support/colocated_live.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.ColocatedLive do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  defmodule SyntaxHighlight do\n    @behaviour Phoenix.Component.MacroComponent\n\n    @impl true\n    def transform({\"pre\", attrs, children, _tag_meta}, _meta) do\n      code = Phoenix.Component.MacroComponent.ast_to_string(children)\n      lang = Map.new(attrs)[\"language\"] || raise ArgumentError, \"language attribute is required\"\n      html_doc = highlight(String.trim_leading(code), lang)\n\n      stylesheet =\n        Makeup.Styles.HTML.Style.stylesheet(Makeup.Styles.HTML.StyleMap.monokai_style())\n\n      {:ok,\n       {\"pre\", [{\"class\", \"highlight\"} | attrs],\n        [\n          {\"style\", [], [stylesheet, \".highlight { padding: 8px; border-radius: 4px; }\"], %{}},\n          html_doc\n        ], %{}}}\n    end\n\n    defp highlight(code, lang) do\n      Application.ensure_all_started([:makeup, :makeup_elixir, :makeup_eex, :makeup_syntect])\n\n      case Makeup.Registry.get_lexer_by_name(lang) do\n        {lexer, opts} ->\n          Makeup.highlight_inner_html(code, lexer: lexer, lexer_options: opts)\n\n        _ ->\n          code\n      end\n    end\n  end\n\n  alias Phoenix.LiveView.ColocatedHook, as: Hook\n  alias Phoenix.LiveView.JS\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, :phone, nil)}\n  end\n\n  def handle_event(\"submit-phone\", params, socket) do\n    {:noreply, assign(socket, :phone, params[\"user\"][\"phone_number\"])}\n  end\n\n  def handle_event(\"push-js\", _params, socket) do\n    {:noreply, push_js_cmd(socket, JS.toggle(to: \"#hello\"))}\n  end\n\n  def render(\"live.html\", assigns) do\n    ~H\"\"\"\n    <meta name=\"csrf-token\" content={Plug.CSRFProtection.get_csrf_token()} />\n    <script src=\"/assets/phoenix/phoenix.min.js\">\n    </script>\n    <script type=\"module\">\n      import { LiveSocket } from \"/assets/phoenix_live_view/phoenix_live_view.esm.js\";\n      import { default as colocated, hooks } from \"/assets/colocated/index.js\";\n      let csrfToken = document\n        .querySelector(\"meta[name='csrf-token']\")\n        .getAttribute(\"content\");\n      let liveSocket = new LiveSocket(\"/live\", window.Phoenix.Socket, {\n        params: { _csrf_token: csrfToken },\n        reloadJitterMin: 50,\n        reloadJitterMax: 500,\n        hooks,\n      });\n      liveSocket.connect();\n      window.liveSocket = liveSocket;\n      // initialize js exec handler from colocated js\n      colocated.js_exec(liveSocket);\n    </script>\n\n    {@inner_content}\n    \"\"\"\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <form phx-submit=\"submit-phone\">\n      <input type=\"text\" name=\"user[phone_number]\" id=\"user-phone-number\" phx-hook=\".PhoneNumber\" />\n      <script :type={Hook} name=\".PhoneNumber\">\n        export default {\n          mounted() {\n            this.el.addEventListener(\"input\", (e) => {\n              let match = this.el.value\n                .replace(/\\D/g, \"\")\n                .match(/^(\\d{3})(\\d{3})(\\d{4})$/);\n              if (match) {\n                this.el.value = `${match[1]}-${match[2]}-${match[3]}`;\n              }\n            });\n          },\n        };\n      </script>\n    </form>\n\n    <p id=\"phone\">{@phone}</p>\n\n    <script :type={Phoenix.LiveView.ColocatedJS} name=\"js_exec\">\n      export default function (liveSocket) {\n        window.addEventListener(\"phx:js:exec\", (e) =>\n          liveSocket.execJS(liveSocket.main.el, e.detail.cmd),\n        );\n      }\n    </script>\n\n    <div id=\"runtime\" phx-hook=\".Runtime\" style=\"display: none;\">Runtime hook works!</div>\n    <script :type={Hook} name=\".Runtime\" runtime>\n      {\n        mounted() {\n          this.js().show(this.el);\n        }\n      }\n    </script>\n\n    <hr />\n\n    <button phx-click=\"push-js\">Push JS from server</button>\n    <h1 id=\"hello\">Hello!</h1>\n\n    <hr />\n\n    <pre :type={SyntaxHighlight} language=\"elixir\" phx-no-curly-interpolation>\n    defmodule SyntaxHighlight do\n      @behaviour Phoenix.Component.MacroComponent\n\n      @impl true\n      def transform({\"pre\", attrs, children, _tag_meta}, _meta) do\n        code = Phoenix.Component.MacroComponent.ast_to_string(children)\n        lang = Map.new(attrs)[\"language\"] || raise ArgumentError, \"language attribute is required\"\n        html_doc = highlight(String.trim_leading(code), lang)\n\n        stylesheet =\n          Makeup.Styles.HTML.Style.stylesheet(Makeup.Styles.HTML.StyleMap.monokai_style())\n\n        {:ok,\n        {\"pre\", [{\"class\", \"highlight\"} | attrs],\n          [\n            {\"style\", [], [stylesheet, \".highlight { padding: 8px; border-radius: 4px; }\"], %{}},\n            html_doc\n          ], %{}}}\n      end\n\n      defp highlight(code, lang) do\n        Application.ensure_all_started([:makeup, :makeup_elixir, :makeup_eex, :makeup_syntect])\n\n        case Makeup.Registry.get_lexer_by_name(lang) do\n          {lexer, opts} ->\n            Makeup.highlight_inner_html(code, lexer: lexer, lexer_options: opts)\n\n          _ ->\n            code\n        end\n      end\n    end\n    </pre>\n\n    <.lv_code_sample />\n    \"\"\"\n  end\n\n  def push_js_cmd(socket, %JS{ops: ops}) do\n    push_event(socket, \"js:exec\", %{cmd: Phoenix.json_library().encode!(ops)})\n  end\n\n  defp lv_code_sample(assigns) do\n    ~H'''\n    <pre :type={SyntaxHighlight} language=\"elixir\" phx-no-curly-interpolation>\n    defmodule MyAppWeb.ThermostatLive do\n      use MyAppWeb, :live_view\n\n      def render(assigns) do\n        ~H\"\"\"\n        Current temperature: {@temperature}°F\n        <button phx-click=\"inc_temperature\">+</button>\n        \"\"\"\n      end\n\n      def mount(_params, _session, socket) do\n        temperature = 70 # Let's assume a fixed temperature for now\n        {:ok, assign(socket, :temperature, temperature)}\n      end\n\n      def handle_event(\"inc_temperature\", _params, socket) do\n        {:noreply, update(socket, :temperature, &(&1 + 1))}\n      end\n    end\n    </pre>\n    '''\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/components_live.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.ComponentsLive do\n  use Phoenix.LiveView\n\n  alias Phoenix.LiveView.JS\n\n  @impl Phoenix.LiveView\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, :tailwind, true)}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_params(params, _uri, socket) do\n    active_tab = params[\"tab\"] || \"focus_wrap\"\n    {:noreply, assign(socket, active_tab: active_tab)}\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <div class=\"p-6\">\n      <h1 class=\"text-2xl font-bold mb-6\">Phoenix Components Demo</h1>\n\n      <!-- Tab Navigation -->\n      <div class=\"border-b border-gray-200 mb-6\">\n        <nav class=\"-mb-px flex space-x-8\">\n          <.tab_link tab=\"focus_wrap\" active_tab={@active_tab} patch=\"/components?tab=focus_wrap\">\n            Focus Wrap\n          </.tab_link>\n        </nav>\n      </div>\n\n      <!-- Tab Content -->\n      <div class=\"mt-6\">\n        <div :if={@active_tab == \"focus_wrap\"}>\n          <.focus_wrap_demo />\n        </div>\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  defp tab_link(assigns) do\n    ~H\"\"\"\n    <.link\n      patch={@patch}\n      class={[\n        \"whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm transition-colors\",\n        if(@tab == @active_tab,\n          do: \"border-blue-500 text-blue-600\",\n          else: \"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300\"\n        )\n      ]}\n    >\n      {render_slot(@inner_block)}\n    </.link>\n    \"\"\"\n  end\n\n  defp focus_wrap_demo(assigns) do\n    ~H\"\"\"\n    <div class=\"space-y-6\">\n      <div>\n        <h2 class=\"text-xl font-semibold mb-4\">Phoenix.Component.focus_wrap Demo</h2>\n        <p class=\"text-gray-600 mb-6\">\n          The focus_wrap component wraps tab focus around a container for accessibility.\n          This is essential for modals, dialogs, and menus.\n        </p>\n      </div>\n\n      <%!-- Dropdown Menu Example --%>\n      <div class=\"space-y-4\">\n        <h3 class=\"text-lg font-medium\">Dropdown Menu Example</h3>\n        <p class=\"text-sm text-gray-600\">\n          Click the button to open a dropdown menu with focus wrapping.\n        </p>\n\n        <div class=\"relative inline-block\">\n          <button\n            id=\"dropdown-button\"\n            phx-click={JS.toggle(to: \"#dropdown-menu\") |> JS.focus_first(to: \"#dropdown-content\")}\n            class=\"px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500\"\n          >\n            Options ▼\n          </button>\n\n          <div\n            id=\"dropdown-menu\"\n            class=\"hidden absolute left-0 mt-2 bg-white border border-gray-300 rounded shadow-lg z-10\"\n          >\n            <.focus_wrap id=\"dropdown-content\" class=\"py-1\">\n              <button\n                phx-click={JS.hide(to: \"#dropdown-menu\")}\n                class=\"block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none\"\n              >\n                Edit Profile\n              </button>\n              <button\n                phx-click={JS.hide(to: \"#dropdown-menu\")}\n                class=\"block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none\"\n              >\n                Settings\n              </button>\n              <button\n                phx-click={JS.hide(to: \"#dropdown-menu\")}\n                class=\"block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none\"\n              >\n                Sign Out\n              </button>\n            </.focus_wrap>\n          </div>\n        </div>\n      </div>\n\n      <%!-- Simple Container Example --%>\n      <div class=\"space-y-4\">\n        <h3 class=\"text-lg font-medium\">Simple Focus Container</h3>\n        <p class=\"text-sm text-gray-600\">\n          A simple container that wraps focus. Notice how Tab navigation cycles within this box.\n        </p>\n\n        <.focus_wrap\n          id=\"simple-focus-container\"\n          class=\"border-2 border-dashed border-gray-300 p-4 rounded\"\n        >\n          <div class=\"space-y-3\">\n            <h4 class=\"font-medium\">Focus Trapped Container</h4>\n            <p class=\"text-sm text-gray-600\">\n              Tab through these elements and notice how focus cycles within this container.\n            </p>\n\n            <div class=\"grid grid-cols-2 gap-3\">\n              <button class=\"px-3 py-2 bg-green-600 text-white rounded hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500\">\n                Button 1\n              </button>\n              <button class=\"px-3 py-2 bg-green-600 text-white rounded hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500\">\n                Button 2\n              </button>\n            </div>\n\n            <input\n              type=\"text\"\n              placeholder=\"Input within container\"\n              class=\"w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-green-500\"\n            />\n          </div>\n        </.focus_wrap>\n      </div>\n    </div>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/error_live.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.ErrorLive do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  # TODO: find a way to silence the raise \"boom\" crashes\n\n  defmodule ChildLive do\n    use Phoenix.LiveView\n\n    @impl Phoenix.LiveView\n    def mount(_params, _session, socket) do\n      if connected?(socket) do\n        send(socket.parent_pid, {:child_mounted, self()})\n\n        receive do\n          :boom ->\n            raise \"boom\"\n\n          :boom_link ->\n            Process.link(socket.parent_pid)\n            raise \"boom\"\n\n          :ok_link ->\n            Process.link(socket.parent_pid)\n            :ok\n\n          _ ->\n            :ok\n        end\n      end\n\n      {:ok, socket}\n    end\n\n    @impl Phoenix.LiveView\n    def handle_event(\"boom\", _params, _socket) do\n      raise \"boom\"\n    end\n\n    @impl Phoenix.LiveView\n    def render(assigns) do\n      ~H\"\"\"\n      {if connected?(@socket), do: \"Child connected\", else: \"Child rendered (dead)\"}\n      <p id=\"child-render-time\">child rendered at: {DateTime.utc_now()}</p>\n\n      <button phx-click=\"boom\">Crash child</button>\n\n      <p class=\"if-phx-error\">Error</p>\n      <p class=\"if-phx-client-error\">Client Error</p>\n      <p class=\"if-phx-server-error\">Server Error</p>\n      <p class=\"if-phx-disconnected\">Disconnected</p>\n      <p class=\"if-phx-loading\">Loading</p>\n      \"\"\"\n    end\n  end\n\n  def render(\"live.html\", assigns) do\n    ~H\"\"\"\n    <meta name=\"csrf-token\" content={Plug.CSRFProtection.get_csrf_token()} />\n    <script src=\"/assets/phoenix/phoenix.min.js\">\n    </script>\n    <script type=\"module\">\n      import { LiveSocket } from \"/assets/phoenix_live_view/phoenix_live_view.esm.js\";\n      let csrfToken = document\n        .querySelector(\"meta[name='csrf-token']\")\n        .getAttribute(\"content\");\n      let liveSocket = new LiveSocket(\"/live\", window.Phoenix.Socket, {\n        params: { _csrf_token: csrfToken },\n        reloadJitterMax: 50,\n        reloadJitterMin: 50,\n        maxReloads: 5,\n        failsafeJitter: 1000,\n        maxChildJoinTries: 3,\n        // override Phoenix.Socket channel join backoff\n        rejoinAfterMs: (_tries) => 50,\n      });\n      liveSocket.connect();\n      window.liveSocket = liveSocket;\n    </script>\n\n    {@inner_content}\n    \"\"\"\n  end\n\n  @impl Phoenix.LiveView\n  def mount(%{\"dead-mount\" => \"raise\"}, _session, _socket), do: raise(\"boom\")\n\n  def mount(%{\"connected-mount\" => \"raise\"}, _session, socket) do\n    if connected?(socket) do\n      raise \"boom\"\n    end\n\n    {:ok, socket}\n  end\n\n  def mount(%{\"connected-child-mount-raise\" => \"link\"}, _session, socket) do\n    # prevent infinite reconnect loop, as the parent always mounts successfully\n    # and therefore the clientside failsafe never triggers;\n    # therefore we only crash once\n    case get_connect_params(socket) do\n      %{\"_mounts\" => 0} ->\n        {:ok, assign(socket, child: true, want_fails: 1, have_fails: 0, link: true)}\n\n      _ ->\n        {:ok, assign(socket, child: true, want_fails: 1, have_fails: 1, link: true)}\n    end\n  end\n\n  def mount(%{\"connected-child-mount-raise\" => want_fails}, _session, socket) do\n    # we send the number of times the child mount should fail\n    # to test that the page is not reloaded, but the child rejoins successfully\n    # up to a certain number of times\n    want_fails = String.to_integer(want_fails)\n    {:ok, assign(socket, child: true, want_fails: want_fails, have_fails: 0)}\n  end\n\n  def mount(%{\"child\" => _}, _session, socket) do\n    {:ok, assign(socket, child: true, want_fails: 0, have_fails: 0)}\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_info({:child_mounted, pid}, socket) do\n    if socket.assigns[:have_fails] < socket.assigns[:want_fails] do\n      send(pid, (socket.assigns[:link] && :boom_link) || :boom)\n      {:noreply, assign(socket, have_fails: socket.assigns[:have_fails] + 1)}\n    else\n      send(pid, (socket.assigns[:link] && :ok_link) || :ok)\n      {:noreply, socket}\n    end\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"boom\", _params, _socket) do\n    raise \"boom\"\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <p id=\"render-time\">main rendered at: {DateTime.utc_now()}</p>\n\n    <button phx-click=\"boom\">Crash main</button>\n\n    <p class=\"if-phx-error\">Error</p>\n    <p class=\"if-phx-client-error\">Client Error</p>\n    <p class=\"if-phx-server-error\">Server Error</p>\n    <p class=\"if-phx-disconnected\">Disconnected</p>\n    <p class=\"if-phx-loading\">Loading</p>\n\n    <div style=\"border: 1px solid lightgray; padding: 4px; margin-top: 16px;\">\n      <%= if assigns[:child] do %>\n        {live_render(@socket, ChildLive, id: \"child\")}\n      <% end %>\n    </div>\n\n    <style>\n      [data-phx-session] .if-phx-error {\n        display: none;\n      }\n\n      [data-phx-session].phx-error > .if-phx-error {\n        display: block;\n      }\n\n      [data-phx-session] .if-phx-client-error {\n        display: none;\n      }\n\n      [data-phx-session].phx-client-error > .if-phx-client-error {\n        display: block;\n      }\n\n      [data-phx-session] .if-phx-server-error {\n        display: none;\n      }\n\n      [data-phx-session].phx-server-error > .if-phx-server-error {\n        display: block;\n      }\n\n      [data-phx-session] .if-phx-disconnected {\n        display: none;\n      }\n\n      [data-phx-session].phx-disconnected > .if-phx-disconnected {\n        display: block;\n      }\n\n      [data-phx-session] .if-phx-loading {\n        display: none;\n      }\n\n      [data-phx-session].phx-loading > .if-phx-loading {\n        display: block;\n      }\n    </style>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/form_dynamic_inputs_live.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.FormDynamicInputsLive do\n  use Phoenix.LiveView\n\n  alias Phoenix.LiveView.JS\n\n  @impl Phoenix.LiveView\n  def mount(params, _session, socket) do\n    {:ok,\n     socket\n     |> assign_form(%{})\n     |> assign(:checkboxes, params[\"checkboxes\"] == \"1\")\n     |> assign(:submitted, false)}\n  end\n\n  defp assign_form(socket, params) do\n    form =\n      Map.take(params, [\"name\"])\n      |> Map.put(\n        \"users\",\n        build_users(\n          params[\"users\"] || %{},\n          params[\"users_sort\"] || [],\n          params[\"users_drop\"] || []\n        )\n      )\n      |> to_form(as: :my_form, id: \"my-form\", default: [])\n\n    assign(socket, :form, form)\n  end\n\n  defp build_users(value, sort, drop) do\n    {sorted, pending} =\n      if is_list(sort) do\n        Enum.map_reduce(sort -- drop, value, &Map.pop(&2, &1, %{\"name\" => nil}))\n      else\n        {[], value}\n      end\n\n    result =\n      sorted ++\n        (pending\n         |> Map.drop(drop)\n         |> Enum.map(&key_as_int/1)\n         |> Enum.sort()\n         |> Enum.map(&elem(&1, 1)))\n\n    Enum.with_index(result)\n    |> Map.new(fn {item, i} -> {to_string(i), item} end)\n  end\n\n  defp key_as_int({key, val}) when is_binary(key) and byte_size(key) < 32 do\n    case Integer.parse(key) do\n      {key, \"\"} -> {key, val}\n      _ -> {key, val}\n    end\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"validate\", %{\"my_form\" => params}, socket) do\n    {:noreply, assign_form(socket, params)}\n  end\n\n  def handle_event(\"save\", %{\"my_form\" => params}, socket) do\n    socket\n    |> assign_form(params)\n    |> assign(:submitted, true)\n    |> then(&{:noreply, &1})\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <.form\n      for={@form}\n      phx-change=\"validate\"\n      phx-submit=\"save\"\n      style=\"display: flex; flex-direction: column; gap: 4px; max-width: 500px;\"\n    >\n      <fieldset>\n        <input\n          type=\"text\"\n          id={@form[:name].id}\n          name={@form[:name].name}\n          value={@form[:name].value}\n          placeholder=\"name\"\n        />\n        <.inputs_for :let={ef} field={@form[:users]} default={[]}>\n          <div style=\"padding: 4px; border: 1px solid gray;\">\n            <input type=\"hidden\" name=\"my_form[users_sort][]\" value={ef.index} />\n            <input\n              type=\"text\"\n              id={ef[:name].id}\n              name={ef[:name].name}\n              value={ef[:name].value}\n              placeholder=\"name\"\n            />\n\n            <button\n              :if={!@checkboxes}\n              type=\"button\"\n              name=\"my_form[users_drop][]\"\n              value={ef.index}\n              phx-click={JS.dispatch(\"change\")}\n            >\n              Remove\n            </button>\n            <label :if={@checkboxes}>\n              <input type=\"checkbox\" name=\"my_form[users_drop][]\" value={ef.index} /> Remove\n            </label>\n          </div>\n        </.inputs_for>\n      </fieldset>\n\n      <input type=\"hidden\" name=\"my_form[users_drop][]\" />\n\n      <button\n        :if={!@checkboxes}\n        type=\"button\"\n        name=\"my_form[users_sort][]\"\n        value=\"new\"\n        phx-click={JS.dispatch(\"change\")}\n      >\n        add more\n      </button>\n      <label :if={@checkboxes}>\n        <input type=\"checkbox\" name=\"my_form[users_sort][]\" /> add more\n      </label>\n    </.form>\n\n    <p :if={@submitted}>Form was submitted!</p>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/form_feedback.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.FormFeedbackLive do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  def render(\"live.html\", assigns) do\n    ~H\"\"\"\n    <meta name=\"csrf-token\" content={Plug.CSRFProtection.get_csrf_token()} />\n    <script src=\"/assets/phoenix/phoenix.min.js\">\n    </script>\n    <script type=\"module\">\n      import {\n        LiveSocket,\n        isUsedInput,\n      } from \"/assets/phoenix_live_view/phoenix_live_view.esm.js\";\n      let resetFeedbacks = (container, feedbacks) => {\n        feedbacks =\n          feedbacks ||\n          Array.from(container.querySelectorAll(\"[phx-feedback-for]\")).map((el) => [\n            el,\n            el.getAttribute(\"phx-feedback-for\"),\n          ]);\n\n        feedbacks.forEach(([feedbackEl, name]) => {\n          let query = `[name=\"${name}\"], [name=\"${name}[]\"]`;\n          let isUsed = Array.from(container.querySelectorAll(query)).find((input) =>\n            isUsedInput(input),\n          );\n          if (isUsed || !feedbackEl.hasAttribute(\"phx-feedback-for\")) {\n            feedbackEl.classList.remove(\"phx-no-feedback\");\n          } else {\n            feedbackEl.classList.add(\"phx-no-feedback\");\n          }\n        });\n      };\n\n      let phxFeedbackDom = (dom) => {\n        window.addEventListener(\"reset\", (e) => resetFeedbacks(document));\n        let feedbacks;\n        let submitPending = false;\n        let inputPending = false;\n        window.addEventListener(\"submit\", (e) => (submitPending = e.target));\n        window.addEventListener(\"input\", (e) => (inputPending = e.target));\n        // extend provided dom options with our own.\n        // accumulate phx-feedback-for containers for each patch and reset feedbacks when patch ends\n        return {\n          onPatchStart(container) {\n            feedbacks = [];\n            dom.onPatchStart && dom.onPatchStart(container);\n          },\n          onNodeAdded(node) {\n            if (node.hasAttribute && node.hasAttribute(\"phx-feedback-for\")) {\n              feedbacks.push([node, node.getAttribute(\"phx-feedback-for\")]);\n            }\n            dom.onNodeAdded && dom.onNodeAdded(node);\n          },\n          onBeforeElUpdated(from, to) {\n            let fromFor = from.getAttribute(\"phx-feedback-for\");\n            let toFor = to.getAttribute(\"phx-feedback-for\");\n            if (fromFor || toFor) {\n              feedbacks.push([from, fromFor || toFor], [to, toFor || fromFor]);\n            }\n\n            dom.onBeforeElUpdated && dom.onBeforeElUpdated(from, to);\n          },\n          onPatchEnd(container) {\n            resetFeedbacks(container, feedbacks);\n            // we might not find some feedback nodes if they are skipped in the patch\n            // therefore we explicitly reset feedbacks for all nodes when the patch\n            // follows a submit or input event\n            if (inputPending || submitPending) {\n              resetFeedbacks(container);\n              inputPending = null;\n              submitPending = null;\n            }\n            dom.onPatchEnd && dom.onPatchEnd(container);\n          },\n        };\n      };\n      let csrfToken = document\n        .querySelector(\"meta[name='csrf-token']\")\n        .getAttribute(\"content\");\n      let liveSocket = new LiveSocket(\"/live\", window.Phoenix.Socket, {\n        params: { _csrf_token: csrfToken },\n        dom: phxFeedbackDom({}),\n      });\n      liveSocket.connect();\n      window.liveSocket = liveSocket;\n    </script>\n\n    {@inner_content}\n    \"\"\"\n  end\n\n  @impl Phoenix.LiveView\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, count: 0, submit_count: 0, validate_count: 0, feedback: true)}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"validate\", _params, socket) do\n    {:noreply, assign(socket, :validate_count, socket.assigns.validate_count + 1)}\n  end\n\n  def handle_event(\"submit\", _params, socket) do\n    {:noreply, assign(socket, :submit_count, socket.assigns.submit_count + 1)}\n  end\n\n  def handle_event(\"inc\", _params, socket) do\n    {:noreply, assign(socket, :count, socket.assigns.count + 1)}\n  end\n\n  def handle_event(\"dec\", _params, socket) do\n    {:noreply, assign(socket, :count, socket.assigns.count - 1)}\n  end\n\n  def handle_event(\"toggle-feedback\", _, socket) do\n    {:noreply, assign(socket, :feedback, !socket.assigns.feedback)}\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <style>\n      .phx-no-feedback {\n        display: none;\n      }\n    </style>\n    <p>Button Count: {@count}</p>\n    <p>Validate Count: {@validate_count}</p>\n    <p>Submit Count: {@submit_count}</p>\n    <button phx-click=\"inc\" class=\"bg-blue-500 text-white p-4\">+</button>\n    <button phx-click=\"dec\" class=\"bg-blue-500 text-white p-4\">-</button>\n\n    <.myform />\n\n    {# render inside function component to trigger the phx-magic-id optimization}\n    <.myfeedback feedback={@feedback} />\n\n    <button phx-click=\"toggle-feedback\">Toggle feedback</button>\n    \"\"\"\n  end\n\n  defp myform(assigns) do\n    ~H\"\"\"\n    <form id=\"myform\" name=\"test\" phx-change=\"validate\" phx-submit=\"submit\">\n      <input type=\"text\" name=\"name\" class=\"border border-gray-500\" placeholder=\"type sth\" />\n\n      <.other_input />\n\n      <button type=\"submit\">Submit</button>\n      <button type=\"reset\">Reset</button>\n    </form>\n    \"\"\"\n  end\n\n  defp myfeedback(assigns) do\n    ~H\"\"\"\n    <div phx-feedback-for={@feedback && \"myfeedback\"} data-feedback-container>\n      I am visible, because phx-no-feedback is not set for myfeedback!\n    </div>\n    \"\"\"\n  end\n\n  defp other_input(assigns) do\n    ~H\"\"\"\n    <input type=\"text\" name=\"myfeedback\" class=\"border border-gray-500\" placeholder=\"myfeedback\" />\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/form_live.ex",
    "content": "for type <- [FormLive, FormLiveNested] do\n  defmodule Module.concat(Phoenix.LiveViewTest.E2E, type) do\n    use Phoenix.LiveView\n\n    alias Phoenix.LiveView.JS\n\n    @compile {:no_warn_undefined, Phoenix.LiveViewTest.E2E.Hooks}\n\n    defmodule FormComponent do\n      use Phoenix.LiveComponent\n\n      @impl Phoenix.LiveComponent\n      def mount(socket) do\n        {:ok, assign(socket, :submitted, false)}\n      end\n\n      @impl Phoenix.LiveComponent\n      def handle_event(\"validate\", params, socket) do\n        {:noreply, assign(socket, :params, Map.merge(socket.assigns.params, params))}\n      end\n\n      def handle_event(\"save\", _params, socket) do\n        {:noreply, assign(socket, :submitted, true)}\n      end\n\n      def handle_event(\"custom-recovery\", _params, socket) do\n        {:noreply,\n         assign(\n           socket,\n           :params,\n           Map.merge(socket.assigns.params, %{\"b\" => \"custom value from server\"})\n         )}\n      end\n\n      def handle_event(\"patch-recovery\", _params, socket) do\n        {:noreply, push_patch(socket, to: \"/form?patched=true\")}\n      end\n\n      @impl Phoenix.LiveComponent\n      def render(assigns) do\n        ~H\"\"\"\n        <div>\n          <Phoenix.LiveViewTest.E2E.FormLive.my_form params={@params} phx-target={@myself} />\n\n          <p :if={@submitted}>LC Form was submitted!</p>\n        </div>\n        \"\"\"\n      end\n    end\n\n    @impl Phoenix.LiveView\n    def mount(params, session, socket) do\n      # if we're nested we need to manually add the on_mount hook\n      # as the live_session doesn't apply\n      socket =\n        if socket.parent_pid do\n          {:cont, socket} =\n            Phoenix.LiveViewTest.E2E.Hooks.on_mount(:default, params, session, socket)\n\n          socket\n        else\n          socket\n        end\n\n      params =\n        case params do\n          :not_mounted_at_router -> session\n          _ -> params\n        end\n\n      {:ok,\n       socket\n       |> assign(\n         :params,\n         Enum.into(params, %{\n           \"a\" => \"foo\",\n           \"b\" => \"bar\",\n           \"c\" => \"baz\",\n           \"id\" => \"test-form\",\n           \"phx-change\" => \"validate\"\n         })\n       )\n       |> update_params(params)\n       |> assign(:submitted, false)}\n    end\n\n    if type === FormLive do\n      def handle_params(_, _, socket), do: {:noreply, socket}\n    end\n\n    def update_params(socket, %{\"no-id\" => _}) do\n      update(socket, :params, &Map.delete(&1, \"id\"))\n    end\n\n    def update_params(socket, %{\"no-change-event\" => _}) do\n      update(socket, :params, &Map.delete(&1, \"phx-change\"))\n    end\n\n    def update_params(socket, %{\"js-change\" => _}) do\n      update(socket, :params, &Map.put(&1, \"phx-change\", JS.push(\"validate\")))\n    end\n\n    def update_params(socket, _), do: socket\n\n    @impl Phoenix.LiveView\n    def handle_event(\"validate\", params, socket) do\n      {:noreply, assign(socket, :params, Map.merge(socket.assigns.params, params))}\n    end\n\n    def handle_event(\"save\", _params, socket) do\n      {:noreply, assign(socket, :submitted, true)}\n    end\n\n    def handle_event(\"custom-recovery\", _params, socket) do\n      {:noreply,\n       assign(\n         socket,\n         :params,\n         Map.merge(socket.assigns.params, %{\"b\" => \"custom value from server\"})\n       )}\n    end\n\n    def handle_event(\"patch-recovery\", _params, socket) do\n      {:noreply, push_patch(socket, to: \"/form?patched=true\")}\n    end\n\n    def handle_event(\"button-test\", _params, socket) do\n      {:noreply, socket}\n    end\n\n    @impl Phoenix.LiveView\n    def render(assigns) do\n      ~H\"\"\"\n      <h1 :if={@params[\"portal\"]}>Form</h1>\n\n      <%= if @params[\"portal\"] do %>\n        <.portal id=\"form-portal\" target=\"body\">\n          <.my_form :if={!@params[\"live-component\"]} params={@params} />\n          <.live_component\n            :if={@params[\"live-component\"]}\n            id=\"form-component\"\n            module={__MODULE__.FormComponent}\n            params={@params}\n          />\n        </.portal>\n      <% else %>\n        <.my_form :if={!@params[\"live-component\"]} params={@params} />\n        <.live_component\n          :if={@params[\"live-component\"]}\n          id=\"form-component\"\n          module={__MODULE__.FormComponent}\n          params={@params}\n        />\n      <% end %>\n\n      <p :if={@submitted}>Form was submitted!</p>\n      \"\"\"\n    end\n\n    def my_form(assigns) do\n      ~H\"\"\"\n      <form\n        id={@params[\"id\"]}\n        phx-submit=\"save\"\n        phx-change={@params[\"phx-change\"]}\n        phx-auto-recover={@params[\"phx-auto-recover\"]}\n        phx-no-unused-field={@params[\"phx-no-unused-field-form\"]}\n        phx-target={assigns[:\"phx-target\"]}\n        class=\"myformclass\"\n      >\n        <fieldset disabled={@params[\"disabled-fieldset\"]}>\n          <input type=\"text\" name=\"a\" readonly value={@params[\"a\"]} />\n          <input type=\"text\" name=\"b\" value={@params[\"b\"]} />\n        </fieldset>\n        <input\n          type=\"text\"\n          name=\"c\"\n          value={@params[\"c\"]}\n          phx-no-unused-field={@params[\"phx-no-unused-field-input\"]}\n        />\n        <select name=\"d\">\n          {Phoenix.HTML.Form.options_for_select([\"foo\", \"bar\", \"baz\"], @params[\"d\"])}\n        </select>\n        <input :if={@params[\"id\"]} type=\"text\" name=\"e\" form={@params[\"id\"]} value={@params[\"e\"]} />\n        <button type=\"submit\" phx-disable-with=\"Submitting\" phx-click={JS.dispatch(\"test\")}>\n          Submit with JS\n        </button>\n        <button id=\"submit\" type=\"submit\" phx-disable-with=\"Submitting\">Submit</button>\n        <button type=\"button\" phx-click=\"button-test\" phx-disable-with=\"Loading\">\n          Non-form Button\n        </button>\n      </form>\n\n      <input :if={@params[\"id\"]} type=\"text\" name=\"f\" form={@params[\"id\"]} value={@params[\"f\"]} />\n      \"\"\"\n    end\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.NestedFormLive do\n  use Phoenix.LiveView\n\n  def mount(params, _session, socket) do\n    {:ok, assign(socket, :params, params)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    {live_render(@socket, Phoenix.LiveViewTest.E2E.FormLiveNested,\n      id: \"nested\",\n      layout: nil,\n      session: @params\n    )}\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.FormStreamLive do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    {@count}\n    <form id=\"test-form\" phx-change=\"validate\" phx-submit=\"save\">\n      <input name=\"myname\" value={@count} />\n      <input id=\"other\" name=\"other\" value={@count} />\n      <div id=\"form-stream-hook\" phx-hook=\"FormHook\" phx-update=\"ignore\"></div>\n      <ul id=\"form-stream\" phx-update=\"stream\">\n        <li :for={{id, item} <- @streams.items} id={id} phx-hook=\"FormStreamHook\">\n          *{inspect(item)}\n        </li>\n      </ul>\n      <button id=\"submit\" phx-disable-with=\"Saving...\">Submit</button>\n    </form>\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    if connected?(socket) do\n      :timer.send_interval(100, self(), :tick)\n    end\n\n    {:ok,\n     socket\n     |> assign(count: 0, stream_count: 3)\n     |> stream(:items, [%{id: 1}, %{id: 2}, %{id: 3}])}\n  end\n\n  def handle_info(:tick, socket) do\n    {:noreply, assign(socket, :count, socket.assigns.count + 1)}\n  end\n\n  def handle_event(\"ping\", _params, socket) do\n    {:reply, %{}, socket}\n  end\n\n  def handle_event(\"validate\", _params, socket) do\n    {:noreply,\n     socket\n     |> inc()\n     |> assign(stream_count: socket.assigns.stream_count + 1)\n     |> stream_insert(:items, %{id: socket.assigns.stream_count + 1})}\n  end\n\n  def handle_event(\"save\", _params, socket) do\n    {:noreply,\n     socket\n     |> inc()\n     |> assign(stream_count: socket.assigns.stream_count + 1)\n     |> stream_insert(:items, %{id: socket.assigns.stream_count + 1})}\n  end\n\n  defp inc(socket) do\n    assign(socket, count: socket.assigns.count + 1)\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_2787.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue2787Live do\n  use Phoenix.LiveView\n\n  # https://github.com/phoenixframework/phoenix_live_view/issues/2787\n\n  @greetings [\"hello\", \"hallo\", \"hei\"]\n  @goodbyes [\"goodbye\", \"auf wiedersehen\", \"ha det bra\"]\n\n  @impl Phoenix.LiveView\n  def mount(_params, _session, socket) do\n    {:ok,\n     assign(socket,\n       form: to_form(changeset(%{}), as: :demo),\n       select1_opts: [\"greetings\", \"goodbyes\"],\n       select2_opts: []\n     )}\n  end\n\n  @types %{\n    select1: :string,\n    select2: :string,\n    dummy: :string\n  }\n\n  def changeset(params) do\n    Ecto.Changeset.cast({%{}, @types}, params, [:select1, :select2, :dummy])\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"updated\", %{\"demo\" => demo_params}, socket) do\n    select2_opts =\n      case Map.get(demo_params, \"select1\") do\n        \"greetings\" -> @greetings\n        \"goodbyes\" -> @goodbyes\n        _ -> []\n      end\n\n    # Ideally select2 gets reset when select1 updates but we'll leave it off\n    # for simplicity\n\n    {:noreply,\n     assign(socket, form: to_form(changeset(demo_params), as: :demo), select2_opts: select2_opts)}\n  end\n\n  def handle_event(\"submitted\", %{\"demo\" => _demo_params}, socket) do\n    {:noreply,\n     assign(socket,\n       form: to_form(changeset(%{}), as: :demo),\n       select2_opts: []\n     )}\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <script src=\"https://cdn.tailwindcss.com/3.4.3\">\n    </script>\n    <div class=\"p-20\">\n      <.form for={@form} phx-change=\"updated\" phx-submit=\"submitted\" class=\"space-y-4\">\n        <.input\n          type=\"select\"\n          field={@form[:select1]}\n          label=\"select1\"\n          prompt=\"Select\"\n          options={@select1_opts}\n        />\n\n        <.input\n          type=\"select\"\n          field={@form[:select2]}\n          label=\"select2\"\n          prompt=\"Select\"\n          options={@select2_opts}\n        />\n\n        <.input type=\"text\" field={@form[:dummy]} label=\"Some text\" />\n\n        <button class=\"text-sm border bg-zinc-200\" type=\"submit\">Submit</button>\n      </.form>\n    </div>\n    \"\"\"\n  end\n\n  attr :for, :string, default: nil\n  slot(:inner_block, required: true)\n\n  def label(assigns) do\n    ~H\"\"\"\n    <label for={@for} class=\"block text-sm font-semibold leading-6 text-zinc-800\">\n      {render_slot(@inner_block)}\n    </label>\n    \"\"\"\n  end\n\n  ###\n  # Input components copied and adjusted from generated core_components\n\n  attr :id, :any, default: nil\n  attr :name, :any\n  attr :label, :string, default: nil\n  attr :value, :any\n\n  attr :type, :string,\n    default: \"text\",\n    values: ~w(checkbox color date datetime-local email file month number password\n               range search select tel text textarea time url week)\n\n  attr :field, Phoenix.HTML.FormField\n\n  attr :errors, :list, default: []\n  attr :checked, :boolean\n  attr :prompt, :string\n  attr :options, :list\n  attr :multiple, :boolean, default: false\n\n  attr :rest, :global,\n    include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength\n                multiple pattern placeholder readonly required rows size step)\n\n  slot(:inner_block)\n\n  def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do\n    errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []\n\n    assigns\n    |> assign(field: nil, id: assigns.id || field.id)\n    |> assign(:errors, errors)\n    |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> \"[]\", else: field.name end)\n    |> assign_new(:value, fn -> field.value end)\n    |> input()\n  end\n\n  def input(%{type: \"select\"} = assigns) do\n    ~H\"\"\"\n    <div>\n      <.label for={@id}>{@label}</.label>\n      <select\n        id={@id}\n        name={@name}\n        class=\"mt-2 block w-full rounded-md border border-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm\"\n        multiple={@multiple}\n        {@rest}\n      >\n        <option :if={@prompt} value=\"\">{@prompt}</option>\n        {Phoenix.HTML.Form.options_for_select(@options, @value)}\n      </select>\n    </div>\n    \"\"\"\n  end\n\n  # All other inputs text, datetime-local, url, password, etc. are handled here...\n  def input(assigns) do\n    ~H\"\"\"\n    <div>\n      <.label for={@id}>{@label}</.label>\n      <input\n        type={@type}\n        name={@name}\n        id={@id}\n        value={Phoenix.HTML.Form.normalize_value(@type, @value)}\n        class={[\n          \"mt-2 px-2 block w-full rounded-lg text-zinc-900 border focus:ring-0 sm:text-sm sm:leading-6\",\n          @errors == [] && \"border-zinc-300 focus:border-zinc-400\",\n          @errors != [] && \"border-rose-400 focus:border-rose-400\"\n        ]}\n        {@rest}\n      />\n    </div>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_2965.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue2965Live do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  defmodule NoOpWriter do\n    @behaviour Phoenix.LiveView.UploadWriter\n\n    @impl true\n    def init(_opts) do\n      {:ok, nil}\n    end\n\n    @impl true\n    def meta(state), do: state\n\n    @impl true\n    def write_chunk(_data, state) do\n      Process.sleep((:rand.uniform() * 200) |> ceil())\n      {:ok, state}\n    end\n\n    def close(_state, :cancel) do\n      {:ok, :aborted}\n    end\n\n    @impl true\n    def close(_state, :done) do\n      {:ok, %{}}\n    end\n  end\n\n  def mount(_params, _session, socket) do\n    socket =\n      socket\n      |> allow_upload(:files,\n        accept: :any,\n        max_entries: 1500,\n        # minimum 5 mb for multipart\n        chunk_size: 5 * 1_024 * 1_024,\n        max_file_size: 10_000_000_000,\n        auto_upload: true,\n        writer: &noop_writer/3,\n        progress: &handle_progress/3\n      )\n      |> assign(:form, to_form(%{}))\n\n    {:ok, socket}\n  end\n\n  def render(\"live.html\", assigns) do\n    ~H\"\"\"\n    <meta name=\"csrf-token\" content={Plug.CSRFProtection.get_csrf_token()} />\n    <script src=\"/assets/phoenix/phoenix.min.js\">\n    </script>\n    <script type=\"module\">\n      import { LiveSocket } from \"/assets/phoenix_live_view/phoenix_live_view.esm.js\";\n      const QueuedUploaderHook = {\n        async mounted() {\n          const maxConcurrency = this.el.dataset.maxConcurrency || 3;\n          let filesRemaining = [];\n\n          this.el.addEventListener(\"input\", async (event) => {\n            event.preventDefault();\n\n            if (event.target instanceof HTMLInputElement) {\n              const files_html = event.target.files;\n              if (files_html) {\n                const rawFiles = Array.from(files_html);\n                const fileNames = rawFiles.map((f) => {\n                  return f.name;\n                });\n\n                this.pushEvent(\n                  \"upload_scrub_list\",\n                  { file_names: fileNames },\n                  ({ deduped_filenames }, ref) => {\n                    const files = rawFiles.filter((f) => {\n                      return deduped_filenames.includes(f.name);\n                    });\n                    filesRemaining = files;\n                    const firstFiles = files.slice(0, maxConcurrency);\n                    this.upload(\"files\", firstFiles);\n\n                    filesRemaining.splice(0, maxConcurrency);\n                  },\n                );\n              }\n            }\n          });\n\n          this.handleEvent(\"upload_send_next_file\", () => {\n            if (filesRemaining.length > 0) {\n              const nextFile = filesRemaining.shift();\n              if (nextFile != undefined) {\n                this.upload(\"files\", [nextFile]);\n              }\n            } else {\n              console.log(\"Done uploading, noop!\");\n            }\n          });\n        },\n      };\n      let csrfToken = document\n        .querySelector(\"meta[name='csrf-token']\")\n        .getAttribute(\"content\");\n      let liveSocket = new LiveSocket(\"/live\", window.Phoenix.Socket, {\n        params: { _csrf_token: csrfToken },\n        hooks: { QueuedUploaderHook },\n      });\n      liveSocket.connect();\n    </script>\n\n    {@inner_content}\n    \"\"\"\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <main>\n      <h1>Uploader reproduction</h1>\n      <.form for={@form} phx-submit=\"save\" phx-change=\"validate\">\n        <section>\n          <.live_file_input upload={@uploads.files} style=\"display: none;\" />\n          <input\n            id=\"fileinput\"\n            type=\"file\"\n            multiple\n            phx-hook=\"QueuedUploaderHook\"\n            disabled={file_picker_disabled?(@uploads)}\n          />\n          <h2 :if={length(@uploads.files.entries) > 0}>Currently uploading files</h2>\n          <div>\n            <table>\n              <!-- head -->\n              <thead>\n                <tr>\n                  <th>File Name</th>\n                  <th>Progress</th>\n                  <th>Cancel</th>\n                  <th>Errors</th>\n                </tr>\n              </thead>\n              <tbody>\n                <%= for entry <- uploads_in_progress(@uploads) do %>\n                  <tr>\n                    <td>{entry.client_name}</td>\n                    <td>\n                      <progress value={entry.progress} max=\"100\">\n                        {entry.progress}%\n                      </progress>\n                    </td>\n\n                    <td>\n                      <button\n                        type=\"button\"\n                        phx-click=\"cancel-upload\"\n                        phx-value-ref={entry.ref}\n                        aria-label=\"cancel\"\n                      >\n                        <span>&times;</span>\n                      </button>\n                    </td>\n                    <td>\n                      <%= for err <- upload_errors(@uploads.files, entry) do %>\n                        <p style=\"color: red;\">{error_to_string(err)}</p>\n                      <% end %>\n                    </td>\n                  </tr>\n                <% end %>\n              </tbody>\n            </table>\n          </div>\n          <%= for err <- upload_errors(@uploads.files) do %>\n            <p style=\"text-red\">{error_to_string(err)}</p>\n          <% end %>\n        </section>\n      </.form>\n    </main>\n    \"\"\"\n  end\n\n  def handle_progress(:files, entry, socket) do\n    if entry.done? do\n      {:noreply, push_event(socket, \"upload_send_next_file\", %{})}\n    else\n      {:noreply, socket}\n    end\n  end\n\n  # This dedupes against s3, just doing a no-op here to preserve the original uploader js code\n  def handle_event(\n        \"upload_scrub_list\",\n        %{\"file_names\" => file_names},\n        socket\n      ) do\n    {:reply, %{deduped_filenames: file_names}, socket}\n  end\n\n  def handle_event(\"validate\", _params, socket) do\n    {:noreply, socket}\n  end\n\n  def handle_event(\"cancel-upload\", %{\"ref\" => ref}, socket) do\n    {:noreply, cancel_upload(socket, :files, ref)}\n  end\n\n  def handle_event(\"save\", _params, socket) do\n    {:noreply, socket}\n  end\n\n  def error_to_string(:too_large), do: \"Too large\"\n  def error_to_string(:not_accepted), do: \"You have selected an unacceptable file type\"\n  def error_to_string(:s3_error), do: \"Error on writing to cloudflare\"\n\n  def error_to_string(_unknown) do\n    \"unknown error\"\n  end\n\n  ## Helpers\n\n  defp file_picker_disabled?(uploads) do\n    Enum.any?(uploads.files.entries, fn e -> !e.done? end)\n  end\n\n  defp noop_writer(_name, %Phoenix.LiveView.UploadEntry{} = entry, _socket) do\n    {\n      __MODULE__.NoOpWriter,\n      provider: :r2, name: entry.client_name\n    }\n  end\n\n  defp uploads_in_progress(uploads) do\n    uploads.files.entries\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3026.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3026Live do\n  use Phoenix.LiveView\n\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3026\n\n  defmodule Form do\n    use Phoenix.LiveComponent\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div>\n        Example form\n        <.form for={to_form(%{})} phx-change=\"validate\" phx-submit=\"submit\">\n          <input label=\"Name\" name=\"name\" type=\"text\" value={@name} />\n          <input label=\"Email\" name=\"email\" type=\"text\" value={@email} />\n          <button type=\"submit\">Submit</button>\n        </.form>\n      </div>\n      \"\"\"\n    end\n  end\n\n  @impl Phoenix.LiveView\n  def mount(_params, _session, socket) do\n    if connected?(socket) do\n      send(self(), :load)\n    end\n\n    status = if connected?(socket), do: :loading, else: :connecting\n\n    {:ok, assign(socket, :status, status)}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_info(:load, socket) do\n    Process.sleep(200)\n\n    {:noreply, assign(socket, %{status: :loaded, name: \"John\", email: \"\"})}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"change_status\", %{\"status\" => status}, socket) do\n    {:noreply, assign(socket, :status, String.to_existing_atom(status))}\n  end\n\n  def handle_event(\"validate\", params, socket) do\n    {:noreply, assign(socket, %{name: params[\"name\"], email: params[\"email\"]})}\n  end\n\n  def handle_event(\"submit\", _params, socket) do\n    send(self(), :load)\n    {:noreply, assign(socket, %{status: :loading})}\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <.form for={to_form(%{})} phx-change=\"change_status\">\n      <select name=\"status\" type=\"select\">\n        {Phoenix.HTML.Form.options_for_select(options(), @status)}\n      </select>\n    </.form>\n\n    <%= case @status do %>\n      <% :connecting -> %>\n        <.status status={@status} />\n      <% :loading -> %>\n        <.status status={@status} />\n      <% :connected -> %>\n        <.status status={@status} />\n      <% :loaded -> %>\n        <.live_component module={__MODULE__.Form} id=\"my-form\" name={@name} email={@email} />\n    <% end %>\n    \"\"\"\n  end\n\n  defp status(assigns) do\n    ~H\"\"\"\n    <div class=\"p-8 bg-gray-200 mb-4\">\n      {@status}\n    </div>\n    \"\"\"\n  end\n\n  defp options do\n    ~w(connecting loading connected loaded)\n    |> Enum.map(fn status -> {String.capitalize(status), status} end)\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3040.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3040Live do\n  use Phoenix.LiveView\n\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3040\n\n  alias Phoenix.LiveView.JS\n\n  @impl Phoenix.LiveView\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, modal_open: false, submitted: false)}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_params(%{\"modal\" => _}, _uri, socket) do\n    {:noreply, socket |> assign(:modal_open, true)}\n  end\n\n  def handle_params(_unsigned_params, _uri, socket) do\n    {:noreply, assign(socket, :modal_open, false)}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"submit\", _params, socket) do\n    {:noreply, assign(socket, :submitted, true)}\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <style>\n      <%= style() %>\n    </style>\n\n    <.link patch=\"/issues/3040?modal=true\">Add new</.link>\n\n    <.modal :if={@modal_open} id=\"my-modal\" show on_cancel={JS.patch(\"/issues/3040\")}>\n      <.form for={%{}} phx-submit=\"submit\">\n        <input type=\"text\" name=\"name\" />\n\n        <p :if={@submitted}>Form was submitted!</p>\n      </.form>\n    </.modal>\n    \"\"\"\n  end\n\n  attr :id, :string, required: true\n  attr :show, :boolean, default: false\n  attr :on_cancel, JS, default: %JS{}\n  slot(:inner_block, required: true)\n\n  defp modal(assigns) do\n    ~H\"\"\"\n    <div\n      id={@id}\n      phx-mounted={@show && show_modal(@id)}\n      phx-remove={hide_modal(@id)}\n      data-cancel={JS.exec(@on_cancel, \"phx-remove\")}\n      class=\"relative z-50 hidden\"\n    >\n      <div id={\"#{@id}-bg\"} class=\"bg-zinc-50/90 fixed inset-0 transition-opacity\" aria-hidden=\"true\" />\n      <div\n        class=\"fixed inset-0 overflow-y-auto\"\n        aria-labelledby={\"#{@id}-title\"}\n        aria-describedby={\"#{@id}-description\"}\n        role=\"dialog\"\n        aria-modal=\"true\"\n        tabindex=\"0\"\n      >\n        <div class=\"flex min-h-full items-center justify-center\">\n          <div class=\"w-full max-w-3xl p-4 sm:p-6 lg:py-8\">\n            <.focus_wrap\n              id={\"#{@id}-container\"}\n              phx-window-keydown={JS.exec(\"data-cancel\", to: \"##{@id}\")}\n              phx-key=\"escape\"\n              phx-click-away={JS.exec(\"data-cancel\", to: \"##{@id}\")}\n              class=\"bg-gray-200 relative hidden rounded-2xl bg-white p-14 ring-1 transition\"\n            >\n              <div class=\"absolute top-6 right-5\">\n                <button\n                  phx-click={JS.exec(\"data-cancel\", to: \"##{@id}\")}\n                  type=\"button\"\n                  class=\"-m-3 flex-none p-3 opacity-20 hover:opacity-40\"\n                  aria-label=\"close\"\n                >\n                  x\n                </button>\n              </div>\n              <div id={\"#{@id}-content\"}>\n                {render_slot(@inner_block)}\n              </div>\n            </.focus_wrap>\n          </div>\n        </div>\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  defp show_modal(js \\\\ %JS{}, id) when is_binary(id) do\n    js\n    |> JS.show(to: \"##{id}\")\n    |> JS.show(\n      to: \"##{id}-bg\",\n      transition: {\"transition-all transform ease-out duration-300\", \"opacity-0\", \"opacity-100\"}\n    )\n    |> show(\"##{id}-container\")\n    |> JS.add_class(\"overflow-hidden\", to: \"body\")\n    |> JS.focus_first(to: \"##{id}-content\")\n  end\n\n  defp hide_modal(js \\\\ %JS{}, id) do\n    js\n    |> JS.hide(\n      to: \"##{id}-bg\",\n      transition: {\"transition-all transform ease-in duration-200\", \"opacity-100\", \"opacity-0\"}\n    )\n    |> hide(\"##{id}-container\")\n    |> JS.hide(to: \"##{id}\", transition: {\"block\", \"block\", \"hidden\"})\n    |> JS.remove_class(\"overflow-hidden\", to: \"body\")\n    |> JS.pop_focus()\n  end\n\n  defp show(js, selector) do\n    JS.show(js,\n      to: selector,\n      transition:\n        {\"transition-all transform ease-out duration-300\",\n         \"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95\",\n         \"opacity-100 translate-y-0 sm:scale-100\"}\n    )\n  end\n\n  defp hide(js, selector) do\n    JS.hide(js,\n      to: selector,\n      time: 200,\n      transition:\n        {\"transition-all transform ease-in duration-200\",\n         \"opacity-100 translate-y-0 sm:scale-100\",\n         \"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95\"}\n    )\n  end\n\n  defp style() do\n    \"\"\"\n    .fixed{\n      position: fixed\n    }\n\n    .absolute{\n      position: absolute\n    }\n\n    .relative{\n      position: relative\n    }\n\n    .inset-0{\n      inset: 0px\n    }\n\n    .right-5{\n      right: 1.25rem\n    }\n\n    .top-6{\n      top: 1.5rem\n    }\n\n    .z-50{\n      z-index: 50\n    }\n\n    .-m-3{\n      margin: -0.75rem\n    }\n\n    .flex{\n      display: flex\n    }\n\n    .hidden{\n      display: none\n    }\n\n    .min-h-full{\n      min-height: 100%\n    }\n\n    .w-full{\n      width: 100%\n    }\n\n    .max-w-3xl{\n      max-width: 48rem\n    }\n\n    .flex-none{\n      flex: none\n    }\n\n    .items-center{\n      align-items: center\n    }\n\n    .justify-center{\n      justify-content: center\n    }\n\n    .overflow-y-auto{\n      overflow-y: auto\n    }\n\n    .rounded-2xl{\n      border-radius: 1rem\n    }\n\n    .bg-white{\n      background-color: rgb(255 255 255 / 1)\n    }\n\n    .bg-gray-200{\n      background-color: rgb(229 231 235 / 1)\n    }\n\n    .bg-zinc-50\\/90{\n      background-color: rgb(250 250 250 / 0.9)\n    }\n\n    .p-14{\n      padding: 3.5rem\n    }\n\n    .p-3{\n      padding: 0.75rem\n    }\n\n    .p-4{\n      padding: 1rem\n    }\n\n    .opacity-20{\n      opacity: 0.2\n    }\n\n    .transition{\n      transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;\n      transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n      transition-duration: 150ms\n    }\n\n    .transition-opacity{\n      transition-property: opacity;\n      transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n      transition-duration: 150ms\n    }\n\n    .hover\\:opacity-40:hover{\n      opacity: 0.4\n    }\n\n    @media (min-width: 640px){\n      .sm\\:p-6{\n        padding: 1.5rem\n      }\n    }\n\n    @media (min-width: 1024px){\n      .lg\\:py-8{\n        padding-top: 2rem;\n        padding-bottom: 2rem\n      }\n    }\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3047.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3047ALive do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  def render(\"live.html\", assigns) do\n    ~H\"\"\"\n    {apply(Phoenix.LiveViewTest.E2E.Layout, :render, [\n      \"live.html\",\n      Map.put(assigns, :inner_content, [])\n    ])}\n\n    <div class=\"flex flex-col items-center justify-center\">\n      <div class=\"flex flex-row gap-3\">\n        <.link class=\"border rounded bg-blue-700 w-fit px-2 text-white\" navigate=\"/issues/3047/a\">\n          Page A\n        </.link>\n        <.link class=\"border rounded bg-blue-700 w-fit px-2 text-white\" navigate=\"/issues/3047/b\">\n          Page B\n        </.link>\n      </div>\n\n      {@inner_content}\n\n      {live_render(@socket, Phoenix.LiveViewTest.E2E.Issue3047.Sticky, id: \"test\", sticky: true)}\n    </div>\n    \"\"\"\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <span id=\"page\">Page A</span>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3047BLive do\n  use Phoenix.LiveView, layout: {Phoenix.LiveViewTest.E2E.Issue3047ALive, :live}\n\n  def render(assigns) do\n    ~H\"\"\"\n    <span id=\"page\">Page B</span>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3047.Sticky do\n  use Phoenix.LiveView\n\n  def mount(:not_mounted_at_router, _session, socket) do\n    items =\n      Enum.map(1..10, fn x ->\n        %{id: x, name: \"item-#{x}\"}\n      end)\n\n    {:ok, socket |> stream(:items, items), layout: false}\n  end\n\n  def handle_event(\"reset\", _, socket) do\n    items =\n      Enum.map(5..15, fn x ->\n        %{id: x, name: \"item-#{x}\"}\n      end)\n\n    {:noreply, socket |> stream(:items, items, reset: true)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div style=\"border: 2px solid black;\">\n      <h1>This is the sticky liveview</h1>\n      <div id=\"items\" phx-update=\"stream\" style=\"display: flex; flex-direction: column; gap: 4px;\">\n        <span :for={{dom_id, item} <- @streams.items} id={dom_id}>{item.name}</span>\n      </div>\n\n      <button phx-click=\"reset\">Reset</button>\n    </div>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3083.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3083Live do\n  use Phoenix.LiveView\n\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3083\n\n  @impl Phoenix.LiveView\n  def mount(params, _session, socket) do\n    if connected?(socket) and not (params[\"auto\"] == \"false\") do\n      :timer.send_interval(1000, self(), :tick)\n    end\n\n    {:ok, socket |> assign(options: [1, 2, 3, 4, 5], form: to_form(%{\"ids\" => []}))}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_info(:tick, socket) do\n    selected = Enum.take_random([1, 2, 3, 4, 5], 2)\n    params = %{\"ids\" => selected}\n\n    {:noreply, socket |> assign(form: to_form(params))}\n  end\n\n  def handle_info({:select, values}, socket) do\n    params = %{\"ids\" => values}\n\n    {:noreply, socket |> assign(form: to_form(params))}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"validate\", _params, socket) do\n    {:noreply, socket}\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <.form id=\"form\" for={@form} phx-change=\"validate\">\n      <select id={@form[:ids].id} name={@form[:ids].name <> \"[]\"} multiple={true}>\n        {Phoenix.HTML.Form.options_for_select(@options, @form[:ids].value)}\n      </select>\n      <input type=\"text\" placeholder=\"focus me!\" />\n    </.form>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3107.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3107Live do\n  use Phoenix.LiveView\n\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3107\n\n  @impl Phoenix.LiveView\n  def mount(_params, _session, socket) do\n    {:ok,\n     socket\n     |> assign(:form, Phoenix.Component.to_form(%{}))\n     |> assign(:disabled, true)}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"validate\", _, socket) do\n    {:noreply, assign(socket, :disabled, false)}\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <.form for={@form} phx-change=\"validate\" style=\"display: flex;\">\n      <select>\n        <option value=\"ONE\">ONE</option>\n        <option value=\"TWO\">TWO</option>\n      </select>\n\n      <button disabled={@disabled}>OK</button>\n    </.form>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3117.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3117Live do\n  use Phoenix.LiveView\n\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3117\n\n  defmodule Row do\n    use Phoenix.LiveComponent\n\n    def update(assigns, socket) do\n      {:ok, assign(socket, assigns) |> assign_async(:foo, fn -> {:ok, %{foo: :bar}} end)}\n    end\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div id={@id}>\n        Example LC Row {inspect(@foo.result)}\n        <.fc />\n      </div>\n      \"\"\"\n    end\n\n    defp fc(assigns) do\n      ~H\"\"\"\n      <div class=\"static\">static content</div>\n      \"\"\"\n    end\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <.link id=\"navigate\" navigate=\"/issues/3117?nav\">Navigate</.link>\n    <div :for={i <- [1, 2]}>\n      <.live_component module={__MODULE__.Row} id={\"row-#{i}\"} />\n    </div>\n    \"\"\"\n  end\n\n  @impl Phoenix.LiveView\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3169.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3169Live.Components do\n  use Phoenix.Component\n\n  def input(assigns) do\n    ~H\"\"\"\n    <div>\n      {@field.value}\n      <input type=\"text\" value={@field.value} />\n      <.input_two field={@field} />\n    </div>\n    \"\"\"\n  end\n\n  def input_two(assigns) do\n    ~H\"\"\"\n    <div>\n      {@field.value}\n      <input type=\"text\" value={@field.value} />\n    </div>\n    \"\"\"\n  end\n\n  def test(assigns) do\n    ~H\"\"\"\n    This is a test! {@var}\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3169Live.FormColumn do\n  use Phoenix.LiveComponent\n  import Phoenix.LiveViewTest.E2E.Issue3169Live.Components\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      FormColumn (c3) <input type=\"text\" value={@form[:name].value} />\n      <.input field={@form[:name]} />\n      <.test var=\"foo\" />\n    </div>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3169Live.FormCore do\n  use Phoenix.LiveComponent\n\n  alias Phoenix.LiveViewTest.E2E.Issue3169Live.FormColumn\n\n  def mount(socket) do\n    {:ok, assign(socket, record: nil)}\n  end\n\n  def update(%{record: record}, socket) do\n    {:ok, assign(socket, record: record)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      FormCore (c2)\n      <.form :let={form} for={@record}>\n        <.live_component module={FormColumn} id={\"column-#{@record[\"id\"]}\"} form={form} />\n      </.form>\n    </div>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3169Live.FormComponent do\n  use Phoenix.LiveComponent\n\n  alias Phoenix.LiveViewTest.E2E.Issue3169Live.FormCore\n\n  def mount(socket) do\n    {:ok, assign(socket, record: nil)}\n  end\n\n  def update(%{selected: nil}, socket) do\n    {:ok, socket}\n  end\n\n  def update(%{selected: name} = assigns, socket) do\n    send_update(__MODULE__, id: assigns.id, action: {:load, name})\n    {:ok, assign(socket, record: nil)}\n  end\n\n  def update(%{action: {:load, name}}, socket) do\n    :timer.sleep(50)\n    record = %{\"id\" => :rand.uniform(1_000_000), \"name\" => \"Record #{name}\"}\n    {:ok, assign(socket, record: record)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      FormComponent (c1)\n      <div :if={@record}>\n        <.live_component module={FormCore} id=\"core\" record={@record} />\n      </div>\n      <hr />\n    </div>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3169Live do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  alias Phoenix.LiveViewTest.E2E.Issue3169Live.FormComponent\n\n  def render(\"live.html\", assigns) do\n    ~H\"\"\"\n    <meta name=\"csrf-token\" content={Plug.CSRFProtection.get_csrf_token()} />\n    <script src=\"/assets/phoenix/phoenix.min.js\">\n    </script>\n    <script type=\"module\">\n      import { LiveSocket } from \"/assets/phoenix_live_view/phoenix_live_view.esm.js\";\n      let csrfToken = document\n        .querySelector(\"meta[name='csrf-token']\")\n        .getAttribute(\"content\");\n      let liveSocket = new LiveSocket(\"/live\", window.Phoenix.Socket, {\n        params: { _csrf_token: csrfToken },\n      });\n      liveSocket.connect();\n    </script>\n    {@inner_content}\n    \"\"\"\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    HomeLive <.live_component module={FormComponent} id=\"form_view\" selected={@selected} />\n    <button id=\"select-a\" phx-click=\"select\" phx-value-name=\"a\">Select A</button>\n    <button id=\"select-b\" phx-click=\"select\" phx-value-name=\"b\">Select B</button>\n    <button id=\"select-z\" phx-click=\"select\" phx-value-name=\"z\">Select Z</button>\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, selected: nil)}\n  end\n\n  def handle_event(\"select\", %{\"name\" => value}, socket) do\n    {:noreply, assign(socket, :selected, value)}\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3194.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3194Live do\n  use Phoenix.LiveView\n\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3194\n\n  @impl Phoenix.LiveView\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, :form, to_form(%{}, as: :foo))}\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <.form for={@form} phx-change=\"validate\" phx-submit=\"submit\">\n      <input\n        id={@form[:store_number].id}\n        name={@form[:store_number].name}\n        value={@form[:store_number].value}\n        type=\"text\"\n        phx-debounce=\"blur\"\n      />\n    </.form>\n    \"\"\"\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"submit\", _params, socket) do\n    {:noreply, push_navigate(socket, to: \"/issues/3194/other\")}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"validate\", _params, socket) do\n    {:noreply, socket}\n  end\n\n  defmodule OtherLive do\n    use Phoenix.LiveView\n\n    @impl Phoenix.LiveView\n    def render(assigns) do\n      ~H\"\"\"\n      <h2>Another LiveView</h2>\n      \"\"\"\n    end\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3200.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3200 do\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3200\n\n  defmodule PanelLive do\n    use Phoenix.LiveView\n\n    alias Phoenix.LiveView.JS\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div>\n        <div>\n          <div>\n            <.tab_button text=\"Messages tab\" route=\"/issues/3200/messages\" />\n            <.tab_button text=\"Settings tab\" route=\"/issues/3200/settings\" />\n          </div>\n\n          <aside>\n            <div>\n              <div :if={@live_action == :messages_tab}>\n                <.live_component\n                  module={Phoenix.LiveViewTest.E2E.Issue3200.MessagesTab}\n                  id=\"messages_tab\"\n                />\n              </div>\n              <div :if={@live_action == :settings_tab}>\n                <.live_component\n                  module={Phoenix.LiveViewTest.E2E.Issue3200.SettingsTab}\n                  id=\"settings_tab\"\n                />\n              </div>\n            </div>\n          </aside>\n        </div>\n      </div>\n      \"\"\"\n    end\n\n    def handle_params(_params, _uri, socket), do: {:noreply, socket}\n\n    defp tab_button(assigns) do\n      ~H\"\"\"\n      <button type=\"button\" phx-click={JS.patch(@route)}>\n        {@text}\n      </button>\n      \"\"\"\n    end\n  end\n\n  defmodule SettingsTab do\n    use Phoenix.LiveComponent\n\n    @impl Phoenix.LiveComponent\n    def render(assigns) do\n      ~H\"\"\"\n      <div>Settings</div>\n      \"\"\"\n    end\n  end\n\n  defmodule MessagesTab do\n    use Phoenix.LiveComponent\n\n    def update(assigns, socket) do\n      {\n        :ok,\n        assign(socket, id: assigns.id, value: \"\")\n      }\n    end\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div>\n        <.live_component\n          module={Phoenix.LiveViewTest.E2E.Issue3200.MessageComponent}\n          id=\"some_unique_message_id\"\n          message=\"Example message\"\n        />\n        <form\n          id=\"full_add_message_form\"\n          phx-change=\"add_message_change\"\n          phx-submit=\"add_message\"\n          phx-target=\"#full_add_message_form\"\n        >\n          <.input id=\"new_message_input\" name=\"new_message\" value={@value} />\n        </form>\n      </div>\n      \"\"\"\n    end\n\n    def input(assigns) do\n      ~H\"\"\"\n      <div phx-feedback-for={@name}>\n        <input name={@name} id={@id} value={@value} />\n      </div>\n      \"\"\"\n    end\n\n    def handle_event(\"add_message_change\", %{\"new_message\" => value}, socket) do\n      {:noreply, assign(socket, :value, value)}\n    end\n  end\n\n  defmodule MessageComponent do\n    use Phoenix.LiveComponent\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div>{@message}</div>\n      \"\"\"\n    end\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3378.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3378.NotificationsLive do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  def mount(_params, _session, socket) do\n    {:ok,\n     socket\n     |> stream(:notifications, [%{id: 1, message: \"Hello\"}])}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      <ul id=\"notifications_list\" phx-update=\"stream\">\n        <div :for={{dom_id, _notification} <- @streams.notifications} id={dom_id}>\n          <p>big!</p>\n        </div>\n      </ul>\n    </div>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3378.AppBarLive do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      {live_render(\n        @socket,\n        Phoenix.LiveViewTest.E2E.Issue3378.NotificationsLive,\n        session: %{},\n        id: :notifications\n      )}\n    </div>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3378.HomeLive do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    {live_render(\n      @socket,\n      Phoenix.LiveViewTest.E2E.Issue3378.AppBarLive,\n      session: %{},\n      id: :appbar\n    )}\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3448.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3448Live do\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3448\n\n  use Phoenix.LiveView\n\n  alias Phoenix.LiveView.JS\n\n  def mount(_params, _session, socket) do\n    form = to_form(%{\"a\" => []})\n\n    {:ok, assign_new(socket, :form, fn -> form end)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <.form for={@form} id=\"my_form\" phx-change=\"validate\" class=\"flex flex-col gap-2\">\n      <.my_component>\n        <:left_content :for={value <- @form[:a].value || []}>\n          <div>{value}</div>\n        </:left_content>\n      </.my_component>\n\n      <div class=\"flex gap-2\">\n        <input\n          type=\"checkbox\"\n          name={@form[:a].name <> \"[]\"}\n          value=\"settings\"\n          checked={\"settings\" in (@form[:a].value || [])}\n          phx-click={JS.dispatch(\"input\") |> JS.focus(to: \"#search\")}\n        />\n\n        <input\n          type=\"checkbox\"\n          name={@form[:a].name <> \"[]\"}\n          value=\"content\"\n          checked={\"content\" in (@form[:a].value || [])}\n          phx-click={JS.dispatch(\"input\") |> JS.focus(to: \"#search\")}\n        />\n      </div>\n    </.form>\n    \"\"\"\n  end\n\n  def handle_event(\"validate\", params, socket) do\n    {:noreply, assign(socket, form: to_form(params))}\n  end\n\n  def handle_event(\"search\", _params, socket) do\n    {:noreply, socket}\n  end\n\n  slot :left_content\n\n  defp my_component(assigns) do\n    ~H\"\"\"\n    <div>\n      <div :for={left_content <- @left_content}>\n        {render_slot(left_content)}\n      </div>\n\n      <input id=\"search\" type=\"search\" name=\"value\" phx-change=\"search\" />\n    </div>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3496.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3496.ALive do\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3496\n\n  use Phoenix.LiveView\n\n  def base(assigns) do\n    ~H\"\"\"\n    <meta name=\"csrf-token\" content={Plug.CSRFProtection.get_csrf_token()} />\n    <script src=\"/assets/phoenix/phoenix.min.js\">\n    </script>\n    <script type=\"module\">\n      import { LiveSocket } from \"/assets/phoenix_live_view/phoenix_live_view.esm.js\";\n\n      let csrfToken = document\n        .querySelector(\"meta[name='csrf-token']\")\n        .getAttribute(\"content\");\n      let liveSocket = new LiveSocket(\"/live\", window.Phoenix.Socket, {\n        params: { _csrf_token: csrfToken },\n        hooks: {\n          MyHook: {\n            mounted() {\n              console.log(\"Hook mounted!\");\n            },\n          },\n        },\n      });\n      liveSocket.connect();\n      window.liveSocket = liveSocket;\n    </script>\n    <style>\n      * { font-size: 1.1em; }\n    </style>\n    \"\"\"\n  end\n\n  def with_sticky(assigns) do\n    ~H\"\"\"\n    <.base />\n\n    <div>\n      {@inner_content}\n    </div>\n\n    {live_render(@socket, Phoenix.LiveViewTest.E2E.Issue3496.StickyLive,\n      id: \"sticky\",\n      sticky: true\n    )}\n    \"\"\"\n  end\n\n  def without_sticky(assigns) do\n    ~H\"\"\"\n    <.base />\n\n    <div>\n      {@inner_content}\n    </div>\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok, socket, layout: {__MODULE__, :with_sticky}}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <h1>Page A</h1>\n    <.link navigate=\"/issues/3496/b\">Go to page B</.link>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3496.BLive do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, socket, layout: {Phoenix.LiveViewTest.E2E.Issue3496.ALive, :without_sticky}}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <h1>Page B</h1>\n    <Phoenix.LiveViewTest.E2E.Issue3496.MyComponent.my_component />\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3496.StickyLive do\n  use Phoenix.LiveView\n\n  def mount(:not_mounted_at_router, _session, socket) do\n    {:ok, socket, layout: false}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      <Phoenix.LiveViewTest.E2E.Issue3496.MyComponent.my_component />\n    </div>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3496.MyComponent do\n  use Phoenix.Component\n\n  def my_component(assigns) do\n    ~H\"\"\"\n    <div id=\"my-component\" phx-hook=\"MyHook\"></div>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3529.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3529Live do\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3529\n\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, :mounted, DateTime.utc_now())}\n  end\n\n  def handle_params(_params, _uri, socket) do\n    {:noreply, assign(socket, :next, :rand.uniform())}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <h1>Mounted at {@mounted}</h1>\n    <.link navigate={\"/issues/3529?param=#{@next}\"}>Navigate</.link>\n    <.link patch={\"/issues/3529?param=#{@next}\"}>Patch</.link>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3530.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3530Live do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  defmodule NestedLive do\n    use Phoenix.LiveView\n\n    def mount(_params, session, socket) do\n      {:ok, assign(socket, :item_id, session[\"item_id\"])}\n    end\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div id={\"item-outer-#{@item_id}\"}>\n        test hook with nested liveview\n        <div id={\"test-hook-#{@item_id}\"} phx-hook=\"test\"></div>\n      </div>\n      \"\"\"\n    end\n  end\n\n  def render(\"live.html\", assigns) do\n    ~H\"\"\"\n    <meta name=\"csrf-token\" content={Plug.CSRFProtection.get_csrf_token()} />\n    <script src=\"/assets/phoenix/phoenix.min.js\">\n    </script>\n    <script type=\"module\">\n      import { LiveSocket } from \"/assets/phoenix_live_view/phoenix_live_view.esm.js\";\n\n      let csrfToken = document\n        .querySelector(\"meta[name='csrf-token']\")\n        .getAttribute(\"content\");\n      let liveSocket = new LiveSocket(\"/live\", window.Phoenix.Socket, {\n        params: { _csrf_token: csrfToken },\n        hooks: {\n          test: {\n            mounted() {\n              console.log(this.__view().id, \"mounted hook!\");\n            },\n          },\n        },\n      });\n      liveSocket.connect();\n      window.liveSocket = liveSocket;\n    </script>\n\n    {@inner_content}\n    \"\"\"\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <ul id=\"stream-list\" phx-update=\"stream\">\n      <%= for {dom_id, item} <- @streams.items do %>\n        {live_render(@socket, NestedLive, id: dom_id, session: %{\"item_id\" => item.id})}\n      <% end %>\n    </ul>\n    <.link patch=\"/issues/3530?q=a\">patch a</.link>\n    <.link patch=\"/issues/3530?q=b\">patch b</.link>\n    <div phx-click=\"inc\">+</div>\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    socket =\n      socket\n      |> assign(:count, 3)\n      |> stream_configure(:items, dom_id: &\"item-#{&1.id}\")\n\n    {:ok, socket}\n  end\n\n  def handle_params(%{\"q\" => \"a\"}, _uri, socket) do\n    socket =\n      socket\n      |> stream(:items, [%{id: 1}, %{id: 3}], reset: true)\n\n    {:noreply, socket}\n  end\n\n  def handle_params(%{\"q\" => \"b\"}, _uri, socket) do\n    socket =\n      socket\n      |> stream(:items, [%{id: 2}, %{id: 3}], reset: true)\n\n    {:noreply, socket}\n  end\n\n  def handle_params(_params, _uri, socket) do\n    socket =\n      socket\n      |> stream(:items, [%{id: 1}, %{id: 2}, %{id: 3}], reset: true)\n\n    {:noreply, socket}\n  end\n\n  def handle_event(\"inc\", _params, socket) do\n    socket =\n      socket\n      |> update(:count, &(&1 + 1))\n      |> then(&stream_insert(&1, :items, %{id: &1.assigns.count}))\n\n    {:noreply, socket}\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3612.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3612.ALive do\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3612\n\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    {live_render(@socket, Phoenix.LiveViewTest.E2E.Issue3612.StickyLive,\n      id: \"sticky\",\n      sticky: true\n    )}\n\n    <h1>Page A</h1>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3612.BLive do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    {live_render(@socket, Phoenix.LiveViewTest.E2E.Issue3612.StickyLive,\n      id: \"sticky\",\n      sticky: true\n    )}\n\n    <h1>Page B</h1>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3612.StickyLive do\n  use Phoenix.LiveView\n\n  def mount(:not_mounted_at_router, _session, socket) do\n    {:ok, socket, layout: false}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      <.link phx-click=\"navigate_to_a\">Go to page A</.link>\n      <.link phx-click=\"navigate_to_b\">Go to page B</.link>\n    </div>\n    \"\"\"\n  end\n\n  def handle_event(\"navigate_to_a\", _params, socket) do\n    {:noreply, push_navigate(socket, to: \"/issues/3612/a\")}\n  end\n\n  def handle_event(\"navigate_to_b\", _params, socket) do\n    {:noreply, push_navigate(socket, to: \"/issues/3612/b\")}\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3636.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3636Live do\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3636\n  use Phoenix.LiveView\n\n  @impl Phoenix.LiveView\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <style>\n      .space-x-8 > :not([hidden]) ~ :not([hidden]) {\n        margin-left: 2rem;\n      }\n    </style>\n\n    <div class=\"container mx-auto p-8\">\n      <button>Outside 1</button>\n      <.focus_wrap id=\"focus-wrap\" class=\"space-x-8\">\n        <button id=\"first\" class=\"border rounded py-2 px-4\">One</button>\n        <button id=\"second\" class=\"border rounded py-2 px-4\">Two</button>\n        <button id=\"third\" class=\"border rounded py-2 px-4\">Three</button>\n      </.focus_wrap>\n      <button>Outside 2</button>\n    </div>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3647.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3647Live do\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3647\n  #\n  # The above issue was caused by LV uploads relying on DOM attributes like\n  # data-phx-active-refs=\"1,2,3\" being in the DOM to track uploads. The problem\n  # arises when the upload input is inside a form that is locked due to another,\n  # unrelated change. The following would happen:\n  #\n  # 1. User clicks on a button to upload a file\n  # 2. A hook calls this.uploadTo(), which triggers a validate event and locks the form\n  # 3. The hook also changes another input in ANOTHER form, which also triggers a separate validate\n  #    event and locks the form\n  # 4. The first validate completes, but the attributes are patched to the clone of the form,\n  #    the real DOM does not contain it.\n  # 5. LiveView tries to start uploading, but does not find any active files.\n  #\n  # This case is special in that the upload input belongs to a separate form (<input form=\"form-id\">),\n  # so it's not the upload input's form that is locked.\n  #\n  # The fix for this is to only try to upload when the closest locked element starting from\n  # the upload input is unlocked.\n  #\n  # There was a separate problem though: LiveView relied on a separate DOM patching mechanism\n  # when patching cloned trees that did not fully share the same logic as the default DOMPatch.\n  # In this case, it did not merge data-attributes on elements that are ignored (phx-update=\"ignore\" / data-phx-update=\"ignore\"),\n  # therefore, the first fix alone would not work.\n  # Now, we use the same patching logic for regular DOM patches and element unlocks.\n  #\n  # This difference in DOM patching logic also caused other issues, notably:\n  #   * https://github.com/phoenixframework/phoenix_live_view/issues/3591\n  #   * https://github.com/phoenixframework/phoenix_live_view/issues/3651\n  use Phoenix.LiveView\n\n  defmodule User do\n    import Ecto.Changeset\n    use Ecto.Schema\n\n    schema \"users\" do\n      field(:name)\n    end\n\n    def change_user(user, params \\\\ %{}) do\n      user |> cast(params, [:name])\n    end\n  end\n\n  def render(\"live.html\", assigns) do\n    ~H\"\"\"\n    <meta name=\"csrf-token\" content={Plug.CSRFProtection.get_csrf_token()} />\n    <script src=\"/assets/phoenix/phoenix.min.js\">\n    </script>\n    <script type=\"module\">\n      import { LiveSocket } from \"/assets/phoenix_live_view/phoenix_live_view.esm.js\";\n\n      let csrfToken = document\n        .querySelector(\"meta[name='csrf-token']\")\n        .getAttribute(\"content\");\n      let liveSocket = new LiveSocket(\"/live\", window.Phoenix.Socket, {\n        params: { _csrf_token: csrfToken },\n        hooks: {\n          JsUpload: {\n            mounted() {\n              this.el.addEventListener(\"click\", () => {\n                const fillBefore = \"before\" in this.el.dataset;\n                if (fillBefore) this.fill_input();\n                this.js_upload();\n                if (!fillBefore) this.fill_input();\n              });\n            },\n\n            js_upload() {\n              const content = \"x\".repeat(1024).repeat(1024);\n              const file = new File([content], \"1mb_of_x.txt\", {\n                type: \"text/plain\",\n              });\n              const input = document.querySelector(\"input[type=file]\");\n              this.uploadTo(input.form, input.name, [file]);\n            },\n\n            fill_input() {\n              const input = document.querySelector(\"input[type=text]\");\n              input.value = input.value + input.value.length;\n              const event = new Event(\"input\", { bubbles: true });\n              input.dispatchEvent(event);\n            },\n          },\n        },\n      });\n      liveSocket.connect();\n      window.liveSocket = liveSocket;\n    </script>\n    <style>\n      * { font-size: 1.1em; }\n    </style>\n\n    <main>{@inner_content}</main>\n    \"\"\"\n  end\n\n  @impl Phoenix.LiveView\n  def mount(_params, _session, socket) do\n    {:ok,\n     socket\n     |> assign(form: to_form(User.change_user(%User{})))\n     |> assign(:uploaded_files, [])\n     |> allow_upload(:avatar,\n       accept: ~w(.txt .md),\n       max_entries: 2,\n       auto_upload: true,\n       progress: &handle_progress/3\n     ), layout: {__MODULE__, :live}}\n  end\n\n  # with auto_upload: true we can consume files here\n  defp handle_progress(:avatar, entry, socket) do\n    if entry.done? do\n      uuid =\n        consume_uploaded_entry(socket, entry, fn _meta ->\n          {:ok, entry.uuid}\n        end)\n\n      {:noreply, update(socket, :uploaded_files, &[uuid | &1])}\n    else\n      {:noreply, socket}\n    end\n  end\n\n  @impl Phoenix.LiveView\n  def handle_params(_params, _uri, socket) do\n    {:noreply, socket}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"validate-user\", %{\"user\" => params}, socket) do\n    form =\n      %User{}\n      |> User.change_user(params)\n      |> to_form(action: :validate)\n\n    {:noreply, assign(socket, form: form)}\n  end\n\n  def handle_event(\"validate\", _params, socket) do\n    {:noreply, socket}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"cancel-upload\", %{\"ref\" => ref}, socket) do\n    {:noreply, cancel_upload(socket, :avatar, ref)}\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <.form for={@form} phx-change=\"validate-user\" id=\"user-form\">\n      <input id={@form[:name].id} name={@form[:name].name} value={@form[:name].value} type=\"text\" />\n      <button id=\"x\" type=\"button\" phx-hook=\"JsUpload\">\n        Upload then Input\n      </button>\n      <button id=\"y\" type=\"button\" phx-hook=\"JsUpload\" data-before>\n        Input then Upload\n      </button>\n      <.live_file_input upload={@uploads.avatar} form=\"auto-form\" />\n    </.form>\n\n    <form id=\"auto-form\" phx-change=\"validate\"></form>\n    <section class=\"pending-uploads\" phx-drop-target={@uploads.avatar.ref} style=\"min-height: 100%;\">\n      <h3>Pending Uploads ({length(@uploads.avatar.entries)})</h3>\n\n      <%= for entry <- @uploads[:avatar].entries do %>\n        <div>\n          <progress value={entry.progress} max=\"100\">{entry.progress}%</progress>\n          <div>\n            {entry.uuid}<br />\n            <a\n              href=\"#\"\n              phx-click=\"cancel-upload\"\n              phx-value-ref={entry.ref}\n              class=\"upload-entry__cancel\"\n            >\n              Cancel Upload\n            </a>\n          </div>\n        </div>\n      <% end %>\n    </section>\n\n    <ul>\n      <li :for={file <- @uploaded_files}><a href={file}>{Path.basename(file)}</a></li>\n    </ul>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3651.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3651Live do\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3651\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    if connected?(socket) do\n      send(self(), :change_id)\n    end\n\n    # assigns for pre_script\n    assigns = %{}\n\n    socket =\n      socket\n      |> assign(id: 1, counter: 0)\n      |> assign(\n        :pre_script,\n        ~H\"\"\"\n        <script>\n          window.hooks.OuterHook = {\n            mounted() {\n              this.pushEvent(\"lol\");\n            },\n          };\n          window.hooks.InnerHook = {\n            mounted() {\n              console.log(\"MOUNTED\", this.el);\n              this.handleEvent(\"myevent\", this._handleEvent(this));\n            },\n            destroyed() {\n              document.getElementById(\"notice\").innerHTML = \"\";\n              console.log(\"DESTROYED\", this.el);\n            },\n            _handleEvent(self) {\n              return () => {\n                setTimeout(() => {\n                  console.warn(\"reloading\", self.el);\n                  self.pushEvent(\"reload\", {});\n                }, 50);\n              };\n            },\n          };\n        </script>\n        \"\"\"\n      )\n      |> push_event(\"myevent\", %{})\n\n    {:ok, socket}\n  end\n\n  def handle_info(:change_id, socket) do\n    {:noreply, assign(socket, id: 2)}\n  end\n\n  def handle_event(\"lol\", _params, socket) do\n    {:noreply, socket}\n  end\n\n  def handle_event(\"reload\", _params, socket) do\n    counter = socket.assigns.counter + 1\n\n    socket =\n      socket\n      |> push_event(\"myevent\", %{})\n      |> assign(counter: counter)\n\n    socket =\n      if counter > 4096 do\n        raise \"that's enough, bye!\"\n      else\n        socket\n      end\n\n    {:noreply, socket}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div id=\"main\" phx-hook=\"OuterHook\">\n      <div phx-hook=\"InnerHook\" id={\"id-#{@id}\"} />\n      This is an example of nested hooks resulting in a \"ghost\" element\n      that isn't on the DOM, and is never cleaned up. In this specific example\n      a timeout is used to show how the number of events being sent to the server\n      grows exponentially.\n      <p>Doing any of the following things fixes it:</p>\n      <ol>\n        <li>Setting the `phx-hook` to use a fixed id.</li>\n        <li>Removing the `pushEvent` from the OuterHook `mounted` callback.</li>\n        <li>Deferring the pushEvent by wrapping it in a setTimeout.</li>\n      </ol>\n    </div>\n    <div>\n      To prevent blowing up your computer, the page will reload after 4096 events, which takes ~12 seconds\n    </div>\n    <div style=\"color: blue; font-size: 20px\" id=\"counter\">\n      Total Event Calls: <span id=\"total\">{@counter}</span>\n    </div>\n    <div style=\"color: red; font-size: 72px\" id=\"notice\" phx-update=\"ignore\">\n      I will disappear if the bug is not present.\n    </div>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3656.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3656Live do\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3656\n\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <style>\n      * { font-size: 1.1em }\n      nav { margin-top: 1em }\n      nav a { padding: 8px 16px; border: 1px solid black; text-decoration: none }\n      nav a:visited { color: inherit }\n      nav a.active { border: 3px solid green }\n      nav a.phx-click-loading { animation: pulsate 2s infinite }\n      @keyframes pulsate {\n        0% {\n          background-color: white;\n        }\n        50% {\n          background-color: red;\n        }\n        100% {\n          background-color: white;\n        }\n      }\n    </style>\n\n    {live_render(@socket, Phoenix.LiveViewTest.E2E.Issue3656Live.Sticky,\n      id: \"sticky\",\n      sticky: true\n    )}\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3656Live.Sticky do\n  use Phoenix.LiveView\n\n  def mount(:not_mounted_at_router, _session, socket) do\n    {:ok, socket, layout: false}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <nav>\n      <.link navigate=\"/issues/3656?navigated=true\">Link 1</.link>\n    </nav>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3658.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3658Live do\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3658\n\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <.link navigate=\"/issues/3658?navigated=true\">Link 1</.link>\n\n    {live_render(@socket, Phoenix.LiveViewTest.E2E.Issue3658Live.Sticky,\n      id: \"sticky\",\n      sticky: true\n    )}\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3658Live.Sticky do\n  use Phoenix.LiveView\n\n  def mount(:not_mounted_at_router, _session, socket) do\n    {:ok, socket, layout: false}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      <div id=\"foo\" phx-remove={Phoenix.LiveView.JS.dispatch(\"my-event\")}>Hi</div>\n    </div>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3681.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3681Live do\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3681\n\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  def render(\"live.html\", assigns) do\n    ~H\"\"\"\n    {apply(Phoenix.LiveViewTest.E2E.Layout, :render, [\n      \"live.html\",\n      Map.put(assigns, :inner_content, [])\n    ])}\n\n    {live_render(\n      @socket,\n      Phoenix.LiveViewTest.E2E.Issue3681.StickyLive,\n      id: \"sticky\",\n      sticky: true\n    )}\n\n    <hr />\n    {@inner_content}\n    <hr />\n    \"\"\"\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <h3>A LiveView that does nothing but render it's layout.</h3>\n    <.link navigate=\"/issues/3681/away\">Go to a different LV with a (funcky) stream</.link>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3681.AwayLive do\n  use Phoenix.LiveView, layout: {Phoenix.LiveViewTest.E2E.Issue3681Live, :live}\n\n  def mount(_params, _session, socket) do\n    socket =\n      socket\n      |> stream(:messages, [])\n      # <--- This is the root cause\n      |> stream(:messages, [msg(4)], reset: true)\n\n    {:ok, socket}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <h3>A liveview with a stream configured twice</h3>\n    <h4>This causes the nested liveview in the layout above to be reset by the client.</h4>\n\n    <.link navigate=\"/issues/3681\">Go back to (the now borked) LV without a stream</.link>\n    <h1>Normal Stream</h1>\n    <div id=\"msgs-normal\" phx-update=\"stream\">\n      <div :for={{dom_id, msg} <- @streams.messages} id={dom_id}>\n        <div>{msg.msg}</div>\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  defp msg(num) do\n    %{id: num, msg: num}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3681.StickyLive do\n  use Phoenix.LiveView, layout: false\n\n  def mount(_params, _session, socket) do\n    {:ok, stream(socket, :messages, [msg(1), msg(2), msg(3)])}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div id=\"msgs-sticky\" phx-update=\"stream\">\n      <div :for={{dom_id, msg} <- @streams.messages} id={dom_id}>\n        <div>{msg.msg}</div>\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  defp msg(num) do\n    %{id: num, msg: num}\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3684.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3684Live do\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3684\n  use Phoenix.LiveView\n\n  defmodule BadgeForm do\n    use Phoenix.LiveComponent\n\n    def mount(socket) do\n      socket =\n        socket\n        |> assign(:type, :huey)\n\n      {:ok, socket}\n    end\n\n    def update(assigns, socket) do\n      socket =\n        socket\n        |> assign(:form, assigns.form)\n\n      {:ok, socket}\n    end\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div>\n        <.form\n          for={@form}\n          id=\"foo\"\n          class=\"max-w-lg p-8 flex flex-col gap-4\"\n          phx-change=\"change\"\n          phx-submit=\"submit\"\n        >\n          <.radios type={@type} form={@form} myself={@myself} />\n        </.form>\n      </div>\n      \"\"\"\n    end\n\n    defp radios(assigns) do\n      ~H\"\"\"\n      <fieldset>\n        <legend>Radio example:</legend>\n        <%= for type <- [:huey, :dewey] do %>\n          <div phx-click=\"change-type\" phx-value-type={type} phx-target={@myself}>\n            <input type=\"radio\" id={type} name=\"type\" value={type} checked={@type == type} />\n            <label for={type}>{type}</label>\n          </div>\n        <% end %>\n      </fieldset>\n      \"\"\"\n    end\n\n    def handle_event(\"change-type\", %{\"type\" => type}, socket) do\n      type = String.to_existing_atom(type)\n      socket = assign(socket, :type, type)\n      {:noreply, socket}\n    end\n  end\n\n  defp changeset(params) do\n    data = %{}\n\n    types = %{\n      type: :string\n    }\n\n    {data, types}\n    |> Ecto.Changeset.cast(params, Map.keys(types))\n    |> Ecto.Changeset.validate_required(:type)\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, form: to_form(changeset(%{}), as: :foo), payload: nil)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <.live_component id=\"badge_form\" module={__MODULE__.BadgeForm} action={@live_action} form={@form} />\n    \"\"\"\n  end\n\n  def handle_event(\"change\", _params, socket) do\n    {:noreply, socket}\n  end\n\n  def handle_event(\"submit\", _params, socket) do\n    {:noreply, socket}\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3686.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3686.ALive do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    <h1>A</h1>\n    <button phx-click=\"go\">To B</button>\n\n    <div id=\"flash\">\n      {inspect(@flash)}\n    </div>\n    \"\"\"\n  end\n\n  def handle_event(\"go\", _unsigned_params, socket) do\n    {:noreply, socket |> put_flash(:info, \"Flash from A\") |> push_navigate(to: \"/issues/3686/b\")}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3686.BLive do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    <h1>B</h1>\n    <button phx-click=\"go\">To C</button>\n\n    <div id=\"flash\">\n      {inspect(@flash)}\n    </div>\n    \"\"\"\n  end\n\n  def handle_event(\"go\", _unsigned_params, socket) do\n    {:noreply, socket |> put_flash(:info, \"Flash from B\") |> redirect(to: \"/issues/3686/c\")}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3686.CLive do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    <h1>C</h1>\n    <button phx-click=\"go\">To A</button>\n\n    <div id=\"flash\">\n      {inspect(@flash)}\n    </div>\n    \"\"\"\n  end\n\n  def handle_event(\"go\", _unsigned_params, socket) do\n    {:noreply, socket |> put_flash(:info, \"Flash from C\") |> push_navigate(to: \"/issues/3686/a\")}\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3709.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3709Live do\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3709\n  use Phoenix.LiveView\n\n  defmodule SomeComponent do\n    use Phoenix.LiveComponent\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div>\n        Hello\n      </div>\n      \"\"\"\n    end\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, id: nil)}\n  end\n\n  def handle_params(params, _, socket) do\n    {:noreply, assign(socket, :id, params[\"id\"])}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <ul>\n      <li :for={i <- 1..10}>\n        <.link patch={\"/issues/3709/#{i}\"}>Link {i}</.link>\n      </li>\n    </ul>\n    <div>\n      <.live_component module={SomeComponent} id={\"user-#{@id}\"} /> id: {@id}\n      <div>\n        Click the button, then click any link.\n        <button onclick=\"document.querySelectorAll('li a').forEach((x) => x.click())\">\n          Break Stuff\n        </button>\n      </div>\n    </div>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3719.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3719Live do\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3719\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, :target, nil)}\n  end\n\n  def handle_event(\"inc\", %{\"_target\" => target}, socket) do\n    {:noreply, assign(socket, :target, target)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <form phx-change=\"inc\">\n      <input id=\"a\" type=\"text\" name=\"foo\" />\n      <input id=\"b\" type=\"text\" name=\"foo[bar]\" />\n    </form>\n    <span id=\"target\">{inspect(@target)}</span>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3814.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3814Live do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, :trigger_submit, false)}\n  end\n\n  def handle_event(\"submit\", _params, socket) do\n    {:noreply, assign(socket, :trigger_submit, true)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <.form phx-submit=\"submit\" phx-trigger-action={@trigger_submit} action=\"/submit\" method=\"post\">\n      <input type=\"hidden\" name=\"greeting\" value=\"hello\" />\n      <button type=\"submit\" name=\"i-am-the-submitter\" value=\"submitter-value\">Submit</button>\n    </.form>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3819.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3819Live do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, :reconnected, false)}\n  end\n\n  def handle_event(\"validate\", _params, socket) do\n    {:noreply, socket}\n  end\n\n  def handle_event(\"save\", _params, socket) do\n    {:noreply, socket}\n  end\n\n  def handle_event(\"reconnected\", _params, socket) do\n    {:noreply, assign(socket, :reconnected, true)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <.form id=\"recover\" phx-change=\"validate\" phx-submit=\"save\">\n      <button>Submit</button>\n    </.form>\n\n    <p :if={@reconnected} id=\"reconnected\">Reconnected!</p>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3919.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3919Live do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, action: %{text: \"No red\"})}\n  end\n\n  def handle_event(\"toggle_special\", %{}, socket) do\n    new_action =\n      if socket.assigns.action[:attrs] do\n        %{text: \"No red\"}\n      else\n        %{text: \"Red\", attrs: %{special: true}}\n      end\n\n    {:noreply, assign(socket, action: new_action)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <.my_component {@action[:attrs] || %{}}>{@action.text}</.my_component>\n\n    <button phx-click=\"toggle_special\">toggle</button>\n    \"\"\"\n  end\n\n  attr(:special, :boolean, default: false)\n  slot(:inner_block)\n\n  defp my_component(assigns) do\n    ~H\"\"\"\n    <div style={if(@special, do: \"background-color: red;\")}>\n      {render_slot(@inner_block)}\n    </div>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3931.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3931Live do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    socket =\n      socket\n      |> assign_async(:slow_data, fn ->\n        Process.sleep(100)\n        {:ok, %{slow_data: \"This was loaded asynchronously!\"}}\n      end)\n\n    {:ok, socket}\n  end\n\n  def layout(assigns) do\n    ~H\"\"\"\n    <div class=\"max-w-4xl mx-auto p-8\">\n      {render_slot(@inner_block)}\n    </div>\n    \"\"\"\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <.layout {assigns}>\n      <.async_result :let={data} assign={@slow_data}>\n        <:loading>\n          <div id=\"async\" class=\"flex items-center space-x-3\">\n            <div class=\"animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500\"></div>\n            <p class=\"text-gray-600\">Loading data...</p>\n          </div>\n        </:loading>\n\n        <div id=\"async\" class=\"space-y-3\">\n          {data}\n        </div>\n      </.async_result>\n    </.layout>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3941.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3941Live do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  alias Phoenix.LiveViewTest.E2E.Issue3941Live.Item\n\n  @all_items [\n    \"Item_1\",\n    \"Item_2\"\n  ]\n\n  @impl true\n  def mount(_params, _session, socket) do\n    socket =\n      socket\n      |> assign(:selected_items, @all_items)\n      |> assign(:filter_options, @all_items)\n\n    {:ok, socket}\n  end\n\n  def render(\"live.html\", assigns) do\n    ~H\"\"\"\n    <meta name=\"csrf-token\" content={Plug.CSRFProtection.get_csrf_token()} />\n    <script src=\"/assets/phoenix/phoenix.min.js\">\n    </script>\n    <script type=\"module\">\n      import { LiveSocket } from \"/assets/phoenix_live_view/phoenix_live_view.esm.js\";\n      let csrfToken = document\n        .querySelector(\"meta[name='csrf-token']\")\n        .getAttribute(\"content\");\n      let liveSocket = new LiveSocket(\"/live\", window.Phoenix.Socket, {\n        params: { _csrf_token: csrfToken },\n        hooks: {\n          PagePositionNotifier: {\n            mounted() {\n              this.pushEvent(\"page_position_update\", {});\n            },\n          },\n        },\n      });\n      liveSocket.connect();\n    </script>\n\n    {@inner_content}\n    \"\"\"\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <.multi_select id=\"multi-select\" items={@filter_options} selected={@selected_items} />\n    <div :for={item <- @selected_items}>\n      <.live_component\n        module={Item}\n        id={\"item-#{item}\"}\n        item={item}\n      />\n    </div>\n    \"\"\"\n  end\n\n  def multi_select(assigns) do\n    ~H\"\"\"\n    <div :for={item <- @items}>\n      <label for={\"item-select-#{item}\"}>\n        <input\n          type=\"checkbox\"\n          phx-click=\"toggle_item\"\n          phx-value-clicked={item}\n          id={\"select-#{item}\"}\n          name=\"select\"\n          value={item}\n          checked={item in @selected}\n        />\n        {item}\n      </label>\n    </div>\n    \"\"\"\n  end\n\n  @impl true\n  def handle_event(\"toggle_item\", params = %{\"clicked\" => clicked_id}, socket) do\n    selected = socket.assigns.selected_items\n\n    selected =\n      case params[\"value\"] do\n        nil ->\n          selected = List.delete(selected, clicked_id)\n\n        value ->\n          selected = selected ++ [value]\n      end\n\n    {:noreply, assign(socket, selected_items: Enum.sort(selected))}\n  end\n\n  def handle_event(\"page_position_update\", _params, socket) do\n    {:noreply, socket}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3941Live.Item do\n  use Phoenix.LiveComponent\n  alias Phoenix.LiveViewTest.E2E.Issue3941Live.ItemHeader\n\n  @impl true\n  def update(assigns, socket) do\n    {:ok,\n     socket\n     |> assign(:item, assigns.item)\n     |> assign_unrendered_component_assigns()}\n  end\n\n  @impl true\n  def render(assigns) do\n    ~H\"\"\"\n    <div id={\"item-#{@item}\"} phx-hook=\"PagePositionNotifier\">\n      <.live_component\n        id={\"item-header-#{@item}\"}\n        module={ItemHeader}\n        item={@item}\n      />\n      <.unrendered_component :if={false} id=\"unrendered\" any_assign={@any_assign} />\n    </div>\n    \"\"\"\n  end\n\n  defp unrendered_component(_) do\n    raise \"SHOULD NOT BE CALLED\"\n  end\n\n  defp assign_unrendered_component_assigns(socket) do\n    socket\n    |> assign(:any_assign, true)\n    |> assign_async(\n      :any_assign,\n      fn ->\n        {:ok, %{any_assign: true}}\n      end\n    )\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3941Live.ItemHeader do\n  use Phoenix.LiveComponent\n\n  def update(assigns, socket) do\n    {:ok,\n     socket\n     |> assign(:item, assigns.item)\n     |> assign_async(\n       :async_assign,\n       fn ->\n         {:ok, %{async_assign: :assign}}\n       end,\n       reset: true\n     )}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div id={\"header-#{@item}\"}>\n      <.async_result assign={@async_assign}>\n        <:loading>\n          <div id={@item} class=\"border border-y-0 bg-red-500 text-white\">\n            {\"#{@item} - I AM LOADING\"}\n          </div>\n        </:loading>\n        <div id={@item} class=\"border border-y-0 bg-green-500 text-white\">\n          {\"#{@item} - I AM LOADED!\"}\n        </div>\n      </.async_result>\n      {inspect(@async_assign)}\n    </div>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3953.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3953Live do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  alias Phoenix.LiveView.JS\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, :show, false)}\n  end\n\n  def handle_event(\"toggle\", _params, socket) do\n    {:noreply, assign(socket, :show, !socket.assigns.show)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <.live_component module={Phoenix.LiveViewTest.E2E.Issue3953Live.Component} id=\"comp\" />\n    <button phx-click=\"toggle\">Show</button>\n    <%= if @show do %>\n      {live_render(@socket, Phoenix.LiveViewTest.E2E.Issue3953Live.NestedViewLive,\n        id: \"nested_view\",\n        session: %{}\n      )}\n    <% end %>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3953Live.NestedViewLive do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    Nested Content\n    <.live_component module={Phoenix.LiveViewTest.E2E.Issue3953Live.Component} id=\"comp2\" />\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3953Live.Component do\n  use Phoenix.LiveComponent\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      Component\n    </div>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_3979.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue3979Live do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  alias Phoenix.LiveView.JS\n\n  def mount(_params, _session, socket) do\n    {:ok,\n     socket\n     |> assign(:counter, 1)\n     |> assign(:components, for(i <- 1..10, do: %{id: i, counter: 0}))}\n  end\n\n  def handle_event(\"bump\", _params, socket) do\n    Process.send_after(self(), {:update, socket.assigns.counter}, 100)\n\n    new_components =\n      for {component, i} <- Enum.with_index(socket.assigns.components, 1) do\n        if i == socket.assigns.counter do\n          %{component | counter: component.counter + 1}\n        else\n          component\n        end\n      end\n\n    {:noreply,\n     socket\n     |> assign(:components, new_components)\n     |> assign(:counter, socket.assigns.counter + 1)}\n  end\n\n  def handle_info({:update, i}, socket) do\n    send_update(Phoenix.LiveViewTest.E2E.Issue3979Live.Component, id: \"comp-#{i}\", counter: 10)\n\n    {:noreply, socket}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <.live_component\n      :for={component <- @components}\n      module={Phoenix.LiveViewTest.E2E.Issue3979Live.Component}\n      id={\"comp-#{component.id}\"}\n      dom_id={\"hello-#{component.id}-#{component.counter}\"}\n      counter={component.counter}\n    />\n    <button phx-click=\"bump\">Bump ID (and counter)</button>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue3979Live.Component do\n  use Phoenix.LiveComponent\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div id={@dom_id}>\n      {@counter}\n    </div>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_4027.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue4027Live do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  alias Phoenix.LiveViewTest.E2E.Issue4027Live\n  alias Phoenix.LiveView.JS\n  alias Phoenix.LiveView.AsyncResult\n\n  def mount(params, _session, socket) do\n    {:ok, socket |> assign(:data, AsyncResult.ok([])) |> assign(:case, params[\"case\"] || \"first\")}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div class=\"p-4\">\n      <p class=\"my-4\">\n        Click Load Data. 3 items should be displayed. Then click Remove First entry. The expected result is 2 items displayed.\n      </p>\n      <div>\n        <.async_result :let={data} :if={@case == \"first\"} assign={@data}>\n          <.live_component module={Issue4027Live.ReproLiveComponent} id=\"repro\" data={data} />\n        </.async_result>\n        <%= if @case == \"second\" do %>\n          <div style=\"margin: 10px; height: 1px; background-color: black;\"></div>\n          <.live_component\n            module={Issue4027Live.ReproLiveComponentWithAsyncResult}\n            id=\"repro_async\"\n            data={@data}\n          />\n        <% end %>\n      </div>\n      <div>\n        <button phx-click=\"load\">Load data</button>\n        <button phx-click=\"remove\">Remove first entry</button>\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  def handle_event(\"load\", _, socket) do\n    socket =\n      assign_async(socket, :data, fn ->\n        Process.sleep(100)\n\n        {:ok,\n         %{data: [%{id: 1, value: \"First\"}, %{id: 2, value: \"Second\"}, %{id: 3, value: \"Third\"}]}}\n      end)\n\n    {:noreply, socket}\n  end\n\n  def handle_event(\"remove\", _, socket) do\n    socket =\n      assign_async(socket, :data, fn ->\n        Process.sleep(100)\n        {:ok, %{data: [%{id: 2, value: \"Second\"}, %{id: 3, value: \"Third\"}]}}\n      end)\n\n    {:noreply, socket}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue4027Live.ReproLiveComponentWithAsyncResult do\n  use Phoenix.LiveComponent\n\n  def mount(socket) do\n    {:ok, socket}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div id=\"result\">\n      <.async_result :let={data} assign={@data}>\n        <p :for={item <- data} :key={item.id}>{item.value}</p>\n      </.async_result>\n    </div>\n    \"\"\"\n  end\n\n  def handle_event(_, _, socket) do\n    {:noreply, socket}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue4027Live.ReproLiveComponent do\n  use Phoenix.LiveComponent\n\n  def mount(socket) do\n    {:ok, socket}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div id=\"result\">\n      <p :for={item <- @data} :key={item.id}>{item.value}</p>\n    </div>\n    \"\"\"\n  end\n\n  def handle_event(_, _, socket) do\n    {:noreply, socket}\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_4066.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue4066Live do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  alias Phoenix.LiveViewTest.E2E.Issue4066Live\n\n  def mount(params, _session, socket) do\n    {:ok, assign(socket, delay: params[\"delay\"] || 3000, render_lc: true)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <p id=\"render-time\">{DateTime.utc_now()}</p>\n    <button phx-click=\"toggle\">Toggle</button>\n    <.live_component :if={@render_lc} id=\"foo\" delay={@delay} module={Issue4066Live.LiveComponent} />\n    \"\"\"\n  end\n\n  def handle_event(\"toggle\", _params, socket) do\n    {:noreply, assign(socket, :render_lc, !socket.assigns.render_lc)}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Issue4066Live.LiveComponent do\n  use Phoenix.LiveComponent\n\n  def render(assigns) do\n    ~H\"\"\"\n    <script :type={Phoenix.LiveView.ColocatedHook} name=\".MyHook\">\n      export default {\n        mounted() {\n          this.el.addEventListener(\"input\", () => {\n            setTimeout(() => {\n              this.pushEventTo(this.el, \"do-something\", { value: 100 });\n              this.liveSocket.js().setAttribute(document.body, \"data-pushed\", \"yes\");\n            }, parseInt(this.el.dataset.delay));\n          });\n        },\n      };\n    </script>\n    <input phx-hook=\".MyHook\" data-delay={@delay} target={@myself} id={@id} />\n    \"\"\"\n  end\n\n  def handle_event(\"do-something\", %{\"value\" => value}, socket) do\n    {:noreply, socket}\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_4078.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue4078Live do\n  # https://github.com/phoenixframework/phoenix_live_view/issues/4078\n  #\n  # live_file_input uses data-phx-update=\"ignore\" to preserve file selection,\n  # but this was blocking updates to attributes like class, disabled, and style.\n  # This test verifies these attributes can now be changed dynamically.\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  def render(\"live.html\", assigns) do\n    ~H\"\"\"\n    <meta name=\"csrf-token\" content={Plug.CSRFProtection.get_csrf_token()} />\n    <script src=\"/assets/phoenix/phoenix.min.js\">\n    </script>\n    <script type=\"module\">\n      import { LiveSocket } from \"/assets/phoenix_live_view/phoenix_live_view.esm.js\";\n      let csrfToken = document\n        .querySelector(\"meta[name='csrf-token']\")\n        .getAttribute(\"content\");\n      let liveSocket = new LiveSocket(\"/live\", window.Phoenix.Socket, {\n        params: { _csrf_token: csrfToken },\n      });\n      liveSocket.connect();\n    </script>\n    {@inner_content}\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok,\n     socket\n     |> assign(:disabled?, true)\n     |> assign(:custom_class, \"initial-class\")\n     |> allow_upload(:avatar, accept: ~w(.jpg .jpeg .png .txt), max_entries: 2)}\n  end\n\n  def handle_event(\"validate\", _params, socket), do: {:noreply, socket}\n\n  def handle_event(\"toggle-disabled\", _params, socket) do\n    {:noreply, assign(socket, :disabled?, !socket.assigns.disabled?)}\n  end\n\n  def handle_event(\"toggle-class\", _params, socket) do\n    new_class =\n      if socket.assigns.custom_class == \"initial-class\",\n        do: \"updated-class\",\n        else: \"initial-class\"\n\n    {:noreply, assign(socket, :custom_class, new_class)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <form id=\"upload-form\" phx-change=\"validate\">\n      <.live_file_input upload={@uploads.avatar} disabled={@disabled?} class={@custom_class} />\n    </form>\n\n    <button id=\"toggle-disabled\" type=\"button\" phx-click=\"toggle-disabled\">Toggle Disabled</button>\n    <button id=\"toggle-class\" type=\"button\" phx-click=\"toggle-class\">Toggle Class</button>\n\n    <article :for={entry <- @uploads.avatar.entries} class=\"upload-entry\">\n      <span class=\"entry-name\">{entry.client_name}</span>\n    </article>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_4088.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue4088Live do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <.live_component module={__MODULE__.LC} id=\"lc\" />\n    \"\"\"\n  end\n\n  defmodule LC do\n    use Phoenix.LiveComponent\n\n    def mount(socket) do\n      {:ok, assign(socket, :test, \"value\")}\n    end\n\n    def render(assigns) do\n      ~H\"\"\"\n      <script :type={Phoenix.LiveView.ColocatedHook} name=\".MyHook\">\n        export default {\n          mounted() {\n            this.pushEventTo(this.el, \"my_update\", {});\n            this.pushEventTo(this.el, \"my_update\", {});\n            this.pushEventTo(this.el, \"my_update\", {});\n          },\n        };\n      </script>\n      <div id=\"foo\" phx-hook=\".MyHook\" phx-target={@myself}>\n        {@test}\n      </div>\n      \"\"\"\n    end\n\n    def handle_event(\"my_update\", _params, socket) do\n      {:noreply, assign(socket, :test, :rand.uniform())}\n    end\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_4094.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue4094Live do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  def handle_params(params, _uri, socket) do\n    if params[\"foo\"] == \"bar\" do\n      {:noreply, redirect(socket, to: \"/navigation/a\")}\n    else\n      {:noreply, socket}\n    end\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <.link patch=\"/issues/4094?foo=bar\">Patch</.link>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_4095.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue4095Live do\n  use Phoenix.LiveView, layout: {__MODULE__, :live}\n\n  def render(assigns) do\n    ~H\"\"\"\n    <.form :let={f} for={@form} phx-change=\"validate\">\n      <input type=\"text\" name={f[:show?].name} id={f[:show?].id} value={f[:show?].value} />\n\n      <.portal id=\"portal\" target=\"#portal_target\">\n        <div>\n          <.button :if={!!f[:show?].value}>Show?</.button>\n        </div>\n      </.portal>\n    </.form>\n\n    <div id=\"portal_target\"></div>\n    \"\"\"\n  end\n\n  def mount(_, _, socket) do\n    form = %{\"show?\" => true} |> to_form\n\n    {:ok, assign(socket, form: form)}\n  end\n\n  def handle_event(\"validate\", params, socket) do\n    form = params |> to_form\n\n    {:noreply, assign(socket, form: form)}\n  end\n\n  attr :rest, :global\n\n  defp button(assigns) do\n    ~H\"\"\"\n    <button {@rest}>{render_slot(@inner_block)}</button>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_4102.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue4102Live do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, socket |> assign(form: to_form(%{\"name\" => \"Test\"}))}\n  end\n\n  def handle_event(\"validate\", %{\"name\" => name}, socket) do\n    IO.inspect(name, label: \"Name\")\n    {:noreply, socket |> assign(form: to_form(%{\"name\" => name}))}\n  end\n\n  def handle_event(\"submit\", %{\"name\" => name}, socket) do\n    IO.inspect(name, label: \"Name\")\n    {:noreply, socket |> assign(form: to_form(%{\"name\" => name}))}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      <input\n        form=\"my-form\"\n        phx-debounce=\"500\"\n        name={@form[:name].name}\n        id={@form[:name].id}\n        value={@form[:name].value}\n        type=\"text\"\n      />\n      <.form for={@form} id=\"my-form\" phx-change=\"validate\" phx-submit=\"submit\">\n        <button type=\"submit\" phx-disable-with=\"Submitting...\">Submit</button>\n      </.form>\n    </div>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_4107.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue4107Live do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      <.portal id=\"test-form-portal\" target=\"body\">\n        <.form id=\"test-form\" for={%{}} as={:test_form} action=\"/api/test\" method=\"post\">\n          <input type=\"hidden\" name=\"test_input\" value=\"test_value\" />\n        </.form>\n      </.portal>\n      <button type=\"submit\" form=\"test-form\">Submit</button>\n    </div>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_4121.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue4121Live do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    socket\n    |> stream(:items, [%{id: 1, name: \"Item 1\"}, %{id: 2, name: \"Item 2\"}])\n    |> then(&{:ok, &1})\n  end\n\n  def handle_event(\"reset-stream\", _params, socket) do\n    id = System.unique_integer()\n\n    {:noreply, stream(socket, :items, [%{id: id, name: \"Item #{id}\"}], reset: true)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <button phx-click=\"reset-stream\">Reset teleported stream</button>\n\n    <.portal id=\"teleported-stream\" target=\"body\">\n      <ul id=\"stream-in-lv\" phx-update=\"stream\">\n        <li :for={{id, item} <- @streams.items} id={id}>\n          {item.name}\n        </li>\n      </ul>\n    </.portal>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/issues/issue_4147.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Issue4147Live do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok,\n     assign(socket, :render_in_root, fn assigns ->\n       ~H\"\"\"\n       <div id=\"foobar\" phx-hook=\".HookOutside\"></div>\n       <script :type={Phoenix.LiveView.ColocatedHook} name=\".HookOutside\">\n         export default {\n           mounted() {\n             console.log(\"HookOutside mounted\");\n           },\n         };\n       </script>\n       \"\"\"\n     end)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <h1>Inside</h1>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/js_live.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.JsLive do\n  use Phoenix.LiveView\n\n  alias Phoenix.LiveView.JS\n\n  @impl Phoenix.LiveView\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, count: 0)}\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <div id=\"my-modal\" aria-expanded=\"false\" style=\"display: none;\">Test</div>\n\n    <button phx-click={\n      JS.show(to: \"#my-modal\", transition: \"fade-in\", time: 50)\n      |> JS.set_attribute({\"aria-expanded\", \"true\"}, to: \"#my-modal\")\n      |> JS.set_attribute({\"open\", \"true\"}, to: \"#my-modal\")\n    }>\n      show modal\n    </button>\n\n    <button phx-click={\n      JS.hide(to: \"#my-modal\", transition: \"fade-out\", time: 50)\n      |> JS.set_attribute({\"aria-expanded\", \"false\"}, to: \"#my-modal\")\n      |> JS.remove_attribute(\"open\", to: \"#my-modal\")\n    }>\n      hide modal\n    </button>\n\n    <button phx-click={\n      JS.toggle(to: \"#my-modal\", in: \"fade-in\", out: \"fade-out\", time: 50)\n      |> JS.toggle_attribute({\"aria-expanded\", \"true\", \"false\"}, to: \"#my-modal\")\n      |> JS.toggle_attribute({\"open\", \"true\"}, to: \"#my-modal\")\n    }>\n      toggle modal\n    </button>\n\n    <details phx-mounted={JS.ignore_attributes([\"open\"])}>\n      <summary>Details</summary>\n      <button phx-click=\"increment\">{@count}</button>\n    </details>\n    \"\"\"\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"increment\", _params, socket) do\n    {:noreply, update(socket, :count, &(&1 + 1))}\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/keyed_comprehension_live.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.KeyedComprehensionLive do\n  use Phoenix.LiveView\n\n  @count 10\n\n  def render(assigns) do\n    ~H\"\"\"\n    <link href=\"https://cdn.jsdelivr.net/npm/daisyui@5\" rel=\"stylesheet\" type=\"text/css\" />\n    <div class=\"p-8\">\n      <div class=\"border-b border-gray-200 mb-6\">\n        <nav role=\"tablist\" class=\"tabs tabs-border\">\n          <.link\n            role=\"tab\"\n            class={\"tab #{if @active_tab == \"all_keyed\", do: \"tab-active\"}\"}\n            patch=\"/keyed-comprehension?tab=all_keyed\"\n          >\n            All keyed\n          </.link>\n          <.link\n            role=\"tab\"\n            class={\"tab #{if @active_tab == \"rows_keyed\", do: \"tab-active\"}\"}\n            patch=\"/keyed-comprehension?tab=rows_keyed\"\n          >\n            Rows keyed\n          </.link>\n          <.link\n            role=\"tab\"\n            class={\"tab #{if @active_tab == \"no_keyed\", do: \"tab-active\"}\"}\n            patch=\"/keyed-comprehension?tab=no_keyed\"\n          >\n            No keyed\n          </.link>\n        </nav>\n      </div>\n\n      <button class=\"btn\" phx-click=\"randomize\">randomize</button>\n      <button class=\"btn\" phx-click=\"change_0\">change first</button>\n      <button class=\"btn\" phx-click=\"change_other\">change other</button>\n\n      <form>\n        <input phx-change=\"change_size\" name=\"size\" value={@size} />\n      </form>\n\n      <div :for={i <- 1..2} :key={i}>\n        <.table_with_all_keyed\n          :if={@active_tab == \"all_keyed\"}\n          rows={@items}\n          id={fn row -> row.id end}\n        >\n          <:col :let={%{entry: entry}} id=\"1\" name=\"Foo\">\n            <.my_component my_count={@count} the_name={entry.foo.bar} /> {i}\n          </:col>\n          <:col id=\"2\" name=\"Count\">{@count}</:col>\n        </.table_with_all_keyed>\n\n        <.table_with_rows_keyed\n          :if={@active_tab == \"rows_keyed\"}\n          rows={@items}\n          id={fn row -> row.id end}\n        >\n          <:col :let={%{entry: entry}} id=\"1\" name=\"Foo\">\n            <.my_component my_count={@count} the_name={entry.foo.bar} /> {i}\n          </:col>\n          <:col id=\"2\" name=\"Count\">{@count}</:col>\n        </.table_with_rows_keyed>\n\n        <.table_with_no_keyed :if={@active_tab == \"no_keyed\"} rows={@items} id={fn row -> row.id end}>\n          <:col :let={%{entry: entry}} id=\"1\" name=\"Foo\">\n            <.my_component my_count={@count} the_name={entry.foo.bar} /> {i}\n          </:col>\n          <:col id=\"2\" name=\"Count\">{@count}</:col>\n        </.table_with_no_keyed>\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  defp my_component(assigns) do\n    ~H\"\"\"\n    <span>\n      Count: {@my_count} Name: {@the_name}\n    </span>\n    \"\"\"\n  end\n\n  attr :rows, :list, required: true\n  slot :col\n\n  defp table_with_all_keyed(assigns) do\n    ~H\"\"\"\n    <div class=\"mt-8 flow-root\">\n      <div class=\"-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8\">\n        <div class=\"inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8\">\n          <table class=\"min-w-full divide-y divide-gray-300\">\n            <thead>\n              <tr>\n                <th\n                  :for={slot <- @col}\n                  :key={slot.id}\n                  scope=\"col\"\n                  class=\"py-3.5 first:pr-3 first:pl-4 px-3 text-left text-sm font-semibold text-gray-900 first:sm:pl-0\"\n                >\n                  {slot.name}\n                </th>\n              </tr>\n            </thead>\n            <tbody class=\"divide-y divide-gray-200\">\n              <tr :for={row <- @rows} :key={@id.(row)}>\n                <td\n                  :for={slot <- @col}\n                  :key={\"#{@id.(row)}_#{slot.id}\"}\n                  class=\"py-4 first:pr-3 first:pl-4 px-3 text-sm first:font-medium whitespace-nowrap first:text-gray-900 text-gray-500 first:sm:pl-0\"\n                >\n                  {render_slot(slot, row)}\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </div>\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  attr :rows, :list, required: true\n  slot :col\n\n  defp table_with_rows_keyed(assigns) do\n    ~H\"\"\"\n    <div class=\"mt-8 flow-root\">\n      <div class=\"-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8\">\n        <div class=\"inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8\">\n          <table class=\"min-w-full divide-y divide-gray-300\">\n            <thead>\n              <tr>\n                <th\n                  :for={slot <- @col}\n                  scope=\"col\"\n                  class=\"py-3.5 first:pr-3 first:pl-4 px-3 text-left text-sm font-semibold text-gray-900 first:sm:pl-0\"\n                >\n                  {slot.name}\n                </th>\n              </tr>\n            </thead>\n            <tbody class=\"divide-y divide-gray-200\">\n              <tr :for={row <- @rows} :key={@id.(row)}>\n                <td\n                  :for={slot <- @col}\n                  class=\"py-4 first:pr-3 first:pl-4 px-3 text-sm first:font-medium whitespace-nowrap first:text-gray-900 text-gray-500 first:sm:pl-0\"\n                >\n                  {render_slot(slot, row)}\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </div>\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  attr :rows, :list, required: true\n  slot :col\n\n  defp table_with_no_keyed(assigns) do\n    ~H\"\"\"\n    <div class=\"mt-8 flow-root\">\n      <div class=\"-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8\">\n        <div class=\"inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8\">\n          <table class=\"min-w-full divide-y divide-gray-300\">\n            <thead>\n              <tr>\n                <th\n                  :for={slot <- @col}\n                  scope=\"col\"\n                  class=\"py-3.5 first:pr-3 first:pl-4 px-3 text-left text-sm font-semibold text-gray-900 first:sm:pl-0\"\n                >\n                  {slot.name}\n                </th>\n              </tr>\n            </thead>\n            <tbody class=\"divide-y divide-gray-200\">\n              <tr :for={row <- @rows}>\n                <td\n                  :for={slot <- @col}\n                  class=\"py-4 first:pr-3 first:pl-4 px-3 text-sm first:font-medium whitespace-nowrap first:text-gray-900 text-gray-500 first:sm:pl-0\"\n                >\n                  {render_slot(slot, row)}\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </div>\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    :timer.send_interval(1000, :report_memory)\n    {:ok, assign(socket, count: 0, items: random_items(@count), size: @count, tailwind: true)}\n  end\n\n  def handle_params(params, _session, socket) do\n    {:noreply, assign_tab(socket, params)}\n  end\n\n  defp assign_tab(socket, %{\"tab\" => tab}) when tab in [\"all_keyed\", \"rows_keyed\", \"no_keyed\"] do\n    assign(socket, :active_tab, tab)\n  end\n\n  defp assign_tab(socket, _), do: assign(socket, :active_tab, \"all_keyed\")\n\n  def handle_event(\"randomize\", _params, socket) do\n    {:noreply,\n     socket |> assign(:items, random_items(socket.assigns.size)) |> update(:count, &(&1 + 1))}\n  end\n\n  def handle_event(\"change_size\", %{\"size\" => size}, socket) do\n    size =\n      case size do\n        \"\" -> 0\n        _ -> String.to_integer(size)\n      end\n\n    {:noreply,\n     socket\n     |> assign(:items, random_items(size))\n     |> assign(:size, size)\n     |> update(:count, &(&1 + 1))}\n  end\n\n  def handle_event(\"change_0\", _params, socket) do\n    {:noreply,\n     socket\n     |> assign(:items, [\n       %{id: 2000, entry: %{other: \"hey\", foo: %{bar: \"#{System.unique_integer()}\"}}}\n       | Enum.slice(socket.assigns.items, 1..(socket.assigns.size + 1))\n     ])}\n  end\n\n  def handle_event(\"change_other\", _params, socket) do\n    {:noreply,\n     socket\n     |> assign(\n       :items,\n       Enum.map(socket.assigns.items, fn item ->\n         %{item | entry: %{item.entry | other: \"hey #{System.unique_integer()}\"}}\n       end)\n     )}\n  end\n\n  def handle_info(:report_memory, socket) do\n    :erlang.garbage_collect()\n    IO.puts(\"Heap size: #{Process.info(self())[:total_heap_size]}\")\n\n    {:noreply, socket}\n  end\n\n  def random_items(size) do\n    1..(size * 2)\n    |> Enum.take_random(size)\n    |> Enum.map(&%{id: &1, entry: %{other: \"hey\", foo: %{bar: \"New#{&1 + 1}\"}}})\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/navigation.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.Navigation.Layout do\n  use Phoenix.LiveView\n\n  def render(\"live.html\", assigns) do\n    ~H\"\"\"\n    <meta name=\"csrf-token\" content={Plug.CSRFProtection.get_csrf_token()} />\n    <script src=\"/assets/phoenix/phoenix.min.js\">\n    </script>\n    <script type=\"module\">\n      import { LiveSocket } from \"/assets/phoenix_live_view/phoenix_live_view.esm.js\";\n      let csrfToken = document\n        .querySelector(\"meta[name='csrf-token']\")\n        .getAttribute(\"content\");\n      let liveSocket = new LiveSocket(\"/live\", window.Phoenix.Socket, {\n        params: { _csrf_token: csrfToken },\n        reloadJitterMin: 50,\n        reloadJitterMax: 500,\n      });\n      liveSocket.connect();\n      window.liveSocket = liveSocket;\n\n      window.addEventListener(\"phx:navigate\", (e) => {\n        console.log(\"navigate event\", JSON.stringify(e.detail));\n      });\n    </script>\n\n    <style>\n      html, body {\n        margin: 0;\n        padding: 0;\n\n        font-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Ubuntu, \"Helvetica Neue\", sans-serif;\n        font-size: 1rem;\n      }\n    </style>\n\n    <div style=\"display: flex; width: 100%; height: 100vh;\">\n      <div style=\"position: fixed; height: 100vh; background-color: #f8fafc; border-right: 1px solid; width: 20rem; display: flex; flex-direction: column; padding: 1rem; gap: 0.5rem;\">\n        <h1 style=\"margin-bottom: 1rem; font-size: 1.125rem; line-height: 1.75rem;\">Navigation</h1>\n\n        <.link navigate=\"/navigation/a\" style=\"background-color: #f1f5f9; padding: 0.5rem;\">\n          LiveView A\n        </.link>\n\n        <.link navigate=\"/navigation/b\" style=\"background-color: #f1f5f9; padding: 0.5rem;\">\n          LiveView B\n        </.link>\n\n        <.link navigate=\"/stream\" style=\"background-color: #f1f5f9; padding: 0.5rem;\">\n          LiveView (other session)\n        </.link>\n\n        <.link navigate=\"/navigation/dead\" style=\"background-color: #f1f5f9; padding: 0.5rem;\">\n          Dead View\n        </.link>\n      </div>\n\n      <div style=\"margin-left: 22rem; flex: 1; padding: 2rem;\">\n        {@inner_content}\n      </div>\n    </div>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Navigation.ALive do\n  use Phoenix.LiveView\n\n  @impl Phoenix.LiveView\n  def mount(_params, _session, socket) do\n    socket\n    |> assign(:param_current, nil)\n    |> then(&{:ok, &1})\n  end\n\n  @impl Phoenix.LiveView\n  def handle_params(params, _uri, socket) do\n    param = Map.get(params, \"param\")\n\n    socket\n    |> assign(:param_current, param)\n    |> assign(:param_next, System.unique_integer())\n    |> then(&{:noreply, &1})\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <h1>This is page A</h1>\n\n    <p>Current param: {@param_current}</p>\n\n    <.styled_link patch={\"/navigation/a?param=#{@param_next}\"}>Patch this LiveView</.styled_link>\n    <.styled_link patch={\"/navigation/a?param=#{@param_next}\"} replace>Patch (Replace)</.styled_link>\n    <.styled_link navigate=\"/navigation/b#items-item-42\">Navigate to 42</.styled_link>\n    \"\"\"\n  end\n\n  defp styled_link(assigns) do\n    ~H\"\"\"\n    <.link\n      style=\"padding-left: 1rem; padding-right: 1rem; padding-top: 0.5rem; padding-bottom: 0.5rem; background-color: #e2e8f0; display: inline-flex; align-items: center; border-radius: 0.375rem; cursor: pointer;\"\n      {Map.delete(assigns, [:inner_block])}\n    >\n      {render_slot(@inner_block)}\n    </.link>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Navigation.BLive do\n  use Phoenix.LiveView\n\n  @impl Phoenix.LiveView\n  def mount(_params, _session, socket) do\n    socket\n    |> then(&{:ok, &1})\n  end\n\n  @impl Phoenix.LiveView\n  def handle_params(params, _uri, socket) do\n    socket\n    |> assign(:container, not is_nil(params[\"container\"]))\n    |> apply_action(socket.assigns.live_action, params)\n    |> then(&{:noreply, &1})\n  end\n\n  def apply_action(socket, :index, _params) do\n    items =\n      for i <- 1..100 do\n        %{id: \"item-#{i}\", name: i}\n      end\n\n    stream(socket, :items, items)\n  end\n\n  def apply_action(socket, :show, %{\"id\" => id}) do\n    assign(socket, :id, id)\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <h1>This is page B</h1>\n\n    <a\n      href=\"#items-item-42\"\n      style=\"margin-bottom: 8px; padding-left: 1rem; padding-right: 1rem; padding-top: 0.5rem; padding-bottom: 0.5rem; background-color: #e2e8f0; display: inline-flex; align-items: center; border-radius: 0.375rem; cursor: pointer;\"\n    >\n      Go to 42.\n    </a>\n\n    <div\n      :if={@live_action == :index}\n      id=\"my-scroll-container\"\n      style={\"#{if @container, do: \"height: 85vh; overflow-y: scroll; \"}width: 100%; border: 1px solid #e2e8f0; border-radius: 0.375rem; position: relative;\"}\n    >\n      <ul id=\"items\" style=\"padding: 1rem; list-style: none;\" phx-update=\"stream\">\n        <%= for {id, item} <- @streams.items do %>\n          <li id={id} style=\"padding: 0.5rem; border-bottom: 1px solid #e2e8f0;\">\n            <.link\n              patch={\"/navigation/b/#{item.id}\"}\n              style=\"display: inline-flex; align-items: center; gap: 0.5rem;\"\n            >\n              Item {item.name}\n            </.link>\n          </li>\n        <% end %>\n      </ul>\n    </div>\n\n    <div :if={@live_action == :show}>\n      <p>Item {@id}</p>\n    </div>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Navigation.Dead do\n  use Phoenix.Controller,\n    formats: [:html],\n    layouts: [html: {Phoenix.LiveViewTest.E2E.Navigation.Layout, :live}]\n\n  import Phoenix.Component, only: [sigil_H: 2]\n\n  def index(conn, _params) do\n    render(conn, :index)\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Navigation.RedirectLoopLive do\n  use Phoenix.LiveView\n\n  @impl Phoenix.LiveView\n  def mount(params, _session, socket) do\n    if params[\"loop\"] do\n      {:ok, assign(socket, message: \"Too many redirects\", loop: false)}\n    else\n      {:ok, assign(socket, message: nil, loop: true)}\n    end\n  end\n\n  @impl Phoenix.LiveView\n  def handle_params(params, _uri, socket) do\n    if params[\"loop\"] && socket.assigns.loop do\n      {:noreply, push_patch(socket, to: \"/navigation/redirectloop?loop=true\")}\n    else\n      {:noreply, socket}\n    end\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <div :if={@message} id=\"message\">{@message}</div>\n    <.link patch=\"/navigation/redirectloop?loop=true\">Redirect Loop</.link>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Navigation.DeadHTML do\n  use Phoenix.Component\n\n  def index(assigns) do\n    ~H\"\"\"\n    <h1>Dead view</h1>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/portal.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.PortalLive do\n  use Phoenix.LiveView\n\n  alias Phoenix.LiveView.JS\n\n  def render(\"root.html\", assigns) do\n    ~H\"\"\"\n    <!DOCTYPE html>\n    <head>\n      <meta name=\"csrf-token\" content={Plug.CSRFProtection.get_csrf_token()} />\n      <script src=\"https://cdn.tailwindcss.com/3.4.3\">\n      </script>\n      <script src=\"/assets/phoenix/phoenix.min.js\">\n      </script>\n      <style>\n        [data-phx-session], [data-phx-teleported-src] { display: contents }\n      </style>\n      <script type=\"module\">\n        import { LiveSocket } from \"/assets/phoenix_live_view/phoenix_live_view.esm.js\";\n        import {\n          computePosition,\n          autoUpdate,\n          offset,\n        } from \"https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.7.0/+esm\";\n        import { hooks as colocatedHooks } from \"/assets/colocated/index.js\";\n        let csrfToken = document\n          .querySelector(\"meta[name='csrf-token']\")\n          .getAttribute(\"content\");\n        let liveSocket = new LiveSocket(\"/live\", window.Phoenix.Socket, {\n          params: { _csrf_token: csrfToken },\n          hooks: {\n            ...colocatedHooks,\n            PortalTooltip: {\n              mounted() {\n                this.tooltipEl = document.getElementById(this.el.dataset.id);\n                this.activatorEl = this.el.querySelector(\n                  `#${this.el.dataset.id}-activator`,\n                );\n                this.activatorEl.addEventListener(\"focusin\", () => this.queueShow());\n                this.activatorEl.addEventListener(\"mouseover\", () => this.queueShow());\n                this.activatorEl.addEventListener(\"focusout\", () => this.queueHide());\n                this.activatorEl.addEventListener(\"mouseout\", () => this.queueHide());\n                this.el.addEventListener(\"phx:hide-tooltip\", () => this.hide());\n              },\n              destroyed() {\n                this.cleanup && this.cleanup();\n              },\n              queueShow() {\n                clearTimeout(this.hideTimeout);\n                this.showTimeout = setTimeout(() => this.show(), 200);\n              },\n              queueHide() {\n                clearTimeout(this.showTimeout);\n                this.hideTimeout = setTimeout(() => this.hide(), 50);\n              },\n              show() {\n                this.cleanup && this.cleanup();\n                this.cleanup = autoUpdate(this.activatorEl, this.tooltipEl, () => {\n                  computePosition(this.activatorEl, this.tooltipEl, {\n                    placement: this.el.dataset.position,\n                    middleware: [offset(10)],\n                  }).then(({ x, y }) => {\n                    this.tooltipEl.style.left = `${x}px`;\n                    this.tooltipEl.style.top = `${y}px`;\n                  });\n                });\n                this.liveSocket.execJS(this.el, this.el.dataset.show);\n              },\n              hide() {\n                this.liveSocket.execJS(this.el, this.el.dataset.hide);\n                this.cleanup && this.cleanup();\n              },\n            },\n          },\n        });\n        liveSocket.connect();\n        window.liveSocket = liveSocket;\n      </script>\n    </head>\n\n    <body>\n      <main style=\"flex: 1; padding: 2rem;\">\n        {@inner_content}\n      </main>\n      <div id=\"root-portal\"></div>\n    </body>\n    \"\"\"\n  end\n\n  def render(\"live.html\", assigns) do\n    ~H\"\"\"\n    {@inner_content}\n\n    <div id=\"app-portal\"></div>\n    \"\"\"\n  end\n\n  @impl Phoenix.LiveView\n  def mount(params, _session, socket) do\n    case params do\n      %{\"tick\" => \"false\"} -> :ok\n      _ -> :timer.send_interval(1000, self(), :tick)\n    end\n\n    socket\n    |> assign(:param_current, nil)\n    |> assign(:count, 0)\n    |> assign(:render_modal, true)\n    |> assign(:render_nested_portals, true)\n    |> assign(:nested_portal_count, 0)\n    |> then(&{:ok, &1, layout: {__MODULE__, :live}})\n  end\n\n  @impl Phoenix.LiveView\n  def handle_params(params, _uri, socket) do\n    param = Map.get(params, \"param\")\n\n    socket\n    |> assign(:param_current, param)\n    |> assign(:param_next, System.unique_integer())\n    |> then(&{:noreply, &1})\n  end\n\n  @impl Phoenix.LiveView\n  def handle_info(:tick, socket) do\n    {:noreply, assign(socket, :count, socket.assigns.count + 1)}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"tick\", _params, socket) do\n    {:noreply, assign(socket, :count, socket.assigns.count + 1)}\n  end\n\n  def handle_event(\"toggle_modal\", _params, socket) do\n    {:noreply, assign(socket, :render_modal, !socket.assigns.render_modal)}\n  end\n\n  def handle_event(\"toggle_nested_portals\", _params, socket) do\n    {:noreply, assign(socket, :render_nested_portals, !socket.assigns.render_nested_portals)}\n  end\n\n  def handle_event(\"nested_portal_click\", _params, socket) do\n    {:noreply, assign(socket, :nested_portal_count, socket.assigns.nested_portal_count + 1)}\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <h1>Modal example</h1>\n\n    <p>Current param: {@param_current}</p>\n\n    <.button phx-click={JS.patch(\"/portal?param=#{@param_next}\")}>Patch this LiveView</.button>\n\n    <.button phx-click={show_modal(\"my-modal\")}>Open modal</.button>\n    <.button phx-click=\"toggle_modal\">Toggle modal render</.button>\n    <.button phx-click={show_modal(\"my-modal-2\") |> JS.show(to: \"#inner-red-box\")}>Open second modal</.button>\n    <.button phx-click={JS.push(\"tick\")}>Tick</.button>\n\n    <.button phx-click={JS.navigate(\"/form\")}>Live navigate</.button>\n\n    <.portal :if={@render_modal} id=\"portal-source\" target=\"#root-portal\">\n      <.modal id=\"my-modal\">\n        This is a modal.\n        <p>DOM patching works as expected: {@count}</p>\n        <.button phx-click={JS.patch(\"/portal?param=#{@param_next}\")}>Patch this LiveView</.button>\n      </.modal>\n\n      <div id=\"hook-test\" phx-hook=\".InsidePortal\">This should get a data attribute</div>\n      <script :type={Phoenix.LiveView.ColocatedHook} name=\".InsidePortal\">\n        export default {\n          mounted() {\n            this.js().setAttribute(this.el, \"data-portalhook-mounted\", \"true\");\n          },\n        };\n      </script>\n    </.portal>\n\n    <.portal id=\"portal-source-2\" target=\"#app-portal\">\n      <.modal id=\"my-modal-2\">\n        This is a second modal.\n        <.portal id=\"modal-2-inner-portal\" target=\"#my-modal-2-content\" class=\"contents\">\n          <div class=\"size-96 bg-gray-300 absolute top-0 right-0\">\n            <.portal id=\"modal-2-inner-portal-2\" target=\"#my-modal-2-content\" class=\"contents\">\n              <div\n                id=\"inner-red-box\"\n                class=\"absolute top-0 right-0 bg-red-500 size-32\"\n                phx-click-away={JS.hide()}\n              >\n                test\n              </div>\n            </.portal>\n          </div>\n        </.portal>\n      </.modal>\n    </.portal>\n\n    <.portal id=\"portal-with-live-component\" target=\"#root-portal\">\n      <.live_component module={Phoenix.LiveViewTest.E2E.PortalLive.LC} id=\"lc\" />\n    </.portal>\n\n    {live_render(@socket, Phoenix.LiveViewTest.E2E.PortalLive.NestedLive, id: \"nested\")}\n\n    <div class=\"border border-sky-600 overflow-hidden mt-8 p-4 flex gap-4\">\n      <Phoenix.LiveViewTest.E2E.PortalTooltip.tooltip id=\"tooltip-example-portal\">\n        <:activator>\n          <.button>Hover me</.button>\n        </:activator>\n        Hey there! {@count}\n      </Phoenix.LiveViewTest.E2E.PortalTooltip.tooltip>\n\n      <Phoenix.LiveViewTest.E2E.PortalTooltip.tooltip id=\"tooltip-example-no-portal\" portal={false}>\n        <:activator>\n          <.button>Hover me (no portal)</.button>\n        </:activator>\n        Hey there! {@count}\n      </Phoenix.LiveViewTest.E2E.PortalTooltip.tooltip>\n    </div>\n\n    <div class=\"border border-purple-600 mt-8 p-4\">\n      <h2>Nested Portal Test</h2>\n      <.button phx-click=\"toggle_nested_portals\">Toggle nested portals</.button>\n      <p>Nested portal count: <span id=\"nested-portal-count\">{@nested_portal_count}</span></p>\n      <.portal :if={@render_nested_portals} id=\"nested-portal-source\" target=\"#root-portal\">\n        <div id=\"outer-portal\" class=\"border border-blue-400 p-4 m-2\">\n          <h3>Outer Portal</h3>\n          <.portal id=\"inner-portal-source\" target=\"body\">\n            <div id=\"inner-portal\" class=\"border border-green-400 p-2 m-2\">\n              <h4>Inner Portal (nested inside outer)</h4>\n              <p id=\"nested-portal-content\">Tick count: {@count}</p>\n              <.button phx-click=\"nested_portal_click\">Click nested portal button</.button>\n            </div>\n          </.portal>\n        </div>\n      </.portal>\n    </div>\n\n    <.modal id=\"non-teleported-modal\">\n      This is a non-teleported modal. Open the menu and click an item. The modal must not close.\n      <.button phx-click={JS.show(to: \"#teleported-menu-content\")}>Open menu</.button>\n      <.portal id=\"teleported-menu\" target=\"body\">\n        <div\n          id=\"teleported-menu-content\"\n          class=\"hidden z-[100] fixed top-0 left-0 border border-red-500 p-4 bg-white\"\n        >\n          <.button phx-click={JS.hide(to: \"#teleported-menu-content\")}>Close menu</.button>\n        </div>\n      </.portal>\n    </.modal>\n\n    <.button phx-click={show_modal(\"non-teleported-modal\")}>Open non-teleported modal</.button>\n    \"\"\"\n  end\n\n  attr :type, :string, default: nil\n  attr :class, :any, default: nil\n  attr :rest, :global, include: ~w(disabled form name value)\n\n  slot :inner_block, required: true\n\n  def button(assigns) do\n    ~H\"\"\"\n    <button\n      type={@type}\n      class={[\n        \"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3\",\n        \"text-sm font-semibold leading-6 text-white active:text-white/80\",\n        @class\n      ]}\n      {@rest}\n    >\n      {render_slot(@inner_block)}\n    </button>\n    \"\"\"\n  end\n\n  attr :id, :string, required: true\n  attr :show, :boolean, default: false\n  attr :on_cancel, JS, default: %JS{}\n  slot :inner_block, required: true\n\n  def modal(assigns) do\n    ~H\"\"\"\n    <div\n      id={@id}\n      phx-mounted={@show && show_modal(@id)}\n      phx-remove={hide_modal(@id)}\n      data-cancel={JS.exec(@on_cancel, \"phx-remove\")}\n      class=\"relative z-50 hidden\"\n    >\n      <div id={\"#{@id}-bg\"} class=\"bg-zinc-50/90 fixed inset-0 transition-opacity\" aria-hidden=\"true\" />\n      <div\n        class=\"fixed inset-0 overflow-y-auto\"\n        aria-labelledby={\"#{@id}-title\"}\n        aria-describedby={\"#{@id}-description\"}\n        role=\"dialog\"\n        aria-modal=\"true\"\n        tabindex=\"0\"\n      >\n        <div class=\"flex min-h-full items-center justify-center\">\n          <div class=\"w-full max-w-3xl p-4 sm:p-6 lg:py-8\">\n            <.focus_wrap\n              id={\"#{@id}-container\"}\n              phx-window-keydown={JS.exec(\"data-cancel\", to: \"##{@id}\")}\n              phx-key=\"escape\"\n              phx-click-away={JS.exec(\"data-cancel\", to: \"##{@id}\")}\n              class=\"shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition\"\n            >\n              <div class=\"absolute top-6 right-5\">\n                <button\n                  phx-click={JS.exec(\"data-cancel\", to: \"##{@id}\")}\n                  type=\"button\"\n                  class=\"-m-3 flex-none p-3 opacity-20 hover:opacity-40\"\n                  aria-label=\"close\"\n                >\n                  x\n                </button>\n              </div>\n              <div id={\"#{@id}-content\"}>\n                {render_slot(@inner_block)}\n              </div>\n            </.focus_wrap>\n          </div>\n        </div>\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  def show_modal(js \\\\ %JS{}, id) when is_binary(id) do\n    js\n    |> JS.show(to: \"##{id}\")\n    |> JS.show(\n      to: \"##{id}-bg\",\n      time: 300,\n      transition: {\"transition-all ease-out duration-300\", \"opacity-0\", \"opacity-100\"}\n    )\n    |> show(\"##{id}-container\")\n    |> JS.add_class(\"overflow-hidden\", to: \"body\")\n    |> JS.focus_first(to: \"##{id}-content\")\n  end\n\n  def hide_modal(js \\\\ %JS{}, id) do\n    js\n    |> JS.hide(\n      to: \"##{id}-bg\",\n      transition: {\"transition-all ease-in duration-200\", \"opacity-100\", \"opacity-0\"}\n    )\n    |> hide(\"##{id}-container\")\n    |> JS.hide(to: \"##{id}\", transition: {\"block\", \"block\", \"hidden\"})\n    |> JS.remove_class(\"overflow-hidden\", to: \"body\")\n    |> JS.pop_focus()\n  end\n\n  def show(js \\\\ %JS{}, selector) do\n    JS.show(js,\n      to: selector,\n      time: 300,\n      transition:\n        {\"transition-all ease-out duration-300\",\n         \"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95\",\n         \"opacity-100 translate-y-0 sm:scale-100\"}\n    )\n  end\n\n  def hide(js \\\\ %JS{}, selector) do\n    JS.hide(js,\n      to: selector,\n      time: 200,\n      transition:\n        {\"transition-all ease-in duration-200\", \"opacity-100 translate-y-0 sm:scale-100\",\n         \"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95\"}\n    )\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.PortalLive.NestedLive do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, :count, 0)}\n  end\n\n  def handle_event(\"event\", _params, socket) do\n    IO.puts(\"Nested LV got event!\")\n    {:noreply, assign(socket, :count, socket.assigns.count + 1)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div class=\"border border-orange-200\">\n      <h1>Nested LiveView</h1>\n\n      <p id=\"nested-event-count\">{@count}</p>\n\n      <button phx-click=\"event\">Trigger event in nested LV</button>\n\n      <.portal id=\"nested-lv-button\" target=\"body\">\n        <button phx-click=\"event\">Trigger event in nested LV (from teleported button)</button>\n      </.portal>\n\n      <.portal id=\"nested-lv\" target=\"body\">\n        {live_render(@socket, Phoenix.LiveViewTest.E2E.PortalLive.NestedTeleportedLive,\n          id: \"nested-teleported\"\n        )}\n      </.portal>\n    </div>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.PortalLive.NestedTeleportedLive do\n  use Phoenix.LiveView\n\n  def handle_event(\"event\", _params, socket) do\n    IO.puts(\"Nested teleported LV got event!\")\n    {:noreply, socket}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div class=\"border border-green-200\">\n      <h1>Nested teleport LiveView</h1>\n      <button phx-click=\"event\">Toggle event in teleported LV</button>\n    </div>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.PortalLive.LC do\n  use Phoenix.LiveComponent\n\n  def update(_assigns, socket) do\n    {:ok, stream(socket, :items, [%{id: 1, name: \"Item 1\"}, %{id: 2, name: \"Item 2\"}])}\n  end\n\n  def handle_event(\"prepend\", _params, socket) do\n    rand = 1000 + floor(:rand.uniform() * 1000)\n    {:noreply, stream_insert(socket, :items, %{id: rand, name: \"Item #{rand}\"}, at: 0)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div id=\"teleported-lc\" class=\"border border-red-200\">\n      <h1>LiveComponent</h1>\n\n      <ul id=\"stream-in-lc\" phx-update=\"stream\">\n        <li :for={{id, item} <- @streams.items} id={id}>\n          {item.name}\n        </li>\n      </ul>\n\n      <button phx-click=\"prepend\" phx-target={@myself}>Prepend item</button>\n\n      <.portal id=\"teleported-from-lc-button\" target=\"body\">\n        <button id=\"lcbtn\" phx-hook=\".TeleportedLCButton\">Prepend item (teleported)</button>\n      </.portal>\n\n      <script :type={Phoenix.LiveView.ColocatedHook} name=\".TeleportedLCButton\">\n        export default {\n          mounted() {\n            this.el.addEventListener(\"click\", () => {\n              this.pushEventTo(this.el, \"prepend\");\n            });\n          },\n        };\n      </script>\n    </div>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.PortalTooltip do\n  use Phoenix.Component\n\n  alias Phoenix.LiveView.JS\n\n  attr :id, :string, required: true\n  attr :portal, :boolean, default: true\n  slot :activator, required: true\n  slot :inner_block, required: true\n\n  def tooltip(assigns) do\n    ~H\"\"\"\n    <div\n      id={\"#{@id}-wrapper\"}\n      class=\"relative inline-block w-fit\"\n      phx-hook=\"PortalTooltip\"\n      data-id={@id}\n      data-show={show_tooltip(@id)}\n      data-hide={hide_tooltip(@id)}\n      data-position=\"top\"\n      phx-window-keydown={JS.dispatch(\"phx:hide-tooltip\")}\n      phx-key=\"escape\"\n    >\n      <div id={\"#{@id}-activator\"} aria-describedby={@id} data-activator>\n        {render_slot(@activator)}\n      </div>\n      <.portal :if={@portal} id={\"#{@id}-portal\"} target=\"body\">\n        <div\n          id={@id}\n          phx-mounted={JS.ignore_attributes([\"style\"])}\n          role=\"tooltip\"\n          class=\"hidden absolute top-0 left-0 z-50 bg-sky-800 text-white text-xs p-1\"\n        >\n          {render_slot(@inner_block)}\n        </div>\n      </.portal>\n      <div\n        :if={!@portal}\n        id={@id}\n        phx-mounted={JS.ignore_attributes([\"style\"])}\n        role=\"tooltip\"\n        class=\"hidden absolute top-0 left-0 z-50 bg-sky-800 text-white text-xs p-1\"\n      >\n        {render_slot(@inner_block)}\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  defp show_tooltip(id) do\n    JS.show(\n      to: \"##{id}\",\n      transition:\n        {\"transform ease-out duration-200 transition origin-bottom\",\n         \"scale-95 translate-y-0.5 opacity-0\", \"scale-100 translate-y-0 opacity-100\"},\n      display: \"block\",\n      time: 200,\n      blocking: false\n    )\n  end\n\n  def hide_tooltip(id) do\n    JS.hide(\n      to: \"##{id}\",\n      transition: {\"transition ease-in duration-100\", \"opacity-100\", \"opacity-0\"},\n      time: 100,\n      blocking: false\n    )\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/select_live.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.SelectLive do\n  use Phoenix.LiveView\n\n  @impl Phoenix.LiveView\n  def mount(_params, _session, socket) do\n    {:ok,\n     assign(socket,\n       tick_timer: nil,\n       select2_timer: nil,\n       select2_countdown: 5,\n       select4_timer: nil,\n       select4_countdown: 5,\n       tick: 0,\n       form: to_form(%{\"select3\" => \"2\"}, as: :select_form),\n       select1_opts: [\"these options\", \"are fixed\"],\n       select2_opts: Enum.to_list(1..10),\n       select3_opts: 1..10,\n       select4_opts: 1..10,\n       select4_value: \"1\"\n     )}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_info(:tick, socket) do\n    {:noreply, update(socket, :tick, &(&1 + 1))}\n  end\n\n  def handle_info(:update_select2_opts, socket) do\n    {:noreply,\n     update(socket, :select2_opts, fn existing_opts ->\n       existing_opts ++ [Enum.max(existing_opts) + 1]\n     end)}\n  end\n\n  def handle_info(:select2_countdown, socket) do\n    if socket.assigns.select2_countdown == 0 do\n      send(self(), :update_select2_opts)\n      {:noreply, assign(socket, select2_timer: nil)}\n    else\n      Process.send_after(self(), :select2_countdown, 1000)\n      {:noreply, update(socket, :select2_countdown, &(&1 - 1))}\n    end\n  end\n\n  def handle_info(:select4_countdown, socket) do\n    if socket.assigns.select4_countdown == 0 do\n      send(self(), :change_select4_value)\n      {:noreply, assign(socket, select4_timer: nil)}\n    else\n      Process.send_after(self(), :select4_countdown, 1000)\n      {:noreply, update(socket, :select4_countdown, &(&1 - 1))}\n    end\n  end\n\n  def handle_info(:change_select4_value, socket) do\n    {:noreply, assign(socket, :select4_value, Enum.random(1..10) |> to_string())}\n  end\n\n  @types %{\n    select1: :string,\n    select2: :integer,\n    select3: :integer,\n    select4: :integer\n  }\n\n  def changeset(params) do\n    Ecto.Changeset.cast({%{}, @types}, params, [:select1, :select2, :select3, :select4])\n    |> Ecto.Changeset.validate_number(:select3, greater_than: 5)\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"validate\", %{\"select_form\" => params}, socket) do\n    changeset = changeset(params)\n\n    {:noreply, assign(socket, form: to_form(changeset, as: :select_form, action: :validate))}\n  end\n\n  def handle_event(\"toggle-tick\", _, socket) do\n    case socket.assigns.tick_timer do\n      nil ->\n        {:ok, timer_ref} = :timer.send_interval(1000, :tick)\n        {:noreply, assign(socket, :tick_timer, timer_ref)}\n\n      ref ->\n        Process.cancel_timer(ref)\n        {:noreply, assign(socket, :tick_timer, nil)}\n    end\n  end\n\n  def handle_event(\"schedule-select2-update\", _, socket) do\n    timer_ref = Process.send_after(self(), :select2_countdown, 1000)\n    {:noreply, assign(socket, select2_countdown: 5, select2_timer: timer_ref)}\n  end\n\n  def handle_event(\"schedule-select4-update\", _, socket) do\n    timer_ref = Process.send_after(self(), :select4_countdown, 1000)\n    {:noreply, assign(socket, select4_countdown: 5, select4_timer: timer_ref)}\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <style>\n      * { font-size: unset; }\n\n      body {\n        padding: 20px;\n        max-width: 500px;\n        font-family: sans-serif;\n      }\n\n      .has-error {\n        border: 5px solid red;\n      }\n\n      select {\n        border: 1px solid black;\n      }\n    </style>\n\n    <h1>Select Playgroud</h1>\n    <p>\n      This page contains multiple select inputs to test various behaviors.\n      Sadly, we cannot test all of them automatically, as there is no way to assert the state of an open select's native UI.\n    </p>\n    Tick: {@tick}\n\n    <div style=\"display: flex; flex-direction: column; gap: 8px\">\n      <button phx-click=\"toggle-tick\">\n        {if @tick_timer, do: \"Disable\", else: \"Enable\"} ticking\n      </button>\n      <button :if={!@select2_timer} phx-click=\"schedule-select2-update\">\n        Schedule select2 update\n      </button>\n      <span :if={@select2_timer}>Select 2 will update in {@select2_countdown}s</span>\n      <button :if={!@select4_timer} phx-click=\"schedule-select4-update\">\n        Schedule select4 update\n      </button>\n      <span :if={@select4_timer}>Select 4 will update in {@select4_countdown}s</span>\n    </div>\n\n    <.form for={@form} phx-change=\"validate\">\n      <h2>Select 1</h2>\n      <p>\n        The select should not close when the page is patched while it is open.\n        You can simulate patching by enabling ticking above.\n      </p>\n      <.input type=\"select\" field={@form[:select1]} label=\"Select 1\" options={@select1_opts} />\n      <hr />\n      <h2>Select 2</h2>\n      <p>\n        The second select's options will be updated after a 5s timeout (button on top).\n        This can be used to test the behavior of the select when its options change while it is open.\n      </p>\n      <.input type=\"select\" field={@form[:select2]} label=\"Select 2\" options={@select2_opts} />\n      <hr />\n      <h2>Select 3</h2>\n      <p>\n        Error classes are correctly applied to the third select.\n        It should have a red border for all values from 1 to 5. The border should disappear when selecting 6 or higher.\n      </p>\n      <.input type=\"select\" field={@form[:select3]} label=\"Select 3\" options={@select3_opts} />\n      <hr />\n      <h2>Select 4</h2>\n      <p>\n        The selected value of this field changes after a 5s timeout (button on top).\n        This can be used to test the behavior of the select when its value changes while it is open.\n        We expect the value to be ignored if the select is open, as value changes to focused inputs are ignored.\n      </p>\n      <.input\n        type=\"select\"\n        field={@form[:select4]}\n        value={@select4_value}\n        label=\"Select 4\"\n        options={@select4_opts}\n      />\n      <hr />\n    </.form>\n    \"\"\"\n  end\n\n  ###\n  # Input components copied and adjusted from generated core_components\n\n  attr :id, :any, default: nil\n  attr :name, :any\n  attr :label, :string, default: nil\n  attr :value, :any\n\n  attr :type, :string,\n    default: \"text\",\n    values: ~w(checkbox color date datetime-local email file month number password\n               range search select tel text textarea time url week)\n\n  attr :field, Phoenix.HTML.FormField\n\n  attr :errors, :list, default: []\n  attr :checked, :boolean\n  attr :prompt, :string, default: nil\n  attr :options, :list\n  attr :multiple, :boolean, default: false\n\n  attr :rest, :global,\n    include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength\n                multiple pattern placeholder readonly required rows size step)\n\n  slot(:inner_block)\n\n  def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do\n    errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []\n\n    assigns\n    |> assign(field: nil, id: assigns.id || field.id)\n    |> assign(:errors, errors)\n    |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> \"[]\", else: field.name end)\n    |> assign_new(:value, fn -> field.value end)\n    |> input()\n  end\n\n  def input(%{type: \"select\"} = assigns) do\n    ~H\"\"\"\n    <div>\n      <select\n        id={@id}\n        name={@name}\n        class={if @errors != [], do: \"has-error\"}\n        multiple={@multiple}\n        {@rest}\n      >\n        <option :if={@prompt} value=\"\">{@prompt}</option>\n        {Phoenix.HTML.Form.options_for_select(@options, @value)}\n      </select>\n    </div>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/e2e/support/upload_live.ex",
    "content": "defmodule Phoenix.LiveViewTest.E2E.UploadLive do\n  use Phoenix.LiveView\n\n  # for end-to-end testing https://hexdocs.pm/phoenix_live_view/uploads.html\n\n  @impl Phoenix.LiveView\n  def mount(_params, _session, socket) do\n    {:ok,\n     socket\n     |> assign(:uploaded_files, [])\n     |> assign(:auto_upload, false)\n     |> allow_upload(:avatar, accept: ~w(.txt .md), max_entries: 2)}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_params(%{\"auto_upload\" => _}, _uri, socket) do\n    socket\n    |> allow_upload(:avatar, accept: ~w(.txt .md), max_entries: 2, auto_upload: true)\n    |> then(&{:noreply, &1})\n  end\n\n  def handle_params(_params, _uri, socket) do\n    {:noreply, socket}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"validate\", _params, socket) do\n    {:noreply, socket}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"cancel-upload\", %{\"ref\" => ref}, socket) do\n    {:noreply, cancel_upload(socket, :avatar, ref)}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"save\", _params, socket) do\n    uploaded_files =\n      consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->\n        dir = Path.join([System.tmp_dir!(), \"lvupload\"])\n        _ = File.mkdir_p(dir)\n        dest = Path.join([dir, Path.basename(path)])\n        File.cp!(path, dest)\n        {:ok, \"/tmp/lvupload/#{Path.basename(dest)}\"}\n      end)\n\n    {:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}\n  end\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <form id=\"upload-form\" phx-submit=\"save\" phx-change=\"validate\">\n      <.live_file_input upload={@uploads.avatar} />\n      <button type=\"submit\">Upload</button>\n\n      <section phx-drop-target={@uploads.avatar.ref}>\n        <article :for={entry <- @uploads.avatar.entries} class=\"upload-entry\">\n          <figure>\n            <.live_img_preview entry={entry} style=\"width: 500px\" />\n            <figcaption>{entry.client_name}</figcaption>\n          </figure>\n          <progress value={entry.progress} max=\"100\">{entry.progress}%</progress>\n          <button\n            type=\"button\"\n            phx-click=\"cancel-upload\"\n            phx-value-ref={entry.ref}\n            aria-label=\"cancel\"\n          >\n            &times;\n          </button>\n          <p :for={err <- upload_errors(@uploads.avatar, entry)} class=\"alert alert-danger\">\n            {error_to_string(err)}\n          </p>\n        </article>\n        <p :for={err <- upload_errors(@uploads.avatar)} class=\"alert alert-danger\">\n          {error_to_string(err)}\n        </p>\n      </section>\n\n      <ul>\n        <li :for={file <- @uploaded_files}><a href={file}>{Path.basename(file)}</a></li>\n      </ul>\n    </form>\n    \"\"\"\n  end\n\n  defp error_to_string(:too_large), do: \"Too large\"\n  defp error_to_string(:too_many_files), do: \"You have selected too many files\"\n  defp error_to_string(:not_accepted), do: \"You have selected an unacceptable file type\"\nend\n"
  },
  {
    "path": "test/e2e/teardown.js",
    "content": "import { request } from \"@playwright/test\";\n\nexport default async () => {\n  try {\n    const context = await request.newContext({\n      baseURL: \"http://localhost:4004\",\n    });\n    // gracefully stops the e2e script to export coverage\n    await context.post(\"/halt\");\n  } catch {\n    // we expect the request to fail because the request\n    // actually stops the server\n    return;\n  }\n};\n"
  },
  {
    "path": "test/e2e/test-fixtures.js",
    "content": "// see https://github.com/cenfun/monocart-reporter?tab=readme-ov-file#global-coverage-report\nimport { test as testBase, expect } from \"@playwright/test\";\nimport { addCoverageReport } from \"monocart-reporter\";\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst liveViewSourceMap = JSON.parse(\n  fs\n    .readFileSync(\n      path.resolve(\n        __dirname + \"../../../priv/static/phoenix_live_view.esm.js.map\",\n      ),\n    )\n    .toString(\"utf-8\"),\n);\n\nconst test = testBase.extend({\n  autoTestFixture: [\n    async ({ page, browserName }, use) => {\n      // NOTE: it depends on your project name\n      const isChromium = browserName === \"chromium\";\n\n      // console.log(\"autoTestFixture setup...\");\n      // coverage API is chromium only\n      if (isChromium) {\n        await Promise.all([\n          page.coverage.startJSCoverage({\n            resetOnNavigation: false,\n          }),\n          page.coverage.startCSSCoverage({\n            resetOnNavigation: false,\n          }),\n        ]);\n      }\n\n      await use(\"autoTestFixture\");\n\n      // console.log(\"autoTestFixture teardown...\");\n      if (isChromium) {\n        const [jsCoverage, cssCoverage] = await Promise.all([\n          page.coverage.stopJSCoverage(),\n          page.coverage.stopCSSCoverage(),\n        ]);\n        jsCoverage.forEach((entry) => {\n          // read sourcemap for the phoenix_live_view.esm.js manually\n          if (entry.url.endsWith(\"phoenix_live_view.esm.js\")) {\n            entry.sourceMap = liveViewSourceMap;\n          }\n        });\n        const coverageList = [...jsCoverage, ...cssCoverage];\n        // console.log(coverageList.map((item) => item.url));\n        await addCoverageReport(coverageList, test.info());\n      }\n    },\n    {\n      scope: \"test\",\n      auto: true,\n    },\n  ],\n});\nexport { test, expect };\n"
  },
  {
    "path": "test/e2e/test_helper.exs",
    "content": "Application.put_env(:phoenix_live_view, Phoenix.LiveViewTest.E2E.Endpoint,\n  http: [ip: {127, 0, 0, 1}, port: 4004],\n  adapter: Bandit.PhoenixAdapter,\n  server: true,\n  live_view: [signing_salt: \"aaaaaaaa\"],\n  secret_key_base: String.duplicate(\"a\", 64),\n  render_errors: [\n    formats: [\n      html: Phoenix.LiveViewTest.E2E.ErrorHTML\n    ],\n    layout: false\n  ],\n  pubsub_server: Phoenix.LiveViewTest.E2E.PubSub,\n  debug_errors: false\n)\n\nProcess.register(self(), :e2e_helper)\n\ndefmodule Phoenix.LiveViewTest.E2E.ErrorHTML do\n  def render(template, _), do: Phoenix.Controller.status_message_from_template(template)\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Layout do\n  use Phoenix.Component\n\n  def render(\"root.html\", assigns) do\n    ~H\"\"\"\n    <%!-- no doctype -> quirks mode --%>\n    <!DOCTYPE html> {assigns[:render_in_root] && assigns[:render_in_root].(assigns)}\n    {@inner_content}\n    \"\"\"\n  end\n\n  def render(\"live.html\", assigns) do\n    ~H\"\"\"\n    <meta name=\"csrf-token\" content={Plug.CSRFProtection.get_csrf_token()} />\n    <script>\n      window.hooks = {};\n    </script>\n    <script src=\"/assets/phoenix/phoenix.min.js\">\n    </script>\n    <script :if={assigns[:tailwind]} src=\"https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4\">\n    </script>\n    {assigns[:pre_script]}\n    <script type=\"module\">\n      import { LiveSocket } from \"/assets/phoenix_live_view/phoenix_live_view.esm.js\";\n      import { hooks as colocatedHooks } from \"/assets/colocated/index.js\";\n\n      let Hooks = {};\n      Hooks.FormHook = {\n        mounted() {\n          this.pushEvent(\"ping\", {}, () => (this.el.innerText += \"pong\"));\n        },\n      };\n      Hooks.FormStreamHook = {\n        mounted() {\n          this.pushEvent(\"ping\", {}, () => (this.el.innerText += \"pong\"));\n        },\n      };\n      let csrfToken = document\n        .querySelector(\"meta[name='csrf-token']\")\n        .getAttribute(\"content\");\n      let liveSocket = new LiveSocket(\"/live\", window.Phoenix.Socket, {\n        params: { _csrf_token: csrfToken },\n        hooks: { ...Hooks, ...window.hooks, ...colocatedHooks },\n      });\n      liveSocket.connect();\n      window.liveSocket = liveSocket;\n    </script>\n    <style>\n      * { font-size: 1.1rem; }\n    </style>\n    {@inner_content}\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Hooks do\n  import Phoenix.LiveView\n\n  require Logger\n\n  def on_mount(:default, _params, _session, socket) do\n    socket\n    |> attach_hook(:eval_handler, :handle_event, &handle_eval_event/3)\n    |> then(&{:cont, &1})\n  end\n\n  # evaluates the given code in the process of the LiveView\n  # see playwright evalLV() function\n  defp handle_eval_event(\"sandbox:eval\", %{\"value\" => code}, socket) do\n    {result, _} = Code.eval_string(code, [socket: socket], __ENV__)\n\n    Logger.debug(\"lv:#{inspect(self())} eval result: #{inspect(result)}\")\n\n    case result do\n      {:noreply, %Phoenix.LiveView.Socket{} = socket} -> {:halt, %{}, socket}\n      %Phoenix.LiveView.Socket{} = socket -> {:halt, %{}, socket}\n      result -> {:halt, %{\"result\" => result}, socket}\n    end\n  end\n\n  defp handle_eval_event(_, _, socket), do: {:cont, socket}\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.EvalController do\n  use Phoenix.Controller\n\n  plug :accepts, [\"json\"]\n\n  def eval(conn, %{\"code\" => code} = _params) do\n    {result, _} = Code.eval_string(code, [], __ENV__)\n    json(conn, result)\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.SubmitController do\n  use Phoenix.Controller\n\n  def submit(conn, params) do\n    send_resp(conn, 200, Phoenix.json_library().encode!(params))\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Router do\n  use Phoenix.Router\n  import Phoenix.LiveView.Router\n\n  pipeline :browser do\n    plug :accepts, [\"html\"]\n    plug :fetch_session\n    plug :fetch_live_flash\n    plug :protect_from_forgery\n    plug :put_root_layout, html: {Phoenix.LiveViewTest.E2E.Layout, :root}\n  end\n\n  pipeline :portal_root do\n    plug :put_root_layout, html: {Phoenix.LiveViewTest.E2E.PortalLive, :root}\n  end\n\n  live_session :default,\n    layout: {Phoenix.LiveViewTest.E2E.Layout, :live},\n    on_mount: {Phoenix.LiveViewTest.E2E.Hooks, :default} do\n    scope \"/\", Phoenix.LiveViewTest do\n      pipe_through(:browser)\n\n      live \"/stream\", Support.StreamLive\n      live \"/stream/reset\", Support.StreamResetLive\n      live \"/stream/reset-lc\", Support.StreamResetLCLive\n      live \"/stream/limit\", Support.StreamLimitLive\n      live \"/stream/nested-component-reset\", Support.StreamNestedComponentResetLive\n      live \"/stream/inside-for\", Support.StreamInsideForLive\n      live \"/healthy/:category\", Support.HealthyLive\n\n      live \"/upload\", E2E.UploadLive\n      live \"/form\", E2E.FormLive\n      live \"/form/dynamic-inputs\", E2E.FormDynamicInputsLive\n      live \"/form/nested\", E2E.NestedFormLive\n      live \"/form/stream\", E2E.FormStreamLive\n      live \"/js\", E2E.JsLive\n      live \"/select\", E2E.SelectLive\n      live \"/components\", E2E.ComponentsLive\n      live \"/keyed-comprehension\", E2E.KeyedComprehensionLive\n    end\n\n    scope \"/portal\", Phoenix.LiveViewTest do\n      pipe_through([:browser, :portal_root])\n\n      live \"/\", E2E.PortalLive\n    end\n\n    scope \"/issues\", Phoenix.LiveViewTest.E2E do\n      pipe_through(:browser)\n\n      live \"/2787\", Issue2787Live\n      live \"/3026\", Issue3026Live\n      live \"/3040\", Issue3040Live\n      live \"/3083\", Issue3083Live\n      live \"/3107\", Issue3107Live\n      live \"/3117\", Issue3117Live\n      live \"/3200/messages\", Issue3200.PanelLive, :messages_tab\n      live \"/3200/settings\", Issue3200.PanelLive, :settings_tab\n      live \"/3194\", Issue3194Live\n      live \"/3194/other\", Issue3194Live.OtherLive\n      live \"/3378\", Issue3378.HomeLive\n      live \"/3448\", Issue3448Live\n      live \"/3496/a\", Issue3496.ALive\n      live \"/3496/b\", Issue3496.BLive\n      live \"/3529\", Issue3529Live\n      live \"/3612/a\", Issue3612.ALive\n      live \"/3612/b\", Issue3612.BLive\n      live \"/3636\", Issue3636Live\n      live \"/3651\", Issue3651Live\n      live \"/3656\", Issue3656Live\n      live \"/3658\", Issue3658Live\n      live \"/3684\", Issue3684Live\n      live \"/3686/a\", Issue3686.ALive\n      live \"/3686/b\", Issue3686.BLive\n      live \"/3709\", Issue3709Live\n      live \"/3709/:id\", Issue3709Live\n      live \"/3719\", Issue3719Live\n      live \"/3814\", Issue3814Live\n      live \"/3819\", Issue3819Live\n      live \"/3919\", Issue3919Live\n      live \"/3931\", Issue3931Live\n      live \"/3953\", Issue3953Live\n      live \"/3979\", Issue3979Live\n      live \"/4027\", Issue4027Live\n      live \"/4066\", Issue4066Live\n      live \"/4088\", Issue4088Live\n      live \"/4094\", Issue4094Live\n      live \"/4095\", Issue4095Live\n      live \"/4102\", Issue4102Live\n      live \"/4107\", Issue4107Live\n      live \"/4121\", Issue4121Live\n      live \"/4147\", Issue4147Live\n    end\n  end\n\n  live_session :other, layout: {Phoenix.LiveViewTest.E2E.Layout, :live} do\n    scope \"/issues\", Phoenix.LiveViewTest.E2E do\n      pipe_through(:browser)\n\n      live \"/3686/c\", Issue3686.CLive\n    end\n  end\n\n  live_session :navigation, layout: {Phoenix.LiveViewTest.E2E.Navigation.Layout, :live} do\n    scope \"/navigation\" do\n      pipe_through(:browser)\n\n      live \"/a\", Phoenix.LiveViewTest.E2E.Navigation.ALive\n      live \"/b\", Phoenix.LiveViewTest.E2E.Navigation.BLive, :index\n      live \"/b/:id\", Phoenix.LiveViewTest.E2E.Navigation.BLive, :show\n      live \"/redirectloop\", Phoenix.LiveViewTest.E2E.Navigation.RedirectLoopLive, :index\n      get \"/dead\", Phoenix.LiveViewTest.E2E.Navigation.Dead, :index\n    end\n  end\n\n  # these routes use a custom layout and therefore cannot be in the live_session\n  scope \"/\", Phoenix.LiveViewTest.E2E do\n    pipe_through(:browser)\n\n    live \"/form/feedback\", FormFeedbackLive\n    live \"/errors\", ErrorLive\n    live \"/colocated\", ColocatedLive\n\n    scope \"/issues\" do\n      live \"/2965\", Issue2965Live\n      live \"/3047/a\", Issue3047ALive\n      live \"/3047/b\", Issue3047BLive\n      live \"/3169\", Issue3169Live\n      live \"/3530\", Issue3530Live\n      live \"/3647\", Issue3647Live\n      live \"/3681\", Issue3681Live\n      live \"/3681/away\", Issue3681.AwayLive\n      live \"/3941\", Issue3941Live\n      live \"/4078\", Issue4078Live\n    end\n\n    post \"/submit\", SubmitController, :submit\n  end\n\n  post \"/eval\", Phoenix.LiveViewTest.E2E.EvalController, :eval\nend\n\ndefmodule Phoenix.LiveViewTest.E2E.Endpoint do\n  use Phoenix.Endpoint, otp_app: :phoenix_live_view\n\n  @session_options [\n    store: :cookie,\n    key: \"_lv_e2e_key\",\n    signing_salt: \"1gk/d8ms\",\n    same_site: \"Lax\"\n  ]\n\n  socket \"/live\", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]\n\n  plug Plug.Static, from: {:phoenix, \"priv/static\"}, at: \"/assets/phoenix\"\n  plug Plug.Static, from: {:phoenix_live_view, \"priv/static\"}, at: \"/assets/phoenix_live_view\"\n\n  plug Plug.Static,\n    from: Path.join(Mix.Project.build_path(), \"phoenix-colocated/phoenix_live_view\"),\n    at: \"/assets/colocated\"\n\n  plug Plug.Static, from: System.tmp_dir!(), at: \"/tmp\"\n\n  plug :health_check\n  plug :halt\n\n  plug Plug.Parsers,\n    parsers: [:urlencoded, :multipart, :json],\n    pass: [\"*/*\"],\n    json_decoder: Phoenix.json_library()\n\n  plug Plug.Session, @session_options\n  plug Phoenix.LiveViewTest.E2E.Router\n\n  defp health_check(%{request_path: \"/health\"} = conn, _opts) do\n    conn |> Plug.Conn.send_resp(200, \"OK\") |> Plug.Conn.halt()\n  end\n\n  defp health_check(conn, _opts), do: conn\n\n  defp halt(%{request_path: \"/halt\"}, _opts) do\n    send(:e2e_helper, :halt)\n    # this ensure playwright waits until the server force stops\n    Process.sleep(:infinity)\n  end\n\n  defp halt(conn, _opts), do: conn\nend\n\n{:ok, _} =\n  Supervisor.start_link(\n    [\n      Phoenix.LiveViewTest.E2E.Endpoint,\n      {Phoenix.PubSub, name: Phoenix.LiveViewTest.E2E.PubSub}\n    ],\n    strategy: :one_for_one\n  )\n\nIO.puts(\"Starting e2e server on port #{Phoenix.LiveViewTest.E2E.Endpoint.config(:http)[:port]}\")\n\n# we need to manually compile the colocated hooks / js\nPhoenix.LiveView.ColocatedJS.compile()\n\nif not IEx.started?() do\n  # when running the test server manually, we halt after\n  # reading from stdin\n  spawn(fn ->\n    IO.read(:stdio, :line)\n    send(:e2e_helper, :halt)\n  end)\n\n  receive do\n    :halt -> :ok\n  end\nend\n"
  },
  {
    "path": "test/e2e/tests/colocated.spec.js",
    "content": "import { test, expect } from \"../test-fixtures\";\nimport { syncLV } from \"../utils\";\n\ntest(\"colocated hooks works\", async ({ page }) => {\n  await page.goto(\"/colocated\");\n  await syncLV(page);\n\n  await page.locator(\"input\").fill(\"1234567890\");\n  await page.keyboard.press(\"Enter\");\n  // the hook formats the phone number with dashes, so if the dashes\n  // are there, the hook works!\n  await expect(page.locator(\"#phone\")).toHaveText(\"123-456-7890\");\n\n  // test runtime hook\n  await expect(page.locator(\"#runtime\")).toBeVisible();\n});\n\ntest(\"colocated JS works\", async ({ page }) => {\n  // our colocated JS provides a window event handler for executing JS commands\n  // from the server; we have a button that triggers a toggle server side\n  await page.goto(\"/colocated\");\n  await syncLV(page);\n\n  await expect(page.locator(\"#hello\")).toBeVisible();\n\n  await page.locator(\"button\").click();\n  await expect(page.locator(\"#hello\")).toBeHidden();\n\n  await page.locator(\"button\").click();\n  await expect(page.locator(\"#hello\")).toBeVisible();\n});\n\ntest(\"custom macro component works (syntax highlighting)\", async ({ page }) => {\n  await page.goto(\"/colocated\");\n  await syncLV(page);\n  // we check if the code has the makeup classes\n  await expect(\n    page.locator(\"pre\").nth(1).getByText(\"button\").first(),\n  ).toHaveClass(\"nt\");\n  await expect(\n    page.locator(\"pre\").nth(1).getByText(\"@temperature\"),\n  ).toHaveClass(\"na\");\n});\n"
  },
  {
    "path": "test/e2e/tests/components.spec.js",
    "content": "import { test, expect } from \"../test-fixtures\";\nimport { syncLV } from \"../utils\";\n\nlet consoleErrors = [];\nlet jsErrors = [];\n\ntest.beforeEach(async ({ page }) => {\n  consoleErrors = [];\n  jsErrors = [];\n\n  // Listen for console errors\n  page.on(\"console\", (msg) => {\n    if (msg.type() === \"error\") {\n      consoleErrors.push(msg.text());\n    }\n  });\n\n  // Listen for JavaScript errors\n  page.on(\"pageerror\", (error) => {\n    jsErrors.push(error.message);\n  });\n});\n\ntest.afterEach(async () => {\n  // Assert no JavaScript errors occurred during the test\n  expect(jsErrors).toEqual([]);\n  expect(consoleErrors).toEqual([]);\n});\n\ntest(\"dropdown menu focus wrapping works correctly\", async ({\n  page,\n  browserName,\n}) => {\n  // skip if webkit, since it doesn't have tab focus enabled by default\n  if (browserName === \"webkit\") {\n    test.skip();\n  }\n\n  await page.goto(\"/components?tab=focus_wrap\");\n  await syncLV(page);\n\n  await expect(page.locator(\"#dropdown-menu\")).toBeHidden();\n  await page.locator(\"#dropdown-button\").click();\n  await expect(page.locator(\"#dropdown-menu\")).toBeVisible();\n\n  const dropdownButtons = page.locator(\"#dropdown-content button\");\n  await expect(dropdownButtons.first()).toBeFocused();\n\n  // Tab through dropdown items - focus should cycle within the dropdown\n  await page.keyboard.press(\"Tab\");\n  await expect(dropdownButtons.nth(1)).toBeFocused();\n\n  await page.keyboard.press(\"Tab\");\n  await expect(dropdownButtons.nth(2)).toBeFocused();\n\n  // Tab again should cycle back to first item (focus wrap behavior)\n  await page.keyboard.press(\"Tab\");\n  await expect(dropdownButtons.first()).toBeFocused();\n\n  // Shift+Tab should go backwards\n  await page.keyboard.press(\"Shift+Tab\");\n  await expect(dropdownButtons.nth(2)).toBeFocused();\n\n  // Click a menu item to close dropdown\n  await dropdownButtons.first().click();\n\n  // Dropdown should be hidden again\n  await expect(page.locator(\"#dropdown-menu\")).toBeHidden();\n});\n\ntest(\"simple focus container traps focus correctly\", async ({\n  page,\n  browserName,\n}) => {\n  // skip if webkit, since it doesn't have tab focus enabled by default\n  if (browserName === \"webkit\") {\n    test.skip();\n  }\n\n  await page.goto(\"/components?tab=focus_wrap\");\n  await syncLV(page);\n\n  // Click on first button in the focus container to start focus there\n  const containerButtons = page.locator(\"#simple-focus-container button\");\n  const containerInput = page.locator(\"#simple-focus-container input\");\n\n  await containerButtons.first().click();\n  await expect(containerButtons.first()).toBeFocused();\n\n  // Tab should move to second button\n  await page.keyboard.press(\"Tab\");\n  await expect(containerButtons.nth(1)).toBeFocused();\n\n  // Tab should move to input\n  await page.keyboard.press(\"Tab\");\n  await expect(containerInput).toBeFocused();\n\n  // Tab should cycle back to first button (focus wrap behavior)\n  await page.keyboard.press(\"Tab\");\n  await expect(containerButtons.first()).toBeFocused();\n\n  // Shift+Tab should go backwards to input\n  await page.keyboard.press(\"Shift+Tab\");\n  await expect(containerInput).toBeFocused();\n\n  // Shift+Tab should go to second button\n  await page.keyboard.press(\"Shift+Tab\");\n  await expect(containerButtons.nth(1)).toBeFocused();\n\n  // Shift+Tab should go to first button\n  await page.keyboard.press(\"Shift+Tab\");\n  await expect(containerButtons.first()).toBeFocused();\n});\n\ntest(\"focus_wrap components have correct attributes\", async ({ page }) => {\n  await page.goto(\"/components?tab=focus_wrap\");\n  await syncLV(page);\n\n  // Check that focus_wrap components have the correct phx-hook attribute\n  await expect(page.locator(\"#dropdown-content\")).toHaveAttribute(\n    \"phx-hook\",\n    \"Phoenix.FocusWrap\",\n  );\n  await expect(page.locator(\"#simple-focus-container\")).toHaveAttribute(\n    \"phx-hook\",\n    \"Phoenix.FocusWrap\",\n  );\n\n  // Check that focus sentinel spans are present\n  await expect(page.locator(\"#dropdown-content-start\")).toHaveAttribute(\n    \"tabindex\",\n    \"0\",\n  );\n  await expect(page.locator(\"#dropdown-content-start\")).toHaveAttribute(\n    \"aria-hidden\",\n    \"true\",\n  );\n  await expect(page.locator(\"#dropdown-content-end\")).toHaveAttribute(\n    \"tabindex\",\n    \"0\",\n  );\n  await expect(page.locator(\"#dropdown-content-end\")).toHaveAttribute(\n    \"aria-hidden\",\n    \"true\",\n  );\n\n  await expect(page.locator(\"#simple-focus-container-start\")).toHaveAttribute(\n    \"tabindex\",\n    \"0\",\n  );\n  await expect(page.locator(\"#simple-focus-container-start\")).toHaveAttribute(\n    \"aria-hidden\",\n    \"true\",\n  );\n  await expect(page.locator(\"#simple-focus-container-end\")).toHaveAttribute(\n    \"tabindex\",\n    \"0\",\n  );\n  await expect(page.locator(\"#simple-focus-container-end\")).toHaveAttribute(\n    \"aria-hidden\",\n    \"true\",\n  );\n});\n"
  },
  {
    "path": "test/e2e/tests/errors.spec.js",
    "content": "import { test, expect } from \"../test-fixtures\";\nimport { syncLV } from \"../utils\";\n\n/**\n * https://hexdocs.pm/phoenix_live_view/error-handling.html\n */\ntest.describe(\"exception handling\", () => {\n  let webSocketEvents = [];\n  let networkEvents = [];\n  let consoleMessages = [];\n\n  test.beforeEach(async ({ page }) => {\n    networkEvents = [];\n    webSocketEvents = [];\n    consoleMessages = [];\n\n    page.on(\"request\", (request) =>\n      networkEvents.push({ method: request.method(), url: request.url() }),\n    );\n\n    page.on(\"websocket\", (ws) => {\n      ws.on(\"framesent\", (event) =>\n        webSocketEvents.push({ type: \"sent\", payload: event.payload }),\n      );\n      ws.on(\"framereceived\", (event) =>\n        webSocketEvents.push({ type: \"received\", payload: event.payload }),\n      );\n      ws.on(\"close\", () => webSocketEvents.push({ type: \"close\" }));\n    });\n\n    page.on(\"console\", (msg) => consoleMessages.push(msg.text()));\n  });\n\n  test.describe(\"during HTTP mount\", () => {\n    test(\"500 error when dead mount fails\", async ({ page }) => {\n      page.on(\"response\", (response) => {\n        expect(response.status()).toBe(500);\n      });\n      await page.goto(\"/errors?dead-mount=raise\");\n    });\n  });\n\n  test.describe(\"during connected mount\", () => {\n    /**\n     * When the connected mount fails, the page is reloaded. The hope here is\n     * that the next time the page is loaded, either the connected mount will\n     * succeed, or the same error will be triggered during the dead mount as well,\n     * rendering an error page.\n     *\n     * In the unlikely case that the dead mount succeeds, but the connected mount\n     * fails repeatedly, the liveSocket enters failsafe mode. This still means that\n     * the page will be reloaded without giving up, but the duration is set to 30s\n     * by default.\n     */\n    test(\"reloads the page when connected mount fails\", async ({ page }) => {\n      await page.goto(\"/errors?connected-mount=raise\");\n\n      // the page was loaded once\n      expect(\n        networkEvents.filter((e) =>\n          e.url.includes(\"http://localhost:4004/errors?connected-mount=raise\"),\n        ),\n      ).toHaveLength(1);\n\n      networkEvents = [];\n\n      await page.waitForTimeout(2000);\n\n      // the page was reloaded 5 times\n      expect(networkEvents).toEqual(\n        expect.arrayContaining([\n          {\n            method: \"GET\",\n            url: \"http://localhost:4004/errors?connected-mount=raise\",\n          },\n          {\n            method: \"GET\",\n            url: \"http://localhost:4004/errors?connected-mount=raise\",\n          },\n          {\n            method: \"GET\",\n            url: \"http://localhost:4004/errors?connected-mount=raise\",\n          },\n          {\n            method: \"GET\",\n            url: \"http://localhost:4004/errors?connected-mount=raise\",\n          },\n          {\n            method: \"GET\",\n            url: \"http://localhost:4004/errors?connected-mount=raise\",\n          },\n          {\n            method: \"GET\",\n            url: \"http://localhost:4004/errors?connected-mount=raise\",\n          },\n          {\n            method: \"GET\",\n            url: \"http://localhost:4004/errors?connected-mount=raise\",\n          },\n        ]),\n      );\n\n      expect(consoleMessages).toEqual(\n        expect.arrayContaining([\n          expect.stringMatching(/consecutive reloads. Entering failsafe mode/),\n        ]),\n      );\n    });\n\n    /**\n     * TBD: if the connected mount of the main LV succeeds, but a child LV fails\n     * on mount, we only try to rejoin the child LV instead of reloading the page.\n     */\n    test(\"rejoin instead of reload when child LV fails on connected mount\", async ({\n      page,\n    }) => {\n      await page.goto(\"/errors?connected-child-mount-raise=2\");\n      await page.waitForTimeout(2000);\n\n      expect(consoleMessages).toEqual(\n        expect.arrayContaining([\n          expect.stringMatching(/mount/),\n          expect.stringMatching(/child error: unable to join/),\n          expect.stringMatching(/child error: unable to join/),\n          // third time's the charm\n          expect.stringMatching(/child mount/),\n        ]),\n      );\n\n      // page was not reloaded\n      expect(\n        networkEvents.filter((e) =>\n          e.url.includes(\"http://localhost:4004/errors?\"),\n        ),\n      ).toHaveLength(1);\n    });\n\n    /**\n     * TBD: if the connected mount of the main LV succeeds, but a child LV fails\n     * repeatedly, we reload the page. Maybe we should give up without reloading the page?\n     */\n    test(\"abandons child remount if child LV fails multiple times\", async ({\n      page,\n    }) => {\n      await page.goto(\"/errors?connected-child-mount-raise=5\");\n      // maybe we can find a better way than waiting for a fixed amount of time\n      await page.waitForTimeout(1000);\n\n      expect(consoleMessages.filter((m) => m.startsWith(\"child \"))).toEqual([\n        expect.stringContaining(\"child error: unable to join\"),\n        expect.stringContaining(\"child error: unable to join\"),\n        expect.stringContaining(\"child error: unable to join\"),\n        // maxChildJoinTries is 3, we count from 0, so the 4th try is the last\n        expect.stringContaining(\"child error: giving up\"),\n        expect.stringContaining(\"child destroyed\"),\n      ]);\n\n      // page remained loaded without parent failsafe reload\n      expect(\n        networkEvents.filter((e) =>\n          e.url.includes(\n            \"http://localhost:4004/errors?connected-child-mount-raise=5\",\n          ),\n        ),\n      ).toHaveLength(1);\n    });\n  });\n\n  test.describe(\"after connected mount\", () => {\n    /**\n     * When a child LV crashes after the connected mount, the parent LV is not\n     * affected. The child LV is simply remounted.\n     */\n    test(\"page does not reload if child LV crashes (handle_event)\", async ({\n      page,\n    }) => {\n      await page.goto(\"/errors?child\");\n      await syncLV(page);\n\n      const parentTime = await page.locator(\"#render-time\").innerText();\n      const childTime = await page.locator(\"#child-render-time\").innerText();\n\n      // both lvs mounted, no other messages\n      expect(consoleMessages).toEqual(\n        expect.arrayContaining([\n          expect.stringMatching(/mount/),\n          expect.stringMatching(/child mount/),\n        ]),\n      );\n      consoleMessages = [];\n\n      await page.getByRole(\"button\", { name: \"Crash child\" }).click();\n      await syncLV(page);\n\n      // child crashed and re-rendered\n      const newChildTime = page.locator(\"#child-render-time\");\n      await expect(newChildTime).not.toHaveText(childTime);\n      expect(consoleMessages).toEqual([\n        expect.stringMatching(/child error: view crashed/),\n        expect.stringMatching(/child mount/),\n      ]);\n\n      // parent did not re-render\n      const newParentTiem = page.locator(\"#render-time\");\n      await expect(newParentTiem).toHaveText(parentTime);\n\n      // page was not reloaded\n      expect(\n        networkEvents.filter((e) =>\n          e.url.includes(\"http://localhost:4004/errors?child\"),\n        ),\n      ).toHaveLength(1);\n    });\n\n    /**\n     * When the main LV crashes after the connected mount, the page is not reloaded.\n     * The main LV is simply remounted over the existing transport.\n     */\n    test(\"page does not reload if main LV crashes (handle_event)\", async ({\n      page,\n    }) => {\n      await page.goto(\"/errors?child\");\n      await syncLV(page);\n\n      const parentTime = await page.locator(\"#render-time\").innerText();\n      const childTime = await page.locator(\"#child-render-time\").innerText();\n\n      // both lvs mounted, no other messages\n      expect(consoleMessages).toEqual(\n        expect.arrayContaining([\n          expect.stringMatching(/mount/),\n          expect.stringMatching(/child mount/),\n        ]),\n      );\n      consoleMessages = [];\n\n      await page.getByRole(\"button\", { name: \"Crash main\" }).click();\n      await syncLV(page);\n\n      // main and child re-rendered (full page refresh)\n      const newChildTime = page.locator(\"#child-render-time\");\n      await expect(newChildTime).not.toHaveText(childTime);\n      const newParentTiem = page.locator(\"#render-time\");\n      await expect(newParentTiem).not.toHaveText(parentTime);\n\n      expect(consoleMessages).toEqual([\n        expect.stringMatching(/child destroyed/),\n        expect.stringMatching(/error: view crashed/),\n        expect.stringMatching(/mount/),\n        expect.stringMatching(/child mount/),\n      ]);\n\n      // page was not reloaded\n      expect(\n        networkEvents.filter((e) =>\n          e.url.includes(\"http://localhost:4004/errors?child\"),\n        ),\n      ).toHaveLength(1);\n    });\n\n    /**\n     * When the main LV mounts successfully, but a child LV crashes which is linked\n     * to the parent, the parent LV crashed too, triggering a remount of both.\n     */\n    test(\"parent crashes and reconnects when linked child LV crashes\", async ({\n      page,\n    }) => {\n      await page.goto(\"/errors?connected-child-mount-raise=link\");\n      await syncLV(page);\n\n      // child crashed on mount, linked to parent -> parent crashed too\n      // second mounts are successful\n      expect(consoleMessages).toEqual(\n        expect.arrayContaining([\n          expect.stringMatching(/mount/),\n          expect.stringMatching(/child error: unable to join/),\n          expect.stringMatching(/child destroyed/),\n          expect.stringMatching(/error: view crashed/),\n          expect.stringMatching(/mount/),\n          expect.stringMatching(/child mount/),\n        ]),\n      );\n      consoleMessages = [];\n\n      const parentTime = await page.locator(\"#render-time\").innerText();\n      const childTime = await page.locator(\"#child-render-time\").innerText();\n\n      // the processes are still linked, crashing the child again crashes the parent\n      await page.getByRole(\"button\", { name: \"Crash child\" }).click();\n      await syncLV(page);\n\n      // main and child re-rendered (full page refresh)\n      const newChildTime = page.locator(\"#child-render-time\");\n      await expect(newChildTime).not.toHaveText(childTime);\n      const newParentTiem = page.locator(\"#render-time\");\n      await expect(newParentTiem).not.toHaveText(parentTime);\n\n      expect(consoleMessages).toEqual([\n        expect.stringMatching(/child error: view crashed/),\n        expect.stringMatching(/child destroyed/),\n        expect.stringMatching(/error: view crashed/),\n        expect.stringMatching(/mount/),\n        expect.stringMatching(/child mount/),\n      ]);\n\n      // page was not reloaded\n      expect(\n        networkEvents.filter((e) =>\n          e.url.includes(\n            \"http://localhost:4004/errors?connected-child-mount-raise=link\",\n          ),\n        ),\n      ).toHaveLength(1);\n    });\n  });\n});\n"
  },
  {
    "path": "test/e2e/tests/forms.spec.js",
    "content": "import { test, expect } from \"../test-fixtures\";\nimport { syncLV, evalLV, evalPlug, attributeMutations } from \"../utils\";\nimport querystring from \"node:querystring\";\n\nfor (const path of [\"/form/nested\", \"/form\"]) {\n  // see also https://github.com/phoenixframework/phoenix_live_view/issues/1759\n  // https://github.com/phoenixframework/phoenix_live_view/issues/2993\n  test.describe(\"restores disabled and readonly states\", () => {\n    test(`${path} - readonly state is restored after submits`, async ({\n      page,\n    }) => {\n      await page.goto(path);\n      await syncLV(page);\n      await expect(page.locator(\"input[name=a]\")).toHaveAttribute(\"readonly\");\n      const changesA = attributeMutations(page, \"input[name=a]\");\n      const changesB = attributeMutations(page, \"input[name=b]\");\n      // can submit multiple times and readonly input stays readonly\n      await page.locator(\"#submit\").click();\n      await syncLV(page);\n      // a is readonly and should stay readonly\n      expect(await changesA()).toEqual(\n        expect.arrayContaining([\n          { attr: \"data-phx-readonly\", oldValue: null, newValue: \"true\" },\n          { attr: \"readonly\", oldValue: \"\", newValue: \"\" },\n          { attr: \"data-phx-readonly\", oldValue: \"true\", newValue: null },\n          { attr: \"readonly\", oldValue: \"\", newValue: \"\" },\n        ]),\n      );\n      // b is not readonly, but LV will set it to readonly while submitting\n      expect(await changesB()).toEqual(\n        expect.arrayContaining([\n          { attr: \"data-phx-readonly\", oldValue: null, newValue: \"false\" },\n          { attr: \"readonly\", oldValue: null, newValue: \"\" },\n          { attr: \"data-phx-readonly\", oldValue: \"false\", newValue: null },\n          { attr: \"readonly\", oldValue: \"\", newValue: null },\n        ]),\n      );\n      await expect(page.locator(\"input[name=a]\")).toHaveAttribute(\"readonly\");\n      await page.locator(\"#submit\").click();\n      await syncLV(page);\n      await expect(page.locator(\"input[name=a]\")).toHaveAttribute(\"readonly\");\n    });\n\n    test(`${path} - button disabled state is restored after submits`, async ({\n      page,\n    }) => {\n      await page.goto(path);\n      await syncLV(page);\n      const changes = attributeMutations(page, \"#submit\");\n      await page.locator(\"#submit\").click();\n      await syncLV(page);\n      // submit button is disabled while submitting, but then restored\n      expect(await changes()).toEqual(\n        expect.arrayContaining([\n          { attr: \"data-phx-disabled\", oldValue: null, newValue: \"false\" },\n          { attr: \"disabled\", oldValue: null, newValue: \"\" },\n          { attr: \"class\", oldValue: null, newValue: \"phx-submit-loading\" },\n          { attr: \"data-phx-disabled\", oldValue: \"false\", newValue: null },\n          { attr: \"disabled\", oldValue: \"\", newValue: null },\n          { attr: \"class\", oldValue: \"phx-submit-loading\", newValue: null },\n        ]),\n      );\n    });\n\n    test(`${path} - non-form button (phx-disable-with) disabled state is restored after click`, async ({\n      page,\n    }) => {\n      await page.goto(path);\n      await syncLV(page);\n      const changes = attributeMutations(page, \"button[type=button]\");\n      await page.locator(\"button[type=button]\").click();\n      await syncLV(page);\n      // submit button is disabled while submitting, but then restored\n      expect(await changes()).toEqual(\n        expect.arrayContaining([\n          { attr: \"data-phx-disabled\", oldValue: null, newValue: \"false\" },\n          { attr: \"disabled\", oldValue: null, newValue: \"\" },\n          { attr: \"class\", oldValue: null, newValue: \"phx-click-loading\" },\n          { attr: \"data-phx-disabled\", oldValue: \"false\", newValue: null },\n          { attr: \"disabled\", oldValue: \"\", newValue: null },\n          { attr: \"class\", oldValue: \"phx-click-loading\", newValue: null },\n        ]),\n      );\n    });\n  });\n\n  for (const additionalParams of [\"live-component\", \"\", \"portal\"]) {\n    if (additionalParams === \"portal\" && path === \"/form/nested\") {\n      // TODO: maybe revisit this in the future. I believe we could also track\n      // nested LiveViews by them being included in the DOM and only destroy when\n      // they are actually removed.\n    } else {\n      const append = additionalParams.length ? ` ${additionalParams}` : \"\";\n      test.describe(`${path}${append} - form recovery`, () => {\n        test(\"form state is recovered when socket reconnects\", async ({\n          page,\n        }) => {\n          let webSocketEvents = [];\n          page.on(\"websocket\", (ws) => {\n            ws.on(\"framesent\", (event) =>\n              webSocketEvents.push({ type: \"sent\", payload: event.payload }),\n            );\n            ws.on(\"framereceived\", (event) =>\n              webSocketEvents.push({\n                type: \"received\",\n                payload: event.payload,\n              }),\n            );\n            ws.on(\"close\", () => webSocketEvents.push({ type: \"close\" }));\n          });\n\n          await page.goto(path + \"?\" + additionalParams);\n          await syncLV(page);\n\n          await page.locator(\"input[name=b]\").fill(\"test\");\n          await page.locator(\"input[name=e]\").fill(\"inside\");\n          await page.locator(\"input[name=f]\").fill(\"outside\");\n          await page.locator(\"input[name=c]\").fill(\"hello world\");\n          await page.locator(\"select[name=d]\").selectOption(\"bar\");\n          await expect(page.locator(\"input[name=c]\")).toBeFocused();\n          await syncLV(page);\n\n          await page.evaluate(\n            () =>\n              new Promise((resolve) => window.liveSocket.disconnect(resolve)),\n          );\n          await expect(page.locator(\".phx-loading\")).toHaveCount(1);\n\n          expect(webSocketEvents).toEqual(\n            expect.arrayContaining([\n              { type: \"sent\", payload: expect.stringContaining(\"phx_join\") },\n              {\n                type: \"received\",\n                payload: expect.stringContaining(\"phx_reply\"),\n              },\n              { type: \"close\" },\n            ]),\n          );\n\n          webSocketEvents = [];\n\n          await page.evaluate(() => window.liveSocket.connect());\n          await syncLV(page);\n          await expect(page.locator(\".phx-loading\")).toHaveCount(0);\n\n          await expect(page.locator(\"input[name=b]\")).toHaveValue(\"test\");\n          // c should still be focused (at least when not using a nested LV)\n          if (path === \"/form\") {\n            await expect(page.locator(\"input[name=c]\")).toBeFocused();\n          }\n          await expect(page.locator(\"select[name=d]\")).toHaveValue(\"bar\");\n\n          expect(webSocketEvents).toEqual(\n            expect.arrayContaining([\n              { type: \"sent\", payload: expect.stringContaining(\"phx_join\") },\n              {\n                type: \"received\",\n                payload: expect.stringContaining(\"phx_reply\"),\n              },\n              {\n                type: \"sent\",\n                payload: expect.stringContaining(\"validate\"),\n              },\n            ]),\n          );\n\n          expect(formPayload(webSocketEvents)).toEqual({\n            _unused_a: \"\",\n            a: \"foo\",\n            b: \"test\",\n            c: \"hello world\",\n            d: \"bar\",\n            e: \"inside\",\n            f: \"outside\",\n          });\n        });\n\n        test(\"JS command in phx-change works during recovery\", async ({\n          page,\n        }) => {\n          await page.goto(path + \"?\" + additionalParams + \"&js-change=1\");\n          await syncLV(page);\n\n          await page.locator(\"input[name=b]\").fill(\"test\");\n          // blur, otherwise the input would not be morphed anyway\n          await page.locator(\"input[name=b]\").blur();\n          await expect(page.locator(\"form\")).toHaveAttribute(\n            \"phx-change\",\n            /push/,\n          );\n          await syncLV(page);\n\n          await page.evaluate(\n            () =>\n              new Promise((resolve) => window.liveSocket.disconnect(resolve)),\n          );\n          await expect(page.locator(\".phx-loading\")).toHaveCount(1);\n\n          await page.evaluate(() => window.liveSocket.connect());\n          await syncLV(page);\n          await expect(page.locator(\".phx-loading\")).toHaveCount(0);\n          await expect(page.locator(\"input[name=b]\")).toHaveValue(\"test\");\n        });\n\n        test(\"does not recover when form is missing id\", async ({ page }) => {\n          await page.goto(`${path}?no-id&${additionalParams}`);\n          await syncLV(page);\n\n          await page.locator(\"input[name=b]\").fill(\"test\");\n          // blur, otherwise the input would not be morphed anyway\n          await page.locator(\"input[name=b]\").blur();\n          await syncLV(page);\n\n          await page.evaluate(\n            () =>\n              new Promise((resolve) => window.liveSocket.disconnect(resolve)),\n          );\n          await expect(page.locator(\".phx-loading\")).toHaveCount(1);\n\n          await page.evaluate(() => window.liveSocket.connect());\n          await syncLV(page);\n          await expect(page.locator(\".phx-loading\")).toHaveCount(0);\n\n          await expect(page.locator(\"input[name=b]\")).toHaveValue(\"bar\");\n        });\n\n        test(\"does not recover when form is missing phx-change\", async ({\n          page,\n        }) => {\n          await page.goto(`${path}?no-change-event&${additionalParams}`);\n          await syncLV(page);\n\n          await page.locator(\"input[name=b]\").fill(\"test\");\n          // blur, otherwise the input would not be morphed anyway\n          await page.locator(\"input[name=b]\").blur();\n          await syncLV(page);\n\n          await page.evaluate(\n            () =>\n              new Promise((resolve) => window.liveSocket.disconnect(resolve)),\n          );\n          await expect(page.locator(\".phx-loading\")).toHaveCount(1);\n\n          await page.evaluate(() => window.liveSocket.connect());\n          await syncLV(page);\n          await expect(page.locator(\".phx-loading\")).toHaveCount(0);\n\n          await expect(page.locator(\"input[name=b]\")).toHaveValue(\"bar\");\n        });\n\n        test(\"phx-auto-recover\", async ({ page }) => {\n          await page.goto(\n            `${path}?phx-auto-recover=custom-recovery&${additionalParams}`,\n          );\n          await syncLV(page);\n\n          await page.locator(\"input[name=b]\").fill(\"test\");\n          // blur, otherwise the input would not be morphed anyway\n          await page.locator(\"input[name=b]\").blur();\n          await syncLV(page);\n\n          await page.evaluate(\n            () =>\n              new Promise((resolve) => window.liveSocket.disconnect(resolve)),\n          );\n          await expect(page.locator(\".phx-loading\")).toHaveCount(1);\n\n          const webSocketEvents = [];\n          page.on(\"websocket\", (ws) => {\n            ws.on(\"framesent\", (event) =>\n              webSocketEvents.push({ type: \"sent\", payload: event.payload }),\n            );\n            ws.on(\"framereceived\", (event) =>\n              webSocketEvents.push({\n                type: \"received\",\n                payload: event.payload,\n              }),\n            );\n            ws.on(\"close\", () => webSocketEvents.push({ type: \"close\" }));\n          });\n\n          await page.evaluate(() => window.liveSocket.connect());\n          await syncLV(page);\n          await expect(page.locator(\".phx-loading\")).toHaveCount(0);\n\n          await expect(page.locator(\"input[name=b]\")).toHaveValue(\n            \"custom value from server\",\n          );\n\n          expect(webSocketEvents).toEqual(\n            expect.arrayContaining([\n              { type: \"sent\", payload: expect.stringContaining(\"phx_join\") },\n              {\n                type: \"received\",\n                payload: expect.stringContaining(\"phx_reply\"),\n              },\n              {\n                type: \"sent\",\n                payload: expect.stringMatching(\n                  /event.*_unused_a=&a=foo&b=test/,\n                ),\n              },\n            ]),\n          );\n        });\n\n        test(\"respects disabled state of a fieldset\", async ({ page }) => {\n          let webSocketEvents = [];\n          page.on(\"websocket\", (ws) => {\n            ws.on(\"framesent\", (event) =>\n              webSocketEvents.push({ type: \"sent\", payload: event.payload }),\n            );\n            ws.on(\"framereceived\", (event) =>\n              webSocketEvents.push({\n                type: \"received\",\n                payload: event.payload,\n              }),\n            );\n            ws.on(\"close\", () => webSocketEvents.push({ type: \"close\" }));\n          });\n\n          await page.goto(path + \"?disabled-fieldset=true&\" + additionalParams);\n          await syncLV(page);\n\n          await page.locator(\"input[name=c]\").fill(\"hello world\");\n          await page.locator(\"select[name=d]\").selectOption(\"bar\");\n          await expect(page.locator(\"input[name=c]\")).toBeFocused();\n          await syncLV(page);\n\n          await page.pause();\n\n          await page.evaluate(\n            () =>\n              new Promise((resolve) => window.liveSocket.disconnect(resolve)),\n          );\n          await expect(page.locator(\".phx-loading\")).toHaveCount(1);\n\n          webSocketEvents = [];\n          await page.evaluate(() => window.liveSocket.connect());\n          await syncLV(page);\n          await expect(page.locator(\".phx-loading\")).toHaveCount(0);\n\n          expect(formPayload(webSocketEvents)).toEqual({\n            c: \"hello world\",\n            d: \"bar\",\n            _unused_e: \"\",\n            e: \"\",\n            _unused_f: \"\",\n            f: \"\",\n          });\n        });\n\n        // nested LiveViews don't support handle_params\n        if (path === \"/form\") {\n          test(\"navigation during recovery is properly handled by the client\", async ({\n            page,\n          }) => {\n            await page.goto(\n              `${path}?phx-auto-recover=patch-recovery&${additionalParams}`,\n            );\n            await syncLV(page);\n\n            await page.evaluate(\n              () =>\n                new Promise((resolve) => window.liveSocket.disconnect(resolve)),\n            );\n            await expect(page.locator(\".phx-loading\")).toHaveCount(1);\n\n            await page.evaluate(() => window.liveSocket.connect());\n            await expect(page).toHaveURL(/\\patched=true/);\n          });\n        }\n      });\n    }\n  }\n\n  test(`${path} - can submit form with button that has phx-click`, async ({\n    page,\n  }) => {\n    await page.goto(`${path}?phx-auto-recover=custom-recovery`);\n    await syncLV(page);\n\n    await expect(page.getByText(\"Form was submitted!\")).toBeHidden();\n\n    await page.getByRole(\"button\", { name: \"Submit with JS\" }).click();\n    await syncLV(page);\n\n    await expect(page.getByText(\"Form was submitted!\")).toBeVisible();\n  });\n\n  test(`${path} - loading and locked states with latency`, async ({\n    page,\n    request,\n  }) => {\n    const nested = !!path.match(/nested/);\n    await page.goto(`${path}?phx-change=validate`);\n    await syncLV(page);\n    const { lv_pid } = await evalLV(\n      page,\n      `\n      <<\"#PID\"::binary, pid::binary>> = inspect(self())\n\n      pid_parts =\n        pid\n        |> String.trim_leading(\"<\")\n        |> String.trim_trailing(\">\")\n        |> String.split(\".\")\n\n      %{lv_pid: pid_parts}\n    `,\n      nested ? \"#nested\" : undefined,\n    );\n    const ack = (event) =>\n      evalPlug(\n        request,\n        `send(IEx.Helpers.pid(${lv_pid[0]}, ${lv_pid[1]}, ${lv_pid[2]}), {:sync, \"${event}\"}); nil`,\n      );\n    // we serialize the test by letting each event handler wait for a {:sync, event} message\n    await evalLV(\n      page,\n      `\n      attach_hook(socket, :sync, :handle_event, fn event, _params, socket ->\n        if event == \"ping\" do\n          {:cont, socket}\n        else\n          receive do {:sync, ^event} -> {:cont, socket} end\n        end\n      end)\n    `,\n      nested ? \"#nested\" : undefined,\n    );\n    await expect(page.getByText(\"Form was submitted!\")).toBeHidden();\n    const testForm = page.locator(\"#test-form\");\n    const submitBtn = page.locator(\"#test-form #submit\");\n    await page.locator(\"#test-form input[name=b]\").fill(\"test\");\n    await expect(testForm).toHaveClass(\"myformclass phx-change-loading\");\n    await expect(testForm).toHaveAttribute(\"data-phx-ref-loading\");\n    // form is locked on phx-change for any changed input\n    await expect(testForm).toHaveAttribute(\"data-phx-ref-lock\");\n    await expect(testForm).toHaveAttribute(\"data-phx-ref-src\");\n    await submitBtn.click();\n    // change-loading and submit-loading classes exist simultaneously\n    await expect(testForm).toHaveClass(\n      \"myformclass phx-change-loading phx-submit-loading\",\n    );\n    // phx-change ack arrives and is removed\n    await ack(\"validate\");\n    await expect(testForm).toHaveClass(\"myformclass phx-submit-loading\");\n    await expect(submitBtn).toHaveClass(\"phx-submit-loading\");\n    await expect(submitBtn).toHaveAttribute(\n      \"data-phx-disable-with-restore\",\n      \"Submit\",\n    );\n    await expect(submitBtn).toHaveAttribute(\"data-phx-ref-loading\");\n    await expect(testForm).toHaveAttribute(\"data-phx-ref-loading\");\n    await expect(testForm).toHaveAttribute(\"data-phx-ref-src\");\n    await expect(submitBtn).toHaveAttribute(\"data-phx-ref-lock\");\n    // form is not locked on submit\n    await expect(testForm).not.toHaveAttribute(\"data-phx-ref-lock\");\n    await expect(submitBtn).toHaveAttribute(\"data-phx-ref-src\");\n    await expect(submitBtn).toHaveAttribute(\"disabled\", \"\");\n    await expect(submitBtn).toHaveAttribute(\"phx-disable-with\", \"Submitting\");\n    await ack(\"save\");\n    await expect(page.getByText(\"Form was submitted!\")).toBeVisible();\n    // all refs are cleaned up\n    await expect(testForm).toHaveClass(\"myformclass\");\n    await expect(submitBtn).toHaveClass(\"\");\n    await expect(submitBtn).not.toHaveAttribute(\n      \"data-phx-disable-with-restore\",\n    );\n    await expect(submitBtn).not.toHaveAttribute(\"data-phx-ref-loading\");\n    await expect(submitBtn).not.toHaveAttribute(\"data-phx-ref-lock\");\n    await expect(submitBtn).not.toHaveAttribute(\"data-phx-ref-src\");\n    await expect(submitBtn).not.toHaveAttribute(\"data-phx-ref-loading\");\n    await expect(submitBtn).not.toHaveAttribute(\"data-phx-ref-lock\");\n    await expect(submitBtn).not.toHaveAttribute(\"data-phx-ref-src\");\n    await expect(submitBtn).not.toHaveAttribute(\"disabled\");\n    await expect(submitBtn).toHaveAttribute(\"phx-disable-with\", \"Submitting\");\n  });\n}\n\ntest(\"loading and locked states with latent clone\", async ({\n  page,\n  request,\n}) => {\n  await page.goto(\"/form/stream\");\n  const formHook = page.locator(\"#form-stream-hook\");\n  await syncLV(page);\n  const { lv_pid } = await evalLV(\n    page,\n    `\n    <<\"#PID\"::binary, pid::binary>> = inspect(self())\n\n    pid_parts =\n      pid\n      |> String.trim_leading(\"<\")\n      |> String.trim_trailing(\">\")\n      |> String.split(\".\")\n\n    %{lv_pid: pid_parts}\n  `,\n  );\n  const ack = (event) =>\n    evalPlug(\n      request,\n      `send(IEx.Helpers.pid(${lv_pid[0]}, ${lv_pid[1]}, ${lv_pid[2]}), {:sync, \"${event}\"}); nil`,\n    );\n  // we serialize the test by letting each event handler wait for a {:sync, event} message\n  // excluding the ping messages from our hook\n  await evalLV(\n    page,\n    `\n    attach_hook(socket, :sync, :handle_event, fn event, _params, socket ->\n      if event == \"ping\" do\n        {:cont, socket}\n      else\n        receive do {:sync, ^event} -> {:cont, socket} end\n      end\n    end)\n  `,\n  );\n  await expect(formHook).toHaveText(\"pong\");\n  const testForm = page.locator(\"#test-form\");\n  const testInput = page.locator(\"#test-form input[name=myname]\");\n  const submitBtn = page.locator(\"#test-form button\");\n  // initial 3 stream items\n  await expect(page.locator(\"#form-stream li\")).toHaveCount(3);\n  await testInput.fill(\"1\");\n  await testInput.fill(\"2\");\n  // form is locked on phx-change and stream remains unchanged\n  await expect(testForm).toHaveClass(\"phx-change-loading\");\n  await expect(testInput).toHaveClass(\"phx-change-loading\");\n  await expect(testForm).toHaveAttribute(\"data-phx-ref-loading\");\n  await expect(testForm).toHaveAttribute(\"data-phx-ref-src\");\n  await expect(testInput).toHaveAttribute(\"data-phx-ref-loading\");\n  await expect(testInput).toHaveAttribute(\"data-phx-ref-src\");\n  // now we submit\n  await submitBtn.click();\n  await expect(testForm).toHaveClass(\"phx-change-loading phx-submit-loading\");\n  await expect(submitBtn).toHaveText(\"Saving...\");\n  await expect(testInput).toHaveClass(\"phx-change-loading\");\n  await expect(testForm).toHaveAttribute(\"data-phx-ref-loading\");\n  await expect(testForm).toHaveAttribute(\"data-phx-ref-src\");\n  await expect(testInput).toHaveAttribute(\"data-phx-ref-loading\");\n  await expect(testInput).toHaveAttribute(\"data-phx-ref-src\");\n  // now we ack the two change events\n  await ack(\"validate\");\n  // the form is still locked, therefore we still have 3 elements\n  await expect(page.locator(\"#form-stream li\")).toHaveCount(3);\n  await ack(\"validate\");\n  // on unlock, cloned stream items that are added on each phx-change are applied to DOM\n  await expect(page.locator(\"#form-stream li\")).toHaveCount(5);\n  // after clones are applied, the stream item hooks are mounted\n  // note that the form still awaits the submit ack, but it is not locked,\n  // therefore the updates from the phx-change are already applied\n  await expect(page.locator(\"#form-stream li\")).toHaveText([\n    \"*%{id: 1}pong\",\n    \"*%{id: 2}pong\",\n    \"*%{id: 3}pong\",\n    \"*%{id: 4}\",\n    \"*%{id: 5}\",\n  ]);\n  // still saving\n  await expect(submitBtn).toHaveText(\"Saving...\");\n  await expect(testForm).toHaveClass(\"phx-submit-loading\");\n  await expect(testInput).toHaveAttribute(\"readonly\", \"\");\n  await expect(submitBtn).toHaveClass(\"phx-submit-loading\");\n  await expect(testForm).toHaveAttribute(\"data-phx-ref-loading\");\n  await expect(testForm).toHaveAttribute(\"data-phx-ref-src\");\n  await expect(testInput).toHaveAttribute(\"data-phx-ref-loading\");\n  await expect(testInput).toHaveAttribute(\"data-phx-ref-src\");\n  await expect(submitBtn).toHaveAttribute(\"data-phx-ref-loading\");\n  await expect(submitBtn).toHaveAttribute(\"data-phx-ref-src\");\n  // now we ack the submit\n  await ack(\"save\");\n  // submit adds 1 more stream item and new hook is mounted\n  await expect(page.locator(\"#form-stream li\")).toHaveText([\n    \"*%{id: 1}pong\",\n    \"*%{id: 2}pong\",\n    \"*%{id: 3}pong\",\n    \"*%{id: 4}pong\",\n    \"*%{id: 5}pong\",\n    \"*%{id: 6}pong\",\n  ]);\n  await expect(submitBtn).toHaveText(\"Submit\");\n  await expect(submitBtn).toHaveAttribute(\"phx-disable-with\", \"Saving...\");\n  await expect(testForm).not.toHaveClass(\"phx-submit-loading\");\n  await expect(testInput).not.toHaveAttribute(\"readonly\");\n  await expect(submitBtn).not.toHaveClass(\"phx-submit-loading\");\n  await expect(testForm).not.toHaveAttribute(\"data-phx-ref\");\n  await expect(testForm).not.toHaveAttribute(\"data-phx-ref-src\");\n  await expect(testInput).not.toHaveAttribute(\"data-phx-ref\");\n  await expect(testInput).not.toHaveAttribute(\"data-phx-ref-src\");\n  await expect(submitBtn).not.toHaveAttribute(\"data-phx-ref\");\n  await expect(submitBtn).not.toHaveAttribute(\"data-phx-ref-src\");\n});\n\ntest(\"can dynamically add/remove inputs (ecto sort_param/drop_param)\", async ({\n  page,\n}) => {\n  await page.goto(\"/form/dynamic-inputs\");\n  await syncLV(page);\n\n  const formData = () =>\n    page\n      .locator(\"form\")\n      .evaluate((form) => Object.fromEntries(new FormData(form).entries()));\n\n  expect(await formData()).toEqual({\n    \"my_form[name]\": \"\",\n    \"my_form[users_drop][]\": \"\",\n  });\n\n  await page.locator(\"#my-form_name\").fill(\"Test\");\n  await page.getByRole(\"button\", { name: \"add more\" }).click();\n  await syncLV(page);\n\n  expect(await formData()).toEqual(\n    expect.objectContaining({\n      \"my_form[name]\": \"Test\",\n      \"my_form[users][0][name]\": \"\",\n    }),\n  );\n\n  await page.locator(\"#my-form_users_0_name\").fill(\"User A\");\n  await page.getByRole(\"button\", { name: \"add more\" }).click();\n  await syncLV(page);\n  await page.getByRole(\"button\", { name: \"add more\" }).click();\n  await syncLV(page);\n\n  await page.locator(\"#my-form_users_1_name\").fill(\"User B\");\n  await page.locator(\"#my-form_users_2_name\").fill(\"User C\");\n\n  expect(await formData()).toEqual(\n    expect.objectContaining({\n      \"my_form[name]\": \"Test\",\n      \"my_form[users_drop][]\": \"\",\n      \"my_form[users][0][name]\": \"User A\",\n      \"my_form[users][1][name]\": \"User B\",\n      \"my_form[users][2][name]\": \"User C\",\n    }),\n  );\n\n  // remove User B\n  await page.locator('button[name=\"my_form[users_drop][]\"][value=\"1\"]').click();\n  await syncLV(page);\n\n  expect(await formData()).toEqual(\n    expect.objectContaining({\n      \"my_form[name]\": \"Test\",\n      \"my_form[users_drop][]\": \"\",\n      \"my_form[users][0][name]\": \"User A\",\n      \"my_form[users][1][name]\": \"User C\",\n    }),\n  );\n});\n\ntest(\"can dynamically add/remove inputs using checkboxes\", async ({ page }) => {\n  await page.goto(\"/form/dynamic-inputs?checkboxes=1\");\n  await syncLV(page);\n\n  const formData = () =>\n    page\n      .locator(\"form\")\n      .evaluate((form) => Object.fromEntries(new FormData(form).entries()));\n\n  expect(await formData()).toEqual({\n    \"my_form[name]\": \"\",\n    \"my_form[users_drop][]\": \"\",\n  });\n\n  await page.locator(\"#my-form_name\").fill(\"Test\");\n  await page.locator(\"label\", { hasText: \"add more\" }).click();\n  await syncLV(page);\n\n  expect(await formData()).toEqual(\n    expect.objectContaining({\n      \"my_form[name]\": \"Test\",\n      \"my_form[users][0][name]\": \"\",\n    }),\n  );\n\n  await page.locator(\"#my-form_users_0_name\").fill(\"User A\");\n  await page.locator(\"label\", { hasText: \"add more\" }).click();\n  await page.locator(\"label\", { hasText: \"add more\" }).click();\n\n  await page.locator(\"#my-form_users_1_name\").fill(\"User B\");\n  await page.locator(\"#my-form_users_2_name\").fill(\"User C\");\n  await syncLV(page);\n\n  expect(await formData()).toEqual(\n    expect.objectContaining({\n      \"my_form[name]\": \"Test\",\n      \"my_form[users_drop][]\": \"\",\n      \"my_form[users][0][name]\": \"User A\",\n      \"my_form[users][1][name]\": \"User B\",\n      \"my_form[users][2][name]\": \"User C\",\n    }),\n  );\n\n  // remove User B\n  await page.locator('input[name=\"my_form[users_drop][]\"][value=\"1\"]').click();\n  await syncLV(page);\n\n  expect(await formData()).toEqual(\n    expect.objectContaining({\n      \"my_form[name]\": \"Test\",\n      \"my_form[users_drop][]\": \"\",\n      \"my_form[users][0][name]\": \"User A\",\n      \"my_form[users][1][name]\": \"User C\",\n    }),\n  );\n});\n\n// this happened from v1.0.17 when the sort_params field is nested in a fieldset\ntest(\"form recovery does not create duplicates of dynamically added fields\", async ({\n  page,\n}) => {\n  await page.goto(\"/form/dynamic-inputs\");\n  await syncLV(page);\n\n  const formData = () =>\n    page\n      .locator(\"form\")\n      .evaluate((form) => Object.fromEntries(new FormData(form).entries()));\n\n  expect(await formData()).toEqual({\n    \"my_form[name]\": \"\",\n    \"my_form[users_drop][]\": \"\",\n  });\n\n  await page.locator(\"#my-form_name\").fill(\"Test\");\n  await page.getByRole(\"button\", { name: \"add more\" }).click();\n  await syncLV(page);\n\n  expect(await formData()).toEqual(\n    expect.objectContaining({\n      \"my_form[name]\": \"Test\",\n      \"my_form[users][0][name]\": \"\",\n    }),\n  );\n\n  await page.evaluate(\n    () => new Promise((resolve) => window.liveSocket.disconnect(resolve)),\n  );\n\n  await page.evaluate(() => window.liveSocket.connect());\n  await syncLV(page);\n\n  const data = await formData();\n\n  expect(\"my_form[users][1][name]\" in data).toBe(false);\n});\n\n// phx-feedback-for was removed in LiveView 1.0, but we still test the shim applied in\n// test_helper.exs layout for backwards compatibility\ntest(\"phx-no-feedback is applied correctly for backwards-compatible-shims\", async ({\n  page,\n}) => {\n  await page.goto(\"/form/feedback\");\n  await syncLV(page);\n\n  await expect(page.locator(\"[phx-feedback-for=myfeedback]\")).toBeHidden();\n  await page.getByRole(\"button\", { name: \"+\" }).click();\n  await syncLV(page);\n  await expect(page.locator(\"[phx-feedback-for=myfeedback]\")).toBeHidden();\n  await expect(page.getByText(\"Validate count\")).toContainText(\"0\");\n\n  await page.locator(\"input[name=name]\").fill(\"Test\");\n  await syncLV(page);\n  await expect(page.locator(\"[phx-feedback-for=myfeedback]\")).toBeHidden();\n  await expect(page.getByText(\"Validate count\")).toContainText(\"1\");\n\n  await page.locator(\"input[name=myfeedback]\").fill(\"Test\");\n  await syncLV(page);\n  await expect(page.getByText(\"Validate count\")).toContainText(\"2\");\n  await expect(page.locator(\"[phx-feedback-for=myfeedback]\")).toBeVisible();\n\n  // feedback appears on submit\n  await page.reload();\n  await syncLV(page);\n  await expect(page.locator(\"[phx-feedback-for=myfeedback]\")).toBeHidden();\n\n  await page.getByRole(\"button\", { name: \"Submit\" }).click();\n  await syncLV(page);\n  await expect(page.getByText(\"Submit count\")).toContainText(\"1\");\n  await expect(page.locator(\"[phx-feedback-for=myfeedback]\")).toBeVisible();\n\n  // feedback hides on reset\n  await page.getByRole(\"button\", { name: \"Reset\" }).click();\n  await syncLV(page);\n  await expect(page.locator(\"[phx-feedback-for=myfeedback]\")).toBeHidden();\n\n  // can toggle feedback visibility\n  await page.reload();\n  await syncLV(page);\n  await expect(page.locator(\"[data-feedback-container]\")).toBeHidden();\n\n  await page.getByRole(\"button\", { name: \"Toggle feedback\" }).click();\n  await syncLV(page);\n  await expect(page.locator(\"[data-feedback-container]\")).toBeVisible();\n\n  await page.getByRole(\"button\", { name: \"Toggle feedback\" }).click();\n  await syncLV(page);\n  await expect(page.locator(\"[data-feedback-container]\")).toBeHidden();\n});\n\ntest(\"phx-no-unused-field on a form is applied correctly and no unused fields are sent\", async ({\n  page,\n}) => {\n  const webSocketEvents = [];\n\n  page.on(\"websocket\", (ws) => {\n    ws.on(\"framesent\", (event) =>\n      webSocketEvents.push({ type: \"sent\", payload: event.payload }),\n    );\n  });\n\n  await page.goto(\"/form?phx-no-unused-field-form\");\n  await syncLV(page);\n\n  await page.locator(\"input[name=b]\").fill(\"test\");\n  // blur, otherwise the input would not be morphed anyway\n  await page.locator(\"input[name=b]\").blur();\n  await syncLV(page);\n\n  // With phx-no-unused-field on the form, no _unused_ parameters should be sent\n  expect(webSocketEvents).toEqual(\n    expect.arrayContaining([\n      {\n        type: \"sent\",\n        payload: expect.stringMatching(/event.*a=foo&b=test&c=baz&d=foo/),\n      },\n    ]),\n  );\n\n  // Ensure no _unused_ parameters are present in any sent events\n  const sentEvents = webSocketEvents.filter((event) => event.type === \"sent\");\n  sentEvents.forEach((event) => {\n    expect(event.payload).not.toMatch(/_unused_/);\n  });\n});\n\ntest(\"phx-no-unused-field on an input is applied correctly and no unused field is sent for that specific input\", async ({\n  page,\n}) => {\n  const webSocketEvents = [];\n\n  page.on(\"websocket\", (ws) => {\n    ws.on(\"framesent\", (event) =>\n      webSocketEvents.push({ type: \"sent\", payload: event.payload }),\n    );\n  });\n\n  await page.goto(\"/form?phx-no-unused-field-input\");\n  await syncLV(page);\n\n  await page.locator(\"input[name=b]\").fill(\"test\");\n  // blur, otherwise the input would not be morphed anyway\n  await page.locator(\"input[name=b]\").blur();\n  await syncLV(page);\n\n  // Check that the form data and unused parameters are sent correctly\n  expect(webSocketEvents).toEqual(\n    expect.arrayContaining([\n      {\n        type: \"sent\",\n        payload: expect.stringMatching(\n          /event.*_unused_a=&a=foo&b=test&c=baz&_unused_d=&d=foo/,\n        ),\n      },\n    ]),\n  );\n\n  // Verify specific unused parameter behavior\n  const sentEvents = webSocketEvents.filter((event) => event.type === \"sent\");\n  const formDataEvent = sentEvents.find((event) =>\n    event.payload.includes(\"a=foo\"),\n  );\n\n  // a and d should have _unused_ parameters (untouched, no phx-no-unused-field)\n  expect(formDataEvent.payload).toMatch(/_unused_a=/);\n  expect(formDataEvent.payload).toMatch(/_unused_d=/);\n  // b and c should NOT have _unused_ parameters (b touched, c has phx-no-unused-field)\n  expect(formDataEvent.payload).not.toMatch(/_unused_b=/);\n  expect(formDataEvent.payload).not.toMatch(/_unused_c=/);\n});\n\nfunction formPayload(events) {\n  const event = events.find(\n    (e) => e.type === \"sent\" && e.payload.includes('\"event\":\"validate\"'),\n  );\n  const parsed = JSON.parse(event.payload);\n  return querystring.parse(parsed[4].value);\n}\n"
  },
  {
    "path": "test/e2e/tests/issues/2787.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\nconst selectOptions = (locator) =>\n  locator.evaluateAll((list) => list.map((option) => option.value));\n\ntest(\"select is properly cleared on submit\", async ({ page }) => {\n  await page.goto(\"/issues/2787\");\n  await syncLV(page);\n\n  const select1 = page.locator(\"#demo_select1\");\n  const select2 = page.locator(\"#demo_select2\");\n\n  // at the beginning, both selects are empty\n  await expect(select1).toHaveValue(\"\");\n  expect(await selectOptions(select1.locator(\"option\"))).toEqual([\n    \"\",\n    \"greetings\",\n    \"goodbyes\",\n  ]);\n  await expect(select2).toHaveValue(\"\");\n  expect(await selectOptions(select2.locator(\"option\"))).toEqual([\"\"]);\n\n  // now we select greetings in the first select\n  await select1.selectOption(\"greetings\");\n  await syncLV(page);\n  // now the second select should have some greeting options\n  expect(await selectOptions(select2.locator(\"option\"))).toEqual([\n    \"\",\n    \"hello\",\n    \"hallo\",\n    \"hei\",\n  ]);\n  await select2.selectOption(\"hei\");\n  await syncLV(page);\n\n  // now we submit the form\n  await page.locator(\"button\").click();\n\n  // now, both selects should be empty again (this was the bug in #2787)\n  await expect(select1).toHaveValue(\"\");\n  await expect(select2).toHaveValue(\"\");\n\n  // now we select goodbyes in the first select\n  await select1.selectOption(\"goodbyes\");\n  await syncLV(page);\n  expect(await selectOptions(select2.locator(\"option\"))).toEqual([\n    \"\",\n    \"goodbye\",\n    \"auf wiedersehen\",\n    \"ha det bra\",\n  ]);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/2965.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\nimport { randomBytes } from \"node:crypto\";\n\ntest(\"can upload files with custom chunk hook\", async ({ page }) => {\n  await page.goto(\"/issues/2965\");\n  await syncLV(page);\n\n  const files = [];\n  for (let i = 1; i <= 20; i++) {\n    files.push({\n      name: `file${i}.txt`,\n      mimeType: \"text/plain\",\n      // random 100 kb\n      buffer: randomBytes(100 * 1024),\n    });\n  }\n\n  await page.locator(\"#fileinput\").setInputFiles(files);\n  await syncLV(page);\n\n  // wait for uploads to finish\n  for (let i = 0; i < 20; i++) {\n    const row = page.locator(\"tbody tr\").nth(i);\n    await expect(row).toContainText(`file${i + 1}.txt`);\n    await expect(row.locator(\"progress\")).toHaveAttribute(\"value\", \"100\");\n  }\n\n  // all uploads are finished!\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3026.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\ntest(\"LiveComponent is re-rendered when racing destory\", async ({ page }) => {\n  const errors = [];\n  page.on(\"pageerror\", (err) => {\n    errors.push(err);\n  });\n\n  await page.goto(\"/issues/3026\");\n  await syncLV(page);\n\n  await expect(page.locator(\"input[name='name']\")).toHaveValue(\"John\");\n\n  // submitting the form unloads the LiveComponent, but it is re-added shortly after\n  await page.locator(\"button\").click();\n  await syncLV(page);\n\n  // the form elements inside the LC should still be visible\n  await expect(page.locator(\"input[name='name']\")).toBeVisible();\n  await expect(page.locator(\"input[name='name']\")).toHaveValue(\"John\");\n\n  // quickly toggle status\n  for (let i = 0; i < 5; i++) {\n    await page.locator(\"select[name='status']\").selectOption(\"connecting\");\n    await syncLV(page);\n    // now the form is not rendered as status is connecting\n    await expect(page.locator(\"input[name='name']\")).toBeHidden();\n\n    // set back to loading\n    await page.locator(\"select[name='status']\").selectOption(\"loaded\");\n    await syncLV(page);\n    // now the form is not rendered as status is connecting\n    await expect(page.locator(\"input[name='name']\")).toBeVisible();\n  }\n\n  // no js errors should be thrown\n  expect(errors).toEqual([]);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3040.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\ntest(\"click-away does not fire when triggering form submit\", async ({\n  page,\n}) => {\n  await page.goto(\"/issues/3040\");\n  await syncLV(page);\n\n  await page.getByRole(\"link\", { name: \"Add new\" }).click();\n  await syncLV(page);\n\n  const modal = page.locator(\"#my-modal-container\");\n  await expect(modal).toBeVisible();\n\n  // focusFirst should have focused the input\n  await expect(page.locator(\"input[name='name']\")).toBeFocused();\n\n  // submit the form\n  await page.keyboard.press(\"Enter\");\n  await syncLV(page);\n\n  await expect(page.locator(\"form\")).toHaveText(\"Form was submitted!\");\n  await expect(modal).toBeVisible();\n\n  // now click outside\n  await page.mouse.click(0, 0);\n  await syncLV(page);\n\n  await expect(modal).toBeHidden();\n});\n\n// see also https://github.com/phoenixframework/phoenix_live_view/issues/1920\ntest(\"does not close modal when moving mouse outside while held down\", async ({\n  page,\n}) => {\n  await page.goto(\"/issues/3040\");\n  await syncLV(page);\n\n  await page.getByRole(\"link\", { name: \"Add new\" }).click();\n  await syncLV(page);\n\n  const modal = page.locator(\"#my-modal-container\");\n  await expect(modal).toBeVisible();\n\n  await expect(page.locator(\"input[name='name']\")).toBeFocused();\n  await page.locator(\"input[name='name']\").fill(\"test\");\n\n  // we move the mouse inside the input field and then drag it outside\n  // while holding the mouse button down\n  await page.mouse.move(434, 350);\n  await page.mouse.down();\n  await page.mouse.move(143, 350);\n  await page.mouse.up();\n\n  // we expect the modal to still be visible because the mousedown happened\n  // inside, not triggering phx-click-away\n  await expect(modal).toBeVisible();\n  await page.keyboard.press(\"Backspace\");\n\n  await expect(page.locator(\"input[name='name']\")).toHaveValue(\"\");\n  await expect(modal).toBeVisible();\n\n  // close modal with escape\n  await page.keyboard.press(\"Escape\");\n  await expect(modal).toBeHidden();\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3047.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\nconst listItems = async (page) =>\n  page\n    .locator('[phx-update=\"stream\"] > span')\n    .evaluateAll((list) => list.map((el) => el.id));\n\ntest(\"streams are not cleared in sticky live views\", async ({ page }) => {\n  await page.goto(\"/issues/3047/a\");\n  await syncLV(page);\n  await expect(page.locator(\"#page\")).toContainText(\"Page A\");\n\n  expect(await listItems(page)).toEqual([\n    \"items-1\",\n    \"items-2\",\n    \"items-3\",\n    \"items-4\",\n    \"items-5\",\n    \"items-6\",\n    \"items-7\",\n    \"items-8\",\n    \"items-9\",\n    \"items-10\",\n  ]);\n\n  await page.getByRole(\"button\", { name: \"Reset\" }).click();\n  await syncLV(page);\n  expect(await listItems(page)).toEqual([\n    \"items-5\",\n    \"items-6\",\n    \"items-7\",\n    \"items-8\",\n    \"items-9\",\n    \"items-10\",\n    \"items-11\",\n    \"items-12\",\n    \"items-13\",\n    \"items-14\",\n    \"items-15\",\n  ]);\n\n  await page.getByRole(\"link\", { name: \"Page B\" }).click();\n  await syncLV(page);\n\n  // stream items should still be visible\n  await expect(page.locator(\"#page\")).toContainText(\"Page B\");\n  expect(await listItems(page)).toEqual([\n    \"items-5\",\n    \"items-6\",\n    \"items-7\",\n    \"items-8\",\n    \"items-9\",\n    \"items-10\",\n    \"items-11\",\n    \"items-12\",\n    \"items-13\",\n    \"items-14\",\n    \"items-15\",\n  ]);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3083.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV, evalLV } from \"../../utils\";\n\ntest(\"select multiple handles option updates properly\", async ({ page }) => {\n  await page.goto(\"/issues/3083?auto=false\");\n  await syncLV(page);\n\n  await expect(page.locator(\"select\")).toHaveValues([]);\n\n  await evalLV(page, \"send(self(), {:select, [1,2]}); nil\");\n  await expect(page.locator(\"select\")).toHaveValues([\"1\", \"2\"]);\n  await evalLV(page, \"send(self(), {:select, [2,3]}); nil\");\n  await expect(page.locator(\"select\")).toHaveValues([\"2\", \"3\"]);\n\n  // now focus the select by interacting with it\n  await page.locator(\"select\").click({ position: { x: 1, y: 1 } });\n  await expect(page.locator(\"select\")).toHaveValues([\"1\"]);\n  await evalLV(page, \"send(self(), {:select, [1,2]}); nil\");\n  // because the select is focused, we do not expect the values to change\n  await expect(page.locator(\"select\")).toHaveValues([\"1\"]);\n  // now blur the select by clicking on the body\n  await page.locator(\"body\").click();\n  await expect(page.locator(\"select\")).toHaveValues([\"1\"]);\n  // now update the selected values again\n  await evalLV(page, \"send(self(), {:select, [3,4]}); nil\");\n  // we had a bug here, where the select was focused, despite the blur\n  await expect(page.locator(\"select\")).not.toBeFocused();\n  await expect(page.locator(\"select\")).toHaveValues([\"3\", \"4\"]);\n  await page.waitForTimeout(1000);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3107.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\ntest(\"keeps value when updating select\", async ({ page }) => {\n  await page.goto(\"/issues/3107\");\n  await syncLV(page);\n\n  await expect(page.locator(\"select\")).toHaveValue(\"ONE\");\n  // focus the element and change the value, like a user would\n  await page.locator(\"select\").focus();\n  await page.locator(\"select\").selectOption(\"TWO\");\n  await syncLV(page);\n  await expect(page.locator(\"select\")).toHaveValue(\"TWO\");\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3117.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\ntest(\"LiveComponent with static FC root is not reset\", async ({ page }) => {\n  const errors = [];\n  page.on(\"pageerror\", (err) => errors.push(err));\n\n  await page.goto(\"/issues/3117\");\n  await syncLV(page);\n\n  // clicking the button performs a live navigation\n  await page.locator(\"#navigate\").click();\n  await syncLV(page);\n\n  // the FC root should still be visible and not empty/skipped\n  await expect(page.locator(\"#row-1 .static\")).toBeVisible();\n  await expect(page.locator(\"#row-2 .static\")).toBeVisible();\n  await expect(page.locator(\"#row-1 .static\")).toHaveText(\"static content\");\n  await expect(page.locator(\"#row-2 .static\")).toHaveText(\"static content\");\n\n  // no js errors should be thrown\n  expect(errors).toEqual([]);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3169.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\nconst inputVals = async (page) => {\n  return page\n    .locator('input[type=\"text\"]')\n    .evaluateAll((list) => list.map((i) => i.value));\n};\n\ntest(\"updates which add cids back on page are properly magic id change tracked\", async ({\n  page,\n}) => {\n  await page.goto(\"/issues/3169\");\n  await syncLV(page);\n\n  await page.locator(\"#select-a\").click();\n  await syncLV(page);\n  await expect(page.locator(\"body\")).toContainText(\"FormColumn (c3)\");\n  expect(await inputVals(page)).toEqual([\"Record a\", \"Record a\", \"Record a\"]);\n\n  await page.locator(\"#select-b\").click();\n  await syncLV(page);\n  await expect(page.locator(\"body\")).toContainText(\"FormColumn (c3)\");\n  expect(await inputVals(page)).toEqual([\"Record b\", \"Record b\", \"Record b\"]);\n\n  await page.locator(\"#select-z\").click();\n  await syncLV(page);\n  await expect(page.locator(\"body\")).toContainText(\"FormColumn (c3)\");\n  expect(await inputVals(page)).toEqual([\"Record z\", \"Record z\", \"Record z\"]);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3194.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\ntest(\"does not send event to wrong LV when submitting form with debounce blur\", async ({\n  page,\n}) => {\n  const logs = [];\n  page.on(\"console\", (e) => logs.push(e.text()));\n\n  await page.goto(\"/issues/3194\");\n  await syncLV(page);\n\n  await page.locator(\"input\").focus();\n  await page.keyboard.type(\"hello\");\n  await page.keyboard.press(\"Enter\");\n  await expect(page).toHaveURL(\"/issues/3194/other\");\n\n  // give it some time for old events to reach the new LV\n  // (this is the failure case!)\n  await page.waitForTimeout(50);\n\n  // we navigated to another LV\n  expect(logs).toEqual(\n    expect.arrayContaining([\n      expect.stringMatching(\n        \"destroyed: the child has been removed from the parent\",\n      ),\n    ]),\n  );\n  // it should not have crashed\n  expect(logs).not.toEqual(\n    expect.arrayContaining([expect.stringMatching(\"view crashed\")]),\n  );\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3200.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3200\ntest(\"phx-target='selector' is used correctly for form recovery\", async ({\n  page,\n}) => {\n  const errors = [];\n  page.on(\"pageerror\", (err) => errors.push(err));\n\n  await page.goto(\"/issues/3200/settings\");\n  await syncLV(page);\n\n  await page.getByRole(\"button\", { name: \"Messages\" }).click();\n  await syncLV(page);\n  await expect(page).toHaveURL(\"/issues/3200/messages\");\n\n  await page.locator(\"#new_message_input\").fill(\"Hello\");\n  await syncLV(page);\n\n  await page.evaluate(\n    () => new Promise((resolve) => window.liveSocket.disconnect(resolve)),\n  );\n  await expect(page.locator(\".phx-loading\")).toHaveCount(1);\n\n  await page.evaluate(() => window.liveSocket.connect());\n  await syncLV(page);\n\n  await expect(page.locator(\"#new_message_input\")).toHaveValue(\"Hello\");\n  expect(errors).toEqual([]);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3378.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\ntest(\"can rejoin with nested streams without errors\", async ({ page }) => {\n  const errors = [];\n  page.on(\"pageerror\", (err) => {\n    errors.push(err);\n  });\n\n  await page.goto(\"/issues/3378\");\n  await syncLV(page);\n\n  await expect(page.locator(\"#notifications\")).toContainText(\"big\");\n  await page.evaluate(\n    () => new Promise((resolve) => window.liveSocket.disconnect(resolve)),\n  );\n\n  await page.evaluate(() => window.liveSocket.connect());\n  await syncLV(page);\n\n  // no js errors should be thrown\n  expect(errors).toEqual([]);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3448.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3448\ntest(\"focus is handled correctly when patching locked form\", async ({\n  page,\n}) => {\n  await page.goto(\"/issues/3448\");\n  await syncLV(page);\n\n  await page.evaluate(() => window.liveSocket.enableLatencySim(500));\n\n  await page.locator(\"input[type=checkbox]\").first().check();\n  await expect(page.locator(\"input#search\")).toBeFocused();\n  await syncLV(page);\n\n  // after the patch is applied, the input should still be focused\n  await expect(page.locator(\"input#search\")).toBeFocused();\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3496.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3496\ntest(\"hook is initialized properly when reusing id between sticky and non sticky LiveViews\", async ({\n  page,\n}) => {\n  const logs = [];\n  page.on(\"console\", (e) => logs.push(e.text()));\n  const errors = [];\n  page.on(\"pageerror\", (err) => errors.push(err));\n\n  await page.goto(\"/issues/3496/a\");\n  await syncLV(page);\n\n  await page.getByRole(\"link\", { name: \"Go to page B\" }).click();\n  await syncLV(page);\n\n  expect(logs.filter((e) => e.includes(\"Hook mounted!\"))).toHaveLength(2);\n  expect(logs).not.toEqual(\n    expect.arrayContaining([\n      expect.stringMatching(\"no hook found for custom element\"),\n    ]),\n  );\n  // no uncaught exceptions\n  expect(errors).toEqual([]);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3529.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\nconst pageText = async (page) =>\n  await page.evaluate(() => document.querySelector(\"h1\").innerText);\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3529\n// https://github.com/phoenixframework/phoenix_live_view/pull/3625\ntest(\"forward and backward navigation is handled properly (replaceRootHistory)\", async ({\n  page,\n}) => {\n  await page.goto(\"/issues/3529\");\n  await syncLV(page);\n\n  let text = await pageText(page);\n  await page.getByRole(\"link\", { name: \"Navigate\" }).click();\n  await syncLV(page);\n\n  // navigate remounts and changes the text\n  expect(await pageText(page)).not.toBe(text);\n  text = await pageText(page);\n\n  await page.getByRole(\"link\", { name: \"Patch\" }).click();\n  await syncLV(page);\n  // patch does not remount\n  expect(await pageText(page)).toBe(text);\n\n  // now we go back (should be patch again)\n  await page.goBack();\n  await syncLV(page);\n  expect(await pageText(page)).toBe(text);\n\n  // and then we back to the initial page and use back/forward\n  // this should be a navigate -> remount!\n  await page.goBack();\n  await syncLV(page);\n  expect(await pageText(page)).not.toBe(text);\n\n  // navigate\n  await page.goForward();\n  await syncLV(page);\n  text = await pageText(page);\n\n  // now back again (navigate)\n  await page.goBack();\n  await syncLV(page);\n  expect(await pageText(page)).not.toBe(text);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3530.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3530\ntest(\"hook is initialized properly when using a stream of nested LiveViews\", async ({\n  page,\n}) => {\n  let logs = [];\n  page.on(\"console\", (e) => logs.push(e.text()));\n  const errors = [];\n  page.on(\"pageerror\", (err) => errors.push(err));\n\n  await page.goto(\"/issues/3530\");\n  await syncLV(page);\n\n  expect(errors).toEqual([]);\n  expect(logs.filter((e) => e.includes(\"item-1 mounted\"))).toHaveLength(1);\n  expect(logs.filter((e) => e.includes(\"item-2 mounted\"))).toHaveLength(1);\n  expect(logs.filter((e) => e.includes(\"item-3 mounted\"))).toHaveLength(1);\n  logs = [];\n\n  await page.getByRole(\"link\", { name: \"patch a\" }).click();\n  await syncLV(page);\n\n  expect(errors).toEqual([]);\n  expect(logs.filter((e) => e.includes(\"item-2 destroyed\"))).toHaveLength(1);\n  expect(logs.filter((e) => e.includes(\"item-1 destroyed\"))).toHaveLength(0);\n  expect(logs.filter((e) => e.includes(\"item-3 destroyed\"))).toHaveLength(0);\n  logs = [];\n\n  await page.getByRole(\"link\", { name: \"patch b\" }).click();\n  await syncLV(page);\n\n  expect(errors).toEqual([]);\n  expect(logs.filter((e) => e.includes(\"item-1 destroyed\"))).toHaveLength(1);\n  expect(logs.filter((e) => e.includes(\"item-2 destroyed\"))).toHaveLength(0);\n  expect(logs.filter((e) => e.includes(\"item-3 destroyed\"))).toHaveLength(0);\n  expect(logs.filter((e) => e.includes(\"item-2 mounted\"))).toHaveLength(1);\n  logs = [];\n\n  await page.locator(\"div[phx-click=inc]\").click();\n  await syncLV(page);\n  expect(logs.filter((e) => e.includes(\"item-4 mounted\"))).toHaveLength(1);\n\n  expect(logs).not.toEqual(\n    expect.arrayContaining([\n      expect.stringMatching(\"no hook found for custom element\"),\n    ]),\n  );\n  // no uncaught exceptions\n  expect(errors).toEqual([]);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3612.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3612\ntest(\"sticky LiveView stays connected when using push_navigate\", async ({\n  page,\n}) => {\n  await page.goto(\"/issues/3612/a\");\n  await syncLV(page);\n  await expect(page.locator(\"h1\")).toHaveText(\"Page A\");\n  await page.getByRole(\"link\", { name: \"Go to page B\" }).click();\n  await syncLV(page);\n  await expect(page.locator(\"h1\")).toHaveText(\"Page B\");\n  await page.getByRole(\"link\", { name: \"Go to page A\" }).click();\n  await syncLV(page);\n  await expect(page.locator(\"h1\")).toHaveText(\"Page A\");\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3636.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3636\ntest(\"focus_wrap - focuses first element when entering focus from outside\", async ({\n  page,\n  browserName,\n}) => {\n  // skip if webkit, since it doesn't have tab focus enabled by default\n  if (browserName === \"webkit\") {\n    test.skip();\n  }\n  await page.goto(\"/issues/3636\");\n  await syncLV(page);\n  // put focus next to the third button\n  await page.mouse.click(250, 37.5);\n  await page.keyboard.press(\"Tab\");\n  await expect(page.getByRole(\"button\", { name: \"One\" })).toBeFocused();\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3647.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3647\ntest(\"upload works when input event follows immediately afterwards\", async ({\n  page,\n}) => {\n  await page.goto(\"/issues/3647\");\n  await syncLV(page);\n\n  await expect(page.locator(\"ul li\")).toHaveCount(0);\n  await expect(page.locator('input[name=\"user[name]\"]')).toHaveValue(\"\");\n\n  await page.getByRole(\"button\", { name: \"Upload then Input\" }).click();\n  await syncLV(page);\n\n  await expect(page.locator(\"ul li\")).toHaveCount(1);\n  await expect(page.locator('input[name=\"user[name]\"]')).toHaveValue(\"0\");\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3651.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3651\ntest(\"locked hook with dynamic id is properly cleared\", async ({ page }) => {\n  await page.goto(\"/issues/3651\");\n  await syncLV(page);\n\n  await expect(page.locator(\"#notice\")).toBeHidden();\n\n  // we want to wait for some events to have been pushed\n  await page.waitForTimeout(100);\n  expect(\n    await page.evaluate(() =>\n      parseInt(document.querySelector(\"#total\").textContent),\n    ),\n  ).toBeLessThanOrEqual(50);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3656.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV, attributeMutations } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3656\ntest(\"phx-click-loading is removed from links in sticky LiveViews\", async ({\n  page,\n}) => {\n  await page.goto(\"/issues/3656\");\n  await syncLV(page);\n\n  const changes = attributeMutations(page, \"nav a\");\n\n  const link = page.getByRole(\"link\", { name: \"Link 1\" });\n  await link.click();\n\n  await syncLV(page);\n  await expect(link).not.toHaveClass(\"phx-click-loading\");\n\n  expect(await changes()).toEqual(\n    expect.arrayContaining([\n      { attr: \"class\", oldValue: null, newValue: \"phx-click-loading\" },\n      { attr: \"class\", oldValue: \"phx-click-loading\", newValue: \"\" },\n    ]),\n  );\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3658.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3658\ntest(\"phx-remove elements inside sticky LiveViews are not removed when navigating\", async ({\n  page,\n}) => {\n  await page.goto(\"/issues/3658\");\n  await syncLV(page);\n\n  await expect(page.locator(\"#foo\")).toBeVisible();\n  await page.getByRole(\"link\", { name: \"Link 1\" }).click();\n\n  await syncLV(page);\n  // the bug would remove the element\n  await expect(page.locator(\"#foo\")).toBeVisible();\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3681.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3681\ntest(\"streams in nested LiveViews are not reset when they share the same stream ref\", async ({\n  page,\n  request,\n}) => {\n  // this was a separate bug where child LiveViews accidentally shared the parent streams\n  // check that the initial render does not contain the messages-4 element twice\n  expect(\n    (await (await request.get(\"/issues/3681/away\")).text()).match(/messages-4/g)\n      .length,\n  ).toBe(1);\n\n  await page.goto(\"/issues/3681\");\n  await syncLV(page);\n\n  await expect(page.locator(\"#msgs-sticky > div\")).toHaveCount(3);\n\n  await page\n    .getByRole(\"link\", { name: \"Go to a different LV with a (funcky) stream\" })\n    .click();\n  await syncLV(page);\n  await expect(page.locator(\"#msgs-sticky > div\")).toHaveCount(3);\n\n  await page\n    .getByRole(\"link\", {\n      name: \"Go back to (the now borked) LV without a stream\",\n    })\n    .click();\n  await syncLV(page);\n  await expect(page.locator(\"#msgs-sticky > div\")).toHaveCount(3);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3684.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3684\ntest(\"nested clones are correctly applied\", async ({ page }) => {\n  await page.goto(\"/issues/3684\");\n  await syncLV(page);\n\n  await expect(page.locator(\"#dewey\")).not.toHaveAttribute(\"checked\");\n\n  await page.locator(\"#dewey\").click();\n  await syncLV(page);\n\n  await expect(page.locator(\"#dewey\")).toHaveAttribute(\"checked\");\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3686.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3686\ntest(\"flash is copied across fallback redirect\", async ({ page }) => {\n  await page.goto(\"/issues/3686/a\");\n  await syncLV(page);\n  await expect(page.locator(\"#flash\")).toHaveText(\"%{}\");\n\n  await page.getByRole(\"button\", { name: \"To B\" }).click();\n  await syncLV(page);\n  await expect(page.locator(\"#flash\")).toContainText(\"Flash from A\");\n\n  await page.getByRole(\"button\", { name: \"To C\" }).click();\n  await syncLV(page);\n  await expect(page.locator(\"#flash\")).toContainText(\"Flash from B\");\n\n  await page.getByRole(\"button\", { name: \"To A\" }).click();\n  await syncLV(page);\n  await expect(page.locator(\"#flash\")).toContainText(\"Flash from C\");\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3709.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3709\ntest(\"pendingDiffs don't race with navigation\", async ({ page }) => {\n  const logs = [];\n  page.on(\"console\", (e) => logs.push(e.text()));\n  const errors = [];\n  page.on(\"pageerror\", (err) => errors.push(err));\n\n  await page.goto(\"/issues/3709/1\");\n  await syncLV(page);\n  await expect(page.locator(\"body\")).toContainText(\"id: 1\");\n\n  await page.getByRole(\"button\", { name: \"Break Stuff\" }).click();\n  await syncLV(page);\n\n  expect(logs).not.toEqual(\n    expect.arrayContaining([\n      expect.stringMatching(\n        \"Cannot read properties of undefined (reading 's')\",\n      ),\n    ]),\n  );\n\n  await page.getByRole(\"link\", { name: \"Link 5\" }).click();\n  await syncLV(page);\n  await expect(page.locator(\"body\")).toContainText(\"id: 5\");\n\n  expect(logs).not.toEqual(\n    expect.arrayContaining([\n      expect.stringMatching(\n        \"Cannot set properties of undefined (setting 'newRender')\",\n      ),\n    ]),\n  );\n\n  // no uncaught exceptions\n  expect(errors).toEqual([]);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3719.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3719\ntest(\"target is properly decoded\", async ({ page }) => {\n  const logs = [];\n  page.on(\"console\", (e) => logs.push(e.text()));\n\n  await page.goto(\"/issues/3719\");\n  await syncLV(page);\n  await page.locator(\"#a\").fill(\"foo\");\n  await syncLV(page);\n  await expect(page.locator(\"#target\")).toHaveText('[\"foo\"]');\n\n  await page.locator(\"#b\").fill(\"foo\");\n  await syncLV(page);\n  await expect(page.locator(\"#target\")).toHaveText('[\"foo\", \"bar\"]');\n\n  expect(logs).not.toEqual(\n    expect.arrayContaining([expect.stringMatching(\"view crashed\")]),\n  );\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3814.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3814\ntest(\"submitter is sent when using phx-trigger-action\", async ({ page }) => {\n  await page.goto(\"/issues/3814\");\n  await syncLV(page);\n\n  await page.locator(\"button\").click();\n  await expect(page.locator(\"body\")).toContainText(\n    '\"i-am-the-submitter\":\"submitter-value\"',\n  );\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3819.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3819\ntest(\"form recovery aborts early when form is empty\", async ({ page }) => {\n  await page.goto(\"/issues/3819\");\n  await syncLV(page);\n\n  await page.evaluate(\n    () => new Promise((resolve) => window.liveSocket.disconnect(resolve)),\n  );\n  await expect(page.locator(\".phx-loading\")).toHaveCount(1);\n  await page.evaluate(() => {\n    window.addEventListener(\"phx:page-loading-stop\", () => {\n      window.liveSocket.js().push(window.liveSocket.main.el, \"reconnected\");\n    });\n    window.liveSocket.connect();\n  });\n\n  await expect(page.locator(\".phx-loading\")).toHaveCount(0);\n  await expect(page.locator(\"#reconnected\")).toBeVisible();\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3919.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3919\ntest(\"attribute defaults are properly considered as changed\", async ({\n  page,\n}) => {\n  await page.goto(\"/issues/3919\");\n  await syncLV(page);\n\n  const styledDiv = page.locator(\"div[style]\");\n\n  await expect(styledDiv).toContainText(\"No red\");\n  await expect(styledDiv).not.toHaveAttribute(\n    \"style\",\n    \"background-color: red;\",\n  );\n  await page.getByRole(\"button\", { name: \"toggle\" }).click();\n  await syncLV(page);\n\n  await expect(styledDiv).not.toContainText(\"No red\");\n  await expect(styledDiv).toHaveAttribute(\"style\", \"background-color: red;\");\n  await page.getByRole(\"button\", { name: \"toggle\" }).click();\n  await syncLV(page);\n\n  // bug: previously, the red background remained\n  await expect(styledDiv).toContainText(\"No red\");\n  await expect(styledDiv).not.toHaveAttribute(\n    \"style\",\n    \"background-color: red;\",\n  );\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3931.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3931\ntest(\"dynamic attributes reset __changed__ and properly re-render\", async ({\n  page,\n}) => {\n  let webSocketEvents = [];\n  page.on(\"websocket\", (ws) => {\n    ws.on(\"framesent\", (event) =>\n      webSocketEvents.push({ type: \"sent\", payload: event.payload }),\n    );\n    ws.on(\"framereceived\", (event) =>\n      webSocketEvents.push({ type: \"received\", payload: event.payload }),\n    );\n    ws.on(\"close\", () => webSocketEvents.push({ type: \"close\" }));\n  });\n\n  await page.goto(\"/issues/3931\");\n  await syncLV(page);\n\n  // it should be updated asynchronously\n  await expect(page.locator(\"#async\")).toContainText(\n    \"This was loaded asynchronously!\",\n  );\n\n  expect(webSocketEvents).toEqual(\n    expect.arrayContaining([\n      { type: \"sent\", payload: expect.stringContaining(\"phx_join\") },\n      { type: \"received\", payload: expect.stringContaining(\"phx_reply\") },\n      { type: \"received\", payload: expect.stringContaining(\"diff\") },\n    ]),\n  );\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3941.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3941\ntest(\"component-only patch in locked tree works\", async ({ page }) => {\n  await page.goto(\"/issues/3941\");\n  await syncLV(page);\n\n  // the bug was that, because the parent container was locked,\n  // the component only patch was applied and later on the (stale) locked\n  // tree was applied, erasing the patch\n  await expect(page.locator(\"#Item_1\")).toContainText(\"I AM LOADED\");\n  await expect(page.locator(\"#Item_2\")).toContainText(\"I AM LOADED\");\n\n  await page.locator(\"#select-Item_1\").uncheck();\n  await page.locator(\"#select-Item_2\").uncheck();\n\n  await expect(page.locator(\"#Item_1\")).toHaveCount(0);\n  await expect(page.locator(\"#Item_2\")).toHaveCount(0);\n\n  await page.locator(\"#select-Item_1\").check();\n  await expect(page.locator(\"#Item_1\")).toContainText(\"I AM LOADED\");\n  await expect(page.locator(\"#Item_2\")).toHaveCount(0);\n\n  await page.locator(\"#select-Item_2\").check();\n  await expect(page.locator(\"#Item_1\")).toContainText(\"I AM LOADED\");\n  await expect(page.locator(\"#Item_2\")).toContainText(\"I AM LOADED\");\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3953.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3953\ntest(\"component destroy messages respect the parent\", async ({ page }) => {\n  await page.goto(\"/issues/3953\");\n  await syncLV(page);\n  await expect(\n    page.locator(\"#nested_view [data-phx-component='1']\"),\n  ).toHaveCount(0);\n\n  // the first render works fine\n  await page.getByRole(\"button\", { name: \"Show\" }).click();\n  await syncLV(page);\n  await expect(\n    page.locator(\"#nested_view [data-phx-component='1']\"),\n  ).toHaveCount(1);\n\n  // the bug was that a cids_destroyed message was sent to the parent view\n  await page.getByRole(\"button\", { name: \"Show\" }).click();\n  await syncLV(page);\n  await expect(\n    page.locator(\"#nested_view [data-phx-component='1']\"),\n  ).toHaveCount(0);\n\n  // so this failed, as CID 1 was not found when rendering\n  await page.getByRole(\"button\", { name: \"Show\" }).click();\n  await syncLV(page);\n  await expect(\n    page.locator(\"#nested_view [data-phx-component='1']\"),\n  ).toHaveCount(1);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/3979.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3979\ntest(\"components destroyed check works properly\", async ({ page }) => {\n  const errors = [];\n  page.on(\"pageerror\", (err) => errors.push(err));\n\n  await page.goto(\"/issues/3979\");\n  await syncLV(page);\n\n  const bumpBtn = page.getByRole(\"button\", { name: \"Bump ID (and counter)\" });\n  for (let i = 0; i < 10; i++) {\n    await bumpBtn.click();\n    await syncLV(page);\n  }\n\n  for (let i = 0; i < 10; i++) {\n    await expect(page.locator(`[data-phx-component=\"${i + 1}\"]`)).toHaveText(\n      \"10\",\n    );\n  }\n\n  expect(errors).toEqual([]);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/4027.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/4027\nfor (let c of [\"first\", \"second\"]) {\n  test(`keyed comprehensions are merged properly in LiveComponents - case ${c}`, async ({\n    page,\n  }) => {\n    const errors = [];\n    page.on(\"pageerror\", (err) => errors.push(err));\n    await page.goto(`/issues/4027?case=${c}`);\n    await syncLV(page);\n\n    await page.getByRole(\"button\", { name: \"Load data\" }).click();\n    await expect(page.locator(\"#result p\")).toHaveCount(3);\n\n    await page.getByRole(\"button\", { name: \"Remove first entry\" }).click();\n    await expect(page.locator(\"#result p\")).toHaveCount(2);\n\n    await expect(page.locator(\"#result\")).not.toContainText(\"First\");\n    await expect(page.locator(\"#result\")).toContainText(\"Second\");\n    await expect(page.locator(\"#result\")).toContainText(\"Third\");\n\n    expect(errors).toEqual([]);\n  });\n}\n"
  },
  {
    "path": "test/e2e/tests/issues/4066.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/4066\ntest(\"events for disconnected elements are ignored\", async ({ page }) => {\n  // The test triggers an input event that triggers an event after a delay\n  // and before the delay fires, the element is removed by a button press.\n  // Previously, the event would bubble to the parent, crashing the LiveView.\n  await page.goto(\"/issues/4066?delay=100\");\n  await syncLV(page);\n\n  const renderTime = await page\n    .locator(\"#render-time\")\n    .evaluate((el) => el.innerText);\n\n  await page.locator(\"input\").fill(\"123\");\n  await page.locator(\"button\").click();\n  await syncLV(page);\n  await expect(page.locator(\"input\")).toBeHidden();\n\n  // The hook sets this attribute when the delay fires - we should not crash here\n  await expect(page.locator(\"body\")).toHaveAttribute(\"data-pushed\", \"yes\");\n\n  // We can show the input again\n  await page.locator(\"button\").click();\n  await syncLV(page);\n  await expect(page.locator(\"input\")).toBeVisible();\n\n  // LiveView did not remount\n  await expect(page.locator(\"#render-time\")).toHaveText(renderTime);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/4078.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/4078\n// live_file_input should respect changing assigns like disabled and class\n\ntest(\"live_file_input respects disabled attribute changes\", async ({\n  page,\n}) => {\n  await page.goto(\"/issues/4078\");\n  await syncLV(page);\n\n  const input = page.locator(\"#upload-form input[type='file']\");\n\n  // Initially disabled\n  await expect(input).toBeDisabled();\n\n  // Click to enable\n  await page.locator(\"#toggle-disabled\").click();\n  await syncLV(page);\n\n  // Should now be enabled\n  await expect(input).not.toBeDisabled();\n\n  // Click to disable again\n  await page.locator(\"#toggle-disabled\").click();\n  await syncLV(page);\n\n  // Should be disabled again\n  await expect(input).toBeDisabled();\n});\n\ntest(\"live_file_input respects class attribute changes\", async ({ page }) => {\n  await page.goto(\"/issues/4078\");\n  await syncLV(page);\n\n  const input = page.locator(\"#upload-form input[type='file']\");\n\n  // Initially has initial-class\n  await expect(input).toHaveClass(/initial-class/);\n\n  // Click to change class\n  await page.locator(\"#toggle-class\").click();\n  await syncLV(page);\n\n  // Should have updated-class\n  await expect(input).toHaveClass(/updated-class/);\n\n  // Click to change class back\n  await page.locator(\"#toggle-class\").click();\n  await syncLV(page);\n\n  // Should have initial-class again\n  await expect(input).toHaveClass(/initial-class/);\n});\n\ntest(\"live_file_input preserves files when attributes change\", async ({\n  page,\n}) => {\n  await page.goto(\"/issues/4078\");\n  await syncLV(page);\n\n  // First enable the input\n  await page.locator(\"#toggle-disabled\").click();\n  await syncLV(page);\n\n  const input = page.locator(\"#upload-form input[type='file']\");\n  await expect(input).not.toBeDisabled();\n\n  // Select a file\n  await input.setInputFiles({\n    name: \"test.txt\",\n    mimeType: \"text/plain\",\n    buffer: Buffer.from(\"test content\"),\n  });\n  await syncLV(page);\n\n  // Verify file is selected (entry should appear)\n  await expect(page.locator(\".upload-entry\")).toBeVisible();\n  await expect(page.locator(\".entry-name\")).toContainText(\"test.txt\");\n\n  // Change class attribute - file should remain selected\n  await page.locator(\"#toggle-class\").click();\n  await syncLV(page);\n\n  // Verify file is still selected after class change\n  await expect(page.locator(\".upload-entry\")).toBeVisible();\n  await expect(page.locator(\".entry-name\")).toContainText(\"test.txt\");\n\n  // Verify class was changed\n  await expect(input).toHaveClass(/updated-class/);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/4088.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/4088\ntest(\"locked LiveComponent container can be patched properly\", async ({\n  page,\n}) => {\n  const errors = [];\n  page.on(\"pageerror\", (err) => {\n    errors.push(err);\n  });\n\n  await page.goto(\"/issues/4088\");\n  await syncLV(page);\n\n  expect(errors).toHaveLength(0);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/4094.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/4094\ntest(\"no errors when handle_params redirects\", async ({ page }) => {\n  const jsErrors = [];\n  page.on(\"pageerror\", (error) => {\n    jsErrors.push(error.message);\n  });\n\n  await page.goto(\"/issues/4094\");\n  await syncLV(page);\n\n  // Clicking a link that redirects in handle_params would throw an exception\n  // on the client.\n  await page.click(\"a\");\n\n  await expect(page).toHaveURL(\"/navigation/a\");\n  expect(jsErrors).toHaveLength(0);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/4095.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/4095\ntest(\"events for disconnected elements are ignored\", async ({ page }) => {\n  await page.goto(\"/issues/4095\");\n  await syncLV(page);\n\n  await expect(page.locator(\"button\")).toBeVisible();\n  await page.evaluate(() => window.liveSocket.enableLatencySim(50));\n\n  await page.locator(\"input\").fill(\"1\");\n  await page.locator(\"input\").fill(\"12\");\n\n  await syncLV(page);\n\n  await expect(page.locator(\"button\")).toHaveText(\"Show?\");\n  await expect(page.locator(\"button\")).not.toHaveAttribute(\"data-phx-skip\");\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/4102.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/4102\ntest(\"debounce works for inputs outside of the form\", async ({ page }) => {\n  const errors = [];\n  page.on(\"pageerror\", (err) => {\n    errors.push(err);\n  });\n\n  await page.goto(\"/issues/4102\");\n  await syncLV(page);\n\n  await page.locator(\"input\").fill(\"123\");\n  await page.locator(\"button\").click();\n  await syncLV(page);\n\n  expect(errors).toHaveLength(0);\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/4107.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/4107\ntest(\"external form submission from teleported form is successful\", async ({\n  page,\n}) => {\n  await page.goto(\"/issues/4107\");\n  await syncLV(page);\n\n  await page.locator(\"button\").click();\n\n  // With the bug, the form would not be submitted, because\n  // the form element was not part of the DOM any more.\n  await expect(page).toHaveURL(\"/api/test\");\n});\n"
  },
  {
    "path": "test/e2e/tests/issues/4121.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/4121\ntest(\"stream teleported outside of LiveView can be reset\", async ({ page }) => {\n  await page.goto(\"/issues/4121\");\n  await syncLV(page);\n\n  expect(await streamElements(page, \"stream-in-lv\")).toEqual([\n    {\n      id: \"items-1\",\n      text: \"Item 1\",\n    },\n    { id: \"items-2\", text: \"Item 2\" },\n  ]);\n\n  await page.locator(\"button\").click();\n  await syncLV(page);\n\n  expect(await streamElements(page, \"stream-in-lv\")).toHaveLength(1);\n});\n\nconst streamElements = async (page, parent) => {\n  return await page.locator(`#${parent} > *`).evaluateAll((list) =>\n    list.map((el) => ({\n      id: el.id,\n      text: el.childNodes[0].nodeValue.trim(),\n    })),\n  );\n};\n"
  },
  {
    "path": "test/e2e/tests/issues/4147.spec.js",
    "content": "import { test, expect } from \"../../test-fixtures\";\nimport { syncLV } from \"../../utils\";\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/4147\ntest(\"hook outside of liveview does works when reconnecting\", async ({\n  page,\n}) => {\n  const logs = [];\n  page.on(\"console\", (msg) => {\n    logs.push(msg.text());\n  });\n\n  const errors = [];\n  page.on(\"pageerror\", (err) => {\n    errors.push(err);\n  });\n\n  await page.goto(\"/issues/4147\");\n  await syncLV(page);\n\n  await page.evaluate(\n    () => new Promise((resolve) => window.liveSocket.disconnect(resolve)),\n  );\n  await expect(page.locator(\".phx-loading\")).toHaveCount(1);\n\n  await page.evaluate(() => window.liveSocket.connect());\n  await syncLV(page);\n\n  expect(errors).toHaveLength(0);\n  // Hook was mounted once\n  expect(\n    logs.filter((log) => log.includes(\"HookOutside mounted\")),\n  ).toHaveLength(1);\n});\n"
  },
  {
    "path": "test/e2e/tests/js.spec.js",
    "content": "import { test, expect } from \"../test-fixtures\";\nimport { syncLV, attributeMutations } from \"../utils\";\n\ntest(\"toggle_attribute\", async ({ page }) => {\n  await page.goto(\"/js\");\n  await syncLV(page);\n\n  await expect(page.locator(\"#my-modal\")).toBeHidden();\n\n  let changes = attributeMutations(page, \"#my-modal\");\n  await page.getByRole(\"button\", { name: \"toggle modal\" }).click();\n  // wait for the transition time (set to 50)\n  await page.waitForTimeout(100);\n  expect(await changes()).toEqual(\n    expect.arrayContaining([\n      {\n        attr: \"style\",\n        oldValue: \"display: none;\",\n        newValue: \"display: block;\",\n      },\n      { attr: \"aria-expanded\", oldValue: \"false\", newValue: \"true\" },\n      { attr: \"open\", oldValue: null, newValue: \"true\" },\n      // chrome and firefox first transition from null to \"\" and then to \"fade-in\";\n      // safari goes straight from null to \"fade-in\", therefore we do not perform an exact match\n      expect.objectContaining({ attr: \"class\", newValue: \"fade-in\" }),\n      expect.objectContaining({ attr: \"class\", oldValue: \"fade-in\" }),\n    ]),\n  );\n  await expect(page.locator(\"#my-modal\")).not.toHaveClass(\"fade-in\");\n  await expect(page.locator(\"#my-modal\")).toHaveAttribute(\n    \"aria-expanded\",\n    \"true\",\n  );\n  await expect(page.locator(\"#my-modal\")).toHaveAttribute(\"open\", \"true\");\n  await expect(page.locator(\"#my-modal\")).toBeVisible();\n\n  changes = attributeMutations(page, \"#my-modal\");\n  await page.getByRole(\"button\", { name: \"toggle modal\" }).click();\n  // wait for the transition time (set to 50)\n  await page.waitForTimeout(100);\n  expect(await changes()).toEqual(\n    expect.arrayContaining([\n      {\n        attr: \"style\",\n        oldValue: \"display: block;\",\n        newValue: \"display: none;\",\n      },\n      { attr: \"aria-expanded\", oldValue: \"true\", newValue: \"false\" },\n      { attr: \"open\", oldValue: \"true\", newValue: null },\n      expect.objectContaining({ attr: \"class\", newValue: \"fade-out\" }),\n      expect.objectContaining({ attr: \"class\", oldValue: \"fade-out\" }),\n    ]),\n  );\n  await expect(page.locator(\"#my-modal\")).not.toHaveClass(\"fade-out\");\n  await expect(page.locator(\"#my-modal\")).toHaveAttribute(\n    \"aria-expanded\",\n    \"false\",\n  );\n  await expect(page.locator(\"#my-modal\")).not.toHaveAttribute(\"open\");\n  await expect(page.locator(\"#my-modal\")).toBeHidden();\n});\n\ntest(\"set and remove_attribute\", async ({ page }) => {\n  await page.goto(\"/js\");\n  await syncLV(page);\n\n  await expect(page.locator(\"#my-modal\")).toBeHidden();\n\n  let changes = attributeMutations(page, \"#my-modal\");\n  await page.getByRole(\"button\", { name: \"show modal\" }).click();\n  // wait for the transition time (set to 50)\n  await page.waitForTimeout(100);\n  expect(await changes()).toEqual(\n    expect.arrayContaining([\n      {\n        attr: \"style\",\n        oldValue: \"display: none;\",\n        newValue: \"display: block;\",\n      },\n      { attr: \"aria-expanded\", oldValue: \"false\", newValue: \"true\" },\n      { attr: \"open\", oldValue: null, newValue: \"true\" },\n      expect.objectContaining({ attr: \"class\", newValue: \"fade-in\" }),\n      expect.objectContaining({ attr: \"class\", oldValue: \"fade-in\" }),\n    ]),\n  );\n  await expect(page.locator(\"#my-modal\")).not.toHaveClass(\"fade-in\");\n  await expect(page.locator(\"#my-modal\")).toHaveAttribute(\n    \"aria-expanded\",\n    \"true\",\n  );\n  await expect(page.locator(\"#my-modal\")).toHaveAttribute(\"open\", \"true\");\n  await expect(page.locator(\"#my-modal\")).toBeVisible();\n\n  changes = attributeMutations(page, \"#my-modal\");\n  await page.getByRole(\"button\", { name: \"hide modal\" }).click();\n  // wait for the transition time (set to 50)\n  await page.waitForTimeout(100);\n  expect(await changes()).toEqual(\n    expect.arrayContaining([\n      {\n        attr: \"style\",\n        oldValue: \"display: block;\",\n        newValue: \"display: none;\",\n      },\n      { attr: \"aria-expanded\", oldValue: \"true\", newValue: \"false\" },\n      { attr: \"open\", oldValue: \"true\", newValue: null },\n      expect.objectContaining({ attr: \"class\", newValue: \"fade-out\" }),\n      expect.objectContaining({ attr: \"class\", oldValue: \"fade-out\" }),\n    ]),\n  );\n  await expect(page.locator(\"#my-modal\")).not.toHaveClass(\"fade-out\");\n  await expect(page.locator(\"#my-modal\")).toHaveAttribute(\n    \"aria-expanded\",\n    \"false\",\n  );\n  await expect(page.locator(\"#my-modal\")).not.toHaveAttribute(\"open\");\n  await expect(page.locator(\"#my-modal\")).toBeHidden();\n});\n\ntest(\"ignore_attributes\", async ({ page }) => {\n  await page.goto(\"/js\");\n  await syncLV(page);\n  await expect(page.locator(\"details\")).not.toHaveAttribute(\"open\");\n  await page.locator(\"details\").click();\n  await expect(page.locator(\"details\")).toHaveAttribute(\"open\");\n  // without ignore_attributes, the open attribute would be reset to false\n  await page.locator(\"details button\").click();\n  await syncLV(page);\n  await expect(page.locator(\"details\")).toHaveAttribute(\"open\");\n});\n"
  },
  {
    "path": "test/e2e/tests/keyed-comprehension.spec.js",
    "content": "import { test, expect } from \"../test-fixtures\";\nimport { syncLV, evalLV } from \"../utils\";\n\nfor (let tab of [\"all_keyed\", \"rows_keyed\", \"no_keyed\"]) {\n  test(`renders correctly - ${tab}`, async ({ page }) => {\n    await page.goto(`/keyed-comprehension?tab=${tab}`);\n    await syncLV(page);\n\n    for (let i = 0; i < 10; i++) {\n      await page.getByRole(\"button\", { name: \"randomize\" }).click();\n      await syncLV(page);\n    }\n\n    const order = await evalLV(page, `socket.assigns.items`);\n\n    const theText = async (page, i, index) =>\n      (\n        await page\n          .locator(\"table\")\n          .nth(i)\n          .locator(\"tbody tr\")\n          .nth(index)\n          .textContent()\n      ).replace(/\\s+/g, \" \");\n\n    await Promise.all(\n      order.map(async (item, index) => {\n        const text0 = await theText(page, 0, index);\n        const text1 = await theText(page, 1, index);\n        expect(text0).toEqual(` Count: 10 Name: ${item.entry.foo.bar} 1 10 `);\n        expect(text1).toEqual(` Count: 10 Name: ${item.entry.foo.bar} 2 10 `);\n      }),\n    );\n  });\n}\n"
  },
  {
    "path": "test/e2e/tests/navigation.spec.js",
    "content": "import { test, expect } from \"../test-fixtures\";\nimport { syncLV } from \"../utils\";\n\nlet webSocketEvents = [];\nlet networkEvents = [];\n\ntest.beforeEach(async ({ page }) => {\n  networkEvents = [];\n  webSocketEvents = [];\n\n  page.on(\"request\", (request) =>\n    networkEvents.push({ method: request.method(), url: request.url() }),\n  );\n\n  page.on(\"websocket\", (ws) => {\n    ws.on(\"framesent\", (event) =>\n      webSocketEvents.push({ type: \"sent\", payload: event.payload }),\n    );\n    ws.on(\"framereceived\", (event) =>\n      webSocketEvents.push({ type: \"received\", payload: event.payload }),\n    );\n    ws.on(\"close\", () => webSocketEvents.push({ type: \"close\" }));\n  });\n});\n\ntest(\"can navigate between LiveViews in the same live session over websocket\", async ({\n  page,\n}) => {\n  await page.goto(\"/navigation/a\");\n  await syncLV(page);\n\n  expect(\n    networkEvents.filter((e) =>\n      e.url.includes(\"http://localhost:4004/navigation/\"),\n    ),\n  ).toHaveLength(1);\n\n  expect(webSocketEvents).toEqual([\n    expect.objectContaining({\n      type: \"sent\",\n      payload: expect.stringContaining(\"phx_join\"),\n    }),\n    expect.objectContaining({\n      type: \"received\",\n      payload: expect.stringContaining(\"phx_reply\"),\n    }),\n  ]);\n\n  // clear events\n  networkEvents = [];\n  webSocketEvents = [];\n\n  // patch the LV\n  const length = await page.evaluate(() => window.history.length);\n  await page.getByRole(\"link\", { name: \"Patch this LiveView\" }).click();\n  await syncLV(page);\n  expect(networkEvents).toEqual([]);\n  expect(webSocketEvents).toEqual([\n    expect.objectContaining({\n      type: \"sent\",\n      payload: expect.stringContaining(\"live_patch\"),\n    }),\n    expect.objectContaining({\n      type: \"received\",\n      payload: expect.stringContaining(\"phx_reply\"),\n    }),\n  ]);\n  expect(await page.evaluate(() => window.history.length)).toEqual(length + 1);\n\n  webSocketEvents = [];\n\n  // live navigation to other LV\n  await page.getByRole(\"link\", { name: \"LiveView B\" }).click();\n  await syncLV(page);\n\n  expect(networkEvents).toEqual([]);\n  // we don't assert the order of the events here, because they are not deterministic\n  expect(webSocketEvents).toEqual(\n    expect.arrayContaining([\n      { type: \"sent\", payload: expect.stringContaining(\"phx_leave\") },\n      { type: \"sent\", payload: expect.stringContaining(\"phx_join\") },\n      { type: \"received\", payload: expect.stringContaining(\"phx_close\") },\n      { type: \"received\", payload: expect.stringContaining(\"phx_reply\") },\n      { type: \"received\", payload: expect.stringContaining(\"phx_reply\") },\n    ]),\n  );\n});\n\ntest(\"handles live redirect loops\", async ({ page }) => {\n  await page.goto(\"/navigation/redirectloop\");\n  await syncLV(page);\n\n  await page.getByRole(\"link\", { name: \"Redirect Loop\" }).click();\n\n  await expect(async () => {\n    expect(webSocketEvents).toEqual(\n      expect.arrayContaining([\n        expect.objectContaining({\n          type: \"received\",\n          payload: expect.stringContaining(\"phx_error\"),\n        }),\n      ]),\n    );\n  }).toPass();\n\n  // We need to wait for the LV to reconnect\n  await syncLV(page);\n  const message = page.locator(\"#message\");\n  await expect(message).toHaveText(\"Too many redirects\");\n});\n\ntest(\"popstate\", async ({ page }) => {\n  await page.goto(\"/navigation/a\");\n  await syncLV(page);\n\n  // clear network events\n  networkEvents = [];\n\n  await page.getByRole(\"link\", { name: \"Patch this LiveView\" }).click();\n  await syncLV(page);\n  await expect(page).toHaveURL(/\\/navigation\\/a\\?/);\n  expect(networkEvents).toEqual([]);\n\n  await page.getByRole(\"link\", { name: \"LiveView B\" }).click(),\n    await syncLV(page);\n  await expect(page).toHaveURL(\"/navigation/b\");\n  expect(networkEvents).toEqual([]);\n\n  await page.goBack();\n  await syncLV(page);\n  expect(networkEvents).toEqual([]);\n  await expect(page).toHaveURL(/\\/navigation\\/a\\?/);\n\n  await page.goBack();\n  await syncLV(page);\n  expect(networkEvents).toEqual([]);\n  await expect(page).toHaveURL(\"/navigation/a\");\n\n  // and forward again\n  await page.goForward();\n  await page.goForward();\n  await syncLV(page);\n  await expect(page).toHaveURL(\"/navigation/b\");\n\n  // everything was sent over the websocket, no network requests\n  expect(networkEvents).toEqual([]);\n});\n\ntest(\"patch with replace replaces history\", async ({ page }) => {\n  await page.goto(\"/navigation/a\");\n  await syncLV(page);\n  const url = page.url();\n\n  const length = await page.evaluate(() => window.history.length);\n\n  await page.getByRole(\"link\", { name: \"Patch (Replace)\" }).click();\n  await syncLV(page);\n\n  expect(await page.evaluate(() => window.history.length)).toEqual(length);\n  expect(page.url()).not.toEqual(url);\n});\n\ntest(\"falls back to http navigation when navigating between live sessions\", async ({\n  page,\n  browserName,\n}) => {\n  await page.goto(\"/navigation/a\");\n  await syncLV(page);\n\n  networkEvents = [];\n  webSocketEvents = [];\n\n  // live navigation to page in another live session\n  await page.getByRole(\"link\", { name: \"LiveView (other session)\" }).click();\n  await syncLV(page);\n\n  expect(networkEvents).toEqual(\n    expect.arrayContaining([\n      { method: \"GET\", url: \"http://localhost:4004/stream\" },\n    ]),\n  );\n  expect(webSocketEvents).toEqual(\n    expect.arrayContaining(\n      [\n        { type: \"sent\", payload: expect.stringContaining(\"phx_leave\") },\n        { type: \"sent\", payload: expect.stringContaining(\"phx_join\") },\n        {\n          type: \"received\",\n          payload: expect.stringMatching(/error.*unauthorized/),\n        },\n      ].concat(browserName === \"webkit\" ? [] : [{ type: \"close\" }]),\n    ),\n  );\n  // ^ webkit doesn't always seem to emit websocket close events\n});\n\ntest(\"restores scroll position after navigation\", async ({ page }) => {\n  await page.goto(\"/navigation/b\");\n  await syncLV(page);\n\n  await expect(page.locator(\"#items\")).toContainText(\"Item 42\");\n\n  expect(await page.evaluate(() => document.documentElement.scrollTop)).toEqual(\n    0,\n  );\n  const offset =\n    (await page.locator(\"#items-item-42\").evaluate((el) => el.offsetTop)) - 200;\n  await page.evaluate((offset) => window.scrollTo(0, offset), offset);\n  // LiveView only updates the scroll position every 100ms\n  await page.waitForTimeout(150);\n\n  await page.getByRole(\"link\", { name: \"Item 42\" }).click();\n  await syncLV(page);\n\n  await page.goBack();\n  await syncLV(page);\n\n  // scroll position is restored\n  await expect\n    .poll(\n      async () => {\n        return await page.evaluate(() => document.documentElement.scrollTop);\n      },\n      { message: \"scrollTop not restored\", timeout: 5000 },\n    )\n    .toBe(offset);\n});\n\ntest(\"does not restore scroll position on custom container after navigation\", async ({\n  page,\n}) => {\n  await page.goto(\"/navigation/b?container=1\");\n  await syncLV(page);\n\n  await expect(page.locator(\"#items\")).toContainText(\"Item 42\");\n\n  expect(\n    await page.locator(\"#my-scroll-container\").evaluate((el) => el.scrollTop),\n  ).toEqual(0);\n  const offset =\n    (await page.locator(\"#items-item-42\").evaluate((el) => el.offsetTop)) - 200;\n  await page\n    .locator(\"#my-scroll-container\")\n    .evaluate((el, offset) => el.scrollTo(0, offset), offset);\n\n  await page.getByRole(\"link\", { name: \"Item 42\" }).click();\n  await syncLV(page);\n\n  await page.goBack();\n  await syncLV(page);\n\n  // scroll position is not restored\n  await expect\n    .poll(\n      async () => {\n        return await page\n          .locator(\"#my-scroll-container\")\n          .evaluate((el) => el.scrollTop);\n      },\n      { message: \"scrollTop not restored\", timeout: 5000 },\n    )\n    .toBe(0);\n});\n\ntest(\"scrolls hash el into view\", async ({ page }) => {\n  await page.goto(\"/navigation/b\");\n  await syncLV(page);\n\n  await expect(page.locator(\"#items\")).toContainText(\"Item 42\");\n\n  expect(\n    await page.locator(\"#my-scroll-container\").evaluate((el) => el.scrollTop),\n  ).toEqual(0);\n  const offset =\n    (await page.locator(\"#items-item-42\").evaluate((el) => el.offsetTop)) - 200;\n\n  await page.getByRole(\"link\", { name: \"Go to 42\" }).click();\n  await expect(page).toHaveURL(\"/navigation/b#items-item-42\");\n\n  let scrollTop = await page.evaluate(() => document.documentElement.scrollTop);\n  expect(scrollTop).not.toBe(0);\n  expect(scrollTop).toBeGreaterThanOrEqual(offset - 500);\n  expect(scrollTop).toBeLessThanOrEqual(offset + 500);\n\n  await page.goto(\"/navigation/a\");\n  await page.goto(\"/navigation/b#items-item-42\");\n\n  scrollTop = await page.evaluate(() => document.documentElement.scrollTop);\n  expect(scrollTop).not.toBe(0);\n  expect(scrollTop).toBeGreaterThanOrEqual(offset - 500);\n  expect(scrollTop).toBeLessThanOrEqual(offset + 500);\n});\n\ntest(\"scrolls hash el into view after live navigation (issue #3452)\", async ({\n  page,\n}) => {\n  await page.goto(\"/navigation/a\");\n  await syncLV(page);\n\n  await page.getByRole(\"link\", { name: \"Navigate to 42\" }).click();\n  await expect(page).toHaveURL(\"/navigation/b#items-item-42\");\n  const scrollTop = await page.evaluate(\n    () => document.documentElement.scrollTop,\n  );\n  const offset =\n    (await page.locator(\"#items-item-42\").evaluate((el) => el.offsetTop)) - 200;\n  expect(scrollTop).not.toBe(0);\n  expect(scrollTop).toBeGreaterThanOrEqual(offset - 500);\n  expect(scrollTop).toBeLessThanOrEqual(offset + 500);\n});\n\ntest(\"restores scroll position when navigating from dead view\", async ({\n  page,\n}) => {\n  await page.goto(\"/navigation/b\");\n  await syncLV(page);\n\n  await expect(page.locator(\"#items\")).toContainText(\"Item 42\");\n\n  expect(await page.evaluate(() => document.documentElement.scrollTop)).toEqual(\n    0,\n  );\n  const offset =\n    (await page.locator(\"#items-item-42\").evaluate((el) => el.offsetTop)) - 200;\n  await page.evaluate((offset) => window.scrollTo(0, offset), offset);\n  // LiveView only updates the scroll position every 100ms\n  await page.waitForTimeout(150);\n\n  await page.getByRole(\"link\", { name: \"Dead\" }).click();\n  await page.waitForURL(\"/navigation/dead\");\n\n  await page.goBack();\n  await syncLV(page);\n\n  // scroll position is restored\n  await expect\n    .poll(\n      async () => {\n        return await page.evaluate(() => document.documentElement.scrollTop);\n      },\n      { message: \"scrollTop not restored\", timeout: 5000 },\n    )\n    .toBe(offset);\n});\n\ntest(\"navigating all the way back works without remounting (only patching)\", async ({\n  page,\n}) => {\n  await page.goto(\"/navigation/a\");\n  await syncLV(page);\n  networkEvents = [];\n  await page.getByRole(\"link\", { name: \"Patch this LiveView\" }).click();\n  await syncLV(page);\n  await page.goBack();\n  await syncLV(page);\n  expect(networkEvents).toEqual([]);\n  // we only expect patch navigation\n  expect(\n    webSocketEvents.filter((e) => e.payload.indexOf(\"phx_leave\") !== -1),\n  ).toHaveLength(0);\n  // we patched 2 times\n  expect(\n    webSocketEvents.filter((e) => e.payload.indexOf(\"live_patch\") !== -1),\n  ).toHaveLength(2);\n});\n\n// see https://github.com/phoenixframework/phoenix_live_view/pull/3513\n// see https://github.com/phoenixframework/phoenix_live_view/issues/3536\ntest(\"back and forward navigation types are tracked\", async ({ page }) => {\n  let consoleMessages = [];\n  page.on(\"console\", (msg) => consoleMessages.push(msg.text()));\n  const getNavigationEvent = () => {\n    const ev = consoleMessages.find((e) => e.startsWith(\"navigate event\"));\n    consoleMessages = [];\n    return JSON.parse(ev.slice(15));\n  };\n  // initial page visit\n  await page.goto(\"/navigation/b\");\n  await syncLV(page);\n  consoleMessages = [];\n  networkEvents = [];\n  // type: redirect\n  await page.getByRole(\"link\", { name: \"LiveView A\" }).click();\n  await syncLV(page);\n  expect(getNavigationEvent()).toEqual({\n    href: \"http://localhost:4004/navigation/a\",\n    patch: false,\n    pop: false,\n    direction: \"forward\",\n  });\n  // type: patch\n  await page.getByRole(\"link\", { name: \"Patch this LiveView\" }).click();\n  await syncLV(page);\n  expect(getNavigationEvent()).toEqual({\n    href: expect.stringMatching(/\\/navigation\\/a\\?param=.*/),\n    patch: true,\n    pop: false,\n    direction: \"forward\",\n  });\n  // back should also be type: patch\n  await page.goBack();\n  await expect(page).toHaveURL(\"/navigation/a\");\n  expect(getNavigationEvent()).toEqual({\n    href: \"http://localhost:4004/navigation/a\",\n    patch: true,\n    pop: true,\n    direction: \"backward\",\n  });\n  await page.goBack();\n  await expect(page).toHaveURL(\"/navigation/b\");\n  expect(getNavigationEvent()).toEqual({\n    href: \"http://localhost:4004/navigation/b\",\n    patch: false,\n    pop: true,\n    direction: \"backward\",\n  });\n  await page.goForward();\n  await expect(page).toHaveURL(\"/navigation/a\");\n  expect(getNavigationEvent()).toEqual({\n    href: \"http://localhost:4004/navigation/a\",\n    patch: false,\n    pop: true,\n    direction: \"forward\",\n  });\n  await page.goForward();\n  await expect(page).toHaveURL(/\\/navigation\\/a\\?param=.*/);\n  expect(getNavigationEvent()).toEqual({\n    href: expect.stringMatching(/\\/navigation\\/a\\?param=.*/),\n    patch: true,\n    pop: true,\n    direction: \"forward\",\n  });\n  // we don't expect any full page reloads\n  expect(networkEvents).toEqual([]);\n  // we only expect 3 navigate navigations (from b to a, back from a to b, back to a)\n  expect(\n    webSocketEvents.filter((e) => e.payload.indexOf(\"phx_leave\") !== -1),\n  ).toHaveLength(3);\n  // we expect 3 patches (a to a with param, back to a, back to a with param)\n  expect(\n    webSocketEvents.filter((e) => e.payload.indexOf(\"live_patch\") !== -1),\n  ).toHaveLength(3);\n});\n"
  },
  {
    "path": "test/e2e/tests/portal.spec.js",
    "content": "import { test, expect } from \"../test-fixtures\";\nimport { syncLV, evalLV } from \"../utils\";\n\ntest(\"renders modal inside portal location\", async ({ page }) => {\n  await page.goto(\"/portal?tick=false\");\n  await syncLV(page);\n\n  await expect(page.locator(\"#my-modal\")).toHaveCount(1);\n  await expect(page.locator(\"#my-modal-content\")).toBeHidden();\n  // no modal inside the main element (rendered in the layout)\n  await expect(page.locator(\"main #my-modal\")).toHaveCount(0);\n\n  await page.getByRole(\"button\", { name: \"Open modal\" }).click();\n  await expect(page.locator(\"#my-modal-content\")).toBeVisible();\n\n  await expect(page.locator(\"#my-modal-content\")).toContainText(\n    \"DOM patching works as expected: 0\",\n  );\n  await evalLV(page, \"send(self(), :tick)\");\n  await expect(page.locator(\"#my-modal-content\")).toContainText(\n    \"DOM patching works as expected: 1\",\n  );\n});\n\ntest(\"teleported element is removed properly\", async ({ page }) => {\n  await page.goto(\"/portal?tick=false\");\n  await syncLV(page);\n\n  await expect(\n    page.locator(\"[data-phx-teleported-src=portal-source]\"),\n  ).toHaveCount(1);\n  await page.getByRole(\"button\", { name: \"Toggle modal render\" }).click();\n  await expect(\n    page.locator(\"[data-phx-teleported-src=portal-source]\"),\n  ).toHaveCount(0);\n  await expect(page.locator(\"#my-modal\")).toHaveCount(0);\n\n  // other modal still exists\n  await expect(\n    page.locator(\"[data-phx-teleported-src=portal-source-2]\"),\n  ).toHaveCount(1);\n\n  // now toggle it again\n  await page.getByRole(\"button\", { name: \"Toggle modal render\" }).click();\n  await expect(\n    page.locator(\"[data-phx-teleported-src=portal-source]\"),\n  ).toHaveCount(1);\n\n  // now navigate to another page\n  await page.getByRole(\"button\", { name: \"Live navigate\" }).click();\n  await syncLV(page);\n  // all teleported elements should be gone\n  await expect(page.locator(\"[data-phx-teleported-src]\")).toHaveCount(0);\n});\n\ntest(\"events are routed to correct LiveView\", async ({ page }) => {\n  await page.goto(\"/portal?tick=false\");\n  await syncLV(page);\n\n  await expect(page.locator(\"#nested-event-count\")).toHaveText(\"0\");\n  await page\n    .getByRole(\"button\", { name: \"Trigger event in nested LV\", exact: true })\n    .click();\n  await expect(page.locator(\"#nested-event-count\")).toHaveText(\"1\");\n  await page\n    .getByRole(\"button\", {\n      name: \"Trigger event in nested LV (from teleported button)\",\n    })\n    .click();\n  await expect(page.locator(\"#nested-event-count\")).toHaveText(\"2\");\n});\n\ntest(\"streams work in teleported LiveComponent\", async ({ page }) => {\n  await page.goto(\"/portal?tick=false\");\n  await syncLV(page);\n\n  expect(\n    await page.evaluate(\n      () => document.querySelector(\"#stream-in-lc\").children.length,\n    ),\n  ).toEqual(2);\n  await page.getByRole(\"button\", { name: \"Prepend item\", exact: true }).click();\n  await syncLV(page);\n  expect(\n    await page.evaluate(\n      () => document.querySelector(\"#stream-in-lc\").children.length,\n    ),\n  ).toEqual(3);\n  // https://github.com/phoenixframework/phoenix_live_view/issues/4101\n  // teleporting outside of live component works too\n  await page\n    .getByRole(\"button\", { name: \"Prepend item (teleported)\", exact: true })\n    .click();\n  await syncLV(page);\n  expect(\n    await page.evaluate(\n      () => document.querySelector(\"#stream-in-lc\").children.length,\n    ),\n  ).toEqual(4);\n});\n\ntest(\"tooltip example\", async ({ page }) => {\n  await page.goto(\"/portal?tick=false\");\n  await syncLV(page);\n\n  await expect(page.locator(\"#tooltip-example-portal\")).toBeHidden();\n  await expect(page.locator(\"#tooltip-example-no-portal\")).toBeHidden();\n\n  await page.getByRole(\"button\", { name: \"Hover me\", exact: true }).hover();\n  await expect(page.locator(\"#tooltip-example-portal\")).toBeVisible();\n  await expect(page.locator(\"#tooltip-example-no-portal\")).toBeHidden();\n\n  await page.getByRole(\"button\", { name: \"Hover me (no portal)\" }).hover();\n  await expect(page.locator(\"#tooltip-example-portal\")).toBeHidden();\n  await expect(page.locator(\"#tooltip-example-no-portal\")).toBeVisible();\n});\n\ntest(\"teleported hook works correctly\", async ({ page }) => {\n  // https://github.com/phoenixframework/phoenix_live_view/issues/3950\n  await page.goto(\"/portal?tick=false\");\n  await syncLV(page);\n\n  await expect(page.locator(\"#hook-test\")).toHaveAttribute(\n    \"data-portalhook-mounted\",\n    \"true\",\n  );\n});\n\ntest(\"nested portals render and work correctly\", async ({ page }) => {\n  await page.goto(\"/portal?tick=false\");\n  await syncLV(page);\n\n  // Verify outer portal is teleported\n  await expect(\n    page.locator(\"[data-phx-teleported-src=nested-portal-source]\"),\n  ).toHaveCount(1);\n  await expect(page.locator(\"#outer-portal\")).toHaveCount(1);\n\n  // Verify inner portal is teleported\n  await expect(\n    page.locator(\"[data-phx-teleported-src=inner-portal-source]\"),\n  ).toHaveCount(1);\n  await expect(page.locator(\"#inner-portal\")).toHaveCount(1);\n\n  // Test that events work in nested portals\n  await expect(page.locator(\"#nested-portal-count\")).toHaveText(\"0\");\n  await page\n    .getByRole(\"button\", { name: \"Click nested portal button\" })\n    .click();\n  await expect(page.locator(\"#nested-portal-count\")).toHaveText(\"1\");\n\n  // Test DOM patching works in nested portals\n  await expect(page.locator(\"#nested-portal-content\")).toContainText(\n    \"Tick count: 0\",\n  );\n  await evalLV(page, \"send(self(), :tick)\");\n  await expect(page.locator(\"#nested-portal-content\")).toContainText(\n    \"Tick count: 1\",\n  );\n});\n\ntest(\"nested portals cleanup and re-render correctly\", async ({ page }) => {\n  await page.goto(\"/portal?tick=false\");\n  await syncLV(page);\n\n  // Verify initial state\n  await expect(page.locator(\"#outer-portal\")).toHaveCount(1);\n  await expect(page.locator(\"#inner-portal\")).toHaveCount(1);\n\n  // Toggle off - both portals should be removed\n  await page.getByRole(\"button\", { name: \"Toggle nested portals\" }).click();\n  await expect(\n    page.locator(\"[data-phx-teleported-src=nested-portal-source]\"),\n  ).toHaveCount(0);\n  await expect(\n    page.locator(\"[data-phx-teleported-src=inner-portal-source]\"),\n  ).toHaveCount(0);\n  await expect(page.locator(\"#outer-portal\")).toHaveCount(0);\n  await expect(page.locator(\"#inner-portal\")).toHaveCount(0);\n\n  // Toggle back on - both portals should reappear with correct nesting\n  await page.getByRole(\"button\", { name: \"Toggle nested portals\" }).click();\n  await expect(\n    page.locator(\"[data-phx-teleported-src=nested-portal-source]\"),\n  ).toHaveCount(1);\n  await expect(\n    page.locator(\"[data-phx-teleported-src=inner-portal-source]\"),\n  ).toHaveCount(1);\n  await expect(page.locator(\"#outer-portal\")).toHaveCount(1);\n  await expect(page.locator(\"#inner-portal\")).toHaveCount(1);\n});\n\ntest(\"click-away is portal aware\", async ({ page }) => {\n  await page.goto(\"/portal?tick=false\");\n  await syncLV(page);\n\n  await page.getByRole(\"button\", { name: \"Open non-teleported modal\" }).click();\n  await expect(page.locator(\"#non-teleported-modal-content\")).toBeVisible();\n  await page.getByRole(\"button\", { name: \"Open menu\" }).click();\n  await expect(page.locator(\"#teleported-menu-content\")).toBeVisible();\n  await page.getByRole(\"button\", { name: \"Close menu\" }).click();\n  await expect(page.locator(\"#teleported-menu-content\")).toBeHidden();\n\n  // Modal must still be visible, despite click away\n  await expect(page.locator(\"#non-teleported-modal-content\")).toBeVisible();\n  // trigger click-away\n  await page\n    .locator(\"#non-teleported-modal .fixed[role='dialog']\")\n    .click({ position: { x: 0, y: 0 } });\n  await expect(page.locator(\"#non-teleported-modal-content\")).toBeHidden();\n\n  // Test that click-away also works properly for teleported modals\n  await page.getByRole(\"button\", { name: \"Open modal\" }).click();\n  await expect(page.locator(\"#my-modal-content\")).toBeVisible();\n  await page\n    .locator(\"#my-modal .fixed[role='dialog']\")\n    .click({ position: { x: 0, y: 0 } });\n  await expect(page.locator(\"#my-modal-content\")).toBeHidden();\n\n  // Test that visibility of the <template> element doesn't interfere with click-away\n  await page.getByRole(\"button\", { name: \"Open second modal\" }).click();\n  await expect(page.locator(\"#inner-red-box\")).toBeVisible();\n  await page.locator(\"#my-modal-2 .bg-gray-300\").click();\n  await expect(page.locator(\"#inner-red-box\")).toBeHidden();\n});\n"
  },
  {
    "path": "test/e2e/tests/select.spec.js",
    "content": "import { test, expect } from \"../test-fixtures\";\nimport { syncLV } from \"../utils\";\n\n// this tests issue #2659\n// https://github.com/phoenixframework/phoenix_live_view/pull/2659\ntest(\"select shows error when invalid option is selected\", async ({ page }) => {\n  await page.goto(\"/select\");\n  await syncLV(page);\n\n  const select3 = page.locator(\"#select_form_select3\");\n  await expect(select3).toHaveValue(\"2\");\n  await expect(select3).not.toHaveClass(\"has-error\");\n\n  // 5 or below should be invalid\n  await select3.selectOption(\"3\");\n  await syncLV(page);\n  await expect(select3).toHaveClass(\"has-error\");\n\n  // 6 or above should be valid\n  await select3.selectOption(\"6\");\n  await syncLV(page);\n  await expect(select3).not.toHaveClass(\"has-error\");\n});\n"
  },
  {
    "path": "test/e2e/tests/streams.spec.js",
    "content": "import { test, expect } from \"../test-fixtures\";\nimport { syncLV, evalLV } from \"../utils\";\n\nconst usersInDom = async (page, parent) => {\n  return await page.locator(`#${parent} > *`).evaluateAll((list) =>\n    list.map((el) => ({\n      id: el.id,\n      text: el.childNodes[0].nodeValue.trim(),\n    })),\n  );\n};\n\ntest(\"renders properly\", async ({ page }) => {\n  await page.goto(\"/stream\");\n  await syncLV(page);\n\n  expect(await usersInDom(page, \"users\")).toEqual([\n    { id: \"users-1\", text: \"chris\" },\n    { id: \"users-2\", text: \"callan\" },\n  ]);\n  expect(await usersInDom(page, \"c_users\")).toEqual([\n    { id: \"c_users-1\", text: \"chris\" },\n    { id: \"c_users-2\", text: \"callan\" },\n  ]);\n  expect(await usersInDom(page, \"admins\")).toEqual([\n    { id: \"admins-1\", text: \"chris-admin\" },\n    { id: \"admins-2\", text: \"callan-admin\" },\n  ]);\n});\n\ntest(\"elements can be updated and deleted (LV)\", async ({ page }) => {\n  await page.goto(\"/stream\");\n  await syncLV(page);\n\n  await page\n    .locator(\"#users-1\")\n    .getByRole(\"button\", { name: \"update\" })\n    .click();\n  await syncLV(page);\n\n  expect(await usersInDom(page, \"users\")).toEqual([\n    { id: \"users-1\", text: \"updated\" },\n    { id: \"users-2\", text: \"callan\" },\n  ]);\n  expect(await usersInDom(page, \"c_users\")).toEqual([\n    { id: \"c_users-1\", text: \"chris\" },\n    { id: \"c_users-2\", text: \"callan\" },\n  ]);\n  expect(await usersInDom(page, \"admins\")).toEqual([\n    { id: \"admins-1\", text: \"chris-admin\" },\n    { id: \"admins-2\", text: \"callan-admin\" },\n  ]);\n\n  await page\n    .locator(\"#users-2\")\n    .getByRole(\"button\", { name: \"update\" })\n    .click();\n  await syncLV(page);\n\n  expect(await usersInDom(page, \"users\")).toEqual([\n    { id: \"users-1\", text: \"updated\" },\n    { id: \"users-2\", text: \"updated\" },\n  ]);\n  expect(await usersInDom(page, \"c_users\")).toEqual([\n    { id: \"c_users-1\", text: \"chris\" },\n    { id: \"c_users-2\", text: \"callan\" },\n  ]);\n  expect(await usersInDom(page, \"admins\")).toEqual([\n    { id: \"admins-1\", text: \"chris-admin\" },\n    { id: \"admins-2\", text: \"callan-admin\" },\n  ]);\n\n  await page\n    .locator(\"#users-1\")\n    .getByRole(\"button\", { name: \"delete\" })\n    .click();\n  await syncLV(page);\n\n  expect(await usersInDom(page, \"users\")).toEqual([\n    { id: \"users-2\", text: \"updated\" },\n  ]);\n});\n\ntest(\"elements can be updated and deleted (LC)\", async ({ page }) => {\n  await page.goto(\"/stream\");\n  await syncLV(page);\n\n  await page\n    .locator(\"#c_users-1\")\n    .getByRole(\"button\", { name: \"update\" })\n    .click();\n  await syncLV(page);\n\n  expect(await usersInDom(page, \"c_users\")).toEqual([\n    { id: \"c_users-1\", text: \"updated\" },\n    { id: \"c_users-2\", text: \"callan\" },\n  ]);\n  expect(await usersInDom(page, \"users\")).toEqual([\n    { id: \"users-1\", text: \"chris\" },\n    { id: \"users-2\", text: \"callan\" },\n  ]);\n  expect(await usersInDom(page, \"admins\")).toEqual([\n    { id: \"admins-1\", text: \"chris-admin\" },\n    { id: \"admins-2\", text: \"callan-admin\" },\n  ]);\n\n  await page\n    .locator(\"#c_users-2\")\n    .getByRole(\"button\", { name: \"update\" })\n    .click();\n  await syncLV(page);\n\n  expect(await usersInDom(page, \"c_users\")).toEqual([\n    { id: \"c_users-1\", text: \"updated\" },\n    { id: \"c_users-2\", text: \"updated\" },\n  ]);\n  expect(await usersInDom(page, \"users\")).toEqual([\n    { id: \"users-1\", text: \"chris\" },\n    { id: \"users-2\", text: \"callan\" },\n  ]);\n  expect(await usersInDom(page, \"admins\")).toEqual([\n    { id: \"admins-1\", text: \"chris-admin\" },\n    { id: \"admins-2\", text: \"callan-admin\" },\n  ]);\n\n  await page\n    .locator(\"#c_users-1\")\n    .getByRole(\"button\", { name: \"delete\" })\n    .click();\n  await syncLV(page);\n\n  expect(await usersInDom(page, \"c_users\")).toEqual([\n    { id: \"c_users-2\", text: \"updated\" },\n  ]);\n});\n\ntest(\"move-to-first moves the second element to the first position (LV)\", async ({\n  page,\n}) => {\n  await page.goto(\"/stream\");\n  await syncLV(page);\n\n  expect(await usersInDom(page, \"c_users\")).toEqual([\n    { id: \"c_users-1\", text: \"chris\" },\n    { id: \"c_users-2\", text: \"callan\" },\n  ]);\n\n  await page\n    .locator(\"#c_users-2\")\n    .getByRole(\"button\", { name: \"make first\" })\n    .click();\n  await syncLV(page);\n\n  expect(await usersInDom(page, \"c_users\")).toEqual([\n    { id: \"c_users-2\", text: \"updated\" },\n    { id: \"c_users-1\", text: \"chris\" },\n  ]);\n});\n\ntest(\"stream reset removes items\", async ({ page }) => {\n  await page.goto(\"/stream\");\n  await syncLV(page);\n\n  expect(await usersInDom(page, \"users\")).toEqual([\n    { id: \"users-1\", text: \"chris\" },\n    { id: \"users-2\", text: \"callan\" },\n  ]);\n\n  await page.getByRole(\"button\", { name: \"Reset\" }).click();\n  await syncLV(page);\n\n  expect(await usersInDom(page, \"users\")).toEqual([]);\n});\n\ntest(\"stream reset properly reorders items\", async ({ page }) => {\n  await page.goto(\"/stream\");\n  await syncLV(page);\n\n  expect(await usersInDom(page, \"users\")).toEqual([\n    { id: \"users-1\", text: \"chris\" },\n    { id: \"users-2\", text: \"callan\" },\n  ]);\n\n  await page.getByRole(\"button\", { name: \"Reorder\" }).click();\n  await syncLV(page);\n\n  expect(await usersInDom(page, \"users\")).toEqual([\n    { id: \"users-3\", text: \"peter\" },\n    { id: \"users-1\", text: \"chris\" },\n    { id: \"users-4\", text: \"mona\" },\n  ]);\n});\n\ntest(\"stream reset updates attributes\", async ({ page }) => {\n  await page.goto(\"/stream\");\n  await syncLV(page);\n\n  expect(await usersInDom(page, \"users\")).toEqual([\n    { id: \"users-1\", text: \"chris\" },\n    { id: \"users-2\", text: \"callan\" },\n  ]);\n\n  await await expect(page.locator(\"#users-1\")).toHaveAttribute(\n    \"data-count\",\n    \"0\",\n  );\n  await await expect(page.locator(\"#users-2\")).toHaveAttribute(\n    \"data-count\",\n    \"0\",\n  );\n\n  await page.getByRole(\"button\", { name: \"Reorder\" }).click();\n  await syncLV(page);\n\n  expect(await usersInDom(page, \"users\")).toEqual([\n    { id: \"users-3\", text: \"peter\" },\n    { id: \"users-1\", text: \"chris\" },\n    { id: \"users-4\", text: \"mona\" },\n  ]);\n\n  await await expect(page.locator(\"#users-1\")).toHaveAttribute(\n    \"data-count\",\n    \"1\",\n  );\n  await await expect(page.locator(\"#users-3\")).toHaveAttribute(\n    \"data-count\",\n    \"1\",\n  );\n  await await expect(page.locator(\"#users-4\")).toHaveAttribute(\n    \"data-count\",\n    \"1\",\n  );\n});\n\ntest.describe(\"Issue #2656\", () => {\n  test(\"stream reset works when patching\", async ({ page }) => {\n    await page.goto(\"/healthy/fruits\");\n    await syncLV(page);\n\n    await expect(page.locator(\"h1\")).toContainText(\"Fruits\");\n    await expect(page.locator(\"ul\")).toContainText(\"Apples\");\n    await expect(page.locator(\"ul\")).toContainText(\"Oranges\");\n\n    await page.getByRole(\"link\", { name: \"Switch\" }).click();\n    await expect(page).toHaveURL(\"/healthy/veggies\");\n    await syncLV(page);\n\n    await expect(page.locator(\"h1\")).toContainText(\"Veggies\");\n\n    await expect(page.locator(\"ul\")).toContainText(\"Carrots\");\n    await expect(page.locator(\"ul\")).toContainText(\"Tomatoes\");\n    await expect(page.locator(\"ul\")).not.toContainText(\"Apples\");\n    await expect(page.locator(\"ul\")).not.toContainText(\"Oranges\");\n\n    await page.getByRole(\"link\", { name: \"Switch\" }).click();\n    await expect(page).toHaveURL(\"/healthy/fruits\");\n    await syncLV(page);\n\n    await expect(page.locator(\"ul\")).not.toContainText(\"Carrots\");\n    await expect(page.locator(\"ul\")).not.toContainText(\"Tomatoes\");\n    await expect(page.locator(\"ul\")).toContainText(\"Apples\");\n    await expect(page.locator(\"ul\")).toContainText(\"Oranges\");\n  });\n});\n\n// helper function used below\nconst listItems = async (page) =>\n  page\n    .locator(\"ul > li\")\n    .evaluateAll((list) =>\n      list.map((el) => ({ id: el.id, text: el.innerText })),\n    );\n\ntest.describe(\"Issue #2994\", () => {\n  test(\"can filter and reset a stream\", async ({ page }) => {\n    await page.goto(\"/stream/reset\");\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-a\", text: \"A\" },\n      { id: \"items-b\", text: \"B\" },\n      { id: \"items-c\", text: \"C\" },\n      { id: \"items-d\", text: \"D\" },\n    ]);\n\n    await page.getByRole(\"button\", { name: \"Filter\" }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-b\", text: \"B\" },\n      { id: \"items-c\", text: \"C\" },\n      { id: \"items-d\", text: \"D\" },\n    ]);\n\n    await page.getByRole(\"button\", { name: \"Reset\" }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-a\", text: \"A\" },\n      { id: \"items-b\", text: \"B\" },\n      { id: \"items-c\", text: \"C\" },\n      { id: \"items-d\", text: \"D\" },\n    ]);\n  });\n\n  test(\"can reorder stream\", async ({ page }) => {\n    await page.goto(\"/stream/reset\");\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-a\", text: \"A\" },\n      { id: \"items-b\", text: \"B\" },\n      { id: \"items-c\", text: \"C\" },\n      { id: \"items-d\", text: \"D\" },\n    ]);\n\n    await page.getByRole(\"button\", { name: \"Reorder\" }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-b\", text: \"B\" },\n      { id: \"items-a\", text: \"A\" },\n      { id: \"items-c\", text: \"C\" },\n      { id: \"items-d\", text: \"D\" },\n    ]);\n  });\n\n  test(\"can filter and then prepend / append stream\", async ({ page }) => {\n    await page.goto(\"/stream/reset\");\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-a\", text: \"A\" },\n      { id: \"items-b\", text: \"B\" },\n      { id: \"items-c\", text: \"C\" },\n      { id: \"items-d\", text: \"D\" },\n    ]);\n\n    await page.getByRole(\"button\", { name: \"Filter\" }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-b\", text: \"B\" },\n      { id: \"items-c\", text: \"C\" },\n      { id: \"items-d\", text: \"D\" },\n    ]);\n\n    await page.getByRole(\"button\", { name: \"Prepend\", exact: true }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: expect.stringMatching(/items-a-.*/), text: expect.any(String) },\n      { id: \"items-b\", text: \"B\" },\n      { id: \"items-c\", text: \"C\" },\n      { id: \"items-d\", text: \"D\" },\n    ]);\n\n    await page.getByRole(\"button\", { name: \"Reset\" }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-a\", text: \"A\" },\n      { id: \"items-b\", text: \"B\" },\n      { id: \"items-c\", text: \"C\" },\n      { id: \"items-d\", text: \"D\" },\n    ]);\n\n    await page.getByRole(\"button\", { name: \"Append\", exact: true }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-a\", text: \"A\" },\n      { id: \"items-b\", text: \"B\" },\n      { id: \"items-c\", text: \"C\" },\n      { id: \"items-d\", text: \"D\" },\n      { id: expect.stringMatching(/items-a-.*/), text: expect.any(String) },\n    ]);\n  });\n});\n\ntest.describe(\"Issue #2982\", () => {\n  test(\"can reorder a stream with LiveComponents as direct stream children\", async ({\n    page,\n  }) => {\n    await page.goto(\"/stream/reset-lc\");\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-a\", text: \"A\" },\n      { id: \"items-b\", text: \"B\" },\n      { id: \"items-c\", text: \"C\" },\n      { id: \"items-d\", text: \"D\" },\n    ]);\n\n    await page.getByRole(\"button\", { name: \"Reorder\" }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-e\", text: \"E\" },\n      { id: \"items-a\", text: \"A\" },\n      { id: \"items-f\", text: \"F\" },\n      { id: \"items-g\", text: \"G\" },\n    ]);\n  });\n});\n\ntest.describe(\"Issue #3023\", () => {\n  test(\"can bulk insert items at a specific index\", async ({ page }) => {\n    await page.goto(\"/stream/reset\");\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-a\", text: \"A\" },\n      { id: \"items-b\", text: \"B\" },\n      { id: \"items-c\", text: \"C\" },\n      { id: \"items-d\", text: \"D\" },\n    ]);\n\n    await page.getByRole(\"button\", { name: \"Bulk insert\" }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-a\", text: \"A\" },\n      { id: \"items-e\", text: \"E\" },\n      { id: \"items-f\", text: \"F\" },\n      { id: \"items-g\", text: \"G\" },\n      { id: \"items-b\", text: \"B\" },\n      { id: \"items-c\", text: \"C\" },\n      { id: \"items-d\", text: \"D\" },\n    ]);\n  });\n});\n\ntest.describe(\"stream limit - issue #2686\", () => {\n  test(\"limit is enforced on mount, but not dead render\", async ({\n    page,\n    request,\n  }) => {\n    const html = await request.get(\"/stream/limit\").then((r) => r.text());\n    for (let i = 1; i <= 10; i++) {\n      expect(html).toContain(`id=\"items-${i}\"`);\n    }\n\n    await page.goto(\"/stream/limit\");\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-6\", text: \"6\" },\n      { id: \"items-7\", text: \"7\" },\n      { id: \"items-8\", text: \"8\" },\n      { id: \"items-9\", text: \"9\" },\n      { id: \"items-10\", text: \"10\" },\n    ]);\n  });\n\n  test(\"removes item at front when appending and limit is negative\", async ({\n    page,\n  }) => {\n    await page.goto(\"/stream/limit\");\n    await syncLV(page);\n\n    // these are the defaults in the LV\n    await expect(page.locator(\"input[name='at']\")).toHaveValue(\"-1\");\n    await expect(page.locator(\"input[name='limit']\")).toHaveValue(\"-5\");\n\n    await page.getByRole(\"button\", { name: \"add 1\", exact: true }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-7\", text: \"7\" },\n      { id: \"items-8\", text: \"8\" },\n      { id: \"items-9\", text: \"9\" },\n      { id: \"items-10\", text: \"10\" },\n      { id: \"items-11\", text: \"11\" },\n    ]);\n    await page.getByRole(\"button\", { name: \"add 10\", exact: true }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-17\", text: \"17\" },\n      { id: \"items-18\", text: \"18\" },\n      { id: \"items-19\", text: \"19\" },\n      { id: \"items-20\", text: \"20\" },\n      { id: \"items-21\", text: \"21\" },\n    ]);\n  });\n\n  test(\"removes item at back when prepending and limit is positive\", async ({\n    page,\n  }) => {\n    await page.goto(\"/stream/limit\");\n    await syncLV(page);\n\n    await page.locator(\"input[name='at']\").fill(\"0\");\n    await page.locator(\"input[name='limit']\").fill(\"5\");\n    await page.getByRole(\"button\", { name: \"recreate stream\" }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-10\", text: \"10\" },\n      { id: \"items-9\", text: \"9\" },\n      { id: \"items-8\", text: \"8\" },\n      { id: \"items-7\", text: \"7\" },\n      { id: \"items-6\", text: \"6\" },\n    ]);\n\n    await page.getByRole(\"button\", { name: \"add 1\", exact: true }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-11\", text: \"11\" },\n      { id: \"items-10\", text: \"10\" },\n      { id: \"items-9\", text: \"9\" },\n      { id: \"items-8\", text: \"8\" },\n      { id: \"items-7\", text: \"7\" },\n    ]);\n\n    await page.getByRole(\"button\", { name: \"add 10\", exact: true }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-21\", text: \"21\" },\n      { id: \"items-20\", text: \"20\" },\n      { id: \"items-19\", text: \"19\" },\n      { id: \"items-18\", text: \"18\" },\n      { id: \"items-17\", text: \"17\" },\n    ]);\n  });\n\n  test(\"does nothing if appending and positive limit is reached\", async ({\n    page,\n  }) => {\n    await page.goto(\"/stream/limit\");\n    await syncLV(page);\n\n    await page.locator(\"input[name='at']\").fill(\"-1\");\n    await page.locator(\"input[name='limit']\").fill(\"5\");\n    await page.getByRole(\"button\", { name: \"recreate stream\" }).click();\n    await syncLV(page);\n\n    await page.getByRole(\"button\", { name: \"clear\" }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([]);\n\n    const items = [];\n    for (let i = 1; i <= 5; i++) {\n      await page.getByRole(\"button\", { name: \"add 1\", exact: true }).click();\n      await syncLV(page);\n      items.push({ id: `items-${i}`, text: i.toString() });\n      expect(await listItems(page)).toEqual(items);\n    }\n\n    // now adding new items should do nothing, as the limit is reached\n    await page.getByRole(\"button\", { name: \"add 1\", exact: true }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-1\", text: \"1\" },\n      { id: \"items-2\", text: \"2\" },\n      { id: \"items-3\", text: \"3\" },\n      { id: \"items-4\", text: \"4\" },\n      { id: \"items-5\", text: \"5\" },\n    ]);\n\n    // same when bulk inserting\n    await page.getByRole(\"button\", { name: \"add 10\", exact: true }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-1\", text: \"1\" },\n      { id: \"items-2\", text: \"2\" },\n      { id: \"items-3\", text: \"3\" },\n      { id: \"items-4\", text: \"4\" },\n      { id: \"items-5\", text: \"5\" },\n    ]);\n  });\n\n  test(\"does nothing if prepending and negative limit is reached\", async ({\n    page,\n  }) => {\n    await page.goto(\"/stream/limit\");\n    await syncLV(page);\n\n    await page.locator(\"input[name='at']\").fill(\"0\");\n    await page.locator(\"input[name='limit']\").fill(\"-5\");\n    await page.getByRole(\"button\", { name: \"recreate stream\" }).click();\n    await syncLV(page);\n\n    await page.getByRole(\"button\", { name: \"clear\" }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([]);\n\n    const items = [];\n    for (let i = 1; i <= 5; i++) {\n      await page.getByRole(\"button\", { name: \"add 1\", exact: true }).click();\n      await syncLV(page);\n      items.unshift({ id: `items-${i}`, text: i.toString() });\n      expect(await listItems(page)).toEqual(items);\n    }\n\n    // now adding new items should do nothing, as the limit is reached\n    await page.getByRole(\"button\", { name: \"add 1\", exact: true }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-5\", text: \"5\" },\n      { id: \"items-4\", text: \"4\" },\n      { id: \"items-3\", text: \"3\" },\n      { id: \"items-2\", text: \"2\" },\n      { id: \"items-1\", text: \"1\" },\n    ]);\n\n    // same when bulk inserting\n    await page.getByRole(\"button\", { name: \"add 10\", exact: true }).click();\n    await syncLV(page);\n\n    expect(await listItems(page)).toEqual([\n      { id: \"items-5\", text: \"5\" },\n      { id: \"items-4\", text: \"4\" },\n      { id: \"items-3\", text: \"3\" },\n      { id: \"items-2\", text: \"2\" },\n      { id: \"items-1\", text: \"1\" },\n    ]);\n  });\n\n  test(\"arbitrary index\", async ({ page }) => {\n    await page.goto(\"/stream/limit\");\n    await syncLV(page);\n\n    await page.locator(\"input[name='at']\").fill(\"1\");\n    await page.locator(\"input[name='limit']\").fill(\"5\");\n    await page.getByRole(\"button\", { name: \"recreate stream\" }).click();\n    await syncLV(page);\n\n    // we tried to insert 10 items\n    expect(await listItems(page)).toEqual([\n      { id: \"items-1\", text: \"1\" },\n      { id: \"items-10\", text: \"10\" },\n      { id: \"items-9\", text: \"9\" },\n      { id: \"items-8\", text: \"8\" },\n      { id: \"items-7\", text: \"7\" },\n    ]);\n\n    await page.getByRole(\"button\", { name: \"add 10\", exact: true }).click();\n    await syncLV(page);\n    expect(await listItems(page)).toEqual([\n      { id: \"items-1\", text: \"1\" },\n      { id: \"items-20\", text: \"20\" },\n      { id: \"items-19\", text: \"19\" },\n      { id: \"items-18\", text: \"18\" },\n      { id: \"items-17\", text: \"17\" },\n    ]);\n\n    await page.locator(\"input[name='at']\").fill(\"1\");\n    await page.locator(\"input[name='limit']\").fill(\"-5\");\n    await page.getByRole(\"button\", { name: \"recreate stream\" }).click();\n    await syncLV(page);\n\n    // we tried to insert 10 items\n    expect(await listItems(page)).toEqual([\n      { id: \"items-10\", text: \"10\" },\n      { id: \"items-5\", text: \"5\" },\n      { id: \"items-4\", text: \"4\" },\n      { id: \"items-3\", text: \"3\" },\n      { id: \"items-2\", text: \"2\" },\n    ]);\n\n    await page.getByRole(\"button\", { name: \"add 10\", exact: true }).click();\n    await syncLV(page);\n    expect(await listItems(page)).toEqual([\n      { id: \"items-20\", text: \"20\" },\n      { id: \"items-5\", text: \"5\" },\n      { id: \"items-4\", text: \"4\" },\n      { id: \"items-3\", text: \"3\" },\n      { id: \"items-2\", text: \"2\" },\n    ]);\n  });\n});\n\ntest(\"any stream insert for elements already in the DOM does not reorder\", async ({\n  page,\n}) => {\n  await page.goto(\"/stream/reset\");\n  await syncLV(page);\n\n  expect(await listItems(page)).toEqual([\n    { id: \"items-a\", text: \"A\" },\n    { id: \"items-b\", text: \"B\" },\n    { id: \"items-c\", text: \"C\" },\n    { id: \"items-d\", text: \"D\" },\n  ]);\n\n  await page.getByRole(\"button\", { name: \"Prepend C\" }).click();\n  await syncLV(page);\n  expect(await listItems(page)).toEqual([\n    { id: \"items-a\", text: \"A\" },\n    { id: \"items-b\", text: \"B\" },\n    { id: \"items-c\", text: \"C\" },\n    { id: \"items-d\", text: \"D\" },\n  ]);\n\n  await page.getByRole(\"button\", { name: \"Append C\" }).click();\n  await syncLV(page);\n  expect(await listItems(page)).toEqual([\n    { id: \"items-a\", text: \"A\" },\n    { id: \"items-b\", text: \"B\" },\n    { id: \"items-c\", text: \"C\" },\n    { id: \"items-d\", text: \"D\" },\n  ]);\n\n  await page.getByRole(\"button\", { name: \"Insert C at 1\" }).click();\n  await syncLV(page);\n  expect(await listItems(page)).toEqual([\n    { id: \"items-a\", text: \"A\" },\n    { id: \"items-b\", text: \"B\" },\n    { id: \"items-c\", text: \"C\" },\n    { id: \"items-d\", text: \"D\" },\n  ]);\n\n  await page.getByRole(\"button\", { name: \"Insert at 1\", exact: true }).click();\n  await syncLV(page);\n  expect(await listItems(page)).toEqual([\n    { id: \"items-a\", text: \"A\" },\n    { id: expect.stringMatching(/items-a-.*/), text: expect.any(String) },\n    { id: \"items-b\", text: \"B\" },\n    { id: \"items-c\", text: \"C\" },\n    { id: \"items-d\", text: \"D\" },\n  ]);\n\n  await page.getByRole(\"button\", { name: \"Reset\" }).click();\n  await syncLV(page);\n  expect(await listItems(page)).toEqual([\n    { id: \"items-a\", text: \"A\" },\n    { id: \"items-b\", text: \"B\" },\n    { id: \"items-c\", text: \"C\" },\n    { id: \"items-d\", text: \"D\" },\n  ]);\n\n  await page.getByRole(\"button\", { name: \"Delete C and insert at 1\" }).click();\n  await syncLV(page);\n  expect(await listItems(page)).toEqual([\n    { id: \"items-a\", text: \"A\" },\n    { id: \"items-c\", text: \"C\" },\n    { id: \"items-b\", text: \"B\" },\n    { id: \"items-d\", text: \"D\" },\n  ]);\n});\n\ntest(\"stream nested in a LiveComponent is properly restored on reset\", async ({\n  page,\n}) => {\n  await page.goto(\"/stream/nested-component-reset\");\n  await syncLV(page);\n\n  const childItems = async (page, id) =>\n    page\n      .locator(`#${id} div[phx-update=stream] > *`)\n      .evaluateAll((div) =>\n        div.map((el) => ({ id: el.id, text: el.innerText })),\n      );\n\n  expect(await listItems(page)).toEqual([\n    { id: \"items-a\", text: expect.stringMatching(/A/) },\n    { id: \"items-b\", text: expect.stringMatching(/B/) },\n    { id: \"items-c\", text: expect.stringMatching(/C/) },\n    { id: \"items-d\", text: expect.stringMatching(/D/) },\n  ]);\n\n  for (const id of [\"a\", \"b\", \"c\", \"d\"]) {\n    expect(await childItems(page, `items-${id}`)).toEqual([\n      { id: `nested-items-${id}-a`, text: \"N-A\" },\n      { id: `nested-items-${id}-b`, text: \"N-B\" },\n      { id: `nested-items-${id}-c`, text: \"N-C\" },\n      { id: `nested-items-${id}-d`, text: \"N-D\" },\n    ]);\n  }\n\n  // now reorder the nested stream of items-a\n  await page.locator(\"#items-a button\").click();\n  await syncLV(page);\n\n  expect(await childItems(page, \"items-a\")).toEqual([\n    { id: \"nested-items-a-e\", text: \"N-E\" },\n    { id: \"nested-items-a-a\", text: \"N-A\" },\n    { id: \"nested-items-a-f\", text: \"N-F\" },\n    { id: \"nested-items-a-g\", text: \"N-G\" },\n  ]);\n  // unchanged\n  for (const id of [\"b\", \"c\", \"d\"]) {\n    expect(await childItems(page, `items-${id}`)).toEqual([\n      { id: `nested-items-${id}-a`, text: \"N-A\" },\n      { id: `nested-items-${id}-b`, text: \"N-B\" },\n      { id: `nested-items-${id}-c`, text: \"N-C\" },\n      { id: `nested-items-${id}-d`, text: \"N-D\" },\n    ]);\n  }\n\n  // now reorder the parent stream\n  await page.locator(\"#parent-reorder\").click();\n  await syncLV(page);\n  expect(await listItems(page)).toEqual([\n    { id: \"items-e\", text: expect.stringMatching(/E/) },\n    { id: \"items-a\", text: expect.stringMatching(/A/) },\n    { id: \"items-f\", text: expect.stringMatching(/F/) },\n    { id: \"items-g\", text: expect.stringMatching(/G/) },\n  ]);\n\n  // the new children's stream items have the correct order\n  for (const id of [\"e\", \"f\", \"g\"]) {\n    expect(await childItems(page, `items-${id}`)).toEqual([\n      { id: `nested-items-${id}-a`, text: \"N-A\" },\n      { id: `nested-items-${id}-b`, text: \"N-B\" },\n      { id: `nested-items-${id}-c`, text: \"N-C\" },\n      { id: `nested-items-${id}-d`, text: \"N-D\" },\n    ]);\n  }\n\n  // Item A has the same children as before, still reordered\n  expect(await childItems(page, \"items-a\")).toEqual([\n    { id: \"nested-items-a-e\", text: \"N-E\" },\n    { id: \"nested-items-a-a\", text: \"N-A\" },\n    { id: \"nested-items-a-f\", text: \"N-F\" },\n    { id: \"nested-items-a-g\", text: \"N-G\" },\n  ]);\n});\n\ntest(\"phx-remove is handled correctly when restoring nodes\", async ({\n  page,\n}) => {\n  await page.goto(\"/stream/reset?phx-remove\");\n  await syncLV(page);\n\n  expect(await listItems(page)).toEqual([\n    { id: \"items-a\", text: \"A\" },\n    { id: \"items-b\", text: \"B\" },\n    { id: \"items-c\", text: \"C\" },\n    { id: \"items-d\", text: \"D\" },\n  ]);\n\n  await page.getByRole(\"button\", { name: \"Filter\" }).click();\n  await syncLV(page);\n\n  expect(await listItems(page)).toEqual([\n    { id: \"items-b\", text: \"B\" },\n    { id: \"items-c\", text: \"C\" },\n    { id: \"items-d\", text: \"D\" },\n  ]);\n\n  await page.getByRole(\"button\", { name: \"Reset\" }).click();\n  await syncLV(page);\n\n  expect(await listItems(page)).toEqual([\n    { id: \"items-a\", text: \"A\" },\n    { id: \"items-b\", text: \"B\" },\n    { id: \"items-c\", text: \"C\" },\n    { id: \"items-d\", text: \"D\" },\n  ]);\n});\n\ntest(\"issue #3129 - streams asynchronously assigned and rendered inside a comprehension\", async ({\n  page,\n}) => {\n  await page.goto(\"/stream/inside-for\");\n  await syncLV(page);\n\n  expect(await listItems(page)).toEqual([\n    { id: \"items-a\", text: \"A\" },\n    { id: \"items-b\", text: \"B\" },\n    { id: \"items-c\", text: \"C\" },\n    { id: \"items-d\", text: \"D\" },\n  ]);\n});\n\ntest(\"issue #3260 - supports non-stream items with id in stream container\", async ({\n  page,\n}) => {\n  await page.goto(\"/stream?empty_item\");\n\n  expect(await usersInDom(page, \"users\")).toEqual([\n    { id: \"users-1\", text: \"chris\" },\n    { id: \"users-2\", text: \"callan\" },\n    { id: \"users-empty\", text: \"Empty!\" },\n  ]);\n\n  await expect(page.getByText(\"Empty\")).toBeHidden();\n  await evalLV(page, 'socket.view.handle_event(\"reset-users\", %{}, socket)');\n  await expect(page.getByText(\"Empty\")).toBeVisible();\n  expect(await usersInDom(page, \"users\")).toEqual([\n    { id: \"users-empty\", text: \"Empty!\" },\n  ]);\n\n  await evalLV(page, 'socket.view.handle_event(\"append-users\", %{}, socket)');\n  await expect(page.getByText(\"Empty\")).toBeHidden();\n  expect(await usersInDom(page, \"users\")).toEqual([\n    { id: \"users-4\", text: \"foo\" },\n    { id: \"users-3\", text: \"last_user\" },\n    { id: \"users-empty\", text: \"Empty!\" },\n  ]);\n});\n\ntest(\"JS commands are applied when re-joining\", async ({ page }) => {\n  await page.goto(\"/stream\");\n  await syncLV(page);\n\n  expect(await usersInDom(page, \"users\")).toEqual([\n    { id: \"users-1\", text: \"chris\" },\n    { id: \"users-2\", text: \"callan\" },\n  ]);\n  await expect(page.locator(\"#users-1\")).toBeVisible();\n  await page\n    .locator(\"#users-1\")\n    .getByRole(\"button\", { name: \"JS Hide\" })\n    .click();\n  await expect(page.locator(\"#users-1\")).toBeHidden();\n  await page.evaluate(\n    () => new Promise((resolve) => window.liveSocket.disconnect(resolve)),\n  );\n  // not reconnect\n  await page.evaluate(() => window.liveSocket.connect());\n  await syncLV(page);\n  // should still be hidden\n  await expect(page.locator(\"#users-1\")).toBeHidden();\n});\n\ntest(\"update_only\", async ({ page }) => {\n  await page.goto(\"/stream/reset\");\n  await syncLV(page);\n\n  expect(await listItems(page)).toEqual([\n    { id: \"items-a\", text: \"A\" },\n    { id: \"items-b\", text: \"B\" },\n    { id: \"items-c\", text: \"C\" },\n    { id: \"items-d\", text: \"D\" },\n  ]);\n\n  await page.getByRole(\"button\", { name: \"Add E (update only)\" }).click();\n  await syncLV(page);\n\n  expect(await listItems(page)).toEqual([\n    { id: \"items-a\", text: \"A\" },\n    { id: \"items-b\", text: \"B\" },\n    { id: \"items-c\", text: \"C\" },\n    { id: \"items-d\", text: \"D\" },\n  ]);\n\n  await page.getByRole(\"button\", { name: \"Update C (update only)\" }).click();\n  await syncLV(page);\n\n  expect(await listItems(page)).toEqual([\n    { id: \"items-a\", text: \"A\" },\n    { id: \"items-b\", text: \"B\" },\n    { id: \"items-c\", text: expect.stringMatching(/C .*/) },\n    { id: \"items-d\", text: \"D\" },\n  ]);\n});\n\n// https://github.com/phoenixframework/phoenix_live_view/issues/3784\n// empty text nodes would accumulate inside streams, leading to linear\n// growth. When locked trees were involved, the growth could become\n// exponential though, because text nodes from the clone would accumulate,\n// doubling the text nodes on unlock\ntest(\"empty text nodes are pruned\", async ({ page }) => {\n  await page.goto(\"/stream/limit\");\n  await syncLV(page);\n\n  const initialCount = await page.evaluate(\n    () => document.getElementById(\"items\").innerHTML.match(/\\n/g).length,\n  );\n  for (let i = 0; i < 10; i++) {\n    await page.getByRole(\"button\", { name: \"add 10\" }).click();\n  }\n\n  await syncLV(page);\n  const newCount = await page.evaluate(\n    () => document.getElementById(\"items\").innerHTML.match(/\\n/g).length,\n  );\n\n  await expect(newCount).toEqual(initialCount);\n});\n"
  },
  {
    "path": "test/e2e/tests/uploads.spec.js",
    "content": "import { test, expect } from \"../test-fixtures\";\nimport { syncLV, attributeMutations } from \"../utils\";\n\n// https://stackoverflow.com/questions/10623798/how-do-i-read-the-contents-of-a-node-js-stream-into-a-string-variable\nconst readStream = (stream) =>\n  new Promise((resolve) => {\n    const chunks = [];\n\n    stream.on(\"data\", function (chunk) {\n      chunks.push(chunk);\n    });\n\n    // Send the buffer or you can put it into a var\n    stream.on(\"end\", function () {\n      resolve(Buffer.concat(chunks));\n    });\n  });\n\ntest(\"can upload a file\", async ({ page }) => {\n  await page.goto(\"/upload\");\n  await syncLV(page);\n\n  const changesForm = attributeMutations(page, \"#upload-form\");\n  const changesInput = attributeMutations(page, \"#upload-form input\");\n\n  // wait for the change listeners to be ready\n  await page.waitForTimeout(50);\n\n  await page.locator(\"#upload-form input\").setInputFiles({\n    name: \"file.txt\",\n    mimeType: \"text/plain\",\n    buffer: Buffer.from(\"this is a test\"),\n  });\n  await syncLV(page);\n  await expect(page.locator(\"progress\")).toHaveAttribute(\"value\", \"0\");\n  await page.getByRole(\"button\", { name: \"Upload\" }).click();\n\n  // we should see one uploaded file in the list\n  await expect(page.locator(\"ul li\")).toBeVisible();\n\n  expect(await changesForm()).toEqual(\n    expect.arrayContaining([\n      { attr: \"class\", oldValue: null, newValue: \"phx-submit-loading\" },\n      { attr: \"class\", oldValue: \"phx-submit-loading\", newValue: null },\n    ]),\n  );\n\n  expect(await changesInput()).toEqual(\n    expect.arrayContaining([\n      { attr: \"class\", oldValue: null, newValue: \"phx-change-loading\" },\n      { attr: \"class\", oldValue: \"phx-change-loading\", newValue: null },\n    ]),\n  );\n\n  // now download the file to see if it contains the expected content\n  const downloadPromise = page.waitForEvent(\"download\");\n  await page.locator(\"ul li a\").click();\n  const download = await downloadPromise;\n\n  await expect(\n    download\n      .createReadStream()\n      .then(readStream)\n      .then((buf) => buf.toString()),\n  ).resolves.toEqual(\"this is a test\");\n});\n\ntest(\"can drop a file\", async ({ page }) => {\n  await page.goto(\"/upload\");\n  await syncLV(page);\n\n  // https://github.com/microsoft/playwright/issues/10667\n  // Create the DataTransfer and File\n  const dataTransfer = await page.evaluateHandle((data) => {\n    const dt = new DataTransfer();\n    // Convert the buffer to a hex array\n    const file = new File([data], \"file.txt\", { type: \"text/plain\" });\n    dt.items.add(file);\n    return dt;\n  }, \"this is a test\");\n\n  // Now dispatch\n  await page.dispatchEvent(\"section\", \"drop\", { dataTransfer });\n\n  await syncLV(page);\n  await page.getByRole(\"button\", { name: \"Upload\" }).click();\n\n  // we should see one uploaded file in the list\n  await expect(page.locator(\"ul li\")).toBeVisible();\n\n  // now download the file to see if it contains the expected content\n  const downloadPromise = page.waitForEvent(\"download\");\n  await page.locator(\"ul li a\").click();\n  const download = await downloadPromise;\n\n  await expect(\n    download\n      .createReadStream()\n      .then(readStream)\n      .then((buf) => buf.toString()),\n  ).resolves.toEqual(\"this is a test\");\n});\n\ntest(\"can upload multiple files\", async ({ page }) => {\n  await page.goto(\"/upload\");\n  await syncLV(page);\n\n  await page.locator(\"#upload-form input\").setInputFiles([\n    {\n      name: \"file.txt\",\n      mimeType: \"text/plain\",\n      buffer: Buffer.from(\"this is a test\"),\n    },\n    {\n      name: \"file.md\",\n      mimeType: \"text/markdown\",\n      buffer: Buffer.from(\"## this is a markdown file\"),\n    },\n  ]);\n  await syncLV(page);\n  await page.getByRole(\"button\", { name: \"Upload\" }).click();\n\n  // we should see two uploaded files in the list\n  await expect(page.locator(\"ul li\")).toHaveCount(2);\n});\n\ntest(\"shows error when there are too many files\", async ({ page }) => {\n  await page.goto(\"/upload\");\n  await syncLV(page);\n\n  await page.locator(\"#upload-form input\").setInputFiles([\n    {\n      name: \"file.txt\",\n      mimeType: \"text/plain\",\n      buffer: Buffer.from(\"this is a test\"),\n    },\n    {\n      name: \"file.md\",\n      mimeType: \"text/markdown\",\n      buffer: Buffer.from(\"## this is a markdown file\"),\n    },\n    {\n      name: \"file2.txt\",\n      mimeType: \"text/plain\",\n      buffer: Buffer.from(\"another file\"),\n    },\n  ]);\n  await syncLV(page);\n\n  await expect(page.locator(\".alert\")).toContainText(\n    \"You have selected too many files\",\n  );\n});\n\ntest(\"shows error for invalid mimetype\", async ({ page }) => {\n  await page.goto(\"/upload\");\n  await syncLV(page);\n\n  await page.locator(\"#upload-form input\").setInputFiles([\n    {\n      name: \"file.html\",\n      mimeType: \"text/html\",\n      buffer: Buffer.from(\"<h1>Hi</h1>\"),\n    },\n  ]);\n  await syncLV(page);\n\n  await expect(page.locator(\".alert\")).toContainText(\n    \"You have selected an unacceptable file type\",\n  );\n});\n\ntest(\"auto upload\", async ({ page }) => {\n  await page.goto(\"/upload?auto_upload=1\");\n  await syncLV(page);\n\n  const changes = attributeMutations(page, \"#upload-form input\");\n  // wait for the change listeners to be ready\n  await page.waitForTimeout(50);\n  await page.locator(\"#upload-form input\").setInputFiles([\n    {\n      name: \"file.txt\",\n      mimeType: \"text/plain\",\n      buffer: Buffer.from(\"this is a test\"),\n    },\n  ]);\n  await syncLV(page);\n  await expect(page.locator(\"progress\")).toHaveAttribute(\"value\", \"100\");\n\n  expect(await changes()).toEqual(\n    expect.arrayContaining([\n      { attr: \"class\", oldValue: null, newValue: \"phx-change-loading\" },\n      { attr: \"class\", oldValue: \"phx-change-loading\", newValue: null },\n    ]),\n  );\n\n  await page.getByRole(\"button\", { name: \"Upload\" }).click();\n\n  await expect(page.locator(\"ul li\")).toBeVisible();\n});\n\ntest(\"issue 3115 - cancelled upload is not re-added\", async ({ page }) => {\n  await page.goto(\"/upload\");\n  await syncLV(page);\n\n  await page.locator(\"#upload-form input\").setInputFiles([\n    {\n      name: \"file.txt\",\n      mimeType: \"text/plain\",\n      buffer: Buffer.from(\"this is a test\"),\n    },\n  ]);\n  await syncLV(page);\n  // cancel the file\n  await page.getByLabel(\"cancel\").click();\n\n  // add other file\n  await page.locator(\"#upload-form input\").setInputFiles([\n    {\n      name: \"file.md\",\n      mimeType: \"text/markdown\",\n      buffer: Buffer.from(\"## this is a markdown file\"),\n    },\n  ]);\n  await syncLV(page);\n  await page.getByRole(\"button\", { name: \"Upload\" }).click();\n  await syncLV(page);\n\n  // we should see one uploaded file in the list\n  await expect(page.locator(\"ul li\")).toHaveCount(1);\n});\n\ntest(\"submitting invalid form multiple times doesn't crash\", async ({\n  page,\n}) => {\n  // https://github.com/phoenixframework/phoenix_live_view/pull/3133#issuecomment-1962439904\n  await page.goto(\"/upload\");\n  await syncLV(page);\n\n  const logs = [];\n  page.on(\"console\", (e) => logs.push(e.text()));\n\n  await page.locator(\"#upload-form input\").setInputFiles([\n    {\n      name: \"file.txt\",\n      mimeType: \"text/plain\",\n      buffer: Buffer.from(\"this is a test\"),\n    },\n    {\n      name: \"file.md\",\n      mimeType: \"text/markdown\",\n      buffer: Buffer.from(\"## this is a markdown file\"),\n    },\n    {\n      name: \"file2.md\",\n      mimeType: \"text/markdown\",\n      buffer: Buffer.from(\"## this is another markdown file\"),\n    },\n  ]);\n  await syncLV(page);\n  await page.getByRole(\"button\", { name: \"Upload\" }).click();\n  await page.getByRole(\"button\", { name: \"Upload\" }).click();\n  await syncLV(page);\n\n  expect(logs).not.toEqual(\n    expect.arrayContaining([expect.stringMatching(\"view crashed\")]),\n  );\n  await expect(page.locator(\".alert\")).toContainText(\n    \"You have selected too many files\",\n  );\n});\n\ntest(\"auto upload - can submit files after fixing too many files error\", async ({\n  page,\n}) => {\n  // https://github.com/phoenixframework/phoenix_live_view/commit/80ddf356faaded097358a784c2515c50c345713e\n  await page.goto(\"/upload?auto_upload=1\");\n  await syncLV(page);\n\n  const logs = [];\n  page.on(\"console\", (e) => logs.push(e.text()));\n\n  await page.locator(\"#upload-form input\").setInputFiles([\n    {\n      name: \"file.txt\",\n      mimeType: \"text/plain\",\n      buffer: Buffer.from(\"this is a test\"),\n    },\n    {\n      name: \"file.md\",\n      mimeType: \"text/markdown\",\n      buffer: Buffer.from(\"## this is a markdown file\"),\n    },\n    {\n      name: \"file2.md\",\n      mimeType: \"text/markdown\",\n      buffer: Buffer.from(\"## this is another markdown file\"),\n    },\n  ]);\n  // before the fix, this already failed because the phx-change-loading class was not removed\n  // because undoRefs failed\n  await syncLV(page);\n  expect(logs).not.toEqual(\n    expect.arrayContaining([\n      expect.stringMatching(\"no preflight upload response returned with ref\"),\n    ]),\n  );\n\n  // too many files\n  await expect(page.locator(\"ul li\")).toHaveCount(0);\n  await expect(page.locator(\".alert\")).toContainText(\n    \"You have selected too many files\",\n  );\n  // now remove the file that caused the error\n  await page\n    .locator(\"article\")\n    .filter({ hasText: \"file2.md\" })\n    .getByLabel(\"cancel\")\n    .click();\n  // now we can upload the files\n  await page.getByRole(\"button\", { name: \"Upload\" }).click();\n  await syncLV(page);\n\n  await expect(page.locator(\".alert\")).toBeHidden();\n  await expect(page.locator(\"ul li\")).toHaveCount(2);\n});\n"
  },
  {
    "path": "test/e2e/utils.js",
    "content": "import { expect } from \"@playwright/test\";\nimport Crypto from \"node:crypto\";\n\nexport const randomString = (size = 21) =>\n  Crypto.randomBytes(size).toString(\"base64\").slice(0, size);\n\n// a helper function to wait until the LV has no pending events\nexport const syncLV = async (page) => {\n  const promises = [\n    expect(page.locator(\".phx-connected\").first()).toBeVisible(),\n    expect(page.locator(\".phx-change-loading\")).toHaveCount(0),\n    expect(page.locator(\".phx-click-loading\")).toHaveCount(0),\n    expect(page.locator(\".phx-submit-loading\")).toHaveCount(0),\n  ];\n  return Promise.all(promises);\n};\n\n// this function executes the given code inside the liveview that is responsible\n// for the given selector; it uses private phoenix live view js functions, so it could\n// break in the future\n// we handle the evaluation in a LV hook\nexport const evalLV = async (page, code, selector = \"[data-phx-main]\") =>\n  await page.evaluate(\n    ([code, selector]) => {\n      return new Promise((resolve) => {\n        window.liveSocket.main.withinTargets(\n          selector,\n          (targetView, targetCtx) => {\n            targetView.pushEvent(\n              \"event\",\n              document.body,\n              targetCtx,\n              \"sandbox:eval\",\n              { value: code },\n              {},\n              ({ result }) => resolve(result),\n            );\n          },\n        );\n      });\n    },\n    [code, selector],\n  );\n\n// executes the given code inside a new process\n// (in context of a plug request)\nexport const evalPlug = async (request, code) => {\n  return await request\n    .post(\"/eval\", {\n      data: { code },\n    })\n    .then((resp) => resp.json());\n};\n\nexport const attributeMutations = (page, selector) => {\n  // this is a bit of a hack, basically we create a MutationObserver on the page\n  // that will record any changes to a selector until the promise is awaited\n  //\n  // we use a random id to store the resolve function in the window object\n  const id = randomString(24);\n  // this promise resolves to the mutation list\n  const promise = page.locator(selector).evaluate((target, id) => {\n    return new Promise((resolve) => {\n      const mutations = [];\n      let observer;\n      window[id] = () => {\n        if (observer) observer.disconnect();\n        resolve(mutations);\n        delete window[id];\n      };\n      // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver\n      observer = new MutationObserver((mutationsList, _observer) => {\n        mutationsList.forEach((mutation) => {\n          if (mutation.type === \"attributes\") {\n            mutations.push({\n              attr: mutation.attributeName,\n              oldValue: mutation.oldValue,\n              newValue: mutation.target.getAttribute(mutation.attributeName),\n            });\n          }\n        });\n      }).observe(target, { attributes: true, attributeOldValue: true });\n    });\n  }, id);\n\n  return () => {\n    // we want to stop observing!\n    page.locator(selector).evaluate((_target, id) => window[id](), id);\n    // return the result of the initial promise\n    return promise;\n  };\n};\n"
  },
  {
    "path": "test/phoenix_component/components_test.exs",
    "content": "defmodule Phoenix.LiveView.ComponentsTest do\n  use ExUnit.Case, async: true\n\n  import ExUnit.CaptureIO\n  import Phoenix.Component\n  import Phoenix.LiveViewTest.TreeDOM, only: [t2h: 1, sigil_X: 2, sigil_x: 2]\n\n  alias Phoenix.LiveViewTest.TreeDOM\n\n  describe \"link patch\" do\n    test \"basic usage\" do\n      assigns = %{}\n\n      assert t2h(~H|<.link patch=\"/home\">text</.link>|) ==\n               ~X[<a data-phx-link=\"patch\" data-phx-link-state=\"push\" href=\"/home\">text</a>]\n    end\n\n    test \"forwards global dom attributes\" do\n      assigns = %{}\n\n      assert t2h(~H|<.link patch=\"/\" class=\"btn btn-large\" data={[page_number: 2]}>next</.link>|) ==\n               ~X[<a class=\"btn btn-large\" data-page-number=\"2\" data-phx-link=\"patch\" data-phx-link-state=\"push\" href=\"/\">next</a>]\n    end\n  end\n\n  describe \"link navigate\" do\n    test \"basic usage\" do\n      assigns = %{}\n\n      assert t2h(~H|<.link navigate=\"/\">text</.link>|) ==\n               ~X[<a data-phx-link=\"redirect\" data-phx-link-state=\"push\" href=\"/\">text</a>]\n    end\n\n    test \"forwards global dom attributes\" do\n      assigns = %{}\n\n      assert t2h(\n               ~H|<.link navigate=\"/\" class=\"btn btn-large\" data={[page_number: 2]}>text</.link>|\n             ) ==\n               ~X[<a class=\"btn btn-large\" data-page-number=\"2\" data-phx-link=\"redirect\" data-phx-link-state=\"push\" href=\"/\">text</a>]\n    end\n  end\n\n  describe \"link href\" do\n    test \"basic usage\" do\n      assigns = %{}\n      assert t2h(~H|<.link href=\"/\">text</.link>|) == ~X|<a href=\"/\">text</a>|\n    end\n\n    test \"arbitrary attrs\" do\n      assigns = %{}\n\n      assert t2h(~H|<.link href=\"/\" class=\"foo\">text</.link>|) ==\n               ~X|<a href=\"/\" class=\"foo\">text</a>|\n    end\n\n    test \"with no href\" do\n      assigns = %{}\n\n      assert t2h(~H|<.link phx-click=\"click\">text</.link>|) ==\n               ~X|<a href=\"#\" phx-click=\"click\">text</a>|\n    end\n\n    test \"with # ref\" do\n      assigns = %{}\n\n      assert t2h(~H|<.link href=\"#\" phx-click=\"click\">text</.link>|) ==\n               ~X|<a href=\"#\" phx-click=\"click\">text</a>|\n    end\n\n    test \"with nil href\" do\n      assigns = %{}\n\n      assert t2h(~H|<.link href={nil} phx-click=\"click\">text</.link>|) ==\n               ~X|<a href=\"#\" phx-click=\"click\">text</a>|\n    end\n\n    test \"csrf with get method\" do\n      assigns = %{}\n\n      assert t2h(~H|<.link href=\"/\" method=\"get\">text</.link>|) == ~X|<a href=\"/\">text</a>|\n\n      assert t2h(~H|<.link href=\"/\" method=\"get\" csrf_token=\"123\">text</.link>|) ==\n               ~X|<a href=\"/\">text</a>|\n    end\n\n    test \"csrf with non-get method\" do\n      assigns = %{}\n      csrf = Plug.CSRFProtection.get_csrf_token_for(\"/users\")\n\n      assert t2h(~H|<.link href=\"/users\" method=\"delete\">delete</.link>|) ==\n               ~x|<a href=\"/users\" data-method=\"delete\" data-csrf=\"#{csrf}\" data-to=\"/users\">delete</a>|\n\n      assert t2h(~H|<.link href=\"/users\" method=\"delete\" csrf_token={true}>delete</.link>|) ==\n               ~x|<a href=\"/users\" data-method=\"delete\" data-csrf=\"#{csrf}\" data-to=\"/users\">delete</a>|\n\n      assert t2h(~H|<.link href=\"/users\" method=\"delete\" csrf_token={false}>delete</.link>|) ==\n               ~X|<a href=\"/users\" data-method=\"delete\" data-to=\"/users\">delete</a>|\n    end\n\n    test \"csrf with custom token\" do\n      assigns = %{}\n\n      assert t2h(~H|<.link href=\"/users\" method=\"post\" csrf_token=\"123\">delete</.link>|) ==\n               ~X|<a href=\"/users\" data-method=\"post\" data-csrf=\"123\" data-to=\"/users\">delete</a>|\n    end\n\n    test \"csrf with confirm\" do\n      assigns = %{}\n\n      assert t2h(\n               ~H|<.link href=\"/users\" method=\"post\" csrf_token=\"123\" data-confirm=\"are you sure?\">delete</.link>|\n             ) ==\n               ~X|<a href=\"/users\" data-method=\"post\" data-csrf=\"123\" data-to=\"/users\" data-confirm=\"are you sure?\">delete</a>|\n    end\n\n    test \"js schemes\" do\n      assigns = %{}\n\n      assert t2h(~H|<.link href={{:javascript, \"alert('bad')\"}}>js</.link>|) ==\n               ~X|<a href=\"javascript:alert(&#39;bad&#39;)\">js</a>|\n    end\n\n    test \"invalid schemes\" do\n      assigns = %{}\n\n      assert_raise ArgumentError, ~r/unsupported scheme given to <.link>/, fn ->\n        t2h(~H|<.link href=\"javascript:alert('bad')\">bad</.link>|) ==\n          ~X|<a href=\"/users\" data-method=\"post\" data-csrf=\"123\">delete</a>|\n      end\n    end\n  end\n\n  describe \"focus_wrap\" do\n    test \"basic usage\" do\n      assigns = %{}\n\n      template = ~H\"\"\"\n      <.focus_wrap id=\"wrap\" class=\"foo\">\n        <div>content</div>\n      </.focus_wrap>\n      \"\"\"\n\n      assert t2h(template) ==\n               ~X\"\"\"\n               <div id=\"wrap\" phx-hook=\"Phoenix.FocusWrap\" class=\"foo\">\n                 <div id=\"wrap-start\" tabindex=\"0\" aria-hidden=\"true\"></div>\n                 <div>content</div>\n                 <div id=\"wrap-end\" tabindex=\"0\" aria-hidden=\"true\"></div>\n               </div>\n               \"\"\"\n    end\n  end\n\n  describe \"live_title/2\" do\n    test \"dynamic attrs\" do\n      assigns = %{prefix: \"MyApp – \", title: \"My Title\"}\n\n      assert t2h(~H|<.live_title prefix={@prefix}>{@title}</.live_title>|) ==\n               ~X|<title data-prefix=\"MyApp – \">MyApp – My Title</title>|\n    end\n\n    test \"prefix only\" do\n      assigns = %{}\n\n      assert t2h(~H|<.live_title prefix=\"MyApp – \">My Title</.live_title>|) ==\n               ~X|<title data-prefix=\"MyApp – \">MyApp – My Title</title>|\n    end\n\n    test \"suffix only\" do\n      assigns = %{}\n\n      assert t2h(~H|<.live_title suffix=\" – MyApp\">My Title</.live_title>|) ==\n               ~X|<title data-suffix=\" – MyApp\">My Title – MyApp</title>|\n    end\n\n    test \"prefix and suffix\" do\n      assigns = %{}\n\n      assert t2h(~H|<.live_title prefix=\"Welcome: \" suffix=\" – MyApp\">My Title</.live_title>|) ==\n               ~X|<title data-prefix=\"Welcome: \" data-suffix=\" – MyApp\">Welcome: My Title – MyApp</title>|\n    end\n\n    test \"without prefix or suffix\" do\n      assigns = %{}\n\n      assert t2h(~H|<.live_title>My Title</.live_title>|) ==\n               ~X|<title>My Title</title>|\n    end\n\n    test \"default with blank inner block\" do\n      assigns = %{\n        val: \"\"\"\n\n\n        \"\"\"\n      }\n\n      assert t2h(~H|<.live_title default=\"DEFAULT\" phx-no-format>   <%= @val %>   </.live_title>|) ==\n               ~X|<title data-default=\"DEFAULT\">DEFAULT</title>|\n    end\n\n    test \"default with present inner block\" do\n      assigns = %{val: \"My Title\"}\n\n      assert t2h(~H|<.live_title default=\"DEFAULT\" phx-no-format>   <%= @val %>   </.live_title>|) ==\n               ~X|<title data-default=\"DEFAULT\">   My Title   </title>|\n    end\n  end\n\n  describe \"dynamic_tag/1\" do\n    test \"ensures HTML safe tag names\" do\n      assigns = %{}\n\n      assert_raise ArgumentError, ~r/expected dynamic_tag name to be safe HTML/, fn ->\n        t2h(~H|<.dynamic_tag tag_name=\"p><script>alert('nice try');</script>\" />|)\n      end\n    end\n\n    test \"escapes attribute values\" do\n      assigns = %{}\n\n      assert t2h(\n               ~H|<.dynamic_tag tag_name=\"p\" class=\"<script>alert('nice try');</script>\"></.dynamic_tag>|\n             ) == ~X|<p class=\"&lt;script&gt;alert(&#39;nice try&#39;);&lt;/script&gt;\"></p>|\n    end\n\n    test \"escapes attribute names\" do\n      assigns = %{}\n\n      assert t2h(\n               ~H|<.dynamic_tag tag_name=\"p\" {%{\"<script>alert('nice try');</script>\" => \"\"}}></.dynamic_tag>|\n             ) == ~X|<p &lt;script&gt;alert(&#39;nice try&#39;);&lt;/script&gt;=\"\"></p>|\n    end\n\n    test \"with empty inner block\" do\n      assigns = %{}\n\n      assert t2h(~H|<.dynamic_tag tag_name=\"tr\"></.dynamic_tag>|) == ~X|<tr></tr>|\n\n      assert t2h(~H|<.dynamic_tag tag_name=\"tr\" class=\"foo\"></.dynamic_tag>|) ==\n               ~X|<tr class=\"foo\"></tr>|\n    end\n\n    test \"with inner block\" do\n      assigns = %{}\n\n      assert t2h(~H|<.dynamic_tag tag_name=\"tr\">content</.dynamic_tag>|) == ~X|<tr>content</tr>|\n\n      assert t2h(~H|<.dynamic_tag tag_name=\"tr\" class=\"foo\">content</.dynamic_tag>|) ==\n               ~X|<tr class=\"foo\">content</tr>|\n    end\n\n    test \"self closing without inner block\" do\n      assigns = %{}\n\n      assert t2h(~H|<.dynamic_tag tag_name=\"br\" />|) == ~X|<br/>|\n      assert t2h(~H|<.dynamic_tag tag_name=\"input\" type=\"text\" />|) == ~X|<input type=\"text\"/>|\n    end\n\n    test \"keeps underscores in attributes\" do\n      assigns = %{}\n\n      assert t2h(~H|<.dynamic_tag tag_name=\"br\" foo_bar=\"baz\" />|) == ~X|<br foo_bar=\"baz\"/>|\n    end\n\n    test \"can pass tag_name and name\" do\n      assigns = %{}\n\n      assert t2h(~H|<.dynamic_tag tag_name=\"input\" name=\"my-input\" />|) ==\n               ~X|<input name=\"my-input\"/>|\n    end\n\n    test \"warns when using deprecated name attribute\" do\n      assigns = %{}\n\n      assert capture_io(:stderr, fn ->\n               assert t2h(~H|<.dynamic_tag name=\"br\" foo_bar=\"baz\" />|) == ~X|<br foo_bar=\"baz\"/>|\n             end) =~\n               \"Passing the tag name to `Phoenix.Component.dynamic_tag/1` using the `name` attribute is deprecated\"\n    end\n  end\n\n  describe \"form\" do\n    test \"let without :for\" do\n      assigns = %{}\n\n      template = ~H\"\"\"\n      <.form :let={f}>\n        <input id={f[:foo].id} name={f[:foo].name} type=\"text\" />\n      </.form>\n      \"\"\"\n\n      assert t2h(template) == ~X[<form><input id=\"foo\" name=\"foo\" type=\"text\"></input></form>]\n    end\n\n    test \"generates form with prebuilt form\" do\n      assigns = %{form: to_form(%{})}\n\n      template = ~H\"\"\"\n      <.form for={@form}>\n        <input id={@form[:foo].id} name={@form[:foo].name} type=\"text\" />\n      </.form>\n      \"\"\"\n\n      assert t2h(template) == ~X[<form><input id=\"foo\" name=\"foo\" type=\"text\"></input></form>]\n    end\n\n    test \"generates form with prebuilt form and :as\" do\n      assigns = %{form: to_form(%{}, as: :data)}\n\n      template = ~H\"\"\"\n      <.form :let={f} for={@form}>\n        <input id={f[:foo].id} name={f[:foo].name} type=\"text\" />\n      </.form>\n      \"\"\"\n\n      assert t2h(template) ==\n               ~X{<form><input id=\"data_foo\" name=\"data[foo]\" type=\"text\"></input></form>}\n    end\n\n    test \"generates form with prebuilt form and options\" do\n      assigns = %{form: to_form(%{})}\n\n      template = ~H\"\"\"\n      <.form :let={f} for={@form} as=\"base\" data-foo=\"bar\" class=\"pretty\" phx-change=\"valid\">\n        <input id={f[:foo].id} name={f[:foo].name} type=\"text\" />\n      </.form>\n      \"\"\"\n\n      assert t2h(template) ==\n               ~X\"\"\"\n               <form data-foo=\"bar\" class=\"pretty\" phx-change=\"valid\">\n                 <input id=\"base_foo\" name=base[foo] type=\"text\"/>\n               </form>\n               \"\"\"\n    end\n\n    test \"generates form with prebuilt form and errors\" do\n      assigns = %{form: to_form(%{})}\n\n      template = ~H\"\"\"\n      <.form :let={form} for={@form} errors={[name: \"can't be blank\"]}>\n        {inspect(form.errors)}\n      </.form>\n      \"\"\"\n\n      assert t2h(template) == [{\"form\", [], [\"\\n  \\n  \\n  \\n  [name: \\\"can't be blank\\\"]\\n\\n\"]}]\n    end\n\n    test \"generates form with form data\" do\n      assigns = %{}\n\n      template = ~H\"\"\"\n      <.form :let={f} for={%{}}>\n        <input id={f[:foo].id} name={f[:foo].name} type=\"text\" />\n      </.form>\n      \"\"\"\n\n      assert t2h(template) ==\n               ~X{<form><input id=\"foo\" name=\"foo\" type=\"text\"></input></form>}\n    end\n\n    test \"does not raise when action is given and method is missing\" do\n      assigns = %{}\n\n      template = ~H\"\"\"\n      <.form for={%{}} action=\"/\"></.form>\n      \"\"\"\n\n      csrf_token = Plug.CSRFProtection.get_csrf_token_for(\"/\")\n\n      assert t2h(template) ==\n               ~x{<form action=\"/\" method=\"post\"><input name=\"_csrf_token\" type=\"hidden\" hidden=\"\" value=\"#{csrf_token}\"></input></form>}\n    end\n\n    test \"generates a csrf_token if if an action is set\" do\n      assigns = %{}\n\n      template = ~H\"\"\"\n      <.form :let={f} for={%{}} action=\"/\">\n        <input id={f[:foo].id} name={f[:foo].name} type=\"text\" />\n      </.form>\n      \"\"\"\n\n      csrf_token = Plug.CSRFProtection.get_csrf_token_for(\"/\")\n\n      assert t2h(template) ==\n               ~x\"\"\"\n               <form action=\"/\" method=\"post\">\n                 <input name=\"_csrf_token\" type=\"hidden\" hidden=\"\" value=\"#{csrf_token}\"></input>\n                 <input id=\"foo\" name=\"foo\" type=\"text\"></input>\n               </form>\n               \"\"\"\n    end\n\n    test \"does not generate csrf_token if method is not post or if no action\" do\n      assigns = %{}\n\n      template = ~H\"\"\"\n      <.form :let={f} for={%{}} method=\"get\" action=\"/\">\n        <input id={f[:foo].id} name={f[:foo].name} type=\"text\" />\n      </.form>\n      \"\"\"\n\n      assert t2h(template) ==\n               ~X\"\"\"\n               <form action=\"/\" method=\"get\">\n                 <input id=\"foo\" name=\"foo\" type=\"text\"></input>\n               </form>\n               \"\"\"\n\n      template = ~H\"\"\"\n      <.form :let={f} for={%{}}>\n        <input id={f[:foo].id} name={f[:foo].name} type=\"text\" />\n      </.form>\n      \"\"\"\n\n      assert t2h(template) ==\n               ~X\"\"\"\n               <form>\n                 <input id=\"foo\" name=\"foo\" type=\"text\"></input>\n               </form>\n               \"\"\"\n    end\n\n    test \"generates form with available options and custom attributes\" do\n      assigns = %{}\n\n      template = ~H\"\"\"\n      <.form\n        :let={user_form}\n        for={%{}}\n        id=\"form\"\n        action=\"/\"\n        method=\"put\"\n        multipart\n        csrf_token=\"123\"\n        as={:user}\n        errors={[name: \"can't be blank\"]}\n        data-foo=\"bar\"\n        class=\"pretty\"\n        phx-change=\"valid\"\n      >\n        <input id={user_form[:foo].id} name={user_form[:foo].name} type=\"text\" />\n        {inspect(user_form.errors)}\n      </.form>\n      \"\"\"\n\n      assert t2h(template) ==\n               ~X\"\"\"\n               <form\n                 id=\"form\"\n                 action=\"/\"\n                 method=\"post\"\n                 enctype=\"multipart/form-data\"\n                 data-foo=\"bar\"\n                 class=\"pretty\"\n                 phx-change=\"valid\"\n               >\n                 <input name=\"_method\" type=\"hidden\" hidden=\"\" value=\"put\">\n                 <input name=\"_csrf_token\" type=\"hidden\" hidden=\"\" value=\"123\">\n                 <input id=\"form_foo\" name=\"user[foo]\" type=\"text\">\n                 [name: \"can't be blank\"]\n\n               </form>\n               \"\"\"\n    end\n\n    test \"method is case insensitive when using get or post with action\" do\n      assigns = %{}\n\n      template = ~H\"\"\"\n      <.form for={%{}} method=\"GET\" action=\"/\"></.form>\n      \"\"\"\n\n      assert t2h(template) ==\n               ~x{<form method=\"get\" action=\"/\"></form>}\n\n      template = ~H\"\"\"\n      <.form for={%{}} method=\"PoST\" action=\"/\"></.form>\n      \"\"\"\n\n      csrf = Plug.CSRFProtection.get_csrf_token_for(\"/\")\n\n      assert t2h(template) ==\n               ~x{<form method=\"post\" action=\"/\"><input name=\"_csrf_token\" type=\"hidden\" hidden=\"\" value=\"#{csrf}\"></form>}\n\n      # for anything != get or post we use post and set the hidden _method field\n      template = ~H\"\"\"\n      <.form for={%{}} method=\"PuT\" action=\"/\"></.form>\n      \"\"\"\n\n      assert t2h(template) ==\n               ~x\"\"\"\n               <form action=\"/\" method=\"post\">\n                 <input name=\"_method\" type=\"hidden\" hidden=\"\" value=\"PuT\">\n                 <input name=\"_csrf_token\" type=\"hidden\" hidden=\"\" value=\"#{csrf}\">\n               </form>\n               \"\"\"\n    end\n  end\n\n  describe \"inputs_for\" do\n    test \"generates nested inputs with no options\" do\n      assigns = %{}\n\n      template = ~H\"\"\"\n      <.form :let={f} as={:myform}>\n        <.inputs_for :let={finner} field={f[:inner]}>\n          <% 0 = finner.index %>\n          <input id={finner[:foo].id} name={finner[:foo].name} type=\"text\" />\n        </.inputs_for>\n      </.form>\n      \"\"\"\n\n      assert t2h(template) ==\n               ~X\"\"\"\n               <form>\n                 <input type=\"hidden\" name=\"myform[inner][_persistent_id]\" value=\"0\"> </input>\n                 <input id=\"myform_inner_0_foo\" name=\"myform[inner][foo]\" type=\"text\"></input>\n               </form>\n               \"\"\"\n    end\n\n    test \"with naming options\" do\n      assigns = %{}\n\n      template = ~H\"\"\"\n      <.form :let={f} as={:myform}>\n        <.inputs_for :let={finner} field={f[:inner]} id=\"test\" as={:name}>\n          <input id={finner[:foo].id} name={finner[:foo].name} type=\"text\" />\n        </.inputs_for>\n      </.form>\n      \"\"\"\n\n      assert t2h(template) ==\n               ~X\"\"\"\n               <form>\n                 <input type=\"hidden\" name=\"name[_persistent_id]\" value=\"0\"> </input>\n                 <input id=\"test_inner_0_foo\" name=\"name[foo]\" type=\"text\"></input>\n               </form>\n               \"\"\"\n\n      template = ~H\"\"\"\n      <.form :let={f} as={:myform}>\n        <.inputs_for :let={finner} field={f[:inner]} as={:name}>\n          <input id={finner[:foo].id} name={finner[:foo].name} type=\"text\" />\n        </.inputs_for>\n      </.form>\n      \"\"\"\n\n      assert t2h(template) ==\n               ~X\"\"\"\n               <form>\n                 <input type=\"hidden\" name=\"name[_persistent_id]\" value=\"0\"> </input>\n                 <input id=\"myform_inner_0_foo\" name=\"name[foo]\" type=\"text\"></input>\n               </form>\n               \"\"\"\n    end\n\n    test \"with default map option\" do\n      assigns = %{}\n\n      template = ~H\"\"\"\n      <.form :let={f} as={:myform}>\n        <.inputs_for :let={finner} field={f[:inner]} default={%{foo: \"123\"}}>\n          <input id={finner[:foo].id} name={finner[:foo].name} type=\"text\" value={finner[:foo].value} />\n        </.inputs_for>\n      </.form>\n      \"\"\"\n\n      assert t2h(template) ==\n               ~X\"\"\"\n               <form>\n                 <input type=\"hidden\" name=\"myform[inner][_persistent_id]\" value=\"0\"> </input>\n                 <input id=\"myform_inner_0_foo\" name=\"myform[inner][foo]\" type=\"text\" value=\"123\"></input>\n               </form>\n               \"\"\"\n    end\n\n    test \"with default list and list related options\" do\n      assigns = %{}\n\n      template = ~H\"\"\"\n      <.form :let={f} as={:myform}>\n        <.inputs_for\n          :let={finner}\n          field={f[:inner]}\n          default={[%{foo: \"456\"}]}\n          prepend={[%{foo: \"123\"}]}\n          append={[%{foo: \"789\"}]}\n        >\n          <input id={finner[:foo].id} name={finner[:foo].name} type=\"text\" value={finner[:foo].value} />\n        </.inputs_for>\n      </.form>\n      \"\"\"\n\n      assert t2h(template) ==\n               ~X\"\"\"\n               <form>\n                 <input type=\"hidden\" name=\"myform[inner][0][_persistent_id]\" value=\"0\"></input>\n                 <input id=\"myform_inner_0_foo\" name=\"myform[inner][0][foo]\" type=\"text\" value=\"123\"></input>\n                 <input type=\"hidden\" name=\"myform[inner][1][_persistent_id]\" value=\"1\"></input>\n                 <input id=\"myform_inner_1_foo\" name=\"myform[inner][1][foo]\" type=\"text\" value=\"456\"></input>\n                 <input type=\"hidden\" name=\"myform[inner][2][_persistent_id]\" value=\"2\"></input>\n                 <input id=\"myform_inner_2_foo\" name=\"myform[inner][2][foo]\" type=\"text\" value=\"789\"></input>\n               </form>\n               \"\"\"\n    end\n\n    test \"with FormData implementation options\" do\n      assigns = %{}\n\n      template = ~H\"\"\"\n      <.form :let={f} as={:myform}>\n        <.inputs_for :let={finner} field={f[:inner]} options={[foo: \"bar\"]}>\n          <p>{finner.options[:foo]}</p>\n        </.inputs_for>\n      </.form>\n      \"\"\"\n\n      html = t2h(template)\n      assert [p] = TreeDOM.filter(html, &(TreeDOM.tag(&1) == \"p\"))\n      assert TreeDOM.to_text(p) =~ \"bar\"\n    end\n\n    test \"can disable persistent ids\" do\n      assigns = %{}\n\n      template = ~H\"\"\"\n      <.form :let={f} as={:myform}>\n        <.inputs_for\n          :let={finner}\n          field={f[:inner]}\n          default={[%{foo: \"456\"}, %{foo: \"789\"}]}\n          prepend={[%{foo: \"123\"}]}\n          append={[%{foo: \"101112\"}]}\n          skip_persistent_id\n        >\n          <input id={finner[:foo].id} name={finner[:foo].name} type=\"text\" value={finner[:foo].value} />\n        </.inputs_for>\n      </.form>\n      \"\"\"\n\n      assert t2h(template) ==\n               ~X\"\"\"\n               <form>\n                 <input id=\"myform_inner_0_foo\" name=\"myform[inner][0][foo]\" type=\"text\" value=\"123\"></input>\n                 <input id=\"myform_inner_1_foo\" name=\"myform[inner][1][foo]\" type=\"text\" value=\"456\"></input>\n                 <input id=\"myform_inner_2_foo\" name=\"myform[inner][2][foo]\" type=\"text\" value=\"789\"></input>\n                 <input id=\"myform_inner_3_foo\" name=\"myform[inner][3][foo]\" type=\"text\" value=\"101112\"></input>\n               </form>\n               \"\"\"\n    end\n  end\n\n  describe \"live_file_input/1\" do\n    test \"renders attributes\" do\n      assigns = %{\n        conf: %Phoenix.LiveView.UploadConfig{\n          auto_upload?: true,\n          entries: [%{preflighted?: false, done?: false, ref: \"foo\"}]\n        }\n      }\n\n      assert t2h(\n               ~H|<.live_file_input upload={@conf} class=\"<script>alert('nice try');</script>\" />|\n             ) ==\n               ~X|<input type=\"file\" accept=\"\" data-phx-hook=\"Phoenix.LiveFileUpload\" data-phx-active-refs=\"foo\" data-phx-done-refs=\"\" data-phx-preflighted-refs=\"\" data-phx-auto-upload class=\"&lt;script&gt;alert(&#39;nice try&#39;);&lt;/script&gt;\">|\n    end\n\n    test \"renders optional webkitdirectory attribute\" do\n      assigns = %{\n        conf: %Phoenix.LiveView.UploadConfig{\n          entries: [%{preflighted?: false, done?: false, ref: \"foo\"}]\n        }\n      }\n\n      assert t2h(~H|<.live_file_input upload={@conf} webkitdirectory />|) ==\n               ~X|<input type=\"file\" accept=\"\" data-phx-hook=\"Phoenix.LiveFileUpload\" data-phx-active-refs=\"foo\" data-phx-done-refs=\"\" data-phx-preflighted-refs=\"\" webkitdirectory>|\n    end\n\n    test \"renders optional capture attribute\" do\n      assigns = %{\n        conf: %Phoenix.LiveView.UploadConfig{\n          entries: [%{preflighted?: false, done?: false, ref: \"foo\"}]\n        }\n      }\n\n      assert t2h(~H|<.live_file_input upload={@conf} capture=\"user\" />|) ==\n               ~X|<input type=\"file\" accept=\"\" data-phx-hook=\"Phoenix.LiveFileUpload\" data-phx-active-refs=\"foo\" data-phx-done-refs=\"\" data-phx-preflighted-refs=\"\" capture=\"user\">|\n    end\n\n    test \"sets accept from config\" do\n      assigns = %{\n        conf: %Phoenix.LiveView.UploadConfig{\n          accept: ~w(.png),\n          entries: [%{preflighted?: false, done?: false, ref: \"foo\"}]\n        }\n      }\n\n      assert t2h(~H|<.live_file_input upload={@conf} />|) ==\n               ~X|<input type=\"file\" accept=\".png\" data-phx-hook=\"Phoenix.LiveFileUpload\" data-phx-active-refs=\"foo\" data-phx-done-refs=\"\" data-phx-preflighted-refs=\"\">|\n    end\n\n    test \"renders accept override\" do\n      assigns = %{\n        conf: %Phoenix.LiveView.UploadConfig{\n          accept: ~w(.png),\n          entries: [%{preflighted?: false, done?: false, ref: \"foo\"}]\n        }\n      }\n\n      assert t2h(~H|<.live_file_input upload={@conf} accept=\".jpeg\" />|) ==\n               ~X|<input type=\"file\" accept=\".jpeg\" data-phx-hook=\"Phoenix.LiveFileUpload\" data-phx-active-refs=\"foo\" data-phx-done-refs=\"\" data-phx-preflighted-refs=\"\">|\n    end\n  end\n\n  describe \"intersperse\" do\n    test \"generates form with no options\" do\n      assigns = %{}\n\n      template = ~H\"\"\"\n      <.intersperse :let={item} enum={[1, 2, 3]}>\n        <:separator><span class=\"sep\">|</span></:separator>\n        Item{item}\n      </.intersperse>\n      \"\"\"\n\n      assert Phoenix.LiveViewTest.rendered_to_string(template) ==\n               ~s\"\\n  Item1\\n<span class=\\\"sep\\\">|</span>\\n  Item2\\n<span class=\\\"sep\\\">|</span>\\n  Item3\\n\"\n\n      template = ~H\"\"\"\n      <.intersperse :let={item} enum={[1]}>\n        <:separator><span class=\"sep\">|</span></:separator>\n        Item{item}\n      </.intersperse>\n      \"\"\"\n\n      assert Phoenix.LiveViewTest.rendered_to_string(template) == ~s\"\\n  Item1\\n\"\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_component/declarative_assigns_test.exs",
    "content": "defmodule Phoenix.ComponentDeclarativeAssignsTest do\n  use ExUnit.Case, async: true\n\n  import Phoenix.LiveViewTest\n  import Phoenix.LiveViewTest.TreeDOM, only: [t2h: 1, sigil_X: 2]\n  use Phoenix.Component\n\n  defp render_template(mod, func, assigns) do\n    apply(mod, func, [Map.put(assigns, :__changed__, %{})])\n  end\n\n  defp render_string(mod, func, assigns) do\n    render_template(mod, func, assigns) |> rendered_to_string()\n  end\n\n  defp render_html(mod, func, assigns) do\n    render_template(mod, func, assigns) |> t2h()\n  end\n\n  test \"__global__?\" do\n    assert Phoenix.Component.Declarative.__global__?(\"id\")\n    refute Phoenix.Component.Declarative.__global__?(\"idnope\")\n    refute Phoenix.Component.Declarative.__global__?(\"not-global\")\n\n    # prefixes\n    assert Phoenix.Component.Declarative.__global__?(\"aria-label\")\n    assert Phoenix.Component.Declarative.__global__?(\"data-whatever\")\n    assert Phoenix.Component.Declarative.__global__?(\"phx-click\")\n  end\n\n  defmodule RemoteFunctionComponentWithAttrs do\n    use Phoenix.Component\n\n    attr :id, :any, required: true\n    slot(:inner_block)\n    def remote(assigns), do: ~H[]\n  end\n\n  defmodule FunctionComponentWithAttrs do\n    use Phoenix.Component\n    import RemoteFunctionComponentWithAttrs\n    alias RemoteFunctionComponentWithAttrs, as: Remote\n\n    def func1_line, do: __ENV__.line\n    attr :id, :any, required: true\n    attr :email, :string, default: nil\n    slot(:inner_block)\n    def func1(assigns), do: ~H[]\n\n    def func2_line, do: __ENV__.line\n    attr :name, :any, required: true\n    attr :age, :integer, default: 0\n    def func2(assigns), do: ~H[]\n\n    def func3_line, do: __ENV__.line\n    attr :on_cancel, :fun, required: true\n    attr :on_complete, {:fun, 2}, required: true\n    def func3(assigns), do: ~H[]\n\n    def with_global_line, do: __ENV__.line\n    attr :id, :string, default: \"container\"\n    def with_global(assigns), do: ~H[<.button id={@id} class=\"btn\" aria-hidden=\"true\" />]\n\n    attr :id, :string, required: true\n    attr :rest, :global\n    def button(assigns), do: ~H[<button id={@id} {@rest} />]\n\n    def button_with_defaults_line, do: __ENV__.line\n    attr :rest, :global, default: %{class: \"primary\"}\n    def button_with_defaults(assigns), do: ~H[<button {@rest} />]\n\n    def button_with_values_line, do: __ENV__.line\n    attr :text, :string, values: [\"Save\", \"Cancel\"]\n    def button_with_values(assigns), do: ~H[<button>{@text}</button>]\n\n    def button_with_values_and_default_1_line, do: __ENV__.line\n    attr :text, :string, values: [\"Save\", \"Cancel\"], default: \"Save\"\n    def button_with_values_and_default_1(assigns), do: ~H[<button>{@text}</button>]\n\n    def button_with_values_and_default_2_line, do: __ENV__.line\n    attr :text, :string, default: \"Save\", values: [\"Save\", \"Cancel\"]\n    def button_with_values_and_default_2(assigns), do: ~H[<button>{@text}</button>]\n\n    def button_with_examples_line, do: __ENV__.line\n    attr :text, :string, examples: [\"Save\", \"Cancel\"]\n    def button_with_examples(assigns), do: ~H[<button>{@text}</button>]\n\n    def render_line, do: __ENV__.line\n\n    def render(assigns) do\n      ~H\"\"\"\n      <!-- local -->\n      <.func1 id=\"1\" />\n      <!-- local with inner content -->\n      <.func1 id=\"2\" email=\"foo@bar\">CONTENT</.func1>\n      <!-- imported -->\n      <.remote id=\"3\" />\n      <!-- remote -->\n      <RemoteFunctionComponentWithAttrs.remote id=\"4\" />\n      <!-- remote with inner content -->\n      <RemoteFunctionComponentWithAttrs.remote id=\"5\">CONTENT</RemoteFunctionComponentWithAttrs.remote>\n      <!-- remote and aliased -->\n      <Remote.remote id=\"6\" {[dynamic: :values]} />\n      \"\"\"\n    end\n  end\n\n  test \"merges globals\" do\n    assert render_html(FunctionComponentWithAttrs, :with_global, %{}) ==\n             ~X[<button id=\"container\" aria-hidden=\"true\" class=\"btn\"></button>]\n  end\n\n  test \"merges globals with defaults\" do\n    assigns = %{id: \"btn\", style: \"display: none;\"}\n\n    assert render_html(FunctionComponentWithAttrs, :button_with_defaults, assigns) ==\n             ~X[<button class=\"primary\" id=\"btn\" style=\"display: none;\"></button>]\n\n    assert render_html(FunctionComponentWithAttrs, :button_with_defaults, %{class: \"hidden\"}) ==\n             ~X[<button class=\"hidden\"></button>]\n\n    # caller passes no globals\n    assert render_html(FunctionComponentWithAttrs, :button_with_defaults, %{}) ==\n             ~X[<button class=\"primary\"></button>]\n  end\n\n  test \"stores attributes definitions\" do\n    func1_line = FunctionComponentWithAttrs.func1_line()\n    func2_line = FunctionComponentWithAttrs.func2_line()\n    func3_line = FunctionComponentWithAttrs.func3_line()\n    with_global_line = FunctionComponentWithAttrs.with_global_line()\n    button_with_defaults_line = FunctionComponentWithAttrs.button_with_defaults_line()\n    button_with_values_line = FunctionComponentWithAttrs.button_with_values_line()\n\n    button_with_values_and_default_1_line =\n      FunctionComponentWithAttrs.button_with_values_and_default_1_line()\n\n    button_with_values_and_default_2_line =\n      FunctionComponentWithAttrs.button_with_values_and_default_2_line()\n\n    button_with_examples_line = FunctionComponentWithAttrs.button_with_examples_line()\n\n    assert FunctionComponentWithAttrs.__components__() == %{\n             func1: %{\n               kind: :def,\n               attrs: [\n                 %{\n                   name: :email,\n                   type: :string,\n                   opts: [default: nil],\n                   required: false,\n                   doc: nil,\n                   slot: nil,\n                   line: func1_line + 2\n                 },\n                 %{\n                   name: :id,\n                   type: :any,\n                   opts: [],\n                   required: true,\n                   doc: nil,\n                   slot: nil,\n                   line: func1_line + 1\n                 }\n               ],\n               slots: [\n                 %{\n                   attrs: [],\n                   doc: nil,\n                   line: func1_line + 3,\n                   name: :inner_block,\n                   opts: [],\n                   required: false,\n                   validate_attrs: true\n                 }\n               ],\n               line: func1_line + 4\n             },\n             func2: %{\n               kind: :def,\n               attrs: [\n                 %{\n                   name: :age,\n                   type: :integer,\n                   opts: [default: 0],\n                   required: false,\n                   doc: nil,\n                   slot: nil,\n                   line: func2_line + 2\n                 },\n                 %{\n                   name: :name,\n                   type: :any,\n                   opts: [],\n                   required: true,\n                   doc: nil,\n                   slot: nil,\n                   line: func2_line + 1\n                 }\n               ],\n               slots: [],\n               line: func2_line + 3\n             },\n             func3: %{\n               kind: :def,\n               attrs: [\n                 %{\n                   name: :on_cancel,\n                   type: :fun,\n                   opts: [],\n                   required: true,\n                   doc: nil,\n                   slot: nil,\n                   line: func3_line + 1\n                 },\n                 %{\n                   name: :on_complete,\n                   type: {:fun, 2},\n                   opts: [],\n                   required: true,\n                   doc: nil,\n                   slot: nil,\n                   line: func3_line + 2\n                 }\n               ],\n               slots: [],\n               line: func3_line + 3\n             },\n             with_global: %{\n               kind: :def,\n               attrs: [\n                 %{\n                   line: with_global_line + 1,\n                   name: :id,\n                   opts: [default: \"container\"],\n                   required: false,\n                   doc: nil,\n                   slot: nil,\n                   type: :string\n                 }\n               ],\n               slots: [],\n               line: with_global_line + 2\n             },\n             button_with_defaults: %{\n               kind: :def,\n               attrs: [\n                 %{\n                   line: button_with_defaults_line + 1,\n                   name: :rest,\n                   opts: [default: %{class: \"primary\"}],\n                   required: false,\n                   doc: nil,\n                   slot: nil,\n                   type: :global\n                 }\n               ],\n               slots: [],\n               line: button_with_defaults_line + 2\n             },\n             button: %{\n               kind: :def,\n               attrs: [\n                 %{\n                   line: with_global_line + 4,\n                   name: :id,\n                   opts: [],\n                   required: true,\n                   doc: nil,\n                   slot: nil,\n                   type: :string\n                 },\n                 %{\n                   line: with_global_line + 5,\n                   name: :rest,\n                   opts: [],\n                   required: false,\n                   doc: nil,\n                   slot: nil,\n                   type: :global\n                 }\n               ],\n               slots: [],\n               line: with_global_line + 6\n             },\n             button_with_values: %{\n               kind: :def,\n               attrs: [\n                 %{\n                   line: button_with_values_line + 1,\n                   name: :text,\n                   opts: [values: [\"Save\", \"Cancel\"]],\n                   required: false,\n                   doc: nil,\n                   slot: nil,\n                   type: :string\n                 }\n               ],\n               slots: [],\n               line: button_with_values_line + 2\n             },\n             button_with_values_and_default_1: %{\n               kind: :def,\n               attrs: [\n                 %{\n                   line: button_with_values_and_default_1_line + 1,\n                   name: :text,\n                   opts: [values: [\"Save\", \"Cancel\"], default: \"Save\"],\n                   required: false,\n                   doc: nil,\n                   slot: nil,\n                   type: :string\n                 }\n               ],\n               slots: [],\n               line: button_with_values_and_default_1_line + 2\n             },\n             button_with_values_and_default_2: %{\n               kind: :def,\n               attrs: [\n                 %{\n                   line: button_with_values_and_default_2_line + 1,\n                   name: :text,\n                   opts: [default: \"Save\", values: [\"Save\", \"Cancel\"]],\n                   required: false,\n                   doc: nil,\n                   slot: nil,\n                   type: :string\n                 }\n               ],\n               slots: [],\n               line: button_with_values_and_default_2_line + 2\n             },\n             button_with_examples: %{\n               kind: :def,\n               attrs: [\n                 %{\n                   line: button_with_examples_line + 1,\n                   name: :text,\n                   opts: [examples: [\"Save\", \"Cancel\"]],\n                   required: false,\n                   doc: nil,\n                   slot: nil,\n                   type: :string\n                 }\n               ],\n               slots: [],\n               line: button_with_examples_line + 2\n             }\n           }\n  end\n\n  defmodule FunctionComponentWithSlots do\n    use Phoenix.Component\n\n    def fun_with_slot_line, do: __ENV__.line + 3\n\n    slot(:inner_block)\n    def fun_with_slot(assigns), do: ~H[]\n\n    def fun_with_named_slots_line, do: __ENV__.line + 4\n\n    slot(:header)\n    slot(:footer)\n    def fun_with_named_slots(assigns), do: ~H[]\n\n    def fun_with_slot_attrs_line, do: __ENV__.line + 6\n\n    slot :slot, required: true do\n      attr :attr, :any\n    end\n\n    def fun_with_slot_attrs(assigns), do: ~H[]\n\n    def table_line, do: __ENV__.line + 8\n\n    slot :col do\n      attr :label, :string\n    end\n\n    attr :rows, :list\n\n    def table(assigns) do\n      ~H\"\"\"\n      <table>\n        <tr>\n          <%= for col <- @col do %>\n            <th>{col.label}</th>\n          <% end %>\n        </tr>\n        <%= for row <- @rows do %>\n          <tr>\n            <%= for col <- @col do %>\n              <td>{render_slot(col, row)}</td>\n            <% end %>\n          </tr>\n        <% end %>\n      </table>\n      \"\"\"\n    end\n\n    def render_line, do: __ENV__.line + 2\n\n    def render(assigns) do\n      ~H\"\"\"\n      <.fun_with_slot>\n        Hello, World\n      </.fun_with_slot>\n\n      <.fun_with_named_slots>\n        <:header>\n          This is a header.\n        </:header>\n        Hello, World\n        <:footer>\n          This is a footer.\n        </:footer>\n      </.fun_with_named_slots>\n\n      <.fun_with_slot_attrs>\n        <:slot attr=\"1\" />\n      </.fun_with_slot_attrs>\n\n      <.table rows={@users}>\n        <:col :let={user} label={@name}>\n          {user.name}\n        </:col>\n\n        <:col :let={user} label=\"Address\">\n          {user.address}\n        </:col>\n      </.table>\n      \"\"\"\n    end\n  end\n\n  test \"stores slots definitions\" do\n    assert FunctionComponentWithSlots.__components__() == %{\n             fun_with_slot: %{\n               attrs: [],\n               kind: :def,\n               slots: [\n                 %{\n                   doc: nil,\n                   line: FunctionComponentWithSlots.fun_with_slot_line() - 1,\n                   name: :inner_block,\n                   opts: [],\n                   attrs: [],\n                   required: false,\n                   validate_attrs: true\n                 }\n               ],\n               line: FunctionComponentWithSlots.fun_with_slot_line()\n             },\n             fun_with_named_slots: %{\n               attrs: [],\n               kind: :def,\n               slots: [\n                 %{\n                   doc: nil,\n                   line: FunctionComponentWithSlots.fun_with_named_slots_line() - 1,\n                   name: :footer,\n                   opts: [],\n                   attrs: [],\n                   required: false,\n                   validate_attrs: true\n                 },\n                 %{\n                   doc: nil,\n                   line: FunctionComponentWithSlots.fun_with_named_slots_line() - 2,\n                   name: :header,\n                   opts: [],\n                   attrs: [],\n                   required: false,\n                   validate_attrs: true\n                 }\n               ],\n               line: FunctionComponentWithSlots.fun_with_named_slots_line()\n             },\n             fun_with_slot_attrs: %{\n               attrs: [],\n               kind: :def,\n               slots: [\n                 %{\n                   doc: nil,\n                   line: FunctionComponentWithSlots.fun_with_slot_attrs_line() - 4,\n                   name: :slot,\n                   opts: [],\n                   attrs: [\n                     %{\n                       doc: nil,\n                       line: FunctionComponentWithSlots.fun_with_slot_attrs_line() - 3,\n                       name: :attr,\n                       opts: [],\n                       required: false,\n                       slot: :slot,\n                       type: :any\n                     }\n                   ],\n                   required: true,\n                   validate_attrs: true\n                 }\n               ],\n               line: FunctionComponentWithSlots.fun_with_slot_attrs_line()\n             },\n             table: %{\n               attrs: [\n                 %{\n                   doc: nil,\n                   line: FunctionComponentWithSlots.table_line() - 2,\n                   name: :rows,\n                   opts: [],\n                   required: false,\n                   slot: nil,\n                   type: :list\n                 }\n               ],\n               kind: :def,\n               slots: [\n                 %{\n                   doc: nil,\n                   line: FunctionComponentWithSlots.table_line() - 6,\n                   name: :col,\n                   opts: [],\n                   attrs: [\n                     %{\n                       doc: nil,\n                       line: FunctionComponentWithSlots.table_line() - 5,\n                       name: :label,\n                       opts: [],\n                       required: false,\n                       slot: :col,\n                       type: :string\n                     }\n                   ],\n                   required: false,\n                   validate_attrs: true\n                 }\n               ],\n               line: FunctionComponentWithSlots.table_line()\n             }\n           }\n  end\n\n  test \"stores components for bodyless clauses\" do\n    defmodule Bodyless do\n      use Phoenix.Component\n\n      def example_line, do: __ENV__.line + 2\n\n      attr :example, :any, required: true\n      def example(assigns)\n\n      def example(_assigns) do\n        \"hello\"\n      end\n\n      def example2_line, do: __ENV__.line + 2\n\n      slot(:slot)\n      def example2(assigns)\n\n      def example2(_assigns) do\n        \"world\"\n      end\n    end\n\n    assert Bodyless.__components__() == %{\n             example: %{\n               kind: :def,\n               attrs: [\n                 %{\n                   line: Bodyless.example_line(),\n                   name: :example,\n                   opts: [],\n                   doc: nil,\n                   required: true,\n                   type: :any,\n                   slot: nil\n                 }\n               ],\n               slots: [],\n               line: Bodyless.example_line() + 1\n             },\n             example2: %{\n               kind: :def,\n               attrs: [],\n               slots: [\n                 %{\n                   doc: nil,\n                   line: Bodyless.example2_line(),\n                   name: :slot,\n                   opts: [],\n                   attrs: [],\n                   required: false,\n                   validate_attrs: true\n                 }\n               ],\n               line: Bodyless.example2_line() + 1\n             }\n           }\n  end\n\n  test \"matches on struct types\" do\n    defmodule StructTypes do\n      use Phoenix.Component\n\n      attr :uri, URI, required: true\n      attr :other, :any\n      def example(%{other: 1}), do: \"one\"\n      def example(%{other: 2}), do: \"two\"\n    end\n\n    assert_raise FunctionClauseError, fn -> StructTypes.example(%{other: 1, uri: :not_uri}) end\n    assert_raise FunctionClauseError, fn -> StructTypes.example(%{other: 2, uri: :not_uri}) end\n\n    uri = URI.parse(\"/relative\")\n    assert StructTypes.example(%{other: 1, uri: uri}) == \"one\"\n    assert StructTypes.example(%{other: 2, uri: uri}) == \"two\"\n  end\n\n  test \"provides attr defaults\" do\n    defmodule AttrDefaults do\n      use Phoenix.Component\n\n      attr :one, :integer, default: 1\n      attr :two, :integer, default: 2\n\n      def add(assigns) do\n        assigns = Phoenix.Component.assign(assigns, :foo, :bar)\n        ~H[{@one + @two}]\n      end\n\n      attr :nil_default, :string, default: nil\n      def example(assigns), do: ~H[{inspect(@nil_default)}]\n\n      attr :value, :string\n      def no_default(assigns), do: ~H[{inspect(@value)}]\n\n      attr :id, :any\n      attr :errors, :list, default: []\n\n      def assigned_with_same_default(assigns) do\n        assign(assigns, errors: [])\n      end\n    end\n\n    assert render_string(AttrDefaults, :add, %{}) == \"3\"\n    assert render_string(AttrDefaults, :example, %{}) == \"nil\"\n    assert render_string(AttrDefaults, :no_default, %{value: 123}) == \"123\"\n\n    assert_raise KeyError, ~r\":value not found\", fn ->\n      render_string(AttrDefaults, :no_default, %{})\n    end\n\n    assigns = AttrDefaults.assigned_with_same_default(%{__changed__: %{}})\n    assert Phoenix.Component.changed?(assigns, :errors)\n\n    assigns = AttrDefaults.assigned_with_same_default(%{__changed__: %{}, errors: []})\n    refute Phoenix.Component.changed?(assigns, :errors)\n\n    assigns = AttrDefaults.assigned_with_same_default(%{__changed__: %{errors: true}, errors: []})\n    assert Phoenix.Component.changed?(assigns, :errors)\n  end\n\n  test \"provides slot defaults\" do\n    defmodule SlotDefaults do\n      use Phoenix.Component\n\n      slot(:inner_block)\n      def func(assigns), do: ~H[{render_slot(@inner_block)}]\n\n      slot(:inner_block, required: true)\n      def func_required(assigns), do: ~H[{render_slot(@inner_block)}]\n    end\n\n    assigns = %{}\n    assert \"\" == rendered_to_string(~H[<SlotDefaults.func />])\n    assert \"hello\" == rendered_to_string(~H[<SlotDefaults.func>hello</SlotDefaults.func>])\n  end\n\n  test \"slots with rest\" do\n    defmodule SlotWithGlobal do\n      use Phoenix.Component\n\n      attr :rest, :global\n      slot(:inner_block, required: true)\n      slot(:col, required: true)\n\n      def test(assigns) do\n        ~H\"\"\"\n        <div {@rest}>\n          {render_slot(@inner_block)}\n          <%= for col <- @col do %>\n            {render_slot(col)},\n          <% end %>\n        </div>\n        \"\"\"\n      end\n    end\n\n    assigns = %{}\n\n    template =\n      ~H\"\"\"\n      <SlotWithGlobal.test class=\"my-class\">\n        block\n        <:col>col1</:col>\n        <:col>col2</:col>\n      </SlotWithGlobal.test>\n      \"\"\"\n\n    assert Phoenix.LiveViewTest.rendered_to_string(template) ==\n             ~s|<div class=\\\"my-class\\\">\\n  \\n  block\\n  \\n  \\n    col1,\\n  \\n    col2,\\n  \\n</div>|\n  end\n\n  defp lookup(_key \\\\ :one)\n\n  for {k, v} <- [one: 1, two: 2, three: 3] do\n    defp lookup(unquote(k)), do: unquote(v)\n  end\n\n  test \"does not change Elixir semantics\" do\n    assert lookup() == 1\n    assert lookup(:two) == 2\n    assert lookup(:three) == 3\n  end\n\n  test \"does not raise when there is a nested module\" do\n    mod = fn ->\n      defmodule NestedModules do\n        use Phoenix.Component\n\n        defmodule Nested do\n          def fun(arg), do: arg\n        end\n      end\n    end\n\n    assert mod.()\n  end\n\n  test \"supports :doc for attr and slot documentation\" do\n    defmodule AttrDocs do\n      use Phoenix.Component\n\n      def attr_line, do: __ENV__.line\n      attr :single, :any, doc: \"a single line description\"\n\n      attr :break, :any, doc: \"a description\n        with a line break\"\n\n      attr :multi, :any,\n        doc: \"\"\"\n        a description\n        that spans\n        multiple lines\n        \"\"\"\n\n      attr :sigil, :any,\n        doc: ~S\"\"\"\n        a description\n        within a multi-line\n        sigil\n        \"\"\"\n\n      attr :no_doc, :any\n\n      @doc \"my function component with attrs\"\n      def func_with_attr_docs(assigns), do: ~H[]\n\n      slot :slot, doc: \"a named slot\" do\n        attr :attr, :any, doc: \"a slot attr\"\n      end\n\n      def func_with_slot_docs(assigns), do: ~H[]\n    end\n\n    line = AttrDocs.attr_line()\n\n    assert AttrDocs.__components__() == %{\n             func_with_attr_docs: %{\n               attrs: [\n                 %{\n                   line: line + 3,\n                   doc: \"a description\\n        with a line break\",\n                   slot: nil,\n                   name: :break,\n                   opts: [],\n                   required: false,\n                   type: :any\n                 },\n                 %{\n                   line: line + 6,\n                   doc: \"a description\\nthat spans\\nmultiple lines\\n\",\n                   slot: nil,\n                   name: :multi,\n                   opts: [],\n                   required: false,\n                   type: :any\n                 },\n                 %{\n                   line: line + 20,\n                   doc: nil,\n                   slot: nil,\n                   name: :no_doc,\n                   opts: [],\n                   required: false,\n                   type: :any\n                 },\n                 %{\n                   line: line + 13,\n                   doc: \"a description\\nwithin a multi-line\\nsigil\\n\",\n                   slot: nil,\n                   name: :sigil,\n                   opts: [],\n                   required: false,\n                   type: :any\n                 },\n                 %{\n                   line: line + 1,\n                   doc: \"a single line description\",\n                   slot: nil,\n                   name: :single,\n                   opts: [],\n                   required: false,\n                   type: :any\n                 }\n               ],\n               kind: :def,\n               slots: [],\n               line: line + 23\n             },\n             func_with_slot_docs: %{\n               attrs: [],\n               kind: :def,\n               slots: [\n                 %{\n                   doc: \"a named slot\",\n                   line: line + 25,\n                   name: :slot,\n                   attrs: [\n                     %{\n                       doc: \"a slot attr\",\n                       line: line + 26,\n                       name: :attr,\n                       opts: [],\n                       required: false,\n                       slot: :slot,\n                       type: :any\n                     }\n                   ],\n                   opts: [],\n                   required: false,\n                   validate_attrs: true\n                 }\n               ],\n               line: line + 29\n             }\n           }\n  end\n\n  test \"inserts attr & slot docs into function component @doc string\" do\n    {_, _, :elixir, \"text/markdown\", _, _, docs} =\n      Code.fetch_docs(Phoenix.LiveViewTest.Support.FunctionComponentWithAttrs)\n\n    components = %{\n      fun_attr_any: \"\"\"\n      ## Attributes\n\n      * `attr` (`:any`)\n      \"\"\",\n      fun_attr_string: \"\"\"\n      ## Attributes\n\n      * `attr` (`:string`)\n      \"\"\",\n      fun_attr_atom: \"\"\"\n      ## Attributes\n\n      * `attr` (`:atom`)\n      \"\"\",\n      fun_attr_boolean: \"\"\"\n      ## Attributes\n\n      * `attr` (`:boolean`)\n      \"\"\",\n      fun_attr_integer: \"\"\"\n      ## Attributes\n\n      * `attr` (`:integer`)\n      \"\"\",\n      fun_attr_float: \"\"\"\n      ## Attributes\n\n      * `attr` (`:float`)\n      \"\"\",\n      fun_attr_map: \"\"\"\n      ## Attributes\n\n      * `attr` (`:map`)\n      \"\"\",\n      fun_attr_list: \"\"\"\n      ## Attributes\n\n      * `attr` (`:list`)\n      \"\"\",\n      fun_attr_global: \"\"\"\n      ## Attributes\n\n      * Global attributes are accepted.\n      \"\"\",\n      fun_attr_global_doc_include: \"\"\"\n      ## Attributes\n\n      * Global attributes are accepted. These are passed to the inner input field. Supports all globals plus: `[\"value\"]`.\n      \"\"\",\n      fun_attr_global_doc: \"\"\"\n      ## Attributes\n\n      * Global attributes are accepted. These are passed to the inner input field.\n      \"\"\",\n      fun_attr_global_include: \"\"\"\n      ## Attributes\n\n      * Global attributes are accepted. Supports all globals plus: `[\"value\"]`.\n      \"\"\",\n      fun_attr_global_and_regular: \"\"\"\n      ## Attributes\n\n      * `name` (`:string`) - The form input name.\n      * Global attributes are accepted. These are passed to the inner input field.\n      \"\"\",\n      fun_attr_struct: \"\"\"\n      ## Attributes\n\n      * `attr` (`Phoenix.LiveViewTest.Support.FunctionComponentWithAttrs.Struct`)\n      \"\"\",\n      fun_attr_required: \"\"\"\n      ## Attributes\n\n      * `attr` (`:any`) (required)\n      \"\"\",\n      fun_attr_default: \"\"\"\n      ## Attributes\n\n      * `attr` (`:any`) - Defaults to `%{}`.\n      \"\"\",\n      fun_doc_false: :hidden,\n      fun_doc_injection: \"\"\"\n      fun docs\n\n      ## Attributes\n\n      * `attr` (`:any`)\n\n      fun docs\n      \"\"\",\n      fun_multiple_attr: \"\"\"\n      ## Attributes\n\n      * `attr1` (`:any`)\n      * `attr2` (`:any`)\n      \"\"\",\n      fun_with_attr_doc: \"\"\"\n      ## Attributes\n\n      * `attr` (`:any`) - attr docs.\n      \"\"\",\n      fun_with_attr_doc_period: \"\"\"\n      ## Attributes\n\n      * `attr` (`:any`) - attr docs. Defaults to `\\\"foo\\\"`.\n      \"\"\",\n      fun_with_attr_doc_multiline: \"\"\"\n      ## Attributes\n\n      * `attr` (`:any`) - attr docs with bullets:\n\n          * foo\n          * bar\n\n        and that's it.\n\n        Defaults to `\"foo\"`.\n      \"\"\",\n      fun_with_hidden_attr: \"\"\"\n      ## Attributes\n\n      * `attr1` (`:any`)\n      \"\"\",\n      fun_with_doc: \"\"\"\n      fun docs\n      ## Attributes\n\n      * `attr` (`:any`)\n      \"\"\",\n      fun_slot: \"\"\"\n      ## Slots\n\n      * `inner_block`\n      \"\"\",\n      fun_slot_doc: \"\"\"\n      ## Slots\n\n      * `inner_block` - slot docs.\n      \"\"\",\n      fun_slot_required: \"\"\"\n      ## Slots\n\n      * `inner_block` (required)\n      \"\"\",\n      fun_slot_with_attrs: \"\"\"\n      ## Slots\n\n      * `named` (required) - a named slot. Accepts attributes:\n\n        * `attr1` (`:any`) (required) - a slot attr doc.\n        * `attr2` (`:any`) - a slot attr doc.\n      \"\"\",\n      fun_slot_no_doc_with_attrs: \"\"\"\n      ## Slots\n\n      * `named` (required) - Accepts attributes:\n\n        * `attr1` (`:any`) (required) - a slot attr doc.\n        * `attr2` (`:any`) - a slot attr doc.\n      \"\"\",\n      fun_slot_doc_multiline_with_attrs: \"\"\"\n      ## Slots\n\n      * `named` (required) - Important slot:\n\n        * for a\n        * for b\n\n        Accepts attributes:\n\n        * `attr1` (`:any`) (required) - a slot attr doc.\n        * `attr2` (`:any`) - a slot attr doc.\n      \"\"\",\n      fun_slot_doc_with_attrs_multiline: \"\"\"\n      ## Slots\n\n      * `named` (required) - Accepts attributes:\n\n        * `attr1` (`:any`) (required) - attr docs with bullets:\n\n            * foo\n            * bar\n\n          and that's it.\n\n        * `attr2` (`:any`) - a slot attr doc.\n      \"\"\",\n      fun_attr_values_examples: \"\"\"\n      ## Attributes\n\n      * `attr1` (`:atom`) - Must be one of `:foo`, `:bar`, or `:baz`.\n      * `attr2` (`:atom`) - Examples include `:foo`, `:bar`, and `:baz`.\n      * `attr3` (`:list`) - Must be one of `[60, 40]`.\n      * `attr4` (`:list`) - Examples include `[60, 40]`.\n      * `attr5` (`:atom`) - Defaults to `:foo`. Must be one of `:foo`, `:bar`, or `:baz`.\n      * `attr6` (`:atom`) - Attr 6 doc. Must be one of `:foo`, `:bar`, or `:baz`.\n      * `attr7` (`:atom`) - Attr 7 doc. Defaults to `:foo`. Must be one of `:foo`, `:bar`, or `:baz`.\n      \"\"\"\n    }\n\n    for {{_, fun, _}, _, _, %{\"en\" => doc}, _} <- docs do\n      assert components[fun] == doc\n    end\n  end\n\n  test \"stores correct line number on AST\" do\n    module = Phoenix.LiveViewTest.Support.FunctionComponentWithAttrs\n\n    {^module, binary, _file} = :code.get_object_code(module)\n\n    {:ok, {_, [{:abstract_code, {_vsn, abstract_code}}]}} =\n      :beam_lib.chunks(binary, [:abstract_code])\n\n    assert Enum.find_value(abstract_code, fn\n             {:function, anno, :identity, 1, _} -> :erl_anno.line(anno)\n             _ -> nil\n           end) == 24\n\n    assert Enum.find_value(abstract_code, fn\n             {:function, anno, :fun_doc_false, 1, _} -> :erl_anno.line(anno)\n             _ -> nil\n           end) == 118\n  end\n\n  test \"does not override signature of Elixir functions\" do\n    {:docs_v1, _, :elixir, \"text/markdown\", _, _, docs} =\n      Code.fetch_docs(Phoenix.LiveViewTest.Support.FunctionComponentWithAttrs)\n\n    assert {{:function, :identity, 1}, _, [\"identity(var)\"], _, %{}} =\n             List.keyfind(docs, {:function, :identity, 1}, 0)\n\n    assert {{:function, :map_identity, 1}, _, [\"map_identity(map)\"], _, %{}} =\n             List.keyfind(docs, {:function, :map_identity, 1}, 0)\n\n    assert Phoenix.LiveViewTest.Support.FunctionComponentWithAttrs.identity(:not_a_map) ==\n             :not_a_map\n\n    assert Phoenix.LiveViewTest.Support.FunctionComponentWithAttrs.identity(%{}) == %{}\n  end\n\n  test \"raise if attr :doc is not a string\" do\n    msg = ~r\"doc must be a string or false, got: :foo\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.AttrDocsInvalidType do\n        use Elixir.Phoenix.Component\n\n        attr :invalid, :any, doc: :foo\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if slot :doc is not a string\" do\n    msg = ~r\"doc must be a string or false, got: :foo\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.SlotDocsInvalidType do\n        use Elixir.Phoenix.Component\n\n        slot(:invalid, doc: :foo)\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise on invalid attr/2 args\" do\n    assert_raise FunctionClauseError, fn ->\n      defmodule Phoenix.ComponentTest.AttrMacroInvalidName do\n        use Elixir.Phoenix.Component\n\n        attr \"not an atom\", :any\n        def func(assigns), do: ~H[]\n      end\n    end\n\n    assert_raise FunctionClauseError, fn ->\n      defmodule Phoenix.ComponentTest.AttrMacroInvalidOpts do\n        use Elixir.Phoenix.Component\n\n        attr :attr, :any, \"not a list\"\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise on invalid slot/3 args\" do\n    assert_raise FunctionClauseError, fn ->\n      defmodule Phoenix.ComponentTest.SlotMacroInvalidName do\n        use Elixir.Phoenix.Component\n\n        slot(\"not an atom\")\n        def func(assigns), do: ~H[]\n      end\n    end\n\n    assert_raise FunctionClauseError, fn ->\n      defmodule Phoenix.ComponentTest.SlotMacroInvalidOpts do\n        use Elixir.Phoenix.Component\n\n        slot(:slot, \"not a list\")\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if attr is declared between multiple function heads\" do\n    msg = ~r\"attributes must be defined before the first function clause at line \\d+\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.MultiClauseWrong do\n        use Elixir.Phoenix.Component\n\n        attr :foo, :any\n        def func(assigns = %{foo: _}), do: ~H[]\n        def func(assigns = %{bar: _}), do: ~H[]\n\n        attr :bar, :any\n        def func(assigns = %{baz: _}), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if slot is declared between multiple function heads\" do\n    msg = ~r\"slots must be defined before the first function clause at line \\d+\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.MultiClauseWrong do\n        use Elixir.Phoenix.Component\n\n        slot(:inner_block)\n        def func(assigns = %{foo: _}), do: ~H[]\n        def func(assigns = %{bar: _}), do: ~H[]\n\n        slot(:named)\n        def func(assigns = %{baz: _}), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if attr is declared on an invalid function\" do\n    msg =\n      ~r\"cannot declare attributes for function func\\/2\\. Components must be functions with arity 1\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.AttrOnInvalidFunction do\n        use Elixir.Phoenix.Component\n\n        attr :foo, :any\n        def func(a, b), do: a + b\n      end\n    end\n  end\n\n  test \"raise if slot is declared on an invalid function\" do\n    msg =\n      ~r\"cannot declare slots for function func\\/2\\. Components must be functions with arity 1\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.SlotOnInvalidFunction do\n        use Elixir.Phoenix.Component\n\n        slot(:inner_block)\n        def func(a, b), do: a + b\n      end\n    end\n  end\n\n  test \"raise if attr is declared without a related function\" do\n    msg = ~r\"cannot define attributes without a related function component\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.AttrOnInvalidFunction do\n        use Elixir.Phoenix.Component\n\n        def func(assigns = %{baz: _}), do: ~H[]\n\n        attr :foo, :any\n      end\n    end\n  end\n\n  test \"raise if slot is declared without a related function\" do\n    msg = ~r\"cannot define slots without a related function component\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.SlotOnInvalidFunction do\n        use Elixir.Phoenix.Component\n\n        def func(assigns = %{baz: _}), do: ~H[]\n\n        slot(:inner_block)\n      end\n    end\n  end\n\n  test \"raise if attr type is not supported\" do\n    msg = ~r\"invalid type :not_a_type for attr :foo\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.AttrTypeNotSupported do\n        use Elixir.Phoenix.Component\n\n        attr :foo, :not_a_type\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if attr function type arity is not integer\" do\n    msg = ~r\"invalid type {:fun, \\\"a\\\"} for attr :foo\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.AttrTypeNotSupported do\n        use Elixir.Phoenix.Component\n\n        attr :foo, {:fun, \"a\"}\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if attr tuple first element is not :fun\" do\n    msg = ~r\"invalid type {:invalid, 1} for attr :foo\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.AttrTypeNotSupported do\n        use Elixir.Phoenix.Component\n\n        attr :foo, {:invalid, 1}\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if slot attr type is not supported\" do\n    msg = ~r\"invalid type :not_a_type for attr :foo in slot :named\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.SlotAttrTypeNotSupported do\n        use Elixir.Phoenix.Component\n\n        slot :named do\n          attr :foo, :not_a_type\n        end\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if slot attr type arity is not integer\" do\n    msg = ~r\"invalid type {:fun, \\\"a\\\"} for attr :foo in slot :named\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.SlotAttrTypeNotSupported do\n        use Elixir.Phoenix.Component\n\n        slot :named do\n          attr :foo, {:fun, \"a\"}\n        end\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if slot attr tuple first element is not :fun\" do\n    msg = ~r\"invalid type {:invalid, 1} for attr :foo in slot :named\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.SlotAttrTypeNotSupported do\n        use Elixir.Phoenix.Component\n\n        slot :named do\n          attr :foo, {:invalid, 1}\n        end\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if slot attr type is :global\" do\n    msg = ~r\"cannot define :global slot attributes\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.SlotAttrGlobalNotSupported do\n        use Elixir.Phoenix.Component\n\n        slot :named do\n          attr :foo, :global\n        end\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"reraise exceptions in slot/3 blocks\" do\n    assert_raise RuntimeError, \"boom!\", fn ->\n      defmodule Phoenix.ComponentTest.SlotExceptionRaised do\n        use Elixir.Phoenix.Component\n\n        slot :named do\n          raise \"boom!\"\n        end\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if attr :values does not match the type\" do\n    msg = ~r\"expected the values for attr :foo to be a :string, got: :not_a_string\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.AttrValueTypeMismatch do\n        use Elixir.Phoenix.Component\n\n        attr :foo, :string, values: [\"a string\", :not_a_string]\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if attr :example does not match the type\" do\n    msg = ~r\"expected the examples for attr :foo to be a :string, got: :not_a_string\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.AttrExampleTypeMismatch do\n        use Elixir.Phoenix.Component\n\n        attr :foo, :string, examples: [\"a string\", :not_a_string]\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if attr :values is not a enum\" do\n    msg = ~r\":values must be a non-empty enumerable, got: :ok\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.AttrsValuesNotAList do\n        use Elixir.Phoenix.Component\n\n        attr :foo, :string, values: :ok\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if attr :examples is not a list\" do\n    msg = ~r\":examples must be a non-empty list, got: :ok\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.AttrsExamplesNotAList do\n        use Elixir.Phoenix.Component\n\n        attr :foo, :string, examples: :ok\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if attr :values is an empty enum\" do\n    msg = ~r\":values must be a non-empty enumerable, got: \\[\\]\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.AttrsValuesEmptyList do\n        use Elixir.Phoenix.Component\n\n        attr :foo, :string, values: []\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if attr :examples is an empty list\" do\n    msg = ~r\":examples must be a non-empty list, got: \\[\\]\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.AttrsExamplesEmptyList do\n        use Elixir.Phoenix.Component\n\n        attr :foo, :string, examples: []\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if attr has both :values and :examples\" do\n    msg = ~r\"only one of :values or :examples must be given\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.AttrDefaultTypeMismatch do\n        use Elixir.Phoenix.Component\n\n        attr :foo, :string, values: [\"a string\"], examples: [\"a string\"]\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if attr :default does not match the type\" do\n    msg = ~r\"expected the default value for attr :foo to be a :string, got: :not_a_string\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.AttrDefaultTypeMismatch do\n        use Elixir.Phoenix.Component\n\n        attr :foo, :string, default: :not_a_string\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if attr :default is not one of :values\" do\n    msg =\n      ~r'expected the default value for attr :foo to be one of \\[\"foo\", \"bar\", \"baz\"\\], got: \"boom\"'\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.AttrDefaultValuesMismatch do\n        use Elixir.Phoenix.Component\n\n        attr :foo, :string, default: \"boom\", values: [\"foo\", \"bar\", \"baz\"]\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if attr :default is not in range\" do\n    msg = ~r'expected the default value for attr :foo to be one of 1\\.\\.10, got: 11'\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.AttrDefaultValuesMismatch do\n        use Elixir.Phoenix.Component\n\n        attr :foo, :integer, default: 11, values: 1..10\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if slot attr has :default\" do\n    msg = ~r\" invalid option :default for attr :foo in slot :named\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.SlotAttrDefault do\n        use Elixir.Phoenix.Component\n\n        slot :named do\n          attr :foo, :any, default: :whatever\n        end\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if attr option is not supported\" do\n    msg = ~r\"invalid option :not_an_opt for attr :foo\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.AttrOptionNotSupported do\n        use Elixir.Phoenix.Component\n\n        attr :foo, :any, not_an_opt: true\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if slot attr option is not supported\" do\n    msg = ~r\"invalid option :not_an_opt for attr :foo in slot :named\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.SlotAttrOptionNotSupported do\n        use Elixir.Phoenix.Component\n\n        slot :named do\n          attr :foo, :any, not_an_opt: true\n        end\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if attr is duplicated\" do\n    msg = ~r\"a duplicate attribute with name :foo already exists\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.AttrDup do\n        use Elixir.Phoenix.Component\n\n        attr :foo, :any, required: true\n        attr :foo, :string\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if slot is duplicated\" do\n    msg = ~r\"a duplicate slot with name :foo already exists\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.SlotDup do\n        use Elixir.Phoenix.Component\n\n        slot(:foo)\n        slot(:foo)\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if slot attr is duplicated\" do\n    msg = ~r\"a duplicate attribute with name :foo in slot :named already exists\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.SlotAttrDup do\n        use Elixir.Phoenix.Component\n\n        slot :named do\n          attr :foo, :any, required: true\n          attr :foo, :string\n        end\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if a slot and attr share the same name\" do\n    msg = ~r\"cannot define a slot with name :named, as an attribute with that name already exists\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule SlotAttrNameConflict do\n        use Elixir.Phoenix.Component\n\n        slot(:named)\n        attr :named, :any\n\n        def func(assigns), do: ~H[]\n      end\n    end\n\n    assert_raise CompileError, msg, fn ->\n      defmodule SlotAttrNameConflict do\n        use Elixir.Phoenix.Component\n\n        attr :named, :any\n        slot(:named)\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"does not raise if multiple slots with different names share the same attr names\" do\n    defmodule MultipleSlotAttrs do\n      use Phoenix.Component\n\n      slot :foo do\n        attr :attr, :any\n      end\n\n      slot :bar do\n        attr :attr, :any\n      end\n\n      def func(assigns), do: ~H[]\n    end\n  end\n\n  test \"raise if slot with name :inner_block has slot attrs\" do\n    msg = ~r\"cannot define attributes in a slot with name :inner_block\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule AttrsInDefaultSlot do\n        use Elixir.Phoenix.Component\n\n        slot :inner_block do\n          attr :attr, :any\n        end\n\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if :inner_block is attribute\" do\n    msg = ~r\"cannot define attribute called :inner_block. Maybe you wanted to use `slot` instead?\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule InnerSlotAttr do\n        use Elixir.Phoenix.Component\n\n        attr :inner_block, :string\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise on more than one :global attr\" do\n    msg = ~r\"cannot define :global attribute :rest2 because one is already defined as :rest\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.MultiGlobal do\n        use Elixir.Phoenix.Component\n\n        attr :rest, :global\n        attr :rest2, :global\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\n\n  test \"raise if global provides :required\" do\n    msg = ~r\"global attributes do not support the :required option\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.GlobalRequiredOpts do\n        use Elixir.Phoenix.Component\n\n        attr :rest, :global, required: true\n        def func(assigns), do: ~H[{@rest}]\n      end\n    end\n  end\n\n  test \"raise if global provides :values\" do\n    msg = ~r\"global attributes do not support the :values option\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.GlobalValueOpts do\n        use Elixir.Phoenix.Component\n\n        attr :rest, :global, values: [\"placeholder\", \"rel\"]\n        def func(assigns), do: ~H[{@rest}]\n      end\n    end\n  end\n\n  test \"raise if global provides :examples\" do\n    msg = ~r\"global attributes do not support the :examples option\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.GlobalExampleOpts do\n        use Elixir.Phoenix.Component\n\n        attr :rest, :global, examples: [\"placeholder\", \"rel\"]\n        def func(assigns), do: ~H[{@rest}]\n      end\n    end\n  end\n\n  test \"raise if slot attribute is not supported\" do\n    msg = ~r\"invalid options .* for slot :foo. The supported options are\"\n\n    assert_raise CompileError, msg, fn ->\n      defmodule Phoenix.ComponentTest.InvalidSlotAttr do\n        use Elixir.Phoenix.Component\n\n        slot :foo, require: true\n        def func(assigns), do: ~H[]\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_component/macro_component_integration_test.exs",
    "content": "defmodule Phoenix.Component.MacroComponentIntegrationTest do\n  # async: false due to manipulating the Application env\n  # for :root_tag_attribute\n  use ExUnit.Case, async: false\n\n  use Phoenix.Component\n\n  import Phoenix.LiveViewTest\n  import Phoenix.LiveViewTest.TreeDOM, only: [sigil_X: 2]\n\n  alias Phoenix.LiveViewTest.TreeDOM\n  alias Phoenix.Component.MacroComponent\n  alias Phoenix.LiveView.TagEngine.Tokenizer.ParseError\n\n  defmodule MyComponent do\n    @behaviour Phoenix.Component.MacroComponent\n\n    @impl true\n    def transform(ast, meta) do\n      send(self(), {:ast, ast, meta})\n      {:ok, Process.get(:new_ast, ast)}\n    end\n  end\n\n  defmodule DirectiveMacroComponent do\n    @behaviour Phoenix.Component.MacroComponent\n\n    @impl true\n    def transform(_ast, _meta) do\n      {:ok, \"\", %{},\n       [\n         root_tag_attribute: {\"phx-sample-one\", \"test\"},\n         root_tag_attribute: {\"phx-sample-two\", \"test\"}\n       ]}\n    end\n  end\n\n  defmodule BadRootTagAttrDirectiveMacroComponent do\n    @behaviour Phoenix.Component.MacroComponent\n\n    @impl true\n    def transform(_ast, _meta) do\n      {:ok, \"\", %{}, [root_tag_attribute: false]}\n    end\n  end\n\n  defmodule UnknownDirectiveMacroComponent do\n    @behaviour Phoenix.Component.MacroComponent\n\n    @impl true\n    def transform(_ast, _meta) do\n      {:ok, \"\", %{}, [unknown: true]}\n    end\n  end\n\n  test \"receives ast\" do\n    defmodule TestComponentAst do\n      use Phoenix.Component\n\n      def render(assigns) do\n        ~H\"\"\"\n        <div :type={MyComponent} id=\"1\" other={@foo}>\n          <p>This is some inner content</p>\n          <h1>Cool</h1>\n          <svg viewBox=\"0 0 100 100\" xmlns=\"http://www.w3.org/2000/svg\">\n            <circle cx=\"50\" cy=\"50\" r=\"50\" />\n          </svg>\n          <hr />\n        </div>\n        \"\"\"\n      end\n    end\n\n    assert_received {:ast, ast, meta}\n\n    assert {\"div\",\n            [\n              {\"id\", \"1\"},\n              {\"other\", {:@, [line: _], [{:foo, [line: _], nil}]}}\n            ],\n            [\n              \"\\n  \",\n              {\"p\", [], [\"This is some inner content\"], %{}},\n              \"\\n  \",\n              {\"h1\", [], [\"Cool\"], %{}},\n              \"\\n  \",\n              {\"svg\", [{\"viewBox\", \"0 0 100 100\"}, {\"xmlns\", \"http://www.w3.org/2000/svg\"}],\n               [\n                 \"\\n    \",\n                 {\"circle\", [{\"cx\", \"50\"}, {\"cy\", \"50\"}, {\"r\", \"50\"}], [], %{closing: :self}},\n                 \"\\n  \"\n               ], %{}},\n              \"\\n  \",\n              {\"hr\", [], [], %{closing: :void}},\n              \"\\n\"\n            ], %{}} = ast\n\n    assert %{env: env} = meta\n    assert env.module == TestComponentAst\n    assert env.file == __ENV__.file\n\n    assert render_component(&TestComponentAst.render/1, foo: \"bar\") |> TreeDOM.normalize_to_tree() ==\n             ~X\"\"\"\n             <div id=\"1\" other=\"bar\">\n               <p>This is some inner content</p>\n               <h1>Cool</h1>\n               <svg viewBox=\"0 0 100 100\" xmlns=\"http://www.w3.org/2000/svg\">\n                 <circle cx=\"50\" cy=\"50\" r=\"50\" />\n               </svg>\n               <hr>\n             </div>\n             \"\"\"\n  end\n\n  test \"can replace the rendered content\" do\n    Process.put(\n      :new_ast,\n      {:div, [{\"data-foo\", \"bar\"}],\n       [\n         {\"h1\", [], [\"Where is this coming from?\"], %{}},\n         {\"div\", [{\"id\", quote(do: @foo)}], [\"I have text content\"], %{}},\n         {\"hr\", [], [], %{closing: :void}}\n       ], %{}}\n    )\n\n    defmodule TestComponentReplacedAst do\n      use Phoenix.Component\n\n      def render(assigns) do\n        ~H\"\"\"\n        <div :type={MyComponent} id=\"1\" other={@foo}>\n          <p>This is some inner content</p>\n          <h1>Cool</h1>\n        </div>\n        \"\"\"\n      end\n    end\n\n    assert_received {:ast, _ast, _meta}\n\n    rendered = render_component(&TestComponentReplacedAst.render/1, foo: \"bar\\\"baz\")\n\n    assert rendered =~ \"bar&quot;baz\"\n\n    assert render_component(&TestComponentReplacedAst.render/1, foo: \"bar\\\"baz\")\n           |> TreeDOM.normalize_to_tree() ==\n             ~X\"\"\"\n             <div data-foo=\"bar\">\n               <h1>Where is this coming from?</h1>\n               <div id=\"bar&quot;baz\">I have text content</div>\n               <hr>\n             </div>\n             \"\"\"\n  end\n\n  test \"raises when there is EEx inside\" do\n    assert_raise ParseError,\n                 ~r/EEx is not currently supported in macro components/,\n                 fn ->\n                   defmodule TestComponentUnsupportedEEx do\n                     use Phoenix.Component\n\n                     def render(assigns) do\n                       ~H\"\"\"\n                       <div :type={MyComponent} id=\"1\" other={@foo}>\n                         <%= if @foo do %>\n                           <p>foo</p>\n                         <% end %>\n                       </div>\n                       \"\"\"\n                     end\n                   end\n                 end\n  end\n\n  test \"raises when there is interpolation inside\" do\n    assert_raise ParseError,\n                 ~r/interpolation is not currently supported in macro components/,\n                 fn ->\n                   defmodule TestComponentUnsupportedInterpolation do\n                     use Phoenix.Component\n\n                     def render(assigns) do\n                       ~H\"\"\"\n                       <div :type={MyComponent} id=\"1\" other={@foo}>\n                         {@foo}\n                       </div>\n                       \"\"\"\n                     end\n                   end\n                 end\n  end\n\n  test \"raises when there are components inside\" do\n    assert_raise ParseError,\n                 ~r/function components cannot be nested inside a macro component/,\n                 fn ->\n                   defmodule TestComponentUnsupportedComponents do\n                     use Phoenix.Component\n\n                     def render(assigns) do\n                       ~H\"\"\"\n                       <div :type={MyComponent} id=\"1\" other={@foo}>\n                         <.my_other_component />\n                       </div>\n                       \"\"\"\n                     end\n                   end\n                 end\n  end\n\n  test \"raises when trying to use :type on a component\" do\n    assert_raise ParseError,\n                 ~r/macro components are only supported on HTML tags/,\n                 fn ->\n                   defmodule TestUnsupportedComponent do\n                     use Phoenix.Component\n\n                     def render(assigns) do\n                       ~H\"\"\"\n                       <.my_other_component :type={MyComponent} />\n                       \"\"\"\n                     end\n                   end\n                 end\n\n    assert_raise ParseError,\n                 ~r/macro components are only supported on HTML tags/,\n                 fn ->\n                   defmodule TestUnsupportedComponent do\n                     use Phoenix.Component\n\n                     def render(assigns) do\n                       ~H\"\"\"\n                       <.my_other_component>\n                         <:my_slot :type={MyComponent} />\n                       </.my_other_component>\n                       \"\"\"\n                     end\n                   end\n                 end\n  end\n\n  test \"raises for dynamic attributes\" do\n    assert_raise ParseError,\n                 ~r/dynamic attributes are not supported in macro components, got: @bar/,\n                 fn ->\n                   defmodule TestComponentUnsupportedDynamicAttributes1 do\n                     use Phoenix.Component\n\n                     def render(assigns) do\n                       ~H\"\"\"\n                       <div :type={MyComponent} id=\"1\" other={@foo} {@bar}></div>\n                       \"\"\"\n                     end\n                   end\n                 end\n\n    assert_raise ParseError,\n                 ~r/dynamic attributes are not supported in macro components, got: @bar/,\n                 fn ->\n                   defmodule TestComponentUnsupportedDynamicAttributes2 do\n                     use Phoenix.Component\n\n                     def render(assigns) do\n                       ~H\"\"\"\n                       <div :type={MyComponent} id=\"1\" other={@foo}>\n                         <span {@bar}>Hey!</span>\n                       </div>\n                       \"\"\"\n                     end\n                   end\n                 end\n  end\n\n  test \"handles quotes\" do\n    Process.put(\n      :new_ast,\n      {:div, [{\"id\", \"1\"}],\n       [\n         {\"span\", [{\"class\", \"\\\"foo\\\"\"}], [\"Test\"], %{}},\n         {\"span\", [{\"class\", \"'foo'\"}], [\"Test\"], %{}}\n       ], %{}}\n    )\n\n    defmodule TestComponentQuotes do\n      use Phoenix.Component\n\n      def render(assigns) do\n        ~H\"\"\"\n        <div :type={MyComponent}></div>\n        \"\"\"\n      end\n    end\n\n    assert_received {:ast, _ast, _meta}\n\n    assert render_component(&TestComponentQuotes.render/1) == \"\"\"\n           <div id=\"1\"><span class='\"foo\"'>Test</span><span class=\"'foo'\">Test</span></div>\\\n           \"\"\"\n\n    # mixed quotes are invalid\n    assert_raise ParseError,\n                 ~r/invalid attribute value for \"class\"/,\n                 fn ->\n                   Process.put(:new_ast, {:div, [{\"class\", ~s[\"'\"]}], [], %{}})\n\n                   defmodule TestComponentQuotesInvalid do\n                     use Phoenix.Component\n\n                     def render(assigns) do\n                       ~H\"\"\"\n                       <div :type={MyComponent}></div>\n                       \"\"\"\n                     end\n                   end\n                 end\n  end\n\n  test \"get_data/2 provides a list of all data entries\" do\n    defmodule MyMacroComponent do\n      @behaviour Phoenix.Component.MacroComponent\n\n      @impl true\n      def transform({_tag, attrs, _children, _meta} = ast, meta) do\n        {:ok, ast, %{file: meta.env.file, line: meta.env.line, opts: Map.new(attrs)}}\n      end\n    end\n\n    defmodule TestComponentWithData1 do\n      use Phoenix.Component\n\n      def render(assigns) do\n        ~H\"\"\"\n        <div :type={MyMacroComponent} foo=\"bar\" baz></div>\n        <div>\n          <h1 :type={MyMacroComponent} id=\"2\">Content</h1>\n        </div>\n        \"\"\"\n      end\n    end\n\n    assert data = MacroComponent.get_data(TestComponentWithData1, MyMacroComponent)\n    assert length(data) == 2\n\n    assert Enum.find(data, fn %{opts: opts} -> opts == %{\"baz\" => nil, \"foo\" => \"bar\"} end)\n    assert Enum.find(data, fn %{opts: opts} -> opts == %{\"id\" => \"2\"} end)\n  end\n\n  test \"root tracking\" do\n    assert eval_heex(\"<div :type={MyComponent}>Test</div>\").root\n\n    refute eval_heex(\"\"\"\n           <div :type={MyComponent}>Test</div>\n           <span>Another</span>\n           \"\"\").root\n\n    Process.put(\n      :new_ast,\n      {:div, [{\"id\", \"1\"}],\n       [\n         {\"span\", [{\"class\", \"\\\"foo\\\"\"}], [\"Test\"], %{}},\n         {\"span\", [{\"class\", \"'foo'\"}], [\"Test\"], %{}}\n       ], %{}}\n    )\n\n    assert eval_heex(\"<div :type={MyComponent}>Test</div>\").root\n\n    Process.put(:new_ast, \"\")\n\n    assert eval_heex(\"\"\"\n           <div :type={MyComponent}>Test</div><span>Another</span>\n           \"\"\").root\n\n    Process.put(:new_ast, \"\")\n\n    assert eval_heex(\"\"\"\n           <div :type={MyComponent}>Test</div>\\n<span>Another</span>\n           \"\"\").root\n\n    Process.put(:new_ast, \"some text\")\n\n    refute eval_heex(\"\"\"\n           <div :type={MyComponent}>Test</div>\n           <span>Another</span>\n           \"\"\").root\n  end\n\n  describe \"directives\" do\n    test \"raises if an unknown directive is provided\" do\n      message =\n        ~r/unknown directive {:unknown, true} provided by macro component #{inspect(__MODULE__)}\\.UnknownDirectiveMacroComponent/\n\n      assert_raise ParseError,\n                   message,\n                   fn ->\n                     defmodule TestUnknownDirective do\n                       use Phoenix.Component\n\n                       def render(assigns) do\n                         ~H\"\"\"\n                         <div :type={UnknownDirectiveMacroComponent}></div>\n                         \"\"\"\n                       end\n                     end\n                   end\n    end\n  end\n\n  describe \"directives - root_tag_attribute\" do\n    setup do\n      # Need to set a :root_tag_attribute as the only directive supported\n      # by macro components currently is [root_tag_attribute: {name, value}] which\n      # requires a :root_tag_attribute to be configured\n      Application.put_env(:phoenix_live_view, :root_tag_attribute, \"phx-r\")\n      on_exit(fn -> Application.delete_env(:phoenix_live_view, :root_tag_attribute) end)\n    end\n\n    test \"happy path\" do\n      defmodule TestComponentRootTagAttr do\n        use Phoenix.Component\n\n        def render(assigns) do\n          ~H\"\"\"\n          <div :type={DirectiveMacroComponent}></div>\n          <div id=\"hello\">\n            <span class=\"inside\">\n              <.my_link><p>I am in an inner block<b>non-root</b></p></.my_link>\n            </span>\n            <.my_component>\n              <span>Inner block</span>\n              <p>More inner block</p>\n              <:other_slot>\n                <div>Hey</div>\n              </:other_slot>\n            </.my_component>\n          </div>\n          \"\"\"\n        end\n\n        defp my_link(assigns) do\n          ~H\"\"\"\n          <a href=\"#\">{render_slot(@inner_block)}</a>\n          \"\"\"\n        end\n\n        defp my_component(assigns) do\n          ~H\"\"\"\n          {render_slot(@inner_block)}\n          {render_slot(@other_slot)}\n\n          <p>Part of the component</p>\n          \"\"\"\n        end\n      end\n\n      assert render_component(&TestComponentRootTagAttr.render/1)\n             |> TreeDOM.normalize_to_tree(sort_attributes: true) ==\n               ~X\"\"\"\n               <div phx-sample-one=\"test\" phx-sample-two=\"test\" phx-r id=\"hello\">\n                 <span class=\"inside\">\n                   <a href=\"#\" phx-r><p phx-sample-one=\"test\" phx-sample-two=\"test\" phx-r>I am in an inner block<b>non-root</b></p></a>\n                 </span>\n\n                 <span phx-sample-one=\"test\" phx-sample-two=\"test\" phx-r>Inner block</span>\n                 <p phx-sample-one=\"test\" phx-sample-two=\"test\" phx-r>More inner block</p>\n                 <div phx-sample-one=\"test\" phx-sample-two=\"test\" phx-r>Hey</div>\n\n                 <p phx-r>Part of the component</p>\n               </div>\n               \"\"\"\n    end\n\n    test \"raises if :root_tag_attribute directive is provided with an invalid value\" do\n      message = ~r\"\"\"\n      expected {name, value} for :root_tag_attribute directive from macro component #{inspect(__MODULE__)}\\.BadRootTagAttrDirectiveMacroComponent, got: false\n\n      name must be a compile-time string, and value must be a compile-time string or true\n      \"\"\"\n\n      assert_raise ParseError,\n                   message,\n                   fn ->\n                     defmodule TestBadRootTagAttrDirective do\n                       use Phoenix.Component\n\n                       def render(assigns) do\n                         ~H\"\"\"\n                         <div :type={BadRootTagAttrDirectiveMacroComponent}></div>\n                         \"\"\"\n                       end\n                     end\n                   end\n    end\n\n    test \"raises if macro components with directives are not defined at the beginning of the template\" do\n      message =\n        ~r/macro component #{inspect(__MODULE__)}\\.DirectiveMacroComponent specified directives and therefore must appear at the very beginning of the template/\n\n      defmodule TestComponentDirectiveAtBeginning1 do\n        use Phoenix.Component\n\n        def render(assigns) do\n          ~H\"\"\"\n          <div :type={DirectiveMacroComponent}></div>\n          \"\"\"\n        end\n      end\n\n      # whitespace is allowed\n      defmodule TestComponentDirectiveAtBeginning2 do\n        use Phoenix.Component\n\n        def render(assigns) do\n          ~H\"\"\"\n\n\n\n          <div :type={DirectiveMacroComponent}></div>\n          \"\"\"noformat\n        end\n      end\n\n      assert_raise ParseError,\n                   message,\n                   fn ->\n                     defmodule TestComponentDirectiveAtBeginning3 do\n                       use Phoenix.Component\n\n                       def render(assigns) do\n                         ~H\"\"\"\n                         <div />\n                         <div :type={DirectiveMacroComponent}></div>\n                         \"\"\"\n                       end\n                     end\n                   end\n\n      assert_raise ParseError,\n                   message,\n                   fn ->\n                     defmodule TestComponentDirectiveAtBeginning4 do\n                       use Phoenix.Component\n\n                       def render(assigns) do\n                         ~H\"\"\"\n                         <div></div>\n                         <div :type={DirectiveMacroComponent}></div>\n                         \"\"\"\n                       end\n                     end\n                   end\n\n      assert_raise ParseError,\n                   message,\n                   fn ->\n                     defmodule TestComponentDirectiveAtBeginning5 do\n                       use Phoenix.Component\n\n                       def render(assigns) do\n                         ~H\"\"\"\n                         <.link>Link</.link>\n                         <div :type={DirectiveMacroComponent}></div>\n                         \"\"\"\n                       end\n                     end\n                   end\n\n      assert_raise ParseError,\n                   message,\n                   fn ->\n                     defmodule TestComponentDirectiveAtBeginning6 do\n                       use Phoenix.Component\n\n                       def render(assigns) do\n                         ~H\"\"\"\n                         <Phoenix.Component.link>Link</Phoenix.Component.link>\n                         <div :type={DirectiveMacroComponent}></div>\n                         \"\"\"\n                       end\n                     end\n                   end\n\n      assert_raise ParseError,\n                   message,\n                   fn ->\n                     defmodule TestComponentDirectiveAtBeginning7 do\n                       use Phoenix.Component\n\n                       def render(assigns) do\n                         ~H\"\"\"\n                         {if true, do: \"Test\"}\n                         <div :type={DirectiveMacroComponent}></div>\n                         \"\"\"\n                       end\n                     end\n                   end\n\n      assert_raise ParseError,\n                   message,\n                   fn ->\n                     defmodule TestComponentDirectiveAtBeginning8 do\n                       use Phoenix.Component\n\n                       def render(assigns) do\n                         ~H\"\"\"\n                         <%= if true do %>\n                           <div :type={DirectiveMacroComponent}></div>\n                         <% end %>\n                         \"\"\"\n                       end\n                     end\n                   end\n\n      assert_raise ParseError,\n                   message,\n                   fn ->\n                     defmodule TestComponentDirectiveAtBeginning9 do\n                       use Phoenix.Component\n\n                       def render(assigns) do\n                         ~H\"\"\"\n                         <div>\n                           <div :type={DirectiveMacroComponent}></div>\n                         </div>\n                         \"\"\"\n                       end\n                     end\n                   end\n\n      assert_raise ParseError,\n                   message,\n                   fn ->\n                     defmodule TestComponentDirectiveAtBeginning10 do\n                       use Phoenix.Component\n\n                       def render(assigns) do\n                         ~H\"\"\"\n                         <.link>\n                           <div :type={DirectiveMacroComponent}></div>\n                         </.link>\n                         \"\"\"\n                       end\n                     end\n                   end\n\n      assert_raise ParseError,\n                   message,\n                   fn ->\n                     defmodule TestComponentDirectiveAtBeginning11 do\n                       use Phoenix.Component\n\n                       def render(assigns) do\n                         ~H\"\"\"\n                         <Phoenix.Component.link>\n                           <div :type={DirectiveMacroComponent}></div>\n                         </Phoenix.Component.link>\n                         \"\"\"\n                       end\n                     end\n                   end\n    end\n  end\n\n  defp eval_heex(source) do\n    Phoenix.LiveView.TagEngine.compile(source,\n      file: __ENV__.file,\n      caller: __ENV__,\n      tag_handler: Phoenix.LiveView.HTMLEngine\n    )\n    |> Code.eval_quoted(assigns: %{})\n    |> elem(0)\n  end\nend\n"
  },
  {
    "path": "test/phoenix_component/macro_component_test.exs",
    "content": "defmodule Phoenix.Component.MacroComponentTest do\n  use ExUnit.Case, async: true\n\n  alias Phoenix.Component.MacroComponent\n\n  setup_all do\n    defmodule MyMacroComponent do\n      @behaviour Phoenix.Component.MacroComponent\n\n      @impl true\n      def transform(ast, _meta), do: {:ok, ast, %{}}\n    end\n\n    :ok\n  end\n\n  describe \"ast_to_string/1\" do\n    test \"simple cases\" do\n      assert MacroComponent.ast_to_string({\"div\", [{\"id\", \"1\"}], [\"Hello\"], %{}}) ==\n               \"<div id=\\\"1\\\">Hello</div>\"\n\n      assert MacroComponent.ast_to_string({\"div\", [{\"id\", \"<bar>\"}], [\"Hello\"], %{}}) ==\n               \"<div id=\\\"<bar>\\\">Hello</div>\"\n    end\n\n    test \"handles self closing and void tags\" do\n      assert MacroComponent.ast_to_string(\n               {\"div\", [{\"id\", \"<bar>\"}], [{\"hr\", [], [], %{closing: :void}}], %{}}\n             ) ==\n               \"<div id=\\\"<bar>\\\"><hr></div>\"\n\n      assert MacroComponent.ast_to_string({\"circle\", [{\"id\", \"1\"}], [], %{closing: :self}}) ==\n               \"<circle id=\\\"1\\\"/>\"\n    end\n\n    test \"attribute without value\" do\n      assert MacroComponent.ast_to_string(\n               {\"div\", [{\"foo\", nil}, {\"bar\", \"baz\"}], [], %{closing: :self}}\n             ) ==\n               \"<div foo bar=\\\"baz\\\"/>\"\n    end\n\n    test \"handles quotes\" do\n      assert MacroComponent.ast_to_string({\"div\", [{\"foo\", ~s['bar']}], [], %{}}) ==\n               ~s[<div foo=\"'bar'\"></div>]\n\n      assert MacroComponent.ast_to_string({\"div\", [{\"foo\", ~s[\"bar\"]}], [], %{}}) ==\n               ~s[<div foo='\"bar\"'></div>]\n\n      assert_raise ArgumentError, ~r/invalid attribute value for \"foo\"/, fn ->\n        MacroComponent.ast_to_string({\"div\", [{\"foo\", ~s[\"'bar'\"]}], [], %{}})\n      end\n    end\n\n    test \"invalid attribute\" do\n      assert_raise ArgumentError,\n                   ~r/cannot convert AST with non-string attribute \"id\" to string. Got: @bar/,\n                   fn ->\n                     MacroComponent.ast_to_string(\n                       {\"div\", [{\"id\", quote(do: @bar)}], [\"Hello\"], %{}}\n                     )\n                   end\n    end\n  end\n\n  describe \"get_data/2\" do\n    test \"returns an empty list if the component module does not exist\" do\n      assert MacroComponent.get_data(IDoNotExist, MyMacroComponent) == []\n    end\n\n    test \"returns an empty list if the component does not define any macro components\" do\n      defmodule MyComponent do\n        use Phoenix.Component\n\n        def render(assigns), do: ~H\"\"\n      end\n\n      assert MacroComponent.get_data(MyComponent, MyMacroComponent) == []\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_component/pages/about_page.html.heex",
    "content": "About us"
  },
  {
    "path": "test/phoenix_component/pages/another_root/root.html.heex",
    "content": "root!"
  },
  {
    "path": "test/phoenix_component/pages/another_root/root.text.eex",
    "content": "root plain text!\n"
  },
  {
    "path": "test/phoenix_component/pages/welcome_page.html.heex",
    "content": "Welcome <%= @name %>"
  },
  {
    "path": "test/phoenix_component/rendering_test.exs",
    "content": "defmodule Phoenix.ComponentRenderingTest do\n  use ExUnit.Case, async: true\n  use Phoenix.Component\n\n  import ExUnit.CaptureIO\n  import Phoenix.LiveViewTest\n\n  embed_templates \"pages/*\"\n  embed_templates \"another_root/*.html\", root: \"pages\"\n  embed_templates \"another_root/*.text\", root: \"pages\", suffix: \"_text\"\n\n  defp h2s(template) do\n    template\n    |> Phoenix.HTML.Safe.to_iodata()\n    |> IO.iodata_to_binary()\n  end\n\n  defp hello(assigns) do\n    assigns = assign_new(assigns, :name, fn -> \"World\" end)\n\n    ~H\"\"\"\n    Hello {@name}\n    \"\"\"\n  end\n\n  describe \"rendering\" do\n    test \"renders component\" do\n      assigns = %{}\n\n      assert h2s(~H\"\"\"\n             <.hello name=\"WORLD\" />\n             \"\"\") == \"\"\"\n             Hello WORLD\\\n             \"\"\"\n    end\n  end\n\n  describe \"embed_templates\" do\n    attr :name, :string, default: \"chris\"\n    def welcome_page(assigns)\n\n    test \"embed from directory pattern\" do\n      # generic template\n      assert render_component(&about_page/1) == \"About us\"\n\n      # root\n      assert render_component(&root/1) == \"root!\"\n      assert Phoenix.Template.render(__MODULE__, \"root_text\", \"text\", []) == \"root plain text!\\n\"\n\n      # attr'd bodyless definition\n      assert render_component(&welcome_page/1) == \"Welcome chris\"\n    end\n  end\n\n  describe \"testing\" do\n    test \"render_component/1\" do\n      assert render_component(&hello/1) == \"Hello World\"\n      assert render_component(&hello/1, name: \"WORLD!\") == \"Hello WORLD!\"\n    end\n  end\n\n  describe \"change tracking\" do\n    defp eval(%Phoenix.LiveView.Rendered{dynamic: dynamic}), do: Enum.map(dynamic.(true), &eval/1)\n    defp eval(other), do: other\n\n    def changed(assigns) do\n      ~H\"\"\"\n      {inspect(Map.get(assigns, :__changed__), custom_options: [sort_maps: true])}\n      \"\"\"\n    end\n\n    test \"without changed assigns on root\" do\n      assigns = %{foo: 1}\n      assert eval(~H\"<.changed foo={@foo} />\") == [[\"nil\"]]\n    end\n\n    @compile {:no_warn_undefined, __MODULE__.Tainted}\n\n    test \"with tainted variable\" do\n      assert capture_io(:stderr, fn ->\n               defmodule Tainted do\n                 def run(assigns) do\n                   foo = 1\n                   ~H\"<Phoenix.ComponentRenderingTest.changed foo={foo} />\"\n                 end\n               end\n             end) =~ \"you are accessing the variable \\\"foo\\\" inside a LiveView template\"\n\n      assert eval(__MODULE__.Tainted.run(%{foo: 1})) == [[\"nil\"]]\n      assert eval(__MODULE__.Tainted.run(%{foo: 1, __changed__: %{}})) == [[\"%{foo: true}\"]]\n    end\n\n    test \"with changed assigns on root\" do\n      assigns = %{foo: 1, __changed__: %{}}\n      assert eval(~H\"<.changed foo={@foo} />\") == [nil]\n\n      assigns = %{foo: 1, __changed__: %{bar: true}}\n      assert eval(~H\"<.changed foo={@foo} />\") == [nil]\n\n      assigns = %{foo: 1, __changed__: %{foo: true}}\n      assert eval(~H\"<.changed foo={@foo} />\") == [[\"%{foo: true}\"]]\n\n      assigns = %{foo: 1, __changed__: %{foo: %{bar: true}}}\n      assert eval(~H\"<.changed foo={@foo} />\") == [[\"%{foo: %{bar: true}}\"]]\n    end\n\n    test \"with changed assigns on map\" do\n      assigns = %{foo: %{bar: :bar}, __changed__: %{}}\n      assert eval(~H\"<.changed foo={@foo.bar} />\") == [nil]\n\n      assigns = %{foo: %{bar: :bar}, __changed__: %{bar: true}}\n      assert eval(~H\"<.changed foo={@foo.bar} />\") == [nil]\n\n      assigns = %{foo: %{bar: :bar}, __changed__: %{foo: true}}\n      assert eval(~H\"<.changed foo={@foo.bar} />\") == [[\"%{foo: true}\"]]\n\n      assigns = %{foo: %{bar: :bar}, __changed__: %{foo: %{bar: :bar}}}\n      assert eval(~H\"<.changed foo={@foo.bar} />\") == [nil]\n\n      assigns = %{foo: %{bar: :bar}, __changed__: %{foo: %{bar: :baz}}}\n      assert eval(~H\"<.changed foo={@foo.bar} />\") == [[\"%{foo: true}\"]]\n\n      assigns = %{foo: %{bar: %{bar: :bar}}, __changed__: %{foo: %{bar: %{bar: :bat}}}}\n      assert eval(~H\"<.changed foo={@foo.bar} />\") == [[\"%{foo: %{bar: :bat}}\"]]\n    end\n\n    test \"with multiple changed assigns\" do\n      assigns = %{foo: 1, bar: 2, __changed__: %{}}\n      assert eval(~H\"<.changed foo={@foo + @bar} />\") == [nil]\n\n      assigns = %{foo: 1, bar: 2, __changed__: %{bar: true}}\n      assert eval(~H\"<.changed foo={@foo + @bar} />\") == [[\"%{foo: true}\"]]\n\n      assigns = %{foo: 1, bar: 2, __changed__: %{foo: true}}\n      assert eval(~H\"<.changed foo={@foo + @bar} />\") == [[\"%{foo: true}\"]]\n\n      assigns = %{foo: 1, bar: 2, __changed__: %{baz: true}}\n      assert eval(~H\"<.changed foo={@foo + @bar} />\") == [nil]\n    end\n\n    test \"with multiple keys\" do\n      assigns = %{foo: 1, bar: 2, __changed__: %{}}\n      assert eval(~H\"<.changed foo={@foo} bar={@bar} />\") == [nil]\n\n      assigns = %{foo: 1, bar: 2, __changed__: %{bar: true}}\n      assert eval(~H\"<.changed foo={@foo} bar={@bar} />\") == [[\"%{bar: true}\"]]\n\n      assigns = %{foo: 1, bar: 2, __changed__: %{foo: true}}\n      assert eval(~H\"<.changed foo={@foo} bar={@bar} />\") == [[\"%{foo: true}\"]]\n\n      assigns = %{foo: 1, bar: 2, __changed__: %{baz: true}}\n      assert eval(~H\"<.changed foo={@foo} bar={@bar} />\") == [nil]\n    end\n\n    test \"with multiple keys and one is static\" do\n      assigns = %{foo: 1, __changed__: %{}}\n      assert eval(~H|<.changed foo={@foo} bar=\"2\" />|) == [nil]\n\n      assigns = %{foo: 1, __changed__: %{bar: true}}\n      assert eval(~H|<.changed foo={@foo} bar=\"2\" />|) == [nil]\n\n      assigns = %{foo: 1, __changed__: %{foo: true}}\n      assert eval(~H|<.changed foo={@foo} bar=\"2\" />|) == [[\"%{foo: true}\"]]\n    end\n\n    test \"with multiple keys and one is tainted\" do\n      assigns = %{foo: 1, __changed__: %{}}\n      assert eval(~H|<.changed foo={@foo} bar={assigns} />|) == [[\"%{bar: true}\"]]\n\n      assigns = %{foo: 1, __changed__: %{foo: true}}\n      assert eval(~H|<.changed foo={@foo} bar={assigns} />|) == [[\"%{bar: true, foo: true}\"]]\n    end\n\n    test \"with conflict on changed assigns\" do\n      assigns = %{foo: 1, bar: %{foo: 2}, __changed__: %{}}\n      assert eval(~H\"<.changed foo={@foo} {@bar} />\") == [nil]\n\n      # We cannot perform any change tracking if dynamic assigns has dependencies\n      assigns = %{foo: 1, __changed__: %{foo: true}}\n      assert eval(~H\"<.changed foo={@foo} {%{foo: 2}} />\") == [[\"%{foo: true}\"]]\n\n      assigns = %{foo: 1, bar: %{foo: 2}, __changed__: %{bar: true}}\n      assert eval(~H\"<.changed foo={@foo} {@bar} />\") == [[\"nil\"]]\n\n      assigns = %{foo: 1, bar: %{foo: 2}, __changed__: %{foo: true}}\n      assert eval(~H\"<.changed foo={@foo} {@bar} />\") == [[\"nil\"]]\n\n      assigns = %{foo: 1, bar: %{foo: 2}, baz: 3, __changed__: %{baz: true}}\n      assert eval(~H\"<.changed foo={@foo} {@bar} baz={@baz} />\") == [[\"nil\"]]\n    end\n\n    test \"with dynamic assigns\" do\n      assigns = %{foo: %{a: 1, b: 2}, __changed__: %{}}\n      assert eval(~H\"<.changed {@foo} />\") == [nil]\n\n      # We cannot perform any change tracking if dynamic assigns has dependencies\n      assigns = %{foo: %{a: 1, b: 2}, __changed__: %{foo: true}}\n      assert eval(~H\"<.changed {@foo} />\") == [[\"nil\"]]\n\n      assigns = %{foo: %{a: 1, b: 2}, bar: 3, __changed__: %{bar: true}}\n      assert eval(~H\"<.changed {%{a: 1, b: 2}} bar={@bar} />\") == [[\"%{bar: true}\"]]\n\n      assigns = %{foo: %{a: 1, b: 2}, bar: 3, __changed__: %{bar: true}}\n      assert eval(~H\"<.changed {assigns} bar={@bar} />\") == [[\"nil\"]]\n\n      assigns = %{foo: %{a: 1, b: 2}, bar: 3, __changed__: %{bar: true}}\n      assert eval(~H\"<.changed {@foo} bar={@bar} />\") == [[\"nil\"]]\n\n      assigns = %{bar: 3, __changed__: %{bar: true}}\n\n      assert eval(~H\"<.changed {%{a: assigns[:b], b: assigns[:a]}} bar={@bar} />\") ==\n               [[\"nil\"]]\n\n      assigns = %{a: 1, b: 2, bar: 3, __changed__: %{a: true, b: true, bar: true}}\n\n      assert eval(~H\"<.changed {%{a: assigns[:b], b: assigns[:a]}} bar={@bar} />\") ==\n               [[\"nil\"]]\n    end\n\n    defp wrapper(assigns) do\n      ~H\"\"\"\n      <div>{render_slot(@inner_block)}</div>\n      \"\"\"\n    end\n\n    defp inner_changed(assigns) do\n      ~H\"\"\"\n      {inspect(Map.get(assigns, :__changed__), custom_options: [sort_maps: true])}\n      {render_slot(@inner_block, \"var\")}\n      \"\"\"\n    end\n\n    test \"with @inner_block\" do\n      assigns = %{foo: 1, __changed__: %{}}\n      assert eval(~H|<.inner_changed foo={@foo}></.inner_changed>|) == [nil]\n      assert eval(~H|<.inner_changed>{@foo}</.inner_changed>|) == [nil]\n\n      assigns = %{foo: 1, __changed__: %{foo: true}}\n\n      assert eval(~H|<.inner_changed foo={@foo}></.inner_changed>|) ==\n               [[\"%{foo: true}\", nil]]\n\n      assert eval(\n               ~H|<.inner_changed foo={@foo}>{inspect(Map.get(assigns, :__changed__))}</.inner_changed>|\n             ) ==\n               [[\"%{foo: true, inner_block: true}\", [\"%{foo: true}\"]]]\n\n      assert eval(~H|<.inner_changed>{@foo}</.inner_changed>|) ==\n               [[\"%{inner_block: true}\", [\"1\"]]]\n\n      assigns = %{foo: 1, __changed__: %{foo: %{bar: true}}}\n\n      assert eval(~H|<.inner_changed foo={@foo}></.inner_changed>|) ==\n               [[\"%{foo: %{bar: true}}\", nil]]\n\n      assert eval(\n               ~H|<.inner_changed foo={@foo}>{inspect(Map.get(assigns, :__changed__))}</.inner_changed>|\n             ) ==\n               [[\"%{foo: %{bar: true}, inner_block: true}\", [\"%{foo: %{bar: true}}\"]]]\n\n      assert eval(~H|<.inner_changed>{@foo}</.inner_changed>|) ==\n               [[\"%{inner_block: %{bar: true}}\", [\"1\"]]]\n    end\n\n    test \"with let\" do\n      assigns = %{foo: 1, __changed__: %{}}\n      assert eval(~H|<.inner_changed :let={_foo} foo={@foo}></.inner_changed>|) == [nil]\n\n      assigns = %{foo: 1, __changed__: %{foo: true}}\n\n      assert eval(~H|<.inner_changed :let={_foo} foo={@foo}></.inner_changed>|) ==\n               [[\"%{foo: true}\", nil]]\n\n      assert eval(~H\"\"\"\n             <.inner_changed :let={_foo} foo={@foo}>\n               {inspect(Map.get(assigns, :__changed__))}\n             </.inner_changed>\n             \"\"\") ==\n               [[\"%{foo: true, inner_block: true}\", [\"%{foo: true}\"]]]\n\n      assert eval(~H\"\"\"\n             <.inner_changed :let={_foo} foo={@foo}>\n               {\"constant\"}{inspect(Map.get(assigns, :__changed__))}\n             </.inner_changed>\n             \"\"\") ==\n               [[\"%{foo: true, inner_block: true}\", [nil, \"%{foo: true}\"]]]\n\n      assert eval(~H\"\"\"\n             <.inner_changed :let={foo} foo={@foo}>\n               <.inner_changed :let={_bar} bar={foo}>\n                 {\"constant\"}{inspect(Map.get(assigns, :__changed__))}\n               </.inner_changed>\n             </.inner_changed>\n             \"\"\") ==\n               [\n                 [\n                   \"%{foo: true, inner_block: true}\",\n                   [[\"%{bar: true, inner_block: true}\", [nil, \"%{foo: true}\"]]]\n                 ]\n               ]\n\n      assert eval(~H\"\"\"\n             <.inner_changed :let={foo} foo={@foo}>\n               {foo}{inspect(Map.get(assigns, :__changed__))}\n             </.inner_changed>\n             \"\"\") ==\n               [[\"%{foo: true, inner_block: true}\", [\"var\", \"%{foo: true}\"]]]\n\n      assert eval(~H\"\"\"\n             <.inner_changed :let={foo} foo={@foo}>\n               <.inner_changed :let={bar} bar={foo}>\n                 {bar}{inspect(Map.get(assigns, :__changed__))}\n               </.inner_changed>\n             </.inner_changed>\n             \"\"\") ==\n               [\n                 [\n                   \"%{foo: true, inner_block: true}\",\n                   [[\"%{bar: true, inner_block: true}\", [\"var\", \"%{foo: true}\"]]]\n                 ]\n               ]\n    end\n\n    test \"with :let inside @inner_block\" do\n      assigns = %{foo: 1, bar: 2, __changed__: %{foo: true}}\n\n      assert eval(~H\"\"\"\n             <.wrapper>\n               {@foo}\n               <.inner_changed :let={var} foo={@bar}>\n                 {var}\n               </.inner_changed>\n             </.wrapper>\n             \"\"\") == [[[\"1\", nil]]]\n    end\n\n    defp optional_wrapper(assigns) do\n      assigns = assign_new(assigns, :inner_block, fn -> [] end)\n\n      ~H\"\"\"\n      <div>{render_slot(@inner_block) || \"DEFAULT!\"}</div>\n      \"\"\"\n    end\n\n    test \"with optional @inner_block\" do\n      assigns = %{foo: 1}\n\n      assert eval(~H\"\"\"\n             <.optional_wrapper>\n               {@foo}\n             </.optional_wrapper>\n             \"\"\") == [[[\"1\"]]]\n\n      assigns = %{foo: 2, __changed__: %{foo: true}}\n\n      assert eval(~H\"\"\"\n             <.optional_wrapper>\n               {@foo}\n             </.optional_wrapper>\n             \"\"\") == [[[\"2\"]]]\n\n      assigns = %{foo: 3}\n\n      assert eval(~H\"\"\"\n             <.optional_wrapper />\n             \"\"\") == [[\"DEFAULT!\"]]\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_component/verify_test.exs",
    "content": "defmodule Phoenix.ComponentVerifyTest do\n  use ExUnit.Case, async: true\n\n  import ExUnit.CaptureIO\n\n  test \"validate unused components\" do\n    assert capture_io(:stderr, fn ->\n             defmodule UnusedComponent do\n               use Phoenix.Component\n               attr :name, :any, required: true\n               defp func(assigns), do: ~H[]\n             end\n           end) =~ \"function func/1 is unused\"\n  end\n\n  test \"validate required attributes\" do\n    warnings =\n      capture_io(:stderr, fn ->\n        defmodule RequiredAttrs do\n          use Phoenix.Component\n\n          attr :name, :any, required: true\n          attr :phone, :any\n          attr :email, :any, required: true\n\n          def func(assigns), do: ~H[]\n\n          def line, do: __ENV__.line + 4\n\n          def render(assigns) do\n            ~H\"\"\"\n            <.func />\n            \"\"\"\n          end\n        end\n      end)\n\n    line = get_line(__MODULE__.RequiredAttrs)\n\n    assert warnings =~ \"\"\"\n           missing required attribute \"email\" for component \\\n           Phoenix.ComponentVerifyTest.RequiredAttrs.func/1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line}: (file)\"\n\n    assert warnings =~ \"\"\"\n           missing required attribute \"name\" for component \\\n           Phoenix.ComponentVerifyTest.RequiredAttrs.func/1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line}: (file)\"\n  end\n\n  test \"loads modules\" do\n    warnings =\n      capture_io(:stderr, fn ->\n        defmodule UnloadedAttrs do\n          use Phoenix.Component\n\n          def render(assigns) do\n            ~H\"\"\"\n            <Phoenix.LiveViewTest.Support.FunctionComponentWithAttrs.fun_attr_any unknown=\"foo\" />\n            \"\"\"\n          end\n        end\n      end)\n\n    assert warnings =~\n             \"undefined attribute \\\"unknown\\\" for component Phoenix.LiveViewTest.Support.FunctionComponentWithAttrs\"\n  end\n\n  test \"validate undefined attributes\" do\n    warnings =\n      capture_io(:stderr, fn ->\n        defmodule UndefinedAttrs do\n          use Phoenix.Component\n\n          attr :class, :any\n          def func(assigns), do: ~H[]\n\n          def line, do: __ENV__.line + 4\n\n          def render(assigns) do\n            ~H\"\"\"\n            <.func width=\"btn\" size={@size} phx-no-format />\n            \"\"\"\n          end\n        end\n      end)\n\n    line = get_line(__MODULE__.UndefinedAttrs)\n\n    assert warnings =~ \"\"\"\n           undefined attribute \"size\" for component \\\n           Phoenix.ComponentVerifyTest.UndefinedAttrs.func/1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line}: (file)\"\n\n    assert warnings =~ \"\"\"\n           undefined attribute \"width\" for component \\\n           Phoenix.ComponentVerifyTest.UndefinedAttrs.func/1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line}: (file)\"\n  end\n\n  test \"validates attrs and slots for external function components\" do\n    warnings =\n      capture_io(:stderr, fn ->\n        defmodule External do\n          use Phoenix.Component\n          attr :id, :string, required: true\n\n          slot :named do\n            attr :attr, :any, required: true\n          end\n\n          def render(assigns), do: ~H[]\n        end\n\n        defmodule ExternalCalls do\n          use Phoenix.Component\n\n          def line, do: __ENV__.line + 4\n\n          def render(assigns) do\n            ~H\"\"\"\n            <External.render>\n              <:named />\n            </External.render>\n            \"\"\"\n          end\n        end\n      end)\n\n    line = get_line(__MODULE__.ExternalCalls)\n\n    assert warnings =~ \"\"\"\n           missing required attribute \"id\" for component \\\n           Phoenix.ComponentVerifyTest.External.render/1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line}: (file)\"\n\n    assert warnings =~ \"\"\"\n           missing required attribute \"attr\" in slot \"named\" for component \\\n           Phoenix.ComponentVerifyTest.External.render/1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 1}: (file)\"\n  end\n\n  test \"validate literal types\" do\n    warnings =\n      capture_io(:stderr, fn ->\n        defmodule TypeAttrs do\n          use Phoenix.Component, global_prefixes: ~w(myprefix-)\n\n          attr :any, :any\n          attr :string, :string\n          attr :atom, :atom\n          attr :boolean, :boolean\n          attr :integer, :integer\n          attr :float, :float\n          attr :map, :map\n          attr :list, :list\n          attr :global, :global\n\n          def func(assigns), do: ~H[]\n\n          def global_line, do: __ENV__.line + 4\n\n          def global_render(assigns) do\n            ~H\"\"\"\n            <.func global=\"global\" />\n            <.func phx-click=\"click\" id=\"id\" />\n            \"\"\"\n          end\n\n          def any_line, do: __ENV__.line + 4\n\n          def any_render(assigns) do\n            ~H\"\"\"\n            <.func any=\"any\" />\n            <.func any={:any} />\n            <.func any={true} />\n            <.func any={1} />\n            <.func any={1.0} />\n            <.func any={%{}} />\n            <.func any={[]} />\n            <.func any={nil} />\n            \"\"\"\n          end\n\n          def render_string_line, do: __ENV__.line + 4\n\n          def string_render(assigns) do\n            ~H\"\"\"\n            <.func string=\"string\" />\n            <.func string={:string} />\n            <.func string={true} />\n            <.func string={1} />\n            <.func string={1.0} />\n            <.func string={%{}} />\n            <.func string={[]} />\n            <.func string={nil} />\n            \"\"\"\n          end\n\n          def render_atom_line, do: __ENV__.line + 4\n\n          def atom_render(assigns) do\n            ~H\"\"\"\n            <.func atom=\"atom\" />\n            <.func atom={:atom} />\n            <.func atom={true} />\n            <.func atom={1} />\n            <.func atom={1.0} />\n            <.func atom={%{}} />\n            <.func atom={[]} />\n            <.func atom={nil} />\n            \"\"\"\n          end\n\n          def render_boolean_line, do: __ENV__.line + 4\n\n          def boolean_render(assigns) do\n            ~H\"\"\"\n            <.func boolean=\"boolean\" />\n            <.func boolean={:boolean} />\n            <.func boolean={true} />\n            <.func boolean={1} />\n            <.func boolean={1.0} />\n            <.func boolean={%{}} />\n            <.func boolean={[]} />\n            <.func boolean={nil} />\n            \"\"\"\n          end\n\n          def render_integer_line, do: __ENV__.line + 4\n\n          def integer_render(assigns) do\n            ~H\"\"\"\n            <.func integer=\"integer\" />\n            <.func integer={:integer} />\n            <.func integer={true} />\n            <.func integer={1} />\n            <.func integer={1.0} />\n            <.func integer={%{}} />\n            <.func integer={[]} />\n            <.func integer={nil} />\n            \"\"\"\n          end\n\n          def render_float_line, do: __ENV__.line + 4\n\n          def float_render(assigns) do\n            ~H\"\"\"\n            <.func float=\"float\" />\n            <.func float={:float} />\n            <.func float={true} />\n            <.func float={1} />\n            <.func float={1.0} />\n            <.func float={%{}} />\n            <.func float={[]} />\n            <.func float={nil} />\n            \"\"\"\n          end\n\n          def render_map_line, do: __ENV__.line + 4\n\n          def map_render(assigns) do\n            ~H\"\"\"\n            <.func map=\"map\" />\n            <.func map={:map} />\n            <.func map={true} />\n            <.func map={1} />\n            <.func map={1.0} />\n            <.func map={%{}} />\n            <.func map={[]} />\n            <.func map={nil} />\n            \"\"\"\n          end\n\n          def render_list_line, do: __ENV__.line + 4\n\n          def list_render(assigns) do\n            ~H\"\"\"\n            <.func list=\"list\" />\n            <.func list={:list} />\n            <.func list={true} />\n            <.func list={1} />\n            <.func list={1.0} />\n            <.func list={%{}} />\n            <.func list={[]} />\n            <.func list={nil} />\n            \"\"\"\n          end\n        end\n      end)\n\n    line = get_line(__MODULE__.TypeAttrs, :global_line)\n\n    assert warnings =~ \"\"\"\n           global attribute \"global\" in component \\\n           Phoenix.ComponentVerifyTest.TypeAttrs.func/1 \\\n           may not be provided directly\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line}: (file)\"\n\n    line = get_line(__MODULE__.TypeAttrs, :render_string_line)\n\n    for {value, offset} <- [\n          {:string, 1},\n          {true, 2},\n          {1, 3},\n          {1.0, 4},\n          {%{}, 5},\n          {[], 6},\n          {nil, 7}\n        ] do\n      assert warnings =~ \"\"\"\n             attribute \"string\" in component \\\n             Phoenix.ComponentVerifyTest.TypeAttrs.func/1 \\\n             must be a :string, got: #{inspect(value)}\n             \"\"\"\n\n      assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + offset}: (file)\"\n    end\n\n    line = get_line(__MODULE__.TypeAttrs, :render_atom_line)\n\n    for {value, offset} <- [\n          {\"atom\", 0},\n          {1, 3},\n          {1.0, 4},\n          {%{}, 5},\n          {[], 6}\n        ] do\n      assert warnings =~ \"\"\"\n             attribute \"atom\" in component \\\n             Phoenix.ComponentVerifyTest.TypeAttrs.func/1 \\\n             must be an :atom, got: #{inspect(value)}\n             \"\"\"\n\n      assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + offset}: (file)\"\n    end\n\n    line = get_line(__MODULE__.TypeAttrs, :render_boolean_line)\n\n    for {value, offset} <- [\n          {\"boolean\", 0},\n          {:boolean, 1},\n          {1, 3},\n          {1.0, 4},\n          {%{}, 5},\n          {[], 6},\n          {nil, 7}\n        ] do\n      assert warnings =~ \"\"\"\n             attribute \"boolean\" in component \\\n             Phoenix.ComponentVerifyTest.TypeAttrs.func/1 \\\n             must be a :boolean, got: #{inspect(value)}\n             \"\"\"\n\n      assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + offset}: (file)\"\n    end\n\n    line = get_line(__MODULE__.TypeAttrs, :render_integer_line)\n\n    for {value, offset} <- [\n          {\"integer\", 0},\n          {:integer, 1},\n          {true, 2},\n          {1.0, 4},\n          {%{}, 5},\n          {[], 6},\n          {nil, 7}\n        ] do\n      assert warnings =~ \"\"\"\n             attribute \"integer\" in component \\\n             Phoenix.ComponentVerifyTest.TypeAttrs.func/1 \\\n             must be an :integer, got: #{inspect(value)}\n             \"\"\"\n\n      assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + offset}: (file)\"\n    end\n\n    line = get_line(__MODULE__.TypeAttrs, :render_float_line)\n\n    for {value, offset} <- [\n          {\"float\", 0},\n          {:float, 1},\n          {true, 2},\n          {1, 3},\n          {%{}, 5},\n          {[], 6},\n          {nil, 7}\n        ] do\n      assert warnings =~ \"\"\"\n             attribute \"float\" in component \\\n             Phoenix.ComponentVerifyTest.TypeAttrs.func/1 \\\n             must be a :float, got: #{inspect(value)}\n             \"\"\"\n\n      assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + offset}: (file)\"\n    end\n\n    line = get_line(__MODULE__.TypeAttrs, :render_map_line)\n\n    for {value, offset} <- [\n          {\"map\", 0},\n          {:map, 1},\n          {true, 2},\n          {1, 3},\n          {1.0, 4},\n          {[], 6},\n          {nil, 7}\n        ] do\n      assert warnings =~ \"\"\"\n             attribute \"map\" in component \\\n             Phoenix.ComponentVerifyTest.TypeAttrs.func/1 \\\n             must be a :map, got: #{inspect(value)}\n             \"\"\"\n\n      assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + offset}: (file)\"\n    end\n\n    line = get_line(__MODULE__.TypeAttrs, :render_list_line)\n\n    for {value, offset} <- [\n          {\"list\", 0},\n          {:list, 1},\n          {true, 2},\n          {1, 3},\n          {1.0, 4},\n          {%{}, 5},\n          {nil, 7}\n        ] do\n      assert warnings =~ \"\"\"\n             attribute \"list\" in component \\\n             Phoenix.ComponentVerifyTest.TypeAttrs.func/1 \\\n             must be a :list, got: #{inspect(value)}\n             \"\"\"\n\n      assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + offset}: (file)\"\n    end\n  end\n\n  test \"validates struct types\" do\n    warnings =\n      capture_io(:stderr, fn ->\n        defmodule SlotStruct do\n          use Phoenix.Component\n\n          attr :uri, URI\n          def func(assigns), do: ~H[]\n\n          def line, do: __ENV__.line + 2\n\n          def render(assigns) do\n            ~H\"\"\"\n            <.func uri={123} />\n            <.func uri={%URI{}} />\n            <.func uri={%{@foo | bar: :baz}} />\n            \"\"\"\n          end\n        end\n      end)\n\n    line = get_line(__MODULE__.SlotStruct, :line)\n\n    assert Regex.scan(~r/attribute \"uri\" in component/, warnings) |> length() == 1\n\n    assert warnings =~ \"\"\"\n           attribute \"uri\" in component Phoenix.ComponentVerifyTest.SlotStruct.func/1 \\\n           must be a URI struct, got: 123\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 2}: (file)\"\n  end\n\n  test \"validates function types\" do\n    warnings =\n      capture_io(:stderr, fn ->\n        defmodule FunAttrs do\n          use Phoenix.Component\n\n          attr :any_fun, :fun\n          attr :arity_1, {:fun, 1}\n          attr :arity_2, {:fun, 2}\n\n          slot :myslot do\n            attr :arity_1, {:fun, 1}\n          end\n\n          def func(assigns), do: ~H[]\n\n          def line, do: __ENV__.line + 4\n\n          def render(assigns) do\n            ~H\"\"\"\n            <%!-- those are valid functions --%>\n            <.func any_fun={fn x, y -> x + y end} />\n            <.func any_fun={&Function.identity/1} />\n            <.func any_fun={&(&1 + &2 + &3)} />\n            <%!-- this is not a function --%>\n            <.func any_fun={:foo} />\n            <%!-- those are valid arity 1 functions --%>\n            <.func arity_1={fn _ -> nil end} />\n            <.func arity_1={&Function.identity/1} />\n            <.func arity_1={& &1} />\n            <%!-- those are not arity 1 functions --%>\n            <.func arity_1={fn _, _ -> nil end} />\n            <.func arity_1={&String.split/2} />\n            <.func arity_1={&Phoenix.LiveView.send_update(@myself, completed: &1, nice: &2)} />\n            <.func arity_1={1.23} />\n            <%!-- those are valid arity 2 functions --%>\n            <.func arity_2={fn _, _ -> nil end} />\n            <.func arity_2={&String.split/2} />\n            <.func arity_2={&Phoenix.LiveView.send_update(@myself, completed: &1, nice: &2)} />\n            <%!-- those are not arity 2 functions --%>\n            <.func arity_2={fn _ -> nil end} />\n            <.func arity_2={&Function.identity/1} />\n            <.func arity_2={&Phoenix.LiveView.send_update(@myself, completed: &1)} />\n            <.func arity_2=\"foo\" />\n            <%!-- also works for slots --%>\n            <.func>\n              <:myslot arity_1={fn _, _ -> nil end} />\n            </.func>\n            \"\"\"\n          end\n        end\n      end)\n\n    line = get_line(__MODULE__.FunAttrs, :line)\n\n    assert Regex.scan(~r/attribute \"any_fun\" in component/, warnings) |> length() == 1\n    assert Regex.scan(~r/attribute \"arity_1\" in component/, warnings) |> length() == 4\n    assert Regex.scan(~r/attribute \"arity_2\" in component/, warnings) |> length() == 4\n\n    assert warnings =~ \"\"\"\n           attribute \"any_fun\" in component Phoenix.ComponentVerifyTest.FunAttrs.func/1 \\\n           must be a function, got: :foo\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 5}: (file)\"\n\n    assert warnings =~ \"\"\"\n           attribute \"arity_1\" in component Phoenix.ComponentVerifyTest.FunAttrs.func/1 \\\n           must be a function of arity 1, got: a function of arity 2\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 11}: (file)\"\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 12}: (file)\"\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 13}: (file)\"\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 14}: (file)\"\n\n    assert warnings =~ \"\"\"\n           attribute \"arity_2\" in component Phoenix.ComponentVerifyTest.FunAttrs.func/1 \\\n           must be a function of arity 2, got: a function of arity 1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 20}: (file)\"\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 21}: (file)\"\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 22}: (file)\"\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 23}: (file)\"\n\n    assert warnings =~ \"\"\"\n           attribute \"arity_1\" in slot \"myslot\" for component Phoenix.ComponentVerifyTest.FunAttrs.func/1 \\\n           must be a function of arity 1, got: a function of arity 2\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 26}: (file)\"\n  end\n\n  test \"validates attr values\" do\n    warnings =\n      capture_io(:stderr, fn ->\n        defmodule AttrValues do\n          use Phoenix.Component\n\n          attr :attr, :string, values: [\"foo\", \"bar\", \"baz\"]\n          def func_string(assigns), do: ~H[]\n\n          attr :attr, :string\n          def func_string_no_values(assigns), do: ~H[]\n\n          attr :attr, :atom, values: [:foo, :bar, :baz]\n          def func_atom(assigns), do: ~H[]\n\n          attr :attr, :integer, values: 1..10\n          def func_integer(assigns), do: ~H[]\n\n          def line, do: __ENV__.line + 2\n\n          def render(assigns) do\n            ~H\"\"\"\n            <.func_string attr=\"boom\" />\n            <.func_string attr={fn _ -> :bar end} />\n            <.func_string_no_values attr={fn _ -> :baz end} />\n            <.func_atom attr={:boom} />\n            <.func_integer attr={11} />\n            <.func_string attr=\"bar\" />\n            <.func_string attr={@string} />\n            <.func_atom attr={:bar} />\n            <.func_atom attr={@atom} />\n            <.func_integer attr={5} />\n            <.func_integer attr={@integer} />\n            \"\"\"\n          end\n        end\n      end)\n\n    line = get_line(__MODULE__.AttrValues, :line)\n\n    assert Regex.scan(~r/attribute \"attr\" in component/, warnings) |> length() == 5\n\n    assert warnings =~ \"\"\"\n           attribute \"attr\" in component \\\n           Phoenix.ComponentVerifyTest.AttrValues.func_string/1 \\\n           must be one of [\"foo\", \"bar\", \"baz\"], got: \"boom\"\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 2}: (file)\"\n\n    assert warnings =~ \"\"\"\n           attribute \"attr\" in component \\\n           Phoenix.ComponentVerifyTest.AttrValues.func_string/1 \\\n           must be one of [\"foo\", \"bar\", \"baz\"], got: a function of arity 1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 3}: (file)\"\n\n    assert warnings =~ \"\"\"\n           attribute \"attr\" in component \\\n           Phoenix.ComponentVerifyTest.AttrValues.func_string_no_values/1 \\\n           must be a :string, got: a function of arity 1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 4}: (file)\"\n\n    assert warnings =~ \"\"\"\n           attribute \"attr\" in component \\\n           Phoenix.ComponentVerifyTest.AttrValues.func_atom/1 \\\n           must be one of [:foo, :bar, :baz], got: :boom\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 5}: (file)\"\n\n    assert warnings =~ \"\"\"\n           attribute \"attr\" in component \\\n           Phoenix.ComponentVerifyTest.AttrValues.func_integer/1 \\\n           must be one of 1..10, got: 11\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 6}: (file)\"\n  end\n\n  test \"does not warn for unknown attribute in slot without do-block when validate_attrs is false\" do\n    warnings =\n      capture_io(:stderr, fn ->\n        defmodule SlotWithoutDoBlockValidateFalse do\n          use Phoenix.Component\n\n          slot :item, validate_attrs: false\n\n          def func_slot_wo_do_block(assigns) do\n            ~H\"\"\"\n            <div>\n              {render_slot(@item)}\n            </div>\n            \"\"\"\n          end\n\n          def render(assigns) do\n            ~H\"\"\"\n            <.func_slot_wo_do_block>\n              <:item class=\"test\"></:item>\n            </.func_slot_wo_do_block>\n            \"\"\"\n          end\n        end\n      end)\n\n    refute warnings =~ \"undefined attribute\"\n  end\n\n  test \"does warn for unknown attribute in slot without do-block when validate_attrs is true\" do\n    warnings =\n      capture_io(:stderr, fn ->\n        defmodule SlotWithoutDoBlock do\n          use Phoenix.Component\n\n          slot :item, validate_attrs: true\n\n          def func_slot_wo_do_block(assigns) do\n            ~H\"\"\"\n            <div>\n              {render_slot(@item)}\n            </div>\n            \"\"\"\n          end\n\n          def render(assigns) do\n            ~H\"\"\"\n            <.func_slot_wo_do_block>\n              <:item class=\"test\"></:item>\n            </.func_slot_wo_do_block>\n            \"\"\"\n          end\n        end\n      end)\n\n    assert warnings =~ \"\"\"\n           undefined attribute \"class\" in slot \"item\" for component \\\n           Phoenix.ComponentVerifyTest.SlotWithoutDoBlock.func_slot_wo_do_block/1\n           \"\"\"\n  end\n\n  test \"validates slot attr values\" do\n    warnings =\n      capture_io(:stderr, fn ->\n        defmodule SlotAttrValues do\n          use Phoenix.Component\n\n          slot :named do\n            attr :string, :string, values: [\"foo\", \"bar\", \"baz\"]\n            attr :atom, :atom, values: [:foo, :bar, :baz]\n            attr :integer, :integer, values: 1..10\n          end\n\n          def func(assigns), do: ~H[]\n\n          def line, do: __ENV__.line + 2\n\n          def render(assigns) do\n            ~H\"\"\"\n            <.func>\n              <:named string=\"boom\" atom={:boom} integer={11} />\n              <:named string={@string} atom={@atom} integer={@integer} />\n              <:named string=\"bar\" atom={:bar} integer={5} />\n            </.func>\n            \"\"\"\n          end\n        end\n      end)\n\n    line = get_line(__MODULE__.SlotAttrValues, :line)\n\n    assert Regex.scan(~r/attribute \"\\w+\" in slot \"named\"/, warnings) |> length() == 3\n\n    assert warnings =~ \"\"\"\n           attribute \"string\" in slot \"named\" for component \\\n           Phoenix.ComponentVerifyTest.SlotAttrValues.func/1 \\\n           must be one of [\"foo\", \"bar\", \"baz\"], got: \"boom\"\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 3}: (file)\"\n\n    assert warnings =~ \"\"\"\n           attribute \"atom\" in slot \"named\" for component \\\n           Phoenix.ComponentVerifyTest.SlotAttrValues.func/1 \\\n           must be one of [:foo, :bar, :baz], got: :boom\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 3}: (file)\"\n\n    assert warnings =~ \"\"\"\n           attribute \"integer\" in slot \"named\" for component \\\n           Phoenix.ComponentVerifyTest.SlotAttrValues.func/1 \\\n           must be one of 1..10, got: 11\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 3}: (file)\"\n  end\n\n  test \"validate required slots\" do\n    warnings =\n      capture_io(:stderr, fn ->\n        defmodule RequiredSlots do\n          use Phoenix.Component\n\n          slot(:inner_block, required: true)\n\n          def func(assigns), do: ~H[]\n\n          slot(:named, required: true)\n\n          def func_named_slot(assigns), do: ~H[]\n\n          def line, do: __ENV__.line + 2\n\n          def render(assigns) do\n            ~H\"\"\"\n            <!-- no default slot provided -->\n            <.func />\n            <!-- with an empty default slot -->\n            <.func></.func>\n            <!-- with content in the default slot -->\n            <.func>Hello!</.func>\n            <!-- no named slots provided -->\n            <.func_named_slot />\n            <!-- with an empty named slot -->\n            <.func_named_slot>\n              <:named />\n            </.func_named_slot>\n            <!-- with content in the named slots -->\n            <.func_named_slot>\n              <:named>\n                Hello!\n              </:named>\n            </.func_named_slot>\n            <!-- with entries for the named slot -->\n            <.func_named_slot>\n              <:named>\n                Hello,\n              </:named>\n              <:named>\n                World!\n              </:named>\n            </.func_named_slot>\n            \"\"\"\n          end\n        end\n      end)\n\n    line = get_line(__MODULE__.RequiredSlots)\n\n    assert warnings =~ \"\"\"\n           missing required slot \"inner_block\" for component \\\n           Phoenix.ComponentVerifyTest.RequiredSlots.func/1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 3}: (file)\"\n\n    assert warnings =~ \"\"\"\n           missing required slot \"named\" for component \\\n           Phoenix.ComponentVerifyTest.RequiredSlots.func_named_slot/1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 9}: (file)\"\n  end\n\n  test \"validate slot attr types\" do\n    warnings =\n      capture_io(:stderr, fn ->\n        defmodule SlotAttrs do\n          use Phoenix.Component\n\n          slot :slot do\n            attr :any, :any\n            attr :string, :string\n            attr :atom, :atom\n            attr :boolean, :boolean\n            attr :integer, :integer\n            attr :float, :float\n            attr :list, :list\n          end\n\n          def func(assigns), do: ~H[]\n\n          def render_any_line, do: __ENV__.line + 5\n\n          def render_any(assigns) do\n            ~H\"\"\"\n            <.func>\n              <:slot any />\n              <:slot any=\"any\" />\n              <:slot any={:any} />\n              <:slot any={true} />\n              <:slot any={1} />\n              <:slot any={1.0} />\n              <:slot any={[]} />\n            </.func>\n            \"\"\"\n          end\n\n          def render_string_line, do: __ENV__.line + 5\n\n          def render_string(assigns) do\n            ~H\"\"\"\n            <.func>\n              <:slot string=\"string\" />\n              <:slot string={:string} />\n              <:slot string={true} />\n              <:slot string={1} />\n              <:slot string={1.0} />\n              <:slot string={[]} />\n              <:slot string={nil} />\n            </.func>\n            \"\"\"\n          end\n\n          def render_atom_line, do: __ENV__.line + 5\n\n          def render_atom(assigns) do\n            ~H\"\"\"\n            <.func>\n              <:slot atom=\"atom\" />\n              <:slot atom={:atom} />\n              <:slot atom={true} />\n              <:slot atom={1} />\n              <:slot atom={1.0} />\n              <:slot atom={[]} />\n              <:slot atom={nil} />\n            </.func>\n            \"\"\"\n          end\n\n          def render_boolean_line, do: __ENV__.line + 5\n\n          def render_boolean(assigns) do\n            ~H\"\"\"\n            <.func>\n              <:slot boolean=\"boolean\" />\n              <:slot boolean={:boolean} />\n              <:slot boolean={true} />\n              <:slot boolean={1} />\n              <:slot boolean={1.0} />\n              <:slot boolean={[]} />\n              <:slot boolean={nil} />\n            </.func>\n            \"\"\"\n          end\n\n          def render_integer_line, do: __ENV__.line + 5\n\n          def render_integer(assigns) do\n            ~H\"\"\"\n            <.func>\n              <:slot integer=\"integer\" />\n              <:slot integer={:integer} />\n              <:slot integer={true} />\n              <:slot integer={1} />\n              <:slot integer={1.0} />\n              <:slot integer={[]} />\n              <:slot integer={nil} />\n            </.func>\n            \"\"\"\n          end\n\n          def render_float_line, do: __ENV__.line + 5\n\n          def render_float(assigns) do\n            ~H\"\"\"\n            <.func>\n              <:slot float=\"float\" />\n              <:slot float={:float} />\n              <:slot float={true} />\n              <:slot float={1} />\n              <:slot float={1.0} />\n              <:slot float={[]} />\n              <:slot float={nil} />\n            </.func>\n            \"\"\"\n          end\n\n          def render_list_line, do: __ENV__.line + 5\n\n          def render_list(assigns) do\n            ~H\"\"\"\n            <.func>\n              <:slot list=\"list\" />\n              <:slot list={:list} />\n              <:slot list={true} />\n              <:slot list={1} />\n              <:slot list={1.0} />\n              <:slot list={[]} />\n              <:slot list={nil} />\n            </.func>\n            \"\"\"\n          end\n        end\n      end)\n\n    line = get_line(__MODULE__.SlotAttrs, :render_string_line)\n\n    for {value, offset} <- [\n          {:string, 1},\n          {true, 2},\n          {1, 3},\n          {1.0, 4},\n          {[], 5},\n          {nil, 6}\n        ] do\n      assert warnings =~ \"\"\"\n             attribute \"string\" \\\n             in slot \\\"slot\\\" \\\n             for component Phoenix.ComponentVerifyTest.SlotAttrs.func/1 \\\n             must be a :string, got: #{inspect(value)}\n             \"\"\"\n\n      assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + offset}: (file)\"\n    end\n\n    line = get_line(__MODULE__.SlotAttrs, :render_atom_line)\n\n    for {value, offset} <- [\n          {\"atom\", 0},\n          {1, 3},\n          {1.0, 4},\n          {[], 5}\n        ] do\n      assert warnings =~ \"\"\"\n             attribute \"atom\" \\\n             in slot \\\"slot\\\" \\\n             for component Phoenix.ComponentVerifyTest.SlotAttrs.func/1 \\\n             must be an :atom, got: #{inspect(value)}\n             \"\"\"\n\n      assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + offset}: (file)\"\n    end\n\n    line = get_line(__MODULE__.SlotAttrs, :render_boolean_line)\n\n    for {value, offset} <- [\n          {\"boolean\", 0},\n          {:boolean, 1},\n          {1, 3},\n          {1.0, 4},\n          {[], 5},\n          {nil, 6}\n        ] do\n      assert warnings =~ \"\"\"\n             attribute \"boolean\" \\\n             in slot \\\"slot\\\" \\\n             for component Phoenix.ComponentVerifyTest.SlotAttrs.func/1 \\\n             must be a :boolean, got: #{inspect(value)}\n             \"\"\"\n\n      assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + offset}: (file)\"\n    end\n\n    line = get_line(__MODULE__.SlotAttrs, :render_integer_line)\n\n    for {value, offset} <- [\n          {\"integer\", 0},\n          {:integer, 1},\n          {true, 2},\n          {1.0, 4},\n          {[], 5},\n          {nil, 6}\n        ] do\n      assert warnings =~ \"\"\"\n             attribute \"integer\" \\\n             in slot \\\"slot\\\" \\\n             for component Phoenix.ComponentVerifyTest.SlotAttrs.func/1 \\\n             must be an :integer, got: #{inspect(value)}\n             \"\"\"\n\n      assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + offset}: (file)\"\n    end\n\n    line = get_line(__MODULE__.SlotAttrs, :render_float_line)\n\n    for {value, offset} <- [\n          {\"float\", 0},\n          {:float, 1},\n          {true, 2},\n          {1, 3},\n          {[], 5},\n          {nil, 6}\n        ] do\n      assert warnings =~ \"\"\"\n             attribute \"float\" \\\n             in slot \\\"slot\\\" \\\n             for component Phoenix.ComponentVerifyTest.SlotAttrs.func/1 \\\n             must be a :float, got: #{inspect(value)}\n             \"\"\"\n\n      assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + offset}: (file)\"\n    end\n\n    line = get_line(__MODULE__.SlotAttrs, :render_list_line)\n\n    for {value, offset} <- [\n          {\"list\", 0},\n          {:list, 1},\n          {true, 2},\n          {1, 3},\n          {1.0, 4},\n          {nil, 6}\n        ] do\n      assert warnings =~ \"\"\"\n             attribute \"list\" \\\n             in slot \\\"slot\\\" \\\n             for component Phoenix.ComponentVerifyTest.SlotAttrs.func/1 \\\n             must be a :list, got: #{inspect(value)}\n             \"\"\"\n\n      assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + offset}: (file)\"\n    end\n  end\n\n  test \"validates required slot attrs\" do\n    warnings =\n      capture_io(:stderr, fn ->\n        defmodule RequiredSlotAttrs do\n          use Phoenix.Component\n\n          slot :slot do\n            attr :attr, :string, required: true\n          end\n\n          def func(assigns) do\n            ~H\"\"\"\n            <div>\n              {render_slot(@slot)}\n            </div>\n            \"\"\"\n          end\n\n          def line(), do: __ENV__.line + 4\n\n          def render(assigns) do\n            ~H\"\"\"\n            <.func>\n              <:slot />\n              <:slot attr=\"foo\" />\n              <:slot>\n                foo\n              </:slot>\n              <:slot attr=\"bar\">\n                bar\n              </:slot>\n              <:slot {[attr: \"bar\"]} />\n            </.func>\n            \"\"\"\n          end\n        end\n      end)\n\n    line = get_line(__MODULE__.RequiredSlotAttrs)\n\n    assert warnings =~ \"\"\"\n           missing required attribute \"attr\" \\\n           in slot \"slot\" \\\n           for component \\\n           Phoenix.ComponentVerifyTest.RequiredSlotAttrs.func/1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 1}: (file)\"\n\n    assert warnings =~ \"\"\"\n           missing required attribute \"attr\" \\\n           in slot \"slot\" \\\n           for component \\\n           Phoenix.ComponentVerifyTest.RequiredSlotAttrs.func/1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 3}: (file)\"\n  end\n\n  test \"validates undefined slots\" do\n    warnings =\n      capture_io(:stderr, fn ->\n        defmodule UndefinedSlots do\n          use Phoenix.Component\n\n          attr :attr, :any\n\n          def fun_no_slots(assigns), do: ~H[]\n\n          slot(:inner_block)\n\n          def func(assigns), do: ~H[]\n\n          slot(:named)\n\n          def func_undefined_slot_attrs(assigns), do: ~H[]\n\n          def line, do: __ENV__.line + 2\n\n          def render(assigns) do\n            ~H\"\"\"\n            <!-- undefined default slot -->\n            <.fun_no_slots>\n              hello\n            </.fun_no_slots>\n            <!-- undefined named slot -->\n            <.func>\n              <:undefined />\n            </.func>\n            <!-- named slot with undefined attrs -->\n            <.func_undefined_slot_attrs>\n              <:named undefined />\n              <:named undefined=\"undefined\" />\n            </.func_undefined_slot_attrs>\n            \"\"\"\n          end\n        end\n      end)\n\n    line = get_line(__MODULE__.UndefinedSlots)\n\n    assert warnings =~ \"\"\"\n           undefined slot \"inner_block\" for component \\\n           Phoenix.ComponentVerifyTest.UndefinedSlots.fun_no_slots/1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 3}: (file)\"\n\n    assert warnings =~ \"\"\"\n           undefined slot \"undefined\" for component \\\n           Phoenix.ComponentVerifyTest.UndefinedSlots.func/1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 8}: (file)\"\n\n    assert warnings =~ \"\"\"\n           undefined attribute \"undefined\" \\\n           in slot \"named\" \\\n           for component \\\n           Phoenix.ComponentVerifyTest.UndefinedSlots.func_undefined_slot_attrs/1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 12}: (file)\"\n\n    assert warnings =~ \"\"\"\n           undefined attribute \"undefined\" \\\n           in slot \"named\" \\\n           for component \\\n           Phoenix.ComponentVerifyTest.UndefinedSlots.func_undefined_slot_attrs/1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 13}: (file)\"\n  end\n\n  test \"validates calls for locally defined components\" do\n    warnings =\n      capture_io(:stderr, fn ->\n        defmodule LocalComponents do\n          use Phoenix.Component\n\n          attr :attr, :string, required: true\n\n          def public(assigns) do\n            ~H\"\"\"\n            {@attr}\n            \"\"\"\n          end\n\n          attr :attr, :string, required: true\n\n          defp private(assigns) do\n            ~H\"\"\"\n            {@attr}\n            \"\"\"\n          end\n\n          def line, do: __ENV__.line + 2\n\n          def render(assigns) do\n            ~H\"\"\"\n            <.public />\n            <.private />\n            \"\"\"\n          end\n        end\n      end)\n\n    line = get_line(__MODULE__.LocalComponents)\n\n    assert warnings =~ \"\"\"\n           missing required attribute \"attr\" \\\n           for component \\\n           Phoenix.ComponentVerifyTest.LocalComponents.public/1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 2}: (file)\"\n\n    assert warnings =~ \"\"\"\n           missing required attribute \"attr\" \\\n           for component \\\n           Phoenix.ComponentVerifyTest.LocalComponents.private/1\n           \"\"\"\n\n    assert warnings =~ \"test/phoenix_component/verify_test.exs:#{line + 3}: (file)\"\n  end\n\n  test \"global includes\" do\n    import Phoenix.LiveViewTest\n\n    defmodule GlobalIncludes do\n      use Phoenix.Component\n\n      attr :id, :any, required: true\n      attr :rest, :global, include: ~w(form)\n      def button(assigns), do: ~H|<button id={@id} {@rest}>button</button>|\n      def any_render(assigns), do: ~H|<.button id=\"123\" form=\"my-form\" />|\n    end\n\n    assigns = %{id: \"abc\", form: \"my-form\"}\n\n    assert render_component(&GlobalIncludes.button/1, assigns) ==\n             \"<button id=\\\"abc\\\" form=\\\"my-form\\\">button</button>\"\n  end\n\n  defp get_line(module, fun \\\\ :line) do\n    apply(module, fun, [])\n  end\nend\n"
  },
  {
    "path": "test/phoenix_component_test.exs",
    "content": "defmodule Phoenix.ComponentUnitTest do\n  use ExUnit.Case, async: true\n\n  alias Phoenix.LiveView.{Socket, Utils}\n  import Phoenix.Component\n\n  @socket Utils.configure_socket(\n            %Socket{\n              endpoint: Endpoint,\n              router: Phoenix.LiveViewTest.Support.Router,\n              view: Phoenix.LiveViewTest.Support.ParamCounterLive\n            },\n            %{\n              connect_params: %{},\n              connect_info: %{},\n              root_view: Phoenix.LiveViewTest.Support.ParamCounterLive,\n              __changed__: %{}\n            },\n            nil,\n            %{},\n            URI.parse(\"https://www.example.com\")\n          )\n\n  @assigns_changes %{key: \"value\", map: %{foo: :bar}, __changed__: %{}}\n  @assigns_nil_changes %{key: \"value\", map: %{foo: :bar}, __changed__: nil}\n\n  describe \"assign with socket\" do\n    test \"tracks changes\" do\n      socket = assign(@socket, existing: \"foo\")\n      assert changed?(socket, :existing)\n\n      socket = Utils.clear_changed(socket)\n      socket = assign(socket, existing: \"foo\")\n      refute changed?(socket, :existing)\n    end\n\n    test \"keeps whole maps in changes\" do\n      socket = assign(@socket, existing: %{foo: :bar})\n      socket = Utils.clear_changed(socket)\n\n      socket = assign(socket, existing: %{foo: :baz})\n      assert socket.assigns.existing == %{foo: :baz}\n      assert socket.assigns.__changed__.existing == %{foo: :bar}\n\n      socket = assign(socket, existing: %{foo: :bat})\n      assert socket.assigns.existing == %{foo: :bat}\n      assert socket.assigns.__changed__.existing == %{foo: :bar}\n\n      socket = assign(socket, %{existing: %{foo: :bam}})\n      assert socket.assigns.existing == %{foo: :bam}\n      assert socket.assigns.__changed__.existing == %{foo: :bar}\n    end\n\n    test \"keeps whole lists in changes\" do\n      socket = assign(@socket, existing: [:foo, :bar])\n      socket = Utils.clear_changed(socket)\n\n      socket = assign(socket, existing: [:foo, :baz])\n      assert socket.assigns.existing == [:foo, :baz]\n      assert socket.assigns.__changed__.existing == [:foo, :bar]\n\n      socket = assign(socket, existing: [:foo, :bat])\n      assert socket.assigns.existing == [:foo, :bat]\n      assert socket.assigns.__changed__.existing == [:foo, :bar]\n\n      socket = assign(socket, %{existing: [:foo, :bam]})\n      assert socket.assigns.existing == [:foo, :bam]\n      assert socket.assigns.__changed__.existing == [:foo, :bar]\n    end\n\n    test \"allows functions\" do\n      socket = assign(@socket, fn _ -> [existing: [:foo, :bar]] end)\n      socket = Utils.clear_changed(socket)\n\n      socket = assign(socket, fn %{existing: [:foo, :bar]} -> %{existing: [:foo, :baz]} end)\n      assert socket.assigns.existing == [:foo, :baz]\n      assert socket.assigns.__changed__.existing == [:foo, :bar]\n    end\n  end\n\n  describe \"assign with assigns\" do\n    test \"tracks changes\" do\n      assigns = assign(@assigns_changes, key: \"value\")\n      assert assigns.key == \"value\"\n      refute changed?(assigns, :key)\n\n      assigns = assign(@assigns_changes, key: \"changed\")\n      assert assigns.key == \"changed\"\n      assert changed?(assigns, :key)\n\n      assigns = assign(@assigns_nil_changes, key: \"changed\")\n      assert assigns.key == \"changed\"\n      assert assigns.__changed__ == nil\n      assert changed?(assigns, :key)\n    end\n\n    test \"track changes on unknown vars\" do\n      assigns = assign(@assigns_changes, unknown: nil)\n      assert assigns.unknown == nil\n      assert changed?(assigns, :unknown)\n\n      assigns = assign(@assigns_changes, unknown: \"changed\")\n      assert assigns.unknown == \"changed\"\n      assert changed?(assigns, :unknown)\n    end\n\n    test \"keeps whole maps in changes\" do\n      assigns = assign(@assigns_changes, map: %{foo: :baz})\n      assert assigns.map == %{foo: :baz}\n      assert assigns.__changed__[:map] == %{foo: :bar}\n\n      assigns = assign(@assigns_nil_changes, map: %{foo: :baz})\n      assert assigns.map == %{foo: :baz}\n      assert assigns.__changed__ == nil\n    end\n\n    test \"allows functions\" do\n      assigns = assign(@assigns_changes, fn _ -> %{map: %{foo: :baz}} end)\n      assert assigns.map == %{foo: :baz}\n      assert assigns.__changed__[:map] == %{foo: :bar}\n\n      assigns = assign(@assigns_nil_changes, fn _ -> [map: %{foo: :baz}] end)\n      assert assigns.map == %{foo: :baz}\n      assert assigns.__changed__ == nil\n    end\n  end\n\n  describe \"assign_new with socket\" do\n    test \"uses socket assigns if no parent assigns are present\" do\n      socket =\n        @socket\n        |> assign(existing: \"existing\")\n        |> assign_new(:existing, fn -> \"new-existing\" end)\n        |> assign_new(:notexisting, fn -> \"new-notexisting\" end)\n\n      assert socket.assigns == %{\n               existing: \"existing\",\n               notexisting: \"new-notexisting\",\n               live_action: nil,\n               flash: %{},\n               __changed__: %{existing: true, notexisting: true}\n             }\n    end\n\n    test \"uses parent assigns when present and falls back to socket assigns\" do\n      socket =\n        put_in(@socket.private[:assign_new], {%{existing: \"existing-parent\"}, []})\n        |> assign(existing2: \"existing2\")\n        |> assign_new(:existing, fn -> \"new-existing\" end)\n        |> assign_new(:existing2, fn -> \"new-existing2\" end)\n        |> assign_new(:notexisting, fn -> \"new-notexisting\" end)\n\n      assert socket.assigns == %{\n               existing: \"existing-parent\",\n               existing2: \"existing2\",\n               notexisting: \"new-notexisting\",\n               live_action: nil,\n               flash: %{},\n               __changed__: %{existing: true, notexisting: true, existing2: true}\n             }\n    end\n\n    test \"has access to assigns\" do\n      socket =\n        put_in(@socket.private[:assign_new], {%{existing: \"existing-parent\"}, []})\n        |> assign(existing2: \"existing2\")\n        |> assign_new(:existing, fn _ -> \"new-existing\" end)\n        |> assign_new(:existing2, fn _ -> \"new-existing2\" end)\n        |> assign_new(:notexisting, fn %{existing: existing} -> existing end)\n        |> assign_new(:notexisting2, fn %{existing2: existing2} -> existing2 end)\n        |> assign_new(:notexisting3, fn %{notexisting: notexisting} -> notexisting end)\n\n      assert socket.assigns == %{\n               existing: \"existing-parent\",\n               existing2: \"existing2\",\n               notexisting: \"existing-parent\",\n               notexisting2: \"existing2\",\n               notexisting3: \"existing-parent\",\n               live_action: nil,\n               flash: %{},\n               __changed__: %{\n                 existing: true,\n                 existing2: true,\n                 notexisting: true,\n                 notexisting2: true,\n                 notexisting3: true\n               }\n             }\n    end\n  end\n\n  describe \"assign_new with assigns\" do\n    test \"tracks changes\" do\n      assigns = assign_new(@assigns_changes, :key, fn -> raise \"won't be invoked\" end)\n      assert assigns.key == \"value\"\n      refute changed?(assigns, :key)\n      refute assigns.__changed__[:key]\n\n      assigns = assign_new(@assigns_changes, :another, fn -> \"changed\" end)\n      assert assigns.another == \"changed\"\n      assert changed?(assigns, :another)\n\n      assigns = assign_new(@assigns_nil_changes, :another, fn -> \"changed\" end)\n      assert assigns.another == \"changed\"\n      assert changed?(assigns, :another)\n      assert assigns.__changed__ == nil\n    end\n\n    test \"has access to new assigns\" do\n      assigns =\n        assign_new(@assigns_changes, :another, fn -> \"changed\" end)\n        |> assign_new(:and_another, fn %{another: another} -> another end)\n\n      assert assigns.and_another == \"changed\"\n      assert changed?(assigns, :another)\n      assert changed?(assigns, :and_another)\n    end\n  end\n\n  describe \"update with socket\" do\n    test \"tracks changes\" do\n      socket = @socket |> assign(key: \"value\") |> Utils.clear_changed()\n\n      socket = update(socket, :key, fn \"value\" -> \"value\" end)\n      assert socket.assigns.key == \"value\"\n      refute changed?(socket, :key)\n\n      socket = update(socket, :key, fn \"value\" -> \"changed\" end)\n      assert socket.assigns.key == \"changed\"\n      assert changed?(socket, :key)\n    end\n  end\n\n  describe \"update with assigns\" do\n    test \"tracks changes\" do\n      assigns = update(@assigns_changes, :key, fn \"value\" -> \"value\" end)\n      assert assigns.key == \"value\"\n      refute changed?(assigns, :key)\n\n      assigns = update(@assigns_changes, :key, fn \"value\" -> \"changed\" end)\n      assert assigns.key == \"changed\"\n      assert changed?(assigns, :key)\n\n      assigns = update(@assigns_nil_changes, :key, fn \"value\" -> \"changed\" end)\n      assert assigns.key == \"changed\"\n      assert changed?(assigns, :key)\n      assert assigns.__changed__ == nil\n    end\n  end\n\n  describe \"update with arity 2 function\" do\n    test \"passes socket assigns to update function\" do\n      socket = @socket |> assign(key: \"value\", key2: \"another\") |> Utils.clear_changed()\n\n      socket = update(socket, :key2, fn key2, %{key: key} -> key2 <> \" \" <> key end)\n      assert socket.assigns.key2 == \"another value\"\n      assert changed?(socket, :key2)\n    end\n\n    test \"passes assigns to update function\" do\n      assigns = update(@assigns_changes, :key, fn _, %{map: %{foo: bar}} -> bar end)\n      assert assigns.key == :bar\n      assert changed?(assigns, :key)\n    end\n  end\n\n  test \"assigns_to_attributes/2\" do\n    assert assigns_to_attributes(%{}) == []\n    assert assigns_to_attributes(%{}, [:non_exists]) == []\n    assert assigns_to_attributes(%{one: 1, two: 2}) == [one: 1, two: 2]\n    assert assigns_to_attributes(%{one: 1, two: 2}, [:one]) == [two: 2]\n    assert assigns_to_attributes(%{__changed__: %{}, one: 1, two: 2}, [:one]) == [two: 2]\n    assert assigns_to_attributes(%{__changed__: %{}, inner_block: fn -> :ok end, a: 1}) == [a: 1]\n    assert assigns_to_attributes(%{__slot__: :foo, inner_block: fn -> :ok end, a: 1}) == [a: 1]\n  end\n\n  describe \"to_form/2\" do\n    test \"with a map\" do\n      form = to_form(%{})\n      assert form.name == nil\n      assert form.id == nil\n\n      form = to_form(%{}, as: :foo)\n      assert form.name == \"foo\"\n      assert form.id == \"foo\"\n\n      form = to_form(%{}, as: :foo, id: \"bar\")\n      assert form.name == \"foo\"\n      assert form.id == \"bar\"\n\n      form = to_form(%{}, custom: \"attr\")\n      assert form.options == [custom: \"attr\"]\n\n      form = to_form(%{}, errors: [name: \"can't be blank\"])\n      assert form.errors == [name: \"can't be blank\"]\n    end\n\n    test \"with a form\" do\n      base = to_form(%{}, as: \"name\", id: \"id\")\n      assert to_form(base, []) == base\n\n      form = to_form(base, as: :foo)\n      assert form.name == \"foo\"\n      assert form.id == \"foo\"\n\n      form = to_form(base, id: \"bar\")\n      assert form.name == \"name\"\n      assert form.id == \"bar\"\n\n      form = to_form(base, as: :foo, id: \"bar\")\n      assert form.name == \"foo\"\n      assert form.id == \"bar\"\n\n      form = to_form(base, as: nil, id: nil)\n      assert form.name == nil\n      assert form.id == nil\n\n      form = to_form(base, custom: \"attr\")\n      assert form.options[:custom] == \"attr\"\n\n      form = to_form(base, errors: [name: \"can't be blank\"])\n      assert form.errors == [name: \"can't be blank\"]\n\n      form = to_form(base, action: :validate)\n      assert form.action == :validate\n\n      form = to_form(%{base | action: :validate})\n      assert form.action == :validate\n    end\n  end\n\n  test \"used_input?/1\" do\n    params = %{}\n    form = to_form(params, as: \"profile\", action: :validate)\n    refute used_input?(form[:username])\n    refute used_input?(form[:email])\n\n    params = %{\"username\" => \"\", \"email\" => \"\"}\n    form = to_form(params, as: \"profile\", action: :validate)\n    assert used_input?(form[:username])\n    assert used_input?(form[:email])\n\n    params = %{\"username\" => \"\", \"email\" => \"\", \"_unused_username\" => \"\"}\n    form = to_form(params, as: \"profile\", action: :validate)\n    refute used_input?(form[:username])\n    assert used_input?(form[:email])\n\n    params = %{\"username\" => \"\", \"email\" => \"\", \"_unused_username\" => \"\", \"_unused_email\" => \"\"}\n    form = to_form(params, as: \"profile\", action: :validate)\n    refute used_input?(form[:username])\n    refute used_input?(form[:email])\n\n    params = %{\n      \"bday\" => %{\"day\" => \"\", \"month\" => \"\", \"year\" => \"\"},\n      \"published_at\" => %{\"date\" => \"\", \"time\" => \"\", \"_unused_date\" => \"\", \"_unused_time\" => \"\"},\n      \"deleted_at\" => %{},\n      \"inserted_at\" => %{\"date\" => \"\", \"time\" => \"\", \"_unused_time\" => \"\"},\n      \"date\" => DateTime.utc_now()\n    }\n\n    form = to_form(params, as: \"profile\", action: :validate)\n    assert used_input?(form[:bday])\n    refute used_input?(form[:published_at])\n    refute used_input?(form[:deleted_at])\n    assert used_input?(form[:inserted_at])\n    assert used_input?(form[:date])\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/async_result_test.exs",
    "content": "defmodule Phoenix.LiveView.AsyncResultTest do\n  use ExUnit.Case, async: true\n  alias Phoenix.LiveView.AsyncResult\n  doctest Phoenix.LiveView.AsyncResult\nend\n"
  },
  {
    "path": "test/phoenix_live_view/async_test.exs",
    "content": "defmodule Phoenix.LiveView.AsyncTest do\n  # run with async: false to prevent other messages from being captured\n  use ExUnit.Case, async: false\n\n  import ExUnit.CaptureIO\n\n  describe \"async operations - eval_quoted\" do\n    for fun <- [:assign_async, :start_async, :stream_async] do\n      test \"warns when passing socket to #{fun} function\" do\n        warnings =\n          capture_io(:stderr, fn ->\n            fun = unquote(fun)\n\n            Code.eval_quoted(\n              quote do\n                require Phoenix.LiveView\n\n                socket = %Phoenix.LiveView.Socket{\n                  assigns: %{__changed__: %{}, bar: :baz},\n                  private: %{lifecycle: %Phoenix.LiveView.Lifecycle{}}\n                }\n\n                Phoenix.LiveView.unquote(fun)(socket, :foo, fn ->\n                  socket.assigns.bar\n                end)\n              end\n            )\n          end)\n\n        assert warnings =~\n                 \"you are accessing the LiveView Socket inside a function given to #{unquote(fun)}\"\n      end\n\n      test \"does not warn when accessing socket outside of function passed to #{fun}\" do\n        warnings =\n          capture_io(:stderr, fn ->\n            fun = unquote(fun)\n\n            Code.eval_quoted(\n              quote do\n                require Phoenix.LiveView\n\n                socket = %Phoenix.LiveView.Socket{\n                  assigns: %{__changed__: %{}, bar: :baz},\n                  private: %{lifecycle: %Phoenix.LiveView.Lifecycle{}}\n                }\n\n                bar = socket.assigns.bar\n\n                Phoenix.LiveView.unquote(fun)(socket, :foo, fn ->\n                  bar\n                end)\n              end\n            )\n          end)\n\n        refute warnings =~\n                 \"you are accessing the LiveView Socket inside a function given to #{unquote(fun)}\"\n      end\n    end\n  end\n\n  describe \"async operations\" do\n    for fun <- [:assign_async, :start_async, :stream_async] do\n      test \"warns when passing socket to #{fun} function\", %{test: test} do\n        warnings =\n          capture_io(:stderr, fn ->\n            defmodule Module.concat(AssignAsyncSocket, \"Test#{:erlang.phash2(test)}\") do\n              use Phoenix.LiveView\n\n              def mount(_params, _session, socket) do\n                {:ok,\n                 unquote(fun)(socket, :foo, fn ->\n                   do_something(socket.assigns)\n                 end)}\n              end\n\n              defp do_something(_socket), do: :ok\n            end\n          end)\n\n        assert warnings =~\n                 \"you are accessing the LiveView Socket inside a function given to #{unquote(fun)}\"\n      end\n\n      test \"does not warn when accessing socket outside of function passed to #{fun}\", %{\n        test: test\n      } do\n        warnings =\n          capture_io(:stderr, fn ->\n            defmodule Module.concat(AssignAsyncSocket, \"Test#{:erlang.phash2(test)}\") do\n              use Phoenix.LiveView\n\n              def mount(_params, _session, socket) do\n                socket = assign(socket, :foo, :bar)\n                foo = socket.assigns.foo\n\n                {:ok,\n                 unquote(fun)(socket, :foo, fn ->\n                   do_something(foo)\n                 end)}\n              end\n\n              defp do_something(assigns), do: :ok\n            end\n          end)\n\n        refute warnings =~\n                 \"you are accessing the LiveView Socket inside a function given to #{unquote(fun)}\"\n      end\n\n      test \"does not warn when argument is not a function (#{fun})\", %{test: test} do\n        warnings =\n          capture_io(:stderr, fn ->\n            defmodule Module.concat(AssignAsyncSocket, \"Test#{:erlang.phash2(test)}\") do\n              use Phoenix.LiveView\n\n              def mount(_params, _session, socket) do\n                socket = assign(socket, :foo, :bar)\n\n                {:ok, unquote(fun)(socket, :foo, function_that_returns_the_func(socket))}\n              end\n\n              defp function_that_returns_the_func(socket) do\n                foo = socket.assigns.foo\n\n                fn ->\n                  do_something(foo)\n                end\n              end\n\n              defp do_something(assigns), do: :ok\n            end\n          end)\n\n        refute warnings =~\n                 \"you are accessing the LiveView Socket inside a function given to #{unquote(fun)}\"\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/colocated_hook_test.exs",
    "content": "defmodule Phoenix.LiveView.ColocatedHookTest do\n  # we set async: false because we call the colocated JS compiler\n  # and it reads / writes to a shared folder\n  use ExUnit.Case, async: false\n\n  test \"can use a hook\" do\n    defmodule TestComponent do\n      use Phoenix.Component\n      alias Phoenix.LiveView.ColocatedHook, as: Hook\n\n      def fun(assigns) do\n        ~H\"\"\"\n        <script :type={Hook} name=\".fun\">\n          export default {\n            mounted() {\n              this.el.textContent = \"Hello, world!\";\n            },\n          };\n        </script>\n\n        <div id=\"hook\" phx-hook=\".fun\"></div>\n        \"\"\"\n      end\n    end\n\n    assert module_folders =\n             File.ls!(Path.join(Mix.Project.build_path(), \"phoenix-colocated/phoenix_live_view\"))\n\n    assert folder =\n             Enum.find(module_folders, fn folder ->\n               folder =~ ~r/#{inspect(__MODULE__)}\\.TestComponent/\n             end)\n\n    assert [script] =\n             Path.wildcard(\n               Path.join(\n                 Mix.Project.build_path(),\n                 \"phoenix-colocated/phoenix_live_view/#{folder}/*.js\"\n               )\n             )\n\n    assert File.read!(script) =~ \"Hello, world!\"\n\n    # now write the manifest manually as we are in a test\n    Phoenix.LiveView.ColocatedJS.compile()\n\n    assert manifest =\n             File.read!(\n               Path.join(Mix.Project.build_path(), \"phoenix-colocated/phoenix_live_view/index.js\")\n             )\n\n    assert manifest =~ ~r/export \\{ imp_.* as hooks \\}/\n\n    # script is in manifest\n    assert manifest =~\n             Path.relative_to(\n               script,\n               Path.join(Mix.Project.build_path(), \"phoenix-colocated/phoenix_live_view/\")\n             )\n  after\n    :code.delete(__MODULE__.TestComponent)\n    :code.purge(__MODULE__.TestComponent)\n  end\n\n  test \"raises for invalid name\" do\n    assert_raise Phoenix.LiveView.TagEngine.Tokenizer.ParseError,\n                 ~r/the name attribute of a colocated hook must be a compile-time string\\. Got: @foo/,\n                 fn ->\n                   defmodule TestComponentInvalidName do\n                     use Phoenix.Component\n                     alias Phoenix.LiveView.ColocatedHook, as: Hook\n\n                     def fun(assigns) do\n                       ~H\"\"\"\n                       <script :type={Hook} name={@foo}>\n                         export default {\n                           mounted() {\n                             this.el.textContent = \"Hello, world!\";\n                           },\n                         };\n                       </script>\n\n                       <div id=\"hook\" phx-hook=\".fun\"></div>\n                       \"\"\"\n                     end\n                   end\n                 end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/colocated_js_test.exs",
    "content": "defmodule Phoenix.LiveView.ColocatedJSTest do\n  # we set async: false because we call the colocated JS compiler\n  # and it reads / writes to a shared folder\n  use ExUnit.Case, async: false\n\n  test \"simple script is extracted and available under default export object\" do\n    defmodule TestComponent do\n      use Phoenix.Component\n      alias Phoenix.LiveView.ColocatedJS, as: Colo\n\n      def fun(assigns) do\n        ~H\"\"\"\n        <script :type={Colo} name=\"my-script\">\n          export default function () {\n            console.log(\"hey!\");\n          }\n        </script>\n        \"\"\"\n      end\n    end\n\n    assert module_folders =\n             File.ls!(Path.join(Mix.Project.build_path(), \"phoenix-colocated/phoenix_live_view\"))\n\n    assert folder =\n             Enum.find(module_folders, fn folder ->\n               folder =~ ~r/#{inspect(__MODULE__)}\\.TestComponent$/\n             end)\n\n    assert [script] =\n             Path.wildcard(\n               Path.join(\n                 Mix.Project.build_path(),\n                 \"phoenix-colocated/phoenix_live_view/#{folder}/*.js\"\n               )\n             )\n\n    assert File.read!(script) == \"\"\"\n\n             export default function () {\n               console.log(\"hey!\");\n             }\n           \"\"\"\n\n    # now write the manifest manually as we are in a test\n    Phoenix.LiveView.ColocatedJS.compile()\n\n    assert manifest =\n             File.read!(\n               Path.join(Mix.Project.build_path(), \"phoenix-colocated/phoenix_live_view/index.js\")\n             )\n\n    assert manifest =~ \"export default js;\"\n    assert manifest =~ \"js[\\\"my-script\\\"] = js_\"\n\n    # script is in manifest\n    assert manifest =~\n             Path.relative_to(\n               script,\n               Path.join(Mix.Project.build_path(), \"phoenix-colocated/phoenix_live_view/\")\n             )\n  after\n    :code.delete(__MODULE__.TestComponent)\n    :code.purge(__MODULE__.TestComponent)\n  end\n\n  test \"keyed script is available under default named export\" do\n    defmodule TestComponentKey do\n      use Phoenix.Component\n      alias Phoenix.LiveView.ColocatedJS, as: Colo\n\n      def fun(assigns) do\n        ~H\"\"\"\n        <script :type={Colo} name=\"my-script\" key=\"components\">\n          export default function () {\n            console.log(\"hey!\");\n          }\n        </script>\n        \"\"\"\n      end\n    end\n\n    assert module_folders =\n             File.ls!(Path.join(Mix.Project.build_path(), \"phoenix-colocated/phoenix_live_view\"))\n\n    assert folder =\n             Enum.find(module_folders, fn folder ->\n               folder =~ ~r/#{inspect(__MODULE__)}\\.TestComponentKey/\n             end)\n\n    assert [script] =\n             Path.wildcard(\n               Path.join(\n                 Mix.Project.build_path(),\n                 \"phoenix-colocated/phoenix_live_view/#{folder}/*.js\"\n               )\n             )\n\n    assert File.read!(script) == \"\"\"\n\n             export default function () {\n               console.log(\"hey!\");\n             }\n           \"\"\"\n\n    # now write the manifest manually as we are in a test\n    Phoenix.LiveView.ColocatedJS.compile()\n\n    relative_script_path =\n      Path.relative_to(\n        script,\n        Path.join(Mix.Project.build_path(), \"phoenix-colocated/phoenix_live_view/\")\n      )\n\n    assert manifest =\n             File.read!(\n               Path.join(Mix.Project.build_path(), \"phoenix-colocated/phoenix_live_view/index.js\")\n             )\n\n    assert line =\n             Enum.find(String.split(manifest, \"\\n\"), fn line ->\n               line =~ inspect(__MODULE__.TestComponentKey)\n             end)\n\n    assert [_match, js_name] =\n             Regex.run(~r/import (js_.*) from \"\\.\\/#{Regex.escape(relative_script_path)}\";/, line)\n\n    assert [_match, export_name] = Regex.run(~r/export \\{ (imp_.*) as components \\}/, manifest)\n    assert manifest =~ \"#{export_name}[\\\"my-script\\\"] = #{js_name};\"\n  after\n    :code.delete(__MODULE__.TestComponentKey)\n    :code.purge(__MODULE__.TestComponentKey)\n  end\n\n  test \"nameless script is imported for side effects only\" do\n    defmodule TestComponentSideEffects do\n      use Phoenix.Component\n      alias Phoenix.LiveView.ColocatedJS, as: Colo\n\n      def fun(assigns) do\n        ~H\"\"\"\n        <script :type={Colo}>\n          console.log(\"hey!\");\n        </script>\n        \"\"\"\n      end\n    end\n\n    assert module_folders =\n             File.ls!(Path.join(Mix.Project.build_path(), \"phoenix-colocated/phoenix_live_view\"))\n\n    assert folder =\n             Enum.find(module_folders, fn folder ->\n               folder =~ ~r/#{inspect(__MODULE__)}\\.TestComponentSideEffects/\n             end)\n\n    assert [script] =\n             Path.wildcard(\n               Path.join(\n                 Mix.Project.build_path(),\n                 \"phoenix-colocated/phoenix_live_view/#{folder}/*.js\"\n               )\n             )\n\n    assert File.read!(script) == \"\"\"\n\n             console.log(\"hey!\");\n           \"\"\"\n\n    # now write the manifest manually as we are in a test\n    Phoenix.LiveView.ColocatedJS.compile()\n\n    relative_script_path =\n      Path.relative_to(\n        script,\n        Path.join(Mix.Project.build_path(), \"phoenix-colocated/phoenix_live_view/\")\n      )\n\n    assert manifest =\n             File.read!(\n               Path.join(Mix.Project.build_path(), \"phoenix-colocated/phoenix_live_view/index.js\")\n             )\n\n    assert line =\n             Enum.find(String.split(manifest, \"\\n\"), fn line ->\n               line =~ inspect(__MODULE__.TestComponentSideEffects)\n             end)\n\n    assert [_match] =\n             Regex.run(~r/import \"\\.\\/#{Regex.escape(relative_script_path)}\";/, line)\n  after\n    :code.delete(__MODULE__.TestComponentSideEffects)\n    :code.purge(__MODULE__.TestComponentSideEffects)\n  end\n\n  test \"raises for invalid name\" do\n    assert_raise Phoenix.LiveView.TagEngine.Tokenizer.ParseError,\n                 ~r/the name attribute of a colocated script must be a compile-time string\\. Got: @foo/,\n                 fn ->\n                   defmodule TestComponentInvalidName do\n                     use Phoenix.Component\n                     alias Phoenix.LiveView.ColocatedJS, as: Colo\n\n                     def fun(assigns) do\n                       ~H\"\"\"\n                       <script :type={Colo} name={@foo}>\n                         1 + 1;\n                       </script>\n                       \"\"\"\n                     end\n                   end\n                 end\n  end\n\n  test \"writes empty index.js when no colocated scripts exist\" do\n    manifest = Path.join(Mix.Project.build_path(), \"phoenix-colocated/phoenix_live_view/index.js\")\n    Phoenix.LiveView.ColocatedJS.compile()\n    assert File.exists?(manifest)\n    assert File.read!(manifest) == \"export const hooks = {};\\nexport default {};\"\n  end\n\n  test \"symlinks node_modules folder if exists\" do\n    node_path = Path.expand(\"../../assets/node_modules\", __DIR__)\n\n    if not File.exists?(node_path) do\n      on_exit(fn -> File.rm_rf!(node_path) end)\n    end\n\n    File.mkdir_p!(Path.join(node_path, \"foo\"))\n    Phoenix.LiveView.ColocatedJS.compile()\n\n    symlink =\n      Path.join(\n        Mix.Project.build_path(),\n        \"phoenix-colocated/phoenix_live_view/node_modules\"\n      )\n\n    assert File.exists?(symlink)\n    link = File.read_link!(symlink)\n\n    if function_exported?(Path, :relative_to, 3) do\n      assert String.starts_with?(link, \"../\")\n    end\n\n    assert \"foo\" in File.ls!(symlink)\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/controller_test.exs",
    "content": "defmodule Phoenix.LiveView.ControllerTest do\n  use ExUnit.Case, async: true\n  import Phoenix.ConnTest\n\n  alias Phoenix.LiveViewTest.Support.Endpoint\n\n  @endpoint Endpoint\n\n  setup do\n    {:ok, conn: Phoenix.ConnTest.build_conn()}\n  end\n\n  test \"live renders from controller without session\", %{conn: conn} do\n    conn = get(conn, \"/controller/live-render-2\")\n    assert html_response(conn, 200) =~ \"session: %{}\"\n  end\n\n  test \"live renders from controller with session\", %{conn: conn} do\n    conn = get(conn, \"/controller/live-render-3\")\n    assert html_response(conn, 200) =~ \"session: %{\\\"custom\\\" => :session}\"\n  end\n\n  test \"live renders from controller with merged assigns\", %{conn: conn} do\n    conn = get(conn, \"/controller/live-render-4\")\n    assert html_response(conn, 200) =~ \"title: Dashboard\"\n  end\n\n  test \"renders function components from dead view\", %{conn: conn} do\n    conn = get(conn, \"/controller/render-with-function-component\")\n    assert html_response(conn, 200) =~ \"RENDER:COMPONENT:from component\"\n  end\n\n  test \"renders function components from dead layout\", %{conn: conn} do\n    conn = get(conn, \"/controller/render-layout-with-function-component\")\n\n    assert html_response(conn, 200) =~ \"\"\"\n           LAYOUT:COMPONENT:from layout\n           Hello\\\n           \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/debug_test.exs",
    "content": "defmodule Phoenix.LiveView.DebugTest do\n  use ExUnit.Case, async: true\n\n  alias Phoenix.LiveView.Debug\n  import Phoenix.LiveViewTest\n\n  @endpoint Phoenix.LiveViewTest.Support.Endpoint\n\n  defmodule TestLV do\n    use Phoenix.LiveView\n\n    defmodule Component do\n      use Phoenix.LiveComponent\n\n      def render(assigns) do\n        ~H\"\"\"\n        <p>Hello</p>\n        \"\"\"\n      end\n    end\n\n    def mount(_params, _session, socket) do\n      {:ok, assign(socket, :hello, :world)}\n    end\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div>\n        <p>Hello</p>\n        <.live_component id=\"component-1\" module={Component} />\n      </div>\n      \"\"\"\n    end\n  end\n\n  defmodule NotALiveView do\n    use GenServer\n\n    def start_link(opts) do\n      GenServer.start_link(__MODULE__, opts)\n    end\n\n    def init(opts) do\n      {:ok, opts}\n    end\n  end\n\n  describe \"list_liveviews/0\" do\n    test \"returns a list of all currently connected LiveView processes\" do\n      conn = Plug.Test.conn(:get, \"/\")\n      {:ok, view, _} = live_isolated(conn, TestLV)\n      live_views = Debug.list_liveviews()\n\n      assert is_list(live_views)\n      assert lv = Enum.find(live_views, fn lv -> lv.pid == view.pid end)\n      assert lv.view == TestLV\n      assert lv.transport_pid\n      assert lv.topic\n    end\n  end\n\n  describe \"liveview_process?/1\" do\n    test \"returns true if the given pid is a LiveView process\" do\n      conn = Plug.Test.conn(:get, \"/\")\n      {:ok, view, _} = live_isolated(conn, TestLV)\n      assert Debug.liveview_process?(view.pid)\n    end\n  end\n\n  describe \"socket/1\" do\n    test \"returns the socket of the given LiveView process\" do\n      conn = Plug.Test.conn(:get, \"/\")\n      {:ok, view, _} = live_isolated(conn, TestLV)\n      assert {:ok, socket} = Debug.socket(view.pid)\n      assert socket.assigns.hello == :world\n    end\n\n    test \"returns an error if the given pid is not a LiveView process\" do\n      pid = start_supervised!(NotALiveView)\n      assert {:error, :not_alive_or_not_a_liveview} = Debug.socket(pid)\n    end\n  end\n\n  describe \"live_components/1\" do\n    test \"returns a list of all LiveComponents rendered in the given LiveView\" do\n      conn = Plug.Test.conn(:get, \"/\")\n      {:ok, view, _} = live_isolated(conn, TestLV)\n\n      assert {:ok, [%{id: \"component-1\", module: TestLV.Component}]} =\n               Debug.live_components(view.pid)\n    end\n\n    test \"returns an error if the given pid is not a LiveView process\" do\n      pid = start_supervised!(NotALiveView)\n      assert {:error, :not_alive_or_not_a_liveview} = Debug.live_components(pid)\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/diff_test.exs",
    "content": "defmodule Phoenix.LiveView.DiffTest do\n  use ExUnit.Case, async: true\n\n  import Phoenix.Component\n\n  alias Phoenix.LiveView.{Socket, Diff, Rendered, Component}\n  alias Phoenix.LiveComponent.CID\n\n  def basic_template(assigns) do\n    ~H\"\"\"\n    <div>\n      <h2>It's {@time}</h2>\n      {@subtitle}\n    </div>\n    \"\"\"\n  end\n\n  def literal_template(assigns) do\n    ~H\"\"\"\n    <div>\n      {@title}\n      {\"<div>\"}\n    </div>\n    \"\"\"\n  end\n\n  def comprehension_template(assigns) do\n    ~H\"\"\"\n    <div>\n      <h1>{@title}</h1>\n      <%= for name <- @names do %>\n        <br />{name}\n      <% end %>\n    </div>\n    \"\"\"\n  end\n\n  def nested_comprehension_template(assigns) do\n    ~H\"\"\"\n    <div>\n      <h1>{@title}</h1>\n      <%= for name <- @names do %>\n        <br />{name}\n        <%= for score <- @scores do %>\n          <br />{score}\n        <% end %>\n      <% end %>\n    </div>\n    \"\"\"\n  end\n\n  defp nested_rendered(changed? \\\\ true) do\n    %Rendered{\n      static: [\"<h2>\", \"</h2>\", \"<span>\", \"</span>\"],\n      dynamic: fn _ ->\n        [\n          \"hi\",\n          %Rendered{\n            static: [\"s1\", \"s2\", \"s3\"],\n            dynamic: fn _ -> if changed?, do: [\"abc\", \"efg\"], else: [nil, nil] end,\n            fingerprint: 456\n          },\n          %Rendered{\n            static: [\"s1\", \"s2\"],\n            dynamic: fn _ -> if changed?, do: [\"efg\"], else: [nil] end,\n            fingerprint: 789\n          }\n        ]\n      end,\n      fingerprint: 123\n    }\n  end\n\n  defp render(\n         rendered,\n         fingerprints \\\\ Diff.new_fingerprints(),\n         components \\\\ Diff.new_components()\n       ) do\n    Diff.render(%Socket{endpoint: __MODULE__}, rendered, fingerprints, components)\n  end\n\n  defp rendered_to_binary(map) do\n    map |> Diff.to_iodata() |> IO.iodata_to_binary()\n  end\n\n  describe \"to_iodata\" do\n    test \"with subtrees chain\" do\n      assert rendered_to_binary(%{\n               0 => %{\n                 k: %{\n                   0 => %{0 => \"1\", 1 => 1},\n                   1 => %{0 => \"2\", 1 => 2},\n                   2 => %{0 => \"3\", 1 => 3},\n                   kc: 3\n                 },\n                 s: [\"\\n\", \":\", \"\"]\n               },\n               :c => %{\n                 1 => %{0 => %{0 => \"index_1\", :s => [\"\\nIF \", \"\"]}, :s => [\"\", \"\"]},\n                 2 => %{0 => %{0 => \"index_2\", :s => [\"\\nELSE \", \"\"]}, :s => 1},\n                 3 => %{0 => %{0 => \"index_3\"}, :s => 2}\n               },\n               :s => [\"<div>\", \"\\n</div>\"]\n             }) == \"\"\"\n             <div>\n             1:\n             IF index_1\n             2:\n             ELSE index_2\n             3:\n             ELSE index_3\n             </div>\\\n             \"\"\"\n    end\n\n    test \"with subtrees where a comprehension is replaced by rendered\" do\n      assert rendered_to_binary(%{\n               0 => 1,\n               1 => 2,\n               :c => %{\n                 1 => %{\n                   0 => %{\n                     0 => %{k: %{0 => %{}, 1 => %{}, 2 => %{}, :kc => 3}, s: [\"ROW\"]},\n                     :s => [\"\\n\", \"\"]\n                   },\n                   :s => [\"<div>\", \"</div>\"]\n                 },\n                 2 => %{\n                   0 => %{\n                     0 => %{0 => \"BAR\", :s => [\"FOO\", \"BAZ\"]},\n                     :s => [\"\\n\", \"\"]\n                   },\n                   :s => 1\n                 }\n               },\n               :s => [\"\", \"\", \"\"]\n             }) == \"<div>\\nROWROWROW</div><div>\\nFOOBARBAZ</div>\"\n    end\n  end\n\n  describe \"full renders without fingerprints\" do\n    test \"basic template\" do\n      rendered = basic_template(%{time: \"10:30\", subtitle: \"Sunny\"})\n      {full_render, fingerprints, _} = render(rendered)\n\n      assert full_render == %{\n               0 => \"10:30\",\n               1 => \"Sunny\",\n               :r => 1,\n               :s => 0,\n               :p => %{0 => [\"<div>\\n  <h2>It's \", \"</h2>\\n  \", \"\\n</div>\"]}\n             }\n\n      assert rendered_to_binary(full_render) ==\n               \"<div>\\n  <h2>It's 10:30</h2>\\n  Sunny\\n</div>\"\n\n      assert fingerprints == {rendered.fingerprint, %{}}\n    end\n\n    test \"template with literal\" do\n      rendered = literal_template(%{title: \"foo\"})\n      {full_render, fingerprints, _} = render(rendered)\n\n      assert full_render ==\n               %{\n                 0 => \"foo\",\n                 1 => \"&lt;div&gt;\",\n                 :r => 1,\n                 :s => 0,\n                 :p => %{0 => [\"<div>\\n  \", \"\\n  \", \"\\n</div>\"]}\n               }\n\n      assert rendered_to_binary(full_render) ==\n               \"<div>\\n  foo\\n  &lt;div&gt;\\n</div>\"\n\n      assert fingerprints == {rendered.fingerprint, %{}}\n    end\n\n    test \"nested %Rendered{}'s\" do\n      {full_render, fingerprints, _} = render(nested_rendered())\n\n      assert full_render ==\n               %{\n                 0 => \"hi\",\n                 1 => %{0 => \"abc\", 1 => \"efg\", :s => 0},\n                 2 => %{0 => \"efg\", :s => 1},\n                 :s => 2,\n                 :p => %{\n                   0 => [\"s1\", \"s2\", \"s3\"],\n                   1 => [\"s1\", \"s2\"],\n                   2 => [\"<h2>\", \"</h2>\", \"<span>\", \"</span>\"]\n                 }\n               }\n\n      assert rendered_to_binary(full_render) ==\n               \"<h2>hi</h2>s1abcs2efgs3<span>s1efgs2</span>\"\n\n      assert fingerprints == {123, %{2 => {789, %{}}, 1 => {456, %{}}}}\n    end\n\n    test \"comprehensions\" do\n      %{fingerprint: fingerprint} =\n        rendered = comprehension_template(%{title: \"Users\", names: [\"phoenix\", \"elixir\"]})\n\n      {full_render, fingerprints, _} = render(rendered)\n\n      assert full_render == %{\n               0 => \"Users\",\n               1 => %{s: 0, k: %{0 => %{0 => \"phoenix\"}, 1 => %{0 => \"elixir\"}, :kc => 2}},\n               :r => 1,\n               :s => 1,\n               :p => %{\n                 0 => [\"\\n    <br>\", \"\\n  \"],\n                 1 => [\"<div>\\n  <h1>\", \"</h1>\\n  \", \"\\n</div>\"]\n               }\n             }\n\n      assert {^fingerprint, %{1 => comprehension_print}} = fingerprints\n\n      assert {_,\n              %{\n                0 => %{index: 0, vars: %{name: \"phoenix\"}, child_prints: print},\n                1 => %{index: 1, vars: %{name: \"elixir\"}, child_prints: print}\n              }} = comprehension_print\n    end\n\n    test \"empty comprehensions\" do\n      # If they are empty on first render, we don't send them\n      %{fingerprint: fingerprint} =\n        rendered = comprehension_template(%{title: \"Users\", names: []})\n\n      {full_render, fingerprints, components} = render(rendered)\n\n      assert full_render == %{\n               0 => \"Users\",\n               1 => \"\",\n               :r => 1,\n               :s => 0,\n               :p => %{0 => [\"<div>\\n  <h1>\", \"</h1>\\n  \", \"\\n</div>\"]}\n             }\n\n      assert {^fingerprint, inner} = fingerprints\n      assert inner == %{}\n\n      # Making them non-empty adds a fingerprint\n      rendered = comprehension_template(%{title: \"Users\", names: [\"phoenix\", \"elixir\"]})\n      {full_render, fingerprints, components} = render(rendered, fingerprints, components)\n\n      assert full_render == %{\n               0 => \"Users\",\n               1 => %{s: 0, k: %{0 => %{0 => \"phoenix\"}, 1 => %{0 => \"elixir\"}, :kc => 2}},\n               :p => %{0 => [\"\\n    <br>\", \"\\n  \"]}\n             }\n\n      assert {^fingerprint, %{1 => comprehension_print}} = fingerprints\n\n      # Making them empty again changes the fingerprint\n      rendered = comprehension_template(%{title: \"Users\", names: []})\n      {full_render, fingerprints, _components} = render(rendered, fingerprints, components)\n\n      assert full_render == %{0 => \"Users\", 1 => %{k: %{kc: 0}}}\n\n      assert {^fingerprint, %{1 => new_comprehension_print}} = fingerprints\n      refute comprehension_print == new_comprehension_print\n    end\n\n    test \"nested comprehensions\" do\n      %{fingerprint: fingerprint} =\n        rendered =\n        nested_comprehension_template(%{\n          title: \"Users\",\n          names: [\"phoenix\", \"elixir\"],\n          scores: [1, 2]\n        })\n\n      {full_render, fingerprints, _} = render(rendered)\n\n      assert full_render == %{\n               0 => \"Users\",\n               1 => %{\n                 k: %{\n                   0 => %{\n                     0 => \"phoenix\",\n                     1 => %{k: %{0 => %{0 => \"1\"}, 1 => %{0 => \"2\"}, :kc => 2}, s: 0}\n                   },\n                   1 => %{\n                     0 => \"elixir\",\n                     1 => %{k: %{0 => %{0 => \"1\"}, 1 => %{0 => \"2\"}, :kc => 2}, s: 0}\n                   },\n                   :kc => 2\n                 },\n                 s: 1\n               },\n               :p => %{\n                 0 => [\"\\n      <br>\", \"\\n    \"],\n                 1 => [\"\\n    <br>\", \"\\n    \", \"\\n  \"],\n                 2 => [\"<div>\\n  <h1>\", \"</h1>\\n  \", \"\\n</div>\"]\n               },\n               :r => 1,\n               :s => 2\n             }\n\n      assert {^fingerprint, %{1 => comprehension_print}} = fingerprints\n\n      assert {_,\n              %{\n                0 => %{index: 0, vars: %{name: \"phoenix\"}, child_prints: print},\n                1 => %{index: 1, vars: %{name: \"elixir\"}, child_prints: print}\n              }} = comprehension_print\n    end\n  end\n\n  describe \"diffed render with fingerprints\" do\n    test \"basic template skips statics for known fingerprints\" do\n      rendered = basic_template(%{time: \"10:30\", subtitle: \"Sunny\"})\n      {full_render, fingerprints, _} = render(rendered, {rendered.fingerprint, %{}})\n\n      assert full_render == %{0 => \"10:30\", 1 => \"Sunny\"}\n      assert fingerprints == {rendered.fingerprint, %{}}\n    end\n\n    test \"renders nested %Rendered{}'s\" do\n      tree = {123, %{2 => {789, %{}}, 1 => {456, %{}}}}\n      {diffed_render, fingerprints, _} = render(nested_rendered(), tree)\n\n      assert diffed_render == %{0 => \"hi\", 1 => %{0 => \"abc\", 1 => \"efg\"}, 2 => %{0 => \"efg\"}}\n      assert fingerprints == tree\n    end\n\n    test \"does not emit nested %Rendered{}'s if they did not change\" do\n      tree = {123, %{2 => {789, %{}}, 1 => {456, %{}}}}\n      {diffed_render, fingerprints, _} = render(nested_rendered(false), tree)\n\n      assert diffed_render == %{0 => \"hi\"}\n      assert fingerprints == tree\n    end\n\n    test \"detects change in nested fingerprint\" do\n      old_tree = {123, %{2 => {789, %{}}, 1 => {100_001, %{}}}}\n      {diffed_render, fingerprints, _} = render(nested_rendered(), old_tree)\n\n      assert diffed_render ==\n               %{\n                 0 => \"hi\",\n                 1 => %{0 => \"abc\", 1 => \"efg\", :s => 0},\n                 2 => %{0 => \"efg\"},\n                 :p => %{0 => [\"s1\", \"s2\", \"s3\"]}\n               }\n\n      assert fingerprints == {123, %{2 => {789, %{}}, 1 => {456, %{}}}}\n    end\n\n    test \"detects change in root fingerprint\" do\n      old_tree = {99999, %{}}\n      {diffed_render, fingerprints, _} = render(nested_rendered(), old_tree)\n\n      assert diffed_render == %{\n               0 => \"hi\",\n               1 => %{0 => \"abc\", 1 => \"efg\", :s => 0},\n               2 => %{0 => \"efg\", :s => 1},\n               :s => 2,\n               :p => %{\n                 0 => [\"s1\", \"s2\", \"s3\"],\n                 1 => [\"s1\", \"s2\"],\n                 2 => [\"<h2>\", \"</h2>\", \"<span>\", \"</span>\"]\n               }\n             }\n\n      assert fingerprints == {123, %{2 => {789, %{}}, 1 => {456, %{}}}}\n    end\n  end\n\n  defmodule MyComponent do\n    use Phoenix.LiveComponent\n\n    def mount(socket) do\n      send(self(), {:mount, socket})\n      {:ok, assign(socket, hello: \"world\")}\n    end\n\n    def update(assigns, socket) do\n      send(self(), {:update, assigns, socket})\n      {:ok, assign(socket, assigns)}\n    end\n\n    def render(assigns) do\n      send(self(), :render)\n\n      ~H\"\"\"\n      <div>FROM {@from} {@hello}</div>\n      \"\"\"\n    end\n  end\n\n  defmodule IfComponent do\n    use Phoenix.LiveComponent\n\n    def mount(socket) do\n      {:ok, assign(socket, if: true)}\n    end\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div>\n        <%= if @if do %>\n          IF {@from}\n        <% else %>\n          ELSE {@from}\n        <% end %>\n      </div>\n      \"\"\"\n    end\n  end\n\n  defmodule RecurComponent do\n    use Phoenix.LiveComponent\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div>\n        ID: {@id}\n        <%= for {id, children} <- @children do %>\n          <.live_component module={__MODULE__} id={id} children={children} />\n        <% end %>\n      </div>\n      \"\"\"\n    end\n  end\n\n  defmodule TempComponent do\n    use Phoenix.LiveComponent\n\n    def mount(socket) do\n      send(self(), {:temporary_mount, socket})\n      {:ok, assign(socket, :first_time, true), temporary_assigns: [first_time: false]}\n    end\n\n    def render(assigns) do\n      send(self(), {:temporary_render, assigns})\n\n      ~H\"\"\"\n      <div>FROM {if @first_time, do: \"WELCOME!\", else: @from}</div>\n      \"\"\"\n    end\n  end\n\n  defmodule RenderOnlyComponent do\n    use Phoenix.LiveComponent\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div>RENDER ONLY {@from}</div>\n      \"\"\"\n    end\n  end\n\n  defmodule SlotComponent do\n    use Phoenix.LiveComponent\n\n    def mount(socket) do\n      {:ok, assign(socket, id: \"DEFAULT\")}\n    end\n\n    def render(%{do: _}), do: raise(\"unexpected :do assign\")\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div>\n        HELLO {@id} {render_slot(@inner_block, %{value: 1})} HELLO {@id} {render_slot(\n          @inner_block,\n          %{value: 2}\n        )}\n      </div>\n      \"\"\"\n    end\n  end\n\n  defmodule FunctionComponent do\n    def render_only(assigns) do\n      ~H\"\"\"\n      <div>RENDER ONLY {@from}</div>\n      \"\"\"\n    end\n\n    def render_inner_block_no_args(assigns) do\n      ~H\"\"\"\n      <div>\n        HELLO {@id} {render_slot(@inner_block)} HELLO {@id} {render_slot(@inner_block)}\n      </div>\n      \"\"\"\n    end\n\n    def render_with_slot_no_args(assigns) do\n      ~H\"\"\"\n      <div>\n        HELLO {@id} {render_slot(@sample)} HELLO {@id} {render_slot(@sample)}\n      </div>\n      \"\"\"\n    end\n\n    def render_inner_block(assigns) do\n      ~H\"\"\"\n      <div>\n        HELLO {@id} {render_slot(@inner_block, 1)} HELLO {@id} {render_slot(\n          @inner_block,\n          2\n        )}\n      </div>\n      \"\"\"\n    end\n\n    def render_with_live_component(assigns) do\n      ~H\"\"\"\n      COMPONENT\n      <.live_component :let={%{value: value}} module={SlotComponent} id=\"WORLD\">\n        WITH VALUE {value}\n      </.live_component>\n      \"\"\"\n    end\n  end\n\n  defmodule TreeComponent do\n    use Phoenix.LiveComponent\n\n    @impl true\n    def update_many(assigns_sockets) do\n      send(self(), {:update_many, assigns_sockets})\n\n      Enum.map(assigns_sockets, fn {assigns, socket} ->\n        socket |> assign(assigns) |> assign(:update_many_ran?, true)\n      end)\n    end\n\n    @impl true\n    def update(_assigns, _socket) do\n      raise \"this won't be invoked\"\n    end\n\n    @impl true\n    def render(assigns) do\n      ~H\"\"\"\n      <div>\n        {@id} - {@update_many_ran?}\n        <%= for {component, index} <- Enum.with_index(@children, 0) do %>\n          {index}: {component}\n        <% end %>\n      </div>\n      \"\"\"\n    end\n  end\n\n  defmodule NestedDynamicComponent do\n    use Phoenix.LiveComponent\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div>\n        {render_itself(assigns)}\n      </div>\n      \"\"\"\n    end\n\n    def render_itself(assigns) do\n      case assigns.key do\n        :a ->\n          ~H\"\"\"\n          <%= for key <- [:nothing] do %>\n            {key}{key}\n          <% end %>\n          \"\"\"\n\n        :b ->\n          ~H\"\"\"\n          {}\n          \"\"\"\n\n        :c ->\n          ~H\"\"\"\n          <.live_component module={__MODULE__} id={make_ref()} key={:a} />\n          \"\"\"\n      end\n    end\n  end\n\n  def component_template(assigns) do\n    ~H\"\"\"\n    <div>\n      {@component}\n    </div>\n    \"\"\"\n  end\n\n  def another_component_template(assigns) do\n    ~H\"\"\"\n    <span>\n      {@component}\n    </span>\n    \"\"\"\n  end\n\n  describe \"function components\" do\n    test \"render only\" do\n      assigns = %{socket: %Socket{}}\n\n      rendered = ~H\"\"\"\n      <FunctionComponent.render_only from={:component} />\n      \"\"\"\n\n      {full_render, fingerprints, components} = render(rendered)\n\n      assert full_render == %{\n               0 => %{0 => \"component\", :r => 1, :s => 0},\n               :s => 1,\n               :p => %{0 => [\"<div>RENDER ONLY \", \"</div>\"], 1 => [\"\", \"\"]}\n             }\n\n      assert fingerprints != {rendered.fingerprint, %{}}\n      assert components == Diff.new_components()\n    end\n\n    @raises_inside_rendered_line __ENV__.line + 3\n    defp raises_inside_rendered(assigns) do\n      ~H\"\"\"\n      {Process.get(:unused, false) || raise \"oops\"}\n      \"\"\"\n    end\n\n    test \"stacktrace\" do\n      assigns = %{socket: %Socket{}}\n      line = __ENV__.line + 3\n\n      rendered = ~H\"\"\"\n      <.raises_inside_rendered />\n      \"\"\"\n\n      try do\n        render(rendered)\n      rescue\n        RuntimeError ->\n          assert [{__MODULE__, _anonymous_fun, _anonymous_arity, info} | rest] = __STACKTRACE__\n          assert List.to_string(info[:file]) =~ \"diff_test.exs\"\n          assert info[:line] == @raises_inside_rendered_line\n\n          assert Enum.any?(rest, fn {mod, fun, arity, info} ->\n                   mod == __MODULE__ and __ENV__.function == {fun, arity} and\n                     List.to_string(info[:file]) =~ \"diff_test.exs\" and info[:line] == line\n                 end)\n      else\n        _ -> flunk(\"should have raised runtime error\")\n      end\n    end\n\n    test \"@inner_block without args\" do\n      assigns = %{socket: %Socket{}}\n\n      rendered = ~H\"\"\"\n      <FunctionComponent.render_inner_block_no_args id=\"DEFAULT\">\n        INSIDE BLOCK\n      </FunctionComponent.render_inner_block_no_args>\n      \"\"\"\n\n      {full_render, fingerprints, components} = render(rendered)\n\n      assert full_render == %{\n               0 => %{\n                 0 => \"DEFAULT\",\n                 1 => %{s: 0},\n                 2 => \"DEFAULT\",\n                 3 => %{s: 0},\n                 :r => 1,\n                 :s => 1\n               },\n               :s => 2,\n               :p => %{\n                 0 => [\"\\n  INSIDE BLOCK\\n\"],\n                 1 => [\"<div>\\n  HELLO \", \" \", \" HELLO \", \" \", \"\\n</div>\"],\n                 2 => [\"\", \"\"]\n               }\n             }\n\n      {full_render, _fingerprints, _components} =\n        render(rendered, fingerprints, components)\n\n      assert full_render == %{0 => %{0 => \"DEFAULT\", 2 => \"DEFAULT\"}}\n    end\n\n    test \"slot tracking without args\" do\n      assigns = %{socket: %Socket{}}\n\n      rendered = ~H\"\"\"\n      <FunctionComponent.render_with_slot_no_args id=\"MY ID\">\n        <:sample>\n          INSIDE SLOT\n        </:sample>\n      </FunctionComponent.render_with_slot_no_args>\n      \"\"\"\n\n      {full_render, fingerprints, components} = render(rendered)\n\n      assert full_render == %{\n               0 => %{0 => \"MY ID\", 1 => %{s: 0}, 2 => \"MY ID\", 3 => %{s: 0}, :r => 1, :s => 1},\n               :s => 2,\n               :p => %{\n                 0 => [\"\\n    INSIDE SLOT\\n  \"],\n                 1 => [\"<div>\\n  HELLO \", \" \", \" HELLO \", \" \", \"\\n</div>\"],\n                 2 => [\"\", \"\"]\n               }\n             }\n\n      {full_render, _fingerprints, _components} =\n        render(rendered, fingerprints, components)\n\n      assert full_render == %{0 => %{0 => \"MY ID\", 2 => \"MY ID\"}}\n    end\n\n    defp function_tracking(assigns) do\n      ~H\"\"\"\n      <FunctionComponent.render_inner_block :let={value} id={@id}>\n        WITH VALUE {value} - {@value}\n      </FunctionComponent.render_inner_block>\n      \"\"\"\n    end\n\n    test \"@inner_block with args and parent assign\" do\n      assigns = %{socket: %Socket{}, value: 123, id: \"DEFAULT\"}\n\n      {full_render, fingerprints, components} = render(function_tracking(assigns))\n\n      assert full_render == %{\n               0 => %{\n                 0 => \"DEFAULT\",\n                 1 => %{0 => \"1\", 1 => \"123\", :s => 0},\n                 2 => \"DEFAULT\",\n                 3 => %{0 => \"2\", 1 => \"123\", :s => 0},\n                 :r => 1,\n                 :s => 1\n               },\n               :s => 2,\n               :p => %{\n                 0 => [\"\\n  WITH VALUE \", \" - \", \"\\n\"],\n                 1 => [\"<div>\\n  HELLO \", \" \", \" HELLO \", \" \", \"\\n</div>\"],\n                 2 => [\"\", \"\"]\n               }\n             }\n\n      {full_render, _fingerprints, _components} =\n        render(function_tracking(assigns), fingerprints, components)\n\n      assert full_render == %{\n               0 => %{\n                 0 => \"DEFAULT\",\n                 1 => %{0 => \"1\", 1 => \"123\"},\n                 2 => \"DEFAULT\",\n                 3 => %{0 => \"2\", 1 => \"123\"}\n               }\n             }\n\n      assigns = Map.put(assigns, :__changed__, %{})\n\n      {full_render, _fingerprints, _components} =\n        render(function_tracking(assigns), fingerprints, components)\n\n      assert full_render == %{}\n\n      assigns = Map.put(assigns, :__changed__, %{id: true})\n\n      {full_render, _fingerprints, _components} =\n        render(function_tracking(assigns), fingerprints, components)\n\n      assert full_render == %{\n               0 => %{\n                 0 => \"DEFAULT\",\n                 2 => \"DEFAULT\"\n               }\n             }\n\n      assigns = Map.put(assigns, :__changed__, %{value: true})\n\n      {full_render, _fingerprints, _components} =\n        render(function_tracking(assigns), fingerprints, components)\n\n      assert full_render == %{\n               0 => %{\n                 1 => %{0 => \"1\", 1 => \"123\"},\n                 3 => %{0 => \"2\", 1 => \"123\"}\n               }\n             }\n    end\n\n    defp fake_form(assigns) do\n      ~H\"\"\"\n      {render_slot(@inner_block, @form)}\n      \"\"\"\n    end\n\n    defp access_let(assigns) do\n      ~H\"\"\"\n      <.fake_form :let={f} form={@form}>\n        {f[:name].value}\n      </.fake_form>\n      \"\"\"\n    end\n\n    test \":let + access\" do\n      assigns = %{socket: %Socket{}, form: %{name: %{value: \"foo\"}}}\n\n      {full_render, fingerprints, components} = render(access_let(assigns))\n\n      assert full_render == %{\n               0 => %{0 => %{0 => \"foo\", :s => 0}, :s => 1},\n               :s => 2,\n               :p => %{0 => [\"\\n  \", \"\\n\"], 1 => [\"\", \"\"], 2 => [\"\", \"\"]}\n             }\n\n      {full_render, _fingerprints, _components} =\n        render(access_let(assigns), fingerprints, components)\n\n      assert full_render == %{0 => %{0 => %{0 => \"foo\"}}}\n\n      assigns = Map.put(assigns, :__changed__, %{})\n\n      {full_render, _fingerprints, _components} =\n        render(access_let(assigns), fingerprints, components)\n\n      assert full_render == %{}\n\n      assigns = Map.put(assigns, :__changed__, %{form: true})\n\n      {full_render, _fingerprints, _components} =\n        render(access_let(assigns), fingerprints, components)\n\n      assert full_render == %{0 => %{0 => %{0 => \"foo\"}}}\n    end\n\n    def render_multiple_slots(assigns) do\n      ~H\"\"\"\n      <div>\n        HEADER: {render_slot(@header)} FOOTER: {render_slot(@footer)}\n      </div>\n      \"\"\"\n    end\n\n    def slot_tracking(assigns) do\n      ~H\"\"\"\n      <.render_multiple_slots>\n        <:header>\n          {@in_header}{@in_both}\n        </:header>\n        <:footer>\n          {@in_footer}{@in_both}\n        </:footer>\n      </.render_multiple_slots>\n      \"\"\"\n    end\n\n    test \"slot tracking with multiple slots\" do\n      assigns = %{socket: %Socket{}, in_header: \"H\", in_footer: \"F\", in_both: \"B\"}\n\n      {full_render, fingerprints, components} = render(slot_tracking(assigns))\n\n      assert full_render == %{\n               0 => %{\n                 0 => %{0 => \"H\", 1 => \"B\", :s => 0},\n                 1 => %{0 => \"F\", 1 => \"B\", :s => 1},\n                 :r => 1,\n                 :s => 2\n               },\n               :s => 3,\n               :p => %{\n                 0 => [\"\\n    \", \"\", \"\\n  \"],\n                 1 => [\"\\n    \", \"\", \"\\n  \"],\n                 2 => [\"<div>\\n  HEADER: \", \" FOOTER: \", \"\\n</div>\"],\n                 3 => [\"\", \"\"]\n               }\n             }\n\n      {full_render, _fingerprints, _components} =\n        render(slot_tracking(assigns), fingerprints, components)\n\n      assert full_render == %{\n               0 => %{\n                 0 => %{0 => \"H\", 1 => \"B\"},\n                 1 => %{0 => \"F\", 1 => \"B\"}\n               }\n             }\n\n      assigns = Map.put(assigns, :__changed__, %{})\n\n      {full_render, _fingerprints, _components} =\n        render(slot_tracking(assigns), fingerprints, components)\n\n      assert full_render == %{}\n\n      assigns = Map.put(assigns, :__changed__, %{in_header: true})\n\n      {full_render, _fingerprints, _components} =\n        render(slot_tracking(assigns), fingerprints, components)\n\n      assert full_render == %{0 => %{0 => %{0 => \"H\"}}}\n\n      assigns = Map.put(assigns, :__changed__, %{in_footer: true})\n\n      {full_render, _fingerprints, _components} =\n        render(slot_tracking(assigns), fingerprints, components)\n\n      assert full_render == %{0 => %{1 => %{0 => \"F\"}}}\n\n      assigns = Map.put(assigns, :__changed__, %{in_both: true})\n\n      {full_render, _fingerprints, _components} =\n        render(slot_tracking(assigns), fingerprints, components)\n\n      assert full_render == %{\n               0 => %{\n                 1 => %{1 => \"B\"},\n                 0 => %{1 => \"B\"}\n               }\n             }\n    end\n\n    def conditional_slot_tracking(assigns) do\n      ~H\"\"\"\n      <.render_multiple_slots>\n        <:header :if={@if}>\n          <.live_component module={MyComponent} id=\"header\" from={:component} />\n        </:header>\n        <:footer :if={@if}>\n          <.live_component module={MyComponent} id=\"footer\" from={:component} />\n        </:footer>\n      </.render_multiple_slots>\n      \"\"\"\n    end\n\n    test \"slot tracking with live component inside conditional slot\" do\n      assigns = %{socket: %Socket{}, if: true}\n      {_socket, _full_render, components} = render(conditional_slot_tracking(assigns))\n      assert {_, _, 3} = components\n\n      assigns = %{socket: %Socket{}, if: false}\n      {_socket, _full_render, components} = render(conditional_slot_tracking(assigns))\n      assert {_, _, 1} = components\n    end\n\n    test \"with live_component\" do\n      assigns = %{socket: %Socket{}}\n\n      rendered = ~H\"\"\"\n      <FunctionComponent.render_with_live_component />\n      \"\"\"\n\n      {full_render, fingerprints, components} = render(rendered)\n\n      assert full_render == %{\n               0 => %{0 => 1, :s => 0},\n               :c => %{\n                 1 => %{\n                   0 => \"WORLD\",\n                   1 => %{0 => \"1\", :s => [\"\\n  WITH VALUE \", \"\\n\"]},\n                   2 => \"WORLD\",\n                   3 => %{0 => \"2\", :s => [\"\\n  WITH VALUE \", \"\\n\"]},\n                   :r => 1,\n                   :s => [\"<div>\\n  HELLO \", \" \", \" HELLO \", \" \", \"\\n</div>\"]\n                 }\n               },\n               :s => 1,\n               :p => %{0 => [\"COMPONENT\\n\", \"\"], 1 => [\"\", \"\"]}\n             }\n\n      {full_render, _fingerprints, _components} =\n        render(rendered, fingerprints, components)\n\n      assert full_render == %{0 => %{0 => 1}}\n    end\n\n    def li(assigns) do\n      ~H\"\"\"\n      <li>{render_slot(@inner_block)}</li>\n      \"\"\"\n    end\n\n    defp comprehension_with_and_without_component(assigns) do\n      ~H\"\"\"\n      <div>\n        <ul>\n          <li :for={item <- @items}>\n            <%!--\n              we deliberately access two fields here, because we had a bug that caused warnings\n              https://github.com/phoenixframework/phoenix_live_view/issues/3912\n            --%>\n            {item.name} {item.price}\n          </li>\n        </ul>\n\n        <ul>\n          <.li :for={item <- @items}>\n            {item.name} {item.price}\n          </.li>\n        </ul>\n      </div>\n      \"\"\"\n    end\n\n    test \"vars_changed\" do\n      # This came up in issue https://github.com/phoenixframework/phoenix_live_view/issues/3906\n      assigns = %{\n        socket: %Socket{},\n        items: [%{price: 0, name: \"First\"}],\n        item: %{name: \"First\", price: 0}\n      }\n\n      {full_render, fingerprints, components} =\n        render(comprehension_with_and_without_component(assigns))\n\n      assert full_render == %{\n               0 => %{k: %{0 => %{0 => \"First\", 1 => \"0\"}, :kc => 1}, s: 0},\n               1 => %{\n                 k: %{\n                   0 => %{0 => %{0 => %{0 => \"First\", 1 => \"0\", :s => 1}, :r => 1, :s => 2}},\n                   :kc => 1\n                 },\n                 s: 3\n               },\n               :p => %{\n                 0 => [\"<li>\\n      \\n      \", \" \", \"\\n    </li>\"],\n                 1 => [\"\\n      \", \" \", \"\\n    \"],\n                 2 => [\"<li>\", \"</li>\"],\n                 3 => [\"\", \"\"],\n                 4 => [\"<div>\\n  <ul>\\n    \", \"\\n  </ul>\\n\\n  <ul>\\n    \", \"\\n  </ul>\\n</div>\"]\n               },\n               :r => 1,\n               :s => 4\n             }\n\n      assigns =\n        assigns\n        |> Map.put(:items, [%{name: \"First\", price: 1}])\n        |> Map.put(:__changed__, %{items: true})\n\n      {full_render, _fingerprints, _components} =\n        render(comprehension_with_and_without_component(assigns), fingerprints, components)\n\n      assert full_render == %{\n               0 => %{k: %{0 => %{1 => \"1\"}, :kc => 1}},\n               1 => %{k: %{0 => %{0 => %{0 => %{1 => \"1\"}}}, :kc => 1}}\n             }\n    end\n  end\n\n  describe \"live components\" do\n    test \"on mount\" do\n      component = %Component{id: \"hello\", assigns: %{from: :component}, component: MyComponent}\n      rendered = component_template(%{component: component})\n      {full_render, fingerprints, components} = render(rendered)\n\n      assert full_render == %{\n               0 => 1,\n               :c => %{\n                 1 => %{\n                   0 => \"component\",\n                   1 => \"world\",\n                   :r => 1,\n                   :s => [\"<div>FROM \", \" \", \"</div>\"]\n                 }\n               },\n               :r => 1,\n               :s => 0,\n               :p => %{0 => [\"<div>\\n  \", \"\\n</div>\"]}\n             }\n\n      assert fingerprints == {rendered.fingerprint, %{}}\n\n      {cid_to_component, _, 2} = components\n      assert {MyComponent, \"hello\", _, _, _} = cid_to_component[1]\n\n      assert_received {:mount, %Socket{endpoint: __MODULE__, assigns: assigns}}\n      assert assigns[:flash] == %{}\n      assert assigns[:myself] == %CID{cid: 1}\n\n      assert_received {:update, %{from: :component}, %Socket{assigns: %{hello: \"world\"}}}\n      assert_received :render\n      refute_received _\n    end\n\n    test \"on root fingerprint change\" do\n      component = %Component{id: \"hello\", assigns: %{from: :component}, component: MyComponent}\n      rendered = component_template(%{component: component})\n      {full_render, fingerprints, components} = render(rendered)\n\n      assert full_render == %{\n               0 => 1,\n               :c => %{\n                 1 => %{\n                   0 => \"component\",\n                   1 => \"world\",\n                   :r => 1,\n                   :s => [\"<div>FROM \", \" \", \"</div>\"]\n                 }\n               },\n               :r => 1,\n               :s => 0,\n               :p => %{0 => [\"<div>\\n  \", \"\\n</div>\"]}\n             }\n\n      assert fingerprints == {rendered.fingerprint, %{}}\n\n      assert_received {:mount, %Socket{endpoint: __MODULE__, assigns: assigns}}\n      assert assigns[:flash] == %{}\n      assert assigns[:myself] == %CID{cid: 1}\n\n      assert_received :render\n\n      another_rendered = another_component_template(%{component: component})\n\n      {another_full_render, another_fingerprints, _} =\n        render(another_rendered, fingerprints, components)\n\n      assert another_full_render == %{\n               0 => 2,\n               :c => %{\n                 2 => %{\n                   0 => \"component\",\n                   1 => \"world\",\n                   :r => 1,\n                   :s => [\"<div>FROM \", \" \", \"</div>\"]\n                 }\n               },\n               :r => 1,\n               :s => 0,\n               :p => %{0 => [\"<span>\\n  \", \"\\n</span>\"]}\n             }\n\n      assert another_fingerprints == {another_rendered.fingerprint, %{}}\n      assert fingerprints != another_fingerprints\n\n      assert_received {:mount, %Socket{endpoint: __MODULE__, assigns: assigns}}\n      assert assigns[:flash] == %{}\n      assert assigns[:myself] == %CID{cid: 2}\n\n      assert_received :render\n    end\n\n    test \"raises on duplicate component IDs\" do\n      assigns = %{socket: %Socket{}}\n\n      rendered = ~H\"\"\"\n      <.live_component module={RenderOnlyComponent} id=\"SAME\" from=\"SAME\" />\n      <.live_component module={RenderOnlyComponent} id=\"SAME\" from=\"SAME\" />\n      \"\"\"\n\n      assert_raise RuntimeError,\n                   \"found duplicate ID \\\"SAME\\\" for component Phoenix.LiveView.DiffTest.RenderOnlyComponent when rendering template\",\n                   fn -> render(rendered) end\n    end\n\n    test \"on update without render\" do\n      component = %Component{id: \"hello\", assigns: %{from: :component}, component: MyComponent}\n      rendered = component_template(%{component: component})\n      {_, previous_fingerprints, previous_components} = render(rendered)\n\n      {full_render, fingerprints, components} =\n        render(rendered, previous_fingerprints, previous_components)\n\n      assert full_render == %{0 => 1}\n      assert fingerprints == previous_fingerprints\n      assert components == previous_components\n\n      assert_received {:mount, %Socket{endpoint: __MODULE__, assigns: assigns}}\n      assert assigns[:flash] == %{}\n      assert assigns[:myself] == %CID{cid: 1}\n\n      assert_received {:update, %{from: :component}, %Socket{assigns: %{hello: \"world\"}}}\n      assert_received :render\n      assert_received {:update, %{from: :component}, %Socket{assigns: %{hello: \"world\"}}}\n      refute_received _\n    end\n\n    test \"on update with render\" do\n      component = %Component{id: \"hello\", assigns: %{from: :component}, component: MyComponent}\n      rendered = component_template(%{component: component})\n      {_, previous_fingerprints, previous_components} = render(rendered)\n\n      component = %Component{id: \"hello\", assigns: %{from: :rerender}, component: MyComponent}\n      rendered = component_template(%{component: component})\n\n      {full_render, fingerprints, components} =\n        render(rendered, previous_fingerprints, previous_components)\n\n      assert full_render == %{0 => 1, :c => %{1 => %{0 => \"rerender\"}}}\n      assert fingerprints == previous_fingerprints\n      assert components != previous_components\n\n      assert_received {:mount, %Socket{endpoint: __MODULE__, assigns: assigns}}\n      assert assigns[:flash] == %{}\n      assert assigns[:myself] == %CID{cid: 1}\n\n      assert_received {:update, %{from: :component},\n                       %Socket{assigns: %{hello: \"world\", myself: %CID{cid: 1}}}}\n\n      assert_received :render\n\n      assert_received {:update, %{from: :rerender},\n                       %Socket{assigns: %{hello: \"world\", myself: %CID{cid: 1}}}}\n\n      assert_received :render\n      refute_received _\n    end\n\n    test \"on update with temporary\" do\n      component = %Component{id: \"hello\", assigns: %{from: :component}, component: TempComponent}\n      rendered = component_template(%{component: component})\n      {full_render, previous_fingerprints, previous_components} = render(rendered)\n\n      assert full_render == %{\n               0 => 1,\n               :c => %{1 => %{0 => \"WELCOME!\", :r => 1, :s => [\"<div>FROM \", \"</div>\"]}},\n               :r => 1,\n               :s => 0,\n               :p => %{0 => [\"<div>\\n  \", \"\\n</div>\"]}\n             }\n\n      component = %Component{id: \"hello\", assigns: %{from: :rerender}, component: TempComponent}\n      rendered = component_template(%{component: component})\n\n      {full_render, fingerprints, components} =\n        render(rendered, previous_fingerprints, previous_components)\n\n      assert full_render == %{0 => 1, :c => %{1 => %{0 => \"rerender\"}}}\n      assert fingerprints == previous_fingerprints\n      assert components != previous_components\n\n      assert_received {:temporary_mount, %Socket{endpoint: __MODULE__}}\n      assert_received {:temporary_render, %{first_time: true}}\n      assert_received {:temporary_render, %{first_time: false}}\n      refute_received _\n    end\n\n    test \"on update_many\" do\n      alias Component, as: C\n      alias TreeComponent, as: TC\n\n      tree = %C{\n        component: TC,\n        id: \"R\",\n        assigns: %{\n          id: \"R\",\n          children: [\n            %C{\n              component: TC,\n              id: \"A\",\n              assigns: %{\n                id: \"A\",\n                children: [\n                  %C{component: TC, id: \"B\", assigns: %{id: \"B\", children: []}},\n                  %C{component: TC, id: \"C\", assigns: %{id: \"C\", children: []}},\n                  %C{component: TC, id: \"D\", assigns: %{id: \"D\", children: []}}\n                ]\n              }\n            },\n            %C{\n              component: TC,\n              id: \"X\",\n              assigns: %{\n                id: \"X\",\n                children: [\n                  %C{component: TC, id: \"Y\", assigns: %{id: \"Y\", children: []}},\n                  %C{component: TC, id: \"Z\", assigns: %{id: \"Z\", children: []}}\n                ]\n              }\n            }\n          ]\n        }\n      }\n\n      rendered = component_template(%{component: tree})\n      {full_render, fingerprints, components} = render(rendered)\n\n      assert %{\n               c: %{\n                 1 => %{0 => \"R\"},\n                 2 => %{0 => \"A\"},\n                 3 => %{0 => \"X\"},\n                 4 => %{0 => \"B\"},\n                 5 => %{0 => \"C\"},\n                 6 => %{0 => \"D\"},\n                 7 => %{0 => \"Y\"},\n                 8 => %{0 => \"Z\"}\n               }\n             } = full_render\n\n      assert fingerprints == {rendered.fingerprint, %{}}\n      assert {_, _, 9} = components\n\n      assert_received {:update_many, [{%{id: \"R\"}, socket0}]}\n      assert %Socket{assigns: %{myself: %CID{cid: 1}}} = socket0\n\n      assert_received {:update_many, [{%{id: \"A\"}, socket0}, {%{id: \"X\"}, socket1}]}\n      assert %Socket{assigns: %{myself: %CID{cid: 2}}} = socket0\n      assert %Socket{assigns: %{myself: %CID{cid: 3}}} = socket1\n\n      assert_received {:update_many,\n                       [\n                         {%{id: \"B\"}, %Socket{assigns: %{myself: %CID{cid: 4}}}},\n                         {%{id: \"C\"}, %Socket{assigns: %{myself: %CID{cid: 5}}}},\n                         {%{id: \"D\"}, %Socket{assigns: %{myself: %CID{cid: 6}}}},\n                         {%{id: \"Y\"}, %Socket{assigns: %{myself: %CID{cid: 7}}}},\n                         {%{id: \"Z\"}, %Socket{assigns: %{myself: %CID{cid: 8}}}}\n                       ]}\n\n      refute_received {:update, _}\n    end\n\n    test \"on addition\" do\n      component = %Component{id: \"hello\", assigns: %{from: :component}, component: MyComponent}\n      rendered = component_template(%{component: component})\n      {_, previous_fingerprints, previous_components} = render(rendered)\n\n      component = %Component{id: \"another\", assigns: %{from: :another}, component: MyComponent}\n      rendered = component_template(%{component: component})\n\n      {full_render, fingerprints, components} =\n        render(rendered, previous_fingerprints, previous_components)\n\n      assert full_render == %{0 => 2, :c => %{2 => %{0 => \"another\", 1 => \"world\", :s => -1}}}\n\n      assert fingerprints == previous_fingerprints\n      assert components != previous_components\n\n      assert_received {:mount, %Socket{endpoint: __MODULE__}}\n      assert_received {:update, %{from: :component}, %Socket{assigns: %{hello: \"world\"}}}\n      assert_received :render\n      assert_received {:mount, %Socket{endpoint: __MODULE__}}\n      assert_received {:update, %{from: :another}, %Socket{assigns: %{hello: \"world\"}}}\n      assert_received :render\n      refute_received _\n    end\n\n    test \"duplicate IDs\" do\n      component = %Component{id: \"hello\", assigns: %{from: :component}, component: TempComponent}\n      rendered = component_template(%{component: component})\n      {_, previous_fingerprints, previous_components} = render(rendered)\n\n      component = %Component{id: \"hello\", assigns: %{from: :replaced}, component: MyComponent}\n      rendered = component_template(%{component: component})\n\n      {full_render, fingerprints, components} =\n        render(rendered, previous_fingerprints, previous_components)\n\n      assert full_render == %{\n               0 => 2,\n               :c => %{\n                 2 => %{\n                   0 => \"replaced\",\n                   1 => \"world\",\n                   :s => [\"<div>FROM \", \" \", \"</div>\"],\n                   :r => 1\n                 }\n               }\n             }\n\n      assert fingerprints == previous_fingerprints\n      assert components != previous_components\n\n      assert_received {:temporary_mount, %Socket{endpoint: __MODULE__}}\n      assert_received {:temporary_render, %{first_time: true, from: :component}}\n      assert_received {:mount, %Socket{endpoint: __MODULE__}}\n      assert_received {:update, %{from: :replaced}, %Socket{assigns: %{hello: \"world\"}}}\n      assert_received :render\n      refute_received _\n    end\n\n    test \"inside comprehension\" do\n      components = [\n        %Component{id: \"index_1\", assigns: %{from: :index_1}, component: MyComponent},\n        %Component{id: \"index_2\", assigns: %{from: :index_2}, component: MyComponent}\n      ]\n\n      assigns = %{components: components}\n\n      %{fingerprint: fingerprint} =\n        rendered = ~H\"\"\"\n        <div>\n          <%= for {component, index} <- Enum.with_index(@components, 0) do %>\n            {index}: {component}\n          <% end %>\n        </div>\n        \"\"\"\n\n      {full_render, fingerprints, components} = render(rendered)\n\n      assert full_render == %{\n               0 => %{s: 0, k: %{0 => %{0 => \"0\", 1 => 1}, 1 => %{0 => \"1\", 1 => 2}, :kc => 2}},\n               :c => %{\n                 1 => %{\n                   0 => \"index_1\",\n                   1 => \"world\",\n                   :r => 1,\n                   :s => [\"<div>FROM \", \" \", \"</div>\"]\n                 },\n                 2 => %{0 => \"index_2\", 1 => \"world\", :s => 1}\n               },\n               :r => 1,\n               :s => 1,\n               :p => %{0 => [\"\\n    \", \": \", \"\\n  \"], 1 => [\"<div>\\n  \", \"\\n</div>\"]}\n             }\n\n      assert {^fingerprint, %{0 => _}} = fingerprints\n\n      {cid_to_component, _, 3} = components\n      assert {MyComponent, \"index_1\", _, _, _} = cid_to_component[1]\n      assert {MyComponent, \"index_2\", _, _, _} = cid_to_component[2]\n\n      assert_received {:mount, %Socket{endpoint: __MODULE__}}\n      assert_received {:update, %{from: :index_1}, %Socket{assigns: %{hello: \"world\"}}}\n      assert_received :render\n      assert_received {:mount, %Socket{endpoint: __MODULE__}}\n      assert_received {:update, %{from: :index_2}, %Socket{assigns: %{hello: \"world\"}}}\n      assert_received :render\n    end\n\n    test \"inside comprehension with subtree\" do\n      template = fn components ->\n        assigns = %{components: components}\n\n        ~H\"\"\"\n        <div>\n          <%= for {component, index} <- Enum.with_index(@components, 0) do %>\n            {index}: {component}\n          <% end %>\n        </div>\n        \"\"\"\n      end\n\n      # We start by rendering two components\n      components = [\n        %Component{id: \"index_1\", assigns: %{from: :index_1}, component: IfComponent},\n        %Component{id: \"index_2\", assigns: %{from: :index_2}, component: IfComponent}\n      ]\n\n      {full_render, fingerprints, diff_components} = render(template.(components))\n\n      assert full_render == %{\n               0 => %{s: 0, k: %{0 => %{0 => \"0\", 1 => 1}, 1 => %{0 => \"1\", 1 => 2}, :kc => 2}},\n               :c => %{\n                 1 => %{\n                   0 => %{0 => \"index_1\", :s => [\"\\n    IF \", \"\\n  \"]},\n                   :r => 1,\n                   :s => [\"<div>\\n  \", \"\\n</div>\"]\n                 },\n                 2 => %{0 => %{0 => \"index_2\"}, :s => 1}\n               },\n               :r => 1,\n               :s => 1,\n               :p => %{0 => [\"\\n    \", \": \", \"\\n  \"], 1 => [\"<div>\\n  \", \"\\n</div>\"]}\n             }\n\n      {cid_to_component, _, 3} = diff_components\n      assert {IfComponent, \"index_1\", _, _, _} = cid_to_component[1]\n      assert {IfComponent, \"index_2\", _, _, _} = cid_to_component[2]\n\n      # Now let's add a third component, it shall reuse index_1\n      components = [\n        %Component{id: \"index_3\", assigns: %{from: :index_3}, component: IfComponent}\n      ]\n\n      {diff, _, diff_components} =\n        render(template.(components), fingerprints, diff_components)\n\n      assert diff == %{\n               0 => %{k: %{0 => %{1 => 3}, :kc => 1}},\n               :c => %{3 => %{0 => %{0 => \"index_3\"}, :s => -1}}\n             }\n\n      {cid_to_component, _, 4} = diff_components\n      assert {IfComponent, \"index_3\", _, _, _} = cid_to_component[3]\n\n      # Now let's add a fourth component, with a different subtree than index_0\n      components = [\n        %Component{id: \"index_4\", assigns: %{from: :index_4, if: false}, component: IfComponent}\n      ]\n\n      {diff, _, diff_components} =\n        render(template.(components), fingerprints, diff_components)\n\n      assert diff == %{\n               0 => %{k: %{0 => %{1 => 4}, :kc => 1}},\n               :c => %{4 => %{0 => %{0 => \"index_4\", :s => [\"\\n    ELSE \", \"\\n  \"]}, :s => -1}}\n             }\n\n      {cid_to_component, _, 5} = diff_components\n      assert {IfComponent, \"index_4\", _, _, _} = cid_to_component[4]\n\n      # Finally, let's add a fifth component while changing the first component at the same time.\n      # We should point to the index tree of index_0 before render.\n      components = [\n        %Component{id: \"index_1\", assigns: %{from: :index_1, if: false}, component: IfComponent},\n        %Component{id: \"index_5\", assigns: %{from: :index_5}, component: IfComponent}\n      ]\n\n      {diff, _, diff_components} =\n        render(template.(components), fingerprints, diff_components)\n\n      assert diff == %{\n               0 => %{k: %{0 => %{1 => 1}, 1 => %{1 => 5}, :kc => 2}},\n               :c => %{\n                 1 => %{0 => %{0 => \"index_1\", :s => [\"\\n    ELSE \", \"\\n  \"]}},\n                 5 => %{0 => %{0 => \"index_5\"}, :s => -1}\n               }\n             }\n\n      {cid_to_component, _, 6} = diff_components\n      assert {IfComponent, \"index_5\", _, _, _} = cid_to_component[5]\n    end\n\n    test \"inside nested comprehension\" do\n      components = [\n        %Component{id: \"index_1\", assigns: %{from: :index_1}, component: MyComponent},\n        %Component{id: \"index_2\", assigns: %{from: :index_2}, component: MyComponent}\n      ]\n\n      assigns = %{components: components, ids: [\"foo\", \"bar\"]}\n\n      %{fingerprint: fingerprint} =\n        rendered = ~H\"\"\"\n        <div>\n          <%= for prefix_id <- @ids do %>\n            {prefix_id}\n            <%= for {component, index} <- Enum.with_index(@components, 0) do %>\n              {index}: {%{component | id: \"#{prefix_id}-#{component.id}\"}}\n            <% end %>\n          <% end %>\n        </div>\n        \"\"\"\n\n      {full_render, fingerprints, components} = render(rendered)\n\n      assert full_render == %{\n               0 => %{\n                 k: %{\n                   0 => %{\n                     0 => \"foo\",\n                     1 => %{\n                       k: %{0 => %{0 => \"0\", 1 => 1}, 1 => %{0 => \"1\", 1 => 2}, :kc => 2},\n                       s: 0\n                     }\n                   },\n                   1 => %{\n                     0 => \"bar\",\n                     1 => %{\n                       k: %{0 => %{0 => \"0\", 1 => 3}, 1 => %{0 => \"1\", 1 => 4}, :kc => 2},\n                       s: 0\n                     }\n                   },\n                   :kc => 2\n                 },\n                 s: 1\n               },\n               :c => %{\n                 1 => %{\n                   0 => \"index_1\",\n                   1 => \"world\",\n                   :r => 1,\n                   :s => [\"<div>FROM \", \" \", \"</div>\"]\n                 },\n                 2 => %{0 => \"index_2\", 1 => \"world\", :s => 1},\n                 3 => %{0 => \"index_1\", 1 => \"world\", :s => 1},\n                 4 => %{0 => \"index_2\", 1 => \"world\", :s => 3}\n               },\n               :p => %{\n                 0 => [\"\\n      \", \": \", \"\\n    \"],\n                 1 => [\"\\n    \", \"\\n    \", \"\\n  \"],\n                 2 => [\"<div>\\n  \", \"\\n</div>\"]\n               },\n               :r => 1,\n               :s => 2\n             }\n\n      assert {^fingerprint, %{0 => _}} = fingerprints\n\n      {cid_to_component, _, 5} = components\n      assert {MyComponent, \"foo-index_1\", _, _, _} = cid_to_component[1]\n      assert {MyComponent, \"foo-index_2\", _, _, _} = cid_to_component[2]\n      assert {MyComponent, \"bar-index_1\", _, _, _} = cid_to_component[3]\n      assert {MyComponent, \"bar-index_2\", _, _, _} = cid_to_component[4]\n\n      for from <- [:index_1, :index_2, :index_1, :index_2] do\n        assert_received {:mount, %Socket{endpoint: __MODULE__}}\n        assert_received {:update, %{from: ^from}, %Socket{assigns: %{hello: \"world\"}}}\n        assert_received :render\n      end\n    end\n\n    test \"inside comprehension with recursive subtree\" do\n      template = fn id, children ->\n        assigns = %{id: id, children: children}\n\n        ~H\"\"\"\n        <.live_component module={RecurComponent} id={@id} children={@children} />\n        \"\"\"\n      end\n\n      {full_render, fingerprints, diff_components} = render(template.(1, []))\n\n      assert full_render == %{\n               0 => 1,\n               :c => %{\n                 1 => %{0 => \"1\", 1 => \"\", :r => 1, :s => [\"<div>\\n  ID: \", \"\\n  \", \"\\n</div>\"]}\n               },\n               :s => 0,\n               :p => %{0 => [\"\", \"\"]}\n             }\n\n      {cid_to_component, _, 2} = diff_components\n      assert {RecurComponent, 1, _, _, _} = cid_to_component[1]\n\n      # Now let's add one level of nesting\n      {diff, _, diff_components} =\n        render(template.(1, [{2, []}]), fingerprints, diff_components)\n\n      assert diff == %{\n               0 => 1,\n               :c => %{\n                 1 => %{1 => %{s: [\"\\n    \", \"\\n  \"], k: %{0 => %{0 => 2}, :kc => 1}}},\n                 2 => %{0 => \"2\", 1 => \"\", :s => -1}\n               }\n             }\n\n      {cid_to_component, _, 3} = diff_components\n      assert {RecurComponent, 2, _, _, _} = cid_to_component[2]\n\n      # Now let's add two levels of nesting\n      {diff, _, diff_components} =\n        render(template.(1, [{2, [{3, []}]}]), fingerprints, diff_components)\n\n      assert diff == %{\n               0 => 1,\n               :c => %{\n                 1 => %{1 => %{k: %{0 => %{0 => 2}, :kc => 1}}},\n                 2 => %{1 => %{k: %{0 => %{0 => 3}, :kc => 1}, s: [\"\\n    \", \"\\n  \"]}},\n                 3 => %{0 => \"3\", 1 => %{k: %{kc: 0}}, :s => -1}\n               }\n             }\n\n      {cid_to_component, _, 4} = diff_components\n      assert {RecurComponent, 3, _, _, _} = cid_to_component[3]\n    end\n\n    test \"inside comprehension with recursive subtree on update\" do\n      template = fn id, children ->\n        assigns = %{id: id, children: children}\n\n        ~H\"\"\"\n        <.live_component module={RecurComponent} id={@id} children={@children} />\n        \"\"\"\n      end\n\n      {full_render, _fingerprints, diff_components} = render(template.(1, []))\n\n      assert full_render == %{\n               0 => 1,\n               :c => %{\n                 1 => %{0 => \"1\", 1 => \"\", :r => 1, :s => [\"<div>\\n  ID: \", \"\\n  \", \"\\n</div>\"]}\n               },\n               :s => 0,\n               :p => %{0 => [\"\", \"\"]}\n             }\n\n      {cid_to_component, _, 2} = diff_components\n      assert {RecurComponent, 1, _, _, _} = cid_to_component[1]\n\n      # Now let's add one level of nesting directly\n      {diff, diff_components, :extra} =\n        Diff.write_component(%Socket{endpoint: __MODULE__}, 1, diff_components, fn\n          socket, _component ->\n            {Phoenix.Component.assign(socket, children: [{2, []}]), :extra}\n        end)\n\n      assert diff == %{\n               c: %{\n                 1 => %{1 => %{s: [\"\\n    \", \"\\n  \"], k: %{0 => %{0 => 2}, :kc => 1}}},\n                 2 => %{0 => \"2\", 1 => \"\", :s => -1}\n               }\n             }\n\n      {cid_to_component, _, 3} = diff_components\n      assert {RecurComponent, 2, _, _, _} = cid_to_component[2]\n    end\n\n    test \"inside rendered inside comprehension\" do\n      components = [\n        %Component{id: \"index_1\", assigns: %{from: :index_1}, component: MyComponent},\n        %Component{id: \"index_2\", assigns: %{from: :index_2}, component: MyComponent}\n      ]\n\n      assigns = %{components: components}\n\n      %{fingerprint: fingerprint} =\n        rendered = ~H\"\"\"\n        <div>\n          <%= for {component, index} <- Enum.with_index(@components, 1) do %>\n            {index}: {component_template(%{component: component})}\n          <% end %>\n        </div>\n        \"\"\"\n\n      {full_render, fingerprints, components} = render(rendered)\n\n      assert full_render == %{\n               0 => %{\n                 k: %{\n                   0 => %{0 => \"1\", 1 => %{0 => 1, :r => 1, :s => 0}},\n                   1 => %{0 => \"2\", 1 => %{0 => 2, :r => 1, :s => 0}},\n                   :kc => 2\n                 },\n                 s: 1\n               },\n               :c => %{\n                 1 => %{\n                   0 => \"index_1\",\n                   1 => \"world\",\n                   :r => 1,\n                   :s => [\"<div>FROM \", \" \", \"</div>\"]\n                 },\n                 2 => %{0 => \"index_2\", 1 => \"world\", :s => 1}\n               },\n               :p => %{\n                 0 => [\"<div>\\n  \", \"\\n</div>\"],\n                 1 => [\"\\n    \", \": \", \"\\n  \"],\n                 2 => [\"<div>\\n  \", \"\\n</div>\"]\n               },\n               :r => 1,\n               :s => 2\n             }\n\n      assert {^fingerprint, %{0 => _}} = fingerprints\n\n      {cid_to_component, _, 3} = components\n      assert {MyComponent, \"index_1\", _, _, _} = cid_to_component[1]\n      assert {MyComponent, \"index_2\", _, _, _} = cid_to_component[2]\n\n      assert_received {:mount, %Socket{endpoint: __MODULE__}}\n      assert_received {:update, %{from: :index_1}, %Socket{assigns: %{hello: \"world\"}}}\n      assert_received :render\n      assert_received {:mount, %Socket{endpoint: __MODULE__}}\n      assert_received {:update, %{from: :index_2}, %Socket{assigns: %{hello: \"world\"}}}\n      assert_received :render\n    end\n\n    test \"inside condition inside comprehension\" do\n      components = [\n        %Component{id: \"index_1\", assigns: %{from: :index_1}, component: MyComponent},\n        %Component{id: \"index_2\", assigns: %{from: :index_2}, component: MyComponent}\n      ]\n\n      assigns = %{components: components}\n\n      %{fingerprint: fingerprint} =\n        rendered = ~H\"\"\"\n        <div>\n          <%= for {component, index} <- Enum.with_index(@components, 1) do %>\n            <%= if index > 1 do %>\n              {index}: {component}\n            <% end %>\n          <% end %>\n        </div>\n        \"\"\"\n\n      {full_render, fingerprints, components} = render(rendered)\n\n      assert full_render == %{\n               0 => %{\n                 s: 1,\n                 k: %{0 => %{0 => \"\"}, 1 => %{0 => %{0 => \"2\", 1 => 1, :s => 0}}, :kc => 2}\n               },\n               :c => %{\n                 1 => %{\n                   0 => \"index_2\",\n                   1 => \"world\",\n                   :r => 1,\n                   :s => [\"<div>FROM \", \" \", \"</div>\"]\n                 }\n               },\n               :r => 1,\n               :s => 2,\n               :p => %{\n                 0 => [\"\\n      \", \": \", \"\\n    \"],\n                 1 => [\"\\n    \", \"\\n  \"],\n                 2 => [\"<div>\\n  \", \"\\n</div>\"]\n               }\n             }\n\n      assert {^fingerprint, %{0 => _}} = fingerprints\n\n      {cid_to_component, _, 2} = components\n      assert {MyComponent, \"index_2\", _, _, _} = cid_to_component[1]\n\n      assert_received {:mount, %Socket{endpoint: __MODULE__}}\n      assert_received {:update, %{from: :index_2}, %Socket{assigns: %{hello: \"world\"}}}\n      assert_received :render\n      refute_received {:update, %{from: :index_1}, %Socket{assigns: %{hello: \"world\"}}}\n    end\n\n    test \"inside comprehension inside live_component without static\" do\n      assigns = %{socket: %Socket{}}\n\n      %{fingerprint: _fingerprint} =\n        rendered = ~H\"\"\"\n        <%= for key <- [:b, :c] do %>\n          <.live_component module={NestedDynamicComponent} id={key} key={key} />\n        <% end %>\n        \"\"\"\n\n      {full_render, _fingerprints, _components} = render(rendered)\n\n      assert %{\n               0 => %{s: 0, k: %{0 => %{0 => 1}, 1 => %{0 => 2}, :kc => 2}},\n               :c => %{\n                 1 => %{0 => %{0 => \"\", :s => [\"\", \"\"]}, :s => [\"<div>\\n  \", \"\\n</div>\"], :r => 1},\n                 2 => %{0 => %{0 => 3, :s => [\"\", \"\"]}, :s => 1},\n                 3 => %{\n                   0 => %{\n                     0 => %{\n                       s: [\"\\n  \", \"\", \"\\n\"],\n                       k: %{0 => %{0 => \"nothing\", 1 => \"nothing\"}, :kc => 1}\n                     },\n                     :s => [\"\", \"\"]\n                   },\n                   :s => static\n                 }\n               },\n               :s => 1,\n               :p => %{0 => [\"\\n  \", \"\\n\"], 1 => [\"\", \"\"]}\n             } = full_render\n\n      assert is_integer(static)\n      assert rendered_to_binary(full_render) =~ \"nothingnothing\"\n    end\n\n    defp tracking(assigns) do\n      ~H\"\"\"\n      <.live_component :let={%{value: value}} module={SlotComponent} id=\"TRACKING\">\n        WITH PARENT VALUE {@parent_value} WITH VALUE {value}\n      </.live_component>\n      \"\"\"\n    end\n\n    test \"@inner_block tracking with args and parent assigns\" do\n      assigns = %{socket: %Socket{}, parent_value: 123}\n      {full_render, fingerprints, components} = render(tracking(assigns))\n\n      assert full_render == %{\n               0 => 1,\n               :c => %{\n                 1 => %{\n                   0 => \"TRACKING\",\n                   1 => %{\n                     0 => \"123\",\n                     1 => \"1\",\n                     :s => [\"\\n  WITH PARENT VALUE \", \" WITH VALUE \", \"\\n\"]\n                   },\n                   2 => \"TRACKING\",\n                   3 => %{\n                     0 => \"123\",\n                     1 => \"2\",\n                     :s => [\"\\n  WITH PARENT VALUE \", \" WITH VALUE \", \"\\n\"]\n                   },\n                   :r => 1,\n                   :s => [\"<div>\\n  HELLO \", \" \", \" HELLO \", \" \", \"\\n</div>\"]\n                 }\n               },\n               :s => 0,\n               :p => %{0 => [\"\", \"\"]}\n             }\n\n      {full_render, _fingerprints, _components} =\n        render(tracking(assigns), fingerprints, components)\n\n      assert full_render == %{0 => 1}\n\n      # Changing the root assign\n      assigns = %{socket: %Socket{}, parent_value: 123, __changed__: %{parent_value: true}}\n\n      {full_render, _fingerprints, _components} =\n        render(tracking(assigns), fingerprints, components)\n\n      assert full_render == %{\n               0 => 1,\n               :c => %{\n                 1 => %{\n                   1 => %{0 => \"123\", 1 => \"1\"},\n                   3 => %{0 => \"123\", 1 => \"2\"}\n                 }\n               }\n             }\n    end\n  end\n\n  describe \"keyed comprehensions\" do\n    defp keyed_comprehension_with_pattern(assigns) do\n      ~H\"\"\"\n      <ul>\n        <li :for={%{id: id, name: name} <- @items} :key={id}>\n          Outside assign: {@count} Inside assign: {name}\n        </li>\n      </ul>\n      \"\"\"\n    end\n\n    defp non_keyed_comprehension_with_pattern(assigns) do\n      ~H\"\"\"\n      <ul>\n        <li :for={%{id: id, name: name} <- @items}>\n          Outside assign: {@count} Inside assign: {name}\n        </li>\n      </ul>\n      \"\"\"\n    end\n\n    defp keyed_comprehension_with_nested_access(assigns) do\n      ~H\"\"\"\n      <ul>\n        <li :for={{id, entry} <- @items} :key={id}>\n          <span>Count: {@count}</span>\n          <span>Dot: {entry.foo.bar}</span>\n          <span>Access: {entry[:foo][:bar]}</span>\n        </li>\n      </ul>\n      \"\"\"\n    end\n\n    defp deep_keyed_comprehension(assigns) do\n      ~H\"\"\"\n      <.keyed_comprehension_with_pattern items={@items} count={100} />\n      <.keyed_comprehension_with_pattern items={@items} count={200} />\n      \"\"\"\n    end\n\n    defp comprehended_keyed_comprehension(assigns) do\n      ~H\"\"\"\n      <%= for count <- [100, 200] do %>\n        <.keyed_comprehension_with_pattern items={@items} count={count} />\n      <% end %>\n      \"\"\"\n    end\n\n    defp keyed_comprehension_with_component(assigns) do\n      ~H\"\"\"\n      <.non_keyed_comprehension_with_pattern\n        :for={{id, items} <- @list_of_items}\n        :key={id}\n        items={items}\n        count={@count}\n      />\n      \"\"\"\n    end\n\n    defp keyed_comprehension_with_component_and_slots(assigns) do\n      ~H\"\"\"\n      <.my_demo_list :for={item <- @items} :key={item.id}>\n        <:entry :for={entry <- item.entries} key={entry.id}>\n          {entry.title} {@count}\n        </:entry>\n      </.my_demo_list>\n      \"\"\"\n    end\n\n    defp my_demo_list(assigns) do\n      ~H\"\"\"\n      <ul>\n        <li :for={slot <- @entry} :key={slot.key}>{render_slot(slot)}</li>\n      </ul>\n      \"\"\"\n    end\n\n    defp comprehended_keyed_comprehension_with_slot(assigns) do\n      ~H\"\"\"\n      <%= for count <- [100, 200] do %>\n        <.slotted_list :let={%{name: name}} items={@items}>\n          Outside assign: {count} Inside assign: {name}\n        </.slotted_list>\n      <% end %>\n      \"\"\"\n    end\n\n    defp slotted_list(assigns) do\n      ~H\"\"\"\n      <ul>\n        <li :for={%{id: id} = item <- @items} :key={id}>\n          {render_slot(@inner_block, item)}}\n        </li>\n      </ul>\n      \"\"\"\n    end\n\n    test \"change tracking with minimal diff updates\" do\n      items = [\n        %{id: 1, name: \"First\"},\n        %{id: 2, name: \"Second\"}\n      ]\n\n      assigns = %{socket: %Socket{}, items: items, count: 0, __changed__: %{}}\n      {full_render, fingerprints, components} = render(keyed_comprehension_with_pattern(assigns))\n\n      assert full_render == %{\n               0 => %{\n                 k: %{0 => %{0 => \"0\", 1 => \"First\"}, 1 => %{0 => \"0\", 1 => \"Second\"}, :kc => 2},\n                 s: 0\n               },\n               :p => %{\n                 0 => [\"<li>\\n    Outside assign: \", \" Inside assign: \", \"\\n  </li>\"],\n                 1 => [\"<ul>\\n  \", \"\\n</ul>\"]\n               },\n               :r => 1,\n               :s => 1\n             }\n\n      # change order of items\n      assigns = Phoenix.Component.assign(assigns, :items, Enum.reverse(assigns.items))\n\n      {second_render, fingerprints, components} =\n        render(keyed_comprehension_with_pattern(assigns), fingerprints, components)\n\n      assert second_render == %{0 => %{k: %{0 => 1, 1 => 0, :kc => 2}}}\n\n      # update count\n      assigns = Phoenix.Component.assign(assigns, :count, 1)\n\n      {third_render, fingerprints, components} =\n        render(keyed_comprehension_with_pattern(assigns), fingerprints, components)\n\n      assert third_render == %{0 => %{k: %{0 => %{0 => \"1\"}, 1 => %{0 => \"1\"}, :kc => 2}}}\n\n      # replace item\n      assigns =\n        assigns\n        |> Map.put(:__changed__, %{})\n        |> Phoenix.Component.assign(:items, [\n          %{id: 1, name: \"First\"},\n          %{id: 3, name: \"Third\"}\n        ])\n\n      {fourth_render, _fingerprints, _components} =\n        render(keyed_comprehension_with_pattern(assigns), fingerprints, components)\n\n      assert fourth_render == %{0 => %{k: %{0 => 1, 1 => %{0 => \"1\", 1 => \"Third\"}, :kc => 2}}}\n    end\n\n    test \"change tracking when no key is given\" do\n      items = [\n        %{id: 1, name: \"First\"},\n        %{id: 2, name: \"Second\"}\n      ]\n\n      assigns = %{socket: %Socket{}, items: items, count: 0, __changed__: %{}}\n\n      {full_render, fingerprints, components} =\n        render(non_keyed_comprehension_with_pattern(assigns))\n\n      assert full_render == %{\n               0 => %{\n                 k: %{0 => %{0 => \"0\", 1 => \"First\"}, 1 => %{0 => \"0\", 1 => \"Second\"}, :kc => 2},\n                 s: 0\n               },\n               :p => %{\n                 0 => [\"<li>\\n    Outside assign: \", \" Inside assign: \", \"\\n  </li>\"],\n                 1 => [\"<ul>\\n  \", \"\\n</ul>\"]\n               },\n               :r => 1,\n               :s => 1\n             }\n\n      # change order of items\n      assigns = Phoenix.Component.assign(assigns, :items, Enum.reverse(assigns.items))\n\n      {second_render, fingerprints, components} =\n        render(non_keyed_comprehension_with_pattern(assigns), fingerprints, components)\n\n      # the index is the key, so we get the reversed order for the names\n      assert second_render == %{\n               0 => %{k: %{0 => %{1 => \"Second\"}, 1 => %{1 => \"First\"}, :kc => 2}}\n             }\n\n      # update count\n      assigns = Phoenix.Component.assign(assigns, :count, 1)\n\n      {third_render, fingerprints, components} =\n        render(non_keyed_comprehension_with_pattern(assigns), fingerprints, components)\n\n      # only the new count is sent\n      assert third_render == %{0 => %{k: %{0 => %{0 => \"1\"}, 1 => %{0 => \"1\"}, :kc => 2}}}\n\n      # replace item\n      assigns =\n        assigns\n        |> Map.put(:__changed__, %{})\n        |> Phoenix.Component.assign(:items, [\n          %{id: 1, name: \"Second\"},\n          %{id: 3, name: \"Third\"}\n        ])\n\n      {fourth_render, _fingerprints, _components} =\n        render(non_keyed_comprehension_with_pattern(assigns), fingerprints, components)\n\n      # only index 1 is updated\n      assert fourth_render == %{0 => %{k: %{1 => %{1 => \"Third\"}, :kc => 2}}}\n    end\n\n    test \"change-tracking for complex access\" do\n      items = [\n        {1, %{foo: %{bar: \"First\", baz: \"1\"}, other: \"hey\"}},\n        {2, %{foo: %{bar: \"Second\", baz: \"1\"}, other: \"hey\"}}\n      ]\n\n      assigns = %{socket: %Socket{}, items: items, count: 0, __changed__: %{}}\n\n      {full_render, fingerprints, components} =\n        render(keyed_comprehension_with_nested_access(assigns))\n\n      assert full_render == %{\n               0 => %{\n                 k: %{\n                   0 => %{0 => \"0\", 1 => \"First\", 2 => \"First\"},\n                   1 => %{0 => \"0\", 1 => \"Second\", 2 => \"Second\"},\n                   :kc => 2\n                 },\n                 s: 0\n               },\n               :p => %{\n                 0 => [\n                   \"<li>\\n    <span>Count: \",\n                   \"</span>\\n    <span>Dot: \",\n                   \"</span>\\n    <span>Access: \",\n                   \"</span>\\n  </li>\"\n                 ],\n                 1 => [\"<ul>\\n  \", \"\\n</ul>\"]\n               },\n               :r => 1,\n               :s => 1\n             }\n\n      # change entries, but no part that is rendered\n      assigns =\n        Phoenix.Component.assign(\n          assigns,\n          :items,\n          [\n            {1, %{foo: %{bar: \"First\", baz: \"2\"}, other: \"heyo\"}},\n            {2, %{foo: %{bar: \"Second\", baz: \"2\"}, other: \"heyo\"}}\n          ]\n        )\n\n      {second_render, fingerprints, components} =\n        render(keyed_comprehension_with_nested_access(assigns), fingerprints, components)\n\n      # no diff, because nothing relevant changed\n      assert second_render == %{}\n\n      # now change bar for first entry\n      assigns =\n        Phoenix.Component.assign(assigns, :items, [\n          {1, %{foo: %{bar: \"Updated\", baz: \"2\"}, other: \"heyo\"}},\n          {2, %{foo: %{bar: \"Second\", baz: \"2\"}, other: \"heyo\"}}\n        ])\n\n      {third_render, _fingerprints, _components} =\n        render(keyed_comprehension_with_nested_access(assigns), fingerprints, components)\n\n      # no diff, because nothing relevant changed\n      assert third_render == %{0 => %{k: %{0 => %{1 => \"Updated\", 2 => \"Updated\"}, :kc => 2}}}\n    end\n\n    test \"keys don't need to be globally unique\" do\n      items = [\n        %{id: 1, name: \"First\"},\n        %{id: 2, name: \"Second\"}\n      ]\n\n      assigns = %{socket: %Socket{}, items: items, __changed__: %{}}\n      {full_render, _fingerprints, _components} = render(deep_keyed_comprehension(assigns))\n\n      assert full_render == %{\n               0 => %{\n                 0 => %{\n                   k: %{\n                     0 => %{0 => \"100\", 1 => \"First\"},\n                     1 => %{0 => \"100\", 1 => \"Second\"},\n                     :kc => 2\n                   },\n                   s: 0\n                 },\n                 :r => 1,\n                 :s => 1\n               },\n               1 => %{\n                 0 => %{\n                   k: %{\n                     0 => %{0 => \"200\", 1 => \"First\"},\n                     1 => %{0 => \"200\", 1 => \"Second\"},\n                     :kc => 2\n                   },\n                   s: 0\n                 },\n                 :r => 1,\n                 :s => 1\n               },\n               :p => %{\n                 0 => [\"<li>\\n    Outside assign: \", \" Inside assign: \", \"\\n  </li>\"],\n                 1 => [\"<ul>\\n  \", \"\\n</ul>\"],\n                 2 => [\"\", \"\\n\", \"\"]\n               },\n               :s => 2\n             }\n    end\n\n    test \"inside another comprehension\" do\n      items = [\n        %{id: 1, name: \"First\"},\n        %{id: 2, name: \"Second\"}\n      ]\n\n      assigns = %{socket: %Socket{}, items: items, __changed__: %{}}\n      {full_render, _fingerprints, components} = render(comprehended_keyed_comprehension(assigns))\n\n      assert full_render == %{\n               0 => %{\n                 k: %{\n                   0 => %{\n                     0 => %{\n                       0 => %{\n                         k: %{\n                           0 => %{0 => \"100\", 1 => \"First\"},\n                           1 => %{0 => \"100\", 1 => \"Second\"},\n                           :kc => 2\n                         },\n                         s: 0\n                       },\n                       :r => 1,\n                       :s => 1\n                     }\n                   },\n                   1 => %{\n                     0 => %{\n                       0 => %{\n                         k: %{\n                           0 => %{0 => \"200\", 1 => \"First\"},\n                           1 => %{0 => \"200\", 1 => \"Second\"},\n                           :kc => 2\n                         },\n                         s: 0\n                       },\n                       :r => 1,\n                       :s => 1\n                     }\n                   },\n                   :kc => 2\n                 },\n                 s: 2\n               },\n               :p => %{\n                 0 => [\"<li>\\n    Outside assign: \", \" Inside assign: \", \"\\n  </li>\"],\n                 1 => [\"<ul>\\n  \", \"\\n</ul>\"],\n                 2 => [\"\\n  \", \"\\n\"],\n                 3 => [\"\", \"\"]\n               },\n               :s => 3\n             }\n\n      assert {%{}, %{}, 1} = components\n    end\n\n    test \"vars_changed is reset for new items\" do\n      items = [\n        %{id: 1, name: \"First\"},\n        %{id: 2, name: \"Second\"}\n      ]\n\n      assigns = %{socket: %Socket{}, items: items, __changed__: %{}}\n\n      {full_render, fingerprints, components} =\n        render(comprehended_keyed_comprehension_with_slot(assigns))\n\n      assert full_render == %{\n               0 => %{\n                 k: %{\n                   0 => %{\n                     0 => %{\n                       0 => %{\n                         k: %{\n                           0 => %{0 => %{0 => \"100\", 1 => \"First\", :s => 0}},\n                           1 => %{0 => %{0 => \"100\", 1 => \"Second\", :s => 0}},\n                           :kc => 2\n                         },\n                         s: 1\n                       },\n                       :r => 1,\n                       :s => 2\n                     }\n                   },\n                   1 => %{\n                     0 => %{\n                       0 => %{\n                         k: %{\n                           0 => %{0 => %{0 => \"200\", 1 => \"First\", :s => 0}},\n                           1 => %{0 => %{0 => \"200\", 1 => \"Second\", :s => 0}},\n                           :kc => 2\n                         },\n                         s: 1\n                       },\n                       :r => 1,\n                       :s => 2\n                     }\n                   },\n                   :kc => 2\n                 },\n                 s: 3\n               },\n               :p => %{\n                 0 => [\"\\n    Outside assign: \", \" Inside assign: \", \"\\n  \"],\n                 1 => [\"<li>\\n    \", \"}\\n  </li>\"],\n                 2 => [\"<ul>\\n  \", \"\\n</ul>\"],\n                 3 => [\"\\n  \", \"\\n\"],\n                 4 => [\"\", \"\"]\n               },\n               :s => 4\n             }\n\n      assert {%{}, %{}, 1} = components\n\n      assigns =\n        Phoenix.Component.assign(assigns, :items, [\n          %{id: 3, name: \"Third\"},\n          %{id: 2, name: \"Second\"}\n        ])\n\n      assert {second_render, _fingerprints, _components} =\n               render(\n                 comprehended_keyed_comprehension_with_slot(assigns),\n                 fingerprints,\n                 components\n               )\n\n      # we still get 100 and 200 from the outer comprehension, even those did not change\n      assert second_render == %{\n               0 => %{\n                 k: %{\n                   0 => %{\n                     0 => %{\n                       0 => %{k: %{0 => %{0 => %{1 => \"Third\", :s => 0, 0 => \"100\"}}, :kc => 2}}\n                     }\n                   },\n                   1 => %{\n                     0 => %{\n                       0 => %{k: %{0 => %{0 => %{1 => \"Third\", :s => 0, 0 => \"200\"}}, :kc => 2}}\n                     }\n                   },\n                   :kc => 2\n                 }\n               },\n               :p => %{0 => [\"\\n    Outside assign: \", \" Inside assign: \", \"\\n  \"]}\n             }\n    end\n\n    test \":key on components\" do\n      list_of_items = [\n        {1,\n         [\n           %{id: 1, name: \"First\"},\n           %{id: 2, name: \"Second\"}\n         ]},\n        {2,\n         [\n           %{id: 1, name: \"Third\"},\n           %{id: 2, name: \"Fourth\"}\n         ]}\n      ]\n\n      assigns = %{socket: %Socket{}, list_of_items: list_of_items, count: 0, __changed__: %{}}\n\n      {full_render, fingerprints, components} =\n        render(keyed_comprehension_with_component(assigns))\n\n      assert full_render == %{\n               0 => %{\n                 k: %{\n                   0 => %{\n                     0 => %{\n                       0 => %{\n                         k: %{\n                           0 => %{0 => \"0\", 1 => \"First\"},\n                           1 => %{0 => \"0\", 1 => \"Second\"},\n                           :kc => 2\n                         },\n                         s: 0\n                       },\n                       :r => 1,\n                       :s => 1\n                     }\n                   },\n                   1 => %{\n                     0 => %{\n                       0 => %{\n                         k: %{\n                           0 => %{0 => \"0\", 1 => \"Third\"},\n                           1 => %{0 => \"0\", 1 => \"Fourth\"},\n                           :kc => 2\n                         },\n                         s: 0\n                       },\n                       :r => 1,\n                       :s => 1\n                     }\n                   },\n                   :kc => 2\n                 },\n                 s: 2\n               },\n               :p => %{\n                 0 => [\"<li>\\n    Outside assign: \", \" Inside assign: \", \"\\n  </li>\"],\n                 1 => [\"<ul>\\n  \", \"\\n</ul>\"],\n                 2 => [\"\", \"\"],\n                 3 => [\"\", \"\"]\n               },\n               :s => 3\n             }\n\n      # change order of items\n      assigns =\n        Phoenix.Component.assign(assigns, :list_of_items, Enum.reverse(assigns.list_of_items))\n\n      {second_render, fingerprints, components} =\n        render(keyed_comprehension_with_component(assigns), fingerprints, components)\n\n      # only the order changed\n      assert second_render == %{0 => %{k: %{0 => 1, 1 => 0, :kc => 2}}}\n\n      # update count\n      assigns = Phoenix.Component.assign(%{assigns | __changed__: %{}}, :count, 1)\n\n      {third_render, _fingerprints, _components} =\n        render(keyed_comprehension_with_component(assigns), fingerprints, components)\n\n      # only sends the updated count\n      assert third_render == %{\n               0 => %{\n                 k: %{\n                   0 => %{0 => %{0 => %{k: %{0 => %{0 => \"1\"}, 1 => %{0 => \"1\"}, :kc => 2}}}},\n                   1 => %{0 => %{0 => %{k: %{0 => %{0 => \"1\"}, 1 => %{0 => \"1\"}, :kc => 2}}}},\n                   :kc => 2\n                 }\n               }\n             }\n    end\n\n    test \":key on component with slots\" do\n      items = [\n        %{id: 1, entries: [%{id: 1, title: \"1-1\"}, %{id: 2, title: \"1-2\"}]},\n        %{id: 2, entries: [%{id: 1, title: \"2-1\"}, %{id: 2, title: \"2-2\"}]}\n      ]\n\n      assigns = %{socket: %Socket{}, items: items, count: 0, __changed__: %{}}\n\n      {full_render, fingerprints, components} =\n        render(keyed_comprehension_with_component_and_slots(assigns))\n\n      assert full_render == %{\n               0 => %{\n                 k: %{\n                   0 => %{\n                     0 => %{\n                       0 => %{\n                         k: %{\n                           0 => %{0 => %{0 => \"1-1\", 1 => \"0\", :s => 0}},\n                           1 => %{0 => %{0 => \"1-2\", 1 => \"0\", :s => 0}},\n                           :kc => 2\n                         },\n                         s: 1\n                       },\n                       :r => 1,\n                       :s => 2\n                     }\n                   },\n                   1 => %{\n                     0 => %{\n                       0 => %{\n                         k: %{\n                           0 => %{0 => %{0 => \"2-1\", 1 => \"0\", :s => 0}},\n                           1 => %{0 => %{0 => \"2-2\", 1 => \"0\", :s => 0}},\n                           :kc => 2\n                         },\n                         s: 1\n                       },\n                       :r => 1,\n                       :s => 2\n                     }\n                   },\n                   :kc => 2\n                 },\n                 s: 3\n               },\n               :p => %{\n                 0 => [\"\\n    \", \" \", \"\\n  \"],\n                 1 => [\"<li>\", \"</li>\"],\n                 2 => [\"<ul>\\n  \", \"\\n</ul>\"],\n                 3 => [\"\", \"\"],\n                 4 => [\"\", \"\"]\n               },\n               :s => 4\n             }\n\n      # change order of items\n      assigns = Phoenix.Component.assign(assigns, :items, Enum.reverse(assigns.items))\n\n      {second_render, fingerprints, components} =\n        render(keyed_comprehension_with_component_and_slots(assigns), fingerprints, components)\n\n      # only the order changed\n      assert second_render == %{0 => %{k: %{0 => 1, 1 => 0, :kc => 2}}}\n\n      # update count\n      assigns = Phoenix.Component.assign(%{assigns | __changed__: %{}}, :count, 1)\n\n      {third_render, _fingerprints, _components} =\n        render(keyed_comprehension_with_component_and_slots(assigns), fingerprints, components)\n\n      # :for on slots is not optimized right now :(\n      assert third_render == %{\n               0 => %{\n                 k: %{\n                   0 => %{\n                     0 => %{\n                       0 => %{\n                         k: %{\n                           0 => %{0 => %{0 => \"2-1\", 1 => \"1\"}},\n                           1 => %{0 => %{0 => \"2-2\", 1 => \"1\"}},\n                           :kc => 2\n                         }\n                       }\n                     }\n                   },\n                   1 => %{\n                     0 => %{\n                       0 => %{\n                         k: %{\n                           0 => %{0 => %{0 => \"1-1\", 1 => \"1\"}},\n                           1 => %{0 => %{0 => \"1-2\", 1 => \"1\"}},\n                           :kc => 2\n                         }\n                       }\n                     }\n                   },\n                   :kc => 2\n                 }\n               }\n             }\n    end\n\n    defmodule LiveComponentForIssue3904 do\n      # https://github.com/phoenixframework/phoenix_live_view/pull/3904\n      use Phoenix.LiveComponent\n\n      def render(assigns) do\n        ~H\"\"\"\n        <div>\n          <div :for={i <- 1..assigns.count}>\n            Shared\n            <%= if i == 2 do %>\n              1 {2} 3\n            <% end %>\n            <%= if i > 0 do %>\n              Always\n            <% end %>\n          </div>\n        </div>\n        \"\"\"\n      end\n    end\n\n    test \"considers element rendered in shared live component trees as new\" do\n      assigns = %{}\n\n      template = ~H\"\"\"\n      <.live_component id={0} module={LiveComponentForIssue3904} count={1} />\n      <.live_component id={1} module={LiveComponentForIssue3904} count={2} />\n      \"\"\"\n\n      {full_render, _fingerprints, _components} = render(template)\n\n      assert full_render == %{\n               0 => 1,\n               1 => 2,\n               :c => %{\n                 1 => %{\n                   0 => %{\n                     p: %{0 => [\"\\n      Always\\n    \"]},\n                     s: [\"<div>\\n    Shared\\n    \", \"\\n    \", \"\\n  </div>\"],\n                     k: %{0 => %{0 => \"\", 1 => %{s: 0}}, :kc => 1}\n                   },\n                   :r => 1,\n                   :s => [\"<div>\\n  \", \"\\n</div>\"]\n                 },\n                 2 => %{\n                   0 => %{\n                     p: %{0 => [\"\\n      Always\\n    \"], 1 => [\"\\n      1 \", \" 3\\n    \"]},\n                     k: %{\n                       0 => %{0 => \"\", 1 => %{s: 0}},\n                       1 => %{0 => %{0 => \"2\", :s => 1}, 1 => %{s: 0}},\n                       :kc => 2\n                     }\n                   },\n                   :s => 1\n                 }\n               },\n               :p => %{0 => [\"\", \"\\n\", \"\"]},\n               :s => 0\n             }\n\n      assert rendered_to_binary(full_render) ==\n               \"<div>\\n  <div>\\n    Shared\\n    \\n    \\n      Always\\n    \\n  </div>\\n</div>\\n<div>\\n  <div>\\n    Shared\\n    \\n    \\n      Always\\n    \\n  </div><div>\\n    Shared\\n    \\n      1 2 3\\n    \\n    \\n      Always\\n    \\n  </div>\\n</div>\"\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/engine_test.exs",
    "content": "defmodule Phoenix.LiveView.EngineTest do\n  use ExUnit.Case, async: true\n\n  alias Phoenix.LiveView.{Engine, Rendered, Comprehension}\n\n  def safe(do: {:safe, _} = safe), do: safe\n  def unsafe(do: {:safe, content}), do: content\n\n  describe \"rendering\" do\n    test \"escapes HTML\" do\n      template = \"\"\"\n      <start> <%= \"<escaped>\" %>\n      \"\"\"\n\n      assert render(template) == \"<start> &lt;escaped&gt;\\n\"\n    end\n\n    test \"escapes HTML from nested content\" do\n      template = \"\"\"\n      <%= Phoenix.LiveView.EngineTest.unsafe do %>\n        <foo>\n      <% end %>\n      \"\"\"\n\n      assert render(template) == \"\\n  &lt;foo&gt;\\n\\n\"\n    end\n\n    test \"does not escape safe expressions\" do\n      assert render(\"Safe <%= {:safe, \\\"<value>\\\"} %>\") == \"Safe <value>\"\n    end\n\n    test \"nested content is always safe\" do\n      template = \"\"\"\n      <%= Phoenix.LiveView.EngineTest.safe do %>\n        <foo>\n      <% end %>\n      \"\"\"\n\n      assert render(template) == \"\\n  <foo>\\n\\n\"\n\n      template = \"\"\"\n      <%= Phoenix.LiveView.EngineTest.safe do %>\n        <%= \"<foo>\" %>\n      <% end %>\n      \"\"\"\n\n      assert render(template) == \"\\n  &lt;foo&gt;\\n\\n\"\n    end\n\n    test \"handles assigns\" do\n      assert render(\"<%= @foo %>\", %{foo: \"<hello>\"}) == \"&lt;hello&gt;\"\n    end\n\n    test \"supports non-output expressions\" do\n      template = \"\"\"\n      <% foo = @foo %>\n      <%= foo %>\n      \"\"\"\n\n      assert render(template, %{foo: \"<hello>\"}) == \"\\n&lt;hello&gt;\\n\"\n    end\n\n    test \"supports mixed non-output expressions\" do\n      template = \"\"\"\n      prea\n      <% @foo %>\n      posta\n      <%= @foo %>\n      preb\n      <% @foo %>\n      middleb\n      <% @foo %>\n      postb\n      \"\"\"\n\n      assert render(template, %{foo: \"<hello>\"}) ==\n               \"prea\\n\\nposta\\n&lt;hello&gt;\\npreb\\n\\nmiddleb\\n\\npostb\\n\"\n    end\n\n    test \"raises KeyError for missing assigns\" do\n      assert_raise KeyError, fn -> render(\"<%= @foo %>\", %{bar: true}) end\n    end\n  end\n\n  describe \"rendered structure\" do\n    test \"contains two static parts and one dynamic\" do\n      %{static: static, dynamic: dynamic} = eval(\"foo<%= 123 %>bar\")\n      assert dynamic.(true) == [\"123\"]\n      assert static == [\"foo\", \"bar\"]\n    end\n\n    test \"contains one static part at the beginning and one dynamic\" do\n      %{static: static, dynamic: dynamic} = eval(\"foo<%= 123 %>\")\n      assert dynamic.(true) == [\"123\"]\n      assert static == [\"foo\", \"\"]\n    end\n\n    test \"contains one static part at the end and one dynamic\" do\n      %{static: static, dynamic: dynamic} = eval(\"<%= 123 %>bar\")\n      assert dynamic.(true) == [\"123\"]\n      assert static == [\"\", \"bar\"]\n    end\n\n    test \"contains one dynamic only\" do\n      %{static: static, dynamic: dynamic} = eval(\"<%= 123 %>\")\n      assert dynamic.(true) == [\"123\"]\n      assert static == [\"\", \"\"]\n    end\n\n    test \"contains two dynamics only\" do\n      %{static: static, dynamic: dynamic} = eval(\"<%= 123 %><%= 456 %>\")\n      assert dynamic.(true) == [\"123\", \"456\"]\n      assert static == [\"\", \"\", \"\"]\n    end\n\n    test \"contains two static parts and two dynamics\" do\n      %{static: static, dynamic: dynamic} = eval(\"foo<%= 123 %><%= 456 %>bar\")\n      assert dynamic.(true) == [\"123\", \"456\"]\n      assert static == [\"foo\", \"\", \"bar\"]\n    end\n\n    test \"contains three static parts and two dynamics\" do\n      %{static: static, dynamic: dynamic} = eval(\"foo<%= 123 %>bar<%= 456 %>baz\")\n      assert dynamic.(true) == [\"123\", \"456\"]\n      assert static == [\"foo\", \"bar\", \"baz\"]\n    end\n\n    test \"contains optimized comprehensions\" do\n      template = \"\"\"\n      before\n      <%= for point <- @points do %>\n        x: <%= point.x %>\n        y: <%= point.y %>\n      <% end %>\n      after\n      \"\"\"\n\n      %{static: static, dynamic: dynamic} =\n        eval(template, %{points: [%{x: 1, y: 2}, %{x: 3, y: 4}]})\n\n      assert static == [\"before\\n\", \"\\nafter\\n\"]\n\n      assert [\n               %Phoenix.LiveView.Comprehension{\n                 entries: [\n                   {nil, %{point: %{x: 1, y: 2}}, _},\n                   {nil, %{point: %{x: 3, y: 4}}, _}\n                 ]\n               }\n             ] = dynamic.(true)\n    end\n  end\n\n  describe \"change tracking\" do\n    test \"does not render dynamic if it is unchanged\" do\n      template = \"<%= @foo %>\"\n      assert changed(template, %{foo: 123}, nil) == [\"123\"]\n      assert changed(template, %{foo: 123}, %{}) == [nil]\n      assert changed(template, %{foo: 123}, %{foo: true}) == [\"123\"]\n    end\n\n    test \"does not render dynamic if it is unchanged via assigns dot\" do\n      template = \"<%= assigns.foo %>\"\n      assert changed(template, %{foo: 123}, nil) == [\"123\"]\n      assert changed(template, %{foo: 123}, %{}) == [nil]\n      assert changed(template, %{foo: 123}, %{foo: true}) == [\"123\"]\n    end\n\n    test \"does not render dynamic if it is unchanged via assigns access\" do\n      template = \"<%= assigns[:foo] %>\"\n      assert changed(template, %{foo: 123}, nil) == [\"123\"]\n      assert changed(template, %{foo: 123}, %{}) == [nil]\n      assert changed(template, %{foo: 123}, %{foo: true}) == [\"123\"]\n      assert changed(template, %{}, %{}) == [nil]\n      assert changed(template, %{}, %{foo: true}) == [\"\"]\n      assert changed(template, %{}, %{foo: true}) == [\"\"]\n\n      template = \"<%= Access.get(assigns, :foo) %>\"\n      assert changed(template, %{foo: 123}, nil) == [\"123\"]\n      assert changed(template, %{foo: 123}, %{}) == [nil]\n      assert changed(template, %{foo: 123}, %{foo: true}) == [\"123\"]\n    end\n\n    test \"renders dynamic if any of the assigns change\" do\n      template = \"<%= @foo + @bar %>\"\n      assert changed(template, %{foo: 123, bar: 456}, nil) == [\"579\"]\n      assert changed(template, %{foo: 123, bar: 456}, %{}) == [nil]\n      assert changed(template, %{foo: 123, bar: 456}, %{foo: true}) == [\"579\"]\n      assert changed(template, %{foo: 123, bar: 456}, %{bar: true}) == [\"579\"]\n    end\n\n    test \"does not render dynamic without assigns\" do\n      template = \"<%= 1 + 2 %>\"\n      assert changed(template, %{}, nil) == [\"3\"]\n      assert changed(template, %{}, %{}) == [nil]\n    end\n\n    test \"does not render dynamic on bitstring modifiers\" do\n      template = \"<%= <<@foo::binary>> %>\"\n      assert changed(template, %{foo: \"123\"}, nil) == [\"123\"]\n      assert changed(template, %{foo: \"123\"}, %{}) == [nil]\n      assert changed(template, %{foo: \"123\"}, %{foo: true}) == [\"123\"]\n    end\n\n    test \"renders dynamic without change tracking\" do\n      assert changed(\"<%= @foo %>\", %{foo: 123}, %{foo: true}, false) == [\"123\"]\n      assert changed(\"<%= 1 + 2 %>\", %{foo: 123}, %{}, false) == [\"3\"]\n    end\n\n    test \"renders dynamic does not change track underscore\" do\n      assert changed(\"<%= _ = 123 %>\", %{}, nil) == [\"123\"]\n      assert changed(\"<%= _ = 123 %>\", %{}, %{}) == [nil]\n    end\n\n    test \"renders dynamic with dot tracking\" do\n      template = \"<%= @map.foo + @map.bar %>\"\n      old = %{map: %{foo: 123, bar: 456}}\n      new_augmented = %{map: %{foo: 123, bar: 456, baz: 789}}\n      new_changed_foo = %{map: %{foo: 321, bar: 456}}\n      new_changed_bar = %{map: %{foo: 123, bar: 654}}\n      assert changed(template, old, nil) == [\"579\"]\n      assert changed(template, old, %{}) == [nil]\n      assert changed(template, old, %{map: true}) == [\"579\"]\n      assert changed(template, old, old) == [nil]\n      assert changed(template, new_augmented, old) == [nil]\n      assert changed(template, new_changed_foo, old) == [\"777\"]\n      assert changed(template, new_changed_bar, old) == [\"777\"]\n    end\n\n    test \"renders dynamic with dot tracking 3-levels deeps\" do\n      template = \"<%= @root.map.foo + @root.map.bar %>\"\n      old = %{root: %{map: %{foo: 123, bar: 456}}}\n      new_augmented = %{root: %{map: %{foo: 123, bar: 456, baz: 789}}}\n      new_changed_foo = %{root: %{map: %{foo: 321, bar: 456}}}\n      new_changed_bar = %{root: %{map: %{foo: 123, bar: 654}}}\n      assert changed(template, old, nil) == [\"579\"]\n      assert changed(template, old, %{}) == [nil]\n      assert changed(template, old, %{root: true}) == [\"579\"]\n      assert changed(template, old, %{root: %{map: true}}) == [\"579\"]\n      assert changed(template, old, old) == [nil]\n      assert changed(template, new_augmented, old) == [nil]\n      assert changed(template, new_changed_foo, old) == [\"777\"]\n      assert changed(template, new_changed_bar, old) == [\"777\"]\n    end\n\n    test \"renders dynamic with access tracking\" do\n      template = \"<%= @not_map[:foo] + @not_map[:bar] %>\"\n      old = %{not_map: [foo: 123, bar: 456]}\n      new_augmented = %{not_map: [foo: 123, bar: 456, baz: 789]}\n      new_changed_foo = %{not_map: [foo: 321, bar: 456]}\n      new_changed_bar = %{not_map: [foo: 123, bar: 654]}\n      assert changed(template, old, nil) == [\"579\"]\n      assert changed(template, old, %{}) == [nil]\n      assert changed(template, old, %{not_map: true}) == [\"579\"]\n      assert changed(template, old, old) == [\"579\"]\n      assert changed(template, new_augmented, old) == [\"579\"]\n      assert changed(template, new_changed_foo, old) == [\"777\"]\n      assert changed(template, new_changed_bar, old) == [\"777\"]\n\n      template = \"<%= @map[:foo] + @map[:bar] %>\"\n      old = %{map: %{foo: 123, bar: 456}}\n      new_augmented = %{map: %{foo: 123, bar: 456, baz: 789}}\n      new_changed_foo = %{map: %{foo: 321, bar: 456}}\n      new_changed_bar = %{map: %{foo: 123, bar: 654}}\n      assert changed(template, old, nil) == [\"579\"]\n      assert changed(template, old, %{}) == [nil]\n      assert changed(template, old, %{map: true}) == [\"579\"]\n      assert changed(template, new_augmented, old) == [nil]\n      assert changed(template, new_changed_foo, old) == [\"777\"]\n      assert changed(template, new_changed_bar, old) == [\"777\"]\n    end\n\n    test \"map access with non existing key\" do\n      template = \"<%= @map[:baz] || \\\"default\\\" %>\"\n      old = %{map: %{foo: 123, bar: 456}}\n      new_augmented = %{map: %{foo: 123, bar: 456, baz: 789}}\n      new_changed_foo = %{map: %{foo: 321, bar: 456}}\n      new_changed_bar = %{map: %{foo: 123, bar: 654}}\n      assert changed(template, old, nil) == [\"default\"]\n      assert changed(template, old, %{}) == [nil]\n      assert changed(template, old, %{map: true}) == [\"default\"]\n      assert changed(template, new_augmented, old) == [\"789\"]\n      # no re-render when the key is still not present\n      assert changed(template, new_changed_foo, old) == [nil]\n      assert changed(template, new_changed_bar, old) == [nil]\n    end\n\n    test \"renders dynamic with access tracking for forms\" do\n      form1 = Phoenix.Component.to_form(%{\"foo\" => \"bar\"})\n      form2 = Phoenix.Component.to_form(%{\"foo\" => \"bar\", \"baz\" => \"bat\"})\n      form3 = Phoenix.Component.to_form(%{\"foo\" => \"baz\"})\n\n      template = \"<%= Map.fetch!(@form[:foo], :value) %>\"\n      assert changed(template, %{form: form1}, nil) == [\"bar\"]\n\n      template = \"<%= Map.fetch!(@form[:foo], :value) %>\"\n      assert changed(template, %{form: form1}, %{}) == [nil]\n      assert changed(template, %{form: form1}, %{form: form1}) == [nil]\n      assert changed(template, %{form: form2}, %{form: form1}) == [nil]\n      assert changed(template, %{form: form3}, %{form: form1}) == [\"baz\"]\n    end\n\n    test \"handles _unused_ parameter changing for forms\" do\n      form1 = Phoenix.Component.to_form(%{\"foo\" => \"bar\", \"_unused_foo\" => \"\"})\n      form2 = Phoenix.Component.to_form(%{\"foo\" => \"bar\"})\n\n      template = \"<%= Map.fetch!(@form[:foo], :value) %>\"\n      assert changed(template, %{form: form1}, nil) == [\"bar\"]\n\n      template = \"<%= Map.fetch!(@form[:foo], :value) %>\"\n      assert changed(template, %{form: form1}, %{}) == [nil]\n      assert changed(template, %{form: form1}, %{form: form1}) == [nil]\n      assert changed(template, %{form: form2}, %{form: form1}) == [\"bar\"]\n    end\n\n    test \"renders dynamic with access tracking inside comprehension\" do\n      template = \"\"\"\n      <%= for x <- [:a, :b, :c] do %>\n        <%= @map[x] %>\n      <% end %>\n      \"\"\"\n\n      old = %{map: [a: 1, b: 2, c: 3]}\n      assert [%Phoenix.LiveView.Comprehension{}] = changed(template, old, nil)\n      assert [nil] = changed(template, old, %{})\n      assert [%Phoenix.LiveView.Comprehension{}] = changed(template, old, %{map: true})\n      assert [%Phoenix.LiveView.Comprehension{}] = changed(template, old, old)\n    end\n\n    test \"renders dynamic if it has variables\" do\n      template = \"<%= foo = 1 + 2 %><%= foo %>\"\n      assert changed(template, %{}, nil) == [\"3\", \"3\"]\n      assert changed(template, %{}, %{}) == [\"3\", \"3\"]\n    end\n\n    test \"does not render dynamic if it has variables on the right side of the pipe\" do\n      template = \"<%= @foo |> Kernel.+(@bar) |> is_integer %>\"\n      assert changed(template, %{foo: 1, bar: 2}, nil) == [\"true\"]\n      assert changed(template, %{foo: 1, bar: 2}, %{}) == [nil]\n      assert changed(template, %{foo: 1, bar: 2}, %{foo: true}) == [\"true\"]\n      assert changed(template, %{foo: 1, bar: 2}, %{bar: true}) == [\"true\"]\n\n      template = \"<%= @foo |> is_integer |> is_boolean %>\"\n      assert changed(template, %{foo: 1}, nil) == [\"true\"]\n      assert changed(template, %{foo: 1}, %{}) == [nil]\n      assert changed(template, %{foo: 1}, %{foo: true}) == [\"true\"]\n    end\n\n    test \"does not render dynamic for special variables\" do\n      template = \"<%= __MODULE__ %>\"\n      assert changed(template, %{}, nil) == [\"\"]\n      assert changed(template, %{}, %{}) == [nil]\n    end\n\n    test \"renders dynamic if it has variables from assigns\" do\n      template = \"<%= foo = @foo %><%= foo %>\"\n      assert changed(template, %{foo: 123}, nil) == [\"123\", \"123\"]\n      assert changed(template, %{foo: 123}, %{}) == [\"123\", \"123\"]\n      assert changed(template, %{foo: 123}, %{foo: true}) == [\"123\", \"123\"]\n    end\n\n    test \"does not render dynamic if it has variables inside special form\" do\n      template = \"<%= cond do foo = @foo -> foo end %>\"\n      assert changed(template, %{foo: 123}, nil) == [\"123\"]\n      assert changed(template, %{foo: 123}, %{}) == [nil]\n      assert changed(template, %{foo: 123}, %{foo: true}) == [\"123\"]\n    end\n\n    test \"renders dynamic if it has variables from outside inside special form\" do\n      template = \"<% f = @foo %><%= cond do foo = f -> foo end %>\"\n      assert changed(template, %{foo: 123}, nil) == [\"123\"]\n      assert changed(template, %{foo: 123}, %{}) == [\"123\"]\n      assert changed(template, %{foo: 123}, %{foo: true}) == [\"123\"]\n    end\n\n    test \"does not render dynamic if it has variables inside unoptimized comprehension\" do\n      template = \"<%= for foo <- @foo, do: foo %>\"\n      assert changed(template, %{foo: [1, 2, 3]}, nil) == [[1, 2, 3]]\n      assert changed(template, %{foo: [1, 2, 3]}, %{}) == [nil]\n      assert changed(template, %{foo: [1, 2, 3]}, %{foo: true}) == [[1, 2, 3]]\n    end\n\n    test \"does not render dynamic if it has variables as comprehension generators\" do\n      template = \"<%= for x <- foo do %><%= x %><% end %>\"\n\n      rendered = eval(template, %{__changed__: nil}, foo: [1, 2, 3])\n      assert [%{entries: [[\"1\"], [\"2\"], [\"3\"]]}] = expand_dynamic(rendered.dynamic, true)\n\n      rendered = eval(template, %{__changed__: %{}}, foo: [1, 2, 3])\n      assert [%{entries: [[\"1\"], [\"2\"], [\"3\"]]}] = expand_dynamic(rendered.dynamic, true)\n    end\n\n    test \"does not render dynamic if it has variables inside optimized comprehension\" do\n      template = \"<%= for foo <- @foo do %><%= foo %><% end %>\"\n\n      assert [%{entries: [[\"1\"], [\"2\"], [\"3\"]]}] = changed(template, %{foo: [\"1\", \"2\", \"3\"]}, nil)\n\n      assert [nil] = changed(template, %{foo: [\"1\", \"2\", \"3\"]}, %{})\n\n      assert [%{entries: [[\"1\"], [\"2\"], [\"3\"]]}] =\n               changed(template, %{foo: [\"1\", \"2\", \"3\"]}, %{foo: true})\n    end\n\n    test \"does not render dynamic if it has a variable after a condition inside optimized comprehension\" do\n      template =\n        \"<%= for foo <- @foo do %><%= if foo == @selected, do: ~s(selected) %><%= foo %><% end %>\"\n\n      assert [%{entries: [[\"\", \"1\"], [\"selected\", \"2\"], [\"\", \"3\"]]}] =\n               changed(template, %{foo: [\"1\", \"2\", \"3\"], selected: \"2\"}, nil)\n\n      assert [nil] = changed(template, %{foo: [\"1\", \"2\", \"3\"], selected: \"2\"}, %{})\n\n      assert [%{entries: [[\"\", \"1\"], [\"selected\", \"2\"], [\"\", \"3\"]]}] =\n               changed(template, %{foo: [\"1\", \"2\", \"3\"], selected: \"2\"}, %{foo: true})\n    end\n\n    test \"does not render dynamic for nested optimized comprehensions with variables\" do\n      template =\n        \"<%= for x <- @foo do %>X: <%= for y <- @bar do %>Y: <%= x %><%= y %><% end %><% end %>\"\n\n      assert [\n               %{\n                 entries: [\n                   [%{entries: [[\"1\", \"1\"]], static: [\"Y: \", \"\", \"\"]}]\n                 ],\n                 static: [\"X: \", \"\"]\n               }\n             ] = changed(template, %{foo: [1], bar: [1]}, nil)\n\n      assert [nil] = changed(template, %{foo: [1], bar: [1]}, %{})\n\n      assert [\n               %{\n                 entries: [\n                   [%{entries: [[\"1\", \"1\"]], static: [\"Y: \", \"\", \"\"]}]\n                 ],\n                 static: [\"X: \", \"\"]\n               }\n             ] = changed(template, %{foo: [1], bar: [1]}, %{foo: true, bar: true})\n    end\n\n    test \"renders dynamics for nested comprehensions\" do\n      template =\n        \"<%= for foo <- @foo do %><%= for bar <- foo.bar do %><%= foo.x %><%= bar.y %><% end %><% end %>\"\n\n      assert [\n               %{\n                 entries: [\n                   [%{entries: [[\"1\", \"1\"]], static: [\"\", \"\", \"\"]}]\n                 ],\n                 static: [\"\", \"\"]\n               }\n             ] = changed(template, %{foo: [%{x: 1, bar: [%{y: 1}]}]}, %{foo: true})\n    end\n\n    test \"renders dynamic if it uses assigns directly\" do\n      template = \"<%= for _ <- [1, 2, 3], do: Map.get(assigns, :foo) %>\"\n      assert changed(template, %{foo: \"a\"}, nil) == [[\"a\", \"a\", \"a\"]]\n      assert changed(template, %{foo: \"a\"}, %{}) == [[\"a\", \"a\", \"a\"]]\n      assert changed(template, %{foo: \"a\"}, %{foo: true}) == [[\"a\", \"a\", \"a\"]]\n    end\n  end\n\n  describe \"if\" do\n    test \"converts if-do into rendered\" do\n      template = \"<%= if true do %>one<%= @foo %>two<% end %>\"\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: 123}, nil)\n\n      assert changed(template, %{foo: 123}, %{}) ==\n               [nil]\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: 123}, %{foo: true})\n    end\n\n    test \"converts if-do into rendered with dynamic condition\" do\n      template = \"<%= if @bar do %>one<%= @foo %>two<% end %>\"\n\n      # bar = true\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: 123, bar: true}, nil)\n\n      assert changed(template, %{foo: 123, bar: true}, %{}) ==\n               [nil]\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: 123, bar: true}, %{foo: true})\n\n      assert [%Rendered{dynamic: [nil], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: 123, bar: true}, %{bar: true})\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: 123, bar: true}, %{foo: true, bar: true})\n\n      # bar = false\n      assert [\"\"] = changed(template, %{foo: 123, bar: false}, nil)\n\n      assert changed(template, %{foo: 123, bar: false}, %{}) ==\n               [nil]\n\n      assert changed(template, %{foo: 123, bar: false}, %{bar: true}) ==\n               [\"\"]\n    end\n\n    test \"converts if-do with var assignments into rendered\" do\n      template = \"<%= if var = @foo do %>one<%= var %>two<% end %>\"\n\n      assert [%Rendered{dynamic: [\"true\"], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: true}, nil)\n\n      assert changed(template, %{foo: true}, %{}) == [nil]\n      assert changed(template, %{foo: false}, %{foo: true}) == [\"\"]\n    end\n\n    test \"converts if-do with external var assignments into rendered but tainted\" do\n      template = \"<%= var = @foo %><%= if var do %>one<%= var %>two<% end %>\"\n\n      assert [\"true\", %Rendered{dynamic: [\"true\"], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: true}, nil)\n\n      assert [\"true\", %Rendered{dynamic: [\"true\"], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: true}, %{})\n\n      assert [\"false\", \"\"] =\n               changed(template, %{foo: false}, %{foo: true})\n    end\n\n    test \"converts if-do-else into rendered with dynamic condition\" do\n      template = \"<%= if @bar do %>one<%= @foo %>two<% else %>uno<%= @baz %>dos<% end %>\"\n\n      # bar = true\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"], fingerprint: fptrue}] =\n               changed(template, %{foo: 123, bar: true, baz: 456}, nil)\n\n      assert [nil] = changed(template, %{foo: 123, bar: true, baz: 456}, %{})\n\n      assert [%Rendered{dynamic: [nil], static: [\"one\", \"two\"], fingerprint: ^fptrue}] =\n               changed(template, %{foo: 123, bar: true, baz: 456}, %{bar: true, baz: true})\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"], fingerprint: ^fptrue}] =\n               changed(template, %{foo: 123, bar: true, baz: 456}, %{foo: true})\n\n      assert [%Rendered{dynamic: [nil], static: [\"one\", \"two\"], fingerprint: ^fptrue}] =\n               changed(template, %{foo: 123, bar: true, baz: 456}, %{bar: true})\n\n      assert [%Rendered{dynamic: [nil], static: [\"one\", \"two\"], fingerprint: ^fptrue}] =\n               changed(template, %{foo: 123, bar: true, baz: 456}, %{baz: true})\n\n      # bar = false\n      assert [%Rendered{dynamic: [\"456\"], static: [\"uno\", \"dos\"], fingerprint: fpfalse}] =\n               changed(template, %{foo: 123, bar: false, baz: 456}, nil)\n\n      assert [nil] = changed(template, %{foo: 123, bar: false, baz: 456}, %{})\n\n      assert [%Rendered{dynamic: [nil], static: [\"uno\", \"dos\"], fingerprint: ^fpfalse}] =\n               changed(template, %{foo: 123, bar: false, baz: 456}, %{foo: true, bar: true})\n\n      assert [%Rendered{dynamic: [nil], static: [\"uno\", \"dos\"], fingerprint: ^fpfalse}] =\n               changed(template, %{foo: 123, bar: false, baz: 456}, %{foo: true})\n\n      assert [%Rendered{dynamic: [nil], static: [\"uno\", \"dos\"], fingerprint: ^fpfalse}] =\n               changed(template, %{foo: 123, bar: false, baz: 456}, %{bar: true})\n\n      assert [%Rendered{dynamic: [\"456\"], static: [\"uno\", \"dos\"], fingerprint: ^fpfalse}] =\n               changed(template, %{foo: 123, bar: false, baz: 456}, %{baz: true})\n\n      assert fptrue != fpfalse\n    end\n\n    test \"converts if-do if-do into rendered\" do\n      template = \"<%= if true do %>one<%= if true do %>uno<%= @foo %>dos<% end %>two<% end %>\"\n\n      assert [\n               %Rendered{\n                 dynamic: [%Rendered{dynamic: [\"123\"], static: [\"uno\", \"dos\"]}],\n                 static: [\"one\", \"two\"]\n               }\n             ] = changed(template, %{foo: 123}, nil)\n\n      assert changed(template, %{foo: 123}, %{}) ==\n               [nil]\n\n      assert [\n               %Rendered{\n                 dynamic: [%Rendered{dynamic: [\"123\"], static: [\"uno\", \"dos\"]}],\n                 static: [\"one\", \"two\"]\n               }\n             ] = changed(template, %{foo: 123}, %{foo: true})\n    end\n\n    test \"converts if-do if-do with var assignment into rendered\" do\n      template = \"<%= if var = @foo do %>one<%= if var do %>uno<%= var %>dos<% end %>two<% end %>\"\n\n      assert [\n               %Rendered{\n                 dynamic: [%Rendered{dynamic: [\"123\"], static: [\"uno\", \"dos\"]}],\n                 static: [\"one\", \"two\"]\n               }\n             ] = changed(template, %{foo: 123}, nil)\n\n      assert changed(template, %{foo: 123}, %{}) ==\n               [nil]\n\n      assert [\n               %Rendered{\n                 dynamic: [%Rendered{dynamic: [\"123\"], static: [\"uno\", \"dos\"]}],\n                 static: [\"one\", \"two\"]\n               }\n             ] = changed(template, %{foo: 123}, %{foo: true})\n    end\n\n    test \"does not convert if-do-else in the wrong format\" do\n      template = \"<%= if @bar do @foo else @baz end %>\"\n\n      assert changed(template, %{foo: 123, bar: true, baz: 456}, nil) == [\"123\"]\n      assert changed(template, %{foo: 123, bar: true, baz: 456}, %{}) == [nil]\n      assert changed(template, %{foo: 123, bar: true, baz: 456}, %{bar: true}) == [\"123\"]\n      assert changed(template, %{foo: 123, bar: true, baz: 456}, %{foo: true}) == [\"123\"]\n      assert changed(template, %{foo: 123, bar: true, baz: 456}, %{baz: true}) == [\"123\"]\n\n      assert changed(template, %{foo: 123, bar: false, baz: 456}, nil) == [\"456\"]\n      assert changed(template, %{foo: 123, bar: false, baz: 456}, %{}) == [nil]\n      assert changed(template, %{foo: 123, bar: false, baz: 456}, %{bar: true}) == [\"456\"]\n      assert changed(template, %{foo: 123, bar: false, baz: 456}, %{foo: true}) == [\"456\"]\n      assert changed(template, %{foo: 123, bar: false, baz: 456}, %{baz: true}) == [\"456\"]\n    end\n\n    test \"converts unless-do into rendered\" do\n      template = \"<%= unless false do %>one<%= @foo %>two<% end %>\"\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: 123}, nil)\n\n      assert changed(template, %{foo: 123}, %{}) ==\n               [nil]\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: 123}, %{foo: true})\n    end\n  end\n\n  describe \"case\" do\n    test \"converts case into rendered\" do\n      template = \"<%= case true do %><% true -> %>one<%= @foo %>two<% end %>\"\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: 123}, nil)\n\n      assert changed(template, %{foo: 123}, %{}) ==\n               [nil]\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: 123}, %{foo: true})\n    end\n\n    test \"converts case into rendered with vars in head\" do\n      template = \"<%= case true do %><% x when x == true -> %>one<%= @foo %>two<% end %>\"\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: 123}, nil)\n\n      assert changed(template, %{foo: 123}, %{}) ==\n               [nil]\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: 123}, %{foo: true})\n\n      template = \"<%= case @foo do %><% x -> %>one<%= x %>two<% end %>\"\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: 123}, nil)\n\n      assert changed(template, %{foo: 123}, %{}) ==\n               [nil]\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: 123}, %{foo: true})\n    end\n\n    test \"converts case into rendered with vars in head and body\" do\n      template = \"<%= case 456 do %><% x -> %>one<%= @foo %>two<%= x %>three<% end %>\"\n\n      assert [%Rendered{dynamic: [\"123\", \"456\"], static: [\"one\", \"two\", \"three\"]}] =\n               changed(template, %{foo: 123}, nil)\n\n      assert changed(template, %{foo: 123}, %{}) ==\n               [nil]\n\n      assert [%Rendered{dynamic: [\"123\", \"456\"], static: [\"one\", \"two\", \"three\"]}] =\n               changed(template, %{foo: 123}, %{foo: true})\n\n      template = \"<%= case @bar do %><% x -> %>one<%= @foo %>two<%= x %>three<% end %>\"\n\n      assert [%Rendered{dynamic: [\"123\", \"456\"], static: [\"one\", \"two\", \"three\"]}] =\n               changed(template, %{foo: 123, bar: 456}, nil)\n\n      assert changed(template, %{foo: 123, bar: 456}, %{}) ==\n               [nil]\n\n      assert [%Rendered{dynamic: [\"123\", \"456\"], static: [\"one\", \"two\", \"three\"]}] =\n               changed(template, %{foo: 123, bar: 456}, %{foo: true})\n\n      assert [%Rendered{dynamic: [nil, \"456\"], static: [\"one\", \"two\", \"three\"]}] =\n               changed(template, %{foo: 123, bar: 456}, %{bar: true})\n    end\n\n    test \"converts multiple case into rendered with dynamic condition\" do\n      template =\n        \"<%= case @bar do %><% true -> %>one<%= @foo %>two<% false -> %>uno<%= @baz %>dos<% end %>\"\n\n      # bar = true\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"], fingerprint: fptrue}] =\n               changed(template, %{foo: 123, bar: true, baz: 456}, nil)\n\n      assert [nil] = changed(template, %{foo: 123, bar: true, baz: 456}, %{})\n\n      assert [%Rendered{dynamic: [nil], static: [\"one\", \"two\"], fingerprint: ^fptrue}] =\n               changed(template, %{foo: 123, bar: true, baz: 456}, %{bar: true, baz: true})\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"], fingerprint: ^fptrue}] =\n               changed(template, %{foo: 123, bar: true, baz: 456}, %{foo: true})\n\n      assert [%Rendered{dynamic: [nil], static: [\"one\", \"two\"], fingerprint: ^fptrue}] =\n               changed(template, %{foo: 123, bar: true, baz: 456}, %{bar: true})\n\n      assert [%Rendered{dynamic: [nil], static: [\"one\", \"two\"], fingerprint: ^fptrue}] =\n               changed(template, %{foo: 123, bar: true, baz: 456}, %{baz: true})\n\n      # bar = false\n      assert [%Rendered{dynamic: [\"456\"], static: [\"uno\", \"dos\"], fingerprint: fpfalse}] =\n               changed(template, %{foo: 123, bar: false, baz: 456}, nil)\n\n      assert [nil] = changed(template, %{foo: 123, bar: false, baz: 456}, %{})\n\n      assert [%Rendered{dynamic: [nil], static: [\"uno\", \"dos\"], fingerprint: ^fpfalse}] =\n               changed(template, %{foo: 123, bar: false, baz: 456}, %{foo: true, bar: true})\n\n      assert [%Rendered{dynamic: [nil], static: [\"uno\", \"dos\"], fingerprint: ^fpfalse}] =\n               changed(template, %{foo: 123, bar: false, baz: 456}, %{foo: true})\n\n      assert [%Rendered{dynamic: [nil], static: [\"uno\", \"dos\"], fingerprint: ^fpfalse}] =\n               changed(template, %{foo: 123, bar: false, baz: 456}, %{bar: true})\n\n      assert [%Rendered{dynamic: [\"456\"], static: [\"uno\", \"dos\"], fingerprint: ^fpfalse}] =\n               changed(template, %{foo: 123, bar: false, baz: 456}, %{baz: true})\n\n      assert fptrue != fpfalse\n    end\n\n    test \"converts nested case into rendered\" do\n      template =\n        \"<%= case true do %><% true -> %>one<%= case true do %><% true -> %>uno<%= @foo %>dos<% end %>two<% end %>\"\n\n      assert [\n               %Rendered{\n                 dynamic: [%Rendered{dynamic: [\"123\"], static: [\"uno\", \"dos\"]}],\n                 static: [\"one\", \"two\"]\n               }\n             ] = changed(template, %{foo: 123}, nil)\n\n      assert changed(template, %{foo: 123}, %{}) ==\n               [nil]\n\n      assert [\n               %Rendered{\n                 dynamic: [%Rendered{dynamic: [\"123\"], static: [\"uno\", \"dos\"]}],\n                 static: [\"one\", \"two\"]\n               }\n             ] = changed(template, %{foo: 123}, %{foo: true})\n    end\n\n    test \"converts case with for into rendered\" do\n      template = \"<%= case @foo do %><% val -> %><%= for i <- val do %><%= i %><% end %><% end %>\"\n\n      assert [\n               %Phoenix.LiveView.Rendered{\n                 dynamic: [\n                   %Phoenix.LiveView.Comprehension{\n                     static: [\"\", \"\"],\n                     entries: [[\"1\"], [\"2\"], [\"3\"]]\n                   }\n                 ],\n                 static: [\"\", \"\"]\n               }\n             ] = changed(template, %{foo: 1..3}, nil)\n\n      assert changed(template, %{foo: 1..3}, %{}) ==\n               [nil]\n\n      assert [\n               %Phoenix.LiveView.Rendered{\n                 dynamic: [\n                   %Phoenix.LiveView.Comprehension{\n                     static: [\"\", \"\"],\n                     entries: [[\"1\"], [\"2\"], [\"3\"]]\n                   }\n                 ],\n                 static: [\"\", \"\"]\n               }\n             ] = changed(template, %{foo: 1..3}, %{foo: true})\n    end\n\n    test \"does not convert cases in the wrong format\" do\n      template = \"<%= case @bar do true -> @foo; false -> @baz end %>\"\n\n      assert changed(template, %{foo: 123, bar: true, baz: 456}, nil) == [\"123\"]\n      assert changed(template, %{foo: 123, bar: true, baz: 456}, %{}) == [nil]\n      assert changed(template, %{foo: 123, bar: true, baz: 456}, %{bar: true}) == [\"123\"]\n      assert changed(template, %{foo: 123, bar: true, baz: 456}, %{foo: true}) == [\"123\"]\n      assert changed(template, %{foo: 123, bar: true, baz: 456}, %{baz: true}) == [\"123\"]\n\n      assert changed(template, %{foo: 123, bar: false, baz: 456}, nil) == [\"456\"]\n      assert changed(template, %{foo: 123, bar: false, baz: 456}, %{}) == [nil]\n      assert changed(template, %{foo: 123, bar: false, baz: 456}, %{bar: true}) == [\"456\"]\n      assert changed(template, %{foo: 123, bar: false, baz: 456}, %{foo: true}) == [\"456\"]\n      assert changed(template, %{foo: 123, bar: false, baz: 456}, %{baz: true}) == [\"456\"]\n    end\n  end\n\n  describe \"cond\" do\n    test \"converts cond into rendered\" do\n      template = \"<%= cond do %><% true -> %>one<%= @foo %>two<% end %>\"\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: 123}, nil)\n\n      assert changed(template, %{foo: 123}, %{}) ==\n               [nil]\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"]}] =\n               changed(template, %{foo: 123}, %{foo: true})\n    end\n\n    test \"converts multiple cond into rendered with dynamic condition\" do\n      template =\n        \"<%= cond do %><% @bar -> %>one<%= @foo %>two<% true -> %>uno<%= @baz %>dos<% end %>\"\n\n      # bar = true\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"], fingerprint: fptrue}] =\n               changed(template, %{foo: 123, bar: true, baz: 456}, nil)\n\n      assert [nil] = changed(template, %{foo: 123, bar: true, baz: 456}, %{})\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"], fingerprint: ^fptrue}] =\n               changed(template, %{foo: 123, bar: true, baz: 456}, %{bar: true, baz: true})\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"], fingerprint: ^fptrue}] =\n               changed(template, %{foo: 123, bar: true, baz: 456}, %{foo: true})\n\n      assert [%Rendered{dynamic: [\"123\"], static: [\"one\", \"two\"], fingerprint: ^fptrue}] =\n               changed(template, %{foo: 123, bar: true, baz: 456}, %{bar: true})\n\n      assert [%Rendered{dynamic: [nil], static: [\"one\", \"two\"], fingerprint: ^fptrue}] =\n               changed(template, %{foo: 123, bar: true, baz: 456}, %{baz: true})\n\n      # bar = false\n      assert [%Rendered{dynamic: [\"456\"], static: [\"uno\", \"dos\"], fingerprint: fpfalse}] =\n               changed(template, %{foo: 123, bar: false, baz: 456}, nil)\n\n      assert [nil] = changed(template, %{foo: 123, bar: false, baz: 456}, %{})\n\n      assert [%Rendered{dynamic: [nil], static: [\"uno\", \"dos\"], fingerprint: ^fpfalse}] =\n               changed(template, %{foo: 123, bar: false, baz: 456}, %{foo: true, bar: true})\n\n      assert [%Rendered{dynamic: [nil], static: [\"uno\", \"dos\"], fingerprint: ^fpfalse}] =\n               changed(template, %{foo: 123, bar: false, baz: 456}, %{foo: true})\n\n      assert [%Rendered{dynamic: [nil], static: [\"uno\", \"dos\"], fingerprint: ^fpfalse}] =\n               changed(template, %{foo: 123, bar: false, baz: 456}, %{bar: true})\n\n      assert [%Rendered{dynamic: [\"456\"], static: [\"uno\", \"dos\"], fingerprint: ^fpfalse}] =\n               changed(template, %{foo: 123, bar: false, baz: 456}, %{baz: true})\n\n      assert fptrue != fpfalse\n    end\n\n    test \"converts nested cond into rendered\" do\n      template =\n        \"<%= cond do %><% true -> %>one<%= cond do %><% true -> %>uno<%= @foo %>dos<% end %>two<% end %>\"\n\n      assert [\n               %Rendered{\n                 dynamic: [%Rendered{dynamic: [\"123\"], static: [\"uno\", \"dos\"]}],\n                 static: [\"one\", \"two\"]\n               }\n             ] = changed(template, %{foo: 123}, nil)\n\n      assert changed(template, %{foo: 123}, %{}) ==\n               [nil]\n\n      assert [\n               %Rendered{\n                 dynamic: [%Rendered{dynamic: [\"123\"], static: [\"uno\", \"dos\"]}],\n                 static: [\"one\", \"two\"]\n               }\n             ] = changed(template, %{foo: 123}, %{foo: true})\n    end\n\n    test \"does not convert conds in the wrong format\" do\n      template = \"<%= cond do @bar -> @foo; true -> @baz end %>\"\n\n      assert changed(template, %{foo: 123, bar: true, baz: 456}, nil) == [\"123\"]\n      assert changed(template, %{foo: 123, bar: true, baz: 456}, %{}) == [nil]\n      assert changed(template, %{foo: 123, bar: true, baz: 456}, %{bar: true}) == [\"123\"]\n      assert changed(template, %{foo: 123, bar: true, baz: 456}, %{foo: true}) == [\"123\"]\n      assert changed(template, %{foo: 123, bar: true, baz: 456}, %{baz: true}) == [\"123\"]\n\n      assert changed(template, %{foo: 123, bar: false, baz: 456}, nil) == [\"456\"]\n      assert changed(template, %{foo: 123, bar: false, baz: 456}, %{}) == [nil]\n      assert changed(template, %{foo: 123, bar: false, baz: 456}, %{bar: true}) == [\"456\"]\n      assert changed(template, %{foo: 123, bar: false, baz: 456}, %{foo: true}) == [\"456\"]\n      assert changed(template, %{foo: 123, bar: false, baz: 456}, %{baz: true}) == [\"456\"]\n    end\n  end\n\n  describe \"fingerprints\" do\n    test \"are integers\" do\n      rendered1 = eval(\"foo<%= @bar %>baz\", %{bar: 123})\n      rendered2 = eval(\"foo<%= @bar %>baz\", %{bar: 456})\n      assert is_integer(rendered1.fingerprint)\n      assert rendered1.fingerprint == rendered2.fingerprint\n    end\n\n    test \"changes even with dynamic content\" do\n      assert eval(\"<%= :foo %>\").fingerprint != eval(\"<%= :bar %>\").fingerprint\n    end\n  end\n\n  describe \"mark_variables_ast_change_tracked/1\" do\n    test \"ignores pinned variables and binary modifiers\" do\n      ast =\n        quote do\n          %{foo: foo, bar: ^bar, bin: <<thebin::binary>>, other: other}\n        end\n\n      assert {new_ast, variables} = Engine.mark_variables_as_change_tracked(ast, %{})\n      assert map_size(variables) == 3\n\n      assert %{\n               foo: {:foo, [change_track: true], _},\n               other: {:other, [change_track: true], _},\n               thebin: {:thebin, [change_track: true], _}\n             } = variables\n\n      assert new_ast != ast\n    end\n  end\n\n  describe \"slots\" do\n    import Phoenix.Component, only: [sigil_H: 2]\n\n    defp component(assigns) do\n      %{inner_block: [%{inner_block: slot}]} = assigns\n      throw(slot)\n    end\n\n    test \"slots with no dynamics represented as rendered struct\" do\n      try do\n        assigns = %{}\n\n        %Phoenix.LiveView.Rendered{dynamic: dynamic} =\n          ~H\"<.component>No dynamics</.component>\"\n\n        dynamic.(true)\n      catch\n        slot ->\n          assert %Phoenix.LiveView.Rendered{} = slot\n      else\n        _ -> flunk(\"Should have caught\")\n      end\n    end\n\n    test \"slots with dynamics are represented as function\" do\n      try do\n        assigns = %{}\n\n        %Phoenix.LiveView.Rendered{dynamic: dynamic} =\n          ~H\"<.component>{1234}</.component>\"\n\n        dynamic.(true)\n      catch\n        slot ->\n          assert is_function(slot)\n      else\n        _ -> flunk(\"Should have caught\")\n      end\n    end\n  end\n\n  defp eval(string, assigns \\\\ %{}, binding \\\\ []) do\n    EEx.eval_string(string, [assigns: assigns] ++ binding, file: __ENV__.file, engine: Engine)\n  end\n\n  defp changed(string, assigns, changed, track_changes? \\\\ true) do\n    %{dynamic: dynamic} = eval(string, Map.put(assigns, :__changed__, changed))\n    expand_dynamic(dynamic, track_changes?)\n  end\n\n  defp expand_dynamic(dynamic, track_changes?) do\n    Enum.map(dynamic.(track_changes?), &expand_rendered(&1, track_changes?))\n  end\n\n  defp expand_rendered(%Rendered{} = rendered, track_changes?) do\n    update_in(rendered.dynamic, &expand_dynamic(&1, track_changes?))\n  end\n\n  defp expand_rendered(%Comprehension{entries: entries} = comprehension, track_changes?) do\n    expanded_entries =\n      Enum.map(entries, fn {_key, _vars, render} ->\n        # for simplicity, we don't care about vars_changed here\n        Enum.map(render.(nil, track_changes?), &expand_rendered(&1, track_changes?))\n      end)\n\n    %{comprehension | entries: expanded_entries}\n  end\n\n  defp expand_rendered(other, _track_changes), do: other\n\n  defp render(string, assigns \\\\ %{}) do\n    string\n    |> eval(assigns)\n    |> Phoenix.HTML.Safe.to_iodata()\n    |> IO.iodata_to_binary()\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/heex_extension_test.exs",
    "content": "defmodule Phoenix.LiveView.HEExExtensionTest do\n  use ExUnit.Case, async: true\n\n  alias Phoenix.LiveView.Rendered\n\n  defmodule View do\n    use Phoenix.View, root: \"test/support/templates/heex\", path: \"\"\n  end\n\n  defmodule SampleComponent do\n    use Phoenix.LiveComponent\n    def render(assigns), do: ~H\"FROM COMPONENT\"\n  end\n\n  @assigns %{\n    pre: \"pre\",\n    inner_content: \"inner\",\n    post: \"post\",\n    socket: %Phoenix.LiveView.Socket{}\n  }\n\n  test \"renders live engine to string\" do\n    assert Phoenix.View.render_to_string(View, \"inner_live.html\", @assigns) == \"live: inner\"\n  end\n\n  test \"renders live engine with live engine to string\" do\n    assert Phoenix.View.render_to_string(View, \"live_with_live.html\", @assigns) ==\n             \"pre: pre\\nlive: inner\\npost: post\"\n  end\n\n  test \"renders live engine with comprehension to string\" do\n    assigns = Map.put(@assigns, :points, [])\n\n    assert Phoenix.View.render_to_string(View, \"live_with_comprehension.html\", assigns) ==\n             \"pre: pre\\n\\npost: post\"\n\n    assigns = Map.put(@assigns, :points, [%{x: 1, y: 2}, %{x: 3, y: 4}])\n\n    assert Phoenix.View.render_to_string(View, \"live_with_comprehension.html\", assigns) ==\n             \"pre: pre\\n\\n  x: 1\\n  live: inner\\n  y: 2\\n\\n  x: 3\\n  live: inner\\n  y: 4\\n\\npost: post\"\n  end\n\n  test \"renders live engine as is\" do\n    assert %Rendered{static: [\"live: \", \"\"], dynamic: [\"inner\"]} =\n             Phoenix.View.render(View, \"inner_live.html\", @assigns) |> expand_rendered(true)\n  end\n\n  test \"renders live engine with nested live view\" do\n    assert %Rendered{\n             static: [\"pre: \", \"\\n\", \"\\npost: \", \"\"],\n             dynamic: [\n               \"pre\",\n               %Rendered{dynamic: [\"inner\"], static: [\"live: \", \"\"]},\n               \"post\"\n             ]\n           } =\n             Phoenix.View.render(View, \"live_with_live.html\", @assigns) |> expand_rendered(true)\n  end\n\n  test \"renders live engine with nested dead view\" do\n    assert %Rendered{\n             static: [\"pre: \", \"\\n\", \"\\npost: \", \"\"],\n             dynamic: [\"pre\", [\"dead: \", \"inner\"], \"post\"]\n           } =\n             Phoenix.View.render(View, \"live_with_dead.html\", @assigns) |> expand_rendered(true)\n  end\n\n  test \"renders dead engine with nested live view\" do\n    assert Phoenix.View.render(View, \"dead_with_live.html\", @assigns) ==\n             {:safe, [\"pre: \", \"pre\", \"\\n\", [\"live: \", \"inner\", \"\"], \"\\npost: \", \"post\"]}\n  end\n\n  test \"renders dead engine with function component\" do\n    assert %Rendered{\n             static: [\"pre: \", \"\\n\", \"\\npost: \", \"\"],\n             dynamic: [\n               \"pre\",\n               %Rendered{dynamic: [\"the value\"], static: [\"COMPONENT:\", \"\"]},\n               \"post\"\n             ]\n           } =\n             Phoenix.View.render(View, \"dead_with_function_component.html\", @assigns)\n             |> expand_rendered(true)\n  end\n\n  test \"renders dead engine with function component with inner content\" do\n    assert %Rendered{\n             static: [\"pre: \", \"\\n\", \"\\npost: \", \"\"],\n             dynamic: [\n               \"pre\",\n               %Rendered{\n                 dynamic: [\n                   \"the value\",\n                   %Rendered{dynamic: [], static: [\"\\n  The inner content\\n\"]}\n                 ],\n                 static: [\"COMPONENT:\", \", Content: \", \"\"]\n               },\n               \"post\"\n             ]\n           } =\n             Phoenix.View.render(\n               View,\n               \"dead_with_function_component_with_inner_content.html\",\n               @assigns\n             )\n             |> expand_rendered(true)\n  end\n\n  defp expand_dynamic(dynamic, track_changes?) do\n    Enum.map(dynamic.(track_changes?), &expand_rendered(&1, track_changes?))\n  end\n\n  defp expand_rendered(%Rendered{} = rendered, track_changes?) do\n    update_in(rendered.dynamic, &expand_dynamic(&1, track_changes?))\n  end\n\n  defp expand_rendered(other, _track_changes), do: other\nend\n"
  },
  {
    "path": "test/phoenix_live_view/hooks_test.exs",
    "content": "defmodule Phoenix.LiveView.IntegrationHooksTest do\n  use ExUnit.Case, async: true\n\n  alias Phoenix.LiveView\n  alias Phoenix.LiveView.Lifecycle\n\n  defp build_socket(router \\\\ Phoenix.LiveViewTest.Support.Router) do\n    %LiveView.Socket{\n      private: %{lifecycle: %Lifecycle{}},\n      router: router\n    }\n  end\n\n  describe \"attach_hook/3\" do\n    test \"raises on invalid lifecycle event\" do\n      assert_raise ArgumentError, ~r/invalid lifecycle event/, fn ->\n        LiveView.attach_hook(build_socket(), :id, nil, &noop/3)\n      end\n\n      assert_raise ArgumentError, ~r/invalid lifecycle event/, fn ->\n        LiveView.attach_hook(build_socket(), :id, :info, &noop/2)\n      end\n\n      assert_raise ArgumentError, ~r/invalid lifecycle event/, fn ->\n        LiveView.attach_hook(build_socket(), :id, :handle_call, &noop/3)\n      end\n\n      assert_raise ArgumentError, ~r/invalid lifecycle event/, fn ->\n        LiveView.attach_hook(build_socket(), :id, :handle_cast, &noop/2)\n      end\n    end\n\n    test \"supports handle_async/3\" do\n      assert %Lifecycle{handle_async: [%{id: :noop}]} =\n               build_socket()\n               |> LiveView.attach_hook(:noop, :handle_async, &noop/3)\n               |> lifecycle()\n    end\n\n    test \"supports handle_event/3\" do\n      assert %Lifecycle{handle_event: [%{id: :noop}]} =\n               build_socket()\n               |> LiveView.attach_hook(:noop, :handle_event, &noop/3)\n               |> lifecycle()\n    end\n\n    test \"supports handle_params/3\" do\n      assert %Lifecycle{handle_params: [%{id: :noop}]} =\n               build_socket()\n               |> LiveView.attach_hook(:noop, :handle_params, &noop/3)\n               |> lifecycle()\n    end\n\n    test \"supports handle_info/2\" do\n      assert %Lifecycle{handle_info: [%{id: :noop}]} =\n               build_socket()\n               |> LiveView.attach_hook(:noop, :handle_info, &noop/2)\n               |> lifecycle()\n    end\n\n    test \"raises when hook with :name is already attached to the same lifecycle event\" do\n      socket = LiveView.attach_hook(build_socket(), :noop, :handle_event, &noop/3)\n\n      assert_raise ArgumentError, ~r/existing hook :noop already attached on :handle_event/, fn ->\n        LiveView.attach_hook(socket, :noop, :handle_event, &noop/3)\n      end\n    end\n\n    test \"supports named hooks for multiple lifecycle events\" do\n      socket =\n        build_socket()\n        |> LiveView.attach_hook(:noop, :handle_async, &noop/3)\n        |> LiveView.attach_hook(:noop, :handle_params, &noop/3)\n        |> LiveView.attach_hook(:noop, :handle_event, &noop/3)\n        |> LiveView.attach_hook(:noop, :handle_info, &noop/2)\n\n      assert %Lifecycle{\n               handle_async: [%{id: :noop, stage: :handle_async}],\n               handle_info: [%{id: :noop, stage: :handle_info}],\n               handle_event: [%{id: :noop, stage: :handle_event}],\n               handle_params: [%{id: :noop, stage: :handle_params}]\n             } = lifecycle(socket)\n    end\n\n    test \"raises on stage :handle_params when socket is not mounted at the router\" do\n      assert_raise RuntimeError, ~r/not mounted at the router/, fn ->\n        LiveView.attach_hook(build_socket(nil), :boom, :handle_params, &noop/3)\n      end\n    end\n  end\n\n  describe \"detach_hook\" do\n    test \"raises on invalid lifecycle event\" do\n      assert_raise ArgumentError, ~r/invalid lifecycle event/, fn ->\n        LiveView.detach_hook(build_socket(), :id, nil)\n      end\n\n      assert_raise ArgumentError, ~r/invalid lifecycle event/, fn ->\n        LiveView.detach_hook(build_socket(), :id, :info)\n      end\n\n      assert_raise ArgumentError, ~r/invalid lifecycle event/, fn ->\n        LiveView.detach_hook(build_socket(), :id, :handle_call)\n      end\n\n      assert_raise ArgumentError, ~r/invalid lifecycle event/, fn ->\n        LiveView.detach_hook(build_socket(), :id, :handle_cast)\n      end\n    end\n\n    test \"removes the hook by a given stage\" do\n      socket =\n        build_socket()\n        |> LiveView.attach_hook(:a, :handle_event, &noop/3)\n        |> LiveView.attach_hook(:b, :handle_event, &noop/3)\n        |> LiveView.attach_hook(:c, :handle_event, &noop/3)\n        |> LiveView.attach_hook(:b, :handle_params, &noop/3)\n\n      assert %Lifecycle{\n               handle_event: [\n                 %{id: :a, stage: :handle_event},\n                 %{id: :b, stage: :handle_event},\n                 %{id: :c, stage: :handle_event}\n               ],\n               handle_params: [\n                 %{id: :b, stage: :handle_params}\n               ]\n             } = lifecycle(socket)\n\n      socket = LiveView.detach_hook(socket, :b, :handle_event)\n\n      assert %Lifecycle{\n               handle_event: [\n                 %{id: :a, stage: :handle_event},\n                 %{id: :c, stage: :handle_event}\n               ],\n               handle_params: [\n                 %{id: :b, stage: :handle_params}\n               ]\n             } = lifecycle(socket)\n    end\n\n    test \"when no hook is registered detach_hook is a no-op\" do\n      socket = LiveView.attach_hook(build_socket(), :foo, :handle_event, &noop/3)\n      assert LiveView.detach_hook(socket, :bar, :handle_event) == socket\n    end\n  end\n\n  defp lifecycle(%LiveView.Socket{private: %{lifecycle: struct}}), do: struct\n  defp lifecycle(%LiveView.Socket{}), do: nil\n\n  defp noop(_, socket), do: {:cont, socket}\n  defp noop(_, _, socket), do: {:cont, socket}\nend\n"
  },
  {
    "path": "test/phoenix_live_view/html_engine_test.exs",
    "content": "defmodule Phoenix.LiveView.HTMLEngineTest do\n  use ExUnit.Case, async: true\n\n  import ExUnit.CaptureIO\n\n  import Phoenix.Component\n\n  alias Phoenix.LiveView.TagEngine.Tokenizer.ParseError\n\n  defp eval(string, assigns \\\\ %{}, opts \\\\ []) do\n    {env, opts} = Keyword.pop(opts, :env, __ENV__)\n\n    opts =\n      Keyword.merge(opts,\n        file: env.file,\n        caller: env,\n        tag_handler: Phoenix.LiveView.HTMLEngine\n      )\n      |> Keyword.put_new(:line, 1)\n\n    quoted = Phoenix.LiveView.TagEngine.compile(string, opts)\n\n    {result, _} = Code.eval_quoted(quoted, [assigns: assigns], env)\n    result\n  end\n\n  defp render(string, assigns \\\\ %{}, opts \\\\ []) do\n    string\n    |> eval(assigns, opts)\n    |> Phoenix.HTML.Safe.to_iodata()\n    |> IO.iodata_to_binary()\n  end\n\n  defmacrop compile(string) do\n    quote do\n      unquote(\n        Phoenix.LiveView.TagEngine.compile(string,\n          file: __ENV__.file,\n          caller: __CALLER__,\n          tag_handler: Phoenix.LiveView.HTMLEngine\n        )\n      )\n      |> Phoenix.HTML.Safe.to_iodata()\n      |> IO.iodata_to_binary()\n    end\n  end\n\n  defmacro test_attr_macro(a) do\n    case a do\n      :base -> quote do: [{\"style\", \"display: flex;\"}, {\"other\", \"foo\"}, {\"another\", @bar}]\n      _ -> quote do: a\n    end\n  end\n\n  def assigns_component(assigns) do\n    ~H\"{inspect(Map.delete(assigns, :__changed__))}\"\n  end\n\n  def textarea(assigns) do\n    assigns =\n      Phoenix.Component.assign(assigns, :extra_assigns, assigns_to_attributes(assigns, []))\n\n    ~H\"<textarea {@extra_assigns}><%= render_slot(@inner_block) %></textarea>\"\n  end\n\n  def remote_function_component(assigns) do\n    ~H\"REMOTE COMPONENT: Value: {@value}\"\n  end\n\n  def remote_function_component_with_inner_block(assigns) do\n    ~H\"REMOTE COMPONENT: Value: {@value}, Content: {render_slot(@inner_block)}\"\n  end\n\n  def remote_function_component_with_inner_block_args(assigns) do\n    ~H\"\"\"\n    REMOTE COMPONENT WITH ARGS: Value: {@value}\n    {render_slot(@inner_block, %{\n      downcase: String.downcase(@value),\n      upcase: String.upcase(@value)\n    })}\n    \"\"\"\n  end\n\n  defp local_function_component(assigns) do\n    ~H\"LOCAL COMPONENT: Value: {@value}\"\n  end\n\n  defp local_function_component_with_inner_block(assigns) do\n    ~H\"LOCAL COMPONENT: Value: {@value}, Content: {render_slot(@inner_block)}\"\n  end\n\n  defp local_function_component_with_inner_block_args(assigns) do\n    ~H\"\"\"\n    LOCAL COMPONENT WITH ARGS: Value: {@value}\n    {render_slot(@inner_block, %{\n      downcase: String.downcase(@value),\n      upcase: String.upcase(@value)\n    })}\n    \"\"\"\n  end\n\n  test \"handles text\" do\n    assert render(\"Hello\") == \"Hello\"\n  end\n\n  test \"handles regular blocks\" do\n    assert render(\"\"\"\n           Hello <%= if true do %>world!<% end %>\n           \"\"\") == \"Hello world!\"\n  end\n\n  test \"handles html blocks with regular blocks\" do\n    assert render(\"\"\"\n           Hello <div>w<%= if true do %>orld<% end %>!</div>\n           \"\"\") == \"Hello <div>world!</div>\"\n  end\n\n  test \"handles phx-no-curly-interpolation\" do\n    assert render(\"\"\"\n           <div phx-no-curly-interpolation>{open}<%= :eval %>{close}</div>\n           \"\"\") == \"<div>{open}eval{close}</div>\"\n\n    assert render(\"\"\"\n           <div phx-no-curly-interpolation>{open}{<%= :eval %>}{close}</div>\n           \"\"\") == \"<div>{open}{eval}{close}</div>\"\n\n    assert render(\"\"\"\n           {:pre}<style phx-no-curly-interpolation>{css}</style>{:post}\n           \"\"\") == \"pre<style>{css}</style>post\"\n\n    assert render(\"\"\"\n           <div phx-no-curly-interpolation>{:pre}<style phx-no-curly-interpolation>{css}</style>{:post}</div>\n           \"\"\") == \"<div>{:pre}<style>{css}</style>{:post}</div>\"\n  end\n\n  test \"handles string attributes\" do\n    assert render(\"\"\"\n           Hello <div name=\"my name\" phone=\"111\">text</div>\n           \"\"\") == \"Hello <div name=\\\"my name\\\" phone=\\\"111\\\">text</div>\"\n  end\n\n  test \"handles string attribute value keeping special chars unchanged\" do\n    assert render(\"<div name='1 < 2'/>\") == \"<div name='1 < 2'></div>\"\n  end\n\n  test \"handles boolean attributes\" do\n    assert render(\"\"\"\n           Hello <div hidden>text</div>\n           \"\"\") == \"Hello <div hidden>text</div>\"\n  end\n\n  test \"handles interpolated attributes\" do\n    assert render(\"\"\"\n           Hello <div name={to_string(123)} phone={to_string(456)}>text</div>\n           \"\"\") == \"Hello <div name=\\\"123\\\" phone=\\\"456\\\">text</div>\"\n  end\n\n  test \"handles interpolated body\" do\n    assert render(\"\"\"\n           Hello <div>2 + 2 = {2 + 2}</div>\n           \"\"\") == \"Hello <div>2 + 2 = 4</div>\"\n  end\n\n  test \"handles interpolated attribute value containing special chars\" do\n    assert render(\"<div name={@val}/>\", %{val: \"1 < 2\"}) == \"<div name=\\\"1 &lt; 2\\\"></div>\"\n  end\n\n  test \"handles interpolated attributes with strings\" do\n    assert render(\"\"\"\n           <div name={String.upcase(\"abc\")}>text</div>\n           \"\"\") == \"<div name=\\\"ABC\\\">text</div>\"\n  end\n\n  test \"handles interpolated attributes with curly braces\" do\n    assert render(\"\"\"\n           <div name={elem({\"abc\"}, 0)}>text</div>\n           \"\"\") == \"<div name=\\\"abc\\\">text</div>\"\n  end\n\n  test \"handles dynamic attributes\" do\n    assert render(\"Hello <div {@attrs}>text</div>\", %{attrs: [name: \"1\", phone: to_string(2)]}) ==\n             \"Hello <div name=\\\"1\\\" phone=\\\"2\\\">text</div>\"\n  end\n\n  test \"keeps underscores in dynamic attributes\" do\n    assert render(\"Hello <div {@attrs}>text</div>\", %{attrs: [full_name: \"1\"]}) ==\n             \"Hello <div full_name=\\\"1\\\">text</div>\"\n  end\n\n  test \"keeps attribute ordering\" do\n    assigns = %{attrs1: [d1: \"1\"], attrs2: [d2: \"2\"]}\n    template = ~S(<div {@attrs1} sd1={1} s1=\"1\" {@attrs2} s2=\"2\" sd2={2} />)\n\n    assert render(template, assigns) ==\n             ~S(<div d1=\"1\" sd1=\"1\" s1=\"1\" d2=\"2\" s2=\"2\" sd2=\"2\"></div>)\n\n    assert %Phoenix.LiveView.Rendered{static: [\"<div\", \"\", \" s1=\\\"1\\\"\", \" s2=\\\"2\\\"\", \"></div>\"]} =\n             eval(template, assigns)\n  end\n\n  test \"inlines dynamic attributes when keys are known at compilation time\" do\n    assigns = %{val: 1}\n\n    # keyword list\n    template = ~S(<div {[d1: @val, d2: \"2\", d3: @val]} />)\n\n    assert %Phoenix.LiveView.Rendered{static: [\"<div\", \" d2=\\\"2\\\"\", \"></div>\"]} =\n             eval(template, assigns)\n\n    # list with string keys\n    template = ~S(<div {[{\"d1\", @val}, {\"d2\", \"2\"}, {\"d3\", @val}]} />)\n\n    assert %Phoenix.LiveView.Rendered{static: [\"<div\", \" d2=\\\"2\\\"\", \"></div>\"]} =\n             eval(template, assigns)\n\n    # map with atom keys\n    template = ~S(<div {%{d1: @val, d2: \"2\", d3: @val}} />)\n\n    assert %Phoenix.LiveView.Rendered{static: [\"<div\", \" d2=\\\"2\\\"\", \"></div>\"]} =\n             eval(template, assigns)\n\n    # map with string keys\n    template = ~S(<div {%{\"d1\" => @val, \"d2\" => \"2\", \"d3\" => @val}} />)\n\n    assert %Phoenix.LiveView.Rendered{static: [\"<div\", \" d2=\\\"2\\\"\", \"></div>\"]} =\n             eval(template, assigns)\n\n    # macro is expanded\n    template = ~S|<div {test_attr_macro(:base)} />|\n\n    assert %Phoenix.LiveView.Rendered{static: [\"<div style=\\\"\", \"\\\" other=\\\"foo\\\"\", \"></div>\"]} =\n             eval(template, %{bar: \"baz\"})\n\n    assert render(template, %{bar: \"baz\"}) ==\n             ~S|<div style=\"display: flex;\" other=\"foo\" another=\"baz\"></div>|\n\n    # if assign map access was expanded, this would raise\n    expected = \"<div qux=\\\"qux\\\"></div>\"\n    assigns = %{foo: %{bar: %{baz: %{\"qux\" => \"qux\"}}}}\n    assert render(~S|<div {@foo.bar.baz} />|, assigns) == expected\n  end\n\n  test \"optimizes attributes with literal string values\" do\n    assigns = %{unsafe: \"<foo>\", safe: {:safe, \"<foo>\"}}\n\n    # binaries are extracted out\n    template = ~S(<div id={\"<foo>\"} />)\n    assert render(template, assigns) == ~S(<div id=\"&lt;foo&gt;\"></div>)\n\n    assert %Phoenix.LiveView.Rendered{static: [\"<div id=\\\"&lt;foo&gt;\\\"></div>\"]} =\n             eval(template, assigns)\n\n    # binary concatenation is extracted out\n    template = ~S(<div id={\"pre-\" <> @unsafe} />)\n    assert render(template, assigns) == ~S(<div id=\"pre-&lt;foo&gt;\"></div>)\n\n    assert %Phoenix.LiveView.Rendered{static: [\"<div id=\\\"pre-\", \"\\\"></div>\"]} =\n             eval(template, assigns)\n\n    template = ~S(<div id={\"pre-\" <> @unsafe <> \"-pos\"} />)\n    assert render(template, assigns) == ~S(<div id=\"pre-&lt;foo&gt;-pos\"></div>)\n\n    assert %Phoenix.LiveView.Rendered{static: [\"<div id=\\\"pre-\", \"-pos\\\"></div>\"]} =\n             eval(template, assigns)\n\n    # interpolation is extracted out\n    template = ~S(<div id={\"pre-#{@unsafe}-pos\"} />)\n    assert render(template, assigns) == ~S(<div id=\"pre-&lt;foo&gt;-pos\"></div>)\n\n    assert %Phoenix.LiveView.Rendered{static: [\"<div id=\\\"pre-\", \"-pos\\\"></div>\"]} =\n             eval(template, assigns)\n\n    # mixture of interpolation and binary concatenation is extracted out\n    template = ~S(<div id={\"pre-\" <> \"#{@unsafe}-pos\"} />)\n    assert render(template, assigns) == ~S(<div id=\"pre-&lt;foo&gt;-pos\"></div>)\n\n    assert %Phoenix.LiveView.Rendered{static: [\"<div id=\\\"pre-\", \"-pos\\\"></div>\"]} =\n             eval(template, assigns)\n\n    # binaries in lists for classes are extracted out\n    template = ~S(<div class={[\"<bar>\", \"<foo>\"]} />)\n    assert render(template, assigns) == ~S(<div class=\"&lt;bar&gt; &lt;foo&gt;\"></div>)\n\n    assert %Phoenix.LiveView.Rendered{static: [\"<div class=\\\"&lt;bar&gt; &lt;foo&gt;\\\"></div>\"]} =\n             eval(template, assigns)\n\n    # binaries in lists for classes are extracted out even with dynamic bits\n    template = ~S(<div class={[\"<bar>\", @unsafe]} />)\n    assert render(template, assigns) == ~S(<div class=\"&lt;bar&gt; &lt;foo&gt;\"></div>)\n\n    assert %Phoenix.LiveView.Rendered{static: [\"<div class=\\\"&lt;bar&gt; \", \"\\\"></div>\"]} =\n             eval(template, assigns)\n\n    # raises if not a binary\n    assert_raise ArgumentError, \"expected a binary in <>, got: {:safe, \\\"<foo>\\\"}\", fn ->\n      render(~S(<div id={\"pre-\" <> @safe} />), assigns)\n    end\n  end\n\n  def do_block(do: block), do: block\n\n  test \"handles do blocks with expressions\" do\n    assigns = %{not_text: \"not text\", text: \"text\"}\n\n    template = ~S\"\"\"\n    <%= @text %>\n    <%= Phoenix.LiveView.HTMLEngineTest.do_block do %><%= assigns[:not_text] %><% end %>\n    \"\"\"\n\n    # A bug made it so \"not text\" appeared inside @text.\n    assert render(template, assigns) == \"text\\nnot text\"\n\n    template = ~S\"\"\"\n    <%= for i <- [\"id1\", \"id2\", \"id3\"] do %>\n      <div id={i}>\n        <%= Phoenix.LiveView.HTMLEngineTest.do_block do %>\n          <%= i %>\n        <% end %>\n      </div>\n    <% end %>\n    \"\"\"\n\n    # A bug made it so \"id={id}\" was not handled properly\n    assert render(template, assigns) =~ ~s'<div id=\"id1\">'\n  end\n\n  test \"optimizes class attributes\" do\n    assigns = %{\n      nil_assign: nil,\n      true_assign: true,\n      false_assign: false,\n      unsafe: \"<foo>\",\n      safe: {:safe, \"<foo>\"},\n      list: [\"safe\", false, nil, \"<unsafe>\"],\n      recursive_list: [\"safe\", false, [nil, \"<unsafe>\"]]\n    }\n\n    assert %Phoenix.LiveView.Rendered{static: [\"<div class=\\\"\", \"\\\"></div>\"]} =\n             eval(~S(<div class={@safe} />), assigns)\n\n    template = ~S(<div class={@nil_assign} />)\n    assert render(template, assigns) == ~S(<div class=\"\"></div>)\n\n    template = ~S(<div class={@false_assign} />)\n    assert render(template, assigns) == ~S(<div class=\"\"></div>)\n\n    template = ~S(<div class={@true_assign} />)\n    assert render(template, assigns) == ~S(<div class=\"\"></div>)\n\n    template = ~S(<div class={@unsafe} />)\n    assert render(template, assigns) == ~S(<div class=\"&lt;foo&gt;\"></div>)\n\n    template = ~S(<div class={@safe} />)\n    assert render(template, assigns) == ~S(<div class=\"<foo>\"></div>)\n\n    template = ~S(<div class={@list} />)\n    assert render(template, assigns) == ~S(<div class=\"safe &lt;unsafe&gt;\"></div>)\n\n    template = ~S(<div class={@recursive_list} />)\n    assert render(template, assigns) == ~S(<div class=\"safe &lt;unsafe&gt;\"></div>)\n  end\n\n  test \"optimizes attributes that can be empty\" do\n    assigns = %{\n      nil_assign: nil,\n      true_assign: true,\n      false_assign: false,\n      unsafe: \"<foo>\",\n      safe: {:safe, \"<foo>\"},\n      list: [\"safe\", false, nil, \"<unsafe>\"]\n    }\n\n    assert %Phoenix.LiveView.Rendered{static: [\"<div style=\\\"\", \"\\\"></div>\"]} =\n             eval(~S(<div style={@safe} />), assigns)\n\n    template = ~S(<div style={@nil_assign} />)\n    assert render(template, assigns) == ~S(<div style=\"\"></div>)\n\n    template = ~S(<div style={@false_assign} />)\n    assert render(template, assigns) == ~S(<div style=\"\"></div>)\n\n    template = ~S(<div style={@true_assign} />)\n    assert render(template, assigns) == ~S(<div style=\"\"></div>)\n\n    template = ~S(<div style={@unsafe} />)\n    assert render(template, assigns) == ~S(<div style=\"&lt;foo&gt;\"></div>)\n\n    template = ~S(<div style={@safe} />)\n    assert render(template, assigns) == ~S(<div style=\"<foo>\"></div>)\n  end\n\n  test \"handle void elements\" do\n    assert render(\"\"\"\n           <div><br></div>\\\n           \"\"\") == \"<div><br></div>\"\n  end\n\n  test \"handle void elements with attributes\" do\n    assert render(\"\"\"\n           <div><br attr='1'></div>\\\n           \"\"\") == \"<div><br attr='1'></div>\"\n  end\n\n  test \"handle self close void elements\" do\n    assert render(\"<hr/>\") == \"<hr>\"\n  end\n\n  test \"handle self close void elements with attributes\" do\n    assert render(~S(<hr id=\"1\"/>)) == ~S(<hr id=\"1\">)\n  end\n\n  test \"handle self close elements\" do\n    assert render(\"<div/>\") == \"<div></div>\"\n  end\n\n  test \"handle self close elements with attributes\" do\n    assert render(\"<div attr='1'/>\") == \"<div attr='1'></div>\"\n  end\n\n  describe \"debug annotations\" do\n    alias Phoenix.LiveViewTest.Support.DebugAnno\n    import Phoenix.LiveViewTest.Support.DebugAnno\n\n    test \"components without tags\" do\n      assigns = %{}\n      assert compile(\"<DebugAnno.remote value='1'/>\") == \"REMOTE COMPONENT: Value: 1\"\n      assert compile(\"<.local value='1'/>\") == \"LOCAL COMPONENT: Value: 1\"\n    end\n\n    test \"components with tags\" do\n      assigns = %{}\n\n      assert compile(\"<DebugAnno.remote_with_tags value='1'/>\") ==\n               \"<!-- <Phoenix.LiveViewTest.Support.DebugAnno.remote_with_tags> test/support/live_views/debug_anno.exs:11 () --><div data-phx-loc=\\\"12\\\">REMOTE COMPONENT: Value: 1</div><!-- </Phoenix.LiveViewTest.Support.DebugAnno.remote_with_tags> -->\"\n\n      assert compile(\"<.local_with_tags value='1'/>\") ==\n               \"<!-- <Phoenix.LiveViewTest.Support.DebugAnno.local_with_tags> test/support/live_views/debug_anno.exs:19 () --><div data-phx-loc=\\\"20\\\">LOCAL COMPONENT: Value: 1</div><!-- </Phoenix.LiveViewTest.Support.DebugAnno.local_with_tags> -->\"\n    end\n\n    test \"nesting\" do\n      assigns = %{}\n\n      assert compile(\"<DebugAnno.nested value='1'/>\") ==\n               \"\"\"\n               <!-- <Phoenix.LiveViewTest.Support.DebugAnno.nested> test/support/live_views/debug_anno.exs:23 () --><div data-phx-loc=\\\"24\\\">\n                 <!-- @caller test/support/live_views/debug_anno.exs:25 () --><!-- <Phoenix.LiveViewTest.Support.DebugAnno.local_with_tags> test/support/live_views/debug_anno.exs:19 () --><div data-phx-loc=\\\"20\\\">LOCAL COMPONENT: Value: local</div><!-- </Phoenix.LiveViewTest.Support.DebugAnno.local_with_tags> -->\n               </div><!-- </Phoenix.LiveViewTest.Support.DebugAnno.nested> -->\\\n               \"\"\"\n    end\n\n    test \"slots without tags\" do\n      assigns = %{}\n\n      assert compile(\"<DebugAnno.slot />\") ==\n               \"\"\"\n               <!-- <Phoenix.LiveViewTest.Support.DebugAnno.slot> test/support/live_views/debug_anno.exs:31 () --><!-- @caller test/support/live_views/debug_anno.exs:32 () -->\n                 1\n               ,\n                 2\n               <!-- </Phoenix.LiveViewTest.Support.DebugAnno.slot> -->\\\n               \"\"\"\n    end\n\n    test \"slots with tags\" do\n      assigns = %{}\n\n      assert compile(\"<DebugAnno.slot_with_tags />\") ==\n               \"\"\"\n               <!-- <Phoenix.LiveViewTest.Support.DebugAnno.slot_with_tags> test/support/live_views/debug_anno.exs:40 () --><!-- @caller test/support/live_views/debug_anno.exs:41 () --><!-- <:inner_block> test/support/live_views/debug_anno.exs:41 () -->\n                 <div data-phx-loc=\\\"43\\\">1</div>\n               <!-- </:inner_block> --><!-- <:separator> test/support/live_views/debug_anno.exs:42 () --><hr data-phx-loc=\\\"42\\\"><!-- </:separator> --><!-- <:inner_block> test/support/live_views/debug_anno.exs:41 () -->\n                 <div data-phx-loc=\\\"43\\\">2</div>\n               <!-- </:inner_block> --><!-- </Phoenix.LiveViewTest.Support.DebugAnno.slot_with_tags> -->\\\n               \"\"\"\n    end\n\n    test \"can opt out\" do\n      alias Phoenix.LiveViewTest.Support.DebugAnnoOptOut\n\n      assigns = %{}\n\n      assert compile(\"<DebugAnnoOptOut.slot_with_tags />\") ==\n               \"\\n  <div>1</div>\\n<hr>\\n  <div>2</div>\\n\"\n    end\n  end\n\n  describe \"handle function components\" do\n    test \"remote call (self close)\" do\n      assigns = %{}\n\n      assert compile(\"<Phoenix.LiveView.HTMLEngineTest.remote_function_component value='1'/>\") ==\n               \"REMOTE COMPONENT: Value: 1\"\n    end\n\n    test \"remote call from alias (self close)\" do\n      alias Phoenix.LiveView.HTMLEngineTest\n      assigns = %{}\n\n      assert compile(\"<HTMLEngineTest.remote_function_component value='1'/>\") ==\n               \"REMOTE COMPONENT: Value: 1\"\n    end\n\n    test \"remote call with inner content\" do\n      assigns = %{}\n\n      assert compile(\"\"\"\n             <Phoenix.LiveView.HTMLEngineTest.remote_function_component_with_inner_block value='1'>\n               The inner content\n             </Phoenix.LiveView.HTMLEngineTest.remote_function_component_with_inner_block>\n             \"\"\") == \"REMOTE COMPONENT: Value: 1, Content: \\n  The inner content\\n\"\n    end\n\n    test \"remote call with :let\" do\n      expected = \"\"\"\n      LOCAL COMPONENT WITH ARGS: Value: aBcD\n\n        Upcase: ABCD\n        Downcase: abcd\n      \"\"\"\n\n      assigns = %{}\n\n      assert compile(\"\"\"\n             <.local_function_component_with_inner_block_args\n               value=\"aBcD\"\n               :let={%{upcase: upcase, downcase: downcase}}\n             >\n               Upcase: <%= upcase %>\n               Downcase: <%= downcase %>\n             </.local_function_component_with_inner_block_args>\n             \"\"\") =~ expected\n    end\n\n    test \"remote call with inner content with args\" do\n      expected = \"\"\"\n      REMOTE COMPONENT WITH ARGS: Value: aBcD\n\n        Upcase: ABCD\n        Downcase: abcd\n      \"\"\"\n\n      assigns = %{}\n\n      assert compile(\"\"\"\n             <Phoenix.LiveView.HTMLEngineTest.remote_function_component_with_inner_block_args\n               value=\"aBcD\"\n               :let={%{upcase: upcase, downcase: downcase}}\n             >\n               Upcase: <%= upcase %>\n               Downcase: <%= downcase %>\n             </Phoenix.LiveView.HTMLEngineTest.remote_function_component_with_inner_block_args>\n             \"\"\") =~ expected\n    end\n\n    test \"raise on remote call with inner content passing non-matching args\" do\n      message = ~r\"\"\"\n      cannot match arguments sent from render_slot/2 against the pattern in :let.\n\n      Expected a value matching `%{wrong: _}`, got: %{downcase: \"abcd\", upcase: \"ABCD\"}\\\n      \"\"\"\n\n      assigns = %{}\n\n      assert_raise(RuntimeError, message, fn ->\n        compile(\"\"\"\n        <Phoenix.LiveView.HTMLEngineTest.remote_function_component_with_inner_block_args\n          {[value: \"aBcD\"]}\n          :let={%{wrong: _}}\n        >\n          ...\n        </Phoenix.LiveView.HTMLEngineTest.remote_function_component_with_inner_block_args>\n        \"\"\")\n      end)\n    end\n\n    test \"raise on remote call passing args to self close components\" do\n      message = ~r\".exs:2: cannot use :let on a component without inner content\"\n\n      assert_raise(CompileError, message, fn ->\n        eval(\"\"\"\n        <br>\n        <Phoenix.LiveView.HTMLEngineTest.remote_function_component value='1' :let={var}/>\n        \"\"\")\n      end)\n    end\n\n    test \"raise when passing :key to slot\" do\n      message = ~r\":key is not supported on slots: sample\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <.function_component_with_single_slot>\n          <:sample :for={i <- 1..2} :key={i}>\n            The sample slot\n          </:sample>\n        </.function_component_with_single_slot>\n        \"\"\")\n      end)\n    end\n\n    test \"local call (self close)\" do\n      assigns = %{}\n\n      assert compile(\"<.local_function_component value='1'/>\") ==\n               \"LOCAL COMPONENT: Value: 1\"\n    end\n\n    test \"local call with inner content\" do\n      assigns = %{}\n\n      assert compile(\"\"\"\n             <.local_function_component_with_inner_block value='1'>\n               The inner content\n             </.local_function_component_with_inner_block>\n             \"\"\") == \"LOCAL COMPONENT: Value: 1, Content: \\n  The inner content\\n\"\n    end\n\n    test \"local call with inner content with args\" do\n      expected = \"\"\"\n      LOCAL COMPONENT WITH ARGS: Value: aBcD\n\n        Upcase: ABCD\n        Downcase: abcd\n      \"\"\"\n\n      assigns = %{}\n\n      assert compile(\"\"\"\n             <.local_function_component_with_inner_block_args\n               value=\"aBcD\"\n               :let={%{upcase: upcase, downcase: downcase}}\n             >\n               Upcase: <%= upcase %>\n               Downcase: <%= downcase %>\n             </.local_function_component_with_inner_block_args>\n             \"\"\") =~ expected\n\n      assert compile(\"\"\"\n             <.local_function_component_with_inner_block_args\n               {[value: \"aBcD\"]}\n               :let={%{upcase: upcase, downcase: downcase}}\n             >\n               Upcase: <%= upcase %>\n               Downcase: <%= downcase %>\n             </.local_function_component_with_inner_block_args>\n             \"\"\") =~ expected\n    end\n\n    test \"raise on local call with inner content passing non-matching args\" do\n      message = ~r\"\"\"\n      cannot match arguments sent from render_slot/2 against the pattern in :let.\n\n      Expected a value matching `%{wrong: _}`, got: %{downcase: \"abcd\", upcase: \"ABCD\"}\\\n      \"\"\"\n\n      assigns = %{}\n\n      assert_raise(RuntimeError, message, fn ->\n        compile(\"\"\"\n        <.local_function_component_with_inner_block_args\n          {[value: \"aBcD\"]}\n          :let={%{wrong: _}}\n        >\n          ...\n        </.local_function_component_with_inner_block_args>\n        \"\"\")\n      end)\n    end\n\n    test \"raise on local call passing args to self close components\" do\n      message = ~r\".exs:2: cannot use :let on a component without inner content\"\n\n      assert_raise(CompileError, message, fn ->\n        eval(\"\"\"\n        <br>\n        <.local_function_component value='1' :let={var}/>\n        \"\"\")\n      end)\n    end\n\n    test \"raise on duplicated :let\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:4:3: cannot define multiple :let attributes. Another :let has already been defined at line 3\n        |\n      1 | <br>\n      2 | <Phoenix.LiveView.HTMLEngineTest.remote_function_component value='1'\n      3 |   :let={var1}\n      4 |   :let={var2}\n        |   ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <br>\n        <Phoenix.LiveView.HTMLEngineTest.remote_function_component value='1'\n          :let={var1}\n          :let={var2}\n        />\n        \"\"\")\n      end)\n\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:4:3: cannot define multiple :let attributes. Another :let has already been defined at line 3\n        |\n      1 | <br>\n      2 | <.local_function_component value='1'\n      3 |   :let={var1}\n      4 |   :let={var2}\n        |   ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <br>\n        <.local_function_component value='1'\n          :let={var1}\n          :let={var2}\n        />\n        \"\"\")\n      end)\n    end\n\n    test \"invalid :let expr\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:2:70: :let must be a pattern between {...} in remote component: Phoenix.LiveView.HTMLEngineTest.remote_function_component\n        |\n      1 | <br>\n      2 | <Phoenix.LiveView.HTMLEngineTest.remote_function_component value='1' :let=\\\"1\\\"\n        |                                                                      ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <br>\n        <Phoenix.LiveView.HTMLEngineTest.remote_function_component value='1' :let=\"1\"\n        />\n        \"\"\")\n      end)\n\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:2:38: :let must be a pattern between {...} in local component: local_function_component\n        |\n      1 | <br>\n      2 | <.local_function_component value='1' :let=\\\"1\\\"\n        |                                      ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <br>\n        <.local_function_component value='1' :let=\"1\"\n        />\n        \"\"\")\n      end)\n    end\n\n    test \"raise with invalid special attr\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:2:38: unsupported attribute \\\":bar\\\" in local component: local_function_component\n        |\n      1 | <br>\n      2 | <.local_function_component value='1' :bar=\\\"1\\\" />\n        |                                      ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <br>\n        <.local_function_component value='1' :bar=\"1\" />\n        />\n        \"\"\")\n      end)\n    end\n\n    test \"raise on unclosed local call\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:1: end of template reached without closing tag for <.local_function_component>\n        |\n      1 | <.local_function_component value='1' :let={var}>\n        | ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <.local_function_component value='1' :let={var}>\n        \"\"\")\n      end)\n\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:2:3: end of do-block reached without closing tag for <.local_function_component>\n        |\n      1 | <%= if true do %>\n      2 |   <.local_function_component value='1' :let={var}>\n        |   ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <%= if true do %>\n          <.local_function_component value='1' :let={var}>\n        <% end %>\n        \"\"\")\n      end)\n    end\n\n    test \"when tag is unclosed\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:2:1: end of template reached without closing tag for <div>\n        |\n      1 | <div>Foo</div>\n      2 | <div>\n        | ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <div>Foo</div>\n        <div>\n        <div>Bar</div>\n        \"\"\")\n      end)\n    end\n\n    test \"when syntax error on HTML attributes\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:2:9: invalid attribute value after `=`. Expected either a value between quotes (such as \\\"value\\\" or 'value') or an Elixir expression between curly braces (such as `{expr}`)\n        |\n      1 | <div>Bar</div>\n      2 | <div id=>Foo</div>\n        |         ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <div>Bar</div>\n        <div id=>Foo</div>\n        \"\"\")\n      end)\n    end\n\n    test \"empty attributes\" do\n      assigns = %{}\n      assert compile(\"<.assigns_component />\") == \"%{}\"\n    end\n\n    test \"dynamic attributes\" do\n      assigns = %{attrs: [name: \"1\", phone: true]}\n\n      assert compile(\"<.assigns_component {@attrs} />\") ==\n               \"%{name: &quot;1&quot;, phone: true}\"\n    end\n\n    test \"sorts attributes by group: static + dynamic\" do\n      assigns = %{attrs1: [d1: \"1\"], attrs2: [d2: \"2\", d3: \"3\"]}\n\n      assert compile(\n               \"<.assigns_component d1=\\\"one\\\" {@attrs1} d=\\\"middle\\\" {@attrs2} d2=\\\"two\\\" />\"\n             ) ==\n               \"%{d: &quot;middle&quot;, d1: &quot;one&quot;, d2: &quot;two&quot;, d3: &quot;3&quot;}\"\n    end\n  end\n\n  describe \"named slots\" do\n    def function_component_with_single_slot(assigns) do\n      ~H\"\"\"\n      BEFORE SLOT\n      <%= render_slot(@sample) %>\n      AFTER SLOT\n      \"\"\"noformat\n    end\n\n    def function_component_with_slots(assigns) do\n      ~H\"\"\"\n      BEFORE HEADER\n      <%= render_slot(@header) %>\n      TEXT\n      <%= render_slot(@footer) %>\n      AFTER FOOTER\n      \"\"\"noformat\n    end\n\n    def function_component_with_slots_and_default(assigns) do\n      ~H\"\"\"\n      BEFORE HEADER\n      <%= render_slot(@header) %>\n      TEXT:<%= render_slot(@inner_block) %>:TEXT\n      <%= render_slot(@footer) %>\n      AFTER FOOTER\n      \"\"\"noformat\n    end\n\n    def function_component_with_slots_and_args(assigns) do\n      ~H\"\"\"\n      BEFORE SLOT\n      <%= render_slot(@sample, 1) %>\n      AFTER SLOT\n      \"\"\"noformat\n    end\n\n    def function_component_with_slot_attrs(assigns) do\n      ~H\"\"\"\n      <%= for entry <- @sample do %>\n      <%= entry.a %>\n      <%= render_slot(entry) %>\n      <%= entry.b %>\n      <% end %>\n      \"\"\"noformat\n    end\n\n    def function_component_with_multiple_slots_entries(assigns) do\n      ~H\"\"\"\n      <%= for entry <- @sample do %>\n        <%= entry.id %>: <%= render_slot(entry, %{}) %>\n      <% end %>\n      \"\"\"noformat\n    end\n\n    def function_component_with_self_close_slots(assigns) do\n      ~H\"\"\"\n      <%= for entry <- @sample do %>\n        <%= entry.id %>\n      <% end %>\n      \"\"\"noformat\n    end\n\n    def render_slot_name(assigns) do\n      ~H\"<%= for entry <- @sample do %>[<%= entry.__slot__ %>]<% end %>\"noformat\n    end\n\n    def render_inner_block_slot_name(assigns) do\n      ~H\"<%= for entry <- @inner_block do %>[<%= entry.__slot__ %>]<% end %>\"noformat\n    end\n\n    test \"single slot\" do\n      assigns = %{}\n\n      expected = \"\"\"\n      COMPONENT WITH SLOTS:\n      BEFORE SLOT\n\n          The sample slot\n        \\\n\n      AFTER SLOT\n      \"\"\"\n\n      assert compile(\"\"\"\n             COMPONENT WITH SLOTS:\n             <.function_component_with_single_slot>\n               <:sample>\n                 The sample slot\n               </:sample>\n             </.function_component_with_single_slot>\n             \"\"\") == expected\n\n      assert compile(\"\"\"\n             COMPONENT WITH SLOTS:\n             <Phoenix.LiveView.HTMLEngineTest.function_component_with_single_slot>\n               <:sample>\n                 The sample slot\n               </:sample>\n             </Phoenix.LiveView.HTMLEngineTest.function_component_with_single_slot>\n             \"\"\") == expected\n    end\n\n    test \"raise when calling render_slot/2 on a slot without inner content\" do\n      message = ~r\"attempted to render slot <:sample> but the slot has no inner content\"\n\n      assigns = %{}\n\n      assert_raise(RuntimeError, message, fn ->\n        compile(\"\"\"\n        <.function_component_with_single_slot>\n          <:sample/>\n        </.function_component_with_single_slot>\n        \"\"\")\n      end)\n\n      assert_raise(RuntimeError, message, fn ->\n        compile(\"\"\"\n        <.function_component_with_single_slot>\n          <:sample/>\n          <:sample/>\n        </.function_component_with_single_slot>\n        \"\"\")\n      end)\n    end\n\n    test \"multiple slot entries randered by a single rende_slot/2 call\" do\n      assigns = %{}\n\n      expected = \"\"\"\n      COMPONENT WITH SLOTS:\n      BEFORE SLOT\n\n          entry 1\n        \\\n\n          entry 2\n        \\\n\n      AFTER SLOT\n      \"\"\"\n\n      assert compile(\"\"\"\n             COMPONENT WITH SLOTS:\n             <.function_component_with_single_slot>\n               <:sample>\n                 entry 1\n               </:sample>\n               <:sample>\n                 entry 2\n               </:sample>\n             </.function_component_with_single_slot>\n             \"\"\") == expected\n\n      assert compile(\"\"\"\n             COMPONENT WITH SLOTS:\n             <Phoenix.LiveView.HTMLEngineTest.function_component_with_single_slot>\n               <:sample>\n                 entry 1\n               </:sample>\n               <:sample>\n                 entry 2\n               </:sample>\n             </Phoenix.LiveView.HTMLEngineTest.function_component_with_single_slot>\n             \"\"\") == expected\n    end\n\n    test \"multiple slot entries handled by an explicit for comprehension\" do\n      assigns = %{}\n\n      expected = \"\"\"\n\n        1: one\n\n        2: two\n      \"\"\"\n\n      assert compile(\"\"\"\n             <.function_component_with_multiple_slots_entries>\n               <:sample id=\"1\">one</:sample>\n               <:sample id=\"2\">two</:sample>\n             </.function_component_with_multiple_slots_entries>\n             \"\"\") == expected\n\n      assert compile(\"\"\"\n             <Phoenix.LiveView.HTMLEngineTest.function_component_with_multiple_slots_entries>\n               <:sample id=\"1\">one</:sample>\n               <:sample id=\"2\">two</:sample>\n             </Phoenix.LiveView.HTMLEngineTest.function_component_with_multiple_slots_entries>\n             \"\"\") == expected\n    end\n\n    test \"slot attrs\" do\n      assigns = %{a: \"A\"}\n      expected = \"\\nA\\n and \\nB\\n\"\n\n      assert compile(\"\"\"\n             <.function_component_with_slot_attrs>\n               <:sample a={@a} b=\"B\"> and </:sample>\n             </.function_component_with_slot_attrs>\n             \"\"\") == expected\n\n      assert compile(\"\"\"\n             <Phoenix.LiveView.HTMLEngineTest.function_component_with_slot_attrs>\n               <:sample a={@a} b=\"B\"> and </:sample>\n             </Phoenix.LiveView.HTMLEngineTest.function_component_with_slot_attrs>\n             \"\"\") == expected\n    end\n\n    test \"multiple slots\" do\n      assigns = %{}\n\n      expected = \"\"\"\n      BEFORE COMPONENT\n      BEFORE HEADER\n\n          The header content\n        \\\n\n      TEXT\n\n          The footer content\n        \\\n\n      AFTER FOOTER\n\n      AFTER COMPONENT\n      \"\"\"\n\n      assert compile(\"\"\"\n             BEFORE COMPONENT\n             <.function_component_with_slots>\n               <:header>\n                 The header content\n               </:header>\n               <:footer>\n                 The footer content\n               </:footer>\n             </.function_component_with_slots>\n             AFTER COMPONENT\n             \"\"\") == expected\n\n      assert compile(\"\"\"\n             BEFORE COMPONENT\n             <Phoenix.LiveView.HTMLEngineTest.function_component_with_slots>\n               <:header>\n                 The header content\n               </:header>\n               <:footer>\n                 The footer content\n               </:footer>\n             </Phoenix.LiveView.HTMLEngineTest.function_component_with_slots>\n             AFTER COMPONENT\n             \"\"\") == expected\n    end\n\n    test \"multiple slots with default\" do\n      assigns = %{middle: \"middle\"}\n\n      expected = \"\"\"\n      BEFORE COMPONENT\n      BEFORE HEADER\n\n          The header content\n        \\\n\n      TEXT:\n        top\n        foo middle bar\n        bot\n      :TEXT\n\n          The footer content\n        \\\n\n      AFTER FOOTER\n\n      AFTER COMPONENT\n      \"\"\"\n\n      assert compile(\"\"\"\n             BEFORE COMPONENT\n             <.function_component_with_slots_and_default>\n               top\n               <:header>\n                 The header content\n               </:header>\n               foo <%= @middle %> bar\n               <:footer>\n                 The footer content\n               </:footer>\n               bot\n             </.function_component_with_slots_and_default>\n             AFTER COMPONENT\n             \"\"\") == expected\n\n      assert compile(\"\"\"\n             BEFORE COMPONENT\n             <Phoenix.LiveView.HTMLEngineTest.function_component_with_slots_and_default>\n               top\n               <:header>\n                 The header content\n               </:header>\n               foo <%= @middle %> bar\n               <:footer>\n                 The footer content\n               </:footer>\n               bot\n             </Phoenix.LiveView.HTMLEngineTest.function_component_with_slots_and_default>\n             AFTER COMPONENT\n             \"\"\") == expected\n    end\n\n    test \"slots with args\" do\n      assigns = %{}\n\n      expected = \"\"\"\n      COMPONENT WITH SLOTS:\n      BEFORE SLOT\n\n          The sample slot\n          Arg: 1\n        \\\n\n      AFTER SLOT\n      \"\"\"\n\n      assert compile(\"\"\"\n             COMPONENT WITH SLOTS:\n             <.function_component_with_slots_and_args>\n               <:sample :let={arg}>\n                 The sample slot\n                 Arg: <%= arg %>\n               </:sample>\n             </.function_component_with_slots_and_args>\n             \"\"\") == expected\n\n      assert compile(\"\"\"\n             COMPONENT WITH SLOTS:\n             <Phoenix.LiveView.HTMLEngineTest.function_component_with_slots_and_args>\n               <:sample :let={arg}>\n                 The sample slot\n                 Arg: <%= arg %>\n               </:sample>\n             </Phoenix.LiveView.HTMLEngineTest.function_component_with_slots_and_args>\n             \"\"\") == expected\n    end\n\n    test \"nested calls with slots\" do\n      assigns = %{}\n\n      expected = \"\"\"\n      BEFORE SLOT\n\n         The outer slot\n          BEFORE SLOT\n\n            The inner slot\n            \\\n\n      AFTER SLOT\n\n        \\\n\n      AFTER SLOT\n      \"\"\"\n\n      assert compile(\"\"\"\n             <Phoenix.LiveView.HTMLEngineTest.function_component_with_single_slot>\n               <:sample>\n                The outer slot\n                 <.function_component_with_single_slot>\n                   <:sample>\n                   The inner slot\n                   </:sample>\n                 </.function_component_with_single_slot>\n               </:sample>\n             </Phoenix.LiveView.HTMLEngineTest.function_component_with_single_slot>\n             \"\"\") == expected\n\n      assert compile(\"\"\"\n             <.function_component_with_single_slot>\n               <:sample>\n                The outer slot\n                 <.function_component_with_single_slot>\n                   <:sample>\n                   The inner slot\n                   </:sample>\n                 </.function_component_with_single_slot>\n               </:sample>\n             </.function_component_with_single_slot>\n             \"\"\") == expected\n    end\n\n    test \"self close slots\" do\n      assigns = %{}\n\n      expected = \"\"\"\n\n        1\n\n        2\n      \"\"\"\n\n      assert compile(\"\"\"\n             <.function_component_with_self_close_slots>\n               <:sample id=\"1\"/>\n               <:sample id=\"2\"/>\n             </.function_component_with_self_close_slots>\n             \"\"\") == expected\n\n      assert compile(\"\"\"\n             <Phoenix.LiveView.HTMLEngineTest.function_component_with_self_close_slots>\n               <:sample id=\"1\"/>\n               <:sample id=\"2\"/>\n             </Phoenix.LiveView.HTMLEngineTest.function_component_with_self_close_slots>\n             \"\"\") == expected\n    end\n\n    test \"raise if self close slot uses :let\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:2:19: cannot use :let on a slot without inner content\n        |\n      1 | <.function_component_with_self_close_slots>\n      2 |   <:sample id=\"1\" :let={var}/>\n        |                   ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <.function_component_with_self_close_slots>\n          <:sample id=\"1\" :let={var}/>\n        </.function_component_with_self_close_slots>\n        \"\"\")\n      end)\n    end\n\n    test \"store the slot name in __slot__\" do\n      assigns = %{}\n\n      assert compile(\"\"\"\n             <.render_slot_name>\n               <:sample>\n                 The sample slot\n               </:sample>\n             </.render_slot_name>\n             \"\"\") == \"[sample]\"\n\n      assert compile(\"\"\"\n             <.render_slot_name>\n               <:sample/>\n               <:sample/>\n             </.render_slot_name>\n             \"\"\") == \"[sample][sample]\"\n    end\n\n    test \"store the inner_block slot name in __slot__\" do\n      assigns = %{}\n\n      assert compile(\"\"\"\n             <.render_inner_block_slot_name>\n                 The content\n             </.render_inner_block_slot_name>\n             \"\"\") == \"[inner_block]\"\n    end\n\n    test \"raise if the slot entry is not a direct child of a component\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:2:3: invalid slot entry <:sample>. A slot entry must be a direct child of a component\n        |\n      1 | <div>\n      2 |   <:sample>\n        |   ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <div>\n          <:sample>\n            Content\n          </:sample>\n        </div>\n        \"\"\")\n      end)\n\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:3:3: invalid slot entry <:sample>. A slot entry must be a direct child of a component\n        |\n      1 | <Phoenix.LiveView.HTMLEngineTest.function_component_with_single_slot>\n      2 | <%= if true do %>\n      3 |   <:sample>\n        |   ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <Phoenix.LiveView.HTMLEngineTest.function_component_with_single_slot>\n        <%= if true do %>\n          <:sample>\n            <p>Content</p>\n          </:sample>\n        <% end %>\n        </Phoenix.LiveView.HTMLEngineTest.function_component_with_single_slot>\n        \"\"\")\n      end)\n\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:3:5: invalid slot entry <:footer>. A slot entry must be a direct child of a component\n        |\n      1 | <.mydiv>\n      2 |   <:sample>\n      3 |     <:footer>\n        |     ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <.mydiv>\n          <:sample>\n            <:footer>\n              Content\n            </:footer>\n          </:sample>\n        </.mydiv>\n        \"\"\")\n      end)\n\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:1: invalid slot entry <:sample>. A slot entry must be a direct child of a component\n        |\n      1 | <:sample>\n        | ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <:sample>\n          Content\n        </:sample>\n        \"\"\")\n      end)\n\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:1: invalid slot entry <:sample>. A slot entry must be a direct child of a component\n        |\n      1 | <:sample>\n        | ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <:sample>\n          <p>Content</p>\n        </:sample>\n        \"\"\")\n      end)\n    end\n  end\n\n  describe \"tracks root\" do\n    test \"valid cases\" do\n      assert eval(\"<foo></foo>\").root == true\n      assert eval(\"<foo><%= 123 %></foo>\").root == true\n      assert eval(\"<foo><bar></bar></foo>\").root == true\n      assert eval(\"<foo><br /></foo>\").root == true\n\n      assert eval(\"<foo />\").root == true\n      assert eval(\"<br />\").root == true\n      assert eval(\"<br>\").root == true\n\n      assert eval(\"  <foo></foo>  \").root == true\n      assert eval(\"\\n\\n<foo></foo>\\n\").root == true\n      assert eval(\"<%!-- comment --%>\\n\\n<foo></foo>\\n\").root == true\n    end\n\n    test \"invalid cases\" do\n      assert eval(\"\").root == false\n      assert eval(\"<foo></foo><bar></bar>\").root == false\n      assert eval(\"<foo></foo><bar></bar>\").root == false\n      assert eval(\"<br /><br />\").root == false\n      assert eval(\"<%= 123 %>\").root == false\n      assert eval(\"<foo></foo><%= 123 %>\").root == false\n      assert eval(\"<%= 123 %><foo></foo>\").root == false\n      assert eval(\"123<foo></foo>\").root == false\n      assert eval(\"<foo></foo>123\").root == false\n      assert eval(\"<.to_string />\").root == false\n      assert eval(\"<.to_string></.to_string>\").root == false\n      assert eval(\"<Kernel.to_string />\").root == false\n      assert eval(\"<Kernel.to_string></Kernel.to_string>\").root == false\n      assert eval(\"<div :for={item <- @items}><%= item %></div>\").root == false\n      assert eval(\"<!-- comment --><div></div>\").root == false\n      assert eval(\"<div></div><!-- comment -->\").root == false\n    end\n  end\n\n  describe \"tag validations\" do\n    test \"handles style\" do\n      assert render(\"<style>a = '<a>';<%= :b %> = '<b>';</style>\") ==\n               \"<style>a = '<a>';b = '<b>';</style>\"\n    end\n\n    test \"handles script\" do\n      assert render(\"<script>a = '<a>';<%= :b %> = '<b>';</script>\") ==\n               \"<script>a = '<a>';b = '<b>';</script>\"\n    end\n\n    test \"handles comments\" do\n      assert render(\"Begin<!-- <%= 123 %> -->End\") ==\n               \"Begin<!-- 123 -->End\"\n    end\n\n    test \"unmatched comment\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:6: expected closing `-->` for comment\n        |\n      1 | Begin<!-- <%= 123 %>\n        |      ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"Begin<!-- <%= 123 %>\")\n      end)\n    end\n\n    test \"unmatched open/close tags\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:4:1: unmatched closing tag. Expected </div> for <div> at line 2, got: </span>\n        |\n      1 | <br>\n      2 | <div>\n      3 |  text\n      4 | </span>\n        | ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <br>\n        <div>\n         text\n        </span>\n        \"\"\")\n      end)\n    end\n\n    test \"unmatched open/close tags with nested tags\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:6:1: unmatched closing tag. Expected </div> for <div> at line 2, got: </span>\n        |\n      3 |   <p>\n      4 |     text\n      5 |   </p>\n      6 | </span>\n        | ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <br>\n        <div>\n          <p>\n            text\n          </p>\n        </span>\n        \"\"\")\n      end)\n    end\n\n    test \"unmatched open/close tags with void tags\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:16: unmatched closing tag. Expected </div> for <div> at line 1, got: </link> (note <link> is a void tag and cannot have any content)\n        |\n      1 | <div><link>Text</link></div>\n        |                ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"<div><link>Text</link></div>\")\n      end)\n    end\n\n    test \"invalid remote tag\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:1: invalid tag <Foo>\n        |\n      1 | <Foo foo=\\\"bar\\\" />\n        | ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <Foo foo=\"bar\" />\n        \"\"\")\n      end)\n    end\n\n    test \"missing open tag\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:2:3: missing opening tag for </span>\n        |\n      1 | text\n      2 |   </span>\n        |   ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        text\n          </span>\n        \"\"\")\n      end)\n    end\n\n    test \"missing open tag with void tag\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:11: missing opening tag for </link> (note <link> is a void tag and cannot have any content)\n        |\n      1 | <link>Text</link>\n        |           ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"<link>Text</link>\")\n      end)\n    end\n\n    test \"missing closing tag\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:2:1: end of template reached without closing tag for <div>\n        |\n      1 | <br>\n      2 | <div foo={@foo}>\n        | ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <br>\n        <div foo={@foo}>\n        \"\"\")\n      end)\n\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:2:3: end of template reached without closing tag for <span>\n        |\n      1 | text\n      2 |   <span foo={@foo}>\n        |   ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        text\n          <span foo={@foo}>\n            text\n        \"\"\")\n      end)\n    end\n\n    test \"invalid tag name\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:2:3: invalid tag <Oops>\n        |\n      1 | <br>\n      2 |   <Oops foo={@foo}>\n        |   ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <br>\n          <Oops foo={@foo}>\n            Bar\n          </Oops>\n        \"\"\")\n      end)\n    end\n\n    test \"invalid tag\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:10: expected closing `}` for expression\n\n      In case you don't want `{` to begin a new interpolation, you may write it using `&lbrace;` or using `<%= \"{\" %>`\n        |\n      1 | <div foo={<%= @foo %>}>bar</div>\n        |          ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <div foo={<%= @foo %>}>bar</div>\n        \"\"\")\n      end)\n\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:2:3: expected closing `}` for expression\n\n      In case you don't want `{` to begin a new interpolation, you may write it using `&lbrace;` or using `<%= \"{\" %>`\n        |\n      1 | <div foo=\n      2 |   {<%= @foo %>}>bar</div>\n        |   ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\n          \"\"\"\n          <div foo=\n            {<%= @foo %>}>bar</div>\n          \"\"\",\n          %{},\n          indentation: 0\n        )\n      end)\n\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:2:6: expected closing `}` for expression\n\n      In case you don't want `{` to begin a new interpolation, you may write it using `&lbrace;` or using `<%= \"{\" %>`\n        |\n      1 |    <div foo=\n      2 |      {<%= @foo %>}>bar</div>\n        |      ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\n          \"\"\"\n          <div foo=\n            {<%= @foo %>}>bar</div>\n\n          \"\"\",\n          %{},\n          indentation: 3\n        )\n      end)\n    end\n  end\n\n  test \"do not render phx-no-format attr\" do\n    rendered = eval(\"<div phx-no-format>Content</div>\")\n    assert rendered.static == [\"<div>Content</div>\"]\n\n    rendered = eval(\"<div phx-no-format />\")\n    assert rendered.static == [\"<div></div>\"]\n\n    assigns = %{}\n\n    assert compile(\"\"\"\n           <Phoenix.LiveView.HTMLEngineTest.textarea phx-no-format>\n            Content\n           </Phoenix.LiveView.HTMLEngineTest.textarea>\n           \"\"\") == \"<textarea>\\n Content\\n</textarea>\"\n\n    assert compile(\"<.textarea phx-no-format>Content</.textarea>\") ==\n             \"<textarea>Content</textarea>\"\n  end\n\n  describe \"html validations\" do\n    test \"phx-update attr requires an unique ID\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:1: attribute \\\"phx-update\\\" requires the \\\"id\\\" attribute to be set\n        |\n      1 | <div phx-update=\\\"ignore\\\">\n        | ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <div phx-update=\"ignore\">\n          Content\n        </div>\n        \"\"\")\n      end)\n\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:1: attribute \\\"phx-update\\\" requires the \\\"id\\\" attribute to be set\n        |\n      1 | <div phx-update=\\\"ignore\\\" class=\\\"foo\\\">\n        | ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <div phx-update=\"ignore\" class=\"foo\">\n          Content\n        </div>\n        \"\"\")\n      end)\n\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:1: attribute \\\"phx-update\\\" requires the \\\"id\\\" attribute to be set\n        |\n      1 | <div phx-update=\\\"ignore\\\" class=\\\"foo\\\" />\n        | ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <div phx-update=\"ignore\" class=\"foo\" />\n        \"\"\")\n      end)\n\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:1: attribute \\\"phx-update\\\" requires the \\\"id\\\" attribute to be set\n        |\n      1 | <div phx-update={@value}>Content</div>\n        | ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <div phx-update={@value}>Content</div>\n        \"\"\")\n      end)\n\n      assert eval(\"\"\"\n             <div id=\"id\" phx-update={@value}>Content</div>\n             \"\"\")\n    end\n\n    test \"validates phx-update values\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:14: the value of the attribute \\\"phx-update\\\" must be: ignore, stream, append, prepend, or replace\n        |\n      1 | <div id=\"id\" phx-update=\"bar\">\n        |              ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <div id=\"id\" phx-update=\"bar\">\n          Content\n        </div>\n        \"\"\")\n      end)\n    end\n\n    test \"phx-hook attr requires an unique ID\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:1: attribute \\\"phx-hook\\\" requires the \\\"id\\\" attribute to be set\n        |\n      1 | <div phx-hook=\\\"MyHook\\\">\n        | ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <div phx-hook=\"MyHook\">\n          Content\n        </div>\n        \"\"\")\n      end)\n\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:1: attribute \\\"phx-hook\\\" requires the \\\"id\\\" attribute to be set\n        |\n      1 | <div phx-hook=\\\"MyHook\\\" />\n        | ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <div phx-hook=\"MyHook\" />\n        \"\"\")\n      end)\n    end\n\n    test \"don't raise when there are dynamic variables\" do\n      assert eval(\"\"\"\n             <div phx-hook=\"MyHook\" {@some_var}>Content</div>\n             \"\"\")\n\n      assert eval(\"\"\"\n             <div phx-update=\"ignore\" {@some_var}>Content</div>\n             \"\"\")\n    end\n\n    test \"raise on unsupported special attrs\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:6: unsupported attribute :let in tags\n        |\n      1 | <div :let={@user}>Content</div>\n        |      ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <div :let={@user}>Content</div>\n        \"\"\")\n      end)\n\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:6: unsupported attribute :foo in tags\n        |\n      1 | <div :foo=\\\"something\\\" />\n        |      ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <div :foo=\"something\" />\n        \"\"\")\n      end)\n    end\n\n    test \"warns when input has id as name\" do\n      assert capture_io(:stderr, fn ->\n               eval(\"\"\"\n               <input name=\"id\" value=\"foo\">\n               \"\"\")\n             end) =~\n               \"Setting the \\\"name\\\" attribute to \\\"id\\\" on an input tag overrides the ID of the corresponding form element\"\n    end\n  end\n\n  describe \"handle errors in expressions\" do\n    test \"inside attribute values\" do\n      exception =\n        assert_raise SyntaxError, fn ->\n          opts = [line: 10, indentation: 8]\n\n          eval(\n            \"\"\"\n            text\n            <%= \"interpolation\" %>\n            <div class={[,]}/>\n            \"\"\",\n            [],\n            opts\n          )\n        end\n\n      message = Exception.message(exception)\n      assert message =~ \"test/phoenix_live_view/html_engine_test.exs:12:22:\"\n      assert message =~ \"syntax error before: ','\"\n    end\n\n    test \"inside root attribute value\" do\n      exception =\n        assert_raise SyntaxError, fn ->\n          opts = [line: 10, indentation: 8]\n\n          eval(\n            \"\"\"\n            text\n            <%= \"interpolation\" %>\n            <div {[,]}/>\n            \"\"\",\n            [],\n            opts\n          )\n        end\n\n      message = Exception.message(exception)\n      assert message =~ \"test/phoenix_live_view/html_engine_test.exs:12:16:\"\n      assert message =~ \"syntax error before: ','\"\n    end\n  end\n\n  describe \":for attr\" do\n    test \"handle :for attr on HTML element\" do\n      expected = \"<div>foo</div><div>bar</div><div>baz</div>\"\n\n      assigns = %{items: [\"foo\", \"bar\", \"baz\"]}\n\n      assert compile(\"\"\"\n               <div :for={item <- @items}><%= item %></div>\n             \"\"\") =~ expected\n    end\n\n    test \"handle :for attr on self closed HTML element\" do\n      expected = ~s(<div class=\"foo\"></div><div class=\"foo\"></div><div class=\"foo\"></div>)\n\n      assigns = %{items: [\"foo\", \"bar\", \"baz\"]}\n\n      assert compile(\"\"\"\n               <div class=\"foo\" :for={_item <- @items} />\n             \"\"\") =~ expected\n    end\n\n    test \"raise on invalid :for expr\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:6: :for must be a generator expression (pattern <- enumerable) between {...}\n        |\n      1 | <div :for={@user}>Content</div>\n        |      ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <div :for={@user}>Content</div>\n        \"\"\")\n      end)\n\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:6: :for must be an expression between {...}\n        |\n      1 | <div :for=\\\"1\\\">Content</div>\n        |      ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <div :for=\"1\">Content</div>\n        \"\"\")\n      end)\n\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:7: :for must be an expression between {...}\n        |\n      1 | <.div :for=\\\"1\\\">Content</.div>\n        |       ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <.div :for=\"1\">Content</.div>\n        \"\"\")\n      end)\n    end\n\n    test \":if components change tracking\" do\n      assert %Phoenix.LiveView.Rendered{static: [\"\", \"\"], dynamic: dynamic} =\n               eval(\n                 \"\"\"\n                 <Phoenix.LiveView.HTMLEngineTest.remote_function_component value={@val} :if={@val == 1} />\n                 \"\"\",\n                 %{__changed__: %{val: true}, val: 1}\n               )\n\n      assert [%Phoenix.LiveView.Rendered{static: [\"\", \"\"]}] = dynamic.(true)\n    end\n\n    test \":for components change tracking\" do\n      %Phoenix.LiveView.Rendered{static: [\"\", \"\"], dynamic: dynamic} =\n        eval(\n          \"\"\"\n          <Phoenix.LiveView.HTMLEngineTest.remote_function_component :for={val <- @items} value={val} />\n          \"\"\",\n          %{__changed__: %{items: true}, items: [1, 2]}\n        )\n\n      assert [%Phoenix.LiveView.Comprehension{}] = dynamic.(true)\n    end\n\n    test \":for in components\" do\n      assigns = %{items: [1, 2]}\n\n      assert compile(\"\"\"\n             <.local_function_component :for={val <- @items} value={val} />\n             \"\"\") == \"LOCAL COMPONENT: Value: 1LOCAL COMPONENT: Value: 2\"\n\n      assert compile(\"\"\"\n             <br>\n             <Phoenix.LiveView.HTMLEngineTest.remote_function_component :for={val <- @items} value={val} />\n             \"\"\") == \"<br>\\nREMOTE COMPONENT: Value: 1REMOTE COMPONENT: Value: 2\"\n\n      assert compile(\"\"\"\n             <br>\n             <Phoenix.LiveView.HTMLEngineTest.remote_function_component_with_inner_block :for={val <- @items} value={val}>inner<%= val %></Phoenix.LiveView.HTMLEngineTest.remote_function_component_with_inner_block>\n             \"\"\") ==\n               \"<br>\\nREMOTE COMPONENT: Value: 1, Content: inner1REMOTE COMPONENT: Value: 2, Content: inner2\"\n\n      assert compile(\"\"\"\n             <.local_function_component_with_inner_block :for={val <- @items} value={val}>inner<%= val %></.local_function_component_with_inner_block>\n             \"\"\") ==\n               \"LOCAL COMPONENT: Value: 1, Content: inner1LOCAL COMPONENT: Value: 2, Content: inner2\"\n    end\n\n    test \"raise on duplicated :for\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:28: cannot define multiple \\\":for\\\" attributes. Another \\\":for\\\" has already been defined at line 1\n        |\n      1 | <div :for={item <- [1, 2]} :for={item <- [1, 2]}>Content</div>\n        |                            ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <div :for={item <- [1, 2]} :for={item <- [1, 2]}>Content</div>\n        \"\"\")\n      end)\n    end\n\n    test \":for in slots\" do\n      assigns = %{items: [1, 2, 3, 4]}\n\n      assert compile(\"\"\"\n             <Phoenix.LiveView.HTMLEngineTest.slot_if value={0}>\n               <:slot :for={i <- @items}>slot<%= i %></:slot>\n             </Phoenix.LiveView.HTMLEngineTest.slot_if>\n             \"\"\") == \"<div>0-slot1slot2slot3slot4</div>\"\n    end\n\n    test \":for and :if in slots\" do\n      assigns = %{items: [1, 2, 3, 4]}\n\n      assert compile(\"\"\"\n             <Phoenix.LiveView.HTMLEngineTest.slot_if value={0}>\n               <:slot :for={i <- @items} :if={rem(i, 2) == 0}>slot<%= i %></:slot>\n             </Phoenix.LiveView.HTMLEngineTest.slot_if>\n             \"\"\") == \"<div>0-slot2slot4</div>\"\n    end\n\n    test \":for and :if and :let in slots\" do\n      assigns = %{items: [1, 2, 3, 4]}\n\n      assert compile(\"\"\"\n             <Phoenix.LiveView.HTMLEngineTest.slot_if value={0}>\n               <:slot :for={i <- @items} :if={rem(i, 2) == 0} :let={val}>slot<%= i %>(<%= val %>)</:slot>\n             </Phoenix.LiveView.HTMLEngineTest.slot_if>\n             \"\"\") == \"<div>0-slot2(0)slot4(0)</div>\"\n    end\n\n    test \"multiple slot definitions with mixed regular/if/for\" do\n      assigns = %{items: [2, 3]}\n\n      assert compile(\"\"\"\n             <Phoenix.LiveView.HTMLEngineTest.slot_if value={0}>\n               <:slot :if={false}>slot0</:slot>\n               <:slot>slot1</:slot>\n               <:slot :for={i <- @items}>slot<%= i %></:slot>\n               <:slot>slot4</:slot>\n             </Phoenix.LiveView.HTMLEngineTest.slot_if>\n             \"\"\") == \"<div>0-slot1slot2slot3slot4</div>\"\n    end\n  end\n\n  describe \":if attr\" do\n    test \"handle :if attr on HTML element\" do\n      assigns = %{flag: Process.get(:flag, true)}\n\n      assert compile(\"\"\"\n               <div :if={@flag} id=\"test\">yes</div>\n             \"\"\") =~ \"<div id=\\\"test\\\">yes</div>\"\n\n      assert compile(\"\"\"\n               <div :if={!@flag} id=\"test\">yes</div>\n             \"\"\") == \"\"\n    end\n\n    test \"handle :if attr on self closed HTML element\" do\n      assigns = %{flag: Process.get(:flag, true)}\n\n      assert compile(\"\"\"\n               <div :if={@flag} id=\"test\" />\n             \"\"\") =~ \"<div id=\\\"test\\\"></div>\"\n\n      assert compile(\"\"\"\n               <div :if={!@flag} id=\"test\" />\n             \"\"\") == \"\"\n    end\n\n    test \"raise on invalid :if expr\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:6: :if must be an expression between {...}\n        |\n      1 | <div :if=\\\"1\\\">test</div>\n        |      ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <div :if=\"1\">test</div>\n        \"\"\")\n      end)\n    end\n\n    test \":if in components\" do\n      assigns = %{flag: Process.get(:flag, true)}\n\n      assert compile(\"\"\"\n             <.local_function_component value=\"123\" :if={@flag} />\n             \"\"\") == \"LOCAL COMPONENT: Value: 123\"\n\n      assert compile(\"\"\"\n             <.local_function_component value=\"123\" :if={!@flag}>test</.local_function_component>\n             \"\"\") == \"\"\n\n      assert compile(\"\"\"\n             <Phoenix.LiveView.HTMLEngineTest.remote_function_component value=\"123\" :if={@flag} />\n             \"\"\") == \"REMOTE COMPONENT: Value: 123\"\n\n      assert compile(\"\"\"\n             <Phoenix.LiveView.HTMLEngineTest.remote_function_component value=\"123\" :if={!@flag}>test</Phoenix.LiveView.HTMLEngineTest.remote_function_component>\n             \"\"\") == \"\"\n    end\n\n    test \"raise on duplicated :if\" do\n      message = \"\"\"\n      test/phoenix_live_view/html_engine_test.exs:1:17: cannot define multiple \\\":if\\\" attributes. Another \\\":if\\\" has already been defined at line 1\n        |\n      1 | <div :if={true} :if={false}>test</div>\n        |                 ^\\\n      \"\"\"\n\n      assert_raise(ParseError, message, fn ->\n        eval(\"\"\"\n        <div :if={true} :if={false}>test</div>\n        \"\"\")\n      end)\n    end\n\n    def slot_if(assigns) do\n      ~H\"\"\"\n      <div>{@value}-{render_slot(@slot, @value)}</div>\n      \"\"\"\n    end\n\n    def slot_if_self_close(assigns) do\n      ~H\"\"\"\n      <div><%= @value %>-<%= for slot <- @slot do %><%= slot.val %>-<% end %></div>\n      \"\"\"noformat\n    end\n\n    test \":if in slots\" do\n      assigns = %{flag: Process.get(:flag, true)}\n\n      assert compile(\"\"\"\n             <Phoenix.LiveView.HTMLEngineTest.slot_if value={0}>\n               <:slot :if={@flag}>slot1</:slot>\n               <:slot :if={!@flag}>slot2</:slot>\n               <:slot :if={@flag}>slot3</:slot>\n             </Phoenix.LiveView.HTMLEngineTest.slot_if>\n             \"\"\") == \"<div>0-slot1slot3</div>\"\n\n      assert compile(\"\"\"\n             <Phoenix.LiveView.HTMLEngineTest.slot_if_self_close value={0}>\n               <:slot :if={@flag} val={1} />\n               <:slot :if={!@flag} val={2} />\n               <:slot :if={@flag} val={3} />\n             </Phoenix.LiveView.HTMLEngineTest.slot_if_self_close>\n             \"\"\") == \"<div>0-1-3-</div>\"\n    end\n  end\n\n  describe \":for and :if attr together\" do\n    test \"handle attrs on HTML element\" do\n      assigns = %{items: [1, 2, 3, 4]}\n\n      assert compile(\"\"\"\n               <div :for={i <- @items} :if={rem(i, 2) == 0}><%= i %></div>\n             \"\"\") =~ \"<div>2</div><div>4</div>\"\n\n      assert compile(\"\"\"\n               <div :for={i <- @items} :if={rem = rem(i, 2)}><%= i %>,<%= rem %></div>\n             \"\"\") =~ \"<div>1,1</div><div>2,0</div><div>3,1</div><div>4,0</div>\"\n\n      assert compile(\"\"\"\n               <div :for={i <- @items} :if={false}><%= i %></div>\n             \"\"\") == \"\"\n    end\n\n    test \"handle attrs on self closed HTML element\" do\n      assigns = %{items: [1, 2, 3, 4]}\n\n      assert compile(\"\"\"\n               <div :for={i <- @items} :if={rem(i, 2) == 0} id={\"post-\" <> to_string(i)} />\n             \"\"\") =~ \"<div id=\\\"post-2\\\"></div><div id=\\\"post-4\\\"></div>\"\n\n      assert compile(\"\"\"\n               <div :for={i <- @items} :if={false}><%= i %></div>\n             \"\"\") == \"\"\n    end\n\n    test \"handle attrs on components\" do\n      assigns = %{items: [1, 2, 3, 4]}\n\n      assert compile(\"\"\"\n               <.local_function_component  :for={i <- @items} :if={rem(i, 2) == 0} value={i}/>\n             \"\"\") == \"LOCAL COMPONENT: Value: 2LOCAL COMPONENT: Value: 4\"\n\n      assert compile(\"\"\"\n               <Phoenix.LiveView.HTMLEngineTest.remote_function_component  :for={i <- @items} :if={rem(i, 2) == 0} value={i}/>\n             \"\"\") == \"REMOTE COMPONENT: Value: 2REMOTE COMPONENT: Value: 4\"\n\n      assert compile(\"\"\"\n               <.local_function_component_with_inner_block  :for={i <- @items} :if={rem(i, 2) == 0} value={i}><%= i %></.local_function_component_with_inner_block>\n             \"\"\") == \"LOCAL COMPONENT: Value: 2, Content: 2LOCAL COMPONENT: Value: 4, Content: 4\"\n\n      assert compile(\"\"\"\n               <Phoenix.LiveView.HTMLEngineTest.remote_function_component_with_inner_block  :for={i <- @items} :if={rem(i, 2) == 0} value={i}><%= i %></Phoenix.LiveView.HTMLEngineTest.remote_function_component_with_inner_block>\n             \"\"\") ==\n               \"REMOTE COMPONENT: Value: 2, Content: 2REMOTE COMPONENT: Value: 4, Content: 4\"\n    end\n  end\n\n  describe \"eex_block line/column positions\" do\n    # Helper to extract `if` nodes that test @status or @other (user's conditionals)\n    # This filters out the internal `if` nodes generated for change tracking\n    # Note: @status is transformed to assigns.status in the AST\n    defp find_user_if_nodes(ast) do\n      {_ast, nodes} =\n        Macro.prewalk(ast, [], fn\n          {:if, meta, [condition | _]} = node, acc ->\n            # Check if the condition references assigns.status or assigns.other\n            if references_assigns_field?(condition, :status) or\n                 references_assigns_field?(condition, :other) do\n              {node, [{:if, meta, condition} | acc]}\n            else\n              {node, acc}\n            end\n\n          node, acc ->\n            {node, acc}\n        end)\n\n      Enum.reverse(nodes)\n    end\n\n    # @status becomes assigns.status which is {{:., _, [{:assigns, _, _}, :status]}, _, _}\n    defp references_assigns_field?(ast, field_name) do\n      {_ast, found} =\n        Macro.prewalk(ast, false, fn\n          {{:., _, [{:assigns, _, _}, ^field_name]}, _, _} = node, _acc -> {node, true}\n          node, acc -> {node, acc}\n        end)\n\n      found\n    end\n\n    test \"if/else/end preserves column positions\" do\n      template = \"\"\"\n      <%= if @status do %>\n          <div>content</div>\n        <% else %>\n          <span>other</span>\n      <% end %>\n      \"\"\"\n\n      opts = [file: \"test.heex\", caller: __ENV__, tag_handler: Phoenix.LiveView.HTMLEngine]\n      quoted = Phoenix.LiveView.TagEngine.compile(template, opts)\n\n      if_nodes = find_user_if_nodes(quoted)\n      assert length(if_nodes) == 1, \"expected 1 user if node, got #{length(if_nodes)}\"\n\n      [{:if, meta, _condition}] = if_nodes\n      # The `if` should be at line 1, column 5 (after `<%= `)\n      assert meta[:line] == 1, \"if should be at line 1, got #{meta[:line]}\"\n      # Column should be 5 (position after `<%= `)\n      assert meta[:column] == 5,\n             \"if should be at column 5, got #{inspect(meta[:column])} (column tracking is broken)\"\n    end\n\n    test \"nested if blocks preserve line and column positions\" do\n      # Template with nested if in else block\n      # Line 1: <%= if @status do %>\n      # Line 5:     <%= if @other do %>\n      template = \"\"\"\n      <%= if @status do %>\n          <div>content</div>\n        <% else %>\n          <span>other</span>\n          <%=  if @other do %>\n             bar\n          <% end %>\n      <% end %>\n      \"\"\"\n\n      opts = [file: \"test.heex\", caller: __ENV__, tag_handler: Phoenix.LiveView.HTMLEngine]\n      quoted = Phoenix.LiveView.TagEngine.compile(template, opts)\n\n      if_nodes = find_user_if_nodes(quoted)\n      assert length(if_nodes) == 2, \"expected 2 user if nodes, got #{length(if_nodes)}\"\n\n      [{:if, outer_meta, _}, {:if, inner_meta, _}] = if_nodes\n\n      # Outer if should be at line 1, column 5\n      assert outer_meta[:line] == 1, \"outer if should be at line 1, got #{outer_meta[:line]}\"\n\n      assert outer_meta[:column] == 5,\n             \"outer if should be at column 5, got #{inspect(outer_meta[:column])}\"\n\n      # Nested if should be at line 5, column 10\n      assert inner_meta[:line] == 5, \"nested if should be at line 5, got #{inner_meta[:line]}\"\n\n      assert inner_meta[:column] == 10,\n             \"nested if should be at column 10, got #{inspect(inner_meta[:column])}\"\n    end\n  end\n\n  describe \"compiler tracing\" do\n    alias Phoenix.Component, as: C, warn: false\n\n    defmodule Tracer do\n      def trace(event, _env)\n          when elem(event, 0) in [\n                 :alias_expansion,\n                 :alias_reference,\n                 :imported_function,\n                 :remote_function\n               ] do\n        send(self(), event)\n        :ok\n      end\n\n      def trace(_event, _env), do: :ok\n    end\n\n    defp tracer_eval(line, content) do\n      eval(content, %{},\n        env: %{__ENV__ | tracers: [Tracer], lexical_tracker: self(), line: line + 1},\n        line: line + 1,\n        indentation: 6\n      )\n    end\n\n    test \"handles imports\" do\n      tracer_eval(__ENV__.line, \"\"\"\n      <.focus_wrap>Ok</.focus_wrap>\n      \"\"\")\n\n      assert_receive {:imported_function, meta, Phoenix.Component, :focus_wrap, 1}\n      assert meta[:line] == __ENV__.line - 4\n      assert meta[:column] == 7\n    end\n\n    test \"handles remote calls\" do\n      tracer_eval(__ENV__.line, \"\"\"\n      <Phoenix.Component.focus_wrap>Ok</Phoenix.Component.focus_wrap>\n      \"\"\")\n\n      assert_receive {:alias_reference, meta, Phoenix.Component}\n      assert meta[:line] == __ENV__.line - 4\n      assert meta[:column] == 7\n\n      assert_receive {:remote_function, meta, Phoenix.Component, :focus_wrap, 1}\n      assert meta[:line] == __ENV__.line - 8\n      assert meta[:column] == 26\n    end\n\n    test \"handles aliases\" do\n      tracer_eval(__ENV__.line, \"\"\"\n      <C.focus_wrap>Ok</C.focus_wrap>\n      \"\"\")\n\n      assert_receive {:alias_expansion, meta, Elixir.C, Phoenix.Component}\n      assert meta[:line] == __ENV__.line - 4\n      assert meta[:column] == 7\n\n      assert_receive {:alias_reference, meta, Phoenix.Component}\n      assert meta[:line] == __ENV__.line - 8\n      assert meta[:column] == 7\n\n      assert_receive {:remote_function, meta, Phoenix.Component, :focus_wrap, 1}\n      assert meta[:line] == __ENV__.line - 12\n      assert meta[:column] == 10\n    end\n  end\n\n  describe \"root tag attributes\" do\n    alias Phoenix.LiveViewTest.Support.RootTagAttr\n    alias Phoenix.LiveViewTest.TreeDOM\n    import Phoenix.LiveViewTest.TreeDOM, only: [sigil_X: 2]\n\n    test \"single self-closing tag\" do\n      assigns = %{}\n\n      compiled = compile(\"<RootTagAttr.single_self_close/>\")\n\n      expected = ~X\"<div phx-r></div>\"\n\n      assert TreeDOM.normalize_to_tree(compiled) == expected\n    end\n\n    test \"single tag with body\" do\n      assigns = %{}\n\n      compiled = compile(\"<RootTagAttr.single_with_body/>\")\n\n      expected = ~X\"<div phx-r>Test</div>\"\n\n      assert TreeDOM.normalize_to_tree(compiled) == expected\n    end\n\n    test \"multiple self-closing tags\" do\n      assigns = %{}\n\n      compiled = compile(\"<RootTagAttr.multiple_self_close/>\")\n\n      expected = ~X\"\"\"\n      <div phx-r></div>\n      <div phx-r></div>\n      <div phx-r></div>\n      \"\"\"\n\n      assert TreeDOM.normalize_to_tree(compiled) == expected\n    end\n\n    test \"multiple tags with bodies\" do\n      assigns = %{}\n\n      compiled = compile(\"<RootTagAttr.multiple_with_bodies/>\")\n\n      expected = ~X\"\"\"\n      <div phx-r>Test1</div>\n      <div phx-r>Test2</div>\n      <div phx-r>Test3</div>\n      \"\"\"\n\n      assert TreeDOM.normalize_to_tree(compiled) == expected\n    end\n\n    test \"tags root tags of nested tags\" do\n      assigns = %{}\n\n      compiled = compile(\"<RootTagAttr.nested_tags/>\")\n\n      expected = ~X\"\"\"\n      <div phx-r>\n        <div>\n          <div></div>\n        </div>\n        <div>\n          <div></div>\n        </div>\n      </div>\n      <div phx-r>\n        <div>\n          <div></div>\n        </div>\n        <div>\n          <div></div>\n        </div>\n      </div>\n      \"\"\"\n\n      assert TreeDOM.normalize_to_tree(compiled) == expected\n    end\n\n    test \"tags root tags of component inner_blocks\" do\n      assigns = %{}\n\n      compiled = compile(\"<RootTagAttr.component_inner_blocks/>\")\n\n      expected =\n        ~X\"\"\"\n        <div phx-r>\n          <div>\n            <section phx-r>\n              <div phx-r>\n                <div>\n                  Inner Block 1\n                </div>\n              </div>\n            </section>\n            <section phx-r>\n              <div phx-r>\n                <div>\n                  Inner Block 2\n                </div>\n              </div>\n            </section>\n          </div>\n        </div>\n        \"\"\"\n\n      assert TreeDOM.normalize_to_tree(compiled) == expected\n    end\n\n    test \"tags root tags of component named slots\" do\n      assigns = %{}\n\n      compiled = compile(\"<RootTagAttr.component_named_slots/>\")\n\n      expected =\n        ~X\"\"\"\n        <div phx-r>\n          <div>\n            <section phx-r>\n              <aside>\n                <div phx-r>\n                  <div>\n                    Inner Block 1\n                  </div>\n                </div>\n              </aside>\n            </section>\n            <section phx-r>\n              <aside>\n                <div phx-r>\n                  <div>\n                    Inner Block 2\n                  </div>\n                </div>\n              </aside>\n            </section>\n          </div>\n        </div>\n        \"\"\"\n\n      assert TreeDOM.normalize_to_tree(compiled) == expected\n    end\n\n    test \"tags root tags correctly for complex nestings of tags, components, and slots\" do\n      assigns = %{}\n\n      compiled = compile(\"<RootTagAttr.nested_tags_components_slots/>\")\n\n      expected =\n        ~X\"\"\"\n        <div phx-r>\n          <div>\n            <section phx-r>\n                <div phx-r>\n                  <section phx-r>\n                    <div phx-r>\n                      <p phx-r>Simple</p>\n                    </div>\n                    <aside>\n                      <div phx-r>\n                        <p phx-r>Simple</p>\n                      </div>\n                    </aside>\n                  </section>\n                </div>\n              <aside>\n                <div phx-r>\n                  <section phx-r>\n                    <div phx-r>\n                      <p phx-r>Simple</p>\n                    </div>\n                    <aside>\n                      <div phx-r>\n                        <p phx-r>Simple</p>\n                      </div>\n                    </aside>\n                  </section>\n                </div>\n              </aside>\n            </section>\n          </div>\n        </div>\n        \"\"\"\n\n      assert TreeDOM.normalize_to_tree(compiled) == expected\n    end\n\n    test \"within nestings\" do\n      assigns = %{}\n\n      compiled = compile(\"<RootTagAttr.within_nestings bool={true}/>\")\n\n      expected = ~X\"\"\"\n        <div phx-r>\n          <div>\n              <section phx-r>\n                <p phx-r>\n                  <span>True</span>\n                </p>\n              </section>\n          </div>\n        </div>\n      \"\"\"\n\n      assert TreeDOM.normalize_to_tree(compiled) == expected\n\n      compiled = compile(\"<RootTagAttr.within_nestings bool={false}/>\")\n\n      expected = ~X\"\"\"\n        <div phx-r>\n          <div>\n              <section phx-r>\n                <p phx-r>\n                  <span>False</span>\n                </p>\n              </section>\n          </div>\n        </div>\n      \"\"\"\n\n      assert TreeDOM.normalize_to_tree(compiled) == expected\n    end\n\n    test \"extra attributes with values provided by macro component directives\" do\n      assigns = %{}\n\n      compiled = compile(\"<RootTagAttr.macro_component_attrs_with_values/>\")\n\n      expected =\n        ~X\"\"\"\n        <div phx-r phx-sample-one=\"test\" phx-sample-two=\"test\">\n          <div>\n            <section phx-r>\n              <div phx-r phx-sample-two=\"test\" phx-sample-one=\"test\">Inner Block</div>\n              <aside>\n                <div phx-r phx-sample-two=\"test\" phx-sample-one=\"test\">\n                  Named Slot\n                </div>\n              </aside>\n            </section>\n          </div>\n        </div>\n        \"\"\"\n\n      assert TreeDOM.normalize_to_tree(compiled) == expected\n    end\n\n    test \"extra attributes without values provided by macro component directives\" do\n      assigns = %{}\n\n      compiled = compile(\"<RootTagAttr.macro_component_attrs_without_values/>\")\n\n      expected =\n        ~X\"\"\"\n        <div phx-r phx-sample-two phx-sample-one>\n          <div>\n            <section phx-r>\n              <div phx-r phx-sample-two phx-sample-one>Inner Block</div>\n              <aside>\n                <div phx-r phx-sample-two phx-sample-one>\n                  Named Slot\n                </div>\n              </aside>\n            </section>\n          </div>\n        </div>\n        \"\"\"\n\n      assert TreeDOM.normalize_to_tree(compiled) == expected\n    end\n\n    test \"extra attributes with values provided by macro component directives within nestings\" do\n      assigns = %{}\n\n      compiled =\n        compile(\"<RootTagAttr.macro_component_attrs_with_values_within_nestings bool={true}/>\")\n\n      expected = ~X\"\"\"\n        <div phx-r phx-sample-two=\"test\" phx-sample-one=\"test\">\n          <div>\n              <section phx-r>\n                <p phx-r phx-sample-two=\"test\" phx-sample-one=\"test\">\n                  <span>True</span>\n                </p>\n              </section>\n          </div>\n        </div>\n      \"\"\"\n\n      assert TreeDOM.normalize_to_tree(compiled) == expected\n\n      compiled =\n        compile(\"<RootTagAttr.macro_component_attrs_with_values_within_nestings bool={false}/>\")\n\n      expected = ~X\"\"\"\n        <div phx-r phx-sample-two=\"test\" phx-sample-one=\"test\">\n          <div>\n              <section phx-r>\n                <p phx-r phx-sample-two=\"test\" phx-sample-one=\"test\">\n                  <span>False</span>\n                </p>\n              </section>\n          </div>\n        </div>\n      \"\"\"\n\n      assert TreeDOM.normalize_to_tree(compiled) == expected\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/html_formatter_test.exs",
    "content": "defmodule Phoenix.LiveView.HTMLFormatterTest do\n  use ExUnit.Case, async: true\n\n  alias Phoenix.LiveView.HTMLFormatter\n\n  defp assert_formatter_output(input, expected, dot_formatter_opts \\\\ []) do\n    dot_formatter_opts =\n      Keyword.put_new(dot_formatter_opts, :migrate_eex_to_curly_interpolation, false)\n\n    first_pass = HTMLFormatter.format(input, dot_formatter_opts) |> IO.iodata_to_binary()\n    assert first_pass == expected\n\n    second_pass = HTMLFormatter.format(first_pass, dot_formatter_opts) |> IO.iodata_to_binary()\n    assert second_pass == expected\n  end\n\n  def assert_formatter_doesnt_change(code, dot_formatter_opts \\\\ []) do\n    dot_formatter_opts =\n      Keyword.put_new(dot_formatter_opts, :migrate_eex_to_curly_interpolation, false)\n\n    first_pass = HTMLFormatter.format(code, dot_formatter_opts) |> IO.iodata_to_binary()\n    assert first_pass == code\n\n    second_pass = HTMLFormatter.format(first_pass, dot_formatter_opts) |> IO.iodata_to_binary()\n    assert second_pass == code\n  end\n\n  test \"errors on invalid HTML\" do\n    assert_raise Phoenix.LiveView.TagEngine.Tokenizer.ParseError,\n                 ~r/end of template reached without closing tag for <style>/,\n                 fn -> assert_formatter_doesnt_change(\"<style>foo\") end\n  end\n\n  test \"always break lines for block elements\" do\n    input = \"\"\"\n      <section><h1><%= @user.name %></h1></section>\n    \"\"\"\n\n    expected = \"\"\"\n    <section>\n      <h1><%= @user.name %></h1>\n    </section>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"keep inline elements in the current line\" do\n    input = \"\"\"\n      <section><h1><b><%= @user.name %></b></h1></section>\n    \"\"\"\n\n    expected = \"\"\"\n    <section>\n      <h1><b><%= @user.name %></b></h1>\n    </section>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"break inline elements to the next line when it doesn't fit\" do\n    input = \"\"\"\n      <section><h1><b><%= @user.name %></b></h1></section>\n    \"\"\"\n\n    expected = \"\"\"\n    <section>\n      <h1>\n        <b><%= @user.name %></b>\n      </h1>\n    </section>\n    \"\"\"\n\n    assert_formatter_output(input, expected, line_length: 20)\n  end\n\n  test \"break inline elements to the next line when it doesn't fit and element is empty\" do\n    input = \"\"\"\n      <section><h1><b class=\"there are several classes\"></b></h1></section>\n    \"\"\"\n\n    expected = \"\"\"\n    <section>\n      <h1>\n        <b class=\"there are several classes\"></b>\n      </h1>\n    </section>\n    \"\"\"\n\n    assert_formatter_output(input, expected, line_length: 20)\n  end\n\n  test \"always break line for block elements\" do\n    input = \"\"\"\n    <h1>1</h1>\n    <h2>2</h2>\n    <h3>3</h3>\n    \"\"\"\n\n    assert_formatter_doesnt_change(input)\n  end\n\n  test \"do not break between EEx tags when there is no space before or after\" do\n    assert_formatter_output(\n      \"\"\"\n      <p>first <%= @name %>second</p>\n      \"\"\",\n      \"\"\"\n      <p>\n        first <%= @name %>second\n      </p>\n      \"\"\",\n      line_length: 10\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <p>first<%= @name %> second</p>\n      \"\"\",\n      \"\"\"\n      <p>\n        first<%= @name %> second\n      </p>\n      \"\"\",\n      line_length: 20\n    )\n  end\n\n  test \"do not break between inline tags when there is no space before or after\" do\n    assert_formatter_output(\n      \"\"\"\n      <p>first <span>name</span>second</p>\n      \"\"\",\n      \"\"\"\n      <p>\n        first <span>name</span>second\n      </p>\n      \"\"\",\n      line_length: 10\n    )\n\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <p>first<span>name</span> second</p>\n      \"\"\",\n      line_length: 40\n    )\n  end\n\n  test \"remove unwanted empty lines\" do\n    input = \"\"\"\n    <section>\n    <div>\n    <h1>    Hello</h1>\n    <h2>\n    Sub title\n    </h2>\n    </div>\n    </section>\n\n    \"\"\"\n\n    expected = \"\"\"\n    <section>\n      <div>\n        <h1>Hello</h1>\n        <h2>\n          Sub title\n        </h2>\n      </div>\n    </section>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"texts with inline elements and block elements\" do\n    input = \"\"\"\n    <div>\n      Long long long loooooooooooong text: <i>...</i>\n      <ul>\n        <li>Item 1</li>\n        <li>Item 2</li>\n      </ul>\n      Texto\n    </div>\n    \"\"\"\n\n    expected = \"\"\"\n    <div>\n      Long long long loooooooooooong text:\n      <i>...</i>\n      <ul>\n        <li>Item 1</li>\n        <li>Item 2</li>\n      </ul>\n      Texto\n    </div>\n    \"\"\"\n\n    assert_formatter_output(input, expected, line_length: 20)\n  end\n\n  test \"add indentation when there aren't any\" do\n    input = \"\"\"\n    <section>\n    <div>\n    <h1>Hello</h1>\n    </div>\n    </section>\n    \"\"\"\n\n    expected = \"\"\"\n    <section>\n      <div>\n        <h1>Hello</h1>\n      </div>\n    </section>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"break HTML into multiple lines when it doesn't fit\" do\n    input = \"\"\"\n    <p class=\"alert alert-info more-class more-class\" role=\"alert\" phx-click=\"lv:clear-flash\" phx-value-key=\"info\">\n      <%= live_flash(@flash, :info) %>\n    </p>\n    \"\"\"\n\n    expected = \"\"\"\n    <p\n      class=\"alert alert-info more-class more-class\"\n      role=\"alert\"\n      phx-click=\"lv:clear-flash\"\n      phx-value-key=\"info\"\n    >\n      <%= live_flash(@flash, :info) %>\n    </p>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"handle HTML attributes\" do\n    input = \"\"\"\n    <p class=\"alert alert-info\" phx-click=\"lv:clear-flash\" phx-value-key=\"info\">\n      <%= live_flash(@flash, :info) %>\n    </p>\n    \"\"\"\n\n    assert_formatter_doesnt_change(input)\n  end\n\n  test \"fix indentation when everything is inline\" do\n    input = \"\"\"\n    <section><div><h1>Hello</h1></div></section>\n    \"\"\"\n\n    expected = \"\"\"\n    <section>\n      <div>\n        <h1>Hello</h1>\n      </div>\n    </section>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"fix indentation when it fits inline\" do\n    input = \"\"\"\n    <section id=\"id\" phx-hook=\"PhxHook\">\n      <.component\n        image_url={@url} />\n    </section>\n    \"\"\"\n\n    expected = \"\"\"\n    <section id=\"id\" phx-hook=\"PhxHook\">\n      <.component image_url={@url} />\n    </section>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"keep attributes at the same line if it fits 98 characters (default)\" do\n    input = \"\"\"\n    <Component foo=\"...........\" bar=\"...............\" baz=\"............\" qux=\"...................\" />\n    \"\"\"\n\n    assert_formatter_doesnt_change(input)\n  end\n\n  test \"keep attributes in separate lines if written as such\" do\n    input = \"\"\"\n    <Component\n      foo=\"...\"\n      bar=\"...\"\n      baz=\"...\"\n      qux=\"...\"\n    >\n      Foo\n    </Component>\n    \"\"\"\n\n    assert_formatter_doesnt_change(input)\n  end\n\n  test \"break attributes into multiple lines in case it doesn't fit 98 characters (default)\" do\n    input = \"\"\"\n    <div foo=\"...........\" bar=\".....................\" baz=\".................\" qux=\"....................\">\n    <p><%= @user.name %></p>\n    </div>\n    \"\"\"\n\n    expected = \"\"\"\n    <div\n      foo=\"...........\"\n      bar=\".....................\"\n      baz=\".................\"\n      qux=\"....................\"\n    >\n      <p><%= @user.name %></p>\n    </div>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"single line inputs are not changed\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <div />\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <.component with=\"attribute\" />\n    \"\"\")\n  end\n\n  test \"handle if/else/end block\" do\n    input = \"\"\"\n    <%= if true do %>\n    <p>do something</p><p>more stuff</p>\n    <% else %>\n    <p>do something else</p><p>more stuff</p>\n    <% end %>\n    \"\"\"\n\n    expected = \"\"\"\n    <%= if true do %>\n      <p>do something</p><p>more stuff</p>\n    <% else %>\n      <p>do something else</p><p>more stuff</p>\n    <% end %>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"handle if/end block\" do\n    input = \"\"\"\n    <%= if true do %><p>do something</p>\n    <% end %>\n    \"\"\"\n\n    expected = \"\"\"\n    <%= if true do %>\n      <p>do something</p>\n    <% end %>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"handle case/end block\" do\n    input = \"\"\"\n    <div>\n    <%= case {:ok, \"elixir\"} do %>\n    <% {:ok, text} -> %>\n    <%= text %>\n    <p>text</p>\n    <div />\n    <% {:error, error} -> %>\n    <%= error %>\n    <p>error</p>\n    <div />\n    <% end %>\n    </div>\n    \"\"\"\n\n    expected = \"\"\"\n    <div>\n      <%= case {:ok, \"elixir\"} do %>\n        <% {:ok, text} -> %>\n          <%= text %>\n          <p>text</p>\n          <div />\n        <% {:error, error} -> %>\n          <%= error %>\n          <p>error</p>\n          <div />\n      <% end %>\n    </div>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"format when there are curly interpolations\" do\n    input = \"\"\"\n      <section>\n        <p>pre{@user.name}pos</p>\n        <p>pre { @user.name}pos</p>\n        <p>pre{@user.name } pos</p>\n        <p>pre { @user.name } pos</p>\n      </section>\n    \"\"\"\n\n    expected = \"\"\"\n    <section>\n      <p>pre{@user.name}pos</p>\n      <p>pre {@user.name}pos</p>\n      <p>pre{@user.name} pos</p>\n      <p>pre {@user.name} pos</p>\n    </section>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"avoids additional whitespace on curly\" do\n    assert_formatter_doesnt_change(\n      \"\"\"\n      {@value}<span\n        :if={is_nil(@value)}\n        class={@class}\n        aria-label={@accessibility_text}\n        {@rest}\n      >{@placeholder}</span>\n      \"\"\",\n      line_length: 50\n    )\n  end\n\n  test \"avoids additional whitespace on curly with html comments\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <select>\n      <!-- Comment -->\n      {hello + world}\n    </select>\n    \"\"\")\n  end\n\n  test \"migrates from eex to curly braces\" do\n    input = \"\"\"\n      <section>\n        <p><%= @user.name %></p>\n        <p><%= \"{\" %></p>\n        <script>window.url = \"<%= @user.name %>\"</script>\n      </section>\n    \"\"\"\n\n    expected = \"\"\"\n    <section>\n      <p>{@user.name}</p>\n      <p><%= \"{\" %></p>\n      <script>\n        window.url = \"<%= @user.name %>\"\n      </script>\n    </section>\n    \"\"\"\n\n    assert_formatter_output(input, expected, migrate_eex_to_curly_interpolation: true)\n  end\n\n  test \"format when there are EEx tags\" do\n    input = \"\"\"\n      <section>\n        <%= live_redirect to: \"url\", id: \"link\", role: \"button\" do %>\n          <div>     <p>content 1</p><p>content 2</p></div>\n        <% end %>\n        <p><%= @user.name %></p>\n        <%= if true do %> <p>it worked</p><% else %><p> it failed </p><% end %>\n      </section>\n    \"\"\"\n\n    expected = \"\"\"\n    <section>\n      <%= live_redirect to: \"url\", id: \"link\", role: \"button\" do %>\n        <div>\n          <p>content 1</p><p>content 2</p>\n        </div>\n      <% end %>\n      <p><%= @user.name %></p>\n      <%= if true do %>\n        <p>it worked</p>\n      <% else %>\n        <p>it failed</p>\n      <% end %>\n    </section>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"does not add newline after DOCTYPE\" do\n    input = \"\"\"\n    <!DOCTYPE html>\n    <html lang=\"en\">\n      <head>\n        <meta charset=\"utf-8\" />\n      </head>\n      <body>\n        <%= @inner_content %>\n      </body>\n    </html>\n    \"\"\"\n\n    assert_formatter_doesnt_change(input)\n  end\n\n  test \"format tags with attributes without value\" do\n    assert_formatter_output(\n      \"\"\"\n\n        <button class=\"btn-primary\" autofocus disabled> Submit </button>\n\n      \"\"\",\n      \"\"\"\n      <button class=\"btn-primary\" autofocus disabled> Submit </button>\n      \"\"\"\n    )\n\n    assert_formatter_output(\n      \"\"\"\n\n        <button class=\"btn-primary\" autofocus disabled>Submit</button>\n\n      \"\"\",\n      \"\"\"\n      <button class=\"btn-primary\" autofocus disabled>Submit</button>\n      \"\"\"\n    )\n  end\n\n  test \"parse EEx inside of html tags\" do\n    assert_formatter_output(\n      \"\"\"\n        <button {build_phx_attrs_dynamically()}>Test</button>\n      \"\"\",\n      \"\"\"\n      <button {build_phx_attrs_dynamically()}>Test</button>\n      \"\"\"\n    )\n  end\n\n  test \"lines with inline or EEx tags\" do\n    assert_formatter_output(\n      \"\"\"\n        <p><span>this is a long long long long long looooooong text</span> <%= @product.value %> and more stuff over here</p>\n      \"\"\",\n      \"\"\"\n      <p>\n        <span>this is a long long long long long looooooong text</span> <%= @product.value %> and more stuff over here\n      </p>\n      \"\"\"\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <p>first <span>name</span> second</p>\n      \"\"\",\n      \"\"\"\n      <p>\n        first\n        <span>name</span>\n        second\n      </p>\n      \"\"\",\n      line_length: 10\n    )\n  end\n\n  test \"text between inline elements\" do\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <span><%= @user_a %></span>\n      X\n      <span><%= @user_b %></span>\n      \"\"\",\n      line_length: 27\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <span><%= @user_a %></span>\n      X\n      <span><%= @user_b %></span>\n      \"\"\",\n      \"\"\"\n      <span><%= @user_a %></span> X <span><%= @user_b %></span>\n      \"\"\"\n    )\n\n    assert_formatter_doesnt_change(\"\"\"\n    <span><%= @user_a %></span> X <span><%= @user_b %></span>\n    \"\"\")\n\n    assert_formatter_output(\n      \"\"\"\n      <span><%= @user_a %></span> X <span><%= @user_b %></span>\n      \"\"\",\n      \"\"\"\n      <span><%= @user_a %></span>\n      X\n      <span><%= @user_b %></span>\n      \"\"\",\n      line_length: 5\n    )\n\n    assert_formatter_doesnt_change(\"\"\"\n    <span><%= link(\"Edit\", to: Routes.post_path(@conn, :edit, @post)) %></span>\n    | <span><%= link(\"Back\", to: Routes.post_path(@conn, :index)) %></span>\n    \"\"\")\n  end\n\n  test \"handle EEx cond statement\" do\n    input = \"\"\"\n    <div>\n    <%= cond do %>\n    <% 1 == 1 -> %>\n    <%= \"Hello\" %>\n    <% 2 == 2 -> %>\n    <%= \"World\" %>\n    <% true -> %>\n    <%= \"\" %>\n    <% end %>\n    </div>\n    \"\"\"\n\n    expected = \"\"\"\n    <div>\n      <%= cond do %>\n        <% 1 == 1 -> %>\n          <%= \"Hello\" %>\n        <% 2 == 2 -> %>\n          <%= \"World\" %>\n        <% true -> %>\n          <%= \"\" %>\n      <% end %>\n    </div>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"proper format elixir functions\" do\n    input = \"\"\"\n    <div>\n    <%= live_component(MyAppWeb.Components.SearchBox, id: :search_box, on_select: :user_selected, label: gettext(\"Search User\")) %>\n    </div>\n    \"\"\"\n\n    expected = \"\"\"\n    <div>\n      <%= live_component(MyAppWeb.Components.SearchBox,\n        id: :search_box,\n        on_select: :user_selected,\n        label: gettext(\"Search User\")\n      ) %>\n    </div>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"does not add parentheses when tag is configured to not to\" do\n    input = \"\"\"\n    <%= text_input f, :name %>\n    \"\"\"\n\n    expected = \"\"\"\n    <%= text_input f, :name %>\n    \"\"\"\n\n    assert_formatter_output(input, expected, locals_without_parens: [text_input: 2])\n  end\n\n  test \"does not add a line break in the first line\" do\n    assert_formatter_output(\n      \"\"\"\n      <%= @user.name %>\n      \"\"\",\n      \"\"\"\n      <%= @user.name %>\n      \"\"\"\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <div />\n      \"\"\",\n      \"\"\"\n      <div />\n      \"\"\"\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <% \"Hello\" %>\n      \"\"\",\n      \"\"\"\n      <% \"Hello\" %>\n      \"\"\"\n    )\n  end\n\n  test \"use the configured line_length for breaking texts into new lines\" do\n    input = \"\"\"\n      <p>My title</p>\n    \"\"\"\n\n    expected = \"\"\"\n    <p>\n      My title\n    </p>\n    \"\"\"\n\n    assert_formatter_output(input, expected, line_length: 5)\n  end\n\n  test \"doesn't break lines when tag doesn't have any attrs and it fits using the configured line length\" do\n    input = \"\"\"\n      <p>\n      My title\n      </p>\n      <p>This is tooooooooooooooooooooooooooooooooooooooo looooooong annnnnnnnnnnnnnd should breeeeeak liines</p>\n      <p class=\"some-class\">Should break line</p>\n      <p><%= @user.name %></p>\n      should not break when there it is not wrapped by any tags\n    \"\"\"\n\n    expected = \"\"\"\n    <p>\n      My title\n    </p>\n    <p>\n      This is tooooooooooooooooooooooooooooooooooooooo looooooong annnnnnnnnnnnnnd should breeeeeak liines\n    </p>\n    <p class=\"some-class\">Should break line</p>\n    <p><%= @user.name %></p>\n    should not break when there it is not wrapped by any tags\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"does not break lines for single long attributes\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <h1 class=\"font-medium leading-tight text-5xl mt-0 mb-2 text-blue-600 text-sm sm:text-sm lg:text-sm font-semibold\">\n      Title\n    </h1>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <div class=\"font-medium leading-tight text-5xl mt-0 mb-2 text-blue-600 text-sm sm:text-sm lg:text-sm font-semibold\" />\n    \"\"\")\n  end\n\n  test \"changes expr to literal when it is an string\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <div class={@id} />\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <div class={some_function(:foo, :bar)} />\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <div class={\n      # test\n      \"mx-auto\"\n    } />\n    \"\"\")\n\n    assert_formatter_output(\n      \"\"\"\n      <div class={\"mx-auto flex-shrink-0 flex items-center justify-center h-8 w-8 rounded-full bg-purple-100 sm:mx-0\"}>\n        Content\n      </div>\n      \"\"\",\n      \"\"\"\n      <div class=\"mx-auto flex-shrink-0 flex items-center justify-center h-8 w-8 rounded-full bg-purple-100 sm:mx-0\">\n        Content\n      </div>\n      \"\"\"\n    )\n  end\n\n  test \"does not break lines when tag doesn't contain content\" do\n    input = \"\"\"\n    <thead>\n      <tr>\n        <th>Name</th>\n        <th>Age</th>\n        <th></th>\n        <th>\n        </th>\n      </tr>\n    </thead>\n    \"\"\"\n\n    expected = \"\"\"\n    <thead>\n      <tr>\n        <th>Name</th>\n        <th>Age</th>\n        <th></th>\n        <th></th>\n      </tr>\n    </thead>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"handle case statement within for statement\" do\n    input = \"\"\"\n    <tr>\n      <%= for value <- @values do %>\n        <td class=\"border-2\">\n          <%= case value.type do %>\n          <% :text -> %>\n          Do something\n          <p>Hello</p>\n          <% _ -> %>\n          Do something else\n          <p>Hello</p>\n          <% end %>\n        </td>\n      <% end %>\n    </tr>\n    \"\"\"\n\n    expected = \"\"\"\n    <tr>\n      <%= for value <- @values do %>\n        <td class=\"border-2\">\n          <%= case value.type do %>\n            <% :text -> %>\n              Do something\n              <p>Hello</p>\n            <% _ -> %>\n              Do something else\n              <p>Hello</p>\n          <% end %>\n        </td>\n      <% end %>\n    </tr>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"proper indent if when it is in the beginning of the template\" do\n    input = \"\"\"\n    <%= if @live_action == :edit do %>\n    <.modal return_to={Routes.store_index_path(@socket, :index)}>\n      <.live_component\n        id={@product.id}\n        module={MystoreWeb.ReserveFormComponent}\n        action={@live_action}\n        product={@product}\n        return_to={Routes.store_index_path(@socket, :index)}\n      />\n    </.modal>\n    <% end %>\n    \"\"\"\n\n    expected = \"\"\"\n    <%= if @live_action == :edit do %>\n      <.modal return_to={Routes.store_index_path(@socket, :index)}>\n        <.live_component\n          id={@product.id}\n          module={MystoreWeb.ReserveFormComponent}\n          action={@live_action}\n          product={@product}\n          return_to={Routes.store_index_path(@socket, :index)}\n        />\n      </.modal>\n    <% end %>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"handle void elements\" do\n    input = \"\"\"\n    <div>\n    <link rel=\"shortcut icon\" href={~p\"/images/favicon.png\"} type=\"image/x-icon\">\n    <p>some text</p>\n    <br>\n    <hr>\n    <input type=\"text\" value=\"Foo Bar\">\n    <img src=\"./image.png\">\n    </div>\n    \"\"\"\n\n    expected = \"\"\"\n    <div>\n      <link rel=\"shortcut icon\" href={~p\"/images/favicon.png\"} type=\"image/x-icon\" />\n      <p>some text</p>\n      <br />\n      <hr />\n      <input type=\"text\" value=\"Foo Bar\" />\n      <img src=\"./image.png\" />\n    </div>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"format expressions within attributes\" do\n    input = \"\"\"\n      <.modal\n        id={id}\n        on_cancel={focus(\"#1\", \"#delete-song-1\")}\n        on_confirm={JS.push(\"delete\", value: %{id: song.id})\n                    |> hide_modal(id)\n                    |> focus_closest(\"#song-1\")\n                    |> hide(\"#song-1\")}\n      />\n    \"\"\"\n\n    expected = \"\"\"\n    <.modal\n      id={id}\n      on_cancel={focus(\"#1\", \"#delete-song-1\")}\n      on_confirm={\n        JS.push(\"delete\", value: %{id: song.id})\n        |> hide_modal(id)\n        |> focus_closest(\"#song-1\")\n        |> hide(\"#song-1\")\n      }\n    />\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"keep intentional line breaks\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <section>\n      <h1>\n        <b>\n          <%= @user.first_name %> <%= @user.last_name %>\n        </b>\n      </h1>\n\n      <div>\n        <p>test</p>\n      </div>\n\n      <h2>Subtitle</h2>\n    </section>\n    \"\"\")\n\n    assert_formatter_output(\n      \"\"\"\n        <p>\n          $ <%= @product.value %> in Dollars\n        </p>\n        <button>\n          Submit\n        </button>\n      \"\"\",\n      \"\"\"\n      <p>\n        $ <%= @product.value %> in Dollars\n      </p>\n      <button>\n        Submit\n      </button>\n      \"\"\"\n    )\n  end\n\n  test \"keep EEx expressions in the next line\" do\n    input = \"\"\"\n    <div class=\"mb-5\">\n      <%= live_file_input(@uploads.image_url) %>\n      <%= error_tag(f, :image_url) %>\n    </div>\n    \"\"\"\n\n    assert_formatter_doesnt_change(input)\n  end\n\n  test \"keep intentional extra line break between EEx expressions\" do\n    input = \"\"\"\n    <div class=\"mb-5\">\n      <%= live_file_input(@uploads.image_url) %>\n\n      <%= error_tag(f, :image_url) %>\n    </div>\n    \"\"\"\n\n    assert_formatter_doesnt_change(input)\n  end\n\n  test \"force unfit when there are line breaks in the text\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <b>\n      Text\n      Text\n      Text\n    </b>\n    <p>\n      Text\n      Text\n      Text\n    </p>\n    \"\"\")\n\n    assert_formatter_output(\n      \"\"\"\n      <b>\\s\\s\n      \\tText\n        Text\n      \\tText\n      </b>\n      \"\"\",\n      \"\"\"\n      <b>\n        Text\n        Text\n        Text\n      </b>\n      \"\"\"\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <b>\\s\\s\n      \\tText\n      \\t\n      \\tText\n      </b>\n      \"\"\",\n      \"\"\"\n      <b>\n        Text\n\n        Text\n      </b>\n      \"\"\"\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <b>\\s\\s\n      \\t\n      \\tText\n      \\t\n      \\t\n      \\tText\n      \\t\n      </b>\n      \"\"\",\n      \"\"\"\n      <b>\n        Text\n\n        Text\n      </b>\n      \"\"\"\n    )\n  end\n\n  test \"doesn't format content within <pre>\" do\n    assert_formatter_output(\n      \"\"\"\n      <div>\n      <pre>\n      Text\n      Text\n      </pre>\n      </div>\n      \"\"\",\n      \"\"\"\n      <div>\n        <pre>\n      Text\n      Text\n      </pre>\n      </div>\n      \"\"\"\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <div><pre>Text\n      Text</pre></div>\n      \"\"\",\n      \"\"\"\n      <div>\n        <pre>Text\n      Text</pre>\n      </div>\n      \"\"\"\n    )\n\n    assert_formatter_doesnt_change(\"\"\"\n    <pre>\n    Text\n      <div>\n          Text\n        </div>\n    </pre>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <pre><code><div>\n    <p>Text</p>\n    <%= if true do %>\n      Hi\n    <% else %>\n      Ho\n    <% end %>\n    <p>Text</p>\n    </div></code>\n    </pre>\n    \"\"\")\n  end\n\n  test \"format <pre> tag with EEx\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <pre>\n      :root &lbrace;\n        <%= 2 + 2 %>\n        <%= 2 + 2 %>\n      }\n    </pre>\n    \"\"\")\n  end\n\n  test \"format label block correctly\" do\n    input = \"\"\"\n    <%= label @f, :email_address, class: \"text-gray font-medium\" do %> Email Address\n    <% end %>\n    \"\"\"\n\n    expected = \"\"\"\n    <%= label @f, :email_address, class: \"text-gray font-medium\" do %>\n      Email Address\n    <% end %>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"formats script tag\" do\n    assert_formatter_output(\n      \"\"\"\n      <body>\n\n      text\n        <div><script>\n      const foo = 1;\n      const map = {\n        a: 1,\n        b: 2,\n      };\n      console.log(foo);\n      </script></div>\n      </body>\n      \"\"\",\n      \"\"\"\n      <body>\n        text\n        <div>\n          <script>\n            const foo = 1;\n            const map = {\n              a: 1,\n              b: 2,\n            };\n            console.log(foo);\n          </script>\n        </div>\n      </body>\n      \"\"\"\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <body>\n\n      text\n        <div><script>\n      \\t\\tconst foo = 1;\n      \\s\\s\n          const map = {\n            a: 1,\n            b: 2,\n          };\n      \\t\n      \\s\\s\\s\\sconsole.log(foo);\n        </script></div>\n\n      </body>\n      \"\"\",\n      \"\"\"\n      <body>\n        text\n        <div>\n          <script>\n            const foo = 1;\n\n            const map = {\n              a: 1,\n              b: 2,\n            };\n\n            console.log(foo);\n          </script>\n        </div>\n      </body>\n      \"\"\"\n    )\n  end\n\n  test \"formats EEx within script tag\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <script>\n      var foo = 1;\n      var bar = <%= @bar %>\n      var baz = <%= @baz %>\n      console.log(1)\n    </script>\n    \"\"\")\n\n    assert_formatter_output(\n      \"\"\"\n      <script type=\"text/props\">\n        <%= %{\n        a: 1,\n        b: 2\n      } %>\n      </script>\n      \"\"\",\n      \"\"\"\n      <script type=\"text/props\">\n          <%= %{\n          a: 1,\n          b: 2\n        } %>\n      </script>\n      \"\"\"\n    )\n\n    assert_formatter_doesnt_change(\"\"\"\n    <script type=\"text/props\">\n        <%= raw(Jason.encode!(%{whatEndpoint: Routes.api_search_options_path(@conn, :role_search_options)},\n      escape: :html_safe)) %>\n    </script>\n    \"\"\")\n  end\n\n  test \"formats EEx blocks within script tag\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <script>\n      var foo = 1;\n      <%= if @bar do %>\n      var bar = 2;\n      <% end %>\n    </script>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <script>\n      var foo = 1;\n      <% if @bar do %>\n      var bar = 2;\n      <% end %>\n    </script>\n    \"\"\")\n  end\n\n  test \"formats style tag\" do\n    input = \"\"\"\n    <div>\n    <style>\n    h1 {\n      font-weight: 900;\n    }\n    </style>\n    </div>\n    \"\"\"\n\n    expected = \"\"\"\n    <div>\n      <style>\n        h1 {\n          font-weight: 900;\n        }\n      </style>\n    </div>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"format style tag with EEx\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <style>\n      :root {\n        <%= 2 + 2 %>\n        <%= 2 + 2 %>\n      }\n    </style>\n    \"\"\")\n  end\n\n  test \"handle HTML comments but doesn't format it\" do\n    assert_formatter_output(\n      \"\"\"\n          <!-- Inline comment -->\n      <section>\n        <!-- commenting out this div\n        <div>\n          <p><%= @user.name %></p>\n          <p\n            class=\"my-class\">\n            text\n          </p>\n        </div>\n           -->\n      </section>\n      \"\"\",\n      \"\"\"\n      <!-- Inline comment -->\n      <section>\n        <!-- commenting out this div\n        <div>\n          <p><%= @user.name %></p>\n          <p\n            class=\"my-class\">\n            text\n          </p>\n        </div>\n           -->\n      </section>\n      \"\"\"\n    )\n\n    assert_formatter_doesnt_change(\"\"\"\n    <!-- Modal content -->\n    <%= render_slot(@inner_block) %>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <!-- a comment -->\n    <!-- a comment -->\n    \"\"\")\n  end\n\n  test \"handle case end when previous block is blank\" do\n    input = \"\"\"\n    <%= case :foo do %>\n      <% :foo -> %>\n        something\n      <% _ -> %>\n      <% end %>\n    \"\"\"\n\n    expected = \"\"\"\n    <%= case :foo do %>\n      <% :foo -> %>\n        something\n      <% _ -> %>\n    <% end %>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"keep intentional spaces\" do\n    input = \"\"\"\n    <p>\n            Last <%= length(@backlog_feeds) %> of <%= @feedcount %> backlog feeds </p>\n    \"\"\"\n\n    expected = \"\"\"\n    <p>\n      Last <%= length(@backlog_feeds) %> of <%= @feedcount %> backlog feeds\n    </p>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"handle comment block with eex\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <div></div>\n    <!-- <%= \"comment\" %> -->\n    <div></div>\n    \"\"\")\n  end\n\n  test \"handle spaces properly\" do\n    input = \"\"\"\n    <button>\n      <i class=\"fa-solid fa-xmark\"></i>\n      Close\n    </button>\n    \"\"\"\n\n    expected = \"\"\"\n    <button>\n      <i class=\"fa-solid fa-xmark\"></i> Close\n    </button>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"keep at least one space around inline tags\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <b>Foo: </b>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <b> Foo: </b>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <b> Foo:</b>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <b>{code}: </b>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <b> :{code}</b>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <p>\n      <b>Foo: </b>bar\n    </p>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <p>\n      <b>Foo: </b><span>bar</span>\n    </p>\n    \"\"\")\n\n    assert_formatter_output(\n      \"\"\"\n      <p> <span>bar </span> </p>\n      \"\"\",\n      \"\"\"\n      <p><span>bar </span></p>\n      \"\"\"\n    )\n\n    assert_formatter_doesnt_change(\"\"\"\n    <p><b>Foo: </b><span>bar</span></p>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <p>\n      <b>Foo: </b><%= some_var %>\n    </p>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <p>\n      <b>Foo:</b><%= some_var %>\n    </p>\n    \"\"\")\n\n    assert_formatter_output(\n      \"\"\"\n      <b>      Foo  Bar    </b>\n      \"\"\",\n      \"\"\"\n      <b> Foo  Bar </b>\n      \"\"\"\n    )\n\n    assert_formatter_doesnt_change(\"\"\"\n    <b> Foo Bar </b>\n    \"\"\")\n\n    assert_formatter_output(\n      \"\"\"\n      <b>Foo:    </b>\n      \"\"\",\n      \"\"\"\n      <b>Foo: </b>\n      \"\"\"\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <b>        Foo: </b>\n      \"\"\",\n      \"\"\"\n      <b> Foo: </b>\n      \"\"\"\n    )\n  end\n\n  test \"doesn't add extra spaces to inline tags with nested inline tags with leading whitespace\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <a>foo<b>bar</b></a>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <a>foo <b>bar</b></a>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <a> foo<b>bar</b></a>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <a> foo <b>bar</b></a>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <a>foo<b>bar</b></a>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <a>foo <b> bar</b></a>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <a> foo<b>bar </b></a>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <a> foo <b> bar </b></a>\n    \"\"\")\n  end\n\n  test \"avoids additional whitespace on text followed by interpolation\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <span class=\"opacity-70\"> -  {@name}</span>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <span class=\"opacity-70\">{@name}  - </span>\n    \"\"\")\n  end\n\n  test \"treats components with link or button in their name as inline\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <.styled_link> Foo: </.styled_link>\n    \"\"\")\n\n    assert_formatter_output(\n      \"\"\"\n      <.styled_link> Foo: </.styled_link>\n      \"\"\",\n      \"\"\"\n      <.styled_link>Foo:</.styled_link>\n      \"\"\",\n      inline_matcher: []\n    )\n\n    assert_formatter_doesnt_change(\"\"\"\n    <.styled_button_custom> Foo: </.styled_button_custom>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <.my_custom_inline_element> Foo: </.my_custom_inline_element>\n      \"\"\",\n      inline_matcher: [~r/inline_element$/]\n    )\n  end\n\n  test \"does not keep empty lines on script and styles tags\" do\n    input = \"\"\"\n    <script>\n\n    </script>\n    \"\"\"\n\n    expected = \"\"\"\n    <script>\n    </script>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n\n    input = \"\"\"\n    <style>\n\n    </style>\n    \"\"\"\n\n    expected = \"\"\"\n    <style>\n    </style>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"does not break lines in self closed elements\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <div>\n      This should not wrap on a new line <input />.\n    </div>\n    \"\"\")\n  end\n\n  test \"keep EEx along with the text\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <div>\n      _______________________________________________________ result<%= if(@row_count != 1, do: \"s\") %>\n    </div>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <div>\n      _______________________________________________________ result <%= if(@row_count != 1, do: \"s\") %>\n    </div>\n    \"\"\")\n  end\n\n  test \"keep single quote delimiter when value has quotes\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <div title='Say \"hi!\"'></div>\n    \"\"\")\n  end\n\n  test \"transform single quotes to double when value has no quotes\" do\n    input = \"\"\"\n    <div title='Say hi!'></div>\n    \"\"\"\n\n    expected = \"\"\"\n    <div title=\"Say hi!\"></div>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"does not format inline elements surrounded by texts without white spaces\" do\n    assert_formatter_output(\n      \"\"\"\n      <p>\n        text text text<a class=\"text-blue-500\" href=\"\" target=\"_blank\" attr1=\"\">link</a>\n      </p>\n      \"\"\",\n      \"\"\"\n      <p>\n        text text text<a\n          class=\"text-blue-500\"\n          href=\"\"\n          target=\"_blank\"\n          attr1=\"\"\n        >link</a>\n      </p>\n      \"\"\",\n      line_length: 50\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <p>\n        first <a class=\"text-blue-500\" href=\"\" target=\"_blank\" attr1=\"\">link</a>second.\n      </p>\n      \"\"\",\n      \"\"\"\n      <p>\n        first <a\n          class=\"text-blue-500\"\n          href=\"\"\n          target=\"_blank\"\n          attr1=\"\"\n        >link</a>second.\n      </p>\n      \"\"\",\n      line_length: 50\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <p>\n        <a class=\"text-blue-500\" href=\"\" target=\"_blank\" attr1=\"\">link</a>text text text text.\n      </p>\n      \"\"\",\n      \"\"\"\n      <p>\n        <a\n          class=\"text-blue-500\"\n          href=\"\"\n          target=\"_blank\"\n          attr1=\"\"\n        >link</a>text text text text.\n      </p>\n      \"\"\",\n      line_length: 50\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <p>\n        <a class=\"text-blue-500\" href=\"\" target=\"_blank\" attr1=\"\">link</a>{code}.\n      </p>\n      \"\"\",\n      \"\"\"\n      <p>\n        <a\n          class=\"text-blue-500\"\n          href=\"\"\n          target=\"_blank\"\n          attr1=\"\"\n        >link</a>{code}.\n      </p>\n      \"\"\",\n      line_length: 50\n    )\n\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <p>\n        long line of text <span>span 1</span>\n        more text <span>span 2</span>\n      </p>\n      \"\"\",\n      line_length: 45\n    )\n  end\n\n  test \"preserve inline element when there aren't whitespaces\" do\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <b>foo</b><i><span>bar</span></i><span>baz</span>\n      \"\"\",\n      line_length: 20\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <b>Foo</b><i>bar</i> <span>baz</span>\n      \"\"\",\n      \"\"\"\n      <b>Foo</b><i>bar</i>\n      <span>baz</span>\n      \"\"\",\n      line_length: 20\n    )\n\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <b>foo</b><i>bar</i><%= @user.name %><span>baz</span>\n      \"\"\",\n      line_length: 20\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <b>foo</b><i><span id=\"myspan\" class=\"a long list of classes\">bar</span></i><span>baz</span>\n      \"\"\",\n      \"\"\"\n      <b>foo</b><i><span\n        id=\"myspan\"\n        class=\"a long list of classes\"\n      >bar</span></i><span>baz</span>\n      \"\"\",\n      line_length: 20\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <b>foo</b><i><span><div>bar</div></span></i><span>baz</span>\n      \"\"\",\n      \"\"\"\n      <b>foo</b><i><span><div>\n        bar\n      </div></span></i><span>baz</span>\n      \"\"\",\n      line_length: 20\n    )\n  end\n\n  test \"does not add space between elements without space\" do\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <span>foo</span><span>bar</span>\n      <span>foo</span><.foo_bar_baz />\n      <.foo_bar_baz /><span>bar</span>\n      <div>foo</div><div>\n        bar\n      </div>\n      <div>foo</div><.foo_bar_baz />\n      <.foo_bar_baz /><div>\n        bar\n      </div>\n      \"\"\",\n      line_length: 20\n    )\n  end\n\n  test \"preserve inline element on the same line when followed by a EEx expression without whitespaces\" do\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <%= some_function(\"arg\") %><span>content</span>\n      \"\"\",\n      line_length: 25\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <%= some_function(\"arg\") %> <span>content</span>\n      \"\"\",\n      \"\"\"\n      <%= some_function(\"arg\") %>\n      <span>content</span>\n      \"\"\",\n      line_length: 25\n    )\n  end\n\n  test \"does not format when contenteditable is present\" do\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <div contenteditable>The content content content content content</div>\n      \"\"\",\n      line_length: 10\n    )\n\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <div contenteditable=\"true\">The content content content content content</div>\n      \"\"\",\n      line_length: 10\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <div contenteditable=\"false\">The content content content content content</div>\n      \"\"\",\n      \"\"\"\n      <div contenteditable=\"false\">\n        The content content content content content\n      </div>\n      \"\"\",\n      line_length: 10\n    )\n  end\n\n  test \"does not format textarea\" do\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <textarea><%= @content %></textarea>\n      \"\"\",\n      line_length: 5\n    )\n\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <textarea>\n        <div\n        class=\"one\"\n        id=\"two\"\n      >\n      <outside />\n        </div>\n      </textarea>\n      \"\"\",\n      line_length: 5\n    )\n\n    assert_formatter_doesnt_change(\"\"\"\n    <textarea />\n    \"\"\")\n\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <textarea></textarea>\n      \"\"\",\n      line_length: 5\n    )\n  end\n\n  test \"keeps right format for inline elements within block elements\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <section>\n      <svg\n        id=\"game\"\n        viewBox=\"0 0 1000 1000\"\n        width=\"1000\"\n        height=\"1000\"\n        class=\"bg-white dark:bg-zinc-900 shadow mx-auto\"\n      >\n        <defs>\n          <pattern id=\"tenthGrid\" width=\"10\" height=\"10\" patternUnits=\"userSpaceOnUse\">\n            <path d=\"M 10 0 L 0 0 0 10\" fill=\"none\" stroke=\"silver\" stroke-width=\"0.5\" />\n          </pattern>\n        </defs>\n      </svg>\n    </section>\n    \"\"\")\n  end\n\n  test \"respects heex_line_length\" do\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <p>\n        <strong>Please let me be in the same line</strong> Value <strong>Please let me be in the same line</strong>.\n      </p>\n      \"\"\",\n      heex_line_length: 1000\n    )\n  end\n\n  test \"does not format when phx-no-format attr is present\" do\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <.textarea phx-no-format>My content</.textarea>\n      \"\"\",\n      line_length: 5\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <script phx-no-format><%= raw(js_code()) %></script>\n      \"\"\",\n      \"\"\"\n      <script phx-no-format><%= raw(js_code()) %></script>\n      \"\"\",\n      line_length: 5\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <span phx-no-format class=\"underline\">Check</span> Messages\n      \"\"\",\n      \"\"\"\n      <span\n        phx-no-format\n        class=\"underline\"\n      >Check</span> Messages\n      \"\"\",\n      line_length: 5\n    )\n  end\n\n  test \"respect interpolation when phx-no-format is present\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <title data-prefix={@prefix} data-default={@default} data-suffix={@suffix} phx-no-format>{@prefix}{render_present(render_slot(@inner_block), @default)}{@suffix}</title>\n    \"\"\")\n  end\n\n  test \"respect nesting of children when phx-no-format is present\" do\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <ul class=\"root\" phx-no-format><!-- comment\n      --><%= for user <- @users do %>\n          <li class=\"list\">\n            <div class=\"child1\">\n              <span class=\"child2\">text</span>\n            </div>\n          </li>\n        <% end %><!-- comment\n      --></ul>\n      \"\"\",\n      line_length: 100\n    )\n\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <ul class=\"root\" phx-no-format>\n      <li class=\"list\">\n          <div\n          class=\"child1\">\n        <span class=\"child2\">text</span>\n          </div>\n      </li>\n      </ul>\n      \"\"\",\n      line_length: 100\n    )\n  end\n\n  test \"order :let :for and :if over HTML attributes\" do\n    assert_formatter_output(\n      \"\"\"\n      <.form for={@changeset} :let={f} class=\"form\">\n        <%= input(f, :foo) %>\n      </.form>\n      \"\"\",\n      \"\"\"\n      <.form :let={f} for={@changeset} class=\"form\">\n        <%= input(f, :foo) %>\n      </.form>\n      \"\"\"\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <div :for={item <- @items} :if={true} :let={@name} />\n      \"\"\",\n      \"\"\"\n      <div :let={@name} :for={item <- @items} :if={true} />\n      \"\"\"\n    )\n\n    assert_formatter_output(\n      \"\"\"\n      <div id=\"id\" class=\"class\" :if={true} :for={item <- @items} :let={@name} />\n      \"\"\",\n      \"\"\"\n      <div :let={@name} :for={item <- @items} :if={true} id=\"id\" class=\"class\" />\n      \"\"\"\n    )\n  end\n\n  test \"handle html comments + EEx expressions\" do\n    assert_formatter_output(\n      \"\"\"\n      <%= if @comment do %><!-- <%= @comment %> --><% end %>\n      \"\"\",\n      \"\"\"\n      <%= if @comment do %>\n        <!-- <%= @comment %> -->\n      <% end %>\n      \"\"\"\n    )\n  end\n\n  test \"keeps self-closing slots on separate lines\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <.func>\n      <:slot />\n      <:slot attr=\"foo\" />\n    </.func>\n    \"\"\")\n  end\n\n  test \"keep intentional line breaks between slots\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <.component>\n      <:title>Guides & Docs</:title>\n\n      <:desc>View our step-by-step guides, or browse the comprehensive API docs</:desc>\n    </.component>\n    \"\"\")\n\n    assert_formatter_output(\n      \"\"\"\n      <.component>\n\n        <:title>Guides & Docs</:title>\n\n\n        <:desc>View our step-by-step guides, or browse the comprehensive API docs</:desc>\n\n      </.component>\n      \"\"\",\n      \"\"\"\n      <.component>\n        <:title>Guides & Docs</:title>\n\n        <:desc>View our step-by-step guides, or browse the comprehensive API docs</:desc>\n      </.component>\n      \"\"\"\n    )\n  end\n\n  test \"does not not break lines for long css lines when there are interpolation\" do\n    assert_formatter_doesnt_change(\n      ~S\"\"\"\n      <div class={\"#{@errors} mt-1 block w-full\"}>\n        Hi\n      </div>\n      \"\"\",\n      heex_line_length: 10\n    )\n\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <div class={@errors <> \"mt-1 block w-full\"}>\n        Hi\n      </div>\n      \"\"\",\n      heex_line_length: 10\n    )\n  end\n\n  test \"break text to next line when previous inline element is indented\" do\n    assert_formatter_output(\n      \"\"\"\n      <p>foo <strong class=\"foo bar baz\"> <%= some_function() %></strong> baz</p>\n      \"\"\",\n      \"\"\"\n      <p>\n        foo\n        <strong class=\"foo bar baz\"><%= some_function() %></strong>\n        baz\n      </p>\n      \"\"\",\n      heex_line_length: 15\n    )\n  end\n\n  test \"format attrs from self tag close correctly within preserve mode\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <button>\n      <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\">\n        <path\n          fill-rule=\"evenodd\"\n          d=\"M15.707 15.707a1 1 0 01-1.414 0l-5-5a1 1 0 010-1.414l5-5a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 010 1.414zm-6 0a1 1 0 01-1.414 0l-5-5a1 1 0 010-1.414l5-5a1 1 0 011.414 1.414L5.414 10l4.293 4.293a1 1 0 010 1.414z\"\n          clip-rule=\"evenodd\"\n        />\n      </svg>Back to previous page\n    </button>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <button>\n      <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\">\n        <nest>\n          <path\n            fill-rule=\"evenodd\"\n            d=\"M15.707 15.707a1 1 0 01-1.414 0l-5-5a1 1 0 010-1.414l5-5a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 010 1.414zm-6 0a1 1 0 01-1.414 0l-5-5a1 1 0 010-1.414l5-5a1 1 0 011.414 1.414L5.414 10l4.293 4.293a1 1 0 010 1.414z\"\n            clip-rule=\"evenodd\"\n          />\n        </nest>\n      </svg>Back to previous page\n    </button>\n    \"\"\")\n  end\n\n  test \"does not break attrs\" do\n    assert_formatter_output(\n      \"\"\"\n      <button\n        type={@type}\n        class={\n          [\n            \"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 py-2 px-3 text-sm font-semibold\",\n            \"leading-6 text-white hover:bg-zinc-700 active:text-white/80\",\n            @class\n          ]\n        }\n        {@rest}\n      >\n        <%= render_slot(@inner_block) %>\n      </button>\n      \"\"\",\n      \"\"\"\n      <button\n        type={@type}\n        class={[\n          \"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 py-2 px-3 text-sm font-semibold\",\n          \"leading-6 text-white hover:bg-zinc-700 active:text-white/80\",\n          @class\n        ]}\n        {@rest}\n      >\n        <%= render_slot(@inner_block) %>\n      </button>\n      \"\"\"\n    )\n\n    assert_formatter_doesnt_change(\"\"\"\n    <div class={\n      [\n        # test\n        \"mx-auto\"\n      ]\n    } />\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <div class={\n      # test\n      [\n        \"mx-auto\"\n      ]\n    } />\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <div class={\n      [\n        # test\n        \"mx-auto\"\n      ]\n    } />\n    \"\"\")\n  end\n\n  test \"doesn't break line when tag/component is right after the text\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <p>\n      (<span label=\"application programming interface\">API</span>).\n    </p>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <p>\n      (<div label=\"application programming interface\">API</div>).\n    </p>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <p>\n      (<.abbr label=\"application programming interface\">API</.abbr>).\n    </p>\n    \"\"\")\n  end\n\n  test \"handle heredocs\" do\n    assert_formatter_output(\n      \"\"\"\n      <.component msg={\"text\"}>\n        <div />\n      </.component>\n      \"\"\",\n      \"\"\"\n      <.component msg=\"text\">\n        <div />\n      </.component>\n      \"\"\"\n    )\n\n    assert_formatter_doesnt_change(\"\"\"\n    <.component msg={\\\"\"\"\n    text\n    \\\"\"\"}>\n      <div />\n    </.component>\n    \"\"\")\n\n    assert_formatter_output(\n      \"\"\"\n      <.component id={@id} msg={\\\"\"\"\n      text\n      \\\"\"\"}>\n        <div />\n      </.component>\n      \"\"\",\n      \"\"\"\n      <.component\n        id={@id}\n        msg={\\\"\"\"\n        text\n        \\\"\"\"}\n      >\n        <div />\n      </.component>\n      \"\"\"\n    )\n  end\n\n  test \"handle var <> heredocs\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <.component id={@id} msg={@test <> \\\"\"\"\n    text\n    \\\"\"\"}>\n      <div />\n    </.component>\n    \"\"\")\n  end\n\n  test \"handle multiple HTML comments with EEx vars\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <!--\n    <button><%= @var %></button>\n    -->\n    <!-- comment -->\n    \"\"\")\n  end\n\n  test \"treats .link component as inline\" do\n    assert_formatter_doesnt_change(\n      \"\"\"\n      <.link class=\"font-semibold\" navigate={~p\"/open/file?autosave=true\"}>Browse them here</.link>.\n      \"\"\",\n      heex_line_length: 72\n    )\n  end\n\n  test \"does not format when empty\" do\n    assert_formatter_doesnt_change(\"\")\n\n    assert_formatter_doesnt_change(\"\", opening_delimiter: \"\\\"\")\n\n    assert_formatter_doesnt_change(\"\", opening_delimiter: \"\\\"\\\"\\\"\")\n  end\n\n  test \"doesn't convert <% to <%=\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <% fun = fn assigns -> %>\n      <hr />\n    <% end %>\n    \"\"\")\n  end\n\n  test \"doesn't flatten strings containing double quotes (#3336)\" do\n    assert_formatter_doesnt_change(~S\"\"\"\n    <div data-foo={\"{\\\"tag\\\": \\\"<something>\\\"}\"}></div>\n    \"\"\")\n  end\n\n  test \"keep intentional lines breaks from HTML comments\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <h1>Title</h1>\n\n    <!-- comment -->\n    <p>Text</p>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <h1>Title</h1>\n\n    <!-- comment -->\n\n    <p>Text</p>\n    \"\"\")\n\n    assert_formatter_output(\n      \"\"\"\n      <h1>Title</h1>\n\n\n      <!-- comment -->\n\n\n      <p>Text</p>\n      \"\"\",\n      \"\"\"\n      <h1>Title</h1>\n\n      <!-- comment -->\n\n      <p>Text</p>\n      \"\"\"\n    )\n  end\n\n  test \"formats HTML comments evenly with the block they belong to\" do\n    spaces = String.duplicate(\" \", 10)\n\n    input = \"\"\"\n    <section>\n    <!-- First section -->\n      <div>\n        <h1>Hello</h1>\n      </div>\n      #{spaces}\n      <!-- Second section -->\n      <div>\n        <h1>World</h1>\n      </div>\n      #{spaces}\n      <div>\n        <h1>!</h1>\n      </div>\n\n      #{spaces}\n    <!-- Indentation to be corrected -->\n\n      <div>\n        <button>Click here</button>\n      </div>\n      <!-- No line break before this one -->\n      <div>\n        <button>Cancel</button>\n      </div>\n    </section>\n    \"\"\"\n\n    expected = \"\"\"\n    <section>\n      <!-- First section -->\n      <div>\n        <h1>Hello</h1>\n      </div>\n\n      <!-- Second section -->\n      <div>\n        <h1>World</h1>\n      </div>\n\n      <div>\n        <h1>!</h1>\n      </div>\n\n      <!-- Indentation to be corrected -->\n\n      <div>\n        <button>Click here</button>\n      </div>\n      <!-- No line break before this one -->\n      <div>\n        <button>Cancel</button>\n      </div>\n    </section>\n    \"\"\"\n\n    assert_formatter_output(input, expected)\n  end\n\n  test \"handle EEx comments\" do\n    assert_formatter_doesnt_change(\"\"\"\n    <div>\n      <%!-- some --%>\n      <%!-- comment --%>\n      <%!--\n        <div>\n          <%= @user.name %>\n        </div>\n      --%>\n    </div>\n    \"\"\")\n\n    assert_formatter_doesnt_change(\"\"\"\n    <div>\n      <%= # some %>\n      <%= # comment %>\n      <%= # lines %>\n    </div>\n    \"\"\")\n  end\n\n  test \"supports attribute_formatters\" do\n    defmodule UpcaseFormatter do\n      def render_attribute({\"upcased\", {:string, value, meta}, attr_meta}, _opts) do\n        {\"upcased\", {:string, String.upcase(value), meta}, attr_meta}\n      end\n    end\n\n    assert_formatter_output(\n      \"\"\"\n      <div upcased='foo' untouched='bar' unloaded='baz' />\n      \"\"\",\n      \"\"\"\n      <div upcased=\"FOO\" untouched=\"bar\" unloaded=\"baz\" />\n      \"\"\",\n      attribute_formatters: %{upcased: UpcaseFormatter, unloaded: Unloaded}\n    )\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/igniter/upgrade_to_1_1_test.exs",
    "content": "defmodule Phoenix.LiveView.Igniter.UpgradeTo1_1Test do\n  use ExUnit.Case, async: false\n  import Igniter.Test\n\n  test \"is idempotent\" do\n    full_project()\n    |> run_upgrade(input: [\"y\\n\", \"y\\n\"])\n    |> apply_igniter!()\n    |> run_upgrade(input: [\"y\\n\", \"y\\n\"])\n    |> assert_unchanged()\n  end\n\n  describe \"dependency updates\" do\n    test \"adds both dependencies\" do\n      test_project()\n      |> run_upgrade()\n      |> assert_has_patch(\"mix.exs\", \"\"\"\n      + |      {:lazy_html, \">= 0.0.0\", only: :test}\n      \"\"\")\n    end\n\n    test \"updates existing phoenix_live_view dependency\" do\n      test_project(\n        files: %{\n          \"mix.exs\" => \"\"\"\n          defmodule Test.MixProject do\n            use Mix.Project\n\n            def project do\n              [\n                app: :test,\n                version: \"0.1.0\",\n                elixir: \"~> 1.14\",\n                deps: deps()\n              ]\n            end\n\n            defp deps do\n              [\n                {:phoenix_live_view, \"~> 0.20.0\"}\n              ]\n            end\n          end\n          \"\"\"\n        }\n      )\n      |> run_upgrade(input: [\"y\\n\", \"n\\n\"])\n      |> assert_has_patch(\"mix.exs\", \"\"\"\n         16 + |      {:lazy_html, \">= 0.0.0\", only: :test},\n      \"\"\")\n    end\n  end\n\n  describe \"compiler configuration\" do\n    test \"adds :phoenix_live_view compiler when compilers is not configured\" do\n      test_project()\n      |> run_upgrade()\n      |> assert_has_patch(\"mix.exs\", \"\"\"\n      - |      deps: deps()\n      + |      deps: deps(),\n      + |      compilers: [:phoenix_live_view] ++ Mix.compilers()\n      \"\"\")\n    end\n\n    test \"does nothing when already configured\" do\n      test_project(\n        files: %{\n          \"mix.exs\" => \"\"\"\n          defmodule Test.MixProject do\n            use Mix.Project\n\n            def project do\n              [\n                app: :test,\n                version: \"0.1.0\",\n                elixir: \"~> 1.14\",\n                compilers: [:phoenix_live_view] ++ Mix.compilers(),\n                deps: deps()\n              ]\n            end\n\n            defp deps do\n              [\n                {:lazy_html, \">= 0.0.0\", only: :test}\n              ]\n            end\n          end\n          \"\"\"\n        }\n      )\n      |> run_upgrade()\n      |> refute_has_warning()\n      |> assert_unchanged()\n    end\n\n    test \"warns when compiler configuration is complex\" do\n      test_project(\n        files: %{\n          \"mix.exs\" => \"\"\"\n          defmodule Test.MixProject do\n            use Mix.Project\n\n            def project do\n              [\n                app: :test,\n                version: \"0.1.0\",\n                elixir: \"~> 1.14\",\n                compilers: custom_compilers(),\n                deps: deps()\n              ]\n            end\n\n            defp custom_compilers do\n              [:gettext] ++ Mix.compilers()\n            end\n\n            defp deps do\n              []\n            end\n          end\n          \"\"\"\n        }\n      )\n      |> run_upgrade()\n      |> assert_has_warning(&(&1 =~ \"Failed to automatically configure compilers\"))\n    end\n  end\n\n  describe \"reloadable compilers configuration\" do\n    test \"updates reloadable_compilers in dev.exs when configured\" do\n      test_project(\n        app_name: :my_app,\n        files: %{\n          \"lib/my_app_web.ex\" => \"\"\"\n          defmodule MyAppWeb do\n          end\n          \"\"\",\n          \"config/dev.exs\" => \"\"\"\n          import Config\n\n          config :my_app, MyAppWeb.Endpoint,\n            http: [port: 4000],\n            reloadable_compilers: [:elixir, :app]\n          \"\"\"\n        }\n      )\n      |> run_upgrade()\n      |> assert_has_patch(\"config/dev.exs\", \"\"\"\n      - |  reloadable_compilers: [:elixir, :app]\n      + |  reloadable_compilers: [:phoenix_live_view, :elixir, :app]\n      \"\"\")\n    end\n\n    test \"moves :phoenix_live_view to first position if already present\" do\n      test_project(\n        app_name: :my_app,\n        files: %{\n          \"lib/my_app_web.ex\" => \"\"\"\n          defmodule MyAppWeb do\n          end\n          \"\"\",\n          \"config/dev.exs\" => \"\"\"\n          import Config\n\n          config :my_app, MyAppWeb.Endpoint,\n            http: [port: 4000],\n            reloadable_compilers: [:elixir, :phoenix_live_view, :app]\n          \"\"\"\n        }\n      )\n      |> run_upgrade()\n      |> assert_has_patch(\"config/dev.exs\", \"\"\"\n      - |  reloadable_compilers: [:elixir, :phoenix_live_view, :app]\n      + |  reloadable_compilers: [:phoenix_live_view, :elixir, :app]\n      \"\"\")\n    end\n\n    test \"doesn't update when :phoenix_live_view is already first\" do\n      test_project(\n        app_name: :my_app,\n        files: %{\n          \"lib/my_app_web.ex\" => \"\"\"\n          defmodule MyAppWeb do\n          end\n          \"\"\",\n          \"config/dev.exs\" => \"\"\"\n          import Config\n\n          config :my_app, MyAppWeb.Endpoint,\n            http: [port: 4000],\n            reloadable_compilers: [:phoenix_live_view, :elixir, :app]\n          \"\"\"\n        }\n      )\n      |> run_upgrade()\n      |> assert_unchanged(\"config/dev.exs\")\n    end\n\n    test \"warns when reloadable_compilers is not a list\" do\n      test_project(\n        app_name: :my_app,\n        files: %{\n          \"lib/my_app_web.ex\" => \"\"\"\n          defmodule MyAppWeb do\n          end\n          \"\"\",\n          \"config/dev.exs\" => \"\"\"\n          import Config\n\n          config :my_app, MyAppWeb.Endpoint,\n            http: [port: 4000],\n            reloadable_compilers: Application.get_env(:my_app, :compilers)\n          \"\"\"\n        }\n      )\n      |> run_upgrade()\n      |> assert_has_warning(\n        &(&1 =~ \"Ensure that `:phoenix_live_view` is set in there as the first entry!\")\n      )\n    end\n\n    test \"does nothing when reloadable_compilers is not configured\" do\n      test_project(\n        app_name: :my_app,\n        files: %{\n          \"lib/my_app_web.ex\" => \"\"\"\n          defmodule MyAppWeb do\n          end\n          \"\"\",\n          \"config/dev.exs\" => \"\"\"\n          import Config\n\n          config :my_app, MyAppWeb.Endpoint,\n            http: [port: 4000]\n          \"\"\"\n        }\n      )\n      |> run_upgrade()\n      |> assert_unchanged(\"config/dev.exs\")\n    end\n  end\n\n  describe \"esbuild configuration\" do\n    test \"doesn't update esbuild when user says no\" do\n      test_project(\n        app_name: :my_app,\n        files: %{\n          \"config/config.exs\" => \"\"\"\n          import Config\n\n          config :esbuild,\n            my_app: [\n              args: ~w(js/app.js --bundle --outdir=../priv/static/assets),\n              cd: Path.expand(\"../assets\", __DIR__),\n              env: %{\"NODE_PATH\" => Path.expand(\"../deps\", __DIR__)}\n            ]\n          \"\"\"\n        }\n      )\n      |> run_upgrade()\n      |> assert_unchanged(\"config/config.exs\")\n      |> refute_has_notice()\n    end\n\n    test \"updates esbuild args and env when user confirms\" do\n      test_project(\n        app_name: :my_app,\n        files: %{\n          \"config/config.exs\" => \"\"\"\n          import Config\n\n          config :esbuild,\n            my_app: [\n              args: ~w(js/app.js --bundle --outdir=../priv/static/assets),\n              cd: Path.expand(\"../assets\", __DIR__),\n              env: %{\"NODE_PATH\" => Path.expand(\"../deps\", __DIR__)}\n            ]\n          \"\"\"\n        }\n      )\n      # yes to esbuild\n      |> run_upgrade(input: \"y\\n\")\n      |> assert_has_patch(\"mix.exs\", \"\"\"\n      + |      {:esbuild, \"~> 0.10\", runtime: Mix.env() == :dev},\n      \"\"\")\n      |> assert_has_patch(\"config/config.exs\", \"\"\"\n      - |    args: ~w(js/app.js --bundle --outdir=../priv/static/assets),\n      + |    args: ~w(js/app.js --bundle --outdir=../priv/static/assets --alias:@=.),\n        |    cd: Path.expand(\"../assets\", __DIR__),\n      - |    env: %{\"NODE_PATH\" => Path.expand(\"../deps\", __DIR__)}\n      + |    env: %{\"NODE_PATH\" => [Path.expand(\"../deps\", __DIR__), Mix.Project.build_path()]}\n      \"\"\")\n      |> assert_has_notice(fn notice -> notice =~ \"Final step for colocated hooks\" end)\n    end\n\n    test \"updates esbuild args list when user confirms\" do\n      test_project(\n        app_name: :my_app,\n        files: %{\n          \"config/config.exs\" => \"\"\"\n          import Config\n\n          config :esbuild,\n            my_app: [\n              args: [\"js/app.js\", \"--bundle\", \"--outdir=../priv/static/assets\"],\n              cd: Path.expand(\"../assets\", __DIR__),\n              env: %{\"NODE_PATH\" => Path.expand(\"../deps\", __DIR__)}\n            ]\n          \"\"\"\n        }\n      )\n      # yes to esbuild (no deps prompt since no existing deps)\n      |> run_upgrade(input: \"y\\n\")\n      |> assert_has_patch(\"mix.exs\", \"\"\"\n      + |      {:esbuild, \"~> 0.10\", runtime: Mix.env() == :dev},\n      \"\"\")\n      |> assert_has_patch(\"config/config.exs\", \"\"\"\n      - |    args: [\"js/app.js\", \"--bundle\", \"--outdir=../priv/static/assets\"],\n      + |    args: [\"js/app.js\", \"--bundle\", \"--outdir=../priv/static/assets\", \"--alias:@=.\"],\n      \"\"\")\n      |> assert_has_notice(&(&1 =~ \"Final step for colocated hooks\"))\n    end\n\n    test \"updates esbuild env, keeping previous value\" do\n      test_project(\n        app_name: :my_app,\n        files: %{\n          \"config/config.exs\" => \"\"\"\n          import Config\n\n          config :esbuild,\n            my_app: [\n              args: ~w(js/app.js --bundle --outdir=../priv/static/assets),\n              cd: Path.expand(\"../assets\", __DIR__),\n              env: %{\"NODE_PATH\" => \"something_custom\"}\n            ]\n          \"\"\"\n        }\n      )\n      # yes to esbuild\n      |> run_upgrade(input: \"y\\n\")\n      |> assert_has_patch(\"mix.exs\", \"\"\"\n      + |      {:esbuild, \"~> 0.10\", runtime: Mix.env() == :dev},\n      \"\"\")\n      |> assert_has_patch(\"config/config.exs\", \"\"\"\n      - |    args: ~w(js/app.js --bundle --outdir=../priv/static/assets),\n      + |    args: ~w(js/app.js --bundle --outdir=../priv/static/assets --alias:@=.),\n        |    cd: Path.expand(\"../assets\", __DIR__),\n      - |    env: %{\"NODE_PATH\" => \"something_custom\"}\n      + |    env: %{\"NODE_PATH\" => [\"something_custom\", Mix.Project.build_path()]}\n      \"\"\")\n      |> assert_has_notice(fn notice -> notice =~ \"Final step for colocated hooks\" end)\n    end\n\n    test \"warns when esbuild config doesn't have expected structure\" do\n      test_project(\n        app_name: :my_app,\n        files: %{\n          \"config/config.exs\" => \"\"\"\n          import Config\n\n          config :esbuild,\n            my_app: [\n              args: :other,\n              cd: Path.expand(\"../assets\", __DIR__),\n              env: %{\"NODE_PATH\" => Path.expand(\"../deps\", __DIR__)}\n            ]\n          \"\"\"\n        }\n      )\n      # yes to esbuild (no deps prompt since no existing deps)\n      |> run_upgrade(input: \"y\\n\")\n      |> assert_has_patch(\"mix.exs\", \"\"\"\n      + |      {:esbuild, \"~> 0.10\", runtime: Mix.env() == :dev},\n      \"\"\")\n      |> assert_has_warning(&(&1 =~ \"Failed to update esbuild configuration for colocated hooks\"))\n      |> refute_has_notice()\n    end\n\n    test \"warns when esbuild config is missing args or env\" do\n      test_project(\n        app_name: :my_app,\n        files: %{\n          \"config/config.exs\" => \"\"\"\n          import Config\n\n          config :esbuild,\n            my_app: [\n              cd: Path.expand(\"../assets\", __DIR__)\n            ]\n          \"\"\"\n        }\n      )\n      # no to deps prompt, yes to esbuild\n      |> run_upgrade(input: \"y\\n\")\n      |> assert_has_patch(\"mix.exs\", \"\"\"\n      + |      {:esbuild, \"~> 0.10\", runtime: Mix.env() == :dev},\n      \"\"\")\n      |> assert_has_warning(&(&1 =~ \"Failed to update esbuild configuration for colocated hooks\"))\n      |> refute_has_notice()\n    end\n\n    test \"skips esbuild update when no esbuild config exists\" do\n      test_project(app_name: :my_app)\n      # yes to esbuild but no config exists (no deps prompt since no existing deps)\n      |> run_upgrade(input: \"y\\n\")\n      |> refute_has_notice()\n    end\n  end\n\n  describe \"debug_attributes\" do\n    test \"adds debug_attributes when debug_heex_annotations is already set\" do\n      test_project(\n        app_name: :my_app,\n        files: %{\n          \"config/dev.exs\" => \"\"\"\n          import Config\n\n          config :phoenix_live_view,\n            enable_expensive_runtime_checks: true,\n            debug_heex_annotations: true\n          \"\"\"\n        }\n      )\n      |> run_upgrade()\n      |> assert_has_patch(\"config/dev.exs\", \"\"\"\n      - |    debug_heex_annotations: true\n      + |    debug_heex_annotations: true,\n      + |    debug_attributes: true\n      \"\"\")\n    end\n\n    test \"does not add debug_attributes when debug_heex_annotations is not set\" do\n      test_project(\n        app_name: :my_app,\n        files: %{\n          \"config/dev.exs\" => \"\"\"\n          import Config\n\n          config :phoenix_live_view,\n            enable_expensive_runtime_checks: true\n          \"\"\"\n        }\n      )\n      |> run_upgrade()\n      |> assert_unchanged(\"config/dev.exs\")\n    end\n  end\n\n  describe \"full upgrade scenario\" do\n    test \"performs complete upgrade for a Phoenix project\" do\n      full_project()\n      |> run_upgrade(input: [\"y\\n\", \"y\\n\"])\n      |> assert_has_patch(\"mix.exs\", \"\"\"\n      - |      deps: deps()\n      + |      deps: deps(),\n      + |      compilers: [:phoenix_live_view] ++ Mix.compilers()\n      \"\"\")\n      |> assert_has_patch(\"mix.exs\", \"\"\"\n      + |      {:lazy_html, \">= 0.0.0\", only: :test},\n      \"\"\")\n      |> assert_has_patch(\"mix.exs\", \"\"\"\n      - |      {:esbuild, \"~> 0.8\", runtime: Mix.env() == :dev},\n      + |      {:esbuild, \"~> 0.10\", runtime: Mix.env() == :dev}\n      \"\"\")\n      |> assert_has_patch(\"config/dev.exs\", \"\"\"\n      - |  reloadable_compilers: [:elixir, :app]\n      + |  reloadable_compilers: [:phoenix_live_view, :elixir, :app]\n      \"\"\")\n      |> assert_has_patch(\"config/dev.exs\", \"\"\"\n      - |    debug_heex_annotations: true\n      + |    debug_heex_annotations: true,\n      + |    debug_attributes: true\n      \"\"\")\n      |> assert_has_patch(\"config/config.exs\", \"\"\"\n      - |    args: ~w(js/app.js --bundle --outdir=../priv/static/assets),\n      + |    args: ~w(js/app.js --bundle --outdir=../priv/static/assets --alias:@=.),\n        |    cd: Path.expand(\"../assets\", __DIR__),\n      - |    env: %{\"NODE_PATH\" => Path.expand(\"../deps\", __DIR__)}\n      + |    env: %{\"NODE_PATH\" => [Path.expand(\"../deps\", __DIR__), Mix.Project.build_path()]}\n      \"\"\")\n    end\n  end\n\n  defp full_project do\n    test_project(\n      app_name: :my_app,\n      files: %{\n        \"mix.exs\" => \"\"\"\n        defmodule MyApp.MixProject do\n          use Mix.Project\n\n          def project do\n            [\n              app: :my_app,\n              version: \"0.1.0\",\n              elixir: \"~> 1.14\",\n              deps: deps()\n            ]\n          end\n\n          defp deps do\n            [\n              {:phoenix, \"~> 1.7.0\"},\n              {:phoenix_live_view, \"~> 0.20.0\"},\n              {:esbuild, \"~> 0.8\", runtime: Mix.env() == :dev},\n            ]\n          end\n        end\n        \"\"\",\n        \"lib/my_app_web.ex\" => \"\"\"\n        defmodule MyAppWeb do\n        end\n        \"\"\",\n        \"config/config.exs\" => \"\"\"\n        import Config\n\n        config :esbuild,\n          my_app: [\n            args: ~w(js/app.js --bundle --outdir=../priv/static/assets),\n            cd: Path.expand(\"../assets\", __DIR__),\n            env: %{\"NODE_PATH\" => Path.expand(\"../deps\", __DIR__)}\n          ]\n        \"\"\",\n        \"config/dev.exs\" => \"\"\"\n        import Config\n\n        config :my_app, MyAppWeb.Endpoint,\n          http: [port: 4000],\n          reloadable_compilers: [:elixir, :app]\n\n        config :phoenix_live_view,\n          enable_expensive_runtime_checks: true,\n          debug_heex_annotations: true\n        \"\"\"\n      }\n    )\n  end\n\n  defp run_upgrade(igniter, opts \\\\ []) do\n    # Default to no for esbuild prompt\n    input = Keyword.get(opts, :input, \"n\\n\")\n\n    shell = Mix.shell()\n\n    try do\n      Mix.shell(Mix.Shell.Process)\n\n      input\n      |> List.wrap()\n      |> Enum.each(&send(self(), {:mix_shell_input, :prompt, &1}))\n\n      Igniter.compose_task(igniter, \"phoenix_live_view.upgrade\", [\"1.0.0\", \"1.1.1\"])\n    after\n      Mix.shell(shell)\n    end\n  end\n\n  defp refute_has_notice(igniter) do\n    assert igniter.notices == []\n    igniter\n  end\n\n  defp refute_has_warning(igniter) do\n    assert igniter.warnings == []\n    igniter\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/assign_async_test.exs",
    "content": "defmodule Phoenix.LiveView.AssignAsyncTest do\n  use ExUnit.Case, async: true\n  import Phoenix.ConnTest\n\n  import Phoenix.LiveViewTest\n  import Phoenix.LiveViewTest.Support.AsyncSync\n  alias Phoenix.LiveViewTest.Support.Endpoint\n\n  @endpoint Endpoint\n\n  setup do\n    Process.flag(:trap_exit, true)\n    {:ok, conn: Plug.Test.init_test_session(Phoenix.ConnTest.build_conn(), %{})}\n  end\n\n  describe \"LiveView assign_async\" do\n    test \"bad return\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=bad_return\")\n\n      assert render_async(lv) =~\n               \"{:exit, {%ArgumentError{message: &quot;expected assign_async to return {:ok, map} of\\\\nassigns for [:data] or {:error, reason}, got: 123\\\\n&quot;}\"\n\n      assert render(lv)\n    end\n\n    test \"missing known key\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=bad_ok\")\n\n      assert render_async(lv) =~\n               \"expected assign_async to return map of assigns for all keys\\\\nin [:data]\"\n\n      assert render(lv)\n    end\n\n    test \"valid return\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=ok\")\n      assert render_async(lv) =~ \"data: 123\"\n    end\n\n    test \"raise during execution\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=raise\")\n\n      assert render_async(lv) =~ \"{:exit, {%RuntimeError{message: &quot;boom&quot;}\"\n      assert render(lv)\n    end\n\n    test \"exit during execution\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=exit\")\n\n      assert render_async(lv) =~ \"{:exit, :boom}\"\n      assert render(lv)\n    end\n\n    test \"lv exit brings down asyncs\", %{conn: conn} do\n      Process.register(self(), :assign_async_test_process)\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=lv_exit\")\n      lv_ref = Process.monitor(lv.pid)\n\n      async_ref = wait_for_async_ready_and_monitor(:lv_exit)\n      send(lv.pid, :boom)\n\n      assert_receive {:DOWN, ^lv_ref, :process, _pid, :boom}, 1000\n      assert_receive {:DOWN, ^async_ref, :process, _pid, :boom}, 1000\n    end\n\n    test \"cancel_async\", %{conn: conn} do\n      Process.register(self(), :assign_async_test_process)\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=cancel\")\n\n      async_ref = wait_for_async_ready_and_monitor(:cancel)\n      send(lv.pid, :cancel)\n\n      assert_receive {:DOWN, ^async_ref, :process, _pid, {:shutdown, :cancel}}, 1000\n\n      assert render(lv) =~ \":cancel\"\n\n      send(lv.pid, :renew_canceled)\n\n      assert render(lv) =~ \"data loading...\"\n      assert render_async(lv, 200) =~ \"data: 123\"\n    end\n\n    test \"trapping exits\", %{conn: conn} do\n      Process.register(self(), :trap_exit_test)\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=trap_exit\")\n\n      assert render_async(lv, 200) =~ \"{:exit, :boom}\"\n      assert render(lv)\n      assert_receive {:exit, _pid, :boom}, 1000\n    end\n  end\n\n  describe \"LiveComponent assign_async\" do\n    test \"bad return\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=lc_bad_return\")\n\n      assert render_async(lv) =~\n               \"exit: {%ArgumentError{message: &quot;expected assign_async to return {:ok, map} of\\\\nassigns for [:lc_data, :other_data] or {:error, reason}, got: 123\\\\n&quot;}\"\n\n      assert render(lv)\n    end\n\n    test \"missing known key\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=lc_bad_ok\")\n\n      assert render_async(lv) =~\n               \"expected assign_async to return map of assigns for all keys\\\\nin [:lc_data, :other_data]\"\n\n      assert render(lv)\n    end\n\n    test \"valid return\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=lc_ok\")\n      assert render_async(lv) =~ \"lc_data: 123\"\n    end\n\n    test \"keeps previous values when updating async assign\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=lc_ok\")\n      assert render_async(lv) =~ \"lc_data: 123\"\n\n      Phoenix.LiveView.send_update(lv.pid, Phoenix.LiveViewTest.Support.AssignAsyncLive.LC,\n        id: \"lc\",\n        action: :assign_async_reset,\n        reset: false\n      )\n\n      assert render(lv) =~ \"lc_data: 123\"\n      assert render_async(lv) =~ \"lc_data: 456\"\n    end\n\n    test \"keeps previous values when using a list for async assign\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=lc_ok\")\n      rendered = render_async(lv)\n      assert rendered =~ \"lc_data: 123\"\n      assert rendered =~ \"other_data: 555\"\n\n      Phoenix.LiveView.send_update(lv.pid, Phoenix.LiveViewTest.Support.AssignAsyncLive.LC,\n        id: \"lc\",\n        action: :assign_async_reset,\n        reset: [:other_data]\n      )\n\n      rendered = render(lv)\n      assert rendered =~ \"lc_data: 123\"\n      assert rendered =~ \"other_data loading\"\n      rendered = render_async(lv)\n      assert rendered =~ \"lc_data: 456\"\n      assert rendered =~ \"other_data: 999\"\n    end\n\n    test \"when using the reset flag\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=lc_ok\")\n      assert render_async(lv) =~ \"lc_data: 123\"\n\n      Phoenix.LiveView.send_update(lv.pid, Phoenix.LiveViewTest.Support.AssignAsyncLive.LC,\n        id: \"lc\",\n        action: :assign_async_reset,\n        reset: true\n      )\n\n      assert render(lv) =~ \"loading\"\n      refute render(lv) =~ \"lc_data: 123\"\n      assert render_async(lv) =~ \"lc_data: 456\"\n    end\n\n    test \"raise during execution\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=lc_raise\")\n\n      assert render_async(lv) =~ \"exit: {%RuntimeError{message: &quot;boom&quot;}\"\n      assert render(lv)\n    end\n\n    test \"exit during execution\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=lc_exit\")\n\n      assert render_async(lv) =~ \"exit: :boom\"\n      assert render(lv)\n    end\n\n    test \"lv exit brings down asyncs\", %{conn: conn} do\n      Process.register(self(), :assign_async_test_process)\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=lc_lv_exit\")\n      lv_ref = Process.monitor(lv.pid)\n\n      async_ref = wait_for_async_ready_and_monitor(:lc_exit)\n      send(lv.pid, :boom)\n\n      assert_receive {:DOWN, ^lv_ref, :process, _pid, :boom}, 1000\n      assert_receive {:DOWN, ^async_ref, :process, _pid, :boom}, 1000\n    end\n\n    test \"cancel_async\", %{conn: conn} do\n      Process.register(self(), :assign_async_test_process)\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=lc_cancel\")\n\n      async_ref = wait_for_async_ready_and_monitor(:lc_cancel)\n\n      Phoenix.LiveView.send_update(lv.pid, Phoenix.LiveViewTest.Support.AssignAsyncLive.LC,\n        id: \"lc\",\n        action: :cancel\n      )\n\n      assert_receive {:DOWN, ^async_ref, :process, _pid, {:shutdown, :cancel}}, 1000\n\n      assert render(lv) =~ \"exit: {:shutdown, :cancel}\"\n\n      Phoenix.LiveView.send_update(lv.pid, Phoenix.LiveViewTest.Support.AssignAsyncLive.LC,\n        id: \"lc\",\n        action: :renew_canceled\n      )\n\n      assert render(lv) =~ \"lc_data loading...\"\n      assert render_async(lv, 200) =~ \"lc_data: 123\"\n    end\n  end\n\n  describe \"LiveView assign_async, supervised\" do\n    setup do\n      start_supervised!({Task.Supervisor, name: TestAsyncSupervisor})\n      :ok\n    end\n\n    test \"valid return\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=sup_ok\")\n      assert render_async(lv) =~ \"data: 123\"\n    end\n\n    test \"raise during execution\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=sup_raise\")\n\n      assert render_async(lv) =~ \"{:exit, {%RuntimeError{message: &quot;boom&quot;}\"\n      assert render(lv)\n    end\n\n    test \"exit during execution\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/assign_async?test=sup_exit\")\n\n      assert render_async(lv) =~ \"{:exit, :boom}\"\n      assert render(lv)\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/assigns_test.exs",
    "content": "defmodule Phoenix.LiveView.AssignsTest do\n  use ExUnit.Case, async: true\n  import Plug.Conn\n  import Phoenix.ConnTest\n\n  import Phoenix.LiveViewTest\n  alias Phoenix.LiveViewTest.Support.Endpoint\n\n  @endpoint Endpoint\n\n  setup do\n    {:ok, conn: Plug.Test.init_test_session(Phoenix.ConnTest.build_conn(), %{})}\n  end\n\n  describe \"assign_new\" do\n    test \"uses conn.assigns on static render then fetches on connected mount\", %{conn: conn} do\n      user = %{name: \"user-from-conn\", id: 123}\n\n      conn =\n        conn\n        |> Plug.Conn.assign(:current_user, user)\n        |> Plug.Conn.put_session(:user_id, user.id)\n        |> get(\"/root\")\n\n      assert html_response(conn, 200) =~ \"root name: user-from-conn\"\n      assert html_response(conn, 200) =~ \"child static name: user-from-conn\"\n\n      {:ok, _, connected_html} = live(conn)\n      assert connected_html =~ \"root name: user-from-root\"\n      assert connected_html =~ \"child static name: user-from-root\"\n    end\n\n    test \"uses assign_new from parent on dynamically added child\", %{conn: conn} do\n      user = %{name: \"user-from-conn\", id: 123}\n\n      {:ok, view, _html} =\n        conn\n        |> Plug.Conn.assign(:current_user, user)\n        |> Plug.Conn.put_session(:user_id, user.id)\n        |> live(\"/root\")\n\n      assert render(view) =~ \"child static name: user-from-root\"\n      refute render(view) =~ \"child dynamic name\"\n\n      :ok = GenServer.call(view.pid, {:dynamic_child, :dynamic})\n\n      html = render(view)\n      assert html =~ \"child static name: user-from-root\"\n      assert html =~ \"child dynamic name: user-from-child\"\n    end\n  end\n\n  describe \"temporary assigns\" do\n    test \"can be configured with mount options\", %{conn: conn} do\n      {:ok, conf_live, html} =\n        conn\n        |> put_session(:opts, temporary_assigns: [description: nil])\n        |> live(\"/opts\")\n\n      assert html =~ \"long description. canary\"\n      assert render(conf_live) =~ \"long description. canary\"\n      socket = GenServer.call(conf_live.pid, {:exec, fn socket -> {:reply, socket, socket} end})\n\n      assert socket.assigns.description == nil\n      assert socket.assigns.canary == \"canary\"\n    end\n\n    test \"raises with invalid options\", %{conn: conn} do\n      assert_raise ArgumentError,\n                   ~r/invalid option returned from Phoenix.LiveViewTest.Support.OptsLive.mount\\/3/,\n                   fn ->\n                     conn\n                     |> put_session(:opts, oops: [:description])\n                     |> live(\"/opts\")\n                   end\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/collocated_test.exs",
    "content": "defmodule Phoenix.LiveView.CollocatedTest do\n  use ExUnit.Case, async: true\n  import Phoenix.ConnTest\n\n  import Phoenix.LiveViewTest\n  alias Phoenix.LiveViewTest.Support.{Endpoint, CollocatedLive, CollocatedComponent}\n\n  @endpoint Endpoint\n\n  test \"supports collocated views\" do\n    {:ok, view, html} = live_isolated(build_conn(), CollocatedLive)\n    assert html =~ \"Hello collocated world from live!\\n</div>\"\n    assert render(view) =~ \"Hello collocated world from live!\\n</div>\"\n  end\n\n  test \"supports collocated components\" do\n    assert render_component(CollocatedComponent, world: \"world\") =~\n             \"Hello collocated world from component!\\n\"\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/connect_test.exs",
    "content": "defmodule Phoenix.LiveView.ConnectTest do\n  use ExUnit.Case, async: true\n  import Phoenix.LiveViewTest\n  import Phoenix.ConnTest\n\n  @endpoint Phoenix.LiveViewTest.Support.Endpoint\n\n  describe \"connect_params\" do\n    test \"can be read on mount\" do\n      {:ok, live, _html} =\n        Phoenix.ConnTest.build_conn()\n        |> put_connect_params(%{\"connect1\" => \"1\"})\n        |> live(\"/connect\")\n\n      assert render(live) =~ rendered_to_string(~s|params: %{\"_mounts\" => 0, \"connect1\" => \"1\"}|)\n    end\n  end\n\n  describe \"connect_info\" do\n    test \"can be read on mount\" do\n      {:ok, live, html} =\n        Phoenix.ConnTest.build_conn()\n        |> Plug.Conn.put_req_header(\"user-agent\", \"custom-client\")\n        |> Plug.Conn.put_req_header(\"x-foo\", \"bar\")\n        |> Plug.Conn.put_req_header(\"x-bar\", \"baz\")\n        |> Plug.Conn.put_req_header(\"tracestate\", \"one\")\n        |> Plug.Conn.put_req_header(\"traceparent\", \"two\")\n        |> live(\"/connect\")\n\n      assert_html = fn html ->\n        html = String.replace(html, \"&quot;\", \"\\\"\")\n        assert html =~ ~S<user-agent: \"custom-client\">\n        assert html =~ ~S<x-headers: [{\"x-foo\", \"bar\"}, {\"x-bar\", \"baz\"}]>\n        assert html =~ ~S<trace: [{\"tracestate\", \"one\"}, {\"traceparent\", \"two\"}]>\n        assert html =~ ~S<peer: %{address: {127, 0, 0, 1}, port: 111317, ssl_cert: nil}>\n        assert html =~ ~S<uri: http://www.example.com/connect>\n      end\n\n      assert_html.(html)\n      assert_html.(render(live))\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/elements_test.exs",
    "content": "defmodule Phoenix.LiveView.ElementsTest do\n  use ExUnit.Case, async: true\n  import Phoenix.ConnTest\n\n  import Phoenix.LiveViewTest\n  alias Phoenix.LiveViewTest.Support.Endpoint\n\n  @endpoint Endpoint\n\n  defp last_event(view) do\n    view |> element(\"#last-event\") |> render() |> HtmlEntities.decode()\n  end\n\n  defp last_component_event(view) do\n    view |> element(\"#component-last-event\") |> render() |> HtmlEntities.decode()\n  end\n\n  setup do\n    conn = Phoenix.ConnTest.build_conn()\n    {:ok, live, _} = live(conn, \"/elements\")\n    %{live: live, conn: conn}\n  end\n\n  describe \"has_element?/1\" do\n    test \"checks if given element is on the page\", %{live: view} do\n      assert view |> element(\"div\") |> has_element?()\n      assert view |> element(\"#scoped-render\") |> has_element?()\n      assert view |> element(\"div\", \"This is a div\") |> has_element?()\n      assert view |> element(\"#scoped-render\", ~r/^This is a div$/) |> has_element?()\n      assert view |> element(\"span\", \"Normalize whitespace\") |> has_element?()\n\n      refute view |> element(\"#unknown\") |> has_element?()\n      refute view |> element(\"div\", \"no matching text\") |> has_element?()\n    end\n  end\n\n  describe \"has_element?/3\" do\n    test \"checks if given element is on the page\", %{live: view} do\n      assert has_element?(view, \"div\")\n      assert has_element?(view, \"#scoped-render\")\n      assert has_element?(view, \"div\", \"This is a div\")\n      assert has_element?(view, \"#scoped-render\", ~r/^This is a div$/)\n\n      refute has_element?(view, \"#unknown\")\n      refute has_element?(view, \"div\", \"no matching text\")\n    end\n  end\n\n  describe \"render/1\" do\n    test \"renders a given element\", %{live: view} do\n      assert view |> element(\"#scoped-render\") |> render() ==\n               ~s|<div id=\"scoped-render\"><span>This</span> is a div</div>|\n    end\n\n    test \"renders with text filter\", %{live: view} do\n      assert view |> element(\"div\", \"This is a div\") |> render() ==\n               ~s|<div id=\"scoped-render\"><span>This</span> is a div</div>|\n\n      assert view |> element(\"#scoped-render\", \"This is a div\") |> render() ==\n               ~s|<div id=\"scoped-render\"><span>This</span> is a div</div>|\n\n      assert view |> element(\"#scoped-render\", ~r/^This is a div$/) |> render() ==\n               ~s|<div id=\"scoped-render\"><span>This</span> is a div</div>|\n    end\n\n    test \"raises on bad selector\", %{live: view} do\n      assert_raise ArgumentError,\n                   ~r/expected selector \"div\" to return a single element, but got 6/,\n                   fn -> view |> element(\"div\") |> render() end\n\n      assert_raise ArgumentError,\n                   ~r/expected selector \"#unknown\" to return a single element, but got none/,\n                   fn -> view |> element(\"#unknown\") |> render() end\n    end\n\n    test \"raises on bad selector with text filter\", %{live: view} do\n      assert_raise ArgumentError,\n                   ~r/selector \"#scoped-render\" did not match text filter \"This is not a div\", got: \\n\\n    <div id=\"scoped-render\"><span>This<\\/span> is a div<\\/div>/,\n                   fn -> view |> element(\"#scoped-render\", \"This is not a div\") |> render() end\n\n      assert_raise ArgumentError,\n                   ~r/selector \"div\" returned 6 elements but none matched the text filter \"This is not a div\"/,\n                   fn -> view |> element(\"div\", \"This is not a div\") |> render() end\n\n      assert_raise ArgumentError,\n                   ~r/selector \"div\" returned 6 elements and 2 of them matched the text filter \"This\"/,\n                   fn -> view |> element(\"div\", \"This\") |> render() end\n    end\n\n    test \"renders a given element via target\", %{live: view} do\n      assert view |> with_target(\"#scoped-render\") |> render() ==\n               ~s|<div id=\"scoped-render\"><span>This</span> is a div</div>|\n    end\n\n    test \"raises on bad selector via target\", %{live: view} do\n      assert_raise ArgumentError,\n                   ~r/expected selector \"div\" to return a single element, but got 6/,\n                   fn -> view |> with_target(\"div\") |> render() end\n    end\n  end\n\n  describe \"render_click\" do\n    test \"clicks the given element\", %{live: view} do\n      assert view |> element(\"span#span-click-no-value\") |> render_click() |> is_binary()\n      assert last_event(view) =~ ~s|span-click: %{}|\n    end\n\n    test \"clicks the given element with value and proper escaping\", %{live: view} do\n      assert view |> element(\"span#span-click-value\") |> render_click() =~\n               ~s|span-click: %{&quot;extra&quot; =&gt; &quot;&lt;456&gt;&quot;, &quot;value&quot; =&gt; &quot;123&quot;}|\n\n      assert view |> element(\"span#span-click-value\") |> render_click(%{\"value\" => \"override\"}) =~\n               ~s|span-click: %{&quot;extra&quot; =&gt; &quot;&lt;456&gt;&quot;, &quot;value&quot; =&gt; &quot;override&quot;}|\n    end\n\n    test \"clicks the given element with phx-value\", %{live: view} do\n      assert view |> element(\"span#span-click-phx-value\") |> render_click() |> is_binary()\n\n      assert last_event(view) =~\n               ~s|span-click: %{\"bar\" => \"456\", \"foo\" => \"123\"}|\n\n      assert view\n             |> element(\"span#span-click-phx-value\")\n             |> render_click(%{\"foo\" => \"override\"})\n             |> is_binary()\n\n      assert last_event(view) =~\n               ~s|span-click: %{\"bar\" => \"456\", \"foo\" => \"override\"}|\n    end\n\n    test \"raises if element does not have attribute\", %{live: view} do\n      assert_raise ArgumentError,\n                   \"element selected by \\\"span#span-no-attr\\\" does not have phx-click attribute\",\n                   fn -> view |> element(\"span#span-no-attr\") |> render_click() end\n    end\n\n    test \"raises if element is disabled\", %{live: view} do\n      assert_raise ArgumentError,\n                   \"cannot click element \\\"button#button-disabled-click\\\" because it is disabled\",\n                   fn -> view |> element(\"button#button-disabled-click\") |> render_click() end\n    end\n\n    test \"clicks links\", %{live: view} do\n      assert view |> element(\"a#click-a\") |> render_click() =~ ~s|link: %{}|\n    end\n\n    test \"clicks redirect links without phx-click\", %{live: view} do\n      assert {:error, {:redirect, %{to: \"/\"}}} = view |> element(\"a#redirect-a\") |> render_click()\n      assert_redirected(view, \"/\")\n    end\n\n    test \"clicks live redirect links without phx-click\", %{live: view} do\n      assert {:error, {:live_redirect, %{to: \"/example\", kind: :push}}} =\n               view |> element(\"a#live-redirect-a\") |> render_click()\n\n      assert_redirected(view, \"/example\")\n    end\n\n    test \"clicks live redirect links without phx-click and kind is replace\", %{live: view} do\n      assert {:error, {:live_redirect, %{to: \"/example\", kind: :replace}}} =\n               view |> element(\"a#live-redirect-replace-a\") |> render_click()\n\n      assert_redirected(view, \"/example\")\n    end\n\n    test \"clicks live patch links without phx-click\", %{live: view} do\n      assert view |> element(\"a#live-patch-a\") |> render_click() |> is_binary()\n      assert last_event(view) =~ ~s|handle_params: %{\"from\" => \"uri\"}|\n\n      assert_patched(view, \"/elements?from=uri\")\n    end\n\n    test \"raises if link does not have attribute\", %{live: view} do\n      assert_raise ArgumentError,\n                   \"clicked link selected by \\\"a#a-no-attr\\\" does not have phx-click or href attributes\",\n                   fn -> view |> element(\"a#a-no-attr\") |> render_click() end\n    end\n\n    test \"clicks live patch declared with JS.patch\", %{live: view} do\n      assert view |> element(\"button#live-patch-button\") |> render_click() |> is_binary()\n      assert last_event(view) =~ ~s|handle_params: %{\"from\" => \"uri\"}|\n      assert_patched(view, \"/elements?from=uri\")\n\n      assert view |> element(\"button#live-push-patch-button\") |> render_click() |> is_binary()\n      assert last_event(view) =~ ~s|handle_params: %{\"from\" => \"uri\"}|\n      assert_patched(view, \"/elements?from=uri\")\n    end\n\n    test \"clicks live redirect declared with JS.navigate (replace: false)\", %{live: view} do\n      assert {:error, {:live_redirect, %{to: \"/example\", kind: :push}}} =\n               view |> element(\"button#live-redirect-push-button\") |> render_click()\n\n      assert_redirected(view, \"/example\")\n    end\n\n    test \"clicks live redirect declared with JS.navigate (replace: true)\", %{live: view} do\n      assert {:error, {:live_redirect, %{to: \"/example\", kind: :replace}}} =\n               view |> element(\"button#live-redirect-replace-button\") |> render_click()\n\n      assert_redirected(view, \"/example\")\n    end\n\n    test \"first navigation declared with JS.(patch/navigate) wins\", %{live: view} do\n      assert {:error, {:live_redirect, %{to: \"/example\", kind: :replace}}} =\n               view |> element(\"button#live-redirect-patch-button\") |> render_click()\n\n      assert_redirected(view, \"/example\")\n    end\n  end\n\n  describe \"render_hook\" do\n    test \"hooks the given element\", %{live: view} do\n      assert view |> element(\"section#hook-section\") |> render_hook(\"custom-event\") |> is_binary()\n\n      assert last_event(view) =~\n               ~s|custom-event: %{}|\n\n      assert view\n             |> element(\"section#hook-section\")\n             |> render_hook(\"custom-event\", %{foo: \"bar\"})\n             |> is_binary()\n\n      assert last_event(view) =~\n               ~s|custom-event: %{\"foo\" => \"bar\"}|\n    end\n\n    test \"raises if element does not have attribute\", %{live: view} do\n      assert_raise ArgumentError,\n                   \"element selected by \\\"span#span-no-attr\\\" does not have phx-hook attribute\",\n                   fn -> view |> element(\"span#span-no-attr\") |> render_hook(\"custom-event\") end\n    end\n\n    test \"works with phx-viewport bindings\", %{live: view} do\n      assert view |> element(\"#posts\") |> render_hook(\"prev-page\") |> is_binary()\n      assert last_event(view) =~ ~s|prev-page: %{}|\n      assert view |> element(\"#posts\") |> render_hook(\"next-page\") |> is_binary()\n      assert last_event(view) =~ ~s|next-page: %{}|\n    end\n  end\n\n  describe \"render_blur\" do\n    test \"blurs the given element\", %{live: view} do\n      assert view |> element(\"span#span-blur-no-value\") |> render_blur() |> is_binary()\n      assert last_event(view) =~ ~s|span-blur: %{}|\n    end\n\n    test \"blurs the given element with value\", %{live: view} do\n      assert view |> element(\"span#span-blur-value\") |> render_blur() |> is_binary()\n\n      assert last_event(view) =~\n               ~s|span-blur: %{\"extra\" => \"456\", \"value\" => \"123\"}|\n\n      assert view\n             |> element(\"span#span-blur-value\")\n             |> render_blur(%{\"value\" => \"override\"})\n             |> is_binary()\n\n      assert last_event(view) =~\n               ~s|span-blur: %{\"extra\" => \"456\", \"value\" => \"override\"}|\n    end\n\n    test \"blurs the given element with phx-value\", %{live: view} do\n      assert view |> element(\"span#span-blur-phx-value\") |> render_blur() |> is_binary()\n\n      assert last_event(view) =~\n               ~s|span-blur: %{\"bar\" => \"456\", \"foo\" => \"123\"}|\n\n      assert view\n             |> element(\"span#span-blur-phx-value\")\n             |> render_blur(%{\"foo\" => \"override\"})\n             |> is_binary()\n\n      assert last_event(view) =~\n               ~s|span-blur: %{\"bar\" => \"456\", \"foo\" => \"override\"}|\n    end\n\n    test \"raises if element does not have attribute\", %{live: view} do\n      assert_raise ArgumentError,\n                   \"element selected by \\\"span#span-no-attr\\\" does not have phx-blur attribute\",\n                   fn -> view |> element(\"span#span-no-attr\") |> render_blur() end\n    end\n  end\n\n  describe \"render_focus\" do\n    test \"focuses the given element\", %{live: view} do\n      assert view |> element(\"span#span-focus-no-value\") |> render_focus() |> is_binary()\n      assert last_event(view) =~ ~s|span-focus: %{}|\n    end\n\n    test \"focuses the given element with value\", %{live: view} do\n      assert view |> element(\"span#span-focus-value\") |> render_focus() |> is_binary()\n\n      assert last_event(view) =~\n               ~s|span-focus: %{\"extra\" => \"456\", \"value\" => \"123\"}|\n\n      assert view\n             |> element(\"span#span-focus-value\")\n             |> render_focus(%{\"value\" => \"override\"})\n             |> is_binary()\n\n      assert last_event(view) =~\n               ~s|span-focus: %{\"extra\" => \"456\", \"value\" => \"override\"}|\n    end\n\n    test \"focuses the given element with phx-value\", %{live: view} do\n      assert view |> element(\"span#span-focus-phx-value\") |> render_focus() |> is_binary()\n\n      assert last_event(view) =~\n               ~s|span-focus: %{\"bar\" => \"456\", \"foo\" => \"123\"}|\n\n      assert view\n             |> element(\"span#span-focus-phx-value\")\n             |> render_focus(%{\"foo\" => \"override\"})\n             |> is_binary()\n\n      assert last_event(view) =~\n               ~s|span-focus: %{\"bar\" => \"456\", \"foo\" => \"override\"}|\n    end\n\n    test \"raises if element does not have attribute\", %{live: view} do\n      assert_raise ArgumentError,\n                   \"element selected by \\\"span#span-no-attr\\\" does not have phx-focus attribute\",\n                   fn -> view |> element(\"span#span-no-attr\") |> render_focus() end\n    end\n  end\n\n  describe \"render_keyup\" do\n    test \"keyups the given element\", %{live: view} do\n      assert view |> element(\"span#span-keyup-no-value\") |> render_keyup() |> is_binary()\n      assert last_event(view) =~ ~s|span-keyup: %{}|\n    end\n\n    test \"keyups the given element with value\", %{live: view} do\n      assert view |> element(\"span#span-keyup-value\") |> render_keyup() |> is_binary()\n\n      assert last_event(view) =~\n               ~s|span-keyup: %{\"extra\" => \"456\", \"value\" => \"123\"}|\n\n      assert view\n             |> element(\"span#span-keyup-value\")\n             |> render_keyup(%{\"value\" => \"override\"})\n             |> is_binary()\n\n      assert last_event(view) =~\n               ~s|span-keyup: %{\"extra\" => \"456\", \"value\" => \"override\"}|\n    end\n\n    test \"keyups the given element with phx-value\", %{live: view} do\n      assert view |> element(\"span#span-keyup-phx-value\") |> render_keyup() |> is_binary()\n\n      assert last_event(view) =~\n               ~s|span-keyup: %{\"bar\" => \"456\", \"foo\" => \"123\"}|\n\n      assert view\n             |> element(\"span#span-keyup-phx-value\")\n             |> render_keyup(%{\"foo\" => \"override\"})\n             |> is_binary()\n\n      assert last_event(view) =~\n               ~s|span-keyup: %{\"bar\" => \"456\", \"foo\" => \"override\"}|\n    end\n\n    test \"keyups the given element with phx-window-keyup\", %{live: view} do\n      assert view |> element(\"span#span-window-keyup-phx-value\") |> render_keyup() |> is_binary()\n\n      assert last_event(view) =~\n               ~s|span-window-keyup: %{\"bar\" => \"456\", \"foo\" => \"123\"}|\n    end\n\n    test \"raises if element does not have attribute\", %{live: view} do\n      assert_raise ArgumentError,\n                   \"element selected by \\\"span#span-no-attr\\\" does not have phx-keyup or phx-window-keyup attributes\",\n                   fn -> view |> element(\"span#span-no-attr\") |> render_keyup() end\n    end\n  end\n\n  describe \"render_keydown\" do\n    test \"keydowns the given element\", %{live: view} do\n      assert view |> element(\"span#span-keydown-no-value\") |> render_keydown() |> is_binary()\n      assert last_event(view) =~ ~s|span-keydown: %{}|\n    end\n\n    test \"keydowns the given element with value\", %{live: view} do\n      assert view |> element(\"span#span-keydown-value\") |> render_keydown() |> is_binary()\n      assert last_event(view) =~ ~s|span-keydown: %{\"extra\" => \"456\", \"value\" => \"123\"}|\n\n      assert view\n             |> element(\"span#span-keydown-value\")\n             |> render_keydown(%{\"value\" => \"override\"})\n             |> is_binary()\n\n      assert last_event(view) =~ ~s|span-keydown: %{\"extra\" => \"456\", \"value\" => \"override\"}|\n    end\n\n    test \"keydowns the given element with phx-value\", %{live: view} do\n      assert view |> element(\"span#span-keydown-phx-value\") |> render_keydown() |> is_binary()\n      assert last_event(view) =~ ~s|span-keydown: %{\"bar\" => \"456\", \"foo\" => \"123\"}|\n\n      assert view\n             |> element(\"span#span-keydown-phx-value\")\n             |> render_keydown(%{\"foo\" => \"override\"})\n             |> is_binary()\n\n      assert last_event(view) =~ ~s|span-keydown: %{\"bar\" => \"456\", \"foo\" => \"override\"}|\n    end\n\n    test \"keydowns the given element with phx-window-keydown\", %{live: view} do\n      assert view\n             |> element(\"span#span-window-keydown-phx-value\")\n             |> render_keydown()\n             |> is_binary()\n\n      assert last_event(view) =~ ~s|span-window-keydown: %{\"bar\" => \"456\", \"foo\" => \"123\"}|\n    end\n\n    test \"raises if element does not have attribute\", %{live: view} do\n      assert_raise ArgumentError,\n                   \"element selected by \\\"span#span-no-attr\\\" does not have phx-keydown or phx-window-keydown attributes\",\n                   fn -> view |> element(\"span#span-no-attr\") |> render_keydown() end\n    end\n  end\n\n  describe \"render_change\" do\n    test \"changes the given element\", %{live: view} do\n      assert view |> element(\"#empty-form\") |> render_change()\n      assert last_event(view) =~ ~s|form-change: %{}|\n\n      assert view |> element(\"#empty-form\") |> render_change(foo: \"bar\")\n      assert last_event(view) =~ ~s|form-change: %{\"foo\" => \"bar\"}|\n\n      assert view |> element(\"#empty-form\") |> render_change(%{\"foo\" => \"bar\"})\n      assert last_event(view) =~ ~s|form-change: %{\"foo\" => \"bar\"}|\n    end\n\n    test \"phx-change on individual input\", %{live: view} do\n      assert view\n             |> element(\"input[name='hello[individual]']\")\n             |> render_change(hello: [individual: \"123\"], _target: \"hello[individual]\")\n\n      assert last_event(view) ==\n               \"<div id=\\\"last-event\\\">individual-changed: %{\\\"_target\\\" => [\\\"hello\\\", \\\"individual\\\"], \\\"hello\\\" => %{\\\"individual\\\" => \\\"123\\\"}}</div>\"\n\n      assert view\n             |> form(\"#form\", hello: [latest: \"i win\"])\n             |> render_change(hello: [latest: \"i truly win\"])\n\n      assert last_event(view) =~ ~s|\"latest\" => \"i truly win\"|\n    end\n  end\n\n  test \"put_submitter/2 puts submitter meta on element\", %{live: view} do\n    selector = \"button[name=submitter]\"\n\n    from_element = view |> element(\"form\") |> put_submitter(element(view, selector))\n    from_selector = view |> element(\"form\") |> put_submitter(selector)\n\n    assert from_element.meta.submitter == from_selector.meta.submitter\n  end\n\n  test \"put_submitter/2 works on forms without IDs\", %{live: view} do\n    view\n    |> element(\"form[data-name='form-without-id']\")\n    |> put_submitter(\"[name=button]\")\n    |> render_submit()\n  end\n\n  describe \"render_submit\" do\n    test \"raises if element is not a form\", %{live: view} do\n      assert_raise ArgumentError, \"phx-submit is only allowed in forms, got \\\"a\\\"\", fn ->\n        view |> element(\"#a-no-form\") |> render_submit()\n      end\n    end\n\n    test \"submits the given element\", %{live: view} do\n      assert view |> element(\"#empty-form\") |> render_submit()\n      assert last_event(view) =~ ~s|form-submit: %{}|\n\n      assert view |> element(\"#empty-form\") |> render_submit(foo: \"bar\")\n      assert last_event(view) =~ ~s|form-submit: %{\"foo\" => \"bar\"}|\n\n      assert view |> element(\"#empty-form\") |> render_submit(%{\"foo\" => \"bar\"})\n      assert last_event(view) =~ ~s|form-submit: %{\"foo\" => \"bar\"}|\n    end\n\n    test \"submits data passed as phx-value-* attributes\", %{live: view} do\n      assert view |> element(\"#phx-value-form\") |> render_submit()\n      assert last_event(view) =~ ~s|form-submit: %{\"foo\" => \"bar\", \"key\" => \"val\"}|\n\n      assert view |> element(\"#phx-value-form\") |> render_submit(foo: \"baz\")\n      assert last_event(view) =~ ~s|form-submit: %{\"foo\" => \"baz\", \"key\" => \"val\"}|\n    end\n\n    test \"raises on invalid submitter\", %{live: view} do\n      assert_raise ArgumentError, ~r\"invalid form submitter\", fn ->\n        assert view\n               |> element(\"#submitter-form\")\n               |> put_submitter(\"#element-does-not-exist\")\n               |> render_submit()\n      end\n\n      assert_raise ArgumentError, ~r\"invalid form submitter\", fn ->\n        assert view\n               |> element(\"#submitter-form\")\n               |> put_submitter(\"button\")\n               |> render_submit()\n      end\n\n      assert_raise ArgumentError,\n                   ~r\"form submitter selected by \\\"#input_no_name\\\" must have a name\",\n                   fn ->\n                     assert view\n                            |> element(\"#submitter-form\")\n                            |> put_submitter(\"#input_no_name\")\n                            |> render_submit()\n                   end\n\n      assert_raise ArgumentError,\n                   ~r\"could not find non-disabled submit input or button with name \\\"input_disabled\\\"\",\n                   fn ->\n                     assert view\n                            |> element(\"#submitter-form\")\n                            |> put_submitter(\"[name=input_disabled]\")\n                            |> render_submit()\n                   end\n\n      assert_raise ArgumentError,\n                   ~r\"could not find non-disabled submit input or button with name \\\"button_disabled\\\"\",\n                   fn ->\n                     assert view\n                            |> element(\"#submitter-form\")\n                            |> put_submitter(\"[name=button_disabled]\")\n                            |> render_submit()\n                   end\n\n      assert_raise ArgumentError,\n                   ~r\"could not find non-disabled submit input or button with name \\\"button_no_submit\\\"\",\n                   fn ->\n                     assert view\n                            |> element(\"#submitter-form\")\n                            |> put_submitter(\"[name=button_no_submit]\")\n                            |> render_submit()\n                   end\n    end\n\n    test \"includes the submitter key/value pair in the payload\", %{live: view} do\n      assert view\n             |> element(\"#submitter-form\")\n             |> put_submitter(\"[name=input]\")\n             |> render_submit()\n\n      assert last_event(view) =~ ~s|form-submit: %{\"data\" => %{\"a\" => \"b\"}, \"input\" => \"yes\"}|\n\n      assert view\n             |> element(\"#submitter-form\")\n             |> put_submitter(\"input#data-nested\")\n             |> render_submit()\n\n      assert last_event(view) =~ ~s|form-submit: %{\"data\" => %{\"a\" => \"b\", \"nested\" => \"yes\"}}|\n\n      assert view\n             |> element(\"#submitter-form\")\n             |> put_submitter(\"[name=button]\")\n             |> render_submit()\n\n      assert last_event(view) =~ ~s|form-submit: %{\"button\" => \"yes\", \"data\" => %{\"a\" => \"b\"}}|\n\n      assert view\n             |> element(\"#submitter-form\")\n             |> put_submitter(\"[name=button_no_type]\")\n             |> render_submit()\n\n      assert last_event(view) =~\n               ~s|form-submit: %{\"button_no_type\" => \"yes\", \"data\" => %{\"a\" => \"b\"}}|\n\n      assert view\n             |> element(\"#submitter-form\")\n             |> put_submitter(\"[name=button_no_value]\")\n             |> render_submit()\n\n      assert last_event(view) =~\n               ~s|form-submit: %{\"button_no_value\" => \"\", \"data\" => %{\"a\" => \"b\"}}|\n    end\n  end\n\n  describe \"follow_trigger_action\" do\n    test \"raises if element is not a form\", %{live: view, conn: conn} do\n      assert_raise ArgumentError,\n                   ~r\"given element did not return a form\",\n                   fn -> view |> element(\"#a-no-form\") |> follow_trigger_action(conn) end\n    end\n\n    test \"raises if element doesn't set phx-trigger-action on the form element\",\n         %{live: view, conn: conn} do\n      assert_raise ArgumentError,\n                   ~r\"\\\"#empty-form\\\" does not have a phx-trigger-action attribute\",\n                   fn -> view |> element(\"#empty-form\") |> follow_trigger_action(conn) end\n    end\n\n    test \"uses default method and request path\", %{live: view, conn: conn} do\n      view |> element(\"#trigger-form-default\") |> render_submit()\n\n      conn = view |> element(\"#trigger-form-default\") |> follow_trigger_action(conn)\n      assert conn.method == \"GET\"\n      assert conn.request_path == \"/elements\"\n\n      conn =\n        view |> form(\"#trigger-form-default\", %{\"foo\" => \"bar\"}) |> follow_trigger_action(conn)\n\n      assert conn.method == \"GET\"\n      assert conn.request_path == \"/elements\"\n      assert %{\"foo\" => \"bar\", \"from-form\" => \"included\"} = URI.decode_query(conn.query_string)\n\n      conn =\n        view |> form(\"#trigger-form-default\", foo: \"bar\") |> follow_trigger_action(conn)\n\n      assert conn.method == \"GET\"\n      assert conn.request_path == \"/elements\"\n      assert %{\"foo\" => \"bar\", \"from-form\" => \"included\"} = URI.decode_query(conn.query_string)\n\n      conn = view |> form(\"#trigger-form-value\", %{\"baz\" => \"bat\"}) |> follow_trigger_action(conn)\n      assert conn.method == \"POST\"\n      assert conn.request_path == \"/not_found\"\n      assert %{\"baz\" => \"bat\", \"from-form\" => \"included\"} = conn.params\n    end\n  end\n\n  describe \"submit_form\" do\n    test \"submits textarea with newline characters\", %{live: view} do\n      view\n      |> form(\"#form\")\n      |> render_submit()\n\n      expected_string_in_event =\n        \"textarea_with_newlines\\\" => \\\"This is a test.\\\\nIt has multiple\\\\nlines of text.\\\"\"\n\n      assert last_event(view) =~ expected_string_in_event\n    end\n\n    test \"raises if element is not a form\", %{live: view, conn: conn} do\n      assert_raise ArgumentError,\n                   ~r\"given element did not return a form\",\n                   fn -> view |> element(\"#a-no-form\") |> submit_form(conn) end\n    end\n\n    test \"raises if element doesn't set action on the form element\",\n         %{live: view, conn: conn} do\n      assert_raise ArgumentError,\n                   ~r\"\\\"#empty-form\\\" does not have an action attribute\",\n                   fn -> view |> element(\"#empty-form\") |> submit_form(conn) end\n    end\n\n    test \"uses default method and form action\", %{live: view, conn: conn} do\n      conn = view |> element(\"#submit-form-default\") |> submit_form(conn)\n      assert conn.method == \"GET\"\n      assert conn.request_path == \"/not_found\"\n\n      conn = view |> form(\"#submit-form-default\", %{\"foo\" => \"bar\"}) |> submit_form(conn)\n\n      assert conn.method == \"GET\"\n      assert conn.request_path == \"/not_found\"\n      assert conn.query_string == \"foo=bar\"\n    end\n\n    test \"named form\", %{live: view, conn: _conn} do\n      view\n      |> form(\"#named\", %{foo: \"a\", bar: \"b\", baz: \"c\", child: \"cc\"})\n      |> put_submitter(\"[name=btn]\")\n      |> render_submit()\n\n      assert last_event(view) =~ ~s|form-submit-named: %{|\n      assert last_event(view) =~ ~s|\"foo\" => \"a\"|\n      assert last_event(view) =~ ~s|\"bar\" => \"b\"|\n      assert last_event(view) =~ ~s|\"baz\" => \"c\"|\n      assert last_event(view) =~ ~s|\"child\" => \"cc\"|\n      assert last_event(view) =~ ~s|\"btn\" => \"x\"|\n    end\n  end\n\n  describe \"form\" do\n    test \"defaults\", %{live: view} do\n      view |> form(\"#form\") |> render_change()\n      form = last_event(view)\n      assert form =~ ~s|form-change: %{\"hello\" => %{|\n\n      # Element without types are still handle\n      assert form =~ ~s|\"no-type\" => \"value\"|\n\n      # Latest always wins\n      assert form =~ ~s|\"latest\" => \"new\"|\n\n      # Hidden elements too\n      assert form =~ ~s|\"hidden\" => \"hidden\"|\n      assert form =~ ~s|\"hidden_or_checkbox\" => \"false\"|\n      assert form =~ ~s|\"hidden_or_text\" => \"true\"|\n\n      # Radio stores checked one but not disabled and not checked\n      assert form =~ ~s|\"radio\" => \"2\"|\n      refute form =~ ~s|\"not-checked-radio\"|\n      refute form =~ ~s|\"disabled-radio\"|\n\n      # Checkbox stores checked ones but not disabled and not checked\n      assert form =~ ~s|\"checkbox\" => \"2\"|\n      refute form =~ ~s|\"not-checked-checkbox\"|\n      refute form =~ ~s|\"disabled-checkbox\"|\n\n      # Multiple checkbox\n      assert form =~ ~s|\"multiple-checkbox\" => [\"2\", \"3\"]|\n\n      # Select\n      assert form =~ ~s|\"selected\" => \"1\"|\n      assert form =~ ~s|\"not-selected\" => \"blank\"|\n      assert form =~ ~s|\"not-selected-treeorder\" => \"blank\"|\n      refute form =~ ~s|\"not-selected-size\"|\n      assert form =~ ~s|\"invalid-multiple-selected\" => \"3\"|\n\n      # Multiple Select\n      assert form =~ ~s|\"multiple-select\" => [\"2\", \"3\"]|\n\n      # Text area\n      assert form =~ ~s|\"textarea\" => \"Text\"|\n      assert form =~ ~s|\"textarea_nl\" => \"Text\"|\n      assert form =~ ~s|\"textarea_empty\" => \"\"|\n\n      # Ignore everything with no name, disabled, or submits\n      refute form =~ \"no-name\"\n      refute form =~ \"disabled\"\n      refute form =~ \"ignore-submit\"\n      refute form =~ \"ignore-image\"\n    end\n\n    test \"fill in target\", %{live: view} do\n      view |> form(\"#form\") |> render_change(%{\"_target\" => \"order_item[addons][][name]\"})\n      form = last_event(view)\n      assert form =~ ~s|\"hello\" => %{|\n      assert form =~ ~s|%{\"_target\" => [\"order_item\", \"addons\", \"name\"]|\n    end\n\n    test \"fill in missing\", %{live: view} do\n      assert_raise ArgumentError,\n                   ~r/could not find non-disabled input, select or textarea with name \"hello\\[unknown\\]\"/,\n                   fn -> view |> form(\"#form\", hello: [unknown: \"true\"]) |> render_change() end\n    end\n\n    test \"fill in forbidden\", %{live: view} do\n      assert_raise ArgumentError,\n                   \"cannot provide value to \\\"hello[ignore-submit]\\\" because submit inputs are never submitted\",\n                   fn ->\n                     view |> form(\"#form\", hello: [\"ignore-submit\": \"true\"]) |> render_change()\n                   end\n\n      assert_raise ArgumentError,\n                   \"cannot provide value to \\\"hello[ignore-image]\\\" because image inputs are never submitted\",\n                   fn ->\n                     view |> form(\"#form\", hello: [\"ignore-image\": \"true\"]) |> render_change()\n                   end\n    end\n\n    test \"fill in hidden\", %{live: view} do\n      assert_raise ArgumentError,\n                   \"value for hidden \\\"hello[hidden]\\\" must be one of [\\\"hidden\\\"], got: \\\"true\\\"\",\n                   fn -> view |> form(\"#form\", hello: [hidden: \"true\"]) |> render_change() end\n\n      assert view\n             |> form(\"#form\",\n               hello: [\n                 hidden: \"hidden\",\n                 hidden_or_checkbox: \"true\",\n                 hidden_or_text: \"any text\"\n               ]\n             )\n             |> render_change()\n\n      form = last_event(view)\n\n      assert form =~ ~s|\"hidden\" => \"hidden\"|\n      assert form =~ ~s|\"hidden_or_checkbox\" => \"true\"|\n      assert form =~ ~s|\"hidden_or_text\" => \"any text\"|\n    end\n\n    test \"fill in radio\", %{live: view} do\n      assert_raise ArgumentError,\n                   \"value for radio \\\"hello[radio]\\\" must be one of [\\\"1\\\", \\\"2\\\", \\\"3\\\"], got: \\\"unknown\\\"\",\n                   fn -> view |> form(\"#form\", hello: [radio: \"unknown\"]) |> render_change() end\n\n      assert view |> form(\"#form\", hello: [radio: \"1\"]) |> render_change()\n      assert last_event(view) =~ ~s|\"radio\" => \"1\"|\n\n      assert_raise ArgumentError,\n                   ~r/could not find non-disabled input, select or textarea with name \"hello\\[radio\\]\\[\\]\"/,\n                   fn ->\n                     view |> form(\"#form\", hello: [radio: [1, 2]]) |> render_change()\n                   end\n    end\n\n    test \"fill in checkbox\", %{live: view} do\n      assert_raise ArgumentError,\n                   \"value for checkbox \\\"hello[checkbox]\\\" must be one of [\\\"1\\\", \\\"2\\\", \\\"3\\\"], got: \\\"unknown\\\"\",\n                   fn ->\n                     view |> form(\"#form\", hello: [checkbox: \"unknown\"]) |> render_change()\n                   end\n\n      assert view |> form(\"#form\", hello: [checkbox: \"1\"]) |> render_change()\n      assert last_event(view) =~ ~s|\"checkbox\" => \"1\"|\n\n      assert_raise ArgumentError,\n                   ~r/could not find non-disabled input, select or textarea with name \"hello\\[checkbox\\]\\[\\]\"/,\n                   fn ->\n                     view |> form(\"#form\", hello: [checkbox: [1, 2]]) |> render_change()\n                   end\n    end\n\n    test \"fill in checkbox without value (default: on)\", %{live: view} do\n      assert view |> form(\"#form\", hello: [checkbox_no_value: \"on\"]) |> render_change\n      assert last_event(view) =~ ~s|\"checkbox_no_value\" => \"on\"|\n    end\n\n    test \"fill in multiple checkbox\", %{live: view} do\n      assert_raise ArgumentError,\n                   \"value for checkbox \\\"hello[multiple-checkbox][]\\\" must be one of [\\\"1\\\", \\\"2\\\", \\\"3\\\"], got: \\\"unknown\\\"\",\n                   fn ->\n                     view\n                     |> form(\"#form\", hello: [\"multiple-checkbox\": [\"unknown\"]])\n                     |> render_change()\n                   end\n\n      assert view |> form(\"#form\", hello: [\"multiple-checkbox\": [1, 2]]) |> render_change()\n      assert last_event(view) =~ ~s|\"multiple-checkbox\" => [\"1\", \"2\"]|\n    end\n\n    test \"fill in select\", %{live: view} do\n      assert_raise ArgumentError,\n                   \"value for select \\\"hello[selected]\\\" must be one of [\\\"blank\\\", \\\"1\\\", \\\"2\\\"], got: \\\"unknown\\\"\",\n                   fn ->\n                     view |> form(\"#form\", hello: [selected: \"unknown\"]) |> render_change()\n                   end\n\n      assert view |> form(\"#form\", hello: [selected: \"1\"]) |> render_change()\n      assert last_event(view) =~ ~s|\"selected\" => \"1\"|\n\n      assert_raise ArgumentError,\n                   ~r/could not find non-disabled input, select or textarea with name \"hello\\[selected\\]\\[\\]\"/,\n                   fn ->\n                     view |> form(\"#form\", hello: [selected: [1, 2]]) |> render_change()\n                   end\n    end\n\n    test \"fill in multiple select\", %{live: view} do\n      assert_raise ArgumentError,\n                   \"value for multiple select \\\"hello[multiple-select][]\\\" must be one of [\\\"1\\\", \\\"2\\\", \\\"3\\\"], got: \\\"unknown\\\"\",\n                   fn ->\n                     view\n                     |> form(\"#form\", hello: [\"multiple-select\": [\"unknown\"]])\n                     |> render_change()\n                   end\n\n      assert view |> form(\"#form\", hello: [\"multiple-select\": [1, 2]]) |> render_change()\n      assert last_event(view) =~ ~s|\"multiple-select\" => [\"1\", \"2\"]|\n    end\n\n    test \"fill in input\", %{live: view} do\n      assert view |> form(\"#form\", hello: [latest: \"i win\"]) |> render_change()\n      assert last_event(view) =~ ~s|\"latest\" => \"i win\"|\n\n      assert view\n             |> form(\"#form\", hello: [latest: \"i win\"])\n             |> render_change(hello: [latest: \"i truly win\"])\n\n      assert last_event(view) =~ ~s|\"latest\" => \"i truly win\"|\n\n      assert_raise ArgumentError,\n                   ~r/could not find non-disabled input, select or textarea with name \"hello\\[latest\\]\\[\\]\"/,\n                   fn ->\n                     view |> form(\"#form\", hello: [latest: [\"i lose\"]]) |> render_change()\n                   end\n    end\n\n    test \"fill in textarea\", %{live: view} do\n      assert view |> form(\"#form\", hello: [textarea: \"i win\"]) |> render_change()\n      assert last_event(view) =~ ~s|\"textarea\" => \"i win\"|\n\n      assert view\n             |> form(\"#form\", hello: [textarea: \"i win\"])\n             |> render_change(hello: [textarea: \"i truly win\"])\n\n      assert last_event(view) =~ ~s|\"textarea\" => \"i truly win\"|\n\n      assert_raise ArgumentError,\n                   ~r/could not find non-disabled input, select or textarea with name \"hello\\[textarea\\]\\[\\]\"/,\n                   fn ->\n                     view |> form(\"#form\", hello: [textarea: [\"i lose\"]]) |> render_change()\n                   end\n    end\n\n    test \"fill in calendar types\", %{live: view} do\n      year = Date.utc_today().year\n\n      assert_raise ArgumentError,\n                   ~r/could not find non-disabled input, select or textarea with name \"hello\\[unknown\\]\"/,\n                   fn ->\n                     view\n                     |> form(\"#form\", hello: [unknown: Date.new!(year, 4, 17)])\n                     |> render_change()\n                   end\n\n      assert view |> form(\"#form\", hello: [date_text: \"#{year}-04-17\"]) |> render_change()\n      assert last_event(view) =~ ~s|\"date_text\" => \"#{year}-04-17\"|\n\n      assert view\n             |> form(\"#form\", hello: [date_select: Date.new!(year, 4, 17)])\n             |> render_change()\n\n      assert last_event(view) =~\n               ~s|\"date_select\" => %{\"day\" => \"17\", \"month\" => \"4\", \"year\" => \"#{year}\"}|\n\n      assert view |> form(\"#form\", hello: [time_text: \"14:15:16\"]) |> render_change()\n      assert last_event(view) =~ ~s|\"time_text\" => \"14:15:16\"|\n\n      assert view |> form(\"#form\", hello: [time_select: ~T\"14:15:16\"]) |> render_change()\n      assert last_event(view) =~ ~s|\"time_select\" => %{\"hour\" => \"14\", \"minute\" => \"15\"}|\n\n      assert view\n             |> form(\"#form\", hello: [naive_text: \"#{year}-04-17 14:15:16\"])\n             |> render_change()\n\n      assert last_event(view) =~ ~s|\"naive_text\" => \"#{year}-04-17 14:15:16\"|\n\n      naive = NaiveDateTime.new!(Date.new!(year, 4, 17), ~T[14:15:16])\n\n      assert view\n             |> form(\"#form\", hello: [naive_select: naive])\n             |> render_change()\n\n      assert last_event(view) =~\n               ~s|\"naive_select\" => %{\"day\" => \"17\", \"hour\" => \"14\", \"minute\" => \"15\", \"month\" => \"4\", \"year\" => \"#{year}\"}|\n\n      assert view\n             |> form(\"#form\", hello: [utc_text: \"#{year}-04-17 14:15:16Z\"])\n             |> render_change()\n\n      assert last_event(view) =~ ~s|\"utc_text\" => \"#{year}-04-17 14:15:16Z\"|\n\n      assert view\n             |> form(\"#form\",\n               hello: [utc_select: DateTime.from_naive!(naive, \"Etc/UTC\")]\n             )\n             |> render_change()\n\n      assert last_event(view) =~\n               ~s|\"utc_select\" => %{\"day\" => \"17\", \"hour\" => \"14\", \"minute\" => \"15\", \"month\" => \"4\", \"second\" => \"16\", \"year\" => \"#{year}\"}|\n    end\n  end\n\n  describe \"open_browser\" do\n    setup do\n      open_fun = fn path ->\n        assert content = File.read!(path)\n\n        assert content =~\n                 ~r[<link rel=\"stylesheet\" href=\"file:.*phoenix_live_view\\/priv\\/css\\/custom\\.css\"\\/>]\n\n        assert content =~\n                 ~r[<link rel=\"stylesheet\" href=\"file:.*phoenix_live_view\\/priv\\/static\\/css\\/app\\.css\"\\/>]\n\n        assert content =~ \"<link rel=\\\"stylesheet\\\" href=\\\"//example.com/a.css\\\"/>\"\n        assert content =~ \"<link rel=\\\"stylesheet\\\" href=\\\"https://example.com/b.css\\\"/>\"\n        assert content =~ \"body { background-color: #eee; }\"\n        refute content =~ \"<script>\"\n        path\n      end\n\n      {:ok, live, _} = live(Phoenix.ConnTest.build_conn(), \"/styled-elements\")\n      %{live: live, open_fun: open_fun}\n    end\n\n    test \"render view\", %{live: view, open_fun: open_fun} do\n      assert view |> open_browser(open_fun) == view\n    end\n\n    test \"render element\", %{live: view, open_fun: open_fun} do\n      element = element(view, \"#scoped-render\")\n      assert element |> open_browser(open_fun) == element\n    end\n  end\n\n  describe \"JS commands\" do\n    test \"push\", %{live: view} do\n      assert view |> element(\"#button-js-click\") |> render_click()\n      assert last_event(view) == \"<div id=\\\"last-event\\\">button-click: %{}</div>\"\n\n      assert view |> element(\"#button-js-click-value\") |> render_click()\n      assert last_event(view) == \"<div id=\\\"last-event\\\">button-click: %{\\\"one\\\" => 1}</div>\"\n    end\n  end\n\n  describe \"child component / JS commands\" do\n    test \"push\", %{live: view} do\n      assert view |> element(\"#component-button-js-click-target\") |> render_click()\n\n      assert last_component_event(view) ==\n               \"<div id=\\\"component-last-event\\\">button-click: %{}</div>\"\n\n      refute last_event(view) == \"<div id=\\\"last-event\\\">button-click: %{}</div>\"\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/event_test.exs",
    "content": "defmodule Phoenix.LiveView.EventTest do\n  use ExUnit.Case, async: true\n\n  import Phoenix.ConnTest\n  import Phoenix.LiveViewTest\n\n  alias Phoenix.{Component, LiveView}\n  alias Phoenix.LiveViewTest.Support.Endpoint\n\n  @endpoint Endpoint\n\n  setup config do\n    {:ok, conn: Plug.Test.init_test_session(build_conn(), config[:session] || %{})}\n  end\n\n  describe \"push_event\" do\n    test \"sends updates with general assigns diff\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/events\")\n\n      GenServer.call(\n        view.pid,\n        {:run,\n         fn socket ->\n           new_socket =\n             socket\n             |> Component.assign(count: 123)\n             |> LiveView.push_event(\"my-event\", %{one: 1})\n\n           {:reply, :ok, new_socket}\n         end}\n      )\n\n      assert_push_event(view, \"my-event\", %{one: 1})\n      assert render(view) =~ \"count: 123\"\n    end\n\n    test \"supports events with redirects\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/events\")\n\n      GenServer.call(\n        view.pid,\n        {:run,\n         fn socket ->\n           new_socket =\n             socket\n             |> Component.assign(count: 123)\n             |> LiveView.push_event(\"my-event\", %{one: 1})\n             |> LiveView.push_event(\"my-event\", %{one: 2})\n             |> LiveView.push_navigate(to: \"/events\")\n\n           {:reply, :ok, new_socket}\n         end}\n      )\n\n      assert_push_event(view, \"my-event\", %{one: 1})\n      assert_push_event(view, \"my-event\", %{one: 2})\n      assert_redirect(view, \"/events\")\n    end\n\n    test \"sends updates with no assigns diff\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/events\")\n\n      GenServer.call(\n        view.pid,\n        {:run,\n         fn socket ->\n           {:reply, :ok, LiveView.push_event(socket, \"my-event\", %{two: 2})}\n         end}\n      )\n\n      assert_push_event(view, \"my-event\", %{two: 2})\n\n      assert render(view) =~ \"count: 0\"\n    end\n\n    test \"sends no events if none are pushed\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/events\")\n\n      GenServer.call(view.pid, {:run, fn socket -> {:reply, :ok, socket} end})\n\n      refute_push_event(view, \"my-event\", _)\n    end\n\n    test \"sends updates in root and child mounts\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/events-in-mount\")\n\n      assert_push_event(view, \"root-mount\", %{root: \"foo\"})\n      assert_push_event(view, \"child-mount\", %{child: \"bar\"})\n    end\n\n    test \"sends updates in components\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/events-in-component\")\n      assert_received {:plug_conn, :sent}\n      assert_received {_, {200, _, _}}\n\n      assert_push_event(view, \"component\", %{count: 1})\n      render_click(view, \"bump\", %{})\n      assert_push_event(view, \"component\", %{count: 2})\n      refute_received _\n    end\n  end\n\n  describe \"replies\" do\n    test \"sends reply from handle_event with general assigns diff\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/events\")\n\n      assert render_hook(view, :reply, %{count: 456, reply: %{\"val\" => \"my-reply\"}}) =~\n               \"count: 456\"\n\n      assert_reply(view, %{\"val\" => \"my-reply\"})\n\n      # Check type is preserved\n      assert render_hook(view, :reply, %{count: 456, reply: %{\"val\" => 123}}) =~\n               \"count: 456\"\n\n      assert_reply(view, %{\"val\" => 123})\n    end\n\n    test \"sends reply from handle_event with no assigns diff\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/events\")\n      assert render_hook(view, :reply, %{reply: %{\"val\" => \"nodiff\"}}) =~ \"count: 0\"\n      assert_reply(view, %{\"val\" => \"nodiff\"})\n    end\n\n    test \"raises when trying to reply outside of handle_event\", %{conn: conn} do\n      Process.flag(:trap_exit, true)\n      {:ok, view, _html} = live(conn, \"/events\")\n      pid = view.pid\n      Process.monitor(pid)\n\n      assert ExUnit.CaptureLog.capture_log(fn ->\n               send(\n                 view.pid,\n                 {:run,\n                  fn socket ->\n                    {:reply, :boom, socket}\n                  end}\n               )\n\n               assert_receive {:DOWN, _ref, :process, ^pid, _reason}, 500\n             end) =~ \"Got: {:reply, :boom\"\n    end\n\n    test \"sends replies in components\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/events-in-component\")\n      assert_received {:plug_conn, :sent}\n      assert_received {_, {200, _, _}}\n\n      assert_push_event(view, \"component\", %{count: 1})\n\n      view\n      |> element(\"#comp-reply\")\n      |> render_click(%{reply: \"123\"})\n\n      assert_reply(view, %{\"comp-reply\" => %{\"reply\" => \"123\"}})\n\n      view\n      |> element(\"#comp-noreply\")\n      |> render_click(%{reply: \"123\"})\n\n      refute_received _\n    end\n  end\n\n  describe \"LiveViewTest supports multiple JS.push events\" do\n    test \"from one click\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/events-multi-js\")\n\n      assert element(view, \"#add-one-and-ten\")\n             |> render_click() =~ \"count: 11\"\n    end\n\n    test \"with replies\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/events-multi-js\")\n\n      assert element(view, \"#reply-values\")\n             |> render_click()\n\n      assert_reply(view, %{value: 1})\n      assert_reply(view, %{value: 2})\n    end\n\n    test \"from a component to itself\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/events-multi-js-in-component\")\n\n      html =\n        element(view, \"#push-to-self-child_1\")\n        |> render_click()\n\n      assert html =~ \"child_1 count: 11\"\n      assert html =~ \"child_2 count: 0\"\n    end\n\n    test \"from a component to other targets\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/events-multi-js-in-component\")\n\n      html =\n        element(view, \"#push-to-other-targets-child_1\")\n        |> render_click()\n\n      assert html =~ \"child_1 count: 1\"\n      assert html =~ \"child_2 count: 2\"\n      assert html =~ \"root count: -1\"\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/expensive_runtime_checks_test.exs",
    "content": "defmodule Phoenix.LiveViewTest.ExpensiveRuntimeChecksTest do\n  # this is intentionally async: false as we change the application\n  # environment and recompile files!\n  use ExUnit.Case, async: false\n\n  import ExUnit.CaptureIO\n\n  import Phoenix.ConnTest\n\n  import Phoenix.LiveViewTest\n  alias Phoenix.LiveViewTest.Support.Endpoint\n\n  @endpoint Endpoint\n\n  setup do\n    {:ok, conn: Plug.Test.init_test_session(Phoenix.ConnTest.build_conn(), %{})}\n  end\n\n  describe \"async\" do\n    for fun <- [:start_async, :assign_async] do\n      test \"#{fun} warns when accessing socket in function at runtime\", %{conn: conn} do\n        _ =\n          capture_io(:stderr, fn ->\n            {:ok, lv, _html} = live(conn, \"/expensive-runtime-checks\")\n            render_async(lv)\n\n            send(self(), {:lv, lv})\n          end)\n\n        lv =\n          receive do\n            {:lv, lv} -> lv\n          end\n\n        warnings =\n          capture_io(:stderr, fn ->\n            render_hook(lv, \"expensive_#{unquote(fun)}_socket\")\n          end)\n\n        assert warnings =~\n                 \"you are accessing the LiveView Socket inside a function given to #{unquote(fun)}\"\n      end\n\n      test \"#{fun} warns when accessing assigns in function at runtime\", %{conn: conn} do\n        _ =\n          capture_io(:stderr, fn ->\n            {:ok, lv, _html} = live(conn, \"/expensive-runtime-checks\")\n            render_async(lv)\n\n            send(self(), {:lv, lv})\n          end)\n\n        lv =\n          receive do\n            {:lv, lv} -> lv\n          end\n\n        warnings =\n          capture_io(:stderr, fn ->\n            render_hook(lv, \"expensive_#{unquote(fun)}_assigns\")\n          end)\n\n        assert warnings =~\n                 \"you are accessing an assigns map inside a function given to #{unquote(fun)}\"\n      end\n\n      test \"#{fun} does not warns when doing it the right way\", %{conn: conn} do\n        _ =\n          capture_io(:stderr, fn ->\n            {:ok, lv, _html} = live(conn, \"/expensive-runtime-checks\")\n            render_async(lv)\n\n            send(self(), {:lv, lv})\n          end)\n\n        lv =\n          receive do\n            {:lv, lv} -> lv\n          end\n\n        warnings =\n          capture_io(:stderr, fn ->\n            render_hook(lv, \"good_#{unquote(fun)}\")\n          end)\n\n        assert warnings == \"\"\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/flash_test.exs",
    "content": "defmodule Phoenix.LiveView.FlashIntegrationTest do\n  use ExUnit.Case, async: true\n  import Phoenix.ConnTest\n\n  import Phoenix.LiveViewTest\n  alias Phoenix.LiveView\n  alias Phoenix.LiveViewTest.Support.{Endpoint, Router}\n\n  @endpoint Endpoint\n\n  setup do\n    conn =\n      Phoenix.ConnTest.build_conn(:get, \"http://www.example.com/\", nil)\n      |> Phoenix.ConnTest.bypass_through(Router, [:browser])\n      |> get(\"/\")\n\n    {:ok, conn: conn}\n  end\n\n  describe \"LiveView <=> LiveView\" do\n    test \"redirect with flash on mount\", %{conn: conn} do\n      {:ok, conn} =\n        conn\n        |> live(\"/flash-child?mount_redirect=ok!\")\n        |> follow_redirect(conn)\n\n      assert conn.resp_body =~ \"root[ok!]:info\"\n    end\n\n    test \"returns flash as a map\", %{conn: conn} do\n      {:error, {:redirect, %{flash: flash}}} =\n        conn\n        |> live(\"/flash-child?mount_redirect=ok!\")\n\n      assert is_map(flash)\n    end\n\n    test \"redirect with flash\", %{conn: conn} do\n      {:ok, flash_child, _} = live(conn, \"/flash-child\")\n\n      {:ok, conn} =\n        flash_child\n        |> render_click(\"redirect\", %{\"to\" => \"/flash-root\", \"info\" => \"ok!\"})\n        |> follow_redirect(conn)\n\n      assert conn.resp_body =~ \"root[ok!]:info\"\n\n      flash = assert_redirected(flash_child, \"/flash-root\")\n      assert flash == %{\"info\" => \"ok!\"}\n    end\n\n    test \"redirect with flash does not include previous event flash\", %{conn: conn} do\n      {:ok, flash_child, _} = live(conn, \"/flash-child\")\n      render_click(flash_child, \"set_error\", %{\"error\" => \"ok!\"})\n\n      {:ok, conn} =\n        flash_child\n        |> render_click(\"redirect\", %{\"to\" => \"/flash-root\", \"info\" => \"ok!\"})\n        |> follow_redirect(conn, \"/flash-root\")\n\n      assert conn.resp_body =~ \"root[ok!]:info\"\n\n      flash = assert_redirected(flash_child, \"/flash-root\")\n      assert flash == %{\"info\" => \"ok!\"}\n    end\n\n    test \"back to back redirect with same flash\", %{conn: conn} do\n      {:ok, flash_root, _} = live(conn, \"/flash-root\")\n\n      {:ok, conn} =\n        flash_root\n        |> render_click(\"redirect\", %{\"to\" => \"/flash-root\", \"info\" => \"ok!\"})\n        |> follow_redirect(conn, \"/flash-root\")\n\n      flash = assert_redirected(flash_root, \"/flash-root\")\n      assert flash == %{\"info\" => \"ok!\"}\n\n      assert conn.resp_body =~ \"root[ok!]:info\"\n\n      # repeat\n\n      {:ok, flash_root, html} = live(conn)\n\n      assert html =~ \"root[ok!]:info\"\n\n      {:ok, conn} =\n        flash_root\n        |> render_click(\"redirect\", %{\"to\" => \"/flash-root\", \"info\" => \"ok!\"})\n        |> follow_redirect(conn, \"/flash-root\")\n\n      flash = assert_redirected(flash_root, \"/flash-root\")\n      assert flash == %{\"info\" => \"ok!\"}\n\n      assert conn.resp_body =~ \"root[ok!]:info\"\n    end\n\n    test \"push_navigate with flash on mount\", %{conn: conn} do\n      {:ok, _, html} =\n        conn\n        |> live(\"/flash-child?mount_push_navigate=ok!\")\n        |> follow_redirect(conn)\n\n      assert html =~ \"root[ok!]:info\"\n    end\n\n    test \"push_navigate with flash\", %{conn: conn} do\n      {:ok, flash_child, _} = live(conn, \"/flash-child\")\n\n      {:ok, root_child, disconnected_html} =\n        flash_child\n        |> render_click(\"push_navigate\", %{\"to\" => \"/flash-root\", \"info\" => \"ok!\"})\n        |> follow_redirect(conn, \"/flash-root\")\n\n      assert disconnected_html =~ \"root[ok!]:info\"\n      assert render(root_child) =~ \"root[ok!]:info\"\n\n      flash = assert_redirected(flash_child, \"/flash-root\")\n      assert flash == %{\"info\" => \"ok!\"}\n    end\n\n    test \"push_navigate with flash does not include previous event flash\", %{conn: conn} do\n      {:ok, flash_child, _} = live(conn, \"/flash-child\")\n\n      render_click(flash_child, \"set_error\", %{\"error\" => \"ok!\"})\n\n      {:ok, root_child, disconnected_html} =\n        flash_child\n        |> render_click(\"push_navigate\", %{\"to\" => \"/flash-root\", \"info\" => \"ok!\"})\n        |> follow_redirect(conn, \"/flash-root\")\n\n      assert disconnected_html =~ \"root[ok!]:info\"\n      assert render(root_child) =~ \"root[ok!]:info\"\n\n      flash = assert_redirected(flash_child, \"/flash-root\")\n      assert flash == %{\"info\" => \"ok!\"}\n    end\n\n    test \"push_patch with flash\", %{conn: conn} do\n      {:ok, flash_live, _} = live(conn, \"/flash-root\")\n\n      result =\n        render_click(flash_live, \"push_patch\", %{\"to\" => \"/flash-root?foo\", \"info\" => \"ok!\"})\n\n      assert result =~ \"uri[http://www.example.com/flash-root?foo]\"\n      assert result =~ \"root[ok!]:info\"\n\n      assert_patch(flash_live, \"/flash-root?foo\")\n    end\n\n    test \"push_patch with flash does not include previous event flash\", %{conn: conn} do\n      {:ok, flash_live, _} = live(conn, \"/flash-root\")\n      result = render_click(flash_live, \"set_error\", %{\"error\" => \"oops!\"})\n      assert result =~ \"root[oops!]:error\"\n\n      result =\n        render_click(flash_live, \"push_patch\", %{\"to\" => \"/flash-root?foo\", \"info\" => \"ok!\"})\n\n      assert result =~ \"uri[http://www.example.com/flash-root?foo]\"\n      assert result =~ \"root[ok!]:info\"\n      assert result =~ \"root[]:error\"\n\n      assert_patch(flash_live, \"/flash-root?foo\")\n    end\n\n    test \"clears flash on client-side patches\", %{conn: conn} do\n      {:ok, flash_live, _} = live(conn, \"/flash-root\")\n      result = render_click(flash_live, \"set_error\", %{\"error\" => \"oops!\"})\n      assert result =~ \"root[oops!]:error\"\n\n      result = render_patch(flash_live, \"/flash-root?foo=bar\")\n      assert result =~ \"root[]:error\"\n      assert_patched(flash_live, \"/flash-root?foo=bar\")\n    end\n\n    test \"nested redirect with flash\", %{conn: conn} do\n      {:ok, flash_live, _} = live(conn, \"/flash-root\")\n\n      flash_child = find_live_child(flash_live, \"flash-child\")\n      render_click(flash_child, \"redirect\", %{\"to\" => \"/flash-root?redirect\", \"info\" => \"ok!\"})\n      flash = assert_redirect(flash_child, \"/flash-root?redirect\")\n      assert flash == %{\"info\" => \"ok!\"}\n    end\n\n    test \"nested push_navigate with flash\", %{conn: conn} do\n      {:ok, flash_live, _} = live(conn, \"/flash-root\")\n\n      flash_child = find_live_child(flash_live, \"flash-child\")\n      render_click(flash_child, \"push_navigate\", %{\"to\" => \"/flash-root?push\", \"info\" => \"ok!\"})\n      flash = assert_redirect(flash_child, \"/flash-root?push\")\n      assert flash == %{\"info\" => \"ok!\"}\n    end\n\n    test \"nested push_patch with flash\", %{conn: conn} do\n      {:ok, flash_live, _} = live(conn, \"/flash-root\")\n\n      flash_child = find_live_child(flash_live, \"flash-child\")\n      render_click(flash_child, \"push_patch\", %{\"to\" => \"/flash-root?patch\", \"info\" => \"ok!\"})\n\n      result = render(flash_live)\n      assert result =~ \"uri[http://www.example.com/flash-root?patch]\"\n      assert result =~ \"root[ok!]\"\n    end\n\n    test \"raises on invalid follow redirect\", %{conn: conn} do\n      {:ok, flash_child, _} = live(conn, \"/flash-child\")\n\n      assert_raise ArgumentError,\n                   \"expected LiveView to redirect to \\\"/wrong\\\", but got \\\"/flash-root\\\"\",\n                   fn ->\n                     flash_child\n                     |> render_click(\"redirect\", %{\"to\" => \"/flash-root\", \"info\" => \"ok!\"})\n                     |> follow_redirect(conn, \"/wrong\")\n                   end\n    end\n\n    test \"raises on invalid assert redirect\", %{conn: conn} do\n      {:ok, flash_child, _} = live(conn, \"/flash-child\")\n      render_click(flash_child, \"redirect\", %{\"to\" => \"/flash-root\", \"info\" => \"ok!\"})\n\n      assert_raise ArgumentError,\n                   \"expected Phoenix.LiveViewTest.Support.FlashChildLive to redirect to \\\"/wrong\\\", but got a redirect to \\\"/flash-root\\\"\",\n                   fn -> assert_redirect(flash_child, \"/wrong\") end\n\n      {:ok, flash_live, _} = live(conn, \"/flash-root\")\n      render_click(flash_live, \"push_patch\", %{\"to\" => \"/flash-root?foo\", \"info\" => \"ok!\"})\n\n      assert_raise ArgumentError,\n                   \"expected Phoenix.LiveViewTest.Support.FlashLive to redirect to \\\"/wrong\\\", but got a patch to \\\"/flash-root?foo\\\"\",\n                   fn -> assert_redirect(flash_live, \"/wrong\") end\n    end\n\n    test \"raises on invalid assert patch\", %{conn: conn} do\n      {:ok, flash_live, _} = live(conn, \"/flash-root\")\n      render_click(flash_live, \"push_patch\", %{\"to\" => \"/flash-root?foo\", \"info\" => \"ok!\"})\n\n      assert_raise ArgumentError,\n                   \"expected Phoenix.LiveViewTest.Support.FlashLive to patch to \\\"/wrong\\\", but got a patch to \\\"/flash-root?foo\\\"\",\n                   fn -> assert_patch(flash_live, \"/wrong\") end\n\n      {:ok, flash_child, _} = live(conn, \"/flash-child\")\n      render_click(flash_child, \"redirect\", %{\"to\" => \"/flash-root\", \"info\" => \"ok!\"})\n\n      assert_raise ArgumentError,\n                   \"expected Phoenix.LiveViewTest.Support.FlashChildLive to patch to \\\"/wrong\\\", but got a redirect to \\\"/flash-root\\\"\",\n                   fn -> assert_patch(flash_child, \"/wrong\") end\n    end\n  end\n\n  describe \"LiveComponent => LiveView\" do\n    test \"redirect with flash from component\", %{conn: conn} do\n      {:ok, flash_live, _} = live(conn, \"/flash-root\")\n\n      {:error, {:redirect, %{flash: _}}} =\n        flash_live\n        |> element(\"#flash-component\")\n        |> render_click(%{\n          \"type\" => \"redirect\",\n          \"to\" => \"/flash-root\",\n          \"info\" => \"ok!\"\n        })\n\n      flash = assert_redirect(flash_live, \"/flash-root\")\n      assert flash == %{\"info\" => \"ok!\"}\n    end\n\n    test \"push_navigate with flash from component\", %{conn: conn} do\n      {:ok, flash_live, _} = live(conn, \"/flash-root\")\n\n      {:error, {:live_redirect, %{flash: _}}} =\n        flash_live\n        |> element(\"#flash-component\")\n        |> render_click(%{\n          \"type\" => \"push_navigate\",\n          \"to\" => \"/flash-root\",\n          \"info\" => \"ok!\"\n        })\n\n      flash = assert_redirect(flash_live, \"/flash-root\")\n      assert flash == %{\"info\" => \"ok!\"}\n    end\n\n    test \"push_patch with flash from component\", %{conn: conn} do\n      {:ok, flash_live, _} = live(conn, \"/flash-root\")\n\n      flash_live\n      |> element(\"#flash-component\")\n      |> render_click(%{\n        \"type\" => \"push_patch\",\n        \"to\" => \"/flash-root?patch\",\n        \"info\" => \"ok!\"\n      })\n\n      result = render(flash_live)\n      assert result =~ \"uri[http://www.example.com/flash-root?patch]\"\n      assert result =~ \"root[ok!]\"\n    end\n  end\n\n  describe \"LiveView <=> DeadView\" do\n    test \"redirect with flash from LiveView to DeadView\", %{conn: conn} do\n      {:ok, flash_live, _} = live(conn, \"/flash-root\")\n\n      {:error, {:redirect, %{flash: _}}} =\n        render_click(flash_live, \"redirect\", %{\"to\" => \"/\", \"info\" => \"ok!\"})\n\n      flash = assert_redirect(flash_live, \"/\")\n      assert flash == %{\"info\" => \"ok!\"}\n    end\n\n    test \"redirect with flash from DeadView to LiveView\", %{conn: conn} do\n      conn =\n        conn\n        |> LiveView.Router.fetch_live_flash([])\n        |> Phoenix.Controller.put_flash(:info, \"flash from the dead\")\n        |> Phoenix.Controller.redirect(to: \"/flash-root\")\n        |> get(\"/flash-root\")\n\n      assert html_response(conn, 200) =~ \"flash from the dead\"\n      {:ok, _flash_live, html} = live(conn)\n      assert html =~ \"flash from the dead\"\n    end\n  end\n\n  test \"lv:clear-flash\", %{conn: conn} do\n    {:ok, flash_live, _} = live(conn, \"/flash-root\")\n\n    result =\n      render_click(flash_live, \"push_patch\", %{\"to\" => \"/flash-root?patch\", \"info\" => \"ok!\"})\n\n    assert result =~ \"uri[http://www.example.com/flash-root?patch]\"\n    assert result =~ \"root[ok!]:info\"\n\n    result = render_click(flash_live, \"lv:clear-flash\", %{key: \"info\"})\n    assert result =~ \"root[]:info\"\n\n    result =\n      render_click(flash_live, \"push_patch\", %{\"to\" => \"/flash-root?patch\", \"info\" => \"ok!\"})\n\n    assert result =~ \"uri[http://www.example.com/flash-root?patch]\"\n    assert result =~ \"root[ok!]:info\"\n\n    result = render_click(flash_live, \"lv:clear-flash\")\n    assert result =~ \"root[]:info\"\n  end\n\n  test \"lv:clear-flash component\", %{conn: conn} do\n    {:ok, flash_live, _} = live(conn, \"/flash-root\")\n\n    flash_live\n    |> element(\"#flash-component\")\n    |> render_click(%{\"type\" => \"put_flash\", \"info\" => \"ok!\"})\n\n    flash_live\n    |> element(\"#flash-component\")\n    |> render_click(%{\"type\" => \"put_flash\", \"error\" => \"oops!\"})\n\n    assert has_element?(\n             flash_live,\n             \"#flash-component span[phx-value-key=info]\",\n             \"component[ok!]:info\"\n           )\n\n    assert has_element?(\n             flash_live,\n             \"#flash-component span[phx-value-key=error]\",\n             \"component[oops!]:error\"\n           )\n\n    flash_live |> element(\"#flash-component span\", \"Clear all\") |> render_click()\n\n    assert has_element?(\n             flash_live,\n             \"#flash-component span[phx-value-key=info]\",\n             \"component[]:info\"\n           )\n\n    assert has_element?(\n             flash_live,\n             \"#flash-component span[phx-value-key=error]\",\n             \"component[]:error\"\n           )\n  end\n\n  test \"lv:clear-flash component with phx-value-key\", %{conn: conn} do\n    {:ok, flash_live, _} = live(conn, \"/flash-root\")\n\n    flash_live\n    |> element(\"#flash-component\")\n    |> render_click(%{\"type\" => \"put_flash\", \"error\" => \"oops!\"})\n\n    assert has_element?(\n             flash_live,\n             \"#flash-component span[phx-value-key=error]\",\n             \"component[oops!]:error\"\n           )\n\n    flash_live |> element(\"#flash-component span\", \":error\") |> render_click()\n\n    assert has_element?(\n             flash_live,\n             \"#flash-component span[phx-value-key=error]\",\n             \"component[]:error\"\n           )\n  end\n\n  test \"works without session and flash\", %{conn: conn} do\n    {:ok, live, html} = live(conn, \"/sessionless-thermo\")\n    assert html =~ \"The temp is: 1\"\n    assert render(live) =~ \"The temp is: 1\"\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/hooks_test.exs",
    "content": "defmodule Phoenix.LiveView.HooksTest do\n  use ExUnit.Case, async: true\n\n  import Phoenix.ConnTest\n  import Phoenix.LiveViewTest\n\n  alias Phoenix.Component\n  alias Phoenix.LiveViewTest.Support.{Endpoint, HooksLive}\n\n  @endpoint Endpoint\n\n  setup do\n    {:ok, conn: Plug.Test.init_test_session(build_conn(), %{})}\n  end\n\n  test \"on_mount hook raises when hook result is invalid\", %{conn: conn} do\n    assert_raise ArgumentError,\n                 ~r(invalid return from hook {Phoenix.LiveViewTest.Support.HooksLive.BadMount, :default}),\n                 fn ->\n                   live(conn, \"/lifecycle/bad-mount\")\n                 end\n  end\n\n  test \"on_mount hooks are invoked in the order they are declared\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/lifecycle\")\n\n    assigns = HooksLive.run(lv, fn socket -> {:reply, socket.assigns, socket} end)\n\n    assert assigns.init_assigns_mount\n    assert assigns.init_assigns_other_mount\n    assert assigns.last_on_mount == :init_assigns_other_mount\n  end\n\n  test \"on_mount hook raises when :halt is returned without a redirected socket\", %{conn: conn} do\n    assert_raise ArgumentError,\n                 ~r(the hook {Phoenix.LiveViewTest.Support.HooksLive.HaltMount, :hook} for lifecycle event :mount attempted to halt without redirecting.),\n                 fn ->\n                   live(conn, \"/lifecycle/halt-mount\")\n                 end\n  end\n\n  test \"on_mount hook raises when :cont is returned with a redirected socket\", %{conn: conn} do\n    assert_raise ArgumentError,\n                 ~r(the hook {Phoenix.LiveViewTest.Support.HooksLive.RedirectMount, :default} for lifecycle event :mount attempted to redirect without halting.),\n                 fn ->\n                   live(conn, \"/lifecycle/redirect-cont-mount\")\n                 end\n  end\n\n  test \"on_mount hook halts with redirected socket\", %{conn: conn} do\n    assert {:error, {:live_redirect, %{to: \"/lifecycle\"}}} =\n             live(conn, \"/lifecycle/redirect-halt-mount\")\n  end\n\n  test \"on_mount hook can set options in return value\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/lifecycle/on-mount-options\")\n\n    assert lv |> element(\"#on-mount\") |> render() =~ \"data-Phoenix\"\n  end\n\n  test \"handle_event/3 raises when hook result is invalid\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/lifecycle\")\n\n    HooksLive.attach_hook(lv, :boom, :handle_event, fn _, _, _ -> :boom end)\n\n    assert HooksLive.exits_with(lv, ArgumentError, fn ->\n             lv |> element(\"#inc\") |> render_click()\n           end) =~ \"Got: :boom\"\n  end\n\n  test \"handle_event/3 halt and continue\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/lifecycle\")\n\n    assert lv |> element(\"#inc\") |> render_click() =~ \"count:1\"\n    assert lv |> element(\"#inc\") |> render_click() =~ \"count:2\"\n\n    HooksLive.attach_hook(lv, :multiply_inc, :handle_event, fn\n      \"inc\", _, socket ->\n        {:halt, Component.update(socket, :count, &(&1 * 2))}\n\n      \"dec\", _, socket ->\n        {:cont, socket}\n    end)\n\n    assert lv |> element(\"#inc\") |> render_click() =~ \"count:4\"\n    assert lv |> element(\"#inc\") |> render_click() =~ \"count:8\"\n\n    assert lv |> element(\"#dec\") |> render_click() =~ \"count:7\"\n    assert lv |> element(\"#dec\") |> render_click() =~ \"count:6\"\n\n    HooksLive.detach_hook(lv, :multiply_inc, :handle_event)\n\n    assert lv |> element(\"#inc\") |> render_click() =~ \"count:7\"\n    assert lv |> element(\"#inc\") |> render_click() =~ \"count:8\"\n  end\n\n  test \"handle_event/3 halts and replies\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/lifecycle\")\n\n    HooksLive.attach_hook(lv, :greet_1, :handle_event, fn \"greet\", %{\"name\" => name}, socket ->\n      {:halt, %{msg: \"Hello, #{name}!\"}, socket}\n    end)\n\n    HooksLive.attach_hook(lv, :greet_2, :handle_event, fn \"greet\", %{\"name\" => name}, socket ->\n      {:halt, %{msg: \"Hi, #{name}!\"}, socket}\n    end)\n\n    render_hook(lv, :greet, %{name: \"Mike\"})\n\n    assert_reply(lv, %{msg: \"Hello, Mike!\"})\n  end\n\n  test \"only handle_event/3 error prints {:halt, map, %Socket{}}\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/lifecycle\")\n\n    HooksLive.attach_hook(lv, :boom, :handle_event, fn _, _, _ -> :boom end)\n\n    result =\n      HooksLive.exits_with(lv, ArgumentError, fn ->\n        lv |> element(\"#inc\") |> render_click()\n      end)\n\n    assert result =~ \"{:halt, map, %Socket{}}\"\n    assert result =~ \"Got: :boom\"\n\n    {:ok, lv, _html} = live(conn, \"/lifecycle\")\n\n    HooksLive.attach_hook(lv, :reply, :handle_info, fn :boom, socket ->\n      {:halt, %{}, socket}\n    end)\n\n    ref = HooksLive.unlink_and_monitor(lv)\n\n    assert ExUnit.CaptureLog.capture_log(fn ->\n             send(lv.pid, :boom)\n             assert_receive {:DOWN, ^ref, _, _, _}\n           end) =~ \"Got: {:halt, %{}, #Phoenix.LiveView.Socket<\"\n  end\n\n  test \"handle_params/3 raises when hook result is invalid\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/lifecycle\")\n\n    HooksLive.attach_hook(lv, :boom, :handle_params, fn _, _, _ -> :boom end)\n\n    assert HooksLive.exits_with(lv, ArgumentError, fn ->\n             lv |> element(\"#patch\") |> render_click()\n           end) =~ \"Got: :boom\"\n  end\n\n  test \"handle_params/3 attached after connected\", %{conn: conn} do\n    {:ok, lv, html} = live(conn, \"/lifecycle\")\n    assert html =~ \"params_hook:</p>\"\n\n    HooksLive.attach_hook(lv, :hook, :handle_params, fn\n      _params, _uri, %{assigns: %{params_hook_ref: _}} = socket ->\n        {:halt, Component.update(socket, :params_hook_ref, &(&1 + 1))}\n\n      _params, _uri, socket ->\n        {:halt, Component.assign(socket, :params_hook_ref, 0)}\n    end)\n\n    lv |> element(\"#patch\") |> render_click() =~ \"params_hook:0\"\n    lv |> element(\"#patch\") |> render_click() =~ \"params_hook:1\"\n\n    HooksLive.detach_hook(lv, :hook, :handle_params)\n\n    assert render(lv) =~ \"params_hook:1\"\n  end\n\n  test \"handle_params/3 without module callback\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/lifecycle/handle-params-not-defined\")\n    assert render(lv) =~ \"url=http://www.example.com/lifecycle/handle-params-not-defined\"\n  end\n\n  test \"handle_params/3 when callback is not exported raises without halt\", %{conn: conn} do\n    {:ok, lv, html} = live(conn, \"/lifecycle\")\n    assert html =~ \"params_hook:</p>\"\n\n    HooksLive.attach_hook(lv, :hook, :handle_params, fn\n      _params, _uri, %{assigns: %{params_hook_ref: 0}} = socket ->\n        {:halt, Component.update(socket, :params_hook_ref, &(&1 + 1))}\n\n      _params, _uri, %{assigns: %{params_hook_ref: 1}} = socket ->\n        {:cont, socket}\n\n      _params, _uri, socket ->\n        {:halt, Component.assign(socket, :params_hook_ref, 0)}\n    end)\n\n    lv |> element(\"#patch\") |> render_click() =~ \"params_hook:0\"\n    lv |> element(\"#patch\") |> render_click() =~ \"params_hook:1\"\n\n    HooksLive.detach_hook(lv, :hook, :handle_params)\n\n    Process.flag(:trap_exit, true)\n\n    assert ExUnit.CaptureLog.capture_log(fn ->\n             try do\n               lv |> element(\"#patch\") |> render_click()\n             catch\n               :exit, _ -> :ok\n             end\n           end) =~\n             \"** (UndefinedFunctionError) function Phoenix.LiveViewTest.Support.HooksLive.handle_params/3 is undefined\"\n  end\n\n  test \"handle_info/2 raises when hook result is invalid\", %{conn: conn} do\n    Process.flag(:trap_exit, true)\n\n    {:ok, lv, _html} = live(conn, \"/lifecycle\")\n    HooksLive.attach_hook(lv, :boom, :handle_info, fn _, _ -> :boom end)\n\n    assert ExUnit.CaptureLog.capture_log(fn ->\n             send(lv.pid, :noop)\n             ref = Process.monitor(lv.pid)\n             assert_receive {:DOWN, ^ref, _, _, _}\n           end) =~\n             \"** (ArgumentError) invalid return from hook :boom for lifecycle event :handle_info.\"\n  end\n\n  test \"handle_info/2 attached and detached\", %{conn: conn} do\n    assert {:ok, lv, _html} = live(conn, \"/lifecycle\")\n\n    ref = make_ref()\n    send(lv.pid, {:ping, ref, self()})\n\n    assert_receive {:pong, ^ref}\n\n    HooksLive.attach_hook(lv, :hitm, :handle_info, fn {:ping, ref, pid}, socket ->\n      send(pid, {:intercepted, ref})\n      {:halt, socket}\n    end)\n\n    ref = make_ref()\n    send(lv.pid, {:ping, ref, self()})\n\n    assert_receive {:intercepted, ^ref}\n    refute_received {:pong, ^ref}\n\n    HooksLive.detach_hook(lv, :hitm, :handle_info)\n\n    ref = make_ref()\n    send(lv.pid, {:ping, ref, self()})\n\n    assert_receive {:pong, ^ref}\n    refute_received {:intercepted, ^ref}\n  end\n\n  test \"handle_info/2 without module callback\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/lifecycle/handle-info-not-defined\")\n    assert render(lv) =~ \"data=somedata\"\n  end\n\n  test \"handle_async/3 raises when hook result is invalid\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/lifecycle\")\n\n    HooksLive.attach_hook(lv, :boom, :handle_async, fn _, _, _ -> :boom end)\n\n    monitor = HooksLive.unlink_and_monitor(lv)\n    lv |> element(\"#async\") |> render_click()\n    assert_receive {:DOWN, ^monitor, :process, _pid, {%error{message: msg}, _}}\n    assert error == ArgumentError\n    assert msg =~ \"Got: :boom\"\n  end\n\n  test \"handle_async/3 attached after connected\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/lifecycle\")\n\n    HooksLive.attach_hook(lv, :hook, :handle_async, fn _, _, socket ->\n      {:cont, Component.update(socket, :task, &(&1 <> \"o\"))}\n    end)\n\n    lv |> element(\"#async\") |> render_click()\n    assert render_async(lv) =~ \"task:o.</p>\"\n\n    HooksLive.detach_hook(lv, :hook, :handle_async)\n\n    lv |> element(\"#async\") |> render_click()\n    assert render_async(lv) =~ \"task:o..</p>\"\n  end\n\n  test \"handle_async/3 halts\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/lifecycle\")\n\n    HooksLive.attach_hook(lv, :hook, :handle_async, fn _, _, socket ->\n      {:halt, Component.update(socket, :task, &(&1 <> \"o\"))}\n    end)\n\n    lv |> element(\"#async\") |> render_click()\n    assert render_async(lv) =~ \"task:o</p>\"\n\n    HooksLive.detach_hook(lv, :hook, :handle_async)\n\n    lv |> element(\"#async\") |> render_click()\n    assert render_async(lv) =~ \"task:o.</p>\"\n  end\n\n  test \"attach/detach_hook with a handle_event live component socket\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/lifecycle/components/handle_event\")\n    lv |> element(\"#attach\") |> render_click()\n    lv |> element(\"#hook\") |> render_click()\n    assert render_async(lv) =~ \"counter: 1\"\n\n    lv |> element(\"#hook\") |> render_click()\n    assert render_async(lv) =~ \"counter: 2\"\n\n    lv |> element(\"#detach-component-hook\") |> render_click()\n    Process.flag(:trap_exit, true)\n\n    assert ExUnit.CaptureLog.capture_log(fn ->\n             try do\n               lv |> element(\"#hook\") |> render_click()\n             catch\n               :exit, _ -> :ok\n             end\n           end) =~\n             \"** (UndefinedFunctionError) function Phoenix.LiveViewTest.Support.HooksEventComponent.handle_event/3 is undefined\"\n  end\n\n  test \"attach_hook with reply and detach_hook with a handle_event live component socket\", %{\n    conn: conn\n  } do\n    {:ok, lv, _html} = live(conn, \"/lifecycle/components/handle_event?reply=true\")\n    lv |> element(\"#attach\") |> render_click()\n    lv |> element(\"#hook\") |> render_click()\n    assert_reply(lv, %{counter: 1})\n    assert render_async(lv) =~ \"counter: 1\"\n\n    lv |> element(\"#hook\") |> render_click()\n    assert_reply(lv, %{counter: 2})\n    assert render_async(lv) =~ \"counter: 2\"\n\n    lv |> element(\"#detach-component-hook\") |> render_click()\n    Process.flag(:trap_exit, true)\n\n    assert ExUnit.CaptureLog.capture_log(fn ->\n             try do\n               lv |> element(\"#hook\") |> render_click()\n             catch\n               :exit, _ -> :ok\n             end\n           end) =~\n             \"** (UndefinedFunctionError) function Phoenix.LiveViewTest.Support.HooksEventComponent.handle_event/3 is undefined\"\n  end\n\n  test \"attach_hook raises when given a live component socket\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/lifecycle/components/handle_info\")\n\n    assert HooksLive.exits_with(lv, ArgumentError, fn ->\n             lv |> element(\"#attach\") |> render_click()\n           end) =~ \"lifecycle hooks are not supported on stateful components.\"\n  end\n\n  test \"detach_hook raises when given a live component socket\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/lifecycle/components/handle_info\")\n\n    assert HooksLive.exits_with(lv, ArgumentError, fn ->\n             lv |> element(\"#detach\") |> render_click()\n           end) =~ \"lifecycle hooks are not supported on stateful components.\"\n  end\n\n  test \"attach/detach_hook with a handle_async live component socket\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/lifecycle/components/handle_async\")\n    lv |> element(\"#attach\") |> render_click()\n    lv |> element(\"#async\") |> render_click()\n    assert render_async(lv) =~ \"task: o\"\n\n    lv |> element(\"#async\") |> render_click()\n    assert render_async(lv) =~ \"task: oo\"\n\n    lv |> element(\"#detach-component-hook\") |> render_click()\n\n    lv |> element(\"#async\") |> render_click()\n    assert render_async(lv) =~ \"task: oo.\"\n  end\n\n  test \"stage_info\", %{conn: conn} do\n    alias Phoenix.LiveView.Lifecycle\n    {:ok, lv, _html} = live(conn, \"/lifecycle\")\n\n    socket = HooksLive.run(lv, fn socket -> {:reply, socket, socket} end)\n\n    assert Lifecycle.stage_info(socket, HooksLive, :mount, 3) == %{\n             any?: true,\n             callbacks?: true,\n             exported?: true\n           }\n\n    assert Lifecycle.stage_info(socket, HooksLive, :handle_async, 3) == %{\n             any?: true,\n             callbacks?: false,\n             exported?: true\n           }\n\n    assert Lifecycle.stage_info(socket, HooksLive, :handle_params, 3) == %{\n             any?: false,\n             callbacks?: false,\n             exported?: false\n           }\n\n    assert Lifecycle.stage_info(socket, HooksLive, :handle_event, 3) == %{\n             any?: true,\n             callbacks?: false,\n             exported?: true\n           }\n\n    assert Lifecycle.stage_info(socket, HooksLive, :handle_info, 2) == %{\n             any?: true,\n             callbacks?: false,\n             exported?: true\n           }\n\n    HooksLive.attach_hook(lv, :ok, :handle_params, fn _, _, socket ->\n      {:cont, socket}\n    end)\n\n    socket = HooksLive.run(lv, fn socket -> {:reply, socket, socket} end)\n\n    assert Lifecycle.stage_info(socket, HooksLive, :handle_params, 3) == %{\n             any?: true,\n             callbacks?: true,\n             exported?: false\n           }\n  end\n\n  test \"after_render hook\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/lifecycle\")\n\n    assert render(lv) =~ \"count:0\"\n\n    HooksLive.attach_hook(lv, :after, :after_render, fn socket ->\n      if Phoenix.Component.changed?(socket, :count) && socket.assigns.count >= 1 do\n        Phoenix.Component.assign(socket, :count, socket.assigns.count * 10)\n      else\n        socket\n      end\n    end)\n\n    assert lv |> element(\"#inc\") |> render_click() =~ \"count:1\"\n\n    socket = HooksLive.run(lv, fn socket -> {:reply, socket, socket} end)\n    assert socket.assigns.count == 10\n\n    assert lv |> element(\"#inc\") |> render_click() =~ \"count:1\"\n    socket = HooksLive.run(lv, fn socket -> {:reply, socket, socket} end)\n    assert socket.assigns.count == 110\n\n    HooksLive.detach_hook(lv, :after, :after_render)\n\n    assert lv |> element(\"#inc\") |> render_click() =~ \"count:111\"\n    assert lv |> element(\"#inc\") |> render_click() =~ \"count:112\"\n    socket = HooksLive.run(lv, fn socket -> {:reply, socket, socket} end)\n    assert socket.assigns.count == 112\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/html_formatter_test.exs",
    "content": "defmodule Phoenix.LiveView.Integrations.HTMLFormatterTest do\n  use ExUnit.Case\n\n  alias Phoenix.LiveView.HTMLFormatter\n\n  defp assert_mix_format_output(input_ex, expected, dot_formatter_opts \\\\ []) do\n    filename = \"index.html.heex\"\n    ex_path = Path.join(System.tmp_dir(), filename)\n    dot_formatter_path = ex_path <> \".formatter.exs\"\n    dot_formatter_opts = Keyword.put(dot_formatter_opts, :plugins, [HTMLFormatter])\n\n    on_exit(fn ->\n      File.rm(ex_path)\n      File.rm(dot_formatter_path)\n    end)\n\n    File.write!(ex_path, input_ex)\n    File.write!(dot_formatter_path, inspect(dot_formatter_opts))\n\n    # Run mix format twice to make sure the formatted file doesn't change after\n    # another mix format.\n    formatted = run_formatter(ex_path, dot_formatter_path)\n    assert formatted == expected\n    assert run_formatter(ex_path, dot_formatter_path) == formatted\n  end\n\n  defp run_formatter(ex_path, dot_formatter_path) do\n    Mix.Tasks.Format.run([ex_path, \"--dot-formatter\", dot_formatter_path])\n    File.read!(ex_path)\n  end\n\n  test \"formats with default options\" do\n    input = \"\"\"\n      <section>\n        <%= live_redirect to: \"url\", id: \"link\", role: \"button\" do %>\n          <div>     <p>content 1</p><p>content 2</p></div>\n        <% end %>\n        <p><%= @user.name %></p>\n        <%= if true do %> <p>good</p><% else %><p>bad</p><% end %>\n      </section>\n\n      <section>\n      <%= for value <- @values do %>\n        <td class=\"border-2\">\n          <%= case value.type do %>\n          <% :text -> %>\n          <p>Hello</p>\n          <% _ -> %>\n          <p>Hello</p>\n          <% end %>\n        </td>\n      <% end %>\n      </section>\n\n      <!-- comment -->\n      <div><p>Hello</p></div>\n\n      <pre>\n               Leave me alone</pre>\n\n      <script>\n      const foo = 1;\n      console.log(foo);\n      </script>\n      <!-- html block comment\n          <div>leave me alone</div>\n      -->\n    \"\"\"\n\n    expected = \"\"\"\n    <section>\n      <%= live_redirect to: \"url\", id: \"link\", role: \"button\" do %>\n        <div>\n          <p>content 1</p><p>content 2</p>\n        </div>\n      <% end %>\n      <p>{@user.name}</p>\n      <%= if true do %>\n        <p>good</p>\n      <% else %>\n        <p>bad</p>\n      <% end %>\n    </section>\n\n    <section>\n      <%= for value <- @values do %>\n        <td class=\"border-2\">\n          <%= case value.type do %>\n            <% :text -> %>\n              <p>Hello</p>\n            <% _ -> %>\n              <p>Hello</p>\n          <% end %>\n        </td>\n      <% end %>\n    </section>\n\n    <!-- comment -->\n    <div>\n      <p>Hello</p>\n    </div>\n\n    <pre>\n               Leave me alone</pre>\n\n    <script>\n      const foo = 1;\n      console.log(foo);\n    </script>\n    <!-- html block comment\n          <div>leave me alone</div>\n      -->\n    \"\"\"\n\n    assert_mix_format_output(input, expected)\n  end\n\n  test \"accept line_length as option\" do\n    input = \"\"\"\n      <section><h1><b class=\"there are several classes\">{@user.name}</b></h1></section>\n    \"\"\"\n\n    expected = \"\"\"\n    <section>\n      <h1>\n        <b class=\"there are several classes\">{@user.name}</b>\n      </h1>\n    </section>\n    \"\"\"\n\n    assert_mix_format_output(input, expected, line_length: 20)\n  end\n\n  test \"heex_line_length overrides line_length\" do\n    input = \"\"\"\n      <section><h1><b class=\"there are several classes\">{@user.name}</b></h1></section>\n    \"\"\"\n\n    expected = \"\"\"\n    <section>\n      <h1><b class=\"there are several classes\">{@user.name}</b></h1>\n    </section>\n    \"\"\"\n\n    assert_mix_format_output(input, expected, line_length: 20, heex_line_length: 80)\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/layout_test.exs",
    "content": "defmodule Phoenix.LiveView.LayoutTest do\n  use ExUnit.Case, async: true\n  import Phoenix.ConnTest\n\n  import Phoenix.LiveViewTest\n  alias Phoenix.LiveViewTest.Support.{Endpoint, LayoutView}\n\n  @endpoint Endpoint\n\n  setup config do\n    {:ok,\n     conn: Plug.Test.init_test_session(Phoenix.ConnTest.build_conn(), config[:session] || %{})}\n  end\n\n  test \"uses dead layout from router\", %{conn: conn} do\n    assert_raise ArgumentError,\n                 ~r\"no \\\"unknown_template\\\" html template defined for UnknownView\",\n                 fn -> live(conn, \"/bad_layout\") end\n\n    {:ok, _, _} = live(conn, \"/layout\")\n  end\n\n  test \"is picked from config on use\", %{conn: conn} do\n    {:ok, view, html} = live(conn, \"/layout\")\n    assert html =~ ~r|LAYOUT<div[^>]+>LIVELAYOUTSTART\\-123\\-The value is: 123\\-LIVELAYOUTEND|\n\n    assert render_click(view, :double) ==\n             \"LIVELAYOUTSTART-246-The value is: 246-LIVELAYOUTEND\\n\"\n  end\n\n  test \"is picked from config on use on first render\", %{conn: conn} do\n    conn = get(conn, \"/layout\")\n\n    assert html_response(conn, 200) =~\n             ~r|LAYOUT<div[^>]+>LIVELAYOUTSTART\\-123\\-The value is: 123\\-LIVELAYOUTEND|\n  end\n\n  @tag session: %{live_layout: {LayoutView, :live_override}}\n  test \"is picked from config on mount when given a layout\", %{conn: conn} do\n    {:ok, view, html} = live(conn, \"/layout\")\n\n    assert html =~\n             ~r|LAYOUT<div[^>]+>LIVEOVERRIDESTART\\-123\\-The value is: 123\\-LIVEOVERRIDEEND|\n\n    assert render_click(view, :double) ==\n             \"LIVEOVERRIDESTART-246-The value is: 246-LIVEOVERRIDEEND\\n\"\n  end\n\n  @tag session: %{live_layout: false}\n  test \"is picked from config on mount when given false\", %{conn: conn} do\n    {:ok, view, html} = live(conn, \"/layout\")\n    assert html =~ \"The value is: 123</div>\"\n    assert render_click(view, :double) == \"The value is: 246\"\n  end\n\n  test \"is not picked from config on use for child live views\", %{conn: conn} do\n    assert get(conn, \"/parent_layout\") |> html_response(200) =~\n             \"The value is: 123</div>\"\n\n    {:ok, _view, html} = live(conn, \"/parent_layout\")\n    assert html =~ \"The value is: 123</div>\"\n  end\n\n  @tag session: %{live_layout: {LayoutView, :live_override}}\n  test \"is picked from config on mount even on child live views\", %{conn: conn} do\n    assert get(conn, \"/parent_layout\") |> html_response(200) =~\n             ~r|<div[^>]+>LIVEOVERRIDESTART\\-123\\-The value is: 123\\-LIVEOVERRIDEEND|\n\n    {:ok, _view, html} = live(conn, \"/parent_layout\")\n\n    assert html =~\n             ~r|<div[^>]+>LIVEOVERRIDESTART\\-123\\-The value is: 123\\-LIVEOVERRIDEEND|\n  end\n\n  test \"uses root page title on first render\", %{conn: conn} do\n    {:ok, view, _html} = live(conn, \"/styled-elements\")\n    assert page_title(view) == \"Styled\"\n\n    {:ok, view, _html} = live(conn, \"/styled-elements\")\n    render_click(view, \"#live-push-patch-button\")\n    assert page_title(view) == \"Styled\"\n\n    {:ok, no_title_tag_view, _html} = live(conn, \"/parent_layout\")\n    assert page_title(no_title_tag_view) == nil\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/live_components_test.exs",
    "content": "defmodule Phoenix.LiveView.LiveComponentsTest do\n  use ExUnit.Case, async: true\n  import Phoenix.ConnTest\n\n  import Phoenix.LiveViewTest\n\n  alias Phoenix.LiveViewTest.{DOM, TreeDOM}\n  alias Phoenix.LiveViewTest.Support.{Endpoint, StatefulComponent}\n\n  @endpoint Endpoint\n  @moduletag session: %{names: [\"chris\", \"jose\"], from: nil}\n\n  setup config do\n    {:ok,\n     conn: Plug.Test.init_test_session(Phoenix.ConnTest.build_conn(), config[:session] || %{})}\n  end\n\n  test \"@myself\" do\n    cid = %Phoenix.LiveComponent.CID{cid: 123}\n    assert String.Chars.to_string(cid) == \"123\"\n    assert Phoenix.HTML.Safe.to_iodata(cid) == \"123\"\n  end\n\n  test \"renders successfully when disconnected\", %{conn: conn} do\n    conn = get(conn, \"/components\")\n\n    assert html_response(conn, 200) =~\n             \"<div phx-click=\\\"transform\\\" id=\\\"chris\\\" phx-target=\\\"#chris\\\">\"\n  end\n\n  test \"renders successfully when connected\", %{conn: conn} do\n    {:ok, view, _html} = live(conn, \"/components\")\n\n    assert [\n             {\"div\", _,\n              [\n                _,\n                {\"div\",\n                 [{\"data-phx-component\", \"1\"}, {\"id\", \"chris\"}, {\"phx-click\", \"transform\"} | _],\n                 [\"\\n  chris says hi\\n  \\n\"]},\n                {\"div\",\n                 [{\"data-phx-component\", \"2\"}, {\"id\", \"jose\"}, {\"phx-click\", \"transform\"} | _],\n                 [\"\\n  jose says hi\\n  \\n\"]}\n              ]}\n           ] = TreeDOM.normalize_to_tree(render(view), sort_attributes: true)\n  end\n\n  test \"tracks additions and updates\", %{conn: conn} do\n    {:ok, view, _} = live(conn, \"/components\")\n    html = render_click(view, \"dup-and-disable\", %{})\n\n    assert [\n             \"Redirect: none\\n\\n  \",\n             {\"div\", [{\"data-phx-component\", \"1\"}], [\"\\n  DISABLED\\n\"]},\n             {\"div\", [{\"data-phx-component\", \"2\"}], [\"\\n  DISABLED\\n\"]},\n             {\"div\",\n              [\n                {\"data-phx-component\", \"3\"},\n                {\"id\", \"chris-new\"},\n                {\"phx-click\", \"transform\"},\n                {\"phx-target\", \"#chris-new\"}\n              ], [\"\\n  chris-new says hi\\n  \\n\"]},\n             {\"div\",\n              [\n                {\"data-phx-component\", \"4\"},\n                {\"id\", \"jose-new\"},\n                {\"phx-click\", \"transform\"},\n                {\"phx-target\", \"#jose-new\"}\n              ], [\"\\n  jose-new says hi\\n  \\n\"]}\n           ] = TreeDOM.normalize_to_tree(html, sort_attributes: true)\n  end\n\n  test \"tracks removals\", %{conn: conn} do\n    ref =\n      :telemetry_test.attach_event_handlers(self(), [[:phoenix, :live_component, :destroyed]])\n\n    {:ok, view, html} = live(conn, \"/components\")\n\n    assert [\n             {\"div\",\n              [{\"data-phx-component\", \"1\"}, {\"id\", \"chris\"}, {\"phx-click\", \"transform\"} | _],\n              [\"\\n  chris says\" <> _]},\n             {\"div\",\n              [{\"data-phx-component\", \"2\"}, {\"id\", \"jose\"}, {\"phx-click\", \"transform\"} | _],\n              [\"\\n  jose says\" <> _]}\n           ] =\n             html\n             |> DOM.parse_fragment()\n             |> elem(0)\n             |> DOM.all(\"#chris, #jose\")\n             |> TreeDOM.normalize_to_tree(sort_attributes: true)\n\n    html = render_click(view, \"delete-name\", %{\"name\" => \"chris\"})\n\n    assert [\n             {\"div\",\n              [{\"data-phx-component\", \"2\"}, {\"id\", \"jose\"}, {\"phx-click\", \"transform\"} | _],\n              [\"\\n  jose says\" <> _]}\n           ] =\n             html\n             |> DOM.parse_fragment()\n             |> elem(0)\n             |> DOM.all(\"#chris, #jose\")\n             |> TreeDOM.normalize_to_tree(sort_attributes: true)\n\n    refute view |> element(\"#chris\") |> has_element?()\n\n    assert_received {[:phoenix, :live_component, :destroyed], ^ref, _,\n                     %{\n                       component: StatefulComponent,\n                       cid: 1,\n                       socket: %{assigns: %{name: \"chris\"}},\n                       live_view_socket: %{assigns: %{names: [\"jose\"]}}\n                     }}\n  end\n\n  test \"tracks removals when whole root changes\", %{conn: conn} do\n    {:ok, view, _html} = live(conn, \"/components\")\n    assert render_click(view, \"disable-all\", %{}) =~ \"Disabled\\n\"\n    # Sync to make sure it is still alive\n    assert render(view) =~ \"Disabled\\n\"\n  end\n\n  test \"tracks removals from a nested LiveView\", %{conn: conn} do\n    {:ok, view, _} = live(conn, \"/component_in_live\")\n    assert render(view) =~ \"Hello World\"\n    view |> find_live_child(\"nested_live\") |> render_click(\"disable\", %{})\n    refute render(view) =~ \"Hello World\"\n  end\n\n  test \"tracks removals of a nested LiveView alongside with a LiveComponent in the root view\", %{\n    conn: conn\n  } do\n    {:ok, view, _} = live(conn, \"/component_and_nested_in_live\")\n    html = render(view)\n    assert html =~ \"hello\"\n    assert html =~ \"world\"\n    render_click(view, \"disable\", %{})\n\n    html = render(view)\n    refute html =~ \"hello\"\n    refute html =~ \"world\"\n  end\n\n  test \"tracks removals when there is a race between server and client\", %{conn: conn} do\n    {:ok, view, _} = live(conn, \"/cids_destroyed\")\n\n    # The button is on the page\n    assert render(view) =~ \"Hello World</button>\"\n\n    # Make sure we can bump the component\n    assert view |> element(\"#bumper\") |> render_click() =~ \"Bump: 1\"\n\n    # Now click the form\n    assert view |> element(\"form\") |> render_submit() =~ \"loading...\"\n\n    # Which will be reset almost immediately\n    assert render(view) =~ \"Hello World</button>\"\n\n    # But the client did not have time to remove it so the bumper still keeps going\n    assert view |> element(\"#bumper\") |> render_click() =~ \"Bump: 2\"\n  end\n\n  describe \"handle_event\" do\n    test \"delegates event to component\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/components\")\n\n      html = view |> element(\"#chris\") |> render_click(%{\"op\" => \"upcase\"})\n\n      assert [\n               _,\n               {\"div\",\n                [{\"data-phx-component\", \"1\"}, {\"id\", \"chris\"}, {\"phx-click\", \"transform\"} | _],\n                [\"\\n  CHRIS says hi\\n\" <> _]},\n               {\"div\",\n                [{\"data-phx-component\", \"2\"}, {\"id\", \"jose\"}, {\"phx-click\", \"transform\"} | _],\n                [\"\\n  jose says hi\\n\" <> _]}\n             ] = TreeDOM.normalize_to_tree(html, sort_attributes: true)\n\n      html = view |> with_target(\"#jose\") |> render_click(\"transform\", %{\"op\" => \"title-case\"})\n\n      assert [\n               _,\n               {\"div\",\n                [{\"data-phx-component\", \"1\"}, {\"id\", \"chris\"}, {\"phx-click\", \"transform\"} | _],\n                [\"\\n  CHRIS says hi\\n\" <> _]},\n               {\"div\",\n                [{\"data-phx-component\", \"2\"}, {\"id\", \"jose\"}, {\"phx-click\", \"transform\"} | _],\n                [\"\\n  Jose says hi\\n\" <> _]}\n             ] = TreeDOM.normalize_to_tree(html, sort_attributes: true)\n\n      html = view |> element(\"#jose\") |> render_click(%{\"op\" => \"dup\"})\n\n      assert [\n               _,\n               {\"div\",\n                [{\"data-phx-component\", \"1\"}, {\"id\", \"chris\"}, {\"phx-click\", \"transform\"} | _],\n                [\"\\n  CHRIS says hi\\n\" <> _]},\n               {\"div\",\n                [{\"data-phx-component\", \"2\"}, {\"id\", \"jose\"}, {\"phx-click\", \"transform\"} | _],\n                [\n                  \"\\n  Jose says hi\\n  \",\n                  {\"div\",\n                   [\n                     {\"data-phx-component\", \"3\"},\n                     {\"id\", \"Jose-dup\"},\n                     {\"phx-click\", \"transform\"} | _\n                   ], [\"\\n  Jose-dup says hi\\n\" <> _]}\n                ]}\n             ] = TreeDOM.normalize_to_tree(html, sort_attributes: true)\n\n      html = view |> element(\"#jose #Jose-dup\") |> render_click(%{\"op\" => \"upcase\"})\n\n      assert [\n               _,\n               {\"div\",\n                [{\"data-phx-component\", \"1\"}, {\"id\", \"chris\"}, {\"phx-click\", \"transform\"} | _],\n                [\"\\n  CHRIS says hi\\n\" <> _]},\n               {\"div\",\n                [{\"data-phx-component\", \"2\"}, {\"id\", \"jose\"}, {\"phx-click\", \"transform\"} | _],\n                [\n                  \"\\n  Jose says hi\\n  \",\n                  {\"div\",\n                   [\n                     {\"data-phx-component\", \"3\"},\n                     {\"id\", \"Jose-dup\"},\n                     {\"phx-click\", \"transform\"} | _\n                   ], [\"\\n  JOSE-DUP says hi\\n\" <> _]}\n                ]}\n             ] = TreeDOM.normalize_to_tree(html, sort_attributes: true)\n\n      assert view |> element(\"#jose #Jose-dup\") |> render() ==\n               \"<div data-phx-component=\\\"3\\\" phx-click=\\\"transform\\\" id=\\\"Jose-dup\\\" phx-target=\\\"#Jose-dup\\\">\\n  JOSE-DUP says hi\\n  \\n</div>\"\n    end\n\n    test \"works with_target to component\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/components\")\n\n      html = view |> with_target(\"#chris\") |> render_click(\"transform\", %{\"op\" => \"upcase\"})\n\n      assert [\n               _,\n               {\"div\",\n                [{\"data-phx-component\", \"1\"}, {\"id\", \"chris\"}, {\"phx-click\", \"transform\"} | _],\n                [\"\\n  CHRIS says hi\\n\" <> _]},\n               {\"div\",\n                [{\"data-phx-component\", \"2\"}, {\"id\", \"jose\"}, {\"phx-click\", \"transform\"} | _],\n                [\"\\n  jose says hi\\n\" <> _]}\n             ] = TreeDOM.normalize_to_tree(html, sort_attributes: true)\n    end\n\n    test \"works with multiple phx-targets\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/multi-targets\")\n\n      view |> element(\"#chris\") |> render_click(%{\"op\" => \"upcase\"})\n\n      html = render(view)\n\n      assert [\n               {\"div\", _,\n                [\n                  {\"div\", [{\"class\", \"parent\"}, {\"id\", \"parent_id\"}],\n                   [\n                     \"\\n  Parent was updated\\n\" <> _,\n                     {\"div\",\n                      [\n                        {\"data-phx-component\", \"1\"},\n                        {\"id\", \"chris\"},\n                        {\"phx-click\", \"transform\"} | _\n                      ], [\"\\n  CHRIS says hi\\n\" <> _]},\n                     {\"div\",\n                      [\n                        {\"data-phx-component\", \"2\"},\n                        {\"id\", \"jose\"},\n                        {\"phx-click\", \"transform\"} | _\n                      ], [\"\\n  jose says hi\\n\" <> _]}\n                   ]}\n                ]}\n             ] = TreeDOM.normalize_to_tree(html, sort_attributes: true)\n    end\n\n    test \"phx-target works with non id selector\", %{conn: conn} do\n      {:ok, view, _html} =\n        conn\n        |> Plug.Conn.put_session(:parent_selector, \".parent\")\n        |> live(\"/multi-targets\")\n\n      view |> element(\"#chris\") |> render_click(%{\"op\" => \"upcase\"})\n\n      html = render(view)\n\n      assert [\n               {\"div\", _,\n                [\n                  {\"div\", [{\"class\", \"parent\"}, {\"id\", \"parent_id\"}],\n                   [\n                     \"\\n  Parent was updated\\n\" <> _,\n                     {\"div\",\n                      [\n                        {\"data-phx-component\", \"1\"},\n                        {\"id\", \"chris\"},\n                        {\"phx-click\", \"transform\"} | _\n                      ], [\"\\n  CHRIS says hi\\n\" <> _]},\n                     {\"div\",\n                      [\n                        {\"data-phx-component\", \"2\"},\n                        {\"id\", \"jose\"},\n                        {\"phx-click\", \"transform\"} | _\n                      ], [\"\\n  jose says hi\\n\" <> _]}\n                   ]}\n                ]}\n             ] = TreeDOM.normalize_to_tree(html, sort_attributes: true)\n    end\n  end\n\n  describe \"send_update\" do\n    test \"updates child from parent\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/components\")\n\n      send(\n        view.pid,\n        {:send_update,\n         [\n           {StatefulComponent, id: \"chris\", name: \"NEW-chris\", from: self()},\n           {StatefulComponent, id: \"jose\", name: \"NEW-jose\", from: self()}\n         ]}\n      )\n\n      assert_receive {:updated, %{id: \"chris\", name: \"NEW-chris\"}}\n      assert_receive {:updated, %{id: \"jose\", name: \"NEW-jose\"}}\n      refute_receive {:updated, _}\n\n      assert [\n               {\"div\",\n                [{\"data-phx-component\", \"1\"}, {\"id\", \"chris\"}, {\"phx-click\", \"transform\"} | _],\n                [\"\\n  NEW-chris says hi\\n  \\n\"]}\n             ] =\n               view\n               |> element(\"#chris\")\n               |> render()\n               |> TreeDOM.normalize_to_tree(sort_attributes: true)\n\n      assert [\n               {\"div\",\n                [{\"data-phx-component\", \"2\"}, {\"id\", \"jose\"}, {\"phx-click\", \"transform\"} | _],\n                [\"\\n  NEW-jose says hi\\n  \\n\"]}\n             ] =\n               view\n               |> element(\"#jose\")\n               |> render()\n               |> TreeDOM.normalize_to_tree(sort_attributes: true)\n    end\n\n    test \"updates child from independent pid\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/components\")\n\n      Phoenix.LiveView.send_update(view.pid, StatefulComponent,\n        id: \"chris\",\n        name: \"NEW-chris\",\n        from: self()\n      )\n\n      Phoenix.LiveView.send_update_after(\n        view.pid,\n        StatefulComponent,\n        [id: \"jose\", name: \"NEW-jose\", from: self()],\n        10\n      )\n\n      assert_receive {:updated, %{id: \"chris\", name: \"NEW-chris\"}}\n      assert_receive {:updated, %{id: \"jose\", name: \"NEW-jose\"}}\n      refute_receive {:updated, _}\n    end\n\n    test \"updates with cid\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/components\")\n\n      Phoenix.LiveView.send_update_after(\n        view.pid,\n        StatefulComponent,\n        [id: \"jose\", name: \"NEW-jose\", from: self(), all_assigns: true],\n        10\n      )\n\n      assert_receive {:updated, %{id: \"jose\", name: \"NEW-jose\", myself: myself}}\n\n      Phoenix.LiveView.send_update(view.pid, myself, name: \"NEXTGEN-jose\", from: self())\n      assert_receive {:updated, %{id: \"jose\", name: \"NEXTGEN-jose\"}}\n\n      Phoenix.LiveView.send_update_after(\n        view.pid,\n        myself,\n        [name: \"after-NEXTGEN-jose\", from: self()],\n        10\n      )\n\n      assert_receive {:updated, %{id: \"jose\", name: \"after-NEXTGEN-jose\"}}, 500\n    end\n\n    test \"updates without :id raise\", %{conn: conn} do\n      Process.flag(:trap_exit, true)\n      {:ok, view, _html} = live(conn, \"/components\")\n\n      assert ExUnit.CaptureLog.capture_log(fn ->\n               send(view.pid, {:send_update, [{StatefulComponent, name: \"NEW-chris\"}]})\n               ref = Process.monitor(view.pid)\n               assert_receive {:DOWN, ^ref, _, _, _}, 500\n             end) =~ \"** (ArgumentError) missing required :id in send_update\"\n    end\n\n    test \"warns if component doesn't exist\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/components\")\n\n      # with module and id\n      assert ExUnit.CaptureLog.capture_log(fn ->\n               send(view.pid, {:send_update, [{StatefulComponent, id: \"nemo\", name: \"NEW-nemo\"}]})\n               render(view)\n               refute_receive {:updated, _}\n             end) =~\n               \"send_update failed because component Phoenix.LiveViewTest.Support.StatefulComponent with ID \\\"nemo\\\" does not exist or it has been removed\"\n\n      # with @myself\n      assert ExUnit.CaptureLog.capture_log(fn ->\n               send(\n                 view.pid,\n                 {:send_update, [{%Phoenix.LiveComponent.CID{cid: 999}, name: \"NEW-nemo\"}]}\n               )\n\n               render(view)\n               refute_receive {:updated, _}\n             end) =~\n               \"send_update failed because component with CID 999 does not exist or it has been removed\"\n    end\n\n    test \"raises if component module is not available\", %{conn: conn} do\n      Process.flag(:trap_exit, true)\n      {:ok, view, _html} = live(conn, \"/components\")\n\n      assert ExUnit.CaptureLog.capture_log(fn ->\n               send(\n                 view.pid,\n                 {:send_update, [{NonexistentComponent, id: \"chris\", name: \"NEW-chris\"}]}\n               )\n\n               ref = Process.monitor(view.pid)\n               assert_receive {:DOWN, ^ref, _, _, _}, 500\n             end) =~\n               \"** (ArgumentError) send_update failed (module NonexistentComponent is not available)\"\n    end\n  end\n\n  describe \"redirects\" do\n    test \"push_navigate\", %{conn: conn} do\n      {:ok, view, html} = live(conn, \"/components\")\n      assert html =~ \"Redirect: none\"\n\n      assert {:error, {:live_redirect, %{to: \"/components?redirect=push\"}}} =\n               view |> element(\"#chris\") |> render_click(%{\"op\" => \"push_navigate\"})\n\n      assert_redirect(view, \"/components?redirect=push\")\n    end\n\n    test \"push_patch\", %{conn: conn} do\n      {:ok, view, html} = live(conn, \"/components\")\n      assert html =~ \"Redirect: none\"\n\n      assert view |> element(\"#chris\") |> render_click(%{\"op\" => \"push_patch\"}) =~\n               \"Redirect: patch\"\n\n      assert_patch(view, \"/components?redirect=patch\")\n    end\n\n    test \"redirect\", %{conn: conn} do\n      {:ok, view, html} = live(conn, \"/components\")\n      assert html =~ \"Redirect: none\"\n\n      assert view |> element(\"#chris\") |> render_click(%{\"op\" => \"redirect\"}) ==\n               {:error, {:redirect, %{to: \"/components?redirect=redirect\", status: 302}}}\n\n      assert_redirect(view, \"/components?redirect=redirect\")\n    end\n  end\n\n  defmodule MyComponent do\n    use Phoenix.LiveComponent\n\n    # Assert endpoint was set\n    def mount(%{endpoint: Endpoint, router: SomeRouter} = socket) do\n      send(self(), {:mount, socket})\n      {:ok, assign(socket, hello: \"world\")}\n    end\n\n    def update(assigns, socket) do\n      send(self(), {:update, assigns, socket})\n      {:ok, assign(socket, assigns)}\n    end\n\n    def render(assigns) do\n      send(self(), :render)\n\n      ~H\"\"\"\n      <div>\n        FROM {@from} {@hello}\n      </div>\n      \"\"\"\n    end\n  end\n\n  defmodule RenderOnlyComponent do\n    use Phoenix.LiveComponent\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div>\n        RENDER ONLY {@from}\n      </div>\n      \"\"\"\n    end\n  end\n\n  defmodule NestedRenderOnlyComponent do\n    use Phoenix.LiveComponent\n\n    def render(assigns) do\n      ~H\"\"\"\n      <.live_component module={RenderOnlyComponent} from={@from} id=\"render-only-component\" />\n      \"\"\"\n    end\n  end\n\n  defmodule BadRootComponent do\n    use Phoenix.LiveComponent\n\n    def render(assigns) do\n      ~H\"\"\"\n      <foo>{@id}</foo>\n      <bar>{@id}</bar>\n      \"\"\"\n    end\n  end\n\n  describe \"render_component/2\" do\n    test \"life-cycle\" do\n      assert render_component(MyComponent, %{from: \"test\", id: \"stateful\"}, router: SomeRouter) =~\n               \"FROM test world\"\n\n      assert_received {:mount,\n                       %{assigns: %{flash: %{}, myself: %Phoenix.LiveComponent.CID{cid: -1}}}}\n\n      assert_received {:update, %{from: \"test\", id: \"stateful\"},\n                       %{assigns: %{flash: %{}, myself: %Phoenix.LiveComponent.CID{cid: -1}}}}\n    end\n\n    test \"render only\" do\n      assert render_component(RenderOnlyComponent, %{from: \"test\"}) =~ \"RENDER ONLY test\"\n    end\n\n    test \"nested render only\" do\n      assert render_component(NestedRenderOnlyComponent, %{from: \"test\"}) =~ \"RENDER ONLY test\"\n    end\n\n    test \"raises on bad root\" do\n      assert_raise ArgumentError, ~r/have a single static HTML tag at the root/, fn ->\n        render_component(BadRootComponent, %{id: \"id\"})\n      end\n    end\n\n    test \"loads unloaded component\" do\n      module = Phoenix.LiveViewTest.Support.ComponentInLive.Component\n      :code.purge(module)\n      :code.delete(module)\n      assert render_component(module, %{}) =~ \"<div>Hello World</div>\"\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/live_reload_test.exs",
    "content": "defmodule Phoenix.LiveView.LiveReloadTest do\n  use ExUnit.Case, async: true\n\n  defmodule Endpoint do\n    use Phoenix.Endpoint, otp_app: :phoenix_live_view\n\n    socket \"/live\", Phoenix.LiveView.Socket\n    socket \"/phoenix/live_reload/socket\", Phoenix.LiveReloader.Socket\n    plug Phoenix.CodeReloader\n    plug Phoenix.LiveViewTest.Support.Router\n  end\n\n  import Phoenix.ConnTest\n  import Phoenix.ChannelTest\n  import Phoenix.LiveViewTest\n\n  @endpoint Endpoint\n  @pubsub PubSub\n\n  defp live_reload_config,\n    do: [\n      url: \"ws://localhost:4004\",\n      patterns: [\n        ~r\"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$\"\n      ],\n      notify: [\n        live_view: [\n          ~r\"lib/test_auth_web/live/.*(ex)$\"\n        ]\n      ]\n    ]\n\n  test \"LiveView renders again when the phoenix_live_reload is received\" do\n    %{conn: conn, socket: socket} = start(live_reload_config())\n\n    Application.put_env(:phoenix_live_view, :vsn, 1)\n    {:ok, lv, _html} = live(conn, \"/live-reload\")\n    assert render(lv) =~ \"<div>Version 1</div>\"\n\n    send(\n      socket.channel_pid,\n      {:file_event, self(), {\"lib/test_auth_web/live/user_live.ex\", :created}}\n    )\n\n    Application.put_env(:phoenix_live_view, :vsn, 2)\n\n    assert_receive {:phoenix_live_reload, :live_view, \"lib/test_auth_web/live/user_live.ex\"}\n    assert render(lv) =~ \"<div>Version 2</div>\"\n  end\n\n  def reload(endpoint, caller) do\n    Phoenix.CodeReloader.reload(endpoint)\n    send(caller, :reloaded)\n  end\n\n  test \"custom reloader\" do\n    reloader = {__MODULE__, :reload, [self()]}\n    %{conn: conn, socket: socket} = start([reloader: reloader] ++ live_reload_config())\n\n    Application.put_env(:phoenix_live_view, :vsn, 1)\n    {:ok, lv, _html} = live(conn, \"/live-reload\")\n    assert render(lv) =~ \"<div>Version 1</div>\"\n\n    send(\n      socket.channel_pid,\n      {:file_event, self(), {\"lib/test_auth_web/live/user_live.ex\", :created}}\n    )\n\n    Application.put_env(:phoenix_live_view, :vsn, 2)\n\n    assert_receive {:phoenix_live_reload, :live_view, \"lib/test_auth_web/live/user_live.ex\"}\n    assert_receive :reloaded, 1000\n    assert render(lv) =~ \"<div>Version 2</div>\"\n  end\n\n  def start(live_reload_config) do\n    start_supervised!(\n      {@endpoint,\n       secret_key_base: String.duplicate(\"1\", 50),\n       live_view: [signing_salt: \"0123456789\"],\n       pubsub_server: @pubsub,\n       live_reload: live_reload_config}\n    )\n\n    conn = Plug.Test.init_test_session(build_conn(), %{})\n    start_supervised!({Phoenix.PubSub, name: @pubsub})\n    Phoenix.PubSub.subscribe(@pubsub, \"live_view\")\n\n    {:ok, _, socket} =\n      subscribe_and_join(\n        socket(Phoenix.LiveReloader.Socket),\n        Phoenix.LiveReloader.Channel,\n        \"phoenix:live_reload\",\n        %{}\n      )\n\n    %{conn: conn, socket: socket}\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/live_view_test.exs",
    "content": "defmodule Phoenix.LiveView.LiveViewTest do\n  use ExUnit.Case, async: true\n\n  import Phoenix.ConnTest\n  import Phoenix.LiveViewTest\n  import Phoenix.LiveViewTest.TreeDOM, only: [sigil_X: 2]\n\n  alias Phoenix.HTML\n  alias Phoenix.LiveView\n  alias Phoenix.LiveViewTest.{DOM, TreeDOM}\n  alias Phoenix.LiveViewTest.Support.Endpoint\n\n  @endpoint Endpoint\n\n  setup config do\n    {:ok, conn: Plug.Test.init_test_session(build_conn(), config[:session] || %{})}\n  end\n\n  defp simulate_bad_token_on_page(%Plug.Conn{} = conn) do\n    html = html_response(conn, 200)\n\n    [{_id, session_token, _static} | _] =\n      html |> DOM.parse_document() |> elem(1) |> TreeDOM.find_live_views()\n\n    %{conn | resp_body: String.replace(html, session_token, \"badsession\")}\n  end\n\n  defp simulate_outdated_token_on_page(%Plug.Conn{} = conn) do\n    html = html_response(conn, 200)\n\n    [{_id, session_token, _static} | _] =\n      html |> DOM.parse_document() |> elem(1) |> TreeDOM.find_live_views()\n\n    salt = Phoenix.LiveView.Utils.salt!(@endpoint)\n    outdated_token = Phoenix.Token.sign(@endpoint, salt, {0, %{}})\n    %{conn | resp_body: String.replace(html, session_token, outdated_token)}\n  end\n\n  defp simulate_expired_token_on_page(%Plug.Conn{} = conn) do\n    html = html_response(conn, 200)\n\n    [{_id, session_token, _static} | _] =\n      html |> DOM.parse_document() |> elem(1) |> TreeDOM.find_live_views()\n\n    salt = Phoenix.LiveView.Utils.salt!(@endpoint)\n\n    expired_token =\n      Phoenix.Token.sign(@endpoint, salt, {Phoenix.LiveView.Static.token_vsn(), %{}},\n        signed_at: 0\n      )\n\n    %{conn | resp_body: String.replace(html, session_token, expired_token)}\n  end\n\n  describe \"mounting\" do\n    test \"static mount followed by connected mount\", %{conn: conn} do\n      conn = get(conn, \"/thermo\")\n      assert html_response(conn, 200) =~ \"The temp is: 0\"\n\n      {:ok, _view, html} = live(conn)\n      assert html =~ \"The temp is: 1\"\n    end\n\n    test \"live mount in single call\", %{conn: conn} do\n      {:ok, _view, html} = live(conn, \"/thermo\")\n      assert html =~ \"The temp is: 1\"\n    end\n\n    test \"live mount sets caller\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/thermo\")\n      {:dictionary, dictionary} = Process.info(view.pid, :dictionary)\n      assert dictionary[:\"$callers\"] == [self()]\n    end\n\n    test \"live mount without issuing request\", %{conn: conn} do\n      assert_raise ArgumentError, ~r/a request has not yet been sent/, fn ->\n        live(conn)\n      end\n    end\n\n    test \"live mount with unexpected status\", %{conn: conn} do\n      assert_raise ArgumentError, ~r/unexpected 404 response/, fn ->\n        conn\n        |> get(\"/not_found\")\n        |> live()\n      end\n    end\n  end\n\n  describe \"render_with\" do\n    test \"with custom function\", %{conn: conn} do\n      conn = get(conn, \"/render-with\")\n      html = html_response(conn, 200)\n      assert html =~ \"FROM RENDER WITH!\"\n\n      {:ok, view, html} = live(conn)\n      assert html =~ \"FROM RENDER WITH!\"\n      assert render(view) =~ \"FROM RENDER WITH!\"\n    end\n  end\n\n  describe \"rendering\" do\n    test \"live render with valid session\", %{conn: conn} do\n      conn = get(conn, \"/thermo\")\n      html = html_response(conn, 200)\n\n      assert html =~ \"\"\"\n             <p>The temp is: 0</p>\n             <button phx-click=\"dec\">-</button>\n             <button phx-click=\"inc\">+</button>\n             \"\"\"\n\n      {:ok, view, html} = live(conn)\n      assert is_pid(view.pid)\n\n      {_tag, _attrs, children} =\n        html |> TreeDOM.normalize_to_tree() |> TreeDOM.by_id!(view.id)\n\n      assert children == [\n               {\"p\", [], [\"Redirect: none\"]},\n               {\"p\", [], [\"The temp is: 1\"]},\n               {\"button\", [{\"phx-click\", \"dec\"}], [\"-\"]},\n               {\"button\", [{\"phx-click\", \"inc\"}], [\"+\"]}\n             ]\n    end\n\n    test \"live render with bad session\", %{conn: conn} do\n      conn = simulate_bad_token_on_page(get(conn, \"/thermo\"))\n      assert {:error, {:redirect, %{to: \"http://www.example.com/thermo\"}}} = live(conn)\n    end\n\n    test \"live render with outdated session\", %{conn: conn} do\n      conn = simulate_outdated_token_on_page(get(conn, \"/thermo\"))\n      assert {:error, {:redirect, %{to: \"http://www.example.com/thermo\"}}} = live(conn)\n    end\n\n    test \"live render with expired session\", %{conn: conn} do\n      conn = simulate_expired_token_on_page(get(conn, \"/thermo\"))\n      assert {:error, {:redirect, %{to: \"http://www.example.com/thermo\"}}} = live(conn)\n    end\n\n    test \"live render in widget-style\", %{conn: conn} do\n      conn = get(conn, \"/widget\")\n      assert html_response(conn, 200) =~ ~r/WIDGET:[\\S\\s]*time: 12:00 NY/\n    end\n\n    test \"live render with socket.assigns\", %{conn: conn} do\n      assert_raise KeyError,\n                   ~r/key :boom not found in:\\s+#Phoenix.LiveView.Socket.AssignsNotInSocket<>/,\n                   fn ->\n                     live(conn, \"/assigns-not-in-socket\")\n                   end\n    end\n\n    @tag session: %{nest: [], users: [%{name: \"Annette O'Connor\", email: \"anne@email.com\"}]}\n    test \"live render with correct escaping\", %{conn: conn} do\n      {:ok, _view, html} = live(conn, \"/thermo\")\n      assert html =~ \"The temp is: 1\"\n      assert html =~ \"O'Connor\" |> HTML.html_escape() |> HTML.safe_to_string()\n    end\n\n    test \"live render with container giving class as list\", %{conn: conn} do\n      {:ok, _view, html} = live(conn, \"/classlist\")\n      assert html =~ ~s|class=\"foo bar\"|\n    end\n\n    test \"raises for duplicate ids by default\", %{conn: conn} do\n      Process.flag(:trap_exit, true)\n\n      fun = fn ->\n        {:ok, view, _html} = live(conn, \"/duplicate-id\")\n        render(view)\n      end\n\n      assert catch_exit(fun.())\n      assert_receive {:EXIT, _pid, {exception, _}}\n      assert Exception.message(exception) =~ \"Duplicate id found while testing LiveView: a\"\n    end\n\n    test \"raises for duplicate ids when on_error: :raise\", %{conn: conn} do\n      Process.flag(:trap_exit, true)\n\n      fun = fn ->\n        {:ok, view, _html} = live(conn, \"/duplicate-id\", on_error: :raise)\n        render(view)\n      end\n\n      assert catch_exit(fun.())\n      assert_receive {:EXIT, _pid, {exception, _}}\n      assert Exception.message(exception) =~ \"Duplicate id found while testing LiveView: a\"\n    end\n\n    test \"raises for duplicate components by default\", %{conn: conn} do\n      Process.flag(:trap_exit, true)\n\n      fun = fn ->\n        {:ok, view, _html} = live(conn, \"/dynamic-duplicate-component\", on_error: :raise)\n        view |> element(\"button\", \"Toggle duplicate LC\") |> render_click()\n        render(view)\n      end\n\n      assert catch_exit(fun.())\n      assert_receive {:EXIT, _pid, {exception, _}}\n      message = Exception.message(exception)\n      assert message =~ \"Duplicate live component found while testing LiveView:\"\n      assert message =~ \"I am LiveComponent2\"\n      refute message =~ \"I am a LC inside nested LV\"\n    end\n\n    test \"raises for duplicate components when on_error: :raise\", %{conn: conn} do\n      Process.flag(:trap_exit, true)\n\n      fun = fn ->\n        {:ok, view, _html} = live(conn, \"/dynamic-duplicate-component\", on_error: :raise)\n        view |> element(\"button\", \"Toggle duplicate LC\") |> render_click()\n        render(view)\n      end\n\n      assert catch_exit(fun.())\n      assert_receive {:EXIT, _pid, {exception, _}}\n      message = Exception.message(exception)\n      assert message =~ \"Duplicate live component found while testing LiveView:\"\n      assert message =~ \"I am LiveComponent2\"\n      refute message =~ \"I am a LC inside nested LV\"\n    end\n  end\n\n  describe \"render_*\" do\n    test \"render_click\", %{conn: conn} do\n      {:ok, view, _} = live(conn, \"/thermo\")\n      assert render_click(view, :save, %{temp: 20}) =~ \"The temp is: 20\"\n    end\n\n    test \"render_submit\", %{conn: conn} do\n      {:ok, view, _} = live(conn, \"/thermo\")\n      assert render_submit(view, :save, %{temp: 20}) =~ \"The temp is: 20\"\n    end\n\n    test \"render_change\", %{conn: conn} do\n      {:ok, view, _} = live(conn, \"/thermo\")\n      assert render_change(view, :save, %{temp: 21}) =~ \"The temp is: 21\"\n    end\n\n    test \"render_change with _target\", %{conn: conn} do\n      {:ok, view, _} = live(conn, \"/thermo\")\n      assert render_change(view, :save, %{_target: \"\", temp: 21}) =~ \"The temp is: 21[]\"\n\n      assert render_change(view, :save, %{_target: [\"user\"], temp: 21}) =~\n               \"The temp is: 21[&quot;user&quot;]\"\n\n      assert render_change(view, :save, %{_target: [\"user\", \"name\"], temp: 21}) =~\n               \"The temp is: 21[&quot;user&quot;, &quot;name&quot;]\"\n\n      assert render_change(view, :save, %{_target: [\"another\", \"field\"], temp: 21}) =~\n               \"The temp is: 21[&quot;another&quot;, &quot;field&quot;]\"\n    end\n\n    test \"render_key|up|down\", %{conn: conn} do\n      {:ok, view, _} = live(conn, \"/thermo\")\n      assert render(view) =~ \"The temp is: 1\"\n      assert render_keyup(view, :key, %{\"key\" => \"i\"}) =~ \"The temp is: 2\"\n      assert render_keydown(view, :key, %{\"key\" => \"d\"}) =~ \"The temp is: 1\"\n      assert render_keyup(view, :key, %{\"key\" => \"d\"}) =~ \"The temp is: 0\"\n      assert render(view) =~ \"The temp is: 0\"\n    end\n\n    test \"render_blur and render_focus\", %{conn: conn} do\n      {:ok, view, _} = live(conn, \"/thermo\")\n      assert render(view) =~ \"The temp is: 1\", view.id\n      assert render_blur(view, :inactive, %{value: \"Zzz\"}) =~ \"Tap to wake – Zzz\"\n      assert render_focus(view, :active, %{value: \"Hello!\"}) =~ \"Waking up – Hello!\"\n    end\n\n    test \"render_hook\", %{conn: conn} do\n      {:ok, view, _} = live(conn, \"/thermo\")\n      assert render_hook(view, :save, %{temp: 20}) =~ \"The temp is: 20\"\n    end\n  end\n\n  describe \"container\" do\n    test \"module DOM container\", %{conn: conn} do\n      conn =\n        conn\n        |> Plug.Test.init_test_session(%{nest: []})\n        |> get(\"/thermo\")\n\n      static_html = html_response(conn, 200)\n      {:ok, view, connected_html} = live(conn)\n\n      dom_matcher = fn html ->\n        assert [\n                 {\"article\",\n                  [\n                    {\"class\", \"thermo\"},\n                    {\"data-phx-main\", _},\n                    {\"data-phx-session\", _},\n                    {\"data-phx-static\", _},\n                    {\"id\", id}\n                  ],\n                  [\n                    _p1,\n                    _p2,\n                    _btn_down,\n                    _btn_up,\n                    {\"section\",\n                     [\n                       {\"class\", \"clock\"},\n                       {\"data-phx-parent-id\", id},\n                       {\"data-phx-session\", _},\n                       {\"data-phx-static\", _},\n                       {\"id\", \"clock\"}\n                     ], _}\n                  ]}\n               ] = TreeDOM.normalize_to_tree(html, sort_attributes: true)\n      end\n\n      dom_matcher.(static_html)\n      dom_matcher.(connected_html)\n      dom_matcher.(render(view))\n    end\n\n    test \"custom DOM container and attributes\", %{conn: conn} do\n      conn =\n        conn\n        |> Plug.Test.init_test_session(%{nest: [container: {:section, class: \"clock-flex\"}]})\n        |> get(\"/thermo-container\")\n\n      static_html = html_response(conn, 200)\n      {:ok, view, connected_html} = live(conn)\n\n      dom_matcher = fn html ->\n        assert [\n                 {\"span\",\n                  [\n                    {\"class\", \"thermo\"},\n                    {\"data-phx-main\", _},\n                    {\"data-phx-session\", _},\n                    {\"data-phx-static\", _},\n                    {\"id\", id},\n                    {\"style\", \"thermo-flex<script>\"}\n                  ],\n                  [\n                    _p1,\n                    _p2,\n                    _btn_down,\n                    _btn_up,\n                    {\"section\",\n                     [\n                       {\"class\", \"clock-flex\"},\n                       {\"data-phx-parent-id\", id},\n                       {\"data-phx-session\", _},\n                       {\"data-phx-static\", _},\n                       {\"id\", \"clock\"}\n                     ], _}\n                  ]}\n               ] = TreeDOM.normalize_to_tree(html, sort_attributes: true)\n      end\n\n      dom_matcher.(static_html)\n      dom_matcher.(connected_html)\n      dom_matcher.(render(view))\n    end\n  end\n\n  describe \"messaging callbacks\" do\n    test \"handle_event with no change in socket\", %{conn: conn} do\n      {:ok, view, html} = live(conn, \"/thermo\")\n      assert html =~ \"The temp is: 1\"\n      assert render_click(view, :noop) =~ \"The temp is: 1\"\n    end\n\n    test \"handle_info with change\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/thermo\")\n\n      assert render(view) =~ \"The temp is: 1\"\n\n      GenServer.call(view.pid, {:set, :val, 1})\n      GenServer.call(view.pid, {:set, :val, 2})\n      GenServer.call(view.pid, {:set, :val, 3})\n\n      assert TreeDOM.normalize_to_tree(render_click(view, :inc)) ==\n               ~X\"\"\"\n               <p>Redirect: none</p>\n               <p>The temp is: 4</p>\n               <button phx-click=\"dec\">-</button>\n               <button phx-click=\"inc\">+</button>\n               \"\"\"\n\n      assert TreeDOM.normalize_to_tree(render_click(view, :dec)) ==\n               ~X\"\"\"\n               <p>Redirect: none</p>\n               <p>The temp is: 3</p>\n               <button phx-click=\"dec\">-</button>\n               <button phx-click=\"inc\">+</button>\n               \"\"\"\n\n      [{_, _, child_nodes} | _] = TreeDOM.normalize_to_tree(render(view))\n\n      assert child_nodes ==\n               ~X\"\"\"\n               <p>Redirect: none</p>\n               <p>The temp is: 3</p>\n               <button phx-click=\"dec\">-</button>\n               <button phx-click=\"inc\">+</button>\n               \"\"\"\n    end\n  end\n\n  describe \"title\" do\n    test \"sends page title updates\", %{conn: conn} do\n      {:ok, view, _html} = live(conn, \"/thermo\")\n      GenServer.call(view.pid, {:set, :page_title, \"New Title\"})\n      assert page_title(view) =~ \"New Title\"\n\n      GenServer.call(view.pid, {:set, :page_title, \"<i>New Title</i>\"})\n      assert page_title(view) =~ \"&lt;i&gt;New Title&lt;/i&gt;\"\n    end\n  end\n\n  describe \"live_isolated\" do\n    test \"renders a live view with custom session\", %{conn: conn} do\n      {:ok, view, _} =\n        live_isolated(conn, Phoenix.LiveViewTest.Support.DashboardLive,\n          session: %{\"hello\" => \"world\"}\n        )\n\n      assert render(view) =~ \"session: %{&quot;hello&quot; =&gt; &quot;world&quot;}\"\n    end\n\n    test \"renders a live view with custom session and a router\", %{conn: %Plug.Conn{} = conn} do\n      conn = %{conn | request_path: \"/router/thermo_defaults/123\"}\n\n      {:ok, view, _} =\n        live_isolated(conn, Phoenix.LiveViewTest.Support.DashboardLive,\n          session: %{\"hello\" => \"world\"}\n        )\n\n      assert render(view) =~ \"session: %{&quot;hello&quot; =&gt; &quot;world&quot;}\"\n    end\n\n    test \"raises if handle_params is implemented\", %{conn: conn} do\n      assert_raise ArgumentError,\n                   ~r/it is not mounted nor accessed through the router live\\/3 macro/,\n                   fn -> live_isolated(conn, Phoenix.LiveViewTest.Support.ParamCounterLive) end\n    end\n\n    test \"works without an initialized session\" do\n      {:ok, view, _} =\n        live_isolated(Phoenix.ConnTest.build_conn(), Phoenix.LiveViewTest.Support.DashboardLive,\n          session: %{\"hello\" => \"world\"}\n        )\n\n      assert render(view) =~ \"session: %{&quot;hello&quot; =&gt; &quot;world&quot;}\"\n    end\n\n    test \"raises on session with atom keys\" do\n      assert_raise ArgumentError, ~r\"LiveView :session must be a map with string keys,\", fn ->\n        live_isolated(Phoenix.ConnTest.build_conn(), Phoenix.LiveViewTest.Support.DashboardLive,\n          session: %{hello: \"world\"}\n        )\n      end\n    end\n\n    test \"raises for duplicate ids by default\" do\n      Process.flag(:trap_exit, true)\n\n      fun = fn ->\n        {:ok, view, _html} =\n          live_isolated(\n            Phoenix.ConnTest.build_conn(),\n            Phoenix.LiveViewTest.Support.DuplicateIdLive\n          )\n\n        # errors are detected asynchronously, so we need to render again for the message to be processed\n        render(view)\n      end\n\n      assert catch_exit(fun.())\n      assert_receive {:EXIT, _, {exception, _}}\n      assert Exception.message(exception) =~ \"Duplicate id found while testing LiveView: a\"\n    end\n\n    test \"raises for duplicate ids when on_error: raise\" do\n      Process.flag(:trap_exit, true)\n\n      fun = fn ->\n        {:ok, view, _html} =\n          live_isolated(\n            Phoenix.ConnTest.build_conn(),\n            Phoenix.LiveViewTest.Support.DuplicateIdLive,\n            on_error: :raise\n          )\n\n        # errors are detected asynchronously, so we need to render again for the message to be processed\n        render(view)\n      end\n\n      assert catch_exit(fun.())\n      assert_receive {:EXIT, _, {exception, _}}\n      assert Exception.message(exception) =~ \"Duplicate id found while testing LiveView: a\"\n    end\n\n    test \"raises for duplicate components by default\" do\n      Process.flag(:trap_exit, true)\n\n      fun = fn ->\n        {:ok, view, _html} =\n          live_isolated(\n            Phoenix.ConnTest.build_conn(),\n            Phoenix.LiveViewTest.Support.DynamicDuplicateComponentLive\n          )\n\n        view |> element(\"button\", \"Toggle duplicate LC\") |> render_click()\n        render(view)\n      end\n\n      # errors are detected asynchronously, so we need to render again for the message to be processed\n      assert catch_exit(fun.())\n\n      assert_receive {:EXIT, _, {exception, _}}\n\n      assert Exception.message(exception) =~\n               \"Duplicate live component found while testing LiveView:\"\n    end\n\n    test \"raises for duplicate components when on_error: raise\" do\n      Process.flag(:trap_exit, true)\n\n      fun = fn ->\n        {:ok, view, _html} =\n          live_isolated(\n            Phoenix.ConnTest.build_conn(),\n            Phoenix.LiveViewTest.Support.DynamicDuplicateComponentLive,\n            on_error: :raise\n          )\n\n        view |> element(\"button\", \"Toggle duplicate LC\") |> render_click()\n        render(view)\n      end\n\n      # errors are detected asynchronously, so we need to render again for the message to be processed\n      assert catch_exit(fun.())\n\n      assert_receive {:EXIT, _, {exception, _}}\n\n      assert Exception.message(exception) =~\n               \"Duplicate live component found while testing LiveView:\"\n    end\n  end\n\n  describe \"format_status/2\" do\n    test \"returns LiveView information\", %{conn: conn} do\n      {:ok, %{pid: pid}, _html} = live(conn, \"/clock\")\n\n      assert {:status, ^pid, {:module, :gen_server},\n              [\n                _pdict,\n                :running,\n                _parent,\n                _dbg_opts,\n                [\n                  header: ~c\"Status for generic server \" ++ _,\n                  data: _gen_server_data,\n                  data: [\n                    {~c\"LiveView\", Phoenix.LiveViewTest.Support.ClockLive},\n                    {~c\"Parent pid\", nil},\n                    {~c\"Transport pid\", _},\n                    {~c\"Topic\", <<_::binary>>},\n                    {~c\"Components count\", 0}\n                  ]\n                ]\n              ]} = :sys.get_status(pid)\n    end\n  end\n\n  describe \"transport_pid/1\" do\n    test \"raises when not connected\" do\n      assert_raise ArgumentError, ~r/may only be called when the socket is connected/, fn ->\n        LiveView.transport_pid(%LiveView.Socket{})\n      end\n    end\n\n    test \"return the transport pid as the test process when connected\", %{conn: conn} do\n      {:ok, clock_view, _html} = live(conn, \"/clock\")\n      parent = self()\n      ref = make_ref()\n\n      send(\n        clock_view.pid,\n        {:run,\n         fn socket ->\n           send(parent, {ref, LiveView.transport_pid(socket)})\n         end}\n      )\n\n      assert_receive {^ref, transport_pid}\n      assert transport_pid == self()\n    end\n  end\n\n  describe \"connected mount exceptions\" do\n    test \"when disconnected, raises normally per plug wrapper\", %{conn: conn} do\n      assert_raise(\n        Phoenix.LiveViewTest.Support.ThermostatLive.Error,\n        fn ->\n          get(conn, \"/thermo?raise_disconnected=500\")\n        end\n      )\n\n      assert_raise(\n        Phoenix.LiveViewTest.Support.ThermostatLive.Error,\n        fn ->\n          get(conn, \"/thermo?raise_disconnected=404\")\n        end\n      )\n    end\n\n    test \"when connected, raises and exits for 5xx\", %{conn: conn} do\n      assert {{exception, _}, _} = catch_exit(live(conn, \"/thermo?raise_connected=500\"))\n      assert %Phoenix.LiveViewTest.Support.ThermostatLive.Error{plug_status: 500} = exception\n    end\n\n    test \"when connected, raises and wraps 4xx in client response\", %{conn: conn} do\n      assert {reason, _} = catch_exit(live(conn, \"/thermo?raise_connected=404\"))\n      assert %{reason: \"reload\", status: 404, token: token} = reason\n\n      # does not expose stack or exception module by default\n      assert Phoenix.LiveView.Static.verify_token(@endpoint, token) ==\n               {:ok,\n                %{\n                  status: 404,\n                  exception: nil,\n                  stack: [],\n                  view: \"Phoenix.LiveViewTest.Support.ThermostatLive\"\n                }}\n\n      response =\n        assert_error_sent(404, fn ->\n          conn\n          |> put_req_cookie(\"__phoenix_reload_status__\", token)\n          |> get(\"/thermo\")\n        end)\n\n      # deletes cookie with response\n      {404, resp_headers, \"Not Found\"} = response\n      assert %{\"set-cookie\" => \"__phoenix_reload_status__=;\" <> _} = Map.new(resp_headers)\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/live_view_test_warnings_test.exs",
    "content": "defmodule Phoenix.LiveView.LiveViewTestWarningsTest do\n  use ExUnit.Case, async: false\n\n  import ExUnit.CaptureIO\n\n  import Phoenix.ConnTest\n  import Phoenix.LiveViewTest\n\n  alias Phoenix.LiveViewTest.Support.Endpoint\n\n  @endpoint Endpoint\n\n  describe \"live\" do\n    test \"warns for duplicate ids when on_error: warn\" do\n      conn = Plug.Test.init_test_session(Phoenix.ConnTest.build_conn(), %{})\n      conn = get(conn, \"/duplicate-id\")\n\n      Process.flag(:trap_exit, true)\n\n      assert capture_io(:stderr, fn ->\n               {:ok, view, _html} = live(conn, nil, on_error: :warn)\n               render(view)\n             end) =~\n               \"Duplicate id found while testing LiveView: a\"\n\n      refute_receive {:EXIT, _, _}\n    end\n\n    test \"warns for duplicate component when on_error: warn\" do\n      conn = Plug.Test.init_test_session(Phoenix.ConnTest.build_conn(), %{})\n      conn = get(conn, \"/dynamic-duplicate-component\")\n\n      Process.flag(:trap_exit, true)\n\n      warning =\n        capture_io(:stderr, fn ->\n          {:ok, view, _html} = live(conn, nil, on_error: :warn)\n\n          view |> element(\"button\", \"Toggle duplicate LC\") |> render_click() =~\n            \"I am LiveComponent2\"\n\n          render(view)\n        end)\n\n      assert warning =~ \"Duplicate live component found while testing LiveView:\"\n      assert warning =~ \"I am LiveComponent2\"\n      refute warning =~ \"I am a LC inside nested LV\"\n\n      refute_receive {:EXIT, _, _}\n    end\n  end\n\n  describe \"live_isolated\" do\n    test \"warns for duplicate ids when on_error: warn\" do\n      Process.flag(:trap_exit, true)\n\n      assert capture_io(:stderr, fn ->\n               {:ok, view, _html} =\n                 live_isolated(\n                   Phoenix.ConnTest.build_conn(),\n                   Phoenix.LiveViewTest.Support.DuplicateIdLive,\n                   on_error: :warn\n                 )\n\n               render(view)\n             end) =~\n               \"Duplicate id found while testing LiveView: a\"\n\n      refute_receive {:EXIT, _, _}\n    end\n\n    test \"warns for duplicate component when on_error: warn\" do\n      Process.flag(:trap_exit, true)\n\n      warning =\n        capture_io(:stderr, fn ->\n          {:ok, view, _html} =\n            live_isolated(\n              Phoenix.ConnTest.build_conn(),\n              Phoenix.LiveViewTest.Support.DynamicDuplicateComponentLive,\n              on_error: :warn\n            )\n\n          view |> element(\"button\", \"Toggle duplicate LC\") |> render_click() =~\n            \"I am LiveComponent2\"\n\n          render(view)\n        end)\n\n      assert warning =~ \"Duplicate live component found while testing LiveView:\"\n      assert warning =~ \"I am LiveComponent2\"\n      refute warning =~ \"I am a LC inside nested LV\"\n\n      refute_receive {:EXIT, _, _}\n    end\n  end\n\n  describe \"missing form id\" do\n    test \"warns for form with missing id\" do\n      orig = Application.get_env(:phoenix_live_view, :test_warnings)\n\n      Application.put_env(:phoenix_live_view, :test_warnings, missing_form_id: :warn)\n\n      on_exit(fn ->\n        Application.put_env(:phoenix_live_view, :test_warnings, orig)\n      end)\n\n      warning =\n        capture_io(:stderr, fn ->\n          {:ok, view, _html} = live(Phoenix.ConnTest.build_conn(), \"/form-missing-id\")\n          render(view)\n        end)\n\n      assert warning =~ \"Detected a form with phx-change but missing id\"\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/navigation_test.exs",
    "content": "defmodule Phoenix.LiveView.NavigationTest do\n  use ExUnit.Case, async: true\n\n  import Phoenix.ConnTest\n  import Phoenix.LiveViewTest\n\n  alias Phoenix.LiveViewTest.TreeDOM\n  alias Phoenix.LiveViewTest.Support.Endpoint\n\n  @endpoint Endpoint\n\n  setup do\n    {:ok, conn: Plug.Test.init_test_session(build_conn(), %{})}\n  end\n\n  # Nested used of navigation helpers go to nested_test.exs\n\n  describe \"push_navigate\" do\n    test \"when disconnected\", %{conn: conn} do\n      conn = get(conn, \"/redir?during=disconnected&kind=push_navigate&to=/thermo\")\n      assert redirected_to(conn) == \"/thermo\"\n\n      {:error, {:live_redirect, %{to: \"/thermo\"}}} =\n        live(conn, \"/redir?during=disconnected&kind=push_navigate&to=/thermo\")\n    end\n\n    test \"when connected\", %{conn: conn} do\n      conn = get(conn, \"/redir?during=connected&kind=push_navigate&to=/thermo\")\n      assert html_response(conn, 200) =~ \"parent_content\"\n      assert {:error, {:live_redirect, %{kind: :push, to: \"/thermo\"}}} = live(conn)\n    end\n\n    test \"child when disconnected\", %{conn: conn} do\n      conn = get(conn, \"/redir?during=disconnected&kind=push_navigate&child_to=/thermo\")\n      assert redirected_to(conn) == \"/thermo\"\n    end\n\n    test \"child when connected\", %{conn: conn} do\n      conn =\n        get(conn, \"/redir?during=connected&kind=push_navigate&child_to=/thermo?from_child=true\")\n\n      assert html_response(conn, 200) =~ \"child_content\"\n      assert {:error, {:live_redirect, %{to: \"/thermo?from_child=true\"}}} = live(conn)\n    end\n  end\n\n  describe \"push_patch\" do\n    test \"when disconnected\", %{conn: conn} do\n      assert_raise RuntimeError, ~r/attempted to live patch while/, fn ->\n        get(conn, \"/redir?during=disconnected&kind=push_patch&to=/redir?patched=true\")\n      end\n    end\n\n    test \"when connected\", %{conn: conn} do\n      conn = get(conn, \"/redir?during=connected&kind=push_patch&to=/redir?patched=true\")\n      assert html_response(conn, 200) =~ \"parent_content\"\n\n      assert Exception.format(:exit, catch_exit(live(conn))) =~\n               \"attempted to live patch while mounting\"\n    end\n\n    test \"child when disconnected\", %{conn: conn} do\n      assert_raise RuntimeError,\n                   ~r/a LiveView cannot be mounted while issuing a live patch to the client/,\n                   fn ->\n                     get(\n                       conn,\n                       \"/redir?during=disconnected&kind=push_patch&child_to=/redir?patched=true\"\n                     )\n                   end\n    end\n\n    test \"child when connected\", %{conn: conn} do\n      conn = get(conn, \"/redir?during=connected&kind=push_patch&child_to=/redir?patched=true\")\n      assert html_response(conn, 200) =~ \"child_content\"\n\n      assert Exception.format(:exit, catch_exit(live(conn))) =~\n               \"a LiveView cannot be mounted while issuing a live patch to the client\"\n    end\n  end\n\n  describe \"redirect\" do\n    test \"when disconnected\", %{conn: conn} do\n      conn = get(conn, \"/redir?during=disconnected&kind=redirect&to=/thermo\")\n      assert redirected_to(conn) == \"/thermo\"\n\n      {:error, {:redirect, %{to: \"/thermo\"}}} =\n        live(conn, \"/redir?during=disconnected&kind=redirect&to=/thermo\")\n    end\n\n    test \"when connected\", %{conn: conn} do\n      conn = get(conn, \"/redir?during=connected&kind=redirect&to=/thermo\")\n      assert html_response(conn, 200) =~ \"parent_content\"\n      assert {:error, {:redirect, %{to: \"/thermo\"}}} = live(conn)\n    end\n\n    test \"child when disconnected\", %{conn: conn} do\n      conn =\n        get(conn, \"/redir?during=disconnected&kind=redirect&child_to=/thermo?from_child=true\")\n\n      assert redirected_to(conn) == \"/thermo?from_child=true\"\n    end\n\n    test \"child when connected\", %{conn: conn} do\n      conn = get(conn, \"/redir?during=connected&kind=redirect&child_to=/thermo?from_child=true\")\n      assert html_response(conn, 200) =~ \"parent_content\"\n      assert {:error, {:redirect, %{to: \"/thermo?from_child=true\"}}} = live(conn)\n    end\n  end\n\n  describe \"external redirect\" do\n    test \"when disconnected\", %{conn: conn} do\n      conn = get(conn, \"/redir?during=disconnected&kind=external&to=https://phoenixframework.org\")\n      assert redirected_to(conn) == \"https://phoenixframework.org\"\n\n      {:error, {:redirect, %{to: \"https://phoenixframework.org\"}}} =\n        live(conn, \"/redir?during=disconnected&kind=external&to=https://phoenixframework.org\")\n    end\n\n    test \"when connected\", %{conn: conn} do\n      conn = get(conn, \"/redir?during=connected&kind=external&to=https://phoenixframework.org\")\n      assert html_response(conn, 200) =~ \"parent_content\"\n      assert {:error, {:redirect, %{to: \"https://phoenixframework.org\"}}} = live(conn)\n    end\n\n    test \"child when disconnected\", %{conn: conn} do\n      conn =\n        get(\n          conn,\n          \"/redir?during=disconnected&kind=external&child_to=https://phoenixframework.org?from_child=true\"\n        )\n\n      assert redirected_to(conn) == \"https://phoenixframework.org?from_child=true\"\n    end\n\n    test \"child when connected\", %{conn: conn} do\n      conn =\n        get(\n          conn,\n          \"/redir?during=connected&kind=external&child_to=https://phoenixframework.org?from_child=true\"\n        )\n\n      assert html_response(conn, 200) =~ \"parent_content\"\n\n      assert {:error, {:redirect, %{to: \"https://phoenixframework.org?from_child=true\"}}} =\n               live(conn)\n    end\n  end\n\n  describe \"live_redirect\" do\n    test \"within same live session\", %{conn: conn} do\n      assert {:ok, thermo_live, html} = live(conn, \"/thermo-live-session\")\n      thermo_ref = Process.monitor(thermo_live.pid)\n\n      assert [{\"article\", root_attrs, _}] = TreeDOM.normalize_to_tree(html)\n\n      %{\"data-phx-session\" => thermo_session, \"data-phx-static\" => thermo_static} =\n        Enum.into(root_attrs, %{})\n\n      assert {:ok, clock_live, html} = live_redirect(thermo_live, to: \"/clock-live-session\")\n\n      for str <- [html, render(clock_live)] do\n        content = TreeDOM.normalize_to_tree(str)\n        assert [{\"section\", attrs, _inner}] = content\n        assert {\"class\", \"clock\"} in attrs\n        assert {\"data-phx-session\", thermo_session} in attrs\n        assert {\"data-phx-static\", thermo_static} in attrs\n        assert str =~ \"time: 12:00 NY\"\n      end\n\n      assert_receive {:DOWN, ^thermo_ref, :process, _pid, {:shutdown, :closed}}\n\n      assert {:ok, thermo_live2, html} = live_redirect(clock_live, to: \"/thermo-live-session\")\n\n      for str <- [html, render(thermo_live2)] do\n        content = TreeDOM.normalize_to_tree(str)\n        assert [{\"article\", attrs, _inner}] = content\n        assert {\"class\", \"thermo\"} in attrs\n        assert str =~ \"The temp is\"\n      end\n\n      assert {:ok, thermo_live3, _html} =\n               live_redirect(thermo_live2, to: \"/thermo-live-session/nested-thermo\")\n\n      assert {:ok, _thermo_live4, _html} =\n               live_redirect(thermo_live3, to: \"/thermo-live-session/nested-thermo\")\n    end\n\n    test \"refused with mismatched live session\", %{conn: conn} do\n      assert {:ok, thermo_live, _html} = live(conn, \"/thermo-live-session\")\n\n      assert {:error, {:redirect, _}} =\n               live_redirect(thermo_live, to: \"/clock-live-session-admin\")\n    end\n\n    test \"refused with no live session\", %{conn: conn} do\n      assert {:ok, thermo_live, _html} = live(conn, \"/thermo\")\n      assert {:error, {:redirect, _}} = live_redirect(thermo_live, to: \"/thermo-live-session\")\n\n      assert {:ok, thermo_live, _html} = live(conn, \"/thermo\")\n\n      assert {:error, {:redirect, _}} =\n               live_redirect(thermo_live, to: \"/thermo-live-session-admin\")\n    end\n\n    test \"with outdated token\", %{conn: conn} do\n      assert {:ok, thermo_live, _html} = live(conn, \"/thermo-live-session\")\n\n      assert {:error, {:redirect, %{to: \"http://www.example.com/clock-live-session\"}}} =\n               Phoenix.LiveViewTest.__live_redirect__(\n                 thermo_live,\n                 [to: \"/clock-live-session\"],\n                 fn _token ->\n                   salt = Phoenix.LiveView.Utils.salt!(@endpoint)\n                   Phoenix.Token.sign(@endpoint, salt, {0, %{}})\n                 end\n               )\n    end\n\n    test \"assigns given class list to redirected to container\", %{conn: conn} do\n      assert {:ok, thermo_live, _} = live(conn, \"/thermo-live-session\")\n      assert {:ok, _classlist_live, html} = live_redirect(thermo_live, to: \"/classlist\")\n\n      assert html =~ ~s|class=\"foo bar\"|\n    end\n  end\n\n  describe \"live_patch\" do\n    test \"patches on custom host with full path\", %{conn: conn} do\n      {:ok, live, html} = live(conn, \"https://app.example.com/with-host/full\")\n      assert html =~ \"URI: https://app.example.com/with-host/full\"\n      assert html =~ \"LiveAction: full\"\n\n      html = live |> element(\"#path\") |> render_click()\n      assert html =~ \"URI: https://app.example.com/with-host/path\"\n      assert html =~ \"LiveAction: path\"\n\n      html = live |> element(\"#full\") |> render_click()\n      assert html =~ \"URI: https://app.example.com/with-host/full\"\n      assert html =~ \"LiveAction: full\"\n    end\n\n    test \"patches on custom host with partial path\", %{conn: conn} do\n      {:ok, live, html} = live(%{conn | host: \"app.example.com\"}, \"/with-host/path\")\n      assert html =~ \"URI: http://app.example.com/with-host/path\"\n      assert html =~ \"LiveAction: path\"\n\n      html = live |> element(\"#full\") |> render_click()\n      assert html =~ \"URI: https://app.example.com/with-host/full\"\n      assert html =~ \"LiveAction: full\"\n\n      html = live |> element(\"#path\") |> render_click()\n      assert html =~ \"URI: http://app.example.com/with-host/path\"\n      assert html =~ \"LiveAction: path\"\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/nested_test.exs",
    "content": "defmodule Phoenix.LiveView.NestedTest do\n  use ExUnit.Case, async: true\n\n  import Plug.Conn\n  import Phoenix.ConnTest\n  import Phoenix.LiveViewTest\n\n  alias Phoenix.LiveView\n  alias Phoenix.LiveViewTest.TreeDOM\n  alias Phoenix.LiveViewTest.Support.{Endpoint, ClockLive, ClockControlsLive, LiveInComponent}\n\n  @endpoint Endpoint\n\n  setup config do\n    {:ok, conn: Plug.Test.init_test_session(build_conn(), config[:session] || %{})}\n  end\n\n  test \"nested child render on disconnected mount\", %{conn: conn} do\n    conn =\n      conn\n      |> Plug.Test.init_test_session(%{nest: []})\n      |> get(\"/thermo\")\n\n    html = html_response(conn, 200)\n    assert html =~ \"The temp is: 0\"\n    assert html =~ \"time: 12:00\"\n    assert html =~ \"<button phx-click=\\\"snooze\\\">+</button>\"\n  end\n\n  @tag session: %{nest: []}\n  test \"nested child render on connected mount\", %{conn: conn} do\n    {:ok, thermo_view, _} = live(conn, \"/thermo\")\n\n    html = render(thermo_view)\n    assert html =~ \"The temp is: 1\"\n    assert html =~ \"time: 12:00\"\n    assert html =~ \"<button phx-click=\\\"snooze\\\">+</button>\"\n\n    GenServer.call(thermo_view.pid, {:set, :nest, false})\n    html = render(thermo_view)\n    assert html =~ \"The temp is: 1\"\n    refute html =~ \"time\"\n    refute html =~ \"snooze\"\n  end\n\n  test \"dynamically added children\", %{conn: conn} do\n    {:ok, thermo_view, _html} = live(conn, \"/thermo\")\n\n    assert render(thermo_view) =~ \"The temp is: 1\"\n    refute render(thermo_view) =~ \"time\"\n    refute render(thermo_view) =~ \"snooze\"\n    GenServer.call(thermo_view.pid, {:set, :nest, []})\n    assert render(thermo_view) =~ \"The temp is: 1\"\n    assert render(thermo_view) =~ \"time\"\n    assert render(thermo_view) =~ \"snooze\"\n\n    assert clock_view = find_live_child(thermo_view, \"clock\")\n    assert controls_view = find_live_child(clock_view, \"NY-controls\")\n    assert clock_view.module == ClockLive\n    assert controls_view.module == ClockControlsLive\n\n    assert render_click(controls_view, :snooze) == \"<button phx-click=\\\"snooze\\\">+</button>\"\n    assert render(clock_view) =~ \"time: 12:05\"\n    assert render(clock_view) =~ \"<button phx-click=\\\"snooze\\\">+</button>\"\n    assert render(controls_view) =~ \"<button phx-click=\\\"snooze\\\">+</button>\"\n\n    :ok = GenServer.call(clock_view.pid, {:set, \"12:01\"})\n\n    assert render(clock_view) =~ \"time: 12:01\"\n    assert render(thermo_view) =~ \"time: 12:01\"\n\n    assert render(thermo_view) =~ \"<button phx-click=\\\"snooze\\\">+</button>\"\n  end\n\n  @tag session: %{nest: []}\n  test \"nested children are removed and killed\", %{conn: conn} do\n    Process.flag(:trap_exit, true)\n\n    html_without_nesting =\n      TreeDOM.normalize_to_tree(\"\"\"\n      <p>Redirect: none</p>\n      <p>The temp is: 1</p>\n      <button phx-click=\"dec\">-</button>\n      <button phx-click=\"inc\">+</button>\n      \"\"\")\n\n    {:ok, thermo_view, _} = live(conn, \"/thermo\")\n\n    assert find_live_child(thermo_view, \"clock\")\n\n    refute TreeDOM.child_nodes(hd(TreeDOM.normalize_to_tree(render(thermo_view)))) ==\n             html_without_nesting\n\n    GenServer.call(thermo_view.pid, {:set, :nest, false})\n\n    assert TreeDOM.child_nodes(hd(TreeDOM.normalize_to_tree(render(thermo_view)))) ==\n             html_without_nesting\n\n    refute find_live_child(thermo_view, \"clock\")\n  end\n\n  @tag session: %{dup: false}\n  test \"multiple nested children of same module\", %{conn: conn} do\n    {:ok, parent, _} = live(conn, \"/same-child\")\n    assert tokyo = find_live_child(parent, \"Tokyo\")\n    assert madrid = find_live_child(parent, \"Madrid\")\n    assert toronto = find_live_child(parent, \"Toronto\")\n    child_ids = for view <- [tokyo, madrid, toronto], do: view.id\n\n    assert Enum.uniq(child_ids) == child_ids\n    assert render(parent) =~ \"Tokyo\"\n    assert render(parent) =~ \"Madrid\"\n    assert render(parent) =~ \"Toronto\"\n  end\n\n  @tag session: %{dup: false}\n  test \"multiple nested children of same module with new session\", %{conn: conn} do\n    {:ok, parent, _} = live(conn, \"/same-child\")\n    assert render_click(parent, :inc) =~ \"Toronto\"\n  end\n\n  test \"nested within comprehensions\", %{conn: conn} do\n    users = [\n      %{name: \"chris\", email: \"chris@test\"},\n      %{name: \"josé\", email: \"jose@test\"}\n    ]\n\n    expected_users =\n      TreeDOM.normalize_to_tree(\"<i>chris chris@test</i><i>josé jose@test</i>\")\n      |> TreeDOM.to_html()\n\n    {:ok, thermo_view, html} =\n      conn\n      |> put_session(:nest, [])\n      |> put_session(:users, users)\n      |> live(\"/thermo\")\n\n    assert TreeDOM.normalize_to_tree(html) |> TreeDOM.to_html() =~ expected_users\n    assert render(thermo_view) =~ expected_users\n  end\n\n  test \"nested within live component\" do\n    assert {:ok, _view, _html} = live_isolated(build_conn(), LiveInComponent.Root)\n  end\n\n  test \"raises on duplicate child LiveView id\", %{conn: conn} do\n    Process.flag(:trap_exit, true)\n\n    {:ok, view, _html} =\n      conn\n      |> Plug.Conn.put_session(:user_id, 13)\n      |> live(\"/root\")\n\n    :ok = GenServer.call(view.pid, {:dynamic_child, :static})\n\n    assert Exception.format(:exit, catch_exit(render(view))) =~\n             \"expected exactly one node with id static, but got 2\"\n  end\n\n  describe \"navigation helpers\" do\n    @tag session: %{nest: []}\n    test \"push_navigate\", %{conn: conn} do\n      {:ok, thermo_view, html} = live(conn, \"/thermo\")\n      assert html =~ \"Redirect: none\"\n\n      assert clock_view = find_live_child(thermo_view, \"clock\")\n\n      send(\n        clock_view.pid,\n        {:run,\n         fn socket ->\n           {:noreply, LiveView.push_navigate(socket, to: \"/thermo?redirect=push\")}\n         end}\n      )\n\n      assert_redirect(thermo_view, \"/thermo?redirect=push\")\n    end\n\n    @tag session: %{nest: []}\n    test \"refute_redirect\", %{conn: conn} do\n      {:ok, thermo_view, _html} = live(conn, \"/thermo\")\n\n      clock_view = find_live_child(thermo_view, \"clock\")\n\n      send(\n        clock_view.pid,\n        {:run,\n         fn socket ->\n           {:noreply, LiveView.push_navigate(socket, to: \"/some_url\")}\n         end}\n      )\n\n      refute_redirected(thermo_view, \"/not_going_here\")\n\n      send(\n        clock_view.pid,\n        {:run,\n         fn socket ->\n           {:noreply, LiveView.push_navigate(socket, to: \"/another_url\")}\n         end}\n      )\n\n      try do\n        refute_redirected(thermo_view, \"/another_url\")\n      rescue\n        e ->\n          assert %ArgumentError{message: message} = e\n          assert message =~ \"not to redirect to\"\n      end\n\n      send(\n        clock_view.pid,\n        {:run,\n         fn socket ->\n           {:noreply, LiveView.push_navigate(socket, to: \"/any_url\")}\n         end}\n      )\n\n      try do\n        refute_redirected(thermo_view)\n      rescue\n        e ->\n          assert %ArgumentError{message: message} = e\n          assert message =~ \"not to redirect, but got a redirect to /any_url\"\n      end\n    end\n\n    @tag session: %{nest: []}\n    test \"push_navigate with destination that can vary\", %{conn: conn} do\n      {:ok, thermo_view, html} = live(conn, \"/thermo\")\n      assert html =~ \"Redirect: none\"\n\n      assert clock_view = find_live_child(thermo_view, \"clock\")\n\n      id = Enum.random(1000..9999)\n\n      send(\n        clock_view.pid,\n        {:run,\n         fn socket ->\n           {:noreply, LiveView.push_navigate(socket, to: \"/thermo?redirect=#{id}\")}\n         end}\n      )\n\n      {path, _flash} = assert_redirect(thermo_view)\n      assert path =~ ~r/\\/thermo\\?redirect=[0-9]+/\n    end\n\n    @tag session: %{nest: []}\n    test \"push_patch\", %{conn: conn} do\n      {:ok, thermo_view, html} = live(conn, \"/thermo\")\n      assert html =~ \"Redirect: none\"\n      assert clock_view = find_live_child(thermo_view, \"clock\")\n\n      send(\n        clock_view.pid,\n        {:run,\n         fn socket ->\n           {:noreply, LiveView.push_patch(socket, to: \"/thermo?redirect=patch\")}\n         end}\n      )\n\n      assert_patch(thermo_view, \"/thermo?redirect=patch\")\n      assert render(thermo_view) =~ \"Redirect: patch\"\n    end\n\n    @tag session: %{nest: []}\n    test \"push_patch to destination which can vary\", %{conn: conn} do\n      {:ok, thermo_view, html} = live(conn, \"/thermo\")\n      assert html =~ \"Redirect: none\"\n      assert clock_view = find_live_child(thermo_view, \"clock\")\n\n      id = Enum.random(1000..9999)\n\n      send(\n        clock_view.pid,\n        {:run,\n         fn socket ->\n           {:noreply, LiveView.push_patch(socket, to: \"/thermo?redirect=#{id}\")}\n         end}\n      )\n\n      path = assert_patch(thermo_view)\n      assert path =~ ~r/\\/thermo\\?redirect=[0-9]+/\n      assert render(thermo_view) =~ \"Redirect: #{id}\"\n    end\n\n    @tag session: %{nest: []}\n    test \"redirect from child\", %{conn: conn} do\n      {:ok, thermo_view, html} = live(conn, \"/thermo\")\n      assert html =~ \"Redirect: none\"\n\n      assert clock_view = find_live_child(thermo_view, \"clock\")\n\n      send(\n        clock_view.pid,\n        {:run,\n         fn socket ->\n           {:noreply, LiveView.redirect(socket, to: \"/thermo?redirect=redirect\")}\n         end}\n      )\n\n      assert_redirect(thermo_view, \"/thermo?redirect=redirect\")\n    end\n\n    @tag session: %{nest: []}\n    test \"external redirect from child\", %{conn: conn} do\n      {:ok, thermo_view, html} = live(conn, \"/thermo\")\n      assert html =~ \"Redirect: none\"\n\n      assert clock_view = find_live_child(thermo_view, \"clock\")\n\n      send(\n        clock_view.pid,\n        {:run,\n         fn socket ->\n           {:noreply, LiveView.redirect(socket, external: \"https://phoenixframework.org\")}\n         end}\n      )\n\n      assert_redirect(thermo_view, \"https://phoenixframework.org\")\n    end\n  end\n\n  describe \"sticky\" do\n    @tag session: %{name: \"ny\"}\n    test \"process does not go down with parent\", %{conn: conn} do\n      {:ok, clock_view, _html} = live(conn, \"/clock?sticky=true\")\n      %Phoenix.LiveViewTest.View{} = sticky_child = find_live_child(clock_view, \"ny-controls\")\n      child_pid = sticky_child.pid\n      assert Process.alive?(child_pid)\n      Process.monitor(child_pid)\n\n      send(\n        clock_view.pid,\n        {:run,\n         fn socket ->\n           {:noreply, LiveView.push_navigate(socket, to: \"/clock?sticky=true&redirected=true\")}\n         end}\n      )\n\n      assert_redirect(clock_view, \"/clock?sticky=true&redirected=true\")\n      refute_receive {:DOWN, _ref, :process, ^child_pid, {:shutdown, :parent_exited}}\n      # client proxy transport\n      assert_receive {:DOWN, _ref, :process, ^child_pid, {:shutdown, :closed}}\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/params_test.exs",
    "content": "defmodule Phoenix.LiveView.ParamsTest do\n  # Telemetry events need to run synchronously\n  use ExUnit.Case, async: false\n\n  import Plug.Conn\n  import Phoenix.ConnTest\n\n  import Phoenix.LiveViewTest\n  import Phoenix.LiveView.TelemetryTestHelpers\n\n  alias Phoenix.LiveViewTest.TreeDOM\n  alias Phoenix.{Component, LiveView}\n  alias Phoenix.LiveViewTest.Support.Endpoint\n\n  @endpoint Endpoint\n\n  setup do\n    conn =\n      Phoenix.ConnTest.build_conn(:get, \"http://www.example.com/\", nil)\n      |> Plug.Test.init_test_session(%{})\n      |> put_session(:test_pid, self())\n\n    {:ok, conn: conn}\n  end\n\n  defp put_serialized_session(conn, key, value) do\n    put_session(conn, key, :erlang.term_to_binary(value))\n  end\n\n  describe \"handle_params on disconnected mount\" do\n    test \"is called with named and query string params\", %{conn: conn} do\n      conn = get(conn, \"/counter/123\", query1: \"query1\", query2: \"query2\")\n\n      response = html_response(conn, 200)\n\n      assert response =~\n               rendered_to_string(\n                 ~s|params: %{\"id\" => \"123\", \"query1\" => \"query1\", \"query2\" => \"query2\"}|\n               )\n\n      assert response =~\n               rendered_to_string(\n                 ~s|mount: %{\"id\" => \"123\", \"query1\" => \"query1\", \"query2\" => \"query2\"}|\n               )\n    end\n\n    test \"telemetry events are emitted on success\", %{conn: conn} do\n      attach_telemetry([:phoenix, :live_view, :handle_params])\n\n      get(conn, \"/counter/123\", query1: \"query1\", query2: \"query2\")\n\n      assert_receive {:event, [:phoenix, :live_view, :handle_params, :start], %{system_time: _},\n                      metadata}\n\n      refute metadata.socket.transport_pid\n      assert metadata.params == %{\"query1\" => \"query1\", \"query2\" => \"query2\", \"id\" => \"123\"}\n      assert metadata.uri == \"http://www.example.com/counter/123?query1=query1&query2=query2\"\n\n      assert_receive {:event, [:phoenix, :live_view, :handle_params, :stop], %{duration: _},\n                      metadata}\n\n      refute metadata.socket.transport_pid\n      assert metadata.params == %{\"query1\" => \"query1\", \"query2\" => \"query2\", \"id\" => \"123\"}\n      assert metadata.uri == \"http://www.example.com/counter/123?query1=query1&query2=query2\"\n    end\n\n    test \"telemetry events are emitted on exception\", %{conn: conn} do\n      attach_telemetry([:phoenix, :live_view, :handle_params])\n\n      assert_raise RuntimeError, ~r/boom/, fn ->\n        get(conn, \"/errors\", crash_on: \"disconnected_handle_params\")\n      end\n\n      assert_receive {:event, [:phoenix, :live_view, :handle_params, :start], %{system_time: _},\n                      metadata}\n\n      refute metadata.socket.transport_pid\n      assert metadata.params == %{\"crash_on\" => \"disconnected_handle_params\"}\n      assert metadata.uri == \"http://www.example.com/errors?crash_on=disconnected_handle_params\"\n\n      assert_receive {:event, [:phoenix, :live_view, :handle_params, :exception], %{duration: _},\n                      metadata}\n\n      refute metadata.socket.transport_pid\n      assert metadata.params == %{\"crash_on\" => \"disconnected_handle_params\"}\n      assert metadata.uri == \"http://www.example.com/errors?crash_on=disconnected_handle_params\"\n    end\n\n    test \"hard redirects\", %{conn: conn} do\n      assert conn\n             |> put_serialized_session(\n               :on_handle_params,\n               &{:noreply, LiveView.redirect(&1, to: \"/\")}\n             )\n             |> get(\"/counter/123?from=handle_params\")\n             |> redirected_to() == \"/\"\n    end\n\n    test \"hard redirects with a custom status\", %{conn: conn} do\n      assert conn\n             |> put_serialized_session(\n               :on_handle_params,\n               &{:noreply, LiveView.redirect(&1, to: \"/\", status: 301)}\n             )\n             |> get(\"/counter/123?from=handle_params\")\n             |> redirected_to(301) == \"/\"\n    end\n\n    test \"hard redirect with flash message\", %{conn: conn} do\n      conn =\n        put_serialized_session(conn, :on_handle_params, fn socket ->\n          {:noreply, socket |> LiveView.put_flash(:info, \"msg\") |> LiveView.redirect(to: \"/\")}\n        end)\n        |> fetch_flash()\n        |> get(\"/counter/123?from=handle_params\")\n\n      assert redirected_to(conn) == \"/\"\n      assert Phoenix.Flash.get(conn.assigns.flash, :info) == \"msg\"\n    end\n\n    test \"push_patch\", %{conn: conn} do\n      assert conn\n             |> put_serialized_session(:on_handle_params, fn socket ->\n               {:noreply, LiveView.push_patch(socket, to: \"/counter/123?from=rehandled_params\")}\n             end)\n             |> get(\"/counter/123?from=handle_params\")\n             |> redirected_to() == \"/counter/123?from=rehandled_params\"\n    end\n\n    test \"push_navigate\", %{conn: conn} do\n      assert conn\n             |> put_serialized_session(:on_handle_params, fn socket ->\n               {:noreply, LiveView.push_navigate(socket, to: \"/thermo/456\")}\n             end)\n             |> get(\"/counter/123?from=handle_params\")\n             |> redirected_to() == \"/thermo/456\"\n    end\n\n    test \"with encoded URL\", %{conn: conn} do\n      assert get(conn, \"/counter/Wm9uZ%2FozNzYxOA%3D%3D?foo=bar+15%26\")\n\n      assert_receive {:handle_params, _uri, _assigns,\n                      %{\"id\" => \"Wm9uZ/ozNzYxOA==\", \"foo\" => \"bar 15&\"}}\n    end\n  end\n\n  describe \"handle_params on connected mount\" do\n    test \"is called on connected mount with query string params from get\", %{conn: conn} do\n      {:ok, _, html} =\n        conn\n        |> get(\"/counter/123?q1=1\", q2: \"2\")\n        |> live()\n\n      assert html =~ rendered_to_string(~s|params: %{\"id\" => \"123\", \"q1\" => \"1\", \"q2\" => \"2\"}|)\n      assert html =~ rendered_to_string(~s|mount: %{\"id\" => \"123\", \"q1\" => \"1\", \"q2\" => \"2\"}|)\n    end\n\n    test \"is called on connected mount with query string params from live\", %{conn: conn} do\n      {:ok, _, html} =\n        conn\n        |> live(\"/counter/123?q1=1\")\n\n      assert html =~ rendered_to_string(~s|%{\"id\" => \"123\", \"q1\" => \"1\"}|)\n    end\n\n    test \"telemetry events are emitted on success\", %{conn: conn} do\n      attach_telemetry([:phoenix, :live_view, :handle_params])\n\n      live(conn, \"/counter/123?foo=bar\")\n\n      assert_receive {:event, [:phoenix, :live_view, :handle_params, :start], %{system_time: _},\n                      %{socket: %{transport_pid: pid}} = metadata}\n                     when is_pid(pid)\n\n      assert metadata.params == %{\"id\" => \"123\", \"foo\" => \"bar\"}\n      assert metadata.uri == \"http://www.example.com/counter/123?foo=bar\"\n\n      assert_receive {:event, [:phoenix, :live_view, :handle_params, :stop], %{duration: _},\n                      %{socket: %{transport_pid: pid}} = metadata}\n                     when is_pid(pid)\n\n      assert metadata.params == %{\"id\" => \"123\", \"foo\" => \"bar\"}\n      assert metadata.uri == \"http://www.example.com/counter/123?foo=bar\"\n    end\n\n    test \"telemetry events are emitted on exception\", %{conn: conn} do\n      attach_telemetry([:phoenix, :live_view, :handle_params])\n\n      assert catch_exit(live(conn, \"/errors?crash_on=connected_handle_params\"))\n\n      assert_receive {:event, [:phoenix, :live_view, :handle_params, :start], %{system_time: _},\n                      %{socket: %Phoenix.LiveView.Socket{transport_pid: pid}}}\n                     when is_pid(pid)\n\n      assert_receive {:event, [:phoenix, :live_view, :handle_params, :exception], %{duration: _},\n                      %{socket: %Phoenix.LiveView.Socket{transport_pid: pid}}}\n                     when is_pid(pid)\n    end\n\n    test \"hard redirects\", %{conn: conn} do\n      {:error, {:redirect, %{to: \"/thermo/456\"}}} =\n        conn\n        |> put_serialized_session(:on_handle_params, fn socket ->\n          if LiveView.connected?(socket) do\n            {:noreply, LiveView.redirect(socket, to: \"/thermo/456\")}\n          else\n            {:noreply, socket}\n          end\n        end)\n        |> get(\"/counter/123?from=handle_params\")\n        |> live()\n    end\n\n    test \"push_patch\", %{conn: conn} do\n      {:ok, counter_live, _html} =\n        conn\n        |> put_serialized_session(:on_handle_params, fn socket ->\n          if LiveView.connected?(socket) do\n            {:noreply, LiveView.push_patch(socket, to: \"/counter/123?from=rehandled_params\")}\n          else\n            {:noreply, socket}\n          end\n        end)\n        |> get(\"/counter/123?from=handle_params\")\n        |> live()\n\n      response = render(counter_live)\n\n      assert response =~\n               rendered_to_string(~s|params: %{\"from\" => \"rehandled_params\", \"id\" => \"123\"}|)\n\n      assert response =~\n               rendered_to_string(~s|mount: %{\"from\" => \"handle_params\", \"id\" => \"123\"}|)\n    end\n\n    test \"push_navigate\", %{conn: conn} do\n      {:error, {:live_redirect, %{to: \"/thermo/456\"}}} =\n        conn\n        |> put_serialized_session(:on_handle_params, fn socket ->\n          if LiveView.connected?(socket) do\n            {:noreply, LiveView.push_navigate(socket, to: \"/thermo/456\")}\n          else\n            {:noreply, socket}\n          end\n        end)\n        |> get(\"/counter/123?from=handle_params\")\n        |> live()\n    end\n\n    test \"with encoded URL\", %{conn: conn} do\n      {:ok, _counter_live, _html} = live(conn, \"/counter/Wm9uZTozNzYxOA%3D%3D?foo=bar+15%26\")\n\n      assert_receive {:handle_params, _uri, %{connected?: true},\n                      %{\"id\" => \"Wm9uZTozNzYxOA==\", \"foo\" => \"bar 15&\"}}\n    end\n  end\n\n  describe \"live_link\" do\n    test \"renders static container\", %{conn: conn} do\n      container =\n        conn\n        |> get(\"/counter/123\", query1: \"query1\", query2: \"query2\")\n        |> html_response(200)\n        |> TreeDOM.normalize_to_tree()\n        |> hd()\n\n      assert {\n               \"div\",\n               [\n                 {\"id\", \"phx-\" <> _},\n                 {\"data-phx-main\", _},\n                 {\"data-phx-session\", _},\n                 {\"data-phx-static\", _}\n               ],\n               [{\"p\", [], [\"The value is: 1\"]} | _]\n             } = container\n    end\n\n    test \"invokes handle_params\", %{conn: conn} do\n      {:ok, counter_live, _html} = live(conn, \"/counter/123\")\n\n      assert render_patch(counter_live, \"/counter/123?filter=true\") =~\n               rendered_to_string(~s|%{\"filter\" => \"true\", \"id\" => \"123\"}|)\n    end\n\n    test \"with encoded URL\", %{conn: conn} do\n      {:ok, counter_live, _html} = live(conn, \"/counter/123\")\n\n      assert render_patch(counter_live, \"/counter/Wm9uZTozNzYxOa%3d%3d?foo=bar+15%26\") =~\n               rendered_to_string(~s|%{\"foo\" => \"bar 15&\", \"id\" => \"Wm9uZTozNzYxOa==\"}|)\n    end\n  end\n\n  describe \"push_patch\" do\n    test \"from event callback ack\", %{conn: conn} do\n      {:ok, counter_live, _html} = live(conn, \"/counter/123\")\n\n      assert render_click(counter_live, :push_patch, %{to: \"/counter/123?from=event_ack\"}) =~\n               rendered_to_string(~s|%{\"from\" => \"event_ack\", \"id\" => \"123\"}|)\n\n      assert_patch(counter_live, \"/counter/123?from=event_ack\")\n    end\n\n    test \"from handle_info\", %{conn: conn} do\n      {:ok, counter_live, _html} = live(conn, \"/counter/123\")\n\n      send(counter_live.pid, {:push_patch, \"/counter/123?from=handle_info\"})\n\n      assert render(counter_live) =~\n               rendered_to_string(~s|%{\"from\" => \"handle_info\", \"id\" => \"123\"}|)\n    end\n\n    test \"from handle_cast\", %{conn: conn} do\n      {:ok, counter_live, _html} = live(conn, \"/counter/123\")\n\n      :ok = GenServer.cast(counter_live.pid, {:push_patch, \"/counter/123?from=handle_cast\"})\n\n      assert render(counter_live) =~\n               rendered_to_string(~s|%{\"from\" => \"handle_cast\", \"id\" => \"123\"}|)\n    end\n\n    test \"from handle_call\", %{conn: conn} do\n      {:ok, counter_live, _html} = live(conn, \"/counter/123\")\n\n      next = fn socket ->\n        {:reply, :ok, LiveView.push_patch(socket, to: \"/counter/123?from=handle_call\")}\n      end\n\n      :ok = GenServer.call(counter_live.pid, {:push_patch, next})\n\n      assert render(counter_live) =~\n               rendered_to_string(~s|%{\"from\" => \"handle_call\", \"id\" => \"123\"}|)\n    end\n\n    test \"from handle_params\", %{conn: conn} do\n      {:ok, counter_live, _html} = live(conn, \"/counter/123\")\n\n      next = fn socket ->\n        send(self(), {:set, :val, 1000})\n\n        new_socket =\n          Component.assign(socket, :on_handle_params, fn socket ->\n            {:noreply, LiveView.push_patch(socket, to: \"/counter/123?from=rehandled_params\")}\n          end)\n\n        {:reply, :ok, LiveView.push_patch(new_socket, to: \"/counter/123?from=handle_params\")}\n      end\n\n      :ok = GenServer.call(counter_live.pid, {:push_patch, next})\n\n      html = render(counter_live)\n      assert html =~ rendered_to_string(~s|%{\"from\" => \"rehandled_params\", \"id\" => \"123\"}|)\n      assert html =~ \"The value is: 1000\"\n\n      assert_receive {:handle_params, \"http://www.example.com/counter/123?from=rehandled_params\",\n                      %{val: 1}, %{\"from\" => \"rehandled_params\", \"id\" => \"123\"}}\n    end\n\n    test \"remove fragment from query\", %{conn: conn} do\n      {:ok, counter_live, _html} = live(conn, \"/counter/123\")\n\n      send(counter_live.pid, {:push_patch, \"/counter/123?query=value#fragment\"})\n      assert render(counter_live) =~ rendered_to_string(~s|%{\"id\" => \"123\", \"query\" => \"value\"}|)\n    end\n  end\n\n  describe \"push_navigate\" do\n    test \"from event callback\", %{conn: conn} do\n      {:ok, counter_live, _html} = live(conn, \"/counter/123\")\n\n      assert {:error, {:live_redirect, %{to: \"/thermo/123\"}}} =\n               render_click(counter_live, :push_navigate, %{to: \"/thermo/123\"})\n\n      assert_redirect(counter_live, \"/thermo/123\")\n    end\n\n    test \"from handle_params\", %{conn: conn} do\n      {:ok, counter_live, _html} = live(conn, \"/counter/123\")\n\n      next = fn socket ->\n        new_socket =\n          Component.assign(socket, :on_handle_params, fn socket ->\n            {:noreply, LiveView.push_navigate(socket, to: \"/thermo/123\")}\n          end)\n\n        {:reply, :ok, LiveView.push_patch(new_socket, to: \"/counter/123?from=handle_params\")}\n      end\n\n      :ok = GenServer.call(counter_live.pid, {:push_patch, next})\n\n      assert_receive {:handle_params, \"http://www.example.com/counter/123?from=handle_params\",\n                      %{val: 1}, %{\"from\" => \"handle_params\", \"id\" => \"123\"}}\n    end\n\n    test \"shuts down with push_navigate\", %{conn: conn} do\n      {:ok, counter_live, _html} = live(conn, \"/counter/123\")\n\n      next = fn socket ->\n        {:noreply, LiveView.push_navigate(socket, to: \"/thermo/123\")}\n      end\n\n      assert {{:shutdown, {:live_redirect, %{to: \"/thermo/123\"}}}, _} =\n               catch_exit(GenServer.call(counter_live.pid, {:push_navigate, next}))\n    end\n  end\n\n  describe \"@live_action\" do\n    test \"when initially set to nil\", %{conn: conn} do\n      {:ok, live, html} = live(conn, \"/action\")\n      assert html =~ \"Live action: nil\"\n      assert html =~ \"Mount action: nil\"\n      assert html =~ \"Params: %{}\"\n\n      html = render_patch(live, \"/action/index\")\n      assert html =~ \"Live action: :index\"\n      assert html =~ \"Mount action: nil\"\n      assert html =~ \"Params: %{}\"\n\n      html = render_patch(live, \"/action/1/edit\")\n      assert html =~ \"Live action: :edit\"\n      assert html =~ \"Mount action: nil\"\n      assert html =~ \"Params: %{&quot;id&quot; =&gt; &quot;1&quot;}\"\n    end\n\n    test \"when initially set to action\", %{conn: conn} do\n      {:ok, live, html} = live(conn, \"/action/index\")\n      assert html =~ \"Live action: :index\"\n      assert html =~ \"Mount action: :index\"\n      assert html =~ \"Params: %{}\"\n\n      html = render_patch(live, \"/action\")\n      assert html =~ \"Live action: nil\"\n      assert html =~ \"Mount action: :index\"\n      assert html =~ \"Params: %{}\"\n\n      html = render_patch(live, \"/action/1/edit\")\n      assert html =~ \"Live action: :edit\"\n      assert html =~ \"Mount action: :index\"\n      assert html =~ \"Params: %{&quot;id&quot; =&gt; &quot;1&quot;}\"\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/start_async_test.exs",
    "content": "defmodule Phoenix.LiveView.StartAsyncTest do\n  use ExUnit.Case, async: true\n  import Phoenix.ConnTest\n\n  import Phoenix.LiveViewTest\n  import Phoenix.LiveViewTest.Support.AsyncSync\n  alias Phoenix.LiveViewTest.Support.Endpoint\n\n  @endpoint Endpoint\n\n  setup do\n    Process.flag(:trap_exit, true)\n    {:ok, conn: Plug.Test.init_test_session(Phoenix.ConnTest.build_conn(), %{})}\n  end\n\n  describe \"LiveView start_async\" do\n    test \"ok task\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/start_async?test=ok\")\n\n      assert render_async(lv) =~ \"result: :good\"\n    end\n\n    test \"raise during execution\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/start_async?test=raise\")\n\n      assert render_async(lv) =~ \"result: {:exit, %RuntimeError{message: &quot;boom&quot;}}\"\n      assert render(lv)\n    end\n\n    test \"exit during execution\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/start_async?test=exit\")\n\n      assert render_async(lv) =~ \"result: {:exit, :boom}\"\n      assert render(lv)\n    end\n\n    test \"lv exit brings down asyncs\", %{conn: conn} do\n      Process.register(self(), :start_async_test_process)\n      {:ok, lv, _html} = live(conn, \"/start_async?test=lv_exit\")\n      lv_ref = Process.monitor(lv.pid)\n\n      async_ref = wait_for_async_ready_and_monitor(:start_async_exit)\n      send(lv.pid, :boom)\n\n      assert_receive {:DOWN, ^lv_ref, :process, _pid, :boom}, 1000\n      assert_receive {:DOWN, ^async_ref, :process, _pid, :boom}, 1000\n    end\n\n    test \"cancel_async\", %{conn: conn} do\n      Process.register(self(), :start_async_test_process)\n      {:ok, lv, _html} = live(conn, \"/start_async?test=cancel\")\n\n      async_ref = wait_for_async_ready_and_monitor(:start_async_cancel)\n\n      assert render(lv) =~ \"result: :loading\"\n\n      send(lv.pid, :cancel)\n\n      assert_receive {:DOWN, ^async_ref, :process, _pid, {:shutdown, :cancel}}, 1000\n\n      assert render(lv) =~ \"result: {:exit, {:shutdown, :cancel}}\"\n\n      send(lv.pid, :renew_canceled)\n\n      assert render(lv) =~ \"result: :loading\"\n      assert render_async(lv, 200) =~ \"result: :renewed\"\n    end\n\n    test \"trapping exits\", %{conn: conn} do\n      Process.register(self(), :start_async_trap_exit_test)\n      {:ok, lv, _html} = live(conn, \"/start_async?test=trap_exit\")\n\n      assert render_async(lv, 200) =~ \"{:exit, :boom}\"\n      assert render(lv)\n      assert_receive {:exit, _pid, :boom}, 1000\n    end\n\n    test \"does not leak normal task exit to handle_info when trapping exits\", %{conn: conn} do\n      {:ok, lv, _html} =\n        live_isolated(conn, Phoenix.LiveViewTest.Support.StartAsyncLive.TrapExitLeak)\n\n      # The LiveView deliberately does not handle exit messages,\n      # so we'd expect it to crash if the exit leaks\n      assert render_async(lv) =~ \"complete\"\n    end\n\n    test \"complex key task\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/start_async?test=complex_key\")\n\n      assert render_async(lv) =~ \"result: :complex_key\"\n    end\n\n    test \"navigate\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/start_async?test=navigate\")\n\n      assert_redirect(lv, \"/start_async?test=ok\")\n    end\n\n    test \"patch\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/start_async?test=patch\")\n\n      assert_patch(lv, \"/start_async?test=ok\")\n    end\n\n    test \"redirect\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/start_async?test=redirect\")\n\n      assert_redirect(lv, \"/not_found\")\n    end\n\n    test \"put_flash\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/start_async?test=put_flash\")\n\n      assert render_async(lv) =~ \"flash: hello\"\n    end\n  end\n\n  describe \"LiveComponent start_async\" do\n    test \"ok task\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/start_async?test=lc_ok\")\n\n      assert render_async(lv) =~ \"lc: :good\"\n    end\n\n    test \"raise during execution\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/start_async?test=lc_raise\")\n\n      assert render_async(lv) =~ \"lc: {:exit, %RuntimeError{message: &quot;boom&quot;}}\"\n      assert render(lv)\n    end\n\n    test \"exit during execution\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/start_async?test=lc_exit\")\n\n      assert render_async(lv) =~ \"lc: {:exit, :boom}\"\n      assert render(lv)\n    end\n\n    test \"lv exit brings down asyncs\", %{conn: conn} do\n      Process.register(self(), :start_async_test_process)\n      {:ok, lv, _html} = live(conn, \"/start_async?test=lc_lv_exit\")\n      lv_ref = Process.monitor(lv.pid)\n\n      async_ref = wait_for_async_ready_and_monitor(:start_async_exit)\n      send(lv.pid, :boom)\n\n      assert_receive {:DOWN, ^lv_ref, :process, _pid, :boom}, 1000\n      assert_receive {:DOWN, ^async_ref, :process, _pid, :boom}, 1000\n    end\n\n    test \"cancel_async\", %{conn: conn} do\n      Process.register(self(), :start_async_test_process)\n      {:ok, lv, _html} = live(conn, \"/start_async?test=lc_cancel\")\n\n      async_ref = wait_for_async_ready_and_monitor(:start_async_cancel)\n\n      Phoenix.LiveView.send_update(lv.pid, Phoenix.LiveViewTest.Support.StartAsyncLive.LC,\n        id: \"lc\",\n        action: :cancel\n      )\n\n      assert_receive {:DOWN, ^async_ref, :process, _pid, {:shutdown, :cancel}}\n\n      assert render(lv) =~ \"lc: {:exit, {:shutdown, :cancel}}\"\n\n      Phoenix.LiveView.send_update(lv.pid, Phoenix.LiveViewTest.Support.StartAsyncLive.LC,\n        id: \"lc\",\n        action: :renew_canceled\n      )\n\n      assert render(lv) =~ \"lc: :loading\"\n      assert render_async(lv, 200) =~ \"lc: :renewed\"\n    end\n\n    test \"complex key task\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/start_async?test=lc_complex_key\")\n\n      assert render_async(lv) =~ \"lc: :complex_key\"\n    end\n\n    test \"navigate\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/start_async?test=lc_navigate\")\n\n      assert_redirect(lv, \"/start_async?test=ok\")\n    end\n\n    test \"patch\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/start_async?test=lc_patch\")\n\n      assert_patch(lv, \"/start_async?test=ok\")\n    end\n\n    test \"redirect\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/start_async?test=lc_redirect\")\n\n      assert_redirect(lv, \"/not_found\")\n    end\n\n    test \"navigate with flash\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/start_async?test=lc_navigate_flash\")\n\n      flash = assert_redirect(lv, \"/start_async?test=ok\")\n      assert %{\"info\" => \"hello\"} = flash\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/stream_async_test.exs",
    "content": "defmodule Phoenix.LiveView.StreamAsyncTest do\n  use ExUnit.Case, async: true\n  import Phoenix.ConnTest\n\n  import Phoenix.LiveViewTest\n  import Phoenix.LiveViewTest.Support.AsyncSync\n  alias Phoenix.LiveViewTest.Support.Endpoint\n\n  @endpoint Endpoint\n\n  setup do\n    Process.flag(:trap_exit, true)\n    {:ok, conn: Plug.Test.init_test_session(Phoenix.ConnTest.build_conn(), %{})}\n  end\n\n  describe \"LiveView stream_async\" do\n    test \"bad return\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/stream_async?test=bad_return\")\n\n      assert render_async(lv) =~\n               \"expected stream_async to return {:ok, Enumerable.t()} or {:error, reason}, got: 123\"\n    end\n\n    test \"not enumerable\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/stream_async?test=bad_ok\")\n\n      assert render_async(lv) =~\n               \"does not implement the Enumerable protocol\"\n    end\n\n    test \"valid return\", %{conn: conn} do\n      {:ok, lv, html} = live(conn, \"/stream_async?test=ok\")\n      # Initial value is already in stream\n      assert html =~ \"Initial\"\n      assert html =~ \"my_stream loading...\"\n\n      rendered = render_async(lv)\n      assert rendered =~ \"First\"\n      assert rendered =~ \"Second\"\n      refute rendered =~ \"loading...\"\n\n      lazy = LazyHTML.from_fragment(rendered)\n\n      # assert the correct order\n      assert [\n               {\"li\", [{\"id\", \"my_stream-0\"}, _], [\"Initial\"]},\n               {\"li\", [{\"id\", \"my_stream-1\"}, _], [\"First\"]},\n               {\"li\", [{\"id\", \"my_stream-2\"}, _], [\"Second\"]}\n             ] = LazyHTML.query(lazy, \"li\") |> LazyHTML.to_tree()\n    end\n\n    test \"valid return with opts\", %{conn: conn} do\n      {:ok, lv, html} = live(conn, \"/stream_async?test=ok_with_opts\")\n      # Initial value is already in stream\n      assert html =~ \"Initial\"\n\n      rendered = render_async(lv)\n      lazy = LazyHTML.from_fragment(rendered)\n\n      # assert the correct order\n      assert [\n               {\"li\", [{\"id\", \"my_stream-2\"}, _], [\"Second\"]},\n               {\"li\", [{\"id\", \"my_stream-1\"}, _], [\"First\"]},\n               {\"li\", [{\"id\", \"my_stream-0\"}, _], [\"Initial\"]}\n             ] = LazyHTML.query(lazy, \"li\") |> LazyHTML.to_tree()\n    end\n\n    test \"valid return with reset\", %{conn: conn} do\n      {:ok, lv, html} = live(conn, \"/stream_async?test=ok_with_reset\")\n      # Initial value is already in stream\n      assert html =~ \"Initial\"\n\n      rendered = render_async(lv)\n      assert rendered =~ \"First\"\n      assert rendered =~ \"Second\"\n      # Initial value is reset\n      refute rendered =~ \"Initial\"\n    end\n\n    test \"error return\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/stream_async?test=error\")\n\n      assert render_async(lv) =~ \"error: :something_wrong\"\n    end\n\n    test \"raise during execution\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/stream_async?test=raise\")\n\n      assert render_async(lv) =~ \"exit:\"\n      assert render_async(lv) =~ \"RuntimeError\"\n      assert render_async(lv) =~ \"boom\"\n    end\n\n    test \"exit during execution\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/stream_async?test=exit\")\n\n      assert render_async(lv) =~ \"exit: :boom\"\n    end\n\n    test \"lv exit brings down asyncs\", %{conn: conn} do\n      Process.register(self(), :stream_async_test_process)\n      {:ok, lv, _html} = live(conn, \"/stream_async?test=lv_exit\")\n      lv_ref = Process.monitor(lv.pid)\n\n      async_ref = wait_for_async_ready_and_monitor(:stream_async_exit)\n      send(lv.pid, :boom)\n\n      assert_receive {:DOWN, ^lv_ref, :process, _pid, :boom}, 1000\n      assert_receive {:DOWN, ^async_ref, :process, _pid, :boom}, 1000\n    end\n\n    test \"cancel_async\", %{conn: conn} do\n      Process.register(self(), :stream_async_test_process)\n      {:ok, lv, _html} = live(conn, \"/stream_async?test=cancel\")\n\n      async_ref = wait_for_async_ready_and_monitor(:cancel_stream)\n      send(lv.pid, :cancel)\n      assert_receive {:DOWN, ^async_ref, :process, _pid, {:shutdown, :cancel}}, 1000\n\n      send(lv.pid, :renew_canceled)\n\n      assert render(lv) =~ \"my_stream loading...\"\n      assert render_async(lv, 200) =~ \"renewed\"\n    end\n\n    test \"reset option does not clear stream during loading\", %{conn: conn} do\n      {:ok, lv, html} = live(conn, \"/stream_async?test=reset_option\")\n      # Initial value is already in stream\n      assert html =~ \"Initial\"\n      assert render(lv) =~ \"my_stream loading...\"\n\n      rendered = render_async(lv)\n      assert rendered =~ \"First\"\n      # Reset only changed the loading state, not the stream itself\n      assert rendered =~ \"Initial\"\n    end\n\n    test \"multiple stream_async calls\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/stream_async?test=ok\")\n\n      # Wait for first load\n      assert render_async(lv) =~ \"Second\"\n\n      # Add more items\n      send(lv.pid, :add_items)\n      rendered = render_async(lv)\n      assert rendered =~ \"Third\"\n      assert rendered =~ \"Fourth\"\n\n      # Should still have original items\n      assert render(lv) =~ \"First\"\n      assert render(lv) =~ \"Second\"\n    end\n\n    test \"stream_async with reset in return\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/stream_async?test=ok\")\n\n      # Wait for first load\n      assert render_async(lv) =~ \"Second\"\n\n      # Reset with new items\n      send(lv.pid, :reset_items)\n      rendered = render_async(lv)\n\n      assert rendered =~ \"Fifth\"\n      assert rendered =~ \"Sixth\"\n      refute rendered =~ \"First\"\n      refute rendered =~ \"Second\"\n    end\n\n    test \"stream is available (empty) before async finishes\", %{conn: conn} do\n      {:ok, lv, html} = live(conn, \"/stream_async?test=ok&no_init=true\")\n\n      refute html =~ \"Initial\"\n\n      rendered = render_async(lv)\n      lazy = LazyHTML.from_fragment(rendered)\n\n      assert [\n               {\"li\", [{\"id\", \"my_stream-1\"}, _], [\"First\"]},\n               {\"li\", [{\"id\", \"my_stream-2\"}, _], [\"Second\"]}\n             ] = LazyHTML.query(lazy, \"li\") |> LazyHTML.to_tree()\n    end\n  end\n\n  describe \"LiveComponent stream_async\" do\n    test \"bad return\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/stream_async?test=lc_bad_return\")\n\n      assert render_async(lv) =~\n               \"expected stream_async to return {:ok, Enumerable.t()} or {:error, reason}, got: 123\"\n    end\n\n    test \"not enumerable\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/stream_async?test=lc_bad_ok\")\n\n      assert render_async(lv) =~\n               \"does not implement the Enumerable protocol\"\n    end\n\n    test \"valid return\", %{conn: conn} do\n      {:ok, lv, html} = live(conn, \"/stream_async?test=lc_ok\")\n      assert html =~ \"lc_stream loading...\"\n\n      rendered = render_async(lv)\n      assert rendered =~ \"LC First\"\n      assert rendered =~ \"LC Second\"\n    end\n\n    test \"raise during execution\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/stream_async?test=lc_raise\")\n\n      assert render_async(lv) =~ \"exit:\"\n      assert render_async(lv) =~ \"RuntimeError\"\n      assert render_async(lv) =~ \"boom\"\n    end\n\n    test \"exit during execution\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/stream_async?test=lc_exit\")\n\n      assert render_async(lv) =~ \"exit: :boom\"\n    end\n\n    test \"lv exit brings down asyncs\", %{conn: conn} do\n      Process.register(self(), :stream_async_test_process)\n      {:ok, lv, _html} = live(conn, \"/stream_async?test=lc_lv_exit\")\n      lv_ref = Process.monitor(lv.pid)\n\n      async_ref = wait_for_async_ready_and_monitor(:lc_stream_exit)\n      send(lv.pid, :boom)\n\n      assert_receive {:DOWN, ^lv_ref, :process, _pid, :boom}, 1000\n      assert_receive {:DOWN, ^async_ref, :process, _pid, :boom}, 1000\n    end\n\n    test \"cancel_async\", %{conn: conn} do\n      Process.register(self(), :stream_async_test_process)\n      {:ok, lv, _html} = live(conn, \"/stream_async?test=lc_cancel\")\n\n      async_ref = wait_for_async_ready_and_monitor(:lc_stream_cancel)\n\n      # Send cancel to the LiveView, which will forward to the component\n      send(lv.pid, {:cancel_lc, \"lc\"})\n\n      assert_receive {:DOWN, ^async_ref, :process, _pid, {:shutdown, :cancel}}, 1000\n\n      refute render(lv) =~ \"LC First\"\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/stream_test.exs",
    "content": "defmodule Phoenix.LiveView.StreamTest do\n  use ExUnit.Case, async: true\n\n  import Phoenix.ConnTest\n  import Phoenix.LiveViewTest\n\n  alias Phoenix.LiveViewTest.{DOM, TreeDOM}\n  alias Phoenix.LiveViewTest.Support.{StreamLive, Endpoint}\n\n  @endpoint Endpoint\n\n  setup do\n    {:ok, conn: Plug.Test.init_test_session(build_conn(), %{})}\n  end\n\n  test \"stream is pruned after render\", %{conn: conn} do\n    {:ok, lv, html} = live(conn, \"/stream\")\n\n    users = [{\"users-1\", \"chris\"}, {\"users-2\", \"callan\"}]\n\n    for {id, name} <- users do\n      assert html =~ ~s|id=\"#{id}\"|\n      assert html =~ name\n    end\n\n    stream = StreamLive.run(lv, fn socket -> {:reply, socket.assigns.streams.users, socket} end)\n\n    assert stream.inserts == []\n    assert stream.deletes == []\n\n    assert lv |> render() |> users_in_dom(\"users\") == [\n             {\"users-1\", \"chris\"},\n             {\"users-2\", \"callan\"}\n           ]\n\n    assert lv\n           |> element(~S|#users-1 button[phx-click=\"update\"]|)\n           |> render_click()\n           |> users_in_dom(\"users\") ==\n             [{\"users-1\", \"updated\"}, {\"users-2\", \"callan\"}]\n\n    assert_pruned_stream(lv)\n\n    assert lv\n           |> element(~S|#users-2 button[phx-click=\"move-to-first\"]|)\n           |> render_click()\n           |> users_in_dom(\"users\") ==\n             [{\"users-2\", \"updated\"}, {\"users-1\", \"updated\"}]\n\n    assert lv\n           |> element(~S|#users-2 button[phx-click=\"move-to-last\"]|)\n           |> render_click()\n           |> users_in_dom(\"users\") ==\n             [{\"users-1\", \"updated\"}, {\"users-2\", \"updated\"}]\n\n    assert lv\n           |> element(~S|#users-1 button[phx-click=\"delete\"]|)\n           |> render_click()\n           |> users_in_dom(\"users\") ==\n             [{\"users-2\", \"updated\"}]\n\n    assert_pruned_stream(lv)\n\n    # second stream in LiveView\n    assert lv |> render() |> users_in_dom(\"admins\") == [\n             {\"admins-1\", \"chris-admin\"},\n             {\"admins-2\", \"callan-admin\"}\n           ]\n\n    assert lv\n           |> element(~S|#admins-1 button[phx-click=\"admin-update\"]|)\n           |> render_click()\n           |> users_in_dom(\"admins\") ==\n             [{\"admins-1\", \"updated\"}, {\"admins-2\", \"callan-admin\"}]\n\n    assert_pruned_stream(lv)\n\n    assert lv\n           |> element(~S|#admins-2 button[phx-click=\"admin-move-to-first\"]|)\n           |> render_click()\n           |> users_in_dom(\"admins\") ==\n             [{\"admins-2\", \"updated\"}, {\"admins-1\", \"updated\"}]\n\n    assert lv\n           |> element(~S|#admins-2 button[phx-click=\"admin-move-to-last\"]|)\n           |> render_click()\n           |> users_in_dom(\"admins\") ==\n             [{\"admins-1\", \"updated\"}, {\"admins-2\", \"updated\"}]\n\n    assert lv\n           |> element(~S|#admins-1 button[phx-click=\"admin-delete\"]|)\n           |> render_click()\n           |> users_in_dom(\"admins\") ==\n             [{\"admins-2\", \"updated\"}]\n\n    # resets\n\n    assert lv |> render() |> users_in_dom(\"users\") == [{\"users-2\", \"updated\"}]\n\n    StreamLive.run(lv, fn socket ->\n      {:reply, :ok, Phoenix.LiveView.stream(socket, :users, [], reset: true)}\n    end)\n\n    assert lv |> render() |> users_in_dom(\"users\") == []\n    assert lv |> render() |> users_in_dom(\"admins\") == [{\"admins-2\", \"updated\"}]\n  end\n\n  test \"should properly reset after a stream has been set after mount\", %{conn: conn} do\n    {:ok, lv, _} = live(conn, \"/stream\")\n    assert lv |> element(\"#users div\") |> has_element?()\n\n    lv |> render_hook(\"reset-users\", %{})\n    refute lv |> element(\"#users div\") |> has_element?()\n\n    lv |> render_hook(\"stream-users\", %{})\n    assert lv |> element(\"#users div\") |> has_element?()\n\n    lv |> render_hook(\"reset-users\", %{})\n    refute lv |> element(\"#users div\") |> has_element?()\n  end\n\n  test \"should preserve the order of appended items\", %{conn: conn} do\n    {:ok, lv, _} = live(conn, \"/stream\")\n    assert lv |> element(\"#users div:last-child\") |> render =~ \"callan\"\n\n    lv |> render_hook(\"append-users\", %{})\n    assert lv |> element(\"#users div:last-child\") |> render =~ \"last_user\"\n  end\n\n  test \"properly orders elements on reset\", %{conn: conn} do\n    {:ok, lv, _} = live(conn, \"/stream\")\n\n    assert lv |> render() |> users_in_dom(\"users\") == [\n             {\"users-1\", \"chris\"},\n             {\"users-2\", \"callan\"}\n           ]\n\n    lv |> render_hook(\"reset-users-reorder\", %{})\n\n    assert lv |> render() |> users_in_dom(\"users\") == [\n             {\"users-3\", \"peter\"},\n             {\"users-1\", \"chris\"},\n             {\"users-4\", \"mona\"}\n           ]\n  end\n\n  test \"updates attributes on reset\", %{conn: conn} do\n    {:ok, lv, _} = live(conn, \"/stream\")\n\n    assert lv |> render() |> users_in_dom(\"users\") == [\n             {\"users-1\", \"chris\"},\n             {\"users-2\", \"callan\"}\n           ]\n\n    html = render(lv)\n    tree = TreeDOM.normalize_to_tree(html)\n    assert TreeDOM.by_id!(tree, \"users-1\") |> TreeDOM.attribute(\"data-count\") == \"0\"\n    assert TreeDOM.by_id!(tree, \"users-2\") |> TreeDOM.attribute(\"data-count\") == \"0\"\n\n    lv |> render_hook(\"reset-users-reorder\", %{})\n\n    assert lv |> render() |> users_in_dom(\"users\") == [\n             {\"users-3\", \"peter\"},\n             {\"users-1\", \"chris\"},\n             {\"users-4\", \"mona\"}\n           ]\n\n    html = render(lv)\n    tree = TreeDOM.normalize_to_tree(html)\n    assert TreeDOM.by_id!(tree, \"users-1\") |> TreeDOM.attribute(\"data-count\") == \"1\"\n    assert TreeDOM.by_id!(tree, \"users-3\") |> TreeDOM.attribute(\"data-count\") == \"1\"\n    assert TreeDOM.by_id!(tree, \"users-4\") |> TreeDOM.attribute(\"data-count\") == \"1\"\n  end\n\n  test \"stream reset on patch\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/healthy/fruits\")\n\n    assert has_element?(lv, \"h1\", \"Fruits\")\n\n    assert has_element?(lv, \"li\", \"Apples\")\n    assert has_element?(lv, \"li\", \"Oranges\")\n\n    lv\n    |> element(\"a\", \"Switch\")\n    |> render_click()\n\n    assert_patched(lv, \"/healthy/veggies\")\n\n    assert has_element?(lv, \"h1\", \"Veggies\")\n\n    assert has_element?(lv, \"li\", \"Carrots\")\n    assert has_element?(lv, \"li\", \"Tomatoes\")\n\n    refute has_element?(lv, \"li\", \"Apples\")\n    refute has_element?(lv, \"li\", \"Oranges\")\n\n    lv\n    |> element(\"a\", \"Switch\")\n    |> render_click()\n\n    assert_patched(lv, \"/healthy/fruits\")\n\n    assert has_element?(lv, \"h1\", \"Fruits\")\n\n    refute has_element?(lv, \"li\", \"Carrots\")\n    refute has_element?(lv, \"li\", \"Tomatoes\")\n\n    assert has_element?(lv, \"li\", \"Apples\")\n    assert has_element?(lv, \"li\", \"Oranges\")\n  end\n\n  describe \"within nested lv\" do\n    test \"does not clear stream when parent updates\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/stream/nested\")\n      lv = find_live_child(lv, \"nested\")\n\n      # let the parent update\n      Process.sleep(100)\n\n      assert ul_list_children(render(lv)) == [\n               {\"items-a\", \"A\"},\n               {\"items-b\", \"B\"},\n               {\"items-c\", \"C\"},\n               {\"items-d\", \"D\"}\n             ]\n\n      html = assert lv |> element(\"button\", \"Filter\") |> render_click()\n\n      assert ul_list_children(html) == [\n               {\"items-b\", \"B\"},\n               {\"items-c\", \"C\"},\n               {\"items-d\", \"D\"}\n             ]\n\n      html = assert lv |> element(\"button\", \"Reset\") |> render_click()\n\n      assert ul_list_children(html) == [\n               {\"items-a\", \"A\"},\n               {\"items-b\", \"B\"},\n               {\"items-c\", \"C\"},\n               {\"items-d\", \"D\"}\n             ]\n    end\n  end\n\n  describe \"issue #2994\" do\n    test \"can filter and reset a stream\", %{conn: conn} do\n      {:ok, lv, html} = live(conn, \"/stream/reset\")\n\n      assert ul_list_children(html) == [\n               {\"items-a\", \"A\"},\n               {\"items-b\", \"B\"},\n               {\"items-c\", \"C\"},\n               {\"items-d\", \"D\"}\n             ]\n\n      html = assert lv |> element(\"button\", \"Filter\") |> render_click()\n\n      assert ul_list_children(html) == [\n               {\"items-b\", \"B\"},\n               {\"items-c\", \"C\"},\n               {\"items-d\", \"D\"}\n             ]\n\n      html = assert lv |> element(\"button\", \"Reset\") |> render_click()\n\n      assert ul_list_children(html) == [\n               {\"items-a\", \"A\"},\n               {\"items-b\", \"B\"},\n               {\"items-c\", \"C\"},\n               {\"items-d\", \"D\"}\n             ]\n    end\n\n    test \"can reorder stream\", %{conn: conn} do\n      {:ok, lv, html} = live(conn, \"/stream/reset\")\n\n      assert ul_list_children(html) == [\n               {\"items-a\", \"A\"},\n               {\"items-b\", \"B\"},\n               {\"items-c\", \"C\"},\n               {\"items-d\", \"D\"}\n             ]\n\n      html = assert lv |> element(\"button\", \"Reorder\") |> render_click()\n\n      assert ul_list_children(html) == [\n               {\"items-b\", \"B\"},\n               {\"items-a\", \"A\"},\n               {\"items-c\", \"C\"},\n               {\"items-d\", \"D\"}\n             ]\n    end\n\n    test \"can filter and then prepend / append stream\", %{conn: conn} do\n      {:ok, lv, html} = live(conn, \"/stream/reset\")\n\n      assert ul_list_children(html) == [\n               {\"items-a\", \"A\"},\n               {\"items-b\", \"B\"},\n               {\"items-c\", \"C\"},\n               {\"items-d\", \"D\"}\n             ]\n\n      html = assert lv |> element(\"button\", \"Filter\") |> render_click()\n\n      assert ul_list_children(html) == [\n               {\"items-b\", \"B\"},\n               {\"items-c\", \"C\"},\n               {\"items-d\", \"D\"}\n             ]\n\n      html = assert lv |> element(~s(button[phx-click=\"prepend\"]), \"Prepend\") |> render_click()\n\n      assert [\n               {<<\"items-a-\", _::binary>>, _},\n               {\"items-b\", \"B\"},\n               {\"items-c\", \"C\"},\n               {\"items-d\", \"D\"}\n             ] = ul_list_children(html)\n\n      html = assert lv |> element(\"button\", \"Reset\") |> render_click()\n\n      assert ul_list_children(html) == [\n               {\"items-a\", \"A\"},\n               {\"items-b\", \"B\"},\n               {\"items-c\", \"C\"},\n               {\"items-d\", \"D\"}\n             ]\n\n      html = assert lv |> element(~s(button[phx-click=\"append\"]), \"Append\") |> render_click()\n\n      assert [\n               {\"items-a\", \"A\"},\n               {\"items-b\", \"B\"},\n               {\"items-c\", \"C\"},\n               {\"items-d\", \"D\"},\n               {<<\"items-a-\", _::binary>>, _}\n             ] =\n               ul_list_children(html)\n    end\n  end\n\n  describe \"within live component\" do\n    test \"stream operations\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/stream\")\n\n      assert lv |> render() |> users_in_dom(\"c_users\") == [\n               {\"c_users-1\", \"chris\"},\n               {\"c_users-2\", \"callan\"}\n             ]\n\n      assert lv\n             |> element(~S|#c_users-1 button[phx-click=\"update\"]|)\n             |> render_click()\n             |> users_in_dom(\"c_users\") ==\n               [{\"c_users-1\", \"updated\"}, {\"c_users-2\", \"callan\"}]\n\n      assert_pruned_stream(lv)\n\n      assert lv\n             |> element(~S|#c_users-2 button[phx-click=\"move-to-first\"]|)\n             |> render_click()\n             |> users_in_dom(\"c_users\") ==\n               [{\"c_users-2\", \"updated\"}, {\"c_users-1\", \"updated\"}]\n\n      assert lv\n             |> element(~S|#c_users-2 button[phx-click=\"move-to-last\"]|)\n             |> render_click()\n             |> users_in_dom(\"c_users\") ==\n               [{\"c_users-1\", \"updated\"}, {\"c_users-2\", \"updated\"}]\n\n      assert lv\n             |> element(~S|#c_users-1 button[phx-click=\"delete\"]|)\n             |> render_click()\n             |> users_in_dom(\"c_users\") ==\n               [{\"c_users-2\", \"updated\"}]\n\n      assert lv |> render() |> users_in_dom(\"users\") == [\n               {\"users-1\", \"chris\"},\n               {\"users-2\", \"callan\"}\n             ]\n\n      assert lv\n             |> element(~S|#users-1 button[phx-click=\"move\"]|)\n             |> render_click(%{at: \"1\", name: \"chris-forward\"})\n             |> users_in_dom(\"users\") ==\n               [{\"users-2\", \"callan\"}, {\"users-1\", \"chris-forward\"}]\n\n      assert lv\n             |> element(~S|#users-1 button[phx-click=\"move\"]|)\n             |> render_click(%{at: \"0\", name: \"chris-backward\"})\n             |> users_in_dom(\"users\") ==\n               [{\"users-1\", \"chris-backward\"}, {\"users-2\", \"callan\"}]\n\n      assert lv\n             |> element(~S|#users-1 button[phx-click=\"move\"]|)\n             |> render_click(%{at: \"0\", name: \"chris-same\"})\n             |> users_in_dom(\"users\") ==\n               [{\"users-1\", \"chris-same\"}, {\"users-2\", \"callan\"}]\n\n      assert lv\n             |> element(~S|#users-2 button[phx-click=\"move\"]|)\n             |> render_click(%{at: \"1\", name: \"callan-same\"})\n             |> users_in_dom(\"users\") ==\n               [{\"users-1\", \"chris-same\"}, {\"users-2\", \"callan-same\"}]\n\n      # resets\n\n      assert lv |> render() |> users_in_dom(\"c_users\") == [{\"c_users-2\", \"updated\"}]\n\n      Phoenix.LiveView.send_update(lv.pid, Phoenix.LiveViewTest.Support.StreamComponent,\n        id: \"stream-component\",\n        reset: {:c_users, []}\n      )\n\n      assert lv |> render() |> users_in_dom(\"c_users\") == []\n\n      Phoenix.LiveView.send_update(lv.pid, Phoenix.LiveViewTest.Support.StreamComponent,\n        id: \"stream-component\",\n        send_assigns_to: self()\n      )\n\n      assert_receive {:assigns, %{streams: streams}}\n      assert streams.c_users.inserts == []\n      assert streams.c_users.deletes == []\n      assert_pruned_stream(lv)\n    end\n\n    test \"issue #2982 - can reorder a stream with LiveComponents as direct stream children\", %{\n      conn: conn\n    } do\n      {:ok, lv, html} = live(conn, \"/stream/reset-lc\")\n\n      assert ul_list_children(html) == [\n               {\"items-a\", \"A\"},\n               {\"items-b\", \"B\"},\n               {\"items-c\", \"C\"},\n               {\"items-d\", \"D\"}\n             ]\n\n      html = assert lv |> element(\"button\", \"Reorder\") |> render_click()\n\n      assert ul_list_children(html) == [\n               {\"items-e\", \"E\"},\n               {\"items-a\", \"A\"},\n               {\"items-f\", \"F\"},\n               {\"items-g\", \"G\"}\n             ]\n    end\n  end\n\n  test \"issue #3023 - can bulk insert at index != -1\", %{conn: conn} do\n    {:ok, lv, html} = live(conn, \"/stream/reset\")\n\n    assert ul_list_children(html) == [\n             {\"items-a\", \"A\"},\n             {\"items-b\", \"B\"},\n             {\"items-c\", \"C\"},\n             {\"items-d\", \"D\"}\n           ]\n\n    html = assert lv |> element(\"button\", \"Bulk insert\") |> render_click()\n\n    assert ul_list_children(html) == [\n             {\"items-a\", \"A\"},\n             {\"items-e\", \"E\"},\n             {\"items-f\", \"F\"},\n             {\"items-g\", \"G\"},\n             {\"items-b\", \"B\"},\n             {\"items-c\", \"C\"},\n             {\"items-d\", \"D\"}\n           ]\n  end\n\n  test \"any stream insert for elements already in the DOM does not reorder\", %{conn: conn} do\n    {:ok, lv, html} = live(conn, \"/stream/reset\")\n\n    assert ul_list_children(html) == [\n             {\"items-a\", \"A\"},\n             {\"items-b\", \"B\"},\n             {\"items-c\", \"C\"},\n             {\"items-d\", \"D\"}\n           ]\n\n    html = assert lv |> element(\"button\", \"Prepend C\") |> render_click()\n\n    assert ul_list_children(html) == [\n             {\"items-a\", \"A\"},\n             {\"items-b\", \"B\"},\n             {\"items-c\", \"C\"},\n             {\"items-d\", \"D\"}\n           ]\n\n    html = assert lv |> element(\"button\", \"Append C\") |> render_click()\n\n    assert ul_list_children(html) == [\n             {\"items-a\", \"A\"},\n             {\"items-b\", \"B\"},\n             {\"items-c\", \"C\"},\n             {\"items-d\", \"D\"}\n           ]\n\n    html = assert lv |> element(\"button\", \"Insert C at 1\") |> render_click()\n\n    assert ul_list_children(html) == [\n             {\"items-a\", \"A\"},\n             {\"items-b\", \"B\"},\n             {\"items-c\", \"C\"},\n             {\"items-d\", \"D\"}\n           ]\n\n    html = assert lv |> element(\"button\", \"Insert at 1\") |> render_click()\n\n    assert [{\"items-a\", \"A\"}, _, {\"items-b\", \"B\"}, {\"items-c\", \"C\"}, {\"items-d\", \"D\"}] =\n             ul_list_children(html)\n\n    html = assert lv |> element(\"button\", \"Reset\") |> render_click()\n\n    assert [{\"items-a\", \"A\"}, {\"items-b\", \"B\"}, {\"items-c\", \"C\"}, {\"items-d\", \"D\"}] =\n             ul_list_children(html)\n\n    html = assert lv |> element(\"button\", \"Delete C and insert at 1\") |> render_click()\n\n    assert [{\"items-a\", \"A\"}, {\"items-c\", \"C\"}, {\"items-b\", \"B\"}, {\"items-d\", \"D\"}] =\n             ul_list_children(html)\n  end\n\n  test \"stream raises when attempting to consume ahead of for\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/stream\")\n\n    assert Phoenix.LiveViewTest.Support.HooksLive.exits_with(lv, ArgumentError, fn ->\n             render_click(lv, \"consume-stream-invalid\", %{})\n           end) =~ ~r/streams can only be consumed directly by a for comprehension/\n  end\n\n  test \"stream raises when nodes without id are in container\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/stream\")\n\n    assert Phoenix.LiveViewTest.Support.HooksLive.exits_with(lv, ArgumentError, fn ->\n             render_click(lv, \"stream-no-id\", %{})\n           end) =~\n             ~r/setting phx-update to \"stream\" requires setting an ID on each child/\n  end\n\n  test \"issue #3260 - supports non-stream items with id in stream container\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/stream\")\n\n    render_click(lv, \"stream-extra-with-id\", %{})\n    html = render(lv)\n\n    assert [{\"users-1\", \"chris\"}, {\"users-2\", \"callan\"}, {\"users-empty\", \"Empty!\"}] =\n             users_in_dom(html, \"users\")\n\n    assert render_click(lv, \"reset-users\", %{}) |> users_in_dom(\"users\") == [\n             {\"users-empty\", \"Empty!\"}\n           ]\n\n    assert render_click(lv, \"append-users\", %{}) |> users_in_dom(\"users\") == [\n             {\"users-empty\", \"Empty!\"},\n             {\"users-4\", \"foo\"},\n             {\"users-3\", \"last_user\"}\n           ]\n  end\n\n  test \"handles high frequency updates properly\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/stream/high-frequency-stream-and-non-stream-updates\")\n\n    for _i <- 1..50 do\n      assert lv |> render_hook(\"insert_item\")\n      Process.sleep(10)\n    end\n\n    {_tag, _attributes, children} =\n      render(lv) |> TreeDOM.normalize_to_tree() |> TreeDOM.by_id!(\"mystream\")\n\n    assert length(children) == 50\n\n    # wait for more updates\n    Process.sleep(100)\n\n    # we should still have 50 items\n    {_tag, _attributes, children} =\n      render(lv) |> TreeDOM.normalize_to_tree() |> TreeDOM.by_id!(\"mystream\")\n\n    assert length(children) == 50\n  end\n\n  describe \"limit\" do\n    test \"limit is enforced on mount, but not dead render\", %{conn: conn} do\n      conn = get(conn, \"/stream/limit\")\n\n      assert html_response(conn, 200) |> ul_list_children() == [\n               {\"items-1\", \"1\"},\n               {\"items-2\", \"2\"},\n               {\"items-3\", \"3\"},\n               {\"items-4\", \"4\"},\n               {\"items-5\", \"5\"},\n               {\"items-6\", \"6\"},\n               {\"items-7\", \"7\"},\n               {\"items-8\", \"8\"},\n               {\"items-9\", \"9\"},\n               {\"items-10\", \"10\"}\n             ]\n\n      {:ok, _lv, html} = live(conn)\n\n      assert ul_list_children(html) == [\n               {\"items-6\", \"6\"},\n               {\"items-7\", \"7\"},\n               {\"items-8\", \"8\"},\n               {\"items-9\", \"9\"},\n               {\"items-10\", \"10\"}\n             ]\n    end\n\n    test \"removes item at front when appending and limit is negative\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/stream/limit\")\n\n      assert lv\n             |> render_hook(\"configure\", %{\"at\" => \"-1\", \"limit\" => \"-5\"})\n             |> ul_list_children() ==\n               [\n                 {\"items-6\", \"6\"},\n                 {\"items-7\", \"7\"},\n                 {\"items-8\", \"8\"},\n                 {\"items-9\", \"9\"},\n                 {\"items-10\", \"10\"}\n               ]\n\n      assert lv |> render_hook(\"insert_1\") |> ul_list_children() == [\n               {\"items-7\", \"7\"},\n               {\"items-8\", \"8\"},\n               {\"items-9\", \"9\"},\n               {\"items-10\", \"10\"},\n               {\"items-11\", \"11\"}\n             ]\n\n      assert lv |> render_hook(\"insert_10\") |> ul_list_children() == [\n               {\"items-17\", \"17\"},\n               {\"items-18\", \"18\"},\n               {\"items-19\", \"19\"},\n               {\"items-20\", \"20\"},\n               {\"items-21\", \"21\"}\n             ]\n    end\n\n    test \"removes item at back when prepending and limit is positive\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/stream/limit\")\n\n      assert lv |> render_hook(\"configure\", %{\"at\" => \"0\", \"limit\" => \"5\"}) |> ul_list_children() ==\n               [\n                 {\"items-10\", \"10\"},\n                 {\"items-9\", \"9\"},\n                 {\"items-8\", \"8\"},\n                 {\"items-7\", \"7\"},\n                 {\"items-6\", \"6\"}\n               ]\n\n      assert lv |> render_hook(\"insert_1\") |> ul_list_children() == [\n               {\"items-11\", \"11\"},\n               {\"items-10\", \"10\"},\n               {\"items-9\", \"9\"},\n               {\"items-8\", \"8\"},\n               {\"items-7\", \"7\"}\n             ]\n\n      assert lv |> render_hook(\"insert_10\") |> ul_list_children() == [\n               {\"items-21\", \"21\"},\n               {\"items-20\", \"20\"},\n               {\"items-19\", \"19\"},\n               {\"items-18\", \"18\"},\n               {\"items-17\", \"17\"}\n             ]\n    end\n\n    test \"does nothing if appending and positive limit is reached\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/stream/limit\")\n\n      assert lv |> render_hook(\"configure\", %{\"at\" => \"-1\", \"limit\" => \"5\"}) |> ul_list_children() ==\n               [\n                 {\"items-1\", \"1\"},\n                 {\"items-2\", \"2\"},\n                 {\"items-3\", \"3\"},\n                 {\"items-4\", \"4\"},\n                 {\"items-5\", \"5\"}\n               ]\n\n      # adding new items should do nothing, as the limit is reached\n      assert lv |> render_hook(\"insert_1\") |> ul_list_children() == [\n               {\"items-1\", \"1\"},\n               {\"items-2\", \"2\"},\n               {\"items-3\", \"3\"},\n               {\"items-4\", \"4\"},\n               {\"items-5\", \"5\"}\n             ]\n\n      assert lv |> render_hook(\"insert_10\") |> ul_list_children() == [\n               {\"items-1\", \"1\"},\n               {\"items-2\", \"2\"},\n               {\"items-3\", \"3\"},\n               {\"items-4\", \"4\"},\n               {\"items-5\", \"5\"}\n             ]\n    end\n\n    test \"does nothing if prepending and negative limit is reached\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/stream/limit\")\n\n      assert lv |> render_hook(\"configure\", %{\"at\" => \"0\", \"limit\" => \"-5\"}) |> ul_list_children() ==\n               [\n                 {\"items-5\", \"5\"},\n                 {\"items-4\", \"4\"},\n                 {\"items-3\", \"3\"},\n                 {\"items-2\", \"2\"},\n                 {\"items-1\", \"1\"}\n               ]\n\n      # adding new items should do nothing, as the limit is reached\n      assert lv |> render_hook(\"insert_1\") |> ul_list_children() == [\n               {\"items-5\", \"5\"},\n               {\"items-4\", \"4\"},\n               {\"items-3\", \"3\"},\n               {\"items-2\", \"2\"},\n               {\"items-1\", \"1\"}\n             ]\n\n      assert lv |> render_hook(\"insert_10\") |> ul_list_children() == [\n               {\"items-5\", \"5\"},\n               {\"items-4\", \"4\"},\n               {\"items-3\", \"3\"},\n               {\"items-2\", \"2\"},\n               {\"items-1\", \"1\"}\n             ]\n    end\n\n    test \"arbitrary index\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/stream/limit\")\n\n      assert lv |> render_hook(\"configure\", %{\"at\" => \"1\", \"limit\" => \"5\"}) |> ul_list_children() ==\n               [\n                 {\"items-1\", \"1\"},\n                 {\"items-10\", \"10\"},\n                 {\"items-9\", \"9\"},\n                 {\"items-8\", \"8\"},\n                 {\"items-7\", \"7\"}\n               ]\n\n      assert lv |> render_hook(\"insert_10\") |> ul_list_children() == [\n               {\"items-1\", \"1\"},\n               {\"items-20\", \"20\"},\n               {\"items-19\", \"19\"},\n               {\"items-18\", \"18\"},\n               {\"items-17\", \"17\"}\n             ]\n\n      assert lv |> render_hook(\"configure\", %{\"at\" => \"1\", \"limit\" => \"-5\"}) |> ul_list_children() ==\n               [\n                 {\"items-10\", \"10\"},\n                 {\"items-5\", \"5\"},\n                 {\"items-4\", \"4\"},\n                 {\"items-3\", \"3\"},\n                 {\"items-2\", \"2\"}\n               ]\n\n      assert lv |> render_hook(\"insert_10\") |> ul_list_children() == [\n               {\"items-20\", \"20\"},\n               {\"items-5\", \"5\"},\n               {\"items-4\", \"4\"},\n               {\"items-3\", \"3\"},\n               {\"items-2\", \"2\"}\n             ]\n    end\n  end\n\n  test \"stream nested in a LiveComponent is properly restored on reset\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/stream/nested-component-reset\")\n\n    childItems = fn html, id ->\n      html\n      |> TreeDOM.normalize_to_tree()\n      |> TreeDOM.by_id!(id)\n      |> TreeDOM.filter(fn node ->\n        TreeDOM.tag(node) == \"div\" && TreeDOM.attribute(node, \"phx-update\") == \"stream\"\n      end)\n      |> case do\n        [{_tag, _attrs, children}] -> children\n      end\n      |> Enum.map(fn {_tag, _attrs, [text | _children]} = child ->\n        {TreeDOM.attribute(child, \"id\"), String.trim(text)}\n      end)\n    end\n\n    assert render(lv) |> ul_list_children() == [\n             {\"items-a\", \"A\"},\n             {\"items-b\", \"B\"},\n             {\"items-c\", \"C\"},\n             {\"items-d\", \"D\"}\n           ]\n\n    for id <- [\"a\", \"b\", \"c\", \"d\"] do\n      assert render(lv) |> childItems.(\"items-#{id}\") == [\n               {\"nested-items-#{id}-a\", \"N-A\"},\n               {\"nested-items-#{id}-b\", \"N-B\"},\n               {\"nested-items-#{id}-c\", \"N-C\"},\n               {\"nested-items-#{id}-d\", \"N-D\"}\n             ]\n    end\n\n    # now reorder the nested stream of items-a\n    assert lv |> element(\"#items-a button\") |> render_click() |> childItems.(\"items-a\") == [\n             {\"nested-items-a-e\", \"N-E\"},\n             {\"nested-items-a-a\", \"N-A\"},\n             {\"nested-items-a-f\", \"N-F\"},\n             {\"nested-items-a-g\", \"N-G\"}\n           ]\n\n    # unchanged\n    for id <- [\"b\", \"c\", \"d\"] do\n      assert render(lv) |> childItems.(\"items-#{id}\") == [\n               {\"nested-items-#{id}-a\", \"N-A\"},\n               {\"nested-items-#{id}-b\", \"N-B\"},\n               {\"nested-items-#{id}-c\", \"N-C\"},\n               {\"nested-items-#{id}-d\", \"N-D\"}\n             ]\n    end\n\n    # now reorder the parent stream\n    assert lv |> element(\"#parent-reorder\") |> render_click() |> ul_list_children() == [\n             {\"items-e\", \"E\"},\n             {\"items-a\", \"A\"},\n             {\"items-f\", \"F\"},\n             {\"items-g\", \"G\"}\n           ]\n\n    # the new children's stream items have the correct order\n    for id <- [\"e\", \"f\", \"g\"] do\n      assert render(lv) |> childItems.(\"items-#{id}\") == [\n               {\"nested-items-#{id}-a\", \"N-A\"},\n               {\"nested-items-#{id}-b\", \"N-B\"},\n               {\"nested-items-#{id}-c\", \"N-C\"},\n               {\"nested-items-#{id}-d\", \"N-D\"}\n             ]\n    end\n\n    # Item A has the same children as before, still reordered\n    assert render(lv) |> childItems.(\"items-a\") == [\n             {\"nested-items-a-e\", \"N-E\"},\n             {\"nested-items-a-a\", \"N-A\"},\n             {\"nested-items-a-f\", \"N-F\"},\n             {\"nested-items-a-g\", \"N-G\"}\n           ]\n  end\n\n  test \"issue #3129 - streams asynchronously assigned and rendered inside a comprehension\", %{\n    conn: conn\n  } do\n    {:ok, lv, _html} = live(conn, \"/stream/inside-for\")\n\n    html = render_async(lv)\n\n    assert ul_list_children(html) == [\n             {\"items-a\", \"A\"},\n             {\"items-b\", \"B\"},\n             {\"items-c\", \"C\"},\n             {\"items-d\", \"D\"}\n           ]\n  end\n\n  test \"update_only\", %{conn: conn} do\n    {:ok, lv, html} = live(conn, \"/stream/reset\")\n\n    assert ul_list_children(html) == [\n             {\"items-a\", \"A\"},\n             {\"items-b\", \"B\"},\n             {\"items-c\", \"C\"},\n             {\"items-d\", \"D\"}\n           ]\n\n    html = assert lv |> element(\"button\", \"Add E (update only)\") |> render_click()\n\n    assert ul_list_children(html) == [\n             {\"items-a\", \"A\"},\n             {\"items-b\", \"B\"},\n             {\"items-c\", \"C\"},\n             {\"items-d\", \"D\"}\n           ]\n\n    html = assert lv |> element(\"button\", \"Update C (update only)\") |> render_click()\n\n    assert [\n             {\"items-a\", \"A\"},\n             {\"items-b\", \"B\"},\n             {\"items-c\", \"C \" <> _},\n             {\"items-d\", \"D\"}\n           ] = ul_list_children(html)\n  end\n\n  test \"issue #3993 - stream reset + keyed comprehensions\", %{conn: conn} do\n    {:ok, lv, _html} = live(conn, \"/healthy/fruits\")\n\n    assert has_element?(lv, \"h1\", \"Fruits\")\n    assert has_element?(lv, \"li\", \"Apples\")\n    assert has_element?(lv, \"li\", \"Oranges\")\n\n    html = render_hook(lv, \"load-more\", %{})\n\n    assert html =~ \"Apples\"\n    assert html =~ \"Oranges\"\n    assert html =~ \"Pumpkins\"\n    assert html =~ \"Melons\"\n  end\n\n  defp assert_pruned_stream(lv) do\n    stream = StreamLive.run(lv, fn socket -> {:reply, socket.assigns.streams.users, socket} end)\n    assert stream.inserts == []\n    assert stream.deletes == []\n  end\n\n  defp users_in_dom(html, parent_id) do\n    html\n    |> DOM.parse_document()\n    |> elem(0)\n    |> DOM.all(\"##{parent_id} > *\")\n    |> DOM.to_tree()\n    |> Enum.map(fn {_tag, _attrs, [text | _children]} = child ->\n      {TreeDOM.attribute(child, \"id\"), String.trim(text)}\n    end)\n  end\n\n  defp ul_list_children(html) do\n    html\n    |> DOM.parse_document()\n    |> elem(0)\n    |> DOM.all(\"ul > li\")\n    |> DOM.to_tree()\n    |> Enum.map(fn {_tag, _attrs, [text | _children]} = child ->\n      {TreeDOM.attribute(child, \"id\"), String.trim(text)}\n    end)\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/telemetry_test.exs",
    "content": "defmodule Phoenix.LiveView.TelemetryTest do\n  # Telemetry tests need to run synchronously\n  use ExUnit.Case, async: false\n\n  import ExUnit.CaptureLog\n\n  import Phoenix.ConnTest\n  import Phoenix.LiveViewTest\n  import Phoenix.LiveView.TelemetryTestHelpers\n\n  alias Phoenix.LiveView.Socket\n  alias Phoenix.LiveViewTest.Support.Endpoint\n\n  @endpoint Endpoint\n  @moduletag session: %{names: [\"chris\", \"jose\"], from: nil}\n\n  setup config do\n    {:ok, conn: Plug.Test.init_test_session(build_conn(), config[:session] || %{})}\n  end\n\n  describe \"live views\" do\n    @tag session: %{current_user_id: \"1\"}\n    test \"static mount emits telemetry events are emitted on successful callback\", %{conn: conn} do\n      attach_telemetry([:phoenix, :live_view, :mount])\n\n      log =\n        capture_log(fn ->\n          conn\n          |> get(\"/thermo?foo=bar\")\n          |> html_response(200)\n\n          assert_receive {:event, [:phoenix, :live_view, :mount, :start], %{system_time: _},\n                          %{socket: %Socket{transport_pid: nil}} = metadata}\n\n          assert metadata.params == %{\"foo\" => \"bar\"}\n          assert metadata.session == %{\"current_user_id\" => \"1\"}\n          assert metadata.uri == \"http://www.example.com/thermo?foo=bar\"\n\n          assert_receive {:event, [:phoenix, :live_view, :mount, :stop], %{duration: _},\n                          %{socket: %Socket{transport_pid: nil}} = metadata}\n\n          assert metadata.params == %{\"foo\" => \"bar\"}\n          assert metadata.session == %{\"current_user_id\" => \"1\"}\n          assert metadata.uri == \"http://www.example.com/thermo?foo=bar\"\n        end)\n\n      refute log =~ \"MOUNT Phoenix.LiveViewTest.Support.ThermostatLive\"\n      refute log =~ \"Replied in \"\n\n      refute log =~ \"HANDLE PARAMS in Phoenix.LiveViewTest.Support.ThermostatLive\"\n      refute log =~ \"Replied in \"\n    end\n\n    @tag session: %{current_user_id: \"1\"}\n    test \"static mount emits telemetry events when callback raises an exception\", %{conn: conn} do\n      attach_telemetry([:phoenix, :live_view, :mount])\n\n      assert_raise RuntimeError, ~r/boom/, fn ->\n        get(conn, \"/errors?crash_on=disconnected_mount\")\n      end\n\n      assert_receive {:event, [:phoenix, :live_view, :mount, :start], %{system_time: _},\n                      %{socket: %Socket{transport_pid: nil}} = metadata}\n\n      assert metadata.params == %{\"crash_on\" => \"disconnected_mount\"}\n      assert metadata.session == %{\"current_user_id\" => \"1\"}\n      assert metadata.uri == \"http://www.example.com/errors?crash_on=disconnected_mount\"\n\n      assert_receive {:event, [:phoenix, :live_view, :mount, :exception], %{duration: _},\n                      %{socket: %Socket{transport_pid: nil}} = metadata}\n\n      assert metadata.kind == :error\n      assert %RuntimeError{} = metadata.reason\n      assert metadata.params == %{\"crash_on\" => \"disconnected_mount\"}\n      assert metadata.session == %{\"current_user_id\" => \"1\"}\n      assert metadata.uri == \"http://www.example.com/errors?crash_on=disconnected_mount\"\n    end\n\n    @tag session: %{current_user_id: \"1\"}\n    test \"live mount emits telemetry events are emitted on successful callback\", %{conn: conn} do\n      attach_telemetry([:phoenix, :live_view, :mount])\n\n      log =\n        capture_log(fn ->\n          {:ok, _view, _html} = live(conn, \"/thermo?foo=bar\")\n\n          assert_receive {:event, [:phoenix, :live_view, :mount, :start], %{system_time: _},\n                          %{socket: %{transport_pid: pid}} = metadata}\n                         when is_pid(pid)\n\n          assert metadata.socket.transport_pid\n          assert metadata.params == %{\"foo\" => \"bar\"}\n          assert metadata.session == %{\"current_user_id\" => \"1\"}\n          assert metadata.uri == \"http://www.example.com/thermo?foo=bar\"\n\n          assert_receive {:event, [:phoenix, :live_view, :mount, :stop], %{duration: _},\n                          %{socket: %{transport_pid: pid}} = metadata}\n                         when is_pid(pid)\n\n          assert metadata.socket.transport_pid\n          assert metadata.params == %{\"foo\" => \"bar\"}\n          assert metadata.session == %{\"current_user_id\" => \"1\"}\n          assert metadata.uri == \"http://www.example.com/thermo?foo=bar\"\n        end)\n\n      assert log =~ \"MOUNT Phoenix.LiveViewTest.Support.ThermostatLive\"\n      assert log =~ \"  Parameters: %{\\\"foo\\\" => \\\"bar\\\"}\"\n      assert log =~ \"  Session: %{\\\"current_user_id\\\" => \\\"1\\\"}\"\n      assert log =~ \"Replied in\"\n\n      assert log =~ \"HANDLE PARAMS in Phoenix.LiveViewTest.Support.ThermostatLive\"\n      assert log =~ \"  Parameters: %{\\\"foo\\\" => \\\"bar\\\"}\"\n      assert log =~ \"Replied in\"\n    end\n\n    @tag session: %{current_user_id: \"1\"}\n    test \"live mount emits telemetry events when callback raises an exception\", %{conn: conn} do\n      attach_telemetry([:phoenix, :live_view, :mount])\n\n      assert catch_exit(live(conn, \"/errors?crash_on=connected_mount\"))\n\n      assert_receive {:event, [:phoenix, :live_view, :mount, :start], %{system_time: _},\n                      %{socket: %{transport_pid: pid}} = metadata}\n                     when is_pid(pid)\n\n      assert metadata.socket.transport_pid\n      assert metadata.params == %{\"crash_on\" => \"connected_mount\"}\n      assert metadata.session == %{\"current_user_id\" => \"1\"}\n      assert metadata.uri == \"http://www.example.com/errors?crash_on=connected_mount\"\n\n      assert_receive {:event, [:phoenix, :live_view, :mount, :exception], %{duration: _},\n                      %{socket: %{transport_pid: pid}} = metadata}\n                     when is_pid(pid)\n\n      assert metadata.socket.transport_pid\n      assert metadata.kind == :error\n      assert %RuntimeError{} = metadata.reason\n      assert metadata.params == %{\"crash_on\" => \"connected_mount\"}\n      assert metadata.session == %{\"current_user_id\" => \"1\"}\n      assert metadata.uri == \"http://www.example.com/errors?crash_on=connected_mount\"\n    end\n\n    test \"render_* with a successful handle_event callback emits telemetry metrics\", %{conn: conn} do\n      attach_telemetry([:phoenix, :live_view, :handle_event])\n\n      log =\n        capture_log(fn ->\n          {:ok, view, _} = live(conn, \"/thermo\")\n          render_submit(view, :save, %{temp: 20})\n\n          assert_receive {:event, [:phoenix, :live_view, :handle_event, :start],\n                          %{system_time: _}, metadata}\n\n          assert metadata.socket.transport_pid\n          assert metadata.event == \"save\"\n          assert metadata.params == %{\"temp\" => \"20\"}\n\n          assert_receive {:event, [:phoenix, :live_view, :handle_event, :stop], %{duration: _},\n                          metadata}\n\n          assert metadata.socket.transport_pid\n          assert metadata.event == \"save\"\n          assert metadata.params == %{\"temp\" => \"20\"}\n        end)\n\n      assert log =~ \"HANDLE EVENT \\\"save\\\" in Phoenix.LiveViewTest.Support.ThermostatLive\"\n      assert log =~ \"  Parameters: %{\\\"temp\\\" => \\\"20\\\"}\"\n      assert log =~ \"Replied in\"\n    end\n\n    test \"render_* with crashing handle_event callback emits telemetry metrics\", %{conn: conn} do\n      Process.flag(:trap_exit, true)\n      attach_telemetry([:phoenix, :live_view, :handle_event])\n\n      {:ok, view, _} = live(conn, \"/errors\")\n      catch_exit(render_submit(view, :crash, %{\"foo\" => \"bar\"}))\n\n      assert_receive {:event, [:phoenix, :live_view, :handle_event, :start], %{system_time: _},\n                      metadata}\n\n      assert metadata.socket.transport_pid\n      assert metadata.event == \"crash\"\n      assert metadata.params == %{\"foo\" => \"bar\"}\n\n      assert_receive {:event, [:phoenix, :live_view, :handle_event, :exception], %{duration: _},\n                      metadata}\n\n      assert metadata.socket.transport_pid\n      assert metadata.kind == :error\n      assert %RuntimeError{} = metadata.reason\n      assert metadata.event == \"crash\"\n      assert metadata.params == %{\"foo\" => \"bar\"}\n    end\n\n    test \" with a successful render callback emits render telemetry events\", %{conn: conn} do\n      attach_telemetry([:phoenix, :live_view, :render])\n\n      {:ok, view, _} = live(conn, \"/thermo\")\n\n      assert_receive {:event, [:phoenix, :live_view, :render, :start], %{system_time: _},\n                      metadata}\n\n      assert metadata.socket.transport_pid\n      assert metadata.force?\n      assert metadata.changed?\n\n      assert_receive {:event, [:phoenix, :live_view, :render, :stop], %{duration: _}, metadata}\n\n      assert metadata.socket.transport_pid\n      assert metadata.force?\n      assert metadata.changed?\n\n      render_submit(view, :save, %{temp: 20})\n\n      assert_receive {:event, [:phoenix, :live_view, :render, :stop], %{duration: _}, _}\n    end\n  end\n\n  describe \"live components\" do\n    test \"emits telemetry events when callback is successful\", %{conn: conn} do\n      attach_telemetry([:phoenix, :live_component, :handle_event])\n\n      log =\n        capture_log(fn ->\n          {:ok, view, _html} = live(conn, \"/components\")\n\n          view |> element(\"#chris\") |> render_click(%{\"op\" => \"upcase\"})\n\n          assert_receive {:event, [:phoenix, :live_component, :handle_event, :start],\n                          %{system_time: _}, metadata}\n\n          assert metadata.socket.transport_pid\n          assert metadata.event == \"transform\"\n          assert metadata.component == Phoenix.LiveViewTest.Support.StatefulComponent\n          assert metadata.params == %{\"op\" => \"upcase\"}\n\n          assert_receive {:event, [:phoenix, :live_component, :handle_event, :stop],\n                          %{duration: _}, metadata}\n\n          assert metadata.socket.transport_pid\n          assert metadata.event == \"transform\"\n          assert metadata.component == Phoenix.LiveViewTest.Support.StatefulComponent\n          assert metadata.params == %{\"op\" => \"upcase\"}\n        end)\n\n      assert log =~ \"HANDLE EVENT \\\"transform\\\" in Phoenix.LiveViewTest.Support.WithComponentLive\"\n      assert log =~ \"  Component: Phoenix.LiveViewTest.Support.StatefulComponent\"\n      assert log =~ \"  Parameters: %{\\\"op\\\" => \\\"upcase\\\"}\"\n      assert log =~ \"Replied in\"\n    end\n\n    test \"emits telemetry events when callback fails\", %{conn: conn} do\n      Process.flag(:trap_exit, true)\n\n      attach_telemetry([:phoenix, :live_component, :handle_event])\n      {:ok, view, _html} = live(conn, \"/components\")\n\n      assert view |> element(\"#chris\") |> render_click(%{\"op\" => \"boom\"}) |> catch_exit\n\n      assert_receive {:event, [:phoenix, :live_component, :handle_event, :start],\n                      %{system_time: _}, metadata}\n\n      assert metadata.socket.transport_pid\n      assert metadata.event == \"transform\"\n      assert metadata.component == Phoenix.LiveViewTest.Support.StatefulComponent\n      assert metadata.params == %{\"op\" => \"boom\"}\n\n      assert_receive {:event, [:phoenix, :live_component, :handle_event, :exception],\n                      %{duration: _}, metadata}\n\n      assert metadata.kind == :error\n      assert metadata.reason == {:case_clause, \"boom\"}\n      assert metadata.socket.transport_pid\n      assert metadata.event == \"transform\"\n      assert metadata.component == Phoenix.LiveViewTest.Support.StatefulComponent\n      assert metadata.params == %{\"op\" => \"boom\"}\n    end\n\n    test \"emits telemetry events for update calls\", %{conn: conn} do\n      attach_telemetry([:phoenix, :live_component, :update])\n\n      {:ok, view, _html} = live(conn, \"/multi-targets\")\n\n      assert_receive {:event, [:phoenix, :live_component, :update, :start], %{system_time: _},\n                      metadata}\n\n      assert metadata.socket\n      assert metadata.component == Phoenix.LiveViewTest.Support.StatefulComponent\n\n      assert [\n               {\n                 %{id: _id, name: name},\n                 %{assigns: %{myself: cid}} = component_socket\n               },\n               _\n             ] = metadata.assigns_sockets\n\n      assert_receive {:event, [:phoenix, :live_component, :update, :stop], %{duration: _},\n                      metadata}\n\n      assert metadata.socket\n      assert metadata.component == Phoenix.LiveViewTest.Support.StatefulComponent\n      assert [updated_component_socket, _] = metadata.sockets\n\n      assert updated_component_socket != component_socket\n      assert %{myself: ^cid} = updated_component_socket.assigns\n\n      render_click(view, \"disable\", %{\"name\" => name})\n\n      assert_receive {:event, [:phoenix, :live_component, :update, :start], %{system_time: _},\n                      %{assigns_sockets: [{%{name: ^name, disabled: true}, _} | _]}}\n    end\n  end\n\n  describe \"logging configuration\" do\n    @tag session: %{current_user_id: \"1\"}\n    test \"log level can be overridden for an individual Live View module\", %{conn: conn} do\n      log =\n        capture_log([level: :warning], fn ->\n          {:ok, _view, _html} = live(conn, \"/log-override\")\n        end)\n\n      assert log =~ \"MOUNT Phoenix.LiveViewTest.Support.WithLogOverride\"\n      assert log =~ \"Replied in \"\n    end\n\n    @tag session: %{current_user_id: \"1\"}\n    test \"logging can be disabled for an individual Live View module\", %{conn: conn} do\n      log =\n        capture_log(fn ->\n          {:ok, _view, _html} = live(conn, \"/log-disabled\")\n        end)\n\n      refute log =~ \"MOUNT Phoenix.LiveViewTest.Support.WithLogDisabled\"\n      refute log =~ \"Replied in \"\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/integrations/update_test.exs",
    "content": "defmodule Phoenix.LiveView.UpdateTest do\n  use ExUnit.Case, async: true\n  import Phoenix.ConnTest\n\n  import Phoenix.LiveViewTest\n  alias Phoenix.LiveViewTest.TreeDOM\n  alias Phoenix.LiveViewTest.Support.Endpoint\n\n  @endpoint Endpoint\n\n  setup config do\n    {:ok,\n     conn: Plug.Test.init_test_session(Phoenix.ConnTest.build_conn(), config[:session] || %{})}\n  end\n\n  describe \"regular updates\" do\n    @tag session: %{\n           time_zones: [%{\"id\" => \"ny\", \"name\" => \"NY\"}, %{\"id\" => \"sf\", \"name\" => \"SF\"}]\n         }\n    test \"existing ids are replaced when patched without respawning children\", %{conn: conn} do\n      {:ok, view, html} = live(conn, \"/shuffle\")\n\n      assert [\n               {\"div\", _, [\"time: 12:00 NY\" | _]},\n               {\"div\", _, [\"time: 12:00 SF\" | _]}\n             ] = find_time_zones(html, [\"ny\", \"sf\"])\n\n      children_pids_before = for child <- live_children(view), do: child.pid\n      html = render_click(view, :reverse)\n      children_pids_after = for child <- live_children(view), do: child.pid\n\n      assert [\n               {\"div\", _, [\"time: 12:00 SF\" | _]},\n               {\"div\", _, [\"time: 12:00 NY\" | _]}\n             ] = find_time_zones(html, [\"ny\", \"sf\"])\n\n      assert children_pids_after == children_pids_before\n    end\n  end\n\n  defp find_time_zones(html, zones) do\n    ids = Enum.map(zones, fn zone -> \"tz-\" <> zone end)\n\n    html\n    |> TreeDOM.normalize_to_tree(sort_attributes: true)\n    |> TreeDOM.filter(fn node -> TreeDOM.attribute(node, \"id\") in ids end)\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/js_test.exs",
    "content": "defmodule Phoenix.LiveView.JSTest do\n  use ExUnit.Case, async: true\n\n  alias Phoenix.LiveView.JS\n\n  describe \"%JS{}\" do\n    test \"implements Jason.Encoder\" do\n      js = JS.push(\"inc\", value: %{one: 1})\n      encodedJS = Jason.encode!(js)\n\n      assert String.starts_with?(encodedJS, ~S<[[\"push\",{>)\n\n      assert Jason.decode!(encodedJS) == [\n               [\"push\", %{\"event\" => \"inc\", \"value\" => %{\"one\" => 1}}]\n             ]\n    end\n\n    if Code.ensure_loaded?(JSON.Encoder) do\n      test \"implements JSON.Encoder\" do\n        js = JS.push(\"inc\", value: %{one: 1})\n        encodedJS = JSON.encode!(js)\n\n        assert String.starts_with?(encodedJS, ~S<[[\"push\",{>)\n\n        assert JSON.decode!(encodedJS) == [\n                 [\"push\", %{\"event\" => \"inc\", \"value\" => %{\"one\" => 1}}]\n               ]\n      end\n    else\n      @tag skip: \"JSON module is not available\"\n      test \"implements JSON.Encoder\", do: flunk(\"should not run\")\n    end\n  end\n\n  describe \"to_encodable/1\" do\n    test \"returns the ops list\" do\n      js = JS.push(\"inc\", value: %{one: 1})\n      assert JS.to_encodable(js) == [[\"push\", %{event: \"inc\", value: %{one: 1}}]]\n    end\n\n    test \"with multiple ops\" do\n      js =\n        JS.push(\"inc\", value: %{one: 1, two: 2})\n        |> JS.add_class(\"show\", to: \"#modal\", time: 100)\n        |> JS.remove_class(\"hidden\")\n\n      assert JS.to_encodable(js) == [\n               [\"push\", %{event: \"inc\", value: %{one: 1, two: 2}}],\n               [\"add_class\", %{names: [\"show\"], to: \"#modal\", time: 100}],\n               [\"remove_class\", %{names: [\"hidden\"]}]\n             ]\n    end\n  end\n\n  describe \"exec\" do\n    test \"with defaults\" do\n      assert JS.exec(\"phx-remove\") == %JS{ops: [[\"exec\", %{attr: \"phx-remove\"}]]}\n\n      assert JS.exec(\"phx-remove\", to: \"#modal\") == %JS{\n               ops: [[\"exec\", %{attr: \"phx-remove\", to: \"#modal\"}]]\n             }\n    end\n  end\n\n  describe \"push\" do\n    test \"with defaults\" do\n      assert JS.push(\"inc\") == %JS{ops: [[\"push\", %{event: \"inc\"}]]}\n    end\n\n    test \"target\" do\n      assert JS.push(\"inc\", target: \"#modal\") == %JS{\n               ops: [[\"push\", %{event: \"inc\", target: \"#modal\"}]]\n             }\n\n      assert JS.push(\"inc\", target: 1) == %JS{\n               ops: [[\"push\", %{event: \"inc\", target: 1}]]\n             }\n    end\n\n    test \"loading\" do\n      assert JS.push(\"inc\", loading: \"#modal\") == %JS{\n               ops: [[\"push\", %{event: \"inc\", loading: \"#modal\"}]]\n             }\n    end\n\n    test \"page_loading\" do\n      assert JS.push(\"inc\", page_loading: true) == %JS{\n               ops: [[\"push\", %{event: \"inc\", page_loading: true}]]\n             }\n    end\n\n    test \"value\" do\n      assert JS.push(\"inc\", value: %{one: 1, two: 2}) == %JS{\n               ops: [[\"push\", %{event: \"inc\", value: %{one: 1, two: 2}}]]\n             }\n\n      assert_raise ArgumentError, ~r/push :value expected to be a map/, fn ->\n        JS.push(\"inc\", value: \"not-a-map\")\n      end\n    end\n\n    test \"raises with unknown options\" do\n      assert_raise ArgumentError, ~r/invalid option for push/, fn ->\n        JS.push(\"inc\", to: \"#modal\", bad: :opt)\n      end\n    end\n\n    test \"composability\" do\n      js = JS.push(\"inc\") |> JS.push(\"dec\", loading: \".foo\")\n\n      assert js == %JS{\n               ops: [[\"push\", %{event: \"inc\"}], [\"push\", %{event: \"dec\", loading: \".foo\"}]]\n             }\n    end\n\n    test \"encoding\" do\n      assert js_to_string(JS.push(\"inc\", value: %{one: 1, two: 2})) ==\n               ~S<[[\"push\",{\"event\":\"inc\",\"value\":{\"one\":1,\"two\":2}}]]>\n    end\n  end\n\n  describe \"add_class\" do\n    test \"with defaults\" do\n      assert JS.add_class(\"show\") == %JS{\n               ops: [\n                 [\"add_class\", %{names: [\"show\"]}]\n               ]\n             }\n\n      assert JS.add_class(\"show\", to: {:closest, \"a\"}) == %JS{\n               ops: [\n                 [\n                   \"add_class\",\n                   %{names: [\"show\"], to: %{closest: \"a\"}}\n                 ]\n               ]\n             }\n\n      assert JS.add_class(\"show\", to: \"#modal\") == %JS{\n               ops: [\n                 [\n                   \"add_class\",\n                   %{names: [\"show\"], to: \"#modal\"}\n                 ]\n               ]\n             }\n    end\n\n    test \"multiple classes\" do\n      assert JS.add_class(\"show hl\") == %JS{\n               ops: [\n                 [\n                   \"add_class\",\n                   %{names: [\"show\", \"hl\"]}\n                 ]\n               ]\n             }\n    end\n\n    test \"custom time\" do\n      assert JS.add_class(\"show\", time: 543) == %JS{\n               ops: [\n                 [\"add_class\", %{names: [\"show\"], time: 543}]\n               ]\n             }\n    end\n\n    test \"transition\" do\n      assert JS.add_class(\"show\", transition: \"fade\") == %JS{\n               ops: [\n                 [\n                   \"add_class\",\n                   %{names: [\"show\"], transition: [[\"fade\"], [], []]}\n                 ]\n               ]\n             }\n\n      assert JS.add_class(\"show\", transition: \"fade\", blocking: false) == %JS{\n               ops: [\n                 [\n                   \"add_class\",\n                   %{names: [\"show\"], transition: [[\"fade\"], [], []], blocking: false}\n                 ]\n               ]\n             }\n\n      assert JS.add_class(\"c\", transition: \"a b\") == %JS{\n               ops: [\n                 [\n                   \"add_class\",\n                   %{names: [\"c\"], transition: [[\"a\", \"b\"], [], []]}\n                 ]\n               ]\n             }\n\n      assert JS.add_class(\"show\", transition: {\"fade\", \"opacity-0\", \"opacity-100\"}) == %JS{\n               ops: [\n                 [\n                   \"add_class\",\n                   %{\n                     names: [\"show\"],\n                     transition: [[\"fade\"], [\"opacity-0\"], [\"opacity-100\"]]\n                   }\n                 ]\n               ]\n             }\n    end\n\n    test \"composability\" do\n      js = JS.add_class(\"show\", to: \"#modal\", time: 100) |> JS.add_class(\"hl\")\n\n      assert js == %JS{\n               ops: [\n                 [\n                   \"add_class\",\n                   %{names: [\"show\"], time: 100, to: \"#modal\"}\n                 ],\n                 [\"add_class\", %{names: [\"hl\"]}]\n               ]\n             }\n    end\n\n    test \"raises with unknown options\" do\n      assert_raise ArgumentError, ~r/invalid option for add_class/, fn ->\n        JS.add_class(\"show\", bad: :opt)\n      end\n\n      assert_raise ArgumentError, ~r/invalid scope for :to option in add_class/, fn ->\n        JS.add_class(\"show\", to: {:sibling, \"foo\"})\n      end\n    end\n\n    test \"encoding\" do\n      assert js_to_string(JS.add_class(\"show\")) ==\n               ~S<[[\"add_class\",{\"names\":[\"show\"]}]]>\n    end\n  end\n\n  describe \"remove_class\" do\n    test \"with defaults\" do\n      assert JS.remove_class(\"show\") == %JS{\n               ops: [\n                 [\n                   \"remove_class\",\n                   %{names: [\"show\"]}\n                 ]\n               ]\n             }\n\n      assert JS.remove_class(\"show\", to: \"#modal\") == %JS{\n               ops: [\n                 [\n                   \"remove_class\",\n                   %{names: [\"show\"], to: \"#modal\"}\n                 ]\n               ]\n             }\n\n      assert JS.remove_class(\"show\", to: {:inner, \"a\"}) == %JS{\n               ops: [\n                 [\n                   \"remove_class\",\n                   %{names: [\"show\"], to: %{inner: \"a\"}}\n                 ]\n               ]\n             }\n    end\n\n    test \"multiple classes\" do\n      assert JS.remove_class(\"show hl\") == %JS{\n               ops: [\n                 [\n                   \"remove_class\",\n                   %{names: [\"show\", \"hl\"]}\n                 ]\n               ]\n             }\n    end\n\n    test \"custom time\" do\n      assert JS.remove_class(\"show\", time: 543) == %JS{\n               ops: [\n                 [\n                   \"remove_class\",\n                   %{names: [\"show\"], time: 543}\n                 ]\n               ]\n             }\n    end\n\n    test \"transition\" do\n      assert JS.remove_class(\"show\", transition: \"fade\") == %JS{\n               ops: [\n                 [\n                   \"remove_class\",\n                   %{names: [\"show\"], transition: [[\"fade\"], [], []]}\n                 ]\n               ]\n             }\n\n      assert JS.remove_class(\"show\", transition: \"fade\", blocking: false) == %JS{\n               ops: [\n                 [\n                   \"remove_class\",\n                   %{names: [\"show\"], transition: [[\"fade\"], [], []], blocking: false}\n                 ]\n               ]\n             }\n\n      assert JS.remove_class(\"c\", transition: \"a b\") == %JS{\n               ops: [\n                 [\n                   \"remove_class\",\n                   %{names: [\"c\"], transition: [[\"a\", \"b\"], [], []]}\n                 ]\n               ]\n             }\n\n      assert JS.remove_class(\"show\", transition: {\"fade\", \"opacity-0\", \"opacity-100\"}) == %JS{\n               ops: [\n                 [\n                   \"remove_class\",\n                   %{\n                     names: [\"show\"],\n                     transition: [[\"fade\"], [\"opacity-0\"], [\"opacity-100\"]]\n                   }\n                 ]\n               ]\n             }\n    end\n\n    test \"composability\" do\n      js = JS.remove_class(\"show\", to: \"#modal\", time: 100) |> JS.remove_class(\"hl\")\n\n      assert js == %JS{\n               ops: [\n                 [\n                   \"remove_class\",\n                   %{names: [\"show\"], time: 100, to: \"#modal\"}\n                 ],\n                 [\"remove_class\", %{names: [\"hl\"]}]\n               ]\n             }\n    end\n\n    test \"raises with unknown options\" do\n      assert_raise ArgumentError, ~r/invalid option for remove_class/, fn ->\n        JS.remove_class(\"show\", bad: :opt)\n      end\n    end\n\n    test \"encoding\" do\n      assert js_to_string(JS.remove_class(\"show\")) ==\n               ~S<[[\"remove_class\",{\"names\":[\"show\"]}]]>\n    end\n  end\n\n  describe \"toggle_class\" do\n    test \"with defaults\" do\n      assert JS.toggle_class(\"show\") == %JS{\n               ops: [\n                 [\n                   \"toggle_class\",\n                   %{names: [\"show\"]}\n                 ]\n               ]\n             }\n\n      assert JS.toggle_class(\"show\", to: \"#modal\") == %JS{\n               ops: [\n                 [\n                   \"toggle_class\",\n                   %{names: [\"show\"], to: \"#modal\"}\n                 ]\n               ]\n             }\n\n      assert JS.toggle_class(\"show\", to: {:document, \"#modal\"}) == %JS{\n               ops: [\n                 [\n                   \"toggle_class\",\n                   %{names: [\"show\"], to: \"#modal\"}\n                 ]\n               ]\n             }\n    end\n\n    test \"multiple classes\" do\n      assert JS.toggle_class(\"show hl\") == %JS{\n               ops: [\n                 [\n                   \"toggle_class\",\n                   %{names: [\"show\", \"hl\"]}\n                 ]\n               ]\n             }\n    end\n\n    test \"custom time\" do\n      assert JS.toggle_class(\"show\", time: 543) == %JS{\n               ops: [\n                 [\n                   \"toggle_class\",\n                   %{names: [\"show\"], time: 543}\n                 ]\n               ]\n             }\n    end\n\n    test \"transition\" do\n      assert JS.toggle_class(\"show\", transition: \"fade\") == %JS{\n               ops: [\n                 [\n                   \"toggle_class\",\n                   %{names: [\"show\"], transition: [[\"fade\"], [], []]}\n                 ]\n               ]\n             }\n\n      assert JS.toggle_class(\"show\", transition: \"fade\", blocking: false) == %JS{\n               ops: [\n                 [\n                   \"toggle_class\",\n                   %{names: [\"show\"], transition: [[\"fade\"], [], []], blocking: false}\n                 ]\n               ]\n             }\n\n      assert JS.toggle_class(\"c\", transition: \"a b\") == %JS{\n               ops: [\n                 [\n                   \"toggle_class\",\n                   %{names: [\"c\"], transition: [[\"a\", \"b\"], [], []]}\n                 ]\n               ]\n             }\n\n      assert JS.toggle_class(\"show\", transition: {\"fade\", \"opacity-0\", \"opacity-100\"}) == %JS{\n               ops: [\n                 [\n                   \"toggle_class\",\n                   %{\n                     names: [\"show\"],\n                     transition: [[\"fade\"], [\"opacity-0\"], [\"opacity-100\"]]\n                   }\n                 ]\n               ]\n             }\n    end\n\n    test \"composability\" do\n      js = JS.toggle_class(\"show\", to: \"#modal\", time: 100) |> JS.toggle_class(\"hl\")\n\n      assert js == %JS{\n               ops: [\n                 [\n                   \"toggle_class\",\n                   %{names: [\"show\"], time: 100, to: \"#modal\"}\n                 ],\n                 [\"toggle_class\", %{names: [\"hl\"]}]\n               ]\n             }\n    end\n\n    test \"raises with unknown options\" do\n      assert_raise ArgumentError, ~r/invalid option for toggle_class/, fn ->\n        JS.toggle_class(\"show\", bad: :opt)\n      end\n    end\n\n    test \"encoding\" do\n      assert js_to_string(JS.toggle_class(\"show\")) ==\n               ~S<[[\"toggle_class\",{\"names\":[\"show\"]}]]>\n    end\n  end\n\n  describe \"dispatch\" do\n    test \"with defaults\" do\n      assert JS.dispatch(\"click\", to: \"#modal\") == %JS{\n               ops: [[\"dispatch\", %{to: \"#modal\", event: \"click\"}]]\n             }\n\n      assert JS.dispatch(\"click\") == %JS{\n               ops: [[\"dispatch\", %{event: \"click\"}]]\n             }\n    end\n\n    test \"with optional flags\" do\n      assert JS.dispatch(\"click\", bubbles: false) == %JS{\n               ops: [[\"dispatch\", %{event: \"click\", bubbles: false}]]\n             }\n    end\n\n    test \"raises with unknown options\" do\n      assert_raise ArgumentError, ~r/invalid option for dispatch/, fn ->\n        JS.dispatch(\"click\", to: \".foo\", bad: :opt)\n      end\n\n      assert_raise ArgumentError, ~r/invalid scope for :to option in dispatch/, fn ->\n        JS.dispatch(\"click\", to: {:winner, \".foo\"})\n      end\n    end\n\n    test \"raises with click details\" do\n      assert_raise ArgumentError, ~r/click events cannot be dispatched with details/, fn ->\n        JS.dispatch(\"click\", to: \".foo\", detail: %{id: 123})\n      end\n    end\n\n    test \"composability\" do\n      js =\n        JS.dispatch(\"click\", to: \"#modal\")\n        |> JS.dispatch(\"keydown\", to: \"#keyboard\")\n        |> JS.dispatch(\"keyup\")\n\n      assert js == %JS{\n               ops: [\n                 [\"dispatch\", %{to: \"#modal\", event: \"click\"}],\n                 [\"dispatch\", %{to: \"#keyboard\", event: \"keydown\"}],\n                 [\"dispatch\", %{event: \"keyup\"}]\n               ]\n             }\n    end\n\n    test \"encoding\" do\n      assert js_to_string(JS.dispatch(\"click\", to: \".foo\")) ==\n               ~S<[[\"dispatch\",{\"event\":\"click\",\"to\":\".foo\"}]]>\n    end\n\n    test \"raises when done is a details key and blocking is true\" do\n      assert_raise ArgumentError, ~r/must not contain a `done` key/, fn ->\n        JS.dispatch(\"foo\", detail: %{done: true}, blocking: true)\n      end\n    end\n\n    test \"raises when detail is not a map\" do\n      assert_raise ArgumentError, ~r/the detail option to JS.dispatch must be a map/, fn ->\n        JS.dispatch(\"foo\", detail: \"not-a-map\")\n      end\n    end\n  end\n\n  describe \"toggle\" do\n    test \"with defaults\" do\n      assert JS.toggle(to: \"#modal\") == %JS{\n               ops: [\n                 [\n                   \"toggle\",\n                   %{to: \"#modal\"}\n                 ]\n               ]\n             }\n\n      assert JS.toggle(to: {:closest, \".modal\"}) == %JS{\n               ops: [\n                 [\n                   \"toggle\",\n                   %{to: %{closest: \".modal\"}}\n                 ]\n               ]\n             }\n    end\n\n    test \"in and out classes\" do\n      assert JS.toggle(to: \"#modal\", in: \"fade-in d-block\", out: \"fade-out d-block\") ==\n               %JS{\n                 ops: [\n                   [\n                     \"toggle\",\n                     %{\n                       ins: [[\"fade-in\", \"d-block\"], [], []],\n                       outs: [[\"fade-out\", \"d-block\"], [], []],\n                       to: \"#modal\"\n                     }\n                   ]\n                 ]\n               }\n\n      assert JS.toggle(\n               to: \"#modal\",\n               in: {\"fade-in\", \"opacity-0\", \"opacity-100\"},\n               out: {\"fade-out\", \"opacity-100\", \"opacity-0\"}\n             ) ==\n               %JS{\n                 ops: [\n                   [\n                     \"toggle\",\n                     %{\n                       ins: [[\"fade-in\"], [\"opacity-0\"], [\"opacity-100\"]],\n                       outs: [[\"fade-out\"], [\"opacity-100\"], [\"opacity-0\"]],\n                       to: \"#modal\"\n                     }\n                   ]\n                 ]\n               }\n    end\n\n    test \"custom time\" do\n      assert JS.toggle(to: \"#modal\", time: 123) == %JS{\n               ops: [\n                 [\n                   \"toggle\",\n                   %{time: 123, to: \"#modal\"}\n                 ]\n               ]\n             }\n    end\n\n    test \"custom display\" do\n      assert JS.toggle(to: \"#modal\", display: \"block\") == %JS{\n               ops: [\n                 [\n                   \"toggle\",\n                   %{\n                     display: \"block\",\n                     to: \"#modal\"\n                   }\n                 ]\n               ]\n             }\n    end\n\n    test \"raises with unknown options\" do\n      assert_raise ArgumentError, ~r/invalid option for toggle/, fn ->\n        JS.toggle(to: \"#modal\", bad: :opt)\n      end\n\n      assert_raise ArgumentError, ~r/invalid scope for :to option in toggle/, fn ->\n        JS.toggle(to: \"#modal\", to: {:bad, \"123\"})\n      end\n    end\n\n    test \"composability\" do\n      js = JS.toggle(to: \"#modal\") |> JS.toggle(to: \"#keyboard\", time: 123)\n\n      assert js == %JS{\n               ops: [\n                 [\"toggle\", %{to: \"#modal\"}],\n                 [\"toggle\", %{to: \"#keyboard\", time: 123}]\n               ]\n             }\n    end\n\n    test \"encoding\" do\n      assert js_to_string(JS.toggle(to: \"#modal\")) ==\n               ~S<[[\"toggle\",{\"to\":\"#modal\"}]]>\n    end\n  end\n\n  describe \"show\" do\n    test \"with defaults\" do\n      assert JS.show(to: \"#modal\") == %JS{\n               ops: [[\"show\", %{to: \"#modal\"}]]\n             }\n\n      assert JS.show(to: {:inner, \".modal\"}) == %JS{\n               ops: [[\"show\", %{to: %{inner: \".modal\"}}]]\n             }\n    end\n\n    test \"transition classes\" do\n      assert JS.show(to: \"#modal\", transition: \"fade-in d-block\") ==\n               %JS{\n                 ops: [\n                   [\n                     \"show\",\n                     %{\n                       transition: [[\"fade-in\", \"d-block\"], [], []],\n                       to: \"#modal\"\n                     }\n                   ]\n                 ]\n               }\n\n      assert JS.show(\n               to: \"#modal\",\n               transition:\n                 {\"fade-in d-block\", \"opacity-0 -translate-x-full\", \"opacity-100 translate-x-0\"}\n             ) ==\n               %JS{\n                 ops: [\n                   [\n                     \"show\",\n                     %{\n                       transition: [\n                         [\"fade-in\", \"d-block\"],\n                         [\"opacity-0\", \"-translate-x-full\"],\n                         [\"opacity-100\", \"translate-x-0\"]\n                       ],\n                       to: \"#modal\"\n                     }\n                   ]\n                 ]\n               }\n    end\n\n    test \"custom time\" do\n      assert JS.show(to: \"#modal\", time: 123) == %JS{\n               ops: [[\"show\", %{time: 123, to: \"#modal\"}]]\n             }\n    end\n\n    test \"custom display\" do\n      assert JS.show(to: \"#modal\", display: \"block\") == %JS{\n               ops: [\n                 [\"show\", %{display: \"block\", to: \"#modal\"}]\n               ]\n             }\n    end\n\n    test \"raises with unknown options\" do\n      assert_raise ArgumentError, ~r/invalid option for show/, fn ->\n        JS.show(to: \"#modal\", bad: :opt)\n      end\n\n      assert_raise ArgumentError, ~r/invalid scope for :to option in show/, fn ->\n        JS.show(to: {:bad, \"#modal\"})\n      end\n    end\n\n    test \"composability\" do\n      js = JS.show(to: \"#modal\") |> JS.show(to: \"#keyboard\", time: 123)\n\n      assert js == %JS{\n               ops: [\n                 [\"show\", %{to: \"#modal\"}],\n                 [\"show\", %{to: \"#keyboard\", time: 123}]\n               ]\n             }\n    end\n\n    test \"encoding\" do\n      assert js_to_string(JS.show(to: \"#modal\")) ==\n               ~S<[[\"show\",{\"to\":\"#modal\"}]]>\n    end\n  end\n\n  describe \"hide\" do\n    test \"with defaults\" do\n      assert JS.hide(to: \"#modal\") == %JS{\n               ops: [[\"hide\", %{to: \"#modal\"}]]\n             }\n\n      assert JS.hide(to: {:closest, \"a\"}) == %JS{\n               ops: [[\"hide\", %{to: %{closest: \"a\"}}]]\n             }\n    end\n\n    test \"transition classes\" do\n      assert JS.hide(to: \"#modal\", transition: \"fade-out d-block\") ==\n               %JS{\n                 ops: [\n                   [\n                     \"hide\",\n                     %{\n                       transition: [[\"fade-out\", \"d-block\"], [], []],\n                       to: \"#modal\"\n                     }\n                   ]\n                 ]\n               }\n\n      assert JS.hide(\n               to: \"#modal\",\n               transition:\n                 {\"fade-in d-block\", \"opacity-0 -translate-x-full\", \"opacity-100 translate-x-0\"}\n             ) ==\n               %JS{\n                 ops: [\n                   [\n                     \"hide\",\n                     %{\n                       transition: [\n                         [\"fade-in\", \"d-block\"],\n                         [\"opacity-0\", \"-translate-x-full\"],\n                         [\"opacity-100\", \"translate-x-0\"]\n                       ],\n                       to: \"#modal\"\n                     }\n                   ]\n                 ]\n               }\n    end\n\n    test \"custom time\" do\n      assert JS.hide(to: \"#modal\", time: 123) == %JS{\n               ops: [[\"hide\", %{time: 123, to: \"#modal\"}]]\n             }\n    end\n\n    test \"raises with unknown options\" do\n      assert_raise ArgumentError, ~r/invalid option for hide/, fn ->\n        JS.hide(to: \"#modal\", bad: :opt)\n      end\n\n      assert_raise ArgumentError, ~r/invalid scope for :to option in hide/, fn ->\n        JS.hide(to: {:bad, \"#modal\"})\n      end\n    end\n\n    test \"composability\" do\n      js = JS.hide(to: \"#modal\") |> JS.hide(to: \"#keyboard\", time: 123)\n\n      assert js == %JS{\n               ops: [\n                 [\"hide\", %{to: \"#modal\"}],\n                 [\"hide\", %{to: \"#keyboard\", time: 123}]\n               ]\n             }\n    end\n\n    test \"encoding\" do\n      assert js_to_string(JS.hide(to: \"#modal\")) ==\n               ~S<[[\"hide\",{\"to\":\"#modal\"}]]>\n    end\n  end\n\n  describe \"transition\" do\n    test \"with defaults\" do\n      assert JS.transition(\"shake\") == %JS{\n               ops: [[\"transition\", %{transition: [[\"shake\"], [], []]}]]\n             }\n\n      assert JS.transition(\"shake\", to: \"#modal\") == %JS{\n               ops: [[\"transition\", %{transition: [[\"shake\"], [], []], to: \"#modal\"}]]\n             }\n\n      assert JS.transition(\"shake\", to: {:inner, \"a\"}) == %JS{\n               ops: [[\"transition\", %{transition: [[\"shake\"], [], []], to: %{inner: \"a\"}}]]\n             }\n\n      assert JS.transition(\"shake swirl\", to: \"#modal\") == %JS{\n               ops: [\n                 [\n                   \"transition\",\n                   %{transition: [[\"shake\", \"swirl\"], [], []], to: \"#modal\"}\n                 ]\n               ]\n             }\n\n      assert JS.transition({\"shake swirl\", \"opacity-0 a\", \"opacity-100 b\"}, to: \"#modal\") == %JS{\n               ops: [\n                 [\n                   \"transition\",\n                   %{\n                     transition: [[\"shake\", \"swirl\"], [\"opacity-0\", \"a\"], [\"opacity-100\", \"b\"]],\n                     to: \"#modal\"\n                   }\n                 ]\n               ]\n             }\n    end\n\n    test \"custom time\" do\n      assert JS.transition(\"shake\", to: \"#modal\", time: 123) == %JS{\n               ops: [[\"transition\", %{transition: [[\"shake\"], [], []], time: 123, to: \"#modal\"}]]\n             }\n    end\n\n    test \"raises with unknown options\" do\n      assert_raise ArgumentError, ~r/invalid option for transition/, fn ->\n        JS.transition(\"shake\", to: \"#modal\", bad: :opt)\n      end\n\n      assert_raise ArgumentError, ~r/invalid scope for :to option in transition/, fn ->\n        JS.transition(\"shake\", to: {:bad, \"#modal\"})\n      end\n    end\n\n    test \"composability\" do\n      js = JS.transition(\"shake\", to: \"#modal\") |> JS.transition(\"hl\", to: \"#keyboard\", time: 123)\n\n      assert js == %JS{\n               ops: [\n                 [\"transition\", %{to: \"#modal\", transition: [[\"shake\"], [], []]}],\n                 [\"transition\", %{to: \"#keyboard\", transition: [[\"hl\"], [], []], time: 123}]\n               ]\n             }\n    end\n\n    test \"encoding\" do\n      assert js_to_string(JS.transition(\"shake\", to: \"#modal\")) ==\n               ~S<[[\"transition\",{\"to\":\"#modal\",\"transition\":[[\"shake\"],[],[]]}]]>\n    end\n  end\n\n  describe \"set_attribute\" do\n    test \"with defaults\" do\n      assert JS.set_attribute({\"aria-expanded\", \"true\"}) == %JS{\n               ops: [\n                 [\"set_attr\", %{attr: [\"aria-expanded\", \"true\"]}]\n               ]\n             }\n\n      assert JS.set_attribute({\"aria-expanded\", \"true\"}, to: \"#dropdown\") == %JS{\n               ops: [\n                 [\"set_attr\", %{attr: [\"aria-expanded\", \"true\"], to: \"#dropdown\"}]\n               ]\n             }\n\n      assert JS.set_attribute({\"aria-expanded\", \"true\"}, to: {:inner, \".dropdown\"}) == %JS{\n               ops: [\n                 [\"set_attr\", %{attr: [\"aria-expanded\", \"true\"], to: %{inner: \".dropdown\"}}]\n               ]\n             }\n    end\n\n    test \"composability\" do\n      js =\n        JS.set_attribute({\"expanded\", \"true\"})\n        |> JS.set_attribute({\"has-popup\", \"true\"})\n        |> JS.set_attribute({\"has-popup\", \"true\"}, to: \"#dropdown\")\n\n      assert js == %JS{\n               ops: [\n                 [\"set_attr\", %{attr: [\"expanded\", \"true\"]}],\n                 [\"set_attr\", %{attr: [\"has-popup\", \"true\"]}],\n                 [\"set_attr\", %{to: \"#dropdown\", attr: [\"has-popup\", \"true\"]}]\n               ]\n             }\n    end\n\n    test \"raises with unknown options\" do\n      assert_raise ArgumentError, ~r/invalid option for set_attribute/, fn ->\n        JS.set_attribute({\"disabled\", \"\"}, bad: :opt)\n      end\n\n      assert_raise ArgumentError, ~r/invalid scope for :to option in set_attribute/, fn ->\n        JS.set_attribute({\"disabled\", \"\"}, to: {:bad, \"#modal\"})\n      end\n    end\n\n    test \"encoding\" do\n      assert js_to_string(JS.set_attribute({\"disabled\", \"true\"})) ==\n               ~S<[[\"set_attr\",{\"attr\":[\"disabled\",\"true\"]}]]>\n    end\n  end\n\n  describe \"remove_attribute\" do\n    test \"with defaults\" do\n      assert JS.remove_attribute(\"aria-expanded\") == %JS{\n               ops: [\n                 [\"remove_attr\", %{attr: \"aria-expanded\"}]\n               ]\n             }\n\n      assert JS.remove_attribute(\"aria-expanded\", to: \"#dropdown\") == %JS{\n               ops: [\n                 [\"remove_attr\", %{attr: \"aria-expanded\", to: \"#dropdown\"}]\n               ]\n             }\n    end\n\n    test \"composability\" do\n      js =\n        JS.remove_attribute(\"expanded\")\n        |> JS.remove_attribute(\"has-popup\")\n        |> JS.remove_attribute(\"has-popup\", to: \"#dropdown\")\n\n      assert js == %JS{\n               ops: [\n                 [\"remove_attr\", %{attr: \"expanded\"}],\n                 [\"remove_attr\", %{attr: \"has-popup\"}],\n                 [\"remove_attr\", %{to: \"#dropdown\", attr: \"has-popup\"}]\n               ]\n             }\n    end\n\n    test \"raises with unknown options\" do\n      assert_raise ArgumentError, ~r/invalid option for remove_attribute/, fn ->\n        JS.remove_attribute(\"disabled\", bad: :opt)\n      end\n    end\n\n    test \"encoding\" do\n      assert js_to_string(JS.remove_attribute(\"disabled\")) ==\n               ~S<[[\"remove_attr\",{\"attr\":\"disabled\"}]]>\n    end\n  end\n\n  describe \"toggle_attribute\" do\n    test \"with defaults\" do\n      assert JS.toggle_attribute({\"open\", \"true\"}) == %JS{\n               ops: [\n                 [\"toggle_attr\", %{attr: [\"open\", \"true\"]}]\n               ]\n             }\n\n      assert JS.toggle_attribute({\"open\", \"true\"}, to: \"#dropdown\") == %JS{\n               ops: [\n                 [\"toggle_attr\", %{attr: [\"open\", \"true\"], to: \"#dropdown\"}]\n               ]\n             }\n\n      assert JS.toggle_attribute({\"aria-expanded\", \"true\", \"false\"}, to: \"#dropdown\") == %JS{\n               ops: [\n                 [\"toggle_attr\", %{attr: [\"aria-expanded\", \"true\", \"false\"], to: \"#dropdown\"}]\n               ]\n             }\n\n      assert JS.toggle_attribute({\"aria-expanded\", \"true\", \"false\"}, to: {:inner, \".dropdown\"}) ==\n               %JS{\n                 ops: [\n                   [\n                     \"toggle_attr\",\n                     %{attr: [\"aria-expanded\", \"true\", \"false\"], to: %{inner: \".dropdown\"}}\n                   ]\n                 ]\n               }\n    end\n\n    test \"composability\" do\n      js =\n        {\"aria-expanded\", \"true\", \"false\"}\n        |> JS.toggle_attribute()\n        |> JS.toggle_attribute({\"open\", \"true\"})\n        |> JS.toggle_attribute({\"disabled\", \"true\"}, to: \"#dropdown\")\n\n      assert js == %JS{\n               ops: [\n                 [\"toggle_attr\", %{attr: [\"aria-expanded\", \"true\", \"false\"]}],\n                 [\"toggle_attr\", %{attr: [\"open\", \"true\"]}],\n                 [\"toggle_attr\", %{to: \"#dropdown\", attr: [\"disabled\", \"true\"]}]\n               ]\n             }\n    end\n\n    test \"raises with unknown options\" do\n      assert_raise ArgumentError, ~r/invalid option for toggle_attribute/, fn ->\n        JS.toggle_attribute({\"disabled\", \"true\"}, bad: :opt)\n      end\n\n      assert_raise ArgumentError, ~r/invalid scope for :to option in toggle_attribute/, fn ->\n        JS.toggle_attribute({\"disabled\", \"true\"}, to: {:bad, \"123\"})\n      end\n    end\n\n    test \"encoding\" do\n      assert js_to_string(JS.toggle_attribute({\"disabled\", \"true\"})) ==\n               ~S<[[\"toggle_attr\",{\"attr\":[\"disabled\",\"true\"]}]]>\n\n      assert js_to_string(JS.toggle_attribute({\"aria-expanded\", \"true\", \"false\"})) ==\n               ~S<[[\"toggle_attr\",{\"attr\":[\"aria-expanded\",\"true\",\"false\"]}]]>\n    end\n  end\n\n  describe \"focus\" do\n    test \"with defaults\" do\n      assert JS.focus() == %JS{ops: [[\"focus\", %{}]]}\n      assert JS.focus(to: \"input\") == %JS{ops: [[\"focus\", %{to: \"input\"}]]}\n      assert JS.focus(to: {:inner, \"input\"}) == %JS{ops: [[\"focus\", %{to: %{inner: \"input\"}}]]}\n    end\n\n    test \"composability\" do\n      js =\n        JS.set_attribute({\"expanded\", \"true\"})\n        |> JS.focus()\n\n      assert js == %JS{\n               ops: [[\"set_attr\", %{attr: [\"expanded\", \"true\"]}], [\"focus\", %{}]]\n             }\n    end\n\n    test \"raises with unknown options\" do\n      assert_raise ArgumentError, ~r/invalid option for focus/, fn ->\n        JS.focus(bad: :opt)\n      end\n\n      assert_raise ArgumentError, ~r/invalid scope for :to option in focus/, fn ->\n        JS.focus(to: {:bad, \"a\"})\n      end\n    end\n\n    test \"encoding\" do\n      assert js_to_string(JS.focus()) == ~S<[[\"focus\",{}]]>\n    end\n  end\n\n  describe \"focus_first\" do\n    test \"with defaults\" do\n      assert JS.focus_first() == %JS{ops: [[\"focus_first\", %{}]]}\n      assert JS.focus_first(to: \"input\") == %JS{ops: [[\"focus_first\", %{to: \"input\"}]]}\n\n      assert JS.focus_first(to: {:inner, \"input\"}) == %JS{\n               ops: [[\"focus_first\", %{to: %{inner: \"input\"}}]]\n             }\n    end\n\n    test \"composability\" do\n      js =\n        JS.set_attribute({\"expanded\", \"true\"})\n        |> JS.focus_first()\n\n      assert js == %JS{\n               ops: [\n                 [\"set_attr\", %{attr: [\"expanded\", \"true\"]}],\n                 [\"focus_first\", %{}]\n               ]\n             }\n    end\n\n    test \"raises with unknown options\" do\n      assert_raise ArgumentError, ~r/invalid option for focus_first/, fn ->\n        JS.focus_first(bad: :opt)\n      end\n\n      assert_raise ArgumentError, ~r/invalid scope for :to option in focus_first/, fn ->\n        JS.focus_first(to: {:bad, \"a\"})\n      end\n    end\n\n    test \"encoding\" do\n      assert js_to_string(JS.focus_first()) == ~S<[[\"focus_first\",{}]]>\n    end\n  end\n\n  describe \"push_focus\" do\n    test \"with defaults\" do\n      assert JS.push_focus() == %JS{ops: [[\"push_focus\", %{}]]}\n      assert JS.push_focus(to: \"input\") == %JS{ops: [[\"push_focus\", %{to: \"input\"}]]}\n\n      assert JS.push_focus(to: {:inner, \"input\"}) == %JS{\n               ops: [[\"push_focus\", %{to: %{inner: \"input\"}}]]\n             }\n    end\n\n    test \"composability\" do\n      js =\n        JS.set_attribute({\"expanded\", \"true\"})\n        |> JS.push_focus()\n\n      assert js == %JS{\n               ops: [\n                 [\"set_attr\", %{attr: [\"expanded\", \"true\"]}],\n                 [\"push_focus\", %{}]\n               ]\n             }\n    end\n\n    test \"raises with unknown options\" do\n      assert_raise ArgumentError, ~r/invalid option for push_focus/, fn ->\n        JS.push_focus(bad: :opt)\n      end\n\n      assert_raise ArgumentError, ~r/invalid scope for :to option in push_focus/, fn ->\n        JS.push_focus(to: {:bad, \"a\"})\n      end\n    end\n\n    test \"encoding\" do\n      assert js_to_string(JS.push_focus()) == ~S<[[\"push_focus\",{}]]>\n    end\n  end\n\n  describe \"pop_focus\" do\n    test \"with defaults\" do\n      assert JS.pop_focus() == %JS{ops: [[\"pop_focus\", %{}]]}\n    end\n\n    test \"composability\" do\n      js =\n        JS.set_attribute({\"expanded\", \"true\"})\n        |> JS.pop_focus()\n\n      assert js == %JS{\n               ops: [[\"set_attr\", %{attr: [\"expanded\", \"true\"]}], [\"pop_focus\", %{}]]\n             }\n    end\n\n    test \"encoding\" do\n      assert js_to_string(JS.pop_focus()) == ~S<[[\"pop_focus\",{}]]>\n    end\n  end\n\n  describe \"concat\" do\n    test \"combines multiple JS structs\" do\n      js1 = JS.push(\"inc\", value: %{one: 1, two: 2})\n      js2 = JS.add_class(\"show\", to: \"#modal\", time: 100)\n      js3 = JS.remove_class(\"show\")\n\n      assert JS.concat(js1, js2) |> JS.concat(js3) == %JS{\n               ops: [\n                 [\"push\", %{event: \"inc\", value: %{one: 1, two: 2}}],\n                 [\"add_class\", %{names: [\"show\"], time: 100, to: \"#modal\"}],\n                 [\"remove_class\", %{names: [\"show\"]}]\n               ]\n             }\n    end\n  end\n\n  defp js_to_string(%JS{} = js) do\n    js\n    |> Map.update!(:ops, &order_ops_map_keys/1)\n    |> JS.to_encodable()\n    |> Jason.encode!()\n  end\n\n  defp order_ops_map_keys(ops) when is_list(ops) do\n    Enum.map(ops, &order_ops_map_keys/1)\n  end\n\n  defp order_ops_map_keys(ops) when is_map(ops) do\n    ops\n    |> Enum.map(&order_ops_map_keys/1)\n    |> Enum.sort_by(fn {k, _v} -> k end)\n    |> Jason.OrderedObject.new()\n  end\n\n  defp order_ops_map_keys(ops) do\n    ops\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/live_stream_test.exs",
    "content": "defmodule Phoenix.LiveView.LiveStreamTest do\n  use ExUnit.Case, async: true\n\n  alias Phoenix.LiveView.LiveStream\n\n  test \"new raises on invalid options\" do\n    msg = ~r/stream :dom_id must return a function which accepts each item, got: false/\n\n    assert_raise ArgumentError, msg, fn ->\n      LiveStream.new(:numbers, 0, [1, 2, 3], dom_id: false)\n    end\n  end\n\n  test \"default dom_id\" do\n    stream = LiveStream.new(:users, 0, [%{id: 1}, %{id: 2}], [])\n\n    assert stream.inserts == [\n             {\"users-2\", -1, %{id: 2}, nil, nil},\n             {\"users-1\", -1, %{id: 1}, nil, nil}\n           ]\n  end\n\n  test \"custom dom_id\" do\n    stream = LiveStream.new(:users, 0, [%{name: \"u1\"}, %{name: \"u2\"}], dom_id: &\"u-#{&1.name}\")\n\n    assert stream.inserts == [\n             {\"u-u2\", -1, %{name: \"u2\"}, nil, nil},\n             {\"u-u1\", -1, %{name: \"u1\"}, nil, nil}\n           ]\n  end\n\n  test \"default dom_id without struct or map with :id\" do\n    msg = ~r/expected stream :users to be a struct or map with :id key/\n\n    assert_raise ArgumentError, msg, fn ->\n      LiveStream.new(:users, 0, [%{user_id: 1}, %{user_id: 2}], [])\n    end\n  end\n\n  test \"inserts are deduplicated (last insert wins)\" do\n    assert stream = LiveStream.new(:users, 0, [%{id: 1}, %{id: 2}], [])\n    stream = LiveStream.insert_item(stream, %{id: 2, updated: true}, -1, nil, nil)\n    stream = %{stream | consumable?: true}\n    assert Enum.to_list(stream) == [{\"users-1\", %{id: 1}}, {\"users-2\", %{id: 2, updated: true}}]\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/plug_test.exs",
    "content": "defmodule Phoenix.LiveView.PlugTest do\n  use ExUnit.Case, async: true\n  import Plug.Conn\n  import Phoenix.ConnTest\n\n  alias Phoenix.LiveView.Plug, as: LiveViewPlug\n  alias Phoenix.LiveViewTest.Support.{ThermostatLive, DashboardLive, Endpoint}\n\n  defp call(conn, view, opts \\\\ []) do\n    opts = Keyword.merge([router: Phoenix.LiveViewTest.Support.Router, layout: false], opts)\n\n    conn\n    |> Plug.Test.init_test_session(%{})\n    |> Phoenix.LiveView.Router.fetch_live_flash([])\n    |> put_private(:phoenix_live_view, {view, opts, %{name: :default, extra: %{}, vsn: 0}})\n    |> LiveViewPlug.call(view)\n  end\n\n  setup config do\n    conn =\n      build_conn()\n      |> fetch_query_params()\n      |> Plug.Test.init_test_session(config[:plug_session] || %{})\n      |> Plug.Conn.put_private(:phoenix_router, Router)\n      |> Plug.Conn.put_private(:phoenix_endpoint, Endpoint)\n\n    {:ok, conn: conn}\n  end\n\n  def with_session(%Plug.Conn{}, key, value) do\n    %{key => value}\n  end\n\n  test \"without session opts\", %{conn: conn} do\n    conn = call(conn, DashboardLive)\n    assert conn.resp_body =~ ~s(session: %{})\n  end\n\n  @tag plug_session: %{user_id: \"alex\"}\n  test \"with user session\", %{conn: conn} do\n    conn = call(conn, DashboardLive)\n    assert conn.resp_body =~ ~s(session: %{\"user_id\" => \"alex\"})\n  end\n\n  test \"with a module container\", %{conn: conn} do\n    conn = call(conn, ThermostatLive)\n\n    assert conn.resp_body =~\n             ~r/<article[^>]*class=\"thermo\"[^>]*>/\n  end\n\n  test \"with container options\", %{conn: conn} do\n    conn = call(conn, DashboardLive, container: {:span, style: \"phx-flex\"})\n\n    assert conn.resp_body =~\n             ~r/<span[^>]*class=\"Phoenix.LiveViewTest.Support.DashboardLive\"[^>]*style=\"phx-flex\">/\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/router_test.exs",
    "content": "defmodule Phoenix.LiveView.RouterTest do\n  use ExUnit.Case, async: true\n  import Phoenix.ConnTest\n  import Phoenix.LiveViewTest\n\n  alias Phoenix.LiveView.{Route, Session}\n  alias Phoenix.LiveViewTest.{DOM, TreeDOM}\n  alias Phoenix.LiveViewTest.Support.{Endpoint, DashboardLive}\n  alias Phoenix.LiveViewTest.Support.Router.Helpers, as: Routes\n\n  @endpoint Endpoint\n\n  def verified_session(html) do\n    [{id, session_token, static_token} | _] =\n      html |> DOM.parse_document() |> elem(1) |> TreeDOM.find_live_views()\n\n    {:ok, live_session} =\n      Session.verify_session(@endpoint, \"lv:#{id}\", session_token, static_token)\n\n    live_session.session\n  end\n\n  setup config do\n    conn = Plug.Test.init_test_session(build_conn(), config[:plug_session] || %{})\n    {:ok, conn: conn}\n  end\n\n  test \"routing at root\", %{conn: conn} do\n    {:ok, _view, html} = live(conn, \"/\")\n    assert html =~ ~r/<article[^>]*class=\"thermo\"[^>]*>/\n  end\n\n  test \"routing with empty session\", %{conn: conn} do\n    conn = get(conn, \"/router/thermo_defaults/123\")\n    assert conn.resp_body =~ ~s()\n  end\n\n  @tag plug_session: %{user_id: \"chris\"}\n  test \"routing with custom session\", %{conn: conn} do\n    conn = get(conn, \"/router/thermo_session/123\")\n    assert conn.resp_body =~ ~s(session: %{\"user_id\" => \"chris\"})\n  end\n\n  test \"routing with module container\", %{conn: conn} do\n    conn = get(conn, \"/thermo\")\n    assert conn.resp_body =~ ~r/<article[^>]*class=\"thermo\"[^>]*>/\n  end\n\n  test \"routing with container\", %{conn: conn} do\n    conn = get(conn, \"/router/thermo_container/123\")\n\n    assert conn.resp_body =~\n             ~r/<span[^>]*class=\"Phoenix.LiveViewTest.Support.DashboardLive\"[^>]*style=\"flex-grow\">/\n  end\n\n  test \"live non-action helpers\", %{conn: conn} do\n    assert Routes.live_path(conn, DashboardLive, 1) == \"/router/thermo_defaults/1\"\n    assert Routes.custom_live_path(conn, DashboardLive, 1) == \"/router/thermo_session/custom/1\"\n  end\n\n  test \"live action helpers\", %{conn: conn} do\n    assert Routes.foo_bar_path(conn, :index) == \"/router/foobarbaz\"\n    assert Routes.foo_bar_index_path(conn, :index) == \"/router/foobarbaz/index\"\n    assert Routes.foo_bar_index_path(conn, :show) == \"/router/foobarbaz/show\"\n    assert Routes.foo_bar_nested_index_path(conn, :index) == \"/router/foobarbaz/nested/index\"\n    assert Routes.foo_bar_nested_index_path(conn, :show) == \"/router/foobarbaz/nested/show\"\n    assert Routes.custom_foo_bar_path(conn, :index) == \"/router/foobarbaz/custom\"\n    assert Routes.nested_module_path(conn, :action) == \"/router/foobarbaz/with_live\"\n    assert Routes.custom_route_path(conn, :index) == \"/router/foobarbaz/nosuffix\"\n  end\n\n  test \"user-defined metadata is available inside of metadata key\" do\n    assert Phoenix.LiveViewTest.Support.Router\n           |> Phoenix.Router.route_info(\"GET\", \"/thermo-with-metadata\", nil)\n           |> Map.get(:route_name) == \"opts\"\n  end\n\n  describe \"live_session\" do\n    test \"with defaults\", %{conn: conn} do\n      path = \"/thermo-live-session\"\n\n      assert {:internal, route} =\n               Route.live_link_info_without_checks(\n                 @endpoint,\n                 Phoenix.LiveViewTest.Support.Router,\n                 path\n               )\n\n      assert route.live_session.name == :test\n\n      assert conn |> get(path) |> html_response(200) |> verified_session() == %{}\n    end\n\n    test \"with extra session metadata\", %{conn: conn} do\n      path = \"/thermo-live-session-admin\"\n\n      assert {:internal, route} =\n               Route.live_link_info_without_checks(\n                 @endpoint,\n                 Phoenix.LiveViewTest.Support.Router,\n                 path\n               )\n\n      assert route.live_session.name == :admin\n\n      assert conn |> get(path) |> html_response(200) |> verified_session() ==\n               %{\"admin\" => true}\n    end\n\n    test \"with session MFA metadata\", %{conn: conn} do\n      path = \"/thermo-live-session-mfa\"\n\n      assert {:internal, route} =\n               Route.live_link_info_without_checks(\n                 @endpoint,\n                 Phoenix.LiveViewTest.Support.Router,\n                 path\n               )\n\n      assert route.live_session.name == :mfa\n\n      assert conn |> get(path) |> html_response(200) |> verified_session() ==\n               %{\"inlined\" => true, \"called\" => true}\n    end\n\n    test \"with on_mount hook\", %{conn: conn} do\n      path = \"/lifecycle/halt-connected-mount\"\n\n      assert {:internal, route} =\n               Route.live_link_info_without_checks(\n                 @endpoint,\n                 Phoenix.LiveViewTest.Support.Router,\n                 path\n               )\n\n      assert route.live_session.extra == %{\n               on_mount: [\n                 %{\n                   id: {Phoenix.LiveViewTest.Support.HaltConnectedMount, :default},\n                   stage: :mount,\n                   function:\n                     Function.capture(\n                       Phoenix.LiveViewTest.Support.HaltConnectedMount,\n                       :on_mount,\n                       4\n                     )\n                 }\n               ]\n             }\n\n      assert conn |> get(path) |> html_response(200) =~\n               \"last_on_mount:Phoenix.LiveViewTest.Support.HaltConnectedMount\"\n\n      assert {:error, {:live_redirect, %{to: \"/lifecycle\"}}} = live(conn, path)\n    end\n\n    test \"with on_mount {Module, arg}\", %{conn: conn} do\n      path = \"/lifecycle/mount-mod-arg\"\n\n      assert {:internal, route} =\n               Route.live_link_info_without_checks(\n                 @endpoint,\n                 Phoenix.LiveViewTest.Support.Router,\n                 path\n               )\n\n      assert route.live_session.extra == %{\n               on_mount: [\n                 %{\n                   id: {Phoenix.LiveViewTest.Support.MountArgs, :inlined},\n                   stage: :mount,\n                   function:\n                     Function.capture(Phoenix.LiveViewTest.Support.MountArgs, :on_mount, 4)\n                 }\n               ]\n             }\n\n      assert {:error, {:live_redirect, %{to: \"/lifecycle?called=true&inlined=true\"}}} =\n               live(conn, path)\n    end\n\n    test \"with on_mount [Module, ...]\", %{conn: conn} do\n      path = \"/lifecycle/mount-mods\"\n\n      assert {:internal, route} =\n               Route.live_link_info_without_checks(\n                 @endpoint,\n                 Phoenix.LiveViewTest.Support.Router,\n                 path\n               )\n\n      assert route.live_session.extra == %{\n               on_mount: [\n                 %{\n                   id: {Phoenix.LiveViewTest.Support.OnMount, :default},\n                   stage: :mount,\n                   function: Function.capture(Phoenix.LiveViewTest.Support.OnMount, :on_mount, 4)\n                 },\n                 %{\n                   id: {Phoenix.LiveViewTest.Support.OtherOnMount, :default},\n                   stage: :mount,\n                   function:\n                     Function.capture(Phoenix.LiveViewTest.Support.OtherOnMount, :on_mount, 4)\n                 }\n               ]\n             }\n\n      assert {:ok, _, _} = live(conn, path)\n    end\n\n    test \"with on_mount [{Module, arg}, ...]\", %{conn: conn} do\n      path = \"/lifecycle/mount-mods-args\"\n\n      assert {:internal, route} =\n               Route.live_link_info_without_checks(\n                 @endpoint,\n                 Phoenix.LiveViewTest.Support.Router,\n                 path\n               )\n\n      assert route.live_session.extra == %{\n               on_mount: [\n                 %{\n                   id: {Phoenix.LiveViewTest.Support.OnMount, :other},\n                   stage: :mount,\n                   function: Function.capture(Phoenix.LiveViewTest.Support.OnMount, :on_mount, 4)\n                 },\n                 %{\n                   id: {Phoenix.LiveViewTest.Support.OtherOnMount, :other},\n                   stage: :mount,\n                   function:\n                     Function.capture(Phoenix.LiveViewTest.Support.OtherOnMount, :on_mount, 4)\n                 }\n               ]\n             }\n\n      assert {:ok, _, _} = live(conn, path)\n    end\n\n    test \"raises when nesting\" do\n      assert_raise(RuntimeError, ~r\"attempting to define live_session :invalid inside :ok\", fn ->\n        Code.eval_quoted(\n          quote do\n            defmodule NestedRouter do\n              import Phoenix.LiveView.Router\n\n              live_session :ok do\n                live_session :invalid do\n                end\n              end\n            end\n          end\n        )\n      end)\n    end\n\n    test \"raises when redefining\" do\n      assert_raise(RuntimeError, ~r\"attempting to redefine live_session :one\", fn ->\n        Code.eval_quoted(\n          quote do\n            defmodule DupRouter do\n              import Phoenix.LiveView.Router\n\n              live_session :one do\n              end\n\n              live_session :two do\n              end\n\n              live_session :one do\n              end\n            end\n          end\n        )\n      end)\n    end\n\n    test \"with layout override\", %{conn: conn} do\n      path = \"/dashboard-live-session-layout\"\n\n      assert {:internal, route} =\n               Route.live_link_info_without_checks(\n                 @endpoint,\n                 Phoenix.LiveViewTest.Support.Router,\n                 path\n               )\n\n      assert route.live_session.extra == %{\n               layout: {Phoenix.LiveViewTest.Support.LayoutView, :live_override}\n             }\n\n      {:ok, view, html} = live(conn, path)\n\n      assert html =~\n               ~r|<div[^>]+>LIVEOVERRIDESTART\\-123\\-The value is: 123\\-LIVEOVERRIDEEND|\n\n      assert render(view) =~\n               ~r|<div[^>]+>LIVEOVERRIDESTART\\-123\\-The value is: 123\\-LIVEOVERRIDEEND|\n    end\n\n    test \"with layout override on disconnected render\", %{conn: conn} do\n      path = \"/dashboard-live-session-layout\"\n\n      assert {:internal, route} =\n               Route.live_link_info_without_checks(\n                 @endpoint,\n                 Phoenix.LiveViewTest.Support.Router,\n                 path\n               )\n\n      assert route.live_session.extra == %{\n               layout: {Phoenix.LiveViewTest.Support.LayoutView, :live_override}\n             }\n\n      conn = get(conn, path)\n\n      assert html_response(conn, 200) =~\n               ~r|<div[^>]+>LIVEOVERRIDESTART\\-123\\-The value is: 123\\-LIVEOVERRIDEEND|\n    end\n\n    test \"classifies route as external when same view, but different session\" do\n      # previously, a patch to the same LV, but a different path in a different live_session\n      # would succeed when it should not\n      {_, %Route{live_session: %{name: :test}}} =\n        Route.live_link_info_without_checks(\n          @endpoint,\n          Phoenix.LiveViewTest.Support.Router,\n          \"/clock-live-session\"\n        )\n\n      socket = %Phoenix.LiveView.Socket{\n        router: Phoenix.LiveViewTest.Support.Router,\n        endpoint: @endpoint,\n        private: %{live_session_name: :test}\n      }\n\n      assert {:external, _} =\n               Route.live_link_info!(\n                 socket,\n                 Phoenix.LiveViewTest.Support.ClockLive,\n                 \"/clock-live-session-admin\"\n               )\n\n      assert {:internal, _} =\n               Route.live_link_info!(\n                 socket,\n                 Phoenix.LiveViewTest.Support.ClockLive,\n                 \"/clock-live-session\"\n               )\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/socket_test.exs",
    "content": "defmodule Phoenix.LiveView.SocketTest do\n  use ExUnit.Case, async: true\n\n  test \"use with no override\" do\n    defmodule MySocket do\n      use Phoenix.LiveView.Socket\n    end\n\n    info = %{peer_data: %{}}\n    assert {:ok, %Phoenix.Socket{} = socket} = MySocket.connect(%{}, %Phoenix.Socket{}, info)\n    assert socket.private.connect_info == info\n    assert MySocket.id(socket) == nil\n  end\n\n  test \"use with overrides\" do\n    defmodule MyOverrides do\n      use Phoenix.LiveView.Socket\n\n      def connect(%{\"error\" => \"true\"}, _socket, _info) do\n        :error\n      end\n\n      def connect(_params, socket, info) do\n        {:ok, assign(socket, :info, info)}\n      end\n\n      def id(_socket), do: \"my-id\"\n    end\n\n    info = %{peer_data: %{}}\n    assert :error = MyOverrides.connect(%{\"error\" => \"true\"}, %Phoenix.Socket{}, info)\n    assert {:ok, %Phoenix.Socket{} = socket} = MyOverrides.connect(%{}, %Phoenix.Socket{}, info)\n    assert socket.private.connect_info == info\n    assert socket.assigns.info == info\n    assert MyOverrides.id(socket) == \"my-id\"\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/tag_engine/tokenizer_test.exs",
    "content": "defmodule Phoenix.LiveView.TagEngine.TokenizerTest do\n  use ExUnit.Case, async: true\n  alias Phoenix.LiveView.TagEngine.Tokenizer.ParseError\n  alias Phoenix.LiveView.TagEngine.Tokenizer\n\n  import ExUnit.CaptureIO\n\n  defp tokenizer_state(text), do: Tokenizer.init(0, \"nofile\", text, Phoenix.LiveView.HTMLEngine)\n\n  defp tokenize(text) do\n    Tokenizer.tokenize(text, [], [], {:text, :enabled}, tokenizer_state(text))\n    |> elem(0)\n    |> Enum.reverse()\n  end\n\n  describe \"text\" do\n    test \"represented as {:text, value}\" do\n      assert tokenize(\"Hello\") == [{:text, \"Hello\", %{line_end: 1, column_end: 6}}]\n    end\n\n    test \"with multiple lines\" do\n      tokens =\n        tokenize(\"\"\"\n        first\n        second\n        third\n        \"\"\")\n\n      assert tokens == [{:text, \"first\\nsecond\\nthird\\n\", %{line_end: 4, column_end: 1}}]\n    end\n\n    test \"keep line breaks unchanged\" do\n      assert tokenize(\"first\\nsecond\\r\\nthird\") == [\n               {:text, \"first\\nsecond\\r\\nthird\", %{line_end: 3, column_end: 6}}\n             ]\n    end\n  end\n\n  describe \"doctype\" do\n    test \"generated as text\" do\n      assert tokenize(\"<!doctype html>\") == [\n               {:text, \"<!doctype html>\", %{line_end: 1, column_end: 16}}\n             ]\n    end\n\n    test \"multiple lines\" do\n      assert tokenize(\"<!DOCTYPE\\nhtml\\n>  <br />\") == [\n               {:text, \"<!DOCTYPE\\nhtml\\n>  \", %{line_end: 3, column_end: 4}},\n               {:tag, \"br\", [],\n                %{column: 4, line: 3, closing: :void, tag_name: \"br\", inner_location: {3, 10}}}\n             ]\n    end\n\n    test \"incomplete\" do\n      assert_raise ParseError, ~r/unexpected end of string inside tag/, fn ->\n        tokenize(\"<!doctype html\")\n      end\n    end\n  end\n\n  describe \"comment\" do\n    test \"generated as text\" do\n      assert tokenize(\"Begin<!-- comment -->End\") == [\n               {:text, \"Begin<!-- comment -->End\",\n                %{line_end: 1, column_end: 25, context: [:comment_start, :comment_end]}}\n             ]\n    end\n\n    test \"followed by curly\" do\n      assert tokenize(\"<!-- comment -->{hello}text\") == [\n               {:text, \"<!-- comment -->\",\n                %{column_end: 17, context: [:comment_start, :comment_end], line_end: 1}},\n               {:body_expr, \"hello\", %{line: 1, column: 17}},\n               {:text, \"text\", %{line_end: 1, column_end: 28}}\n             ]\n    end\n\n    test \"multiple lines and wrapped by tags\" do\n      code = \"\"\"\n      <p>\n      <!--\n      <div>\n      -->\n      </p><br>\\\n      \"\"\"\n\n      assert [\n               {:tag, \"p\", [], %{line: 1, column: 1}},\n               {:text, \"\\n<!--\\n<div>\\n-->\\n\", %{line_end: 5, column_end: 1}},\n               {:close, :tag, \"p\", %{line: 5, column: 1}},\n               {:tag, \"br\", [], %{line: 5, column: 5}}\n             ] = tokenize(code)\n    end\n\n    test \"adds comment_start and comment_end\" do\n      first_part = \"\"\"\n      <p>\n      <!--\n      <div>\n      \"\"\"\n\n      {first_tokens, cont} =\n        Tokenizer.tokenize(first_part, [], [], {:text, :enabled}, tokenizer_state(first_part))\n\n      second_part = \"\"\"\n      </div>\n      -->\n      </p>\n      <div>\n        <p>Hello</p>\n      </div>\n      \"\"\"\n\n      {tokens, {:text, :enabled}} =\n        Tokenizer.tokenize(second_part, [], first_tokens, cont, tokenizer_state(second_part))\n\n      assert Enum.reverse(tokens) == [\n               {:tag, \"p\", [], %{column: 1, line: 1, inner_location: {1, 4}, tag_name: \"p\"}},\n               {:text, \"\\n<!--\\n<div>\\n\",\n                %{column_end: 1, context: [:comment_start], line_end: 4}},\n               {:text, \"</div>\\n-->\\n\", %{column_end: 1, context: [:comment_end], line_end: 3}},\n               {:close, :tag, \"p\", %{column: 1, line: 3, inner_location: {3, 1}, tag_name: \"p\"}},\n               {:text, \"\\n\", %{column_end: 1, line_end: 4}},\n               {:tag, \"div\", [], %{column: 1, line: 4, inner_location: {4, 6}, tag_name: \"div\"}},\n               {:text, \"\\n  \", %{column_end: 3, line_end: 5}},\n               {:tag, \"p\", [], %{column: 3, line: 5, inner_location: {5, 6}, tag_name: \"p\"}},\n               {:text, \"Hello\", %{column_end: 11, line_end: 5}},\n               {:close, :tag, \"p\",\n                %{column: 11, line: 5, inner_location: {5, 11}, tag_name: \"p\"}},\n               {:text, \"\\n\", %{column_end: 1, line_end: 6}},\n               {:close, :tag, \"div\",\n                %{column: 1, line: 6, inner_location: {6, 1}, tag_name: \"div\"}},\n               {:text, \"\\n\", %{column_end: 1, line_end: 7}}\n             ]\n    end\n\n    test \"two comments in a row\" do\n      first_part = \"\"\"\n      <p>\n      <!--\n      <%= \"Hello\" %>\n      \"\"\"\n\n      {first_tokens, cont} =\n        Tokenizer.tokenize(first_part, [], [], {:text, :enabled}, tokenizer_state(first_part))\n\n      second_part = \"\"\"\n      -->\n      <!--\n      <p><%= \"World\"</p>\n      \"\"\"\n\n      {second_tokens, cont} =\n        Tokenizer.tokenize(second_part, [], first_tokens, cont, tokenizer_state(second_part))\n\n      third_part = \"\"\"\n      -->\n      <div>\n        <p>Hi</p>\n      </p>\n      \"\"\"\n\n      {tokens, {:text, :enabled}} =\n        Tokenizer.tokenize(third_part, [], second_tokens, cont, tokenizer_state(third_part))\n\n      assert Enum.reverse(tokens) == [\n               {:tag, \"p\", [], %{column: 1, line: 1, inner_location: {1, 4}, tag_name: \"p\"}},\n               {:text, \"\\n<!--\\n<%= \\\"Hello\\\" %>\\n\",\n                %{column_end: 1, context: [:comment_start], line_end: 4}},\n               {:text, \"-->\\n<!--\\n<p><%= \\\"World\\\"</p>\\n\",\n                %{column_end: 1, context: [:comment_end, :comment_start], line_end: 4}},\n               {:text, \"-->\\n\", %{column_end: 1, context: [:comment_end], line_end: 2}},\n               {:tag, \"div\", [], %{column: 1, line: 2, inner_location: {2, 6}, tag_name: \"div\"}},\n               {:text, \"\\n  \", %{column_end: 3, line_end: 3}},\n               {:tag, \"p\", [], %{column: 3, line: 3, inner_location: {3, 6}, tag_name: \"p\"}},\n               {:text, \"Hi\", %{column_end: 8, line_end: 3}},\n               {:close, :tag, \"p\", %{column: 8, line: 3, inner_location: {3, 8}, tag_name: \"p\"}},\n               {:text, \"\\n\", %{column_end: 1, line_end: 4}},\n               {:close, :tag, \"p\", %{column: 1, line: 4, inner_location: {4, 1}, tag_name: \"p\"}},\n               {:text, \"\\n\", %{column_end: 1, line_end: 5}}\n             ]\n    end\n  end\n\n  describe \"opening tag\" do\n    test \"represented as {:tag, name, attrs, meta}\" do\n      tokens = tokenize(\"<div>\")\n      assert [{:tag, \"div\", [], %{}}] = tokens\n    end\n\n    test \"with space after name\" do\n      tokens = tokenize(\"<div >\")\n      assert [{:tag, \"div\", [], %{}}] = tokens\n    end\n\n    test \"with line break after name\" do\n      tokens = tokenize(\"<div\\n>\")\n      assert [{:tag, \"div\", [], %{}}] = tokens\n    end\n\n    test \"self close\" do\n      tokens = tokenize(\"<div/>\")\n      assert [{:tag, \"div\", [], %{closing: :self}}] = tokens\n    end\n\n    test \"compute line and column\" do\n      tokens =\n        tokenize(\"\"\"\n        <div>\n          <span>\n\n        <p/><br>\\\n        \"\"\")\n\n      assert [\n               {:tag, \"div\", [], %{line: 1, column: 1}},\n               {:text, _, %{line_end: 2, column_end: 3}},\n               {:tag, \"span\", [], %{line: 2, column: 3}},\n               {:text, _, %{line_end: 4, column_end: 1}},\n               {:tag, \"p\", [], %{column: 1, line: 4, closing: :self}},\n               {:tag, \"br\", [], %{column: 5, line: 4}}\n             ] = tokens\n    end\n\n    test \"raise on missing/incomplete tag name\" do\n      message = \"\"\"\n      nofile:2:4: expected tag name after <. If you meant to use < as part of a text, use &lt; instead\n        |\n      1 | <div>\n      2 |   <>\n        |    ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(\"\"\"\n        <div>\n          <>\\\n        \"\"\")\n      end\n\n      message = \"\"\"\n      nofile:1:2: expected tag name after <. If you meant to use < as part of a text, use &lt; instead\n        |\n      1 | <\n        |  ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(\"<\")\n      end\n\n      message = \"\"\"\n      nofile:1:2: a component name is required after .\n        |\n      1 | <./typo>\n        |  ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(\"<./typo>\")\n      end\n\n      assert_raise ParseError, ~r\"nofile:1:5: expected closing `>` or `/>`\", fn ->\n        tokenize(\"<foo\")\n      end\n    end\n  end\n\n  describe \"attributes\" do\n    test \"represented as a list of {name, tuple | nil, meta}, where tuple is the {type, value}\" do\n      attrs = tokenize_attrs(~S(<div class=\"panel\" style={@style} hidden>))\n\n      assert [\n               {\"class\", {:string, \"panel\", %{}}, %{column: 6, line: 1}},\n               {\"style\", {:expr, \"@style\", %{}}, %{column: 20, line: 1}},\n               {\"hidden\", nil, %{column: 35, line: 1}}\n             ] = attrs\n    end\n\n    test \"accepts space between the name and `=`\" do\n      attrs = tokenize_attrs(~S(<div class =\"panel\">))\n\n      assert [{\"class\", {:string, \"panel\", %{}}, %{}}] = attrs\n    end\n\n    test \"accepts line breaks between the name and `=`\" do\n      attrs = tokenize_attrs(\"<div class\\n=\\\"panel\\\">\")\n\n      assert [{\"class\", {:string, \"panel\", %{}}, %{}}] = attrs\n\n      attrs = tokenize_attrs(\"<div class\\r\\n=\\\"panel\\\">\")\n\n      assert [{\"class\", {:string, \"panel\", %{}}, %{}}] = attrs\n    end\n\n    test \"accepts space between `=` and the value\" do\n      attrs = tokenize_attrs(~S(<div class= \"panel\">))\n\n      assert [{\"class\", {:string, \"panel\", %{}}, %{}}] = attrs\n    end\n\n    test \"accepts line breaks between `=` and the value\" do\n      attrs = tokenize_attrs(\"<div class=\\n\\\"panel\\\">\")\n\n      assert [{\"class\", {:string, \"panel\", %{}}, %{}}] = attrs\n\n      attrs = tokenize_attrs(\"<div class=\\r\\n\\\"panel\\\">\")\n\n      assert [{\"class\", {:string, \"panel\", %{}}, %{}}] = attrs\n    end\n\n    test \"raise on incomplete attribute\" do\n      message = \"\"\"\n      nofile:1:11: unexpected end of string inside tag\n        |\n      1 | <div class\n        |           ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(\"<div class\")\n      end\n    end\n\n    test \"raise on missing value\" do\n      message = \"\"\"\n      nofile:2:9: invalid attribute value after `=`. Expected either a value between quotes (such as \\\"value\\\" or 'value') or an Elixir expression between curly braces (such as `{expr}`)\n        |\n      1 | <div\n      2 |   class=>\n        |         ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(\"\"\"\n        <div\n          class=>\\\n        \"\"\")\n      end\n\n      message = \"\"\"\n      nofile:1:13: invalid attribute value after `=`. Expected either a value between quotes (such as \\\"value\\\" or 'value') or an Elixir expression between curly braces (such as `{expr}`)\n        |\n      1 | <div class= >\n        |             ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(~S(<div class= >))\n      end\n\n      message = \"\"\"\n      nofile:1:12: invalid attribute value after `=`. Expected either a value between quotes (such as \\\"value\\\" or 'value') or an Elixir expression between curly braces (such as `{expr}`)\n        |\n      1 | <div class=\n        |            ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(\"<div class=\")\n      end\n    end\n\n    test \"raise on missing attribute name\" do\n      message = \"\"\"\n      nofile:2:8: expected attribute name\n        |\n      1 | <div>\n      2 |   <div =\\\"panel\\\">\n        |        ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(\"\"\"\n        <div>\n          <div =\"panel\">\\\n        \"\"\")\n      end\n\n      message = \"\"\"\n      nofile:1:6: expected attribute name\n        |\n      1 | <div = >\n        |      ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(~S(<div = >))\n      end\n\n      message = \"\"\"\n      nofile:1:6: expected attribute name\n        |\n      1 | <div / >\n        |      ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(~S(<div / >))\n      end\n    end\n\n    test \"raise on attribute names with quotes\" do\n      message = \"\"\"\n      nofile:1:5: invalid character in attribute name: '\n        |\n      1 | <div'>\n        |     ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(~S(<div'>))\n      end\n\n      message = \"\"\"\n      nofile:1:5: invalid character in attribute name: \\\"\n        |\n      1 | <div\">\n        |     ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(~S(<div\">))\n      end\n\n      message = \"\"\"\n      nofile:1:10: invalid character in attribute name: '\n        |\n      1 | <div attr'>\n        |          ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(~S(<div attr'>))\n      end\n\n      message = \"\"\"\n      nofile:1:20: invalid character in attribute name: \\\"\n        |\n      1 | <div class={\"test\"}\">\n        |                    ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(~S(<div class={\"test\"}\">))\n      end\n    end\n\n    test \"raise on missing opening interpolation\" do\n      message = \"\"\"\n      nofile:1:29: expected attribute, but found end of interpolation: }\n        |\n      1 | <div class=\\\"image-container\\\"}>\n        |                             ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(~S(<div class=\"image-container\"}>))\n      end\n    end\n  end\n\n  describe \"boolean attributes\" do\n    test \"represented as {name, nil, meta}\" do\n      attrs = tokenize_attrs(\"<div hidden>\")\n\n      assert [{\"hidden\", nil, %{}}] = attrs\n    end\n\n    test \"multiple attributes\" do\n      attrs = tokenize_attrs(\"<div hidden selected>\")\n\n      assert [{\"hidden\", nil, %{}}, {\"selected\", nil, %{}}] = attrs\n    end\n\n    test \"with space after\" do\n      attrs = tokenize_attrs(\"<div hidden >\")\n\n      assert [{\"hidden\", nil, %{}}] = attrs\n    end\n\n    test \"in self close tag\" do\n      attrs = tokenize_attrs(\"<div hidden/>\")\n\n      assert [{\"hidden\", nil, %{}}] = attrs\n    end\n\n    test \"in self close tag with space after\" do\n      attrs = tokenize_attrs(\"<div hidden />\")\n\n      assert [{\"hidden\", nil, %{}}] = attrs\n    end\n  end\n\n  describe \"attributes as double quoted string\" do\n    test \"value is represented as {:string, value, meta}}\" do\n      attrs = tokenize_attrs(~S(<div class=\"panel\">))\n\n      assert [{\"class\", {:string, \"panel\", %{delimiter: ?\"}}, %{}}] = attrs\n    end\n\n    test \"multiple attributes\" do\n      attrs = tokenize_attrs(~S(<div class=\"panel\" style=\"margin: 0px;\">))\n\n      assert [\n               {\"class\", {:string, \"panel\", %{delimiter: ?\"}}, %{}},\n               {\"style\", {:string, \"margin: 0px;\", %{delimiter: ?\"}}, %{}}\n             ] = attrs\n    end\n\n    test \"value containing single quotes\" do\n      attrs = tokenize_attrs(~S(<div title=\"i'd love to!\">))\n\n      assert [{\"title\", {:string, \"i'd love to!\", %{delimiter: ?\"}}, %{}}] = attrs\n    end\n\n    test \"value containing line breaks\" do\n      tokens =\n        tokenize(\"\"\"\n        <div title=\"first\n          second\n        third\"><span>\\\n        \"\"\")\n\n      assert [\n               {:tag, \"div\", [{\"title\", {:string, \"first\\n  second\\nthird\", _meta}, %{}}], %{}},\n               {:tag, \"span\", [], %{line: 3, column: 8}}\n             ] = tokens\n    end\n\n    test \"raise on incomplete attribute value (EOF)\" do\n      assert_raise ParseError, ~r\"nofile:2:15: expected closing `\\\"` for attribute value\", fn ->\n        tokenize(\"\"\"\n        <div\n          class=\"panel\\\n        \"\"\")\n      end\n    end\n  end\n\n  describe \"attributes as single quoted string\" do\n    test \"value is represented as {:string, value, meta}}\" do\n      attrs = tokenize_attrs(~S(<div class='panel'>))\n\n      assert [{\"class\", {:string, \"panel\", %{delimiter: ?'}}, %{}}] = attrs\n    end\n\n    test \"multiple attributes\" do\n      attrs = tokenize_attrs(~S(<div class='panel' style='margin: 0px;'>))\n\n      assert [\n               {\"class\", {:string, \"panel\", %{delimiter: ?'}}, %{}},\n               {\"style\", {:string, \"margin: 0px;\", %{delimiter: ?'}}, %{}}\n             ] = attrs\n    end\n\n    test \"value containing double quotes\" do\n      attrs = tokenize_attrs(~S(<div title='Say \"hi!\"'>))\n\n      assert [{\"title\", {:string, ~S(Say \"hi!\"), %{delimiter: ?'}}, %{}}] = attrs\n    end\n\n    test \"value containing line breaks\" do\n      tokens =\n        tokenize(\"\"\"\n        <div title='first\n          second\n        third'><span>\\\n        \"\"\")\n\n      assert [\n               {:tag, \"div\", [{\"title\", {:string, \"first\\n  second\\nthird\", _meta}, %{}}], %{}},\n               {:tag, \"span\", [], %{line: 3, column: 8}}\n             ] = tokens\n    end\n\n    test \"raise on incomplete attribute value (EOF)\" do\n      assert_raise ParseError, ~r\"nofile:2:15: expected closing `\\'` for attribute value\", fn ->\n        tokenize(\"\"\"\n        <div\n          class='panel\\\n        \"\"\")\n      end\n    end\n  end\n\n  describe \"attributes as expressions\" do\n    test \"value is represented as {:expr, value, meta}\" do\n      attrs = tokenize_attrs(~S(<div class={@class}>))\n\n      assert [{\"class\", {:expr, \"@class\", %{line: 1, column: 13}}, %{}}] = attrs\n    end\n\n    test \"multiple attributes\" do\n      attrs = tokenize_attrs(~S(<div class={@class} style={@style}>))\n\n      assert [\n               {\"class\", {:expr, \"@class\", %{}}, %{}},\n               {\"style\", {:expr, \"@style\", %{}}, %{}}\n             ] = attrs\n    end\n\n    test \"double quoted strings inside expression\" do\n      attrs = tokenize_attrs(~S(<div class={\"text\"}>))\n\n      assert [{\"class\", {:expr, ~S(\"text\"), %{}}, %{}}] = attrs\n    end\n\n    test \"value containing curly braces\" do\n      attrs = tokenize_attrs(~S(<div class={ [{:active, @active}] }>))\n\n      assert [{\"class\", {:expr, \" [{:active, @active}] \", %{}}, %{}}] = attrs\n    end\n\n    test \"ignore escaped curly braces inside elixir strings\" do\n      attrs = tokenize_attrs(~S(<div class={\"\\{hi\"}>))\n\n      assert [{\"class\", {:expr, ~S(\"\\{hi\"), %{}}, %{}}] = attrs\n\n      attrs = tokenize_attrs(~S(<div class={\"hi\\}\"}>))\n\n      assert [{\"class\", {:expr, ~S(\"hi\\}\"), %{}}, %{}}] = attrs\n    end\n\n    test \"compute line and columns\" do\n      attrs =\n        tokenize_attrs(\"\"\"\n        <div\n          class={@class}\n            style={\n              @style\n            }\n          title={@title}\n        >\\\n        \"\"\")\n\n      assert [\n               {\"class\", {:expr, _, %{line: 2, column: 10}}, %{}},\n               {\"style\", {:expr, _, %{line: 3, column: 12}}, %{}},\n               {\"title\", {:expr, _, %{line: 6, column: 10}}, %{}}\n             ] = attrs\n    end\n\n    test \"raise on incomplete attribute expression (EOF)\" do\n      message = \"\"\"\n      nofile:2:9: expected closing `}` for expression\n\n      In case you don't want `{` to begin a new interpolation, you may write it using `&lbrace;` or using `<%= \"{\" %>`\n        |\n      1 | <div\n      2 |   class={panel\n        |         ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(\"\"\"\n        <div\n          class={panel\\\n        \"\"\")\n      end\n    end\n  end\n\n  describe \"root attributes\" do\n    test \"represented as {:root, value, meta}\" do\n      attrs = tokenize_attrs(\"<div {@attrs}>\")\n\n      assert [{:root, {:expr, \"@attrs\", %{}}, %{}}] = attrs\n    end\n\n    test \"with space after\" do\n      attrs = tokenize_attrs(\"<div {@attrs} >\")\n\n      assert [{:root, {:expr, \"@attrs\", %{}}, %{}}] = attrs\n    end\n\n    test \"with line break after\" do\n      attrs = tokenize_attrs(\"<div {@attrs}\\n>\")\n\n      assert [{:root, {:expr, \"@attrs\", %{}}, %{}}] = attrs\n    end\n\n    test \"in self close tag\" do\n      attrs = tokenize_attrs(\"<div {@attrs}/>\")\n\n      assert [{:root, {:expr, \"@attrs\", %{}}, %{}}] = attrs\n    end\n\n    test \"in self close tag with space after\" do\n      attrs = tokenize_attrs(\"<div {@attrs} />\")\n\n      assert [{:root, {:expr, \"@attrs\", %{}}, %{}}] = attrs\n    end\n\n    test \"multiple values among other attributes\" do\n      attrs = tokenize_attrs(\"<div class={@class} {@attrs1} hidden {@attrs2}/>\")\n\n      assert [\n               {\"class\", {:expr, \"@class\", %{}}, %{}},\n               {:root, {:expr, \"@attrs1\", %{}}, %{}},\n               {\"hidden\", nil, %{}},\n               {:root, {:expr, \"@attrs2\", %{}}, %{}}\n             ] = attrs\n    end\n\n    test \"compute line and columns\" do\n      attrs =\n        tokenize_attrs(\"\"\"\n        <div\n          {@root1}\n            {\n              @root2\n            }\n          {@root3}\n        >\\\n        \"\"\")\n\n      assert [\n               {:root, {:expr, \"@root1\", %{line: 2, column: 4}}, %{line: 2, column: 4}},\n               {:root, {:expr, \"\\n      @root2\\n    \", %{line: 3, column: 6}},\n                %{line: 3, column: 6}},\n               {:root, {:expr, \"@root3\", %{line: 6, column: 4}}, %{line: 6, column: 4}}\n             ] = attrs\n    end\n\n    test \"raise on incomplete expression (EOF)\" do\n      message = \"\"\"\n      nofile:2:3: expected closing `}` for expression\n\n      In case you don't want `{` to begin a new interpolation, you may write it using `&lbrace;` or using `<%= \"{\" %>`\n        |\n      1 | <div\n      2 |   {@attrs\n        |   ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(\"\"\"\n        <div\n          {@attrs\\\n        \"\"\")\n      end\n    end\n  end\n\n  describe \"closing tag\" do\n    test \"represented as {:close, :tag, name, meta}\" do\n      tokens = tokenize(\"</div>\")\n      assert [{:close, :tag, \"div\", %{}}] = tokens\n    end\n\n    test \"compute line and columns\" do\n      tokens =\n        tokenize(\"\"\"\n        <div>\n        </div><br>\\\n        \"\"\")\n\n      assert [\n               {:tag, \"div\", [], _meta},\n               {:text, \"\\n\", %{column_end: 1, line_end: 2}},\n               {:close, :tag, \"div\", %{line: 2, column: 1}},\n               {:tag, \"br\", [], %{line: 2, column: 7}}\n             ] = tokens\n    end\n\n    test \"raise on missing closing `>`\" do\n      message = \"\"\"\n      nofile:2:6: expected closing `>`\n        |\n      1 | <div>\n      2 | </div text\n        |      ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(\"\"\"\n        <div>\n        </div text\\\n        \"\"\")\n      end\n    end\n\n    test \"raise on missing tag name\" do\n      message = \"\"\"\n      nofile:2:5: expected tag name after </\n        |\n      1 | <div>\n      2 |   </>\n        |     ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(\"\"\"\n        <div>\n          </>\\\n        \"\"\")\n      end\n    end\n  end\n\n  describe \"script\" do\n    test \"self-closing\" do\n      assert tokenize(\"\"\"\n             <script src=\"foo.js\" />\n             \"\"\") == [\n               {:tag, \"script\",\n                [{\"src\", {:string, \"foo.js\", %{delimiter: 34}}, %{column: 9, line: 1}}],\n                %{\n                  column: 1,\n                  line: 1,\n                  closing: :self,\n                  tag_name: \"script\",\n                  inner_location: {1, 24}\n                }},\n               {:text, \"\\n\", %{column_end: 1, line_end: 2}}\n             ]\n    end\n\n    test \"traverses until </script>\" do\n      assert tokenize(\"\"\"\n             <script>\n               a = \"<a>Link</a>\"\n             </script>\n             \"\"\") == [\n               {:tag, \"script\", [],\n                %{column: 1, line: 1, inner_location: {1, 9}, tag_name: \"script\"}},\n               {:text, \"\\n  a = \\\"<a>Link</a>\\\"\\n\", %{column_end: 1, line_end: 3}},\n               {:close, :tag, \"script\", %{column: 1, line: 3, inner_location: {3, 1}}},\n               {:text, \"\\n\", %{column_end: 1, line_end: 4}}\n             ]\n    end\n  end\n\n  describe \"style\" do\n    test \"self-closing\" do\n      assert tokenize(\"\"\"\n             <style src=\"foo.js\" />\n             \"\"\") == [\n               {:tag, \"style\",\n                [{\"src\", {:string, \"foo.js\", %{delimiter: 34}}, %{column: 8, line: 1}}],\n                %{\n                  column: 1,\n                  line: 1,\n                  closing: :self,\n                  inner_location: {1, 23},\n                  tag_name: \"style\"\n                }},\n               {:text, \"\\n\", %{column_end: 1, line_end: 2}}\n             ]\n    end\n\n    test \"traverses until </style>\" do\n      assert tokenize(\"\"\"\n             <style>\n               a = \"<a>Link</a>\"\n             </style>\n             \"\"\") == [\n               {:tag, \"style\", [],\n                %{column: 1, line: 1, inner_location: {1, 8}, tag_name: \"style\"}},\n               {:text, \"\\n  a = \\\"<a>Link</a>\\\"\\n\", %{column_end: 1, line_end: 3}},\n               {:close, :tag, \"style\", %{column: 1, line: 3, inner_location: {3, 1}}},\n               {:text, \"\\n\", %{column_end: 1, line_end: 4}}\n             ]\n    end\n  end\n\n  describe \"local component\" do\n    test \"self-closing\" do\n      assert tokenize(\"\"\"\n             <.live_component module={MyApp.WeatherComponent} id=\"thermostat\" city=\"Kraków\" />\n             \"\"\") == [\n               {:local_component, \"live_component\",\n                [\n                  {\"module\", {:expr, \"MyApp.WeatherComponent\", %{line: 1, column: 26}},\n                   %{line: 1, column: 18}},\n                  {\"id\", {:string, \"thermostat\", %{delimiter: 34}}, %{line: 1, column: 50}},\n                  {\"city\", {:string, \"Kraków\", %{delimiter: 34}}, %{line: 1, column: 66}}\n                ],\n                %{\n                  line: 1,\n                  closing: :self,\n                  column: 1,\n                  tag_name: \".live_component\",\n                  inner_location: {1, 82}\n                }},\n               {:text, \"\\n\", %{line_end: 2, column_end: 1}}\n             ]\n    end\n\n    test \"traverses until </.link>\" do\n      assert tokenize(\"\"\"\n             <.link href=\"/\">Regular anchor link</.link>\n             \"\"\") == [\n               {:local_component, \"link\",\n                [{\"href\", {:string, \"/\", %{delimiter: 34}}, %{line: 1, column: 8}}],\n                %{line: 1, column: 1, tag_name: \".link\", inner_location: {1, 17}}},\n               {:text, \"Regular anchor link\", %{line_end: 1, column_end: 36}},\n               {:close, :local_component, \"link\",\n                %{line: 1, column: 36, tag_name: \".link\", inner_location: {1, 36}}},\n               {:text, \"\\n\", %{line_end: 2, column_end: 1}}\n             ]\n    end\n  end\n\n  describe \"remote component\" do\n    test \"self-closing\" do\n      assert tokenize(\"\"\"\n             <MyAppWeb.CoreComponents.flash kind={:info} flash={@flash} />\n             \"\"\") == [\n               {\n                 :remote_component,\n                 \"MyAppWeb.CoreComponents.flash\",\n                 [\n                   {\"kind\", {:expr, \":info\", %{column: 38, line: 1}}, %{column: 32, line: 1}},\n                   {\"flash\", {:expr, \"@flash\", %{column: 52, line: 1}}, %{column: 45, line: 1}}\n                 ],\n                 %{\n                   closing: :self,\n                   column: 1,\n                   inner_location: {1, 62},\n                   line: 1,\n                   tag_name: \"MyAppWeb.CoreComponents.flash\"\n                 }\n               },\n               {:text, \"\\n\", %{column_end: 1, line_end: 2}}\n             ]\n    end\n\n    test \"traverses until </MyAppWeb.CoreComponents.modal>\" do\n      assert tokenize(\"\"\"\n             <MyAppWeb.CoreComponents.modal id=\"confirm\" on_cancel={JS.navigate(~p\"/posts\")}>\n               This is another modal.\n             </MyAppWeb.CoreComponents.modal>\n             \"\"\") == [\n               {\n                 :remote_component,\n                 \"MyAppWeb.CoreComponents.modal\",\n                 [\n                   {\"id\", {:string, \"confirm\", %{delimiter: 34}}, %{line: 1, column: 32}},\n                   {\"on_cancel\", {:expr, \"JS.navigate(~p\\\"/posts\\\")\", %{line: 1, column: 56}},\n                    %{line: 1, column: 45}}\n                 ],\n                 %{\n                   line: 1,\n                   column: 1,\n                   tag_name: \"MyAppWeb.CoreComponents.modal\",\n                   inner_location: {1, 81}\n                 }\n               },\n               {:text, \"\\n  This is another modal.\\n\", %{line_end: 3, column_end: 1}},\n               {:close, :remote_component, \"MyAppWeb.CoreComponents.modal\",\n                %{\n                  line: 3,\n                  column: 1,\n                  tag_name: \"MyAppWeb.CoreComponents.modal\",\n                  inner_location: {3, 1}\n                }},\n               {:text, \"\\n\", %{line_end: 4, column_end: 1}}\n             ]\n    end\n  end\n\n  describe \"reserved component\" do\n    test \"raise on using reserved slot :inner_block\" do\n      message = \"\"\"\n      nofile:1:2: the slot name :inner_block is reserved\n        |\n      1 | <:inner_block>Inner</:inner_block>\n        |  ^\\\n      \"\"\"\n\n      assert_raise ParseError, message, fn ->\n        tokenize(\"<:inner_block>Inner</:inner_block>\")\n      end\n    end\n  end\n\n  test \"mixing text and tags\" do\n    tokens =\n      tokenize(\"\"\"\n      text before\n      <div>\n        text\n      </div>\n      text after\n      \"\"\")\n\n    assert [\n             {:text, \"text before\\n\", %{line_end: 2, column_end: 1}},\n             {:tag, \"div\", [], %{}},\n             {:text, \"\\n  text\\n\", %{line_end: 4, column_end: 1}},\n             {:close, :tag, \"div\", %{line: 4, column: 1}},\n             {:text, \"\\ntext after\\n\", %{line_end: 6, column_end: 1}}\n           ] = tokens\n  end\n\n  test \"warns when no space between attributes\" do\n    warning =\n      capture_io(:stderr, fn ->\n        tokenize(~S(<div style=\"\"id=\"123\"/>))\n      end)\n\n    assert warning =~ \"missing space before attribute\"\n\n    warning =\n      capture_io(:stderr, fn ->\n        tokenize(~S(<div style={@s}id=\"123\"/>))\n      end)\n\n    assert warning =~ \"missing space before attribute\"\n  end\n\n  defp tokenize_attrs(code) do\n    [{:tag, \"div\", attrs, %{}}] = tokenize(code)\n    attrs\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/test/diff_test.exs",
    "content": "defmodule Phoenix.LiveViewTest.DiffTest do\n  use ExUnit.Case, async: true\n\n  alias Phoenix.LiveViewTest.Diff\n\n  defstruct [:foo]\n\n  describe \"merge_diff\" do\n    test \"merges unless static\" do\n      assert Diff.merge_diff(%{0 => \"bar\", s: \"foo\"}, %{0 => \"baz\"}) ==\n               %{0 => \"baz\", s: \"foo\", streams: []}\n\n      assert Diff.merge_diff(%{s: \"foo\", d: []}, %{s: \"bar\"}) ==\n               %{s: \"bar\", streams: []}\n    end\n\n    test \"resolves moved comprehensions\" do\n      base = %{\n        k: %{\n          0 => %{0 => \"A\"},\n          1 => %{0 => \"B\"},\n          2 => %{0 => \"C\", 1 => %{0 => \"var1\", :s => [\"\", \"\"]}},\n          kc: 3\n        }\n      }\n\n      diff = %{\n        k: %{\n          0 => 1,\n          1 => [2, %{1 => %{0 => \"var2\"}}],\n          kc: 2\n        }\n      }\n\n      result = %{\n        k: %{\n          0 => %{0 => \"B\"},\n          1 => %{0 => \"C\", 1 => %{0 => \"var2\", :s => [\"\", \"\"]}},\n          kc: 2\n        },\n        streams: []\n      }\n\n      assert Diff.merge_diff(base, diff) == result\n    end\n\n    test \"no warning when keyed count is 0\" do\n      base = %{\n        k: %{\n          0 => %{0 => \"A\"},\n          1 => %{0 => \"B\"},\n          2 => %{0 => \"C\", 1 => %{0 => \"var1\", :s => [\"\", \"\"]}},\n          :kc => 3\n        }\n      }\n\n      diff = %{\n        k: %{kc: 0}\n      }\n\n      result = %{\n        k: %{kc: 0},\n        streams: []\n      }\n\n      assert Diff.merge_diff(base, diff) == result\n    end\n\n    test \"ignores structs when resolving templates\" do\n      assert Diff.merge_diff(%{0 => %{}}, %{\n               0 => %{:s => 1, 0 => %__MODULE__{foo: :bar}},\n               :p => %{1 => [\"foo\", \"bar\"]}\n             }) == %{0 => %{0 => %__MODULE__{foo: :bar}, :s => [\"foo\", \"bar\"]}, :streams => []}\n    end\n\n    test \"copies streams\" do\n      base = %{\n        k: %{\n          0 => %{0 => \"A\"},\n          1 => %{0 => \"B\"},\n          2 => %{0 => \"C\", 1 => %{0 => \"var1\", :s => [\"\", \"\"]}},\n          kc: 3\n        },\n        stream: \"foo\"\n      }\n\n      diff = %{\n        k: %{\n          0 => 1,\n          1 => [2, %{1 => %{0 => \"var2\"}}],\n          kc: 2\n        },\n        stream: \"bar\"\n      }\n\n      result = %{\n        k: %{\n          0 => %{0 => \"B\"},\n          1 => %{0 => \"C\", 1 => %{0 => \"var2\", :s => [\"\", \"\"]}},\n          kc: 2\n        },\n        stream: \"bar\",\n        streams: [\"bar\"]\n      }\n\n      assert Diff.merge_diff(base, diff) == result\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/test/dom_test.exs",
    "content": "defmodule Phoenix.LiveViewTest.DOMTest do\n  use ExUnit.Case, async: true\n\n  alias Phoenix.LiveViewTest.DOM\n\n  describe \"parse_fragment\" do\n    test \"detects duplicate ids\" do\n      assert DOM.parse_fragment(\n               \"\"\"\n               <div id=\"foo\">\n                 <div id=\"foo\"></div>\n               </div>\n               \"\"\",\n               fn type, msg -> send(self(), {:error, type, msg}) end\n             )\n\n      assert_receive {:error, :duplicate_id, msg}\n      assert msg =~ \"Duplicate id found while testing LiveView\"\n    end\n\n    test \"handles declarations (issue #3594)\" do\n      assert DOM.parse_fragment(\n               \"\"\"\n               <div id=\"foo\">\n                 <?xml version=\"1.0\" standalone=\"yes\"?>\n               </div>\n               \"\"\",\n               fn type, msg -> send(self(), {:error, type, msg}) end\n             )\n\n      refute_receive {:error, _, _}\n    end\n  end\n\n  describe \"parse_document\" do\n    test \"detects duplicate ids\" do\n      assert DOM.parse_document(\n               \"\"\"\n               <html>\n                <body>\n                  <div id=\"foo\">\n                    <div id=\"foo\"></div>\n                  </div>\n                </body>\n               </html>\n               \"\"\",\n               fn type, msg -> send(self(), {:error, type, msg}) end\n             )\n\n      assert_receive {:error, :duplicate_id, msg}\n      assert msg =~ \"Duplicate id found while testing LiveView\"\n    end\n\n    test \"handles declarations (issue #3594)\" do\n      assert DOM.parse_document(\n               \"\"\"\n               <html>\n                <body>\n                  <div id=\"foo\">\n                    <?xml version=\"1.0\" standalone=\"yes\"?>\n                  </div>\n                </body>\n               </html>\n               \"\"\",\n               fn type, msg -> send(self(), {:error, type, msg}) end\n             )\n\n      refute_receive {:error, _, _}\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/test/tree_dom_test.exs",
    "content": "defmodule Phoenix.LiveViewTest.TreeDOMTest do\n  use ExUnit.Case, async: true\n\n  import Phoenix.LiveViewTest.TreeDOM, only: [sigil_X: 2, sigil_x: 2]\n\n  alias Phoenix.LiveViewTest.TreeDOM\n\n  describe \"find_live_views\" do\n    # >= 4432 characters\n    @too_big_session Enum.map_join(1..4432, fn _ -> \"t\" end)\n\n    test \"finds views given html\" do\n      assert TreeDOM.find_live_views(~x\"\"\"\n             <h1>top</h1>\n             <div data-phx-session=\"SESSION1\"\n               id=\"phx-123\"></div>\n             <div data-phx-parent-id=\"456\"\n                 data-phx-session=\"SESSION2\"\n                 data-phx-static=\"STATIC2\"\n                 id=\"phx-456\"></div>\n             <div data-phx-session=\"#{@too_big_session}\"\n               id=\"phx-458\"></div>\n             <h1>bottom</h1>\n             \"\"\") == [\n               {\"phx-123\", \"SESSION1\", nil},\n               {\"phx-456\", \"SESSION2\", \"STATIC2\"},\n               {\"phx-458\", @too_big_session, nil}\n             ]\n\n      assert TreeDOM.find_live_views([\"none\"]) == []\n    end\n\n    test \"returns main live view as first result\" do\n      assert TreeDOM.find_live_views(~X\"\"\"\n             <h1>top</h1>\n             <div data-phx-session=\"SESSION1\"\n               id=\"phx-123\"></div>\n             <div data-phx-parent-id=\"456\"\n                 data-phx-session=\"SESSION2\"\n                 data-phx-static=\"STATIC2\"\n                 id=\"phx-456\"></div>\n             <div data-phx-session=\"SESSIONMAIN\"\n               data-phx-main\n               id=\"phx-458\"></div>\n             <h1>bottom</h1>\n             \"\"\") == [\n               {\"phx-458\", \"SESSIONMAIN\", nil},\n               {\"phx-123\", \"SESSION1\", nil},\n               {\"phx-456\", \"SESSION2\", \"STATIC2\"}\n             ]\n    end\n  end\n\n  describe \"replace_root_html\" do\n    test \"replaces tag name and merges attributes\" do\n      container =\n        ~X\"\"\"\n        <div id=\"container\"\n             data-phx-main=\"true\"\n             data-phx-session=\"session\"\n             data-phx-static=\"static\"\n             class=\"old\">contents</div>\n        \"\"\"\n\n      assert TreeDOM.replace_root_container(container, :span, %{class: \"new\"})\n             |> TreeDOM.normalize_to_tree(sort_attributes: true) ==\n               [\n                 {\"span\",\n                  [\n                    {\"class\", \"new\"},\n                    {\"data-phx-main\", \"true\"},\n                    {\"data-phx-session\", \"session\"},\n                    {\"data-phx-static\", \"static\"},\n                    {\"id\", \"container\"}\n                  ], [\"contents\"]}\n               ]\n    end\n\n    test \"does not overwrite reserved attributes\" do\n      container =\n        ~X\"\"\"\n        <div id=\"container\"\n             data-phx-main=\"true\"\n             data-phx-session=\"session\"\n             data-phx-static=\"static\">contents</div>\n        \"\"\"\n\n      new_attrs = %{\n        \"id\" => \"new\",\n        \"data-phx-session\" => \"new\",\n        \"data-phx-static\" => \"new\",\n        \"data-phx-main\" => \"new\"\n      }\n\n      assert TreeDOM.replace_root_container(container, :div, new_attrs)\n             |> TreeDOM.normalize_to_tree(sort_attributes: true) ==\n               [\n                 {\"div\",\n                  [\n                    {\"data-phx-main\", \"true\"},\n                    {\"data-phx-session\", \"session\"},\n                    {\"data-phx-static\", \"static\"},\n                    {\"id\", \"container\"}\n                  ], [\"contents\"]}\n               ]\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/upload/channel_test.exs",
    "content": "defmodule Phoenix.LiveView.UploadChannelTest do\n  use ExUnit.Case, async: false\n  require Phoenix.ChannelTest\n\n  import Phoenix.LiveViewTest\n\n  alias Phoenix.{Component, LiveView}\n  alias Phoenix.LiveViewTest.UploadClient\n  alias Phoenix.LiveViewTest.Support.{UploadLive, NestedUploadLive, UploadLiveWithComponent}\n\n  @endpoint Phoenix.LiveViewTest.Support.Endpoint\n\n  defmodule TestWriter do\n    @behaviour Phoenix.LiveView.UploadWriter\n\n    @impl true\n    def init(test_name) do\n      send(test_name, :init)\n      {:ok, test_name}\n    end\n\n    @impl true\n    def meta(test_name) do\n      send(test_name, :meta)\n      test_name\n    end\n\n    @impl true\n    def write_chunk(\"error\", test_name) do\n      {:error, :custom_error, test_name}\n    end\n\n    def write_chunk(data, test_name) do\n      send(test_name, {:write_chunk, data})\n      {:ok, test_name}\n    end\n\n    @impl true\n    def close(test_name, reason) do\n      send(test_name, {:close, reason})\n      {:ok, test_name}\n    end\n  end\n\n  def build_writer(_name, %Phoenix.LiveView.UploadEntry{}, %Phoenix.LiveView.Socket{}) do\n    {TestWriter, :test_writer}\n  end\n\n  def valid_token(lv_pid, ref) do\n    LiveView.Static.sign_token(@endpoint, %{pid: lv_pid, ref: ref})\n  end\n\n  def mount_lv(setup) when is_function(setup, 1) do\n    conn = Plug.Test.init_test_session(Phoenix.ConnTest.build_conn(), %{})\n    {:ok, lv, _} = live_isolated(conn, UploadLive, session: %{})\n    :ok = GenServer.call(lv.pid, {:setup, setup})\n    {:ok, lv}\n  end\n\n  def mount_nested_lv(setup) when is_function(setup, 1) do\n    conn = Plug.Test.init_test_session(Phoenix.ConnTest.build_conn(), %{})\n    {:ok, lv, _} = live_isolated(conn, NestedUploadLive, session: %{})\n    nested_lv = find_live_child(lv, \"upload\")\n    :ok = GenServer.call(nested_lv.pid, {:setup, setup})\n    {:ok, nested_lv}\n  end\n\n  def mount_lv_with_component(setup) when is_function(setup, 1) do\n    conn = Plug.Test.init_test_session(Phoenix.ConnTest.build_conn(), %{})\n    {:ok, lv, _} = live_isolated(conn, UploadLiveWithComponent, session: %{})\n    :ok = GenServer.call(lv.pid, {:run, setup})\n    {:ok, lv}\n  end\n\n  def get_uploaded_entries(lv, name) do\n    UploadLive.run(lv, fn socket ->\n      {:reply, Phoenix.LiveView.uploaded_entries(socket, name), socket}\n    end)\n  end\n\n  def build_entries(count, opts \\\\ []) do\n    content = String.duplicate(\"0\", 100)\n    size = byte_size(content)\n\n    for i <- 1..count do\n      Enum.into(opts, %{\n        last_modified: 1_594_171_879_000,\n        name: \"myfile#{i}.jpeg\",\n        relative_path: \"./myfile#{i}.jpeg\",\n        content: content,\n        size: size,\n        type: \"image/jpeg\"\n      })\n    end\n  end\n\n  def unlink(\n        channel_pid,\n        %Phoenix.LiveViewTest.View{} = lv,\n        %Phoenix.LiveViewTest.Upload{} = upload\n      ) do\n    Process.unlink(upload.pid)\n    unlink(channel_pid, lv)\n  end\n\n  def unlink(channel_pid, %Phoenix.LiveViewTest.View{} = lv) do\n    Process.flag(:trap_exit, true)\n    Process.unlink(UploadLive.proxy_pid(lv))\n    Process.unlink(lv.pid)\n    Process.unlink(channel_pid)\n  end\n\n  def consume(%LiveView.UploadEntry{} = entry, socket) do\n    socket =\n      cond do\n        entry.client_name == \"redirect.jpeg\" ->\n          Phoenix.LiveView.push_navigate(socket, to: \"/redirected\")\n\n        entry.client_name == \"consume-and-redirect.jpeg\" and entry.done? ->\n          _ =\n            Phoenix.LiveView.consume_uploaded_entry(socket, entry, fn _ ->\n              {:ok, entry.client_name}\n            end)\n\n          Phoenix.LiveView.push_navigate(socket, to: \"/redirected\")\n\n        entry.done? ->\n          name =\n            Phoenix.LiveView.consume_uploaded_entry(socket, entry, fn _ ->\n              {:ok, entry.client_name}\n            end)\n\n          Phoenix.Component.update(socket, :consumed, fn consumed -> [name] ++ consumed end)\n\n        true ->\n          socket\n      end\n\n    {:noreply, socket}\n  end\n\n  setup_all do\n    start_supervised!(Phoenix.PubSub.child_spec(name: Phoenix.LiveView.PubSub))\n    :ok\n  end\n\n  test \"rejects invalid token\" do\n    {:ok, socket} = Phoenix.ChannelTest.connect(Phoenix.LiveView.Socket, %{})\n\n    assert {:error, %{reason: :invalid_token}} =\n             Phoenix.ChannelTest.subscribe_and_join(socket, \"lvu:123\", %{\"token\" => \"bad\"})\n  end\n\n  defp setup_lv(%{allow: opts}) do\n    opts = opts_for_allow_upload(opts)\n    {:ok, lv} = mount_lv(fn socket -> Phoenix.LiveView.allow_upload(socket, :avatar, opts) end)\n    {:ok, lv: lv}\n  end\n\n  defp setup_nested_lv(%{allow: opts}) do\n    opts = opts_for_allow_upload(opts)\n\n    {:ok, lv} =\n      mount_nested_lv(fn socket -> Phoenix.LiveView.allow_upload(socket, :avatar, opts) end)\n\n    {:ok, lv: lv}\n  end\n\n  defp setup_component(%{allow: opts}) do\n    opts = opts_for_allow_upload(opts)\n\n    {:ok, lv} =\n      mount_lv_with_component(fn component_socket ->\n        new_socket = Phoenix.LiveView.allow_upload(component_socket, :avatar, opts)\n        {:reply, :ok, new_socket}\n      end)\n\n    {:ok, lv: lv}\n  end\n\n  defp opts_for_allow_upload(opts) do\n    case Keyword.fetch(opts, :progress) do\n      {:ok, progress} ->\n        Keyword.put(opts, :progress, fn _, entry, socket ->\n          apply(__MODULE__, progress, [entry, socket])\n        end)\n\n      :error ->\n        opts\n    end\n  end\n\n  for context <- [:lv, :nested_lv, :component] do\n    @context context\n\n    describe \"#{@context} with valid token\" do\n      setup :\"setup_#{@context}\"\n\n      @tag allow: [accept: :any]\n      test \"upload channel exits when LiveView channel exits\", %{lv: lv} do\n        avatar = file_input(lv, \"form\", :avatar, build_entries(1))\n        assert render_upload(avatar, \"myfile1.jpeg\", 1) =~ \"#{@context}:myfile1.jpeg:1%\"\n        assert %{\"myfile1.jpeg\" => channel_pid} = UploadClient.channel_pids(avatar)\n\n        unlink(channel_pid, lv, avatar)\n        Process.monitor(channel_pid)\n        Process.exit(lv.pid, :kill)\n        assert_receive {:DOWN, _ref, :process, ^channel_pid, :killed}\n      end\n\n      @tag allow: [accept: :any]\n      test \"abnormal channel exit brings down LiveView\", %{lv: lv} do\n        avatar = file_input(lv, \"form\", :avatar, build_entries(1))\n        assert render_upload(avatar, \"myfile1.jpeg\", 1) =~ \"#{@context}:myfile1.jpeg:1%\"\n        assert %{\"myfile1.jpeg\" => channel_pid} = UploadClient.channel_pids(avatar)\n\n        lv_pid = lv.pid\n        unlink(channel_pid, lv, avatar)\n        Process.monitor(lv_pid)\n        Process.exit(channel_pid, :kill)\n\n        assert_receive {:DOWN, _ref, :process, ^lv_pid,\n                        {:shutdown, {:channel_upload_exit, :killed}}}\n      end\n\n      @tag allow: [accept: :any]\n      test \"normal channel exit is cleaned up by LiveView\", %{lv: lv} do\n        avatar = file_input(lv, \"form\", :avatar, build_entries(1))\n        assert render_upload(avatar, \"myfile1.jpeg\", 1) =~ \"#{@context}:myfile1.jpeg:1%\"\n        assert %{\"myfile1.jpeg\" => channel_pid} = UploadClient.channel_pids(avatar)\n\n        lv_pid = lv.pid\n        unlink(channel_pid, lv)\n        Process.monitor(lv_pid)\n\n        assert render(lv) =~ \"channel:#{UploadLive.inspect_html_safe(channel_pid)}\"\n        GenServer.stop(channel_pid, :normal)\n        refute_receive {:DOWN, _ref, :process, ^lv_pid, _}\n        refute render(lv) =~ \"channel:\"\n      end\n\n      @tag allow: [accept: :any, max_file_size: 100]\n      test \"upload channel exits when client sends more bytes than allowed\", %{lv: lv} do\n        avatar =\n          file_input(lv, \"form\", :avatar, [\n            %{name: \"foo.jpeg\", content: String.duplicate(\"0\", 100)}\n          ])\n\n        assert render_upload(avatar, \"foo.jpeg\", 1) =~ \"#{@context}:foo.jpeg:1%\"\n        assert %{\"foo.jpeg\" => channel_pid} = UploadClient.channel_pids(avatar)\n\n        unlink(channel_pid, lv)\n        Process.monitor(channel_pid)\n\n        assert UploadClient.simulate_attacker_chunk(\n                 avatar,\n                 \"foo.jpeg\",\n                 String.duplicate(\"0\", 1000)\n               ) ==\n                 {:error, %{limit: 100, reason: :file_size_limit_exceeded}}\n\n        assert_receive {:DOWN, _ref, :process, ^channel_pid, {:shutdown, :closed}}, 1000\n      end\n\n      @tag allow: [accept: :any, max_file_size: 100, chunk_timeout: 500]\n      test \"upload channel exits when client does not send chunk after timeout\", %{lv: lv} do\n        avatar =\n          file_input(lv, \"form\", :avatar, [\n            %{name: \"foo.jpeg\", content: String.duplicate(\"0\", 100)}\n          ])\n\n        assert render_upload(avatar, \"foo.jpeg\", 1) =~ \"#{@context}:foo.jpeg:1%\"\n        assert %{\"foo.jpeg\" => channel_pid} = UploadClient.channel_pids(avatar)\n\n        unlink(channel_pid, lv)\n        Process.monitor(channel_pid)\n\n        assert_receive {:DOWN, _ref, :process, ^channel_pid, {:shutdown, :closed}}, 1000\n      end\n\n      @tag allow: [max_entries: 3, accept: :any]\n      test \"multiple entries under max\", %{lv: lv} do\n        avatar = file_input(lv, \"form\", :avatar, build_entries(2))\n        assert render_upload(avatar, \"myfile1.jpeg\", 1) =~ \"#{@context}:myfile1.jpeg:1%\"\n        assert render_upload(avatar, \"myfile2.jpeg\", 2) =~ \"#{@context}:myfile2.jpeg:2%\"\n\n        assert %{\"myfile1.jpeg\" => chan1_pid, \"myfile2.jpeg\" => chan2_pid} =\n                 UploadClient.channel_pids(avatar)\n\n        assert render(lv) =~ \"channel:#{UploadLive.inspect_html_safe(chan1_pid)}\"\n        assert render(lv) =~ \"channel:#{UploadLive.inspect_html_safe(chan2_pid)}\"\n      end\n\n      @tag allow: [max_entries: 1, accept: :any]\n      test \"too many entries over max\", %{lv: lv} do\n        avatar = file_input(lv, \"form\", :avatar, build_entries(2))\n\n        assert lv\n               |> form(\"form\", user: %{})\n               |> render_change(avatar) =~ \"config_error::too_many_files\"\n\n        assert {:error, [[_ref, :too_many_files]]} = render_upload(avatar, \"myfile1.jpeg\", 1)\n      end\n\n      @tag allow: [accept: :any]\n      test \"registering returns too_many_files on back-to-back entries\", %{lv: lv} do\n        avatar = file_input(lv, \"form\", :avatar, build_entries(1))\n        assert render_upload(avatar, \"myfile1.jpeg\", 1) =~ \"#{@context}:myfile1.jpeg:1%\"\n        dup_avatar = file_input(lv, \"form\", :avatar, build_entries(1))\n        assert {:error, [[_, :too_many_files]]} = preflight_upload(dup_avatar)\n      end\n\n      @tag allow: [max_entries: 3, accept: :any]\n      test \"preflight_upload\", %{lv: lv} do\n        avatar = file_input(lv, \"form\", :avatar, build_entries(1))\n        assert {:ok, %{ref: _ref, config: %{chunk_size: _}}} = preflight_upload(avatar)\n      end\n\n      @tag allow: [max_entries: 3, accept: :any]\n      test \"preflighting an already in progress entry is ignored\", %{lv: lv} do\n        avatar = file_input(lv, \"form\", :avatar, build_entries(1))\n        assert render_upload(avatar, \"myfile1.jpeg\", 1) =~ \"1%\"\n        assert %{\"myfile1.jpeg\" => channel_pid} = UploadClient.channel_pids(avatar)\n        assert render(lv) =~ \"channel:#{UploadLive.inspect_html_safe(channel_pid)}\"\n\n        assert {:ok, _} = preflight_upload(avatar)\n        assert %{\"myfile1.jpeg\" => ^channel_pid} = UploadClient.channel_pids(avatar)\n      end\n\n      @tag allow: [max_entries: 3, chunk_size: 20, accept: :any]\n      test \"render_upload uploads entire file by default\", %{lv: lv} do\n        avatar =\n          file_input(lv, \"form\", :avatar, [\n            %{name: \"foo.jpeg\", content: String.duplicate(\"0\", 100)}\n          ])\n\n        assert render_upload(avatar, \"foo.jpeg\") =~ \"100%\"\n      end\n\n      @tag allow: [max_entries: 3, chunk_size: 20, accept: :any]\n      test \"render_upload uploads specified chunk percentage\", %{lv: lv} do\n        avatar =\n          file_input(lv, \"form\", :avatar, [\n            %{name: \"foo.jpeg\", content: String.duplicate(\"0\", 100)}\n          ])\n\n        assert render_upload(avatar, \"foo.jpeg\", 20) =~ \"#{@context}:foo.jpeg:20%\"\n        assert render_upload(avatar, \"foo.jpeg\", 25) =~ \"#{@context}:foo.jpeg:45%\"\n      end\n\n      @tag allow: [max_entries: 3, chunk_size: 20, accept: :any, progress: :consume]\n      test \"render_upload uploads with progress callback\", %{lv: lv} do\n        avatar =\n          file_input(lv, \"form\", :avatar, [\n            %{name: \"foo.jpeg\", content: String.duplicate(\"0\", 100)}\n          ])\n\n        assert render_upload(avatar, \"foo.jpeg\") =~ \"consumed:foo.jpeg\"\n      end\n\n      @tag allow: [max_entries: 3, chunk_size: 20, accept: :any, progress: :consume]\n      test \"render_upload uploads with progress redirect\", %{lv: lv} do\n        avatar =\n          file_input(lv, \"form\", :avatar, [\n            %{name: \"redirect.jpeg\", content: String.duplicate(\"0\", 100)}\n          ])\n\n        assert {:error, {:live_redirect, redir}} = render_upload(avatar, \"redirect.jpeg\")\n        assert redir[:to] == \"/redirected\"\n      end\n\n      @tag allow: [max_entries: 3, chunk_size: 20, accept: :any, progress: :consume]\n      test \"render_upload uploads with progress consume + redirect\", %{lv: lv} do\n        # this is similar to https://github.com/phoenixframework/phoenix_live_view/issues/3662\n        avatar =\n          file_input(lv, \"form\", :avatar, [\n            %{name: \"consume-and-redirect.jpeg\", content: String.duplicate(\"0\", 100)}\n          ])\n\n        assert {:error, {:live_redirect, redir}} =\n                 render_upload(avatar, \"consume-and-redirect.jpeg\")\n\n        assert redir[:to] == \"/redirected\"\n      end\n\n      @tag allow: [max_entries: 3, chunk_size: 20, accept: :any]\n      test \"render_upload with unknown entry\", %{lv: lv} do\n        avatar =\n          file_input(lv, \"form\", :avatar, [\n            %{name: \"foo.jpeg\", content: String.duplicate(\"0\", 100)}\n          ])\n\n        assert UploadLive.exits_with(lv, avatar, RuntimeError, fn ->\n                 render_upload(avatar, \"unknown.jpeg\")\n               end) =~ \"no file input with name \\\"unknown.jpeg\\\"\"\n      end\n\n      @tag allow: [max_entries: 1, chunk_size: 20, accept: :any, max_file_size: 1]\n      test \"render_change error with upload\", %{lv: lv} do\n        avatar = file_input(lv, \"form\", :avatar, [%{name: \"foo.jpeg\", content: \"overmax\"}])\n\n        assert lv\n               |> form(\"form\", user: %{})\n               |> render_change(avatar) =~ \"entry_error::too_large\"\n\n        assert {:error, [[_ref, :too_large]]} = render_upload(avatar, \"foo.jpeg\")\n      end\n\n      @tag allow: [max_entries: 1, chunk_size: 20, accept: :any, auto_upload: true]\n      test \"render_upload too many files with auto_upload\", %{lv: lv} do\n        avatar =\n          file_input(lv, \"form\", :avatar, [\n            %{name: \"foo1.jpeg\", content: \"bytes\"},\n            %{name: \"foo2.jpeg\", content: \"bytes\"}\n          ])\n\n        html =\n          lv\n          |> form(\"form\", user: %{})\n          |> render_change(avatar)\n\n        assert html =~ \"config_error::too_many_files\"\n        assert html =~ \"foo1.jpeg:0%\"\n        assert html =~ \"foo2.jpeg:0%\"\n\n        assert render_upload(avatar, \"foo1.jpeg\") =~ \"foo1.jpeg:100%\"\n        assert {:error, :not_allowed} = render_upload(avatar, \"foo2.jpeg\")\n      end\n\n      @tag allow: [\n             max_entries: 1,\n             chunk_size: 20,\n             accept: :any,\n             max_file_size: 1,\n             auto_upload: true\n           ]\n      test \"render_upload invalid with auto_upload\", %{lv: lv} do\n        avatar = file_input(lv, \"form\", :avatar, [%{name: \"foo.jpeg\", content: \"overmax\"}])\n\n        html =\n          lv\n          |> form(\"form\", user: %{})\n          |> render_change(avatar)\n\n        assert html =~ \"entry_error::too_large\"\n        assert html =~ \"foo.jpeg:0%\"\n\n        assert {:error, [[_ref, :too_large]]} = render_upload(avatar, \"foo.jpeg\")\n      end\n\n      @tag allow: [max_entries: 1, chunk_size: 20, accept: :any]\n      test \"render_change success with upload\", %{lv: lv} do\n        avatar = file_input(lv, \"form\", :avatar, [%{name: \"foo.jpeg\", content: \"ok\"}])\n\n        refute lv\n               |> form(\"form\", user: %{})\n               |> render_change(avatar) =~ \"error\"\n\n        assert render_upload(avatar, \"foo.jpeg\") =~ \"100%\"\n      end\n\n      @tag allow: [max_entries: 1, chunk_size: 20, accept: :any]\n      test \"get_uploaded_entries\", %{lv: lv} do\n        avatar =\n          file_input(lv, \"form\", :avatar, [\n            %{name: \"foo.jpeg\", content: String.duplicate(\"0\", 100)}\n          ])\n\n        assert get_uploaded_entries(lv, :avatar) == {[], []}\n        assert render_upload(avatar, \"foo.jpeg\", 1) =~ \"1%\"\n\n        assert {[], [%Phoenix.LiveView.UploadEntry{progress: 1}]} =\n                 get_uploaded_entries(lv, :avatar)\n\n        assert render_upload(avatar, \"foo.jpeg\", 99) =~ \"100%\"\n\n        assert {[%Phoenix.LiveView.UploadEntry{progress: 100}], []} =\n                 get_uploaded_entries(lv, :avatar)\n      end\n\n      @tag allow: [max_entries: 1, chunk_size: 20, accept: :any]\n      test \"consume_uploaded_entries executes function against all entries and shuts down\",\n           %{lv: lv} do\n        parent = self()\n        avatar = file_input(lv, \"form\", :avatar, [%{name: \"foo.jpeg\", content: \"123\"}])\n        avatar_pid = avatar.pid\n        assert render_upload(avatar, \"foo.jpeg\") =~ \"100%\"\n        assert %{\"foo.jpeg\" => channel_pid} = UploadClient.channel_pids(avatar)\n\n        Process.monitor(avatar_pid)\n        Process.monitor(channel_pid)\n\n        UploadLive.run(lv, fn socket ->\n          Phoenix.LiveView.consume_uploaded_entries(socket, :avatar, fn %{path: path}, entry ->\n            {:ok, send(parent, {:file, path, entry.client_name, File.read!(path)})}\n          end)\n\n          {:reply, :ok, socket}\n        end)\n\n        # Wait for the UploadClient and UploadChannel to shutdown\n        assert_receive {:DOWN, _ref, :process, ^avatar_pid, {:shutdown, :closed}}, 1000\n        assert_receive {:DOWN, _ref, :process, ^channel_pid, {:shutdown, :closed}}, 1000\n        assert_receive {:file, _tmp_path, \"foo.jpeg\", \"123\"}\n      end\n\n      @tag allow: [max_entries: 1, chunk_size: 20, accept: :any]\n      test \"consume_uploaded_entry executes function and shuts down\", %{\n        lv: lv\n      } do\n        parent = self()\n        avatar = file_input(lv, \"form\", :avatar, [%{name: \"foo.jpeg\", content: \"123\"}])\n        avatar_pid = avatar.pid\n        assert render_upload(avatar, \"foo.jpeg\") =~ \"100%\"\n        Process.monitor(avatar_pid)\n\n        UploadLive.run(lv, fn socket ->\n          {[entry], []} = Phoenix.LiveView.uploaded_entries(socket, :avatar)\n\n          Phoenix.LiveView.consume_uploaded_entry(socket, entry, fn %{path: path} ->\n            {:ok, send(parent, {:file, path, entry.client_name, File.read!(path)})}\n          end)\n\n          {:reply, :ok, socket}\n        end)\n\n        assert_receive {:DOWN, _ref, :process, ^avatar_pid, {:shutdown, :closed}}, 1000\n        assert_receive {:file, _tmp_path, \"foo.jpeg\", \"123\"}\n      end\n\n      @tag allow: [max_entries: 1, chunk_size: 20, accept: :any]\n      test \"consume_uploaded_entries returns empty list when no uploads exist\", %{lv: lv} do\n        parent = self()\n\n        UploadLive.run(lv, fn socket ->\n          result =\n            Phoenix.LiveView.consume_uploaded_entries(socket, :avatar, fn _file, _entry ->\n              :boom\n            end)\n\n          send(parent, {:consumed, result})\n          {:reply, :ok, socket}\n        end)\n\n        assert_receive {:consumed, []}\n      end\n\n      @tag allow: [max_entries: 1, chunk_size: 20, accept: :any]\n      test \"consume_uploaded_entries raises when upload is still in progress\", %{lv: lv} do\n        avatar =\n          file_input(lv, \"form\", :avatar, [\n            %{name: \"foo.jpeg\", content: String.duplicate(\"0\", 100)}\n          ])\n\n        assert render_upload(avatar, \"foo.jpeg\", 1) =~ \"1%\"\n\n        try do\n          UploadLive.run(lv, fn socket ->\n            Phoenix.LiveView.consume_uploaded_entries(socket, :avatar, fn _file, _entry ->\n              :boom\n            end)\n          end)\n        catch\n          :exit, {{%ArgumentError{message: msg}, _}, _} ->\n            assert msg =~ \"cannot consume uploaded files when entries are still in progress\"\n        end\n      end\n\n      @tag allow: [max_entries: 1, chunk_size: 20, accept: :any]\n      test \"consume_uploaded_entry raises when upload is still in progress\", %{lv: lv} do\n        avatar =\n          file_input(lv, \"form\", :avatar, [\n            %{name: \"foo.jpeg\", content: String.duplicate(\"0\", 100)}\n          ])\n\n        assert render_upload(avatar, \"foo.jpeg\", 1) =~ \"1%\"\n\n        try do\n          UploadLive.run(lv, fn socket ->\n            {[], [in_progress_entry]} = Phoenix.LiveView.uploaded_entries(socket, :avatar)\n\n            Phoenix.LiveView.consume_uploaded_entry(socket, in_progress_entry, fn _file ->\n              :boom\n            end)\n          end)\n        catch\n          :exit, {{%ArgumentError{message: msg}, _}, _} ->\n            assert msg =~ \"cannot consume uploaded files when entries are still in progress\"\n        end\n      end\n\n      @tag allow: [max_entries: 1, chunk_size: 20, accept: :any]\n      test \"consume_uploaded_entries can postpone consumption\",\n           %{lv: lv} do\n        parent = self()\n        avatar = file_input(lv, \"form\", :avatar, [%{name: \"foo.jpeg\", content: \"123\"}])\n        avatar_pid = avatar.pid\n        assert render_upload(avatar, \"foo.jpeg\") =~ \"100%\"\n        assert %{\"foo.jpeg\" => channel_pid} = UploadClient.channel_pids(avatar)\n\n        Process.monitor(avatar_pid)\n        Process.monitor(channel_pid)\n\n        UploadLive.run(lv, fn socket ->\n          results =\n            Phoenix.LiveView.consume_uploaded_entries(socket, :avatar, fn %{path: path}, entry ->\n              send(parent, {:file, path, entry.client_name, File.read!(path)})\n              {:postpone, {:postponed, path}}\n            end)\n\n          send(parent, {:results, results})\n          {:reply, :ok, socket}\n        end)\n\n        assert_receive {:results, [{:postponed, tmp_path}]}\n        assert_receive {:file, ^tmp_path, \"foo.jpeg\", \"123\"}\n        refute_receive {:DOWN, _ref, :process, ^avatar_pid, _}\n        refute_receive {:DOWN, _ref, :process, ^channel_pid, _}\n        assert File.exists?(tmp_path)\n\n        UploadLive.run(lv, fn socket ->\n          results =\n            Phoenix.LiveView.consume_uploaded_entries(socket, :avatar, fn %{path: path}, entry ->\n              send(parent, {:file, path, entry.client_name, File.read!(path)})\n              {:ok, {:consumed, path}}\n            end)\n\n          send(parent, {:results, results})\n          {:reply, :ok, socket}\n        end)\n\n        assert_receive {:results, [{:consumed, tmp_path}]}\n        assert_receive {:file, ^tmp_path, \"foo.jpeg\", \"123\"}\n        assert_receive {:DOWN, _ref, :process, ^avatar_pid, {:shutdown, :closed}}, 1000\n        assert_receive {:DOWN, _ref, :process, ^channel_pid, {:shutdown, :closed}}, 1000\n      end\n\n      @tag allow: [max_entries: 1, chunk_size: 20, accept: :any]\n      test \"consume_uploaded_entry can postpone consumption\",\n           %{lv: lv} do\n        parent = self()\n        avatar = file_input(lv, \"form\", :avatar, [%{name: \"foo.jpeg\", content: \"123\"}])\n        avatar_pid = avatar.pid\n        assert render_upload(avatar, \"foo.jpeg\") =~ \"100%\"\n        assert %{\"foo.jpeg\" => channel_pid} = UploadClient.channel_pids(avatar)\n\n        Process.monitor(avatar_pid)\n        Process.monitor(channel_pid)\n\n        UploadLive.run(lv, fn socket ->\n          {[entry], []} = Phoenix.LiveView.uploaded_entries(socket, :avatar)\n\n          result =\n            Phoenix.LiveView.consume_uploaded_entry(socket, entry, fn %{path: path} ->\n              send(parent, {:file, path, entry.client_name, File.read!(path)})\n              {:postpone, {:postponed, path}}\n            end)\n\n          send(parent, {:result, result})\n          {:reply, :ok, socket}\n        end)\n\n        assert_receive {:result, {:postponed, tmp_path}}\n        assert_receive {:file, ^tmp_path, \"foo.jpeg\", \"123\"}\n        refute_receive {:DOWN, _ref, :process, ^avatar_pid, _}\n        refute_receive {:DOWN, _ref, :process, ^channel_pid, _}\n        assert File.exists?(tmp_path)\n\n        UploadLive.run(lv, fn socket ->\n          {[entry], []} = Phoenix.LiveView.uploaded_entries(socket, :avatar)\n\n          result =\n            Phoenix.LiveView.consume_uploaded_entry(socket, entry, fn %{path: path} ->\n              send(parent, {:file, path, entry.client_name, File.read!(path)})\n              {:ok, {:consumed, path}}\n            end)\n\n          send(parent, {:result, result})\n          {:reply, :ok, socket}\n        end)\n\n        assert_receive {:result, {:consumed, tmp_path}}\n        assert_receive {:file, ^tmp_path, \"foo.jpeg\", \"123\"}\n        assert_receive {:DOWN, _ref, :process, ^avatar_pid, {:shutdown, :closed}}, 1000\n        assert_receive {:DOWN, _ref, :process, ^channel_pid, {:shutdown, :closed}}, 1000\n      end\n\n      @tag allow: [max_entries: 1, accept: :any]\n      test \"cancel_upload with invalid ref\", %{lv: lv} do\n        avatar =\n          file_input(lv, \"form\", :avatar, [\n            %{name: \"foo.jpeg\", content: String.duplicate(\"0\", 100)}\n          ])\n\n        assert UploadLive.exits_with(lv, avatar, ArgumentError, fn ->\n                 UploadLive.run(lv, fn socket ->\n                   {:reply, :ok, Phoenix.LiveView.cancel_upload(socket, :avatar, \"bad_ref\")}\n                 end)\n               end) =~ \"no entry in upload \\\":avatar\\\" with ref \\\"bad_ref\\\"\"\n      end\n\n      @tag allow: [max_entries: 1, chunk_size: 20, accept: :any]\n      test \"cancel_upload in progress\", %{lv: lv} do\n        avatar =\n          file_input(lv, \"form\", :avatar, [\n            %{name: \"foo.jpeg\", content: String.duplicate(\"0\", 100)}\n          ])\n\n        assert render_upload(avatar, \"foo.jpeg\", 1) =~ \"1%\"\n        assert %{\"foo.jpeg\" => channel_pid} = UploadClient.channel_pids(avatar)\n\n        unlink(channel_pid, lv, avatar)\n        Process.monitor(channel_pid)\n\n        UploadLive.run(lv, fn socket ->\n          {[], [%{ref: ref}]} = Phoenix.LiveView.uploaded_entries(socket, :avatar)\n          {:reply, :ok, Phoenix.LiveView.cancel_upload(socket, :avatar, ref)}\n        end)\n\n        assert_receive {:DOWN, _ref, :process, ^channel_pid, {:shutdown, :closed}}, 1000\n\n        assert UploadLive.run(lv, fn socket ->\n                 {:reply, Phoenix.LiveView.uploaded_entries(socket, :avatar), socket}\n               end) == {[], []}\n      end\n\n      @tag allow: [max_entries: 1, chunk_size: 20, accept: :any]\n      test \"cancel_upload not yet in progress\", %{lv: lv} do\n        file_name = \"foo.jpeg\"\n        avatar = file_input(lv, \"form\", :avatar, [%{name: file_name, content: \"ok\"}])\n\n        assert lv\n               |> form(\"form\", user: %{})\n               |> render_change(avatar) =~ file_name\n\n        assert UploadClient.channel_pids(avatar) == %{}\n\n        assert {[], [%{ref: ref}]} =\n                 UploadLive.run(lv, fn socket ->\n                   {:reply, Phoenix.LiveView.uploaded_entries(socket, :avatar), socket}\n                 end)\n\n        UploadLive.run(lv, fn socket ->\n          {:reply, :ok, Phoenix.LiveView.cancel_upload(socket, :avatar, ref)}\n        end)\n\n        refute render(lv) =~ file_name\n      end\n\n      @tag allow: [max_entries: 1, chunk_size: 20, accept: :any]\n      test \"allow_upload with active entries\", %{lv: lv} do\n        avatar =\n          file_input(lv, \"form\", :avatar, [\n            %{name: \"foo.jpeg\", content: String.duplicate(\"0\", 100)}\n          ])\n\n        assert render_upload(avatar, \"foo.jpeg\", 1) =~ \"1%\"\n\n        assert UploadLive.exits_with(lv, avatar, ArgumentError, fn ->\n                 UploadLive.run(lv, fn socket ->\n                   {:reply, :ok, Phoenix.LiveView.allow_upload(socket, :avatar, accept: :any)}\n                 end)\n               end) =~ \"cannot allow_upload on an existing upload with active entries\"\n      end\n\n      @tag allow: [\n             max_entries: 1,\n             chunk_size: 50,\n             accept: :any,\n             writer: &__MODULE__.build_writer/3\n           ]\n      test \"writer can be configured\", %{lv: lv} do\n        Process.register(self(), :test_writer)\n\n        content = String.duplicate(\"0\", 100)\n\n        avatar =\n          file_input(lv, \"form\", :avatar, [\n            %{name: \"foo.jpeg\", content: content}\n          ])\n\n        assert render_upload(avatar, \"foo.jpeg\", 50) =~ \"#{@context}:foo.jpeg:50%\"\n        assert render_upload(avatar, \"foo.jpeg\", 50) =~ \"#{@context}:foo.jpeg:100%\"\n\n        metas =\n          UploadLive.run(lv, fn socket ->\n            results =\n              Phoenix.LiveView.consume_uploaded_entries(socket, :avatar, fn meta, _entry ->\n                {:ok, meta}\n              end)\n\n            {:reply, results, socket}\n          end)\n\n        assert metas == [:test_writer]\n        assert_receive :init\n        assert_receive :meta\n        assert_receive {:write_chunk, chunk1}\n        assert_receive {:write_chunk, chunk2}\n        refute_receive {:write_chunk, _}\n        assert chunk1 <> chunk2 == content\n        assert_receive {:close, :done}\n      end\n\n      @tag allow: [\n             max_entries: 1,\n             chunk_size: 50,\n             accept: :any,\n             writer: &__MODULE__.build_writer/3\n           ]\n      test \"writer with LiveView exit\", %{lv: lv} do\n        Process.register(self(), :test_writer)\n\n        content = String.duplicate(\"0\", 100)\n\n        avatar =\n          file_input(lv, \"form\", :avatar, [\n            %{name: \"foo.jpeg\", content: content}\n          ])\n\n        assert render_upload(avatar, \"foo.jpeg\", 50) =~ \"#{@context}:foo.jpeg:50%\"\n\n        assert %{\"foo.jpeg\" => channel_pid} = UploadClient.channel_pids(avatar)\n\n        unlink(channel_pid, lv, avatar)\n        Process.monitor(channel_pid)\n        Process.exit(lv.pid, :kill)\n\n        assert_receive {:write_chunk, _chunk1}\n        refute_receive {:write_chunk, _}\n        assert_receive {:close, :cancel}\n      end\n\n      @tag allow: [\n             max_entries: 1,\n             chunk_size: 50,\n             accept: :any,\n             writer: &__MODULE__.build_writer/3\n           ]\n      test \"writer with error\", %{lv: lv} do\n        Process.register(self(), :test_writer)\n\n        content = \"error\"\n\n        avatar =\n          file_input(lv, \"form\", :avatar, [\n            %{name: \"foo.jpeg\", content: content}\n          ])\n\n        assert render_upload(avatar, \"foo.jpeg\") =~\n                 ~s/entry_error:{:writer_failure, :custom_error}/\n\n        assert_receive {:close, {:error, :custom_error}}\n      end\n    end\n  end\n\n  describe \"component uploads\" do\n    setup :setup_component\n\n    @tag allow: [accept: :any]\n    test \"liveview exits when duplicate name registered for another cid\", %{lv: lv} do\n      avatar = file_input(lv, \"#upload0\", :avatar, build_entries(1))\n      assert render_upload(avatar, \"myfile1.jpeg\", 1) =~ \"component:myfile1.jpeg:1%\"\n\n      GenServer.call(\n        lv.pid,\n        {:setup, fn socket -> Component.assign(socket, uploads_count: 2) end}\n      )\n\n      GenServer.call(\n        lv.pid,\n        {:setup,\n         fn socket ->\n           run = fn component_socket ->\n             new_socket = LiveView.allow_upload(component_socket, :avatar, accept: :any)\n             {:reply, :ok, new_socket}\n           end\n\n           LiveView.send_update(Phoenix.LiveViewTest.Support.UploadComponent,\n             id: \"upload1\",\n             run: {run, nil}\n           )\n\n           socket\n         end}\n      )\n\n      dup_avatar = file_input(lv, \"#upload1\", :avatar, build_entries(1))\n\n      assert UploadLive.exits_with(lv, dup_avatar, RuntimeError, fn ->\n               render_upload(dup_avatar, \"myfile1.jpeg\", 1)\n             end) =~ \"existing upload for avatar already allowed in another component\"\n\n      refute Process.alive?(lv.pid)\n    end\n\n    @tag allow: [max_entries: 1, chunk_size: 20, accept: :any]\n    test \"cancel_upload in progress when component is removed\", %{lv: lv} do\n      avatar = file_input(lv, \"#upload0\", :avatar, build_entries(1))\n      assert render_upload(avatar, \"myfile1.jpeg\", 1) =~ \"component:myfile1.jpeg:1%\"\n      assert %{\"myfile1.jpeg\" => channel_pid} = UploadClient.channel_pids(avatar)\n\n      unlink(channel_pid, lv, avatar)\n      Process.monitor(channel_pid)\n\n      assert render(lv) =~ \"myfile1.jpeg\"\n      GenServer.call(lv.pid, {:uploads, 0})\n\n      refute render(lv) =~ \"myfile1.jpeg\"\n\n      assert_receive {:DOWN, _ref, :process, ^channel_pid, {:shutdown, :closed}}, 1000\n\n      # retry with new component\n      GenServer.call(lv.pid, {:uploads, 1})\n\n      UploadLive.run(lv, fn component_socket ->\n        new_socket = Phoenix.LiveView.allow_upload(component_socket, :avatar, accept: :any)\n        {:reply, :ok, new_socket}\n      end)\n\n      avatar = file_input(lv, \"#upload0\", :avatar, build_entries(1))\n      assert render_upload(avatar, \"myfile1.jpeg\", 100) =~ \"component:myfile1.jpeg:100%\"\n    end\n\n    @tag allow: [max_entries: 1, chunk_size: 20, accept: :any]\n    test \"cancel_upload not yet in progress when component is removed\", %{lv: lv} do\n      file_name = \"myfile1.jpeg\"\n      avatar = file_input(lv, \"#upload0\", :avatar, [%{name: file_name, content: \"ok\"}])\n\n      assert lv\n             |> form(\"form\", user: %{})\n             |> render_change(avatar) =~ file_name\n\n      assert UploadClient.channel_pids(avatar) == %{}\n\n      assert render(lv) =~ file_name\n\n      GenServer.call(lv.pid, {:uploads, 0})\n\n      refute render(lv) =~ file_name\n\n      # retry with new component\n      GenServer.call(lv.pid, {:uploads, 1})\n\n      UploadLive.run(lv, fn component_socket ->\n        new_socket = Phoenix.LiveView.allow_upload(component_socket, :avatar, accept: :any)\n        {:reply, :ok, new_socket}\n      end)\n\n      avatar = file_input(lv, \"#upload0\", :avatar, build_entries(1))\n      assert render_upload(avatar, \"myfile1.jpeg\", 100) =~ \"component:myfile1.jpeg:100%\"\n    end\n\n    @tag allow: [accept: :any]\n    test \"get allowed uploads from the form's target cid\", %{lv: lv} do\n      GenServer.call(\n        lv.pid,\n        {:setup, fn socket -> Component.assign(socket, uploads_count: 2) end}\n      )\n\n      GenServer.call(\n        lv.pid,\n        {:setup,\n         fn socket ->\n           run = fn component_socket ->\n             new_socket =\n               component_socket\n               |> Phoenix.LiveView.allow_upload(:avatar, accept: :any)\n               |> Phoenix.LiveView.allow_upload(:background, accept: :any)\n\n             {:reply, :ok, new_socket}\n           end\n\n           LiveView.send_update(Phoenix.LiveViewTest.Support.UploadComponent,\n             id: \"upload1\",\n             run: {run, nil}\n           )\n\n           socket\n         end}\n      )\n\n      assert %Phoenix.LiveViewTest.Upload{} =\n               file_input(lv, \"#upload1\", :background, build_entries(1))\n\n      assert_raise RuntimeError, \"no uploads allowed for background\", fn ->\n        file_input(lv, \"#upload0\", :background, build_entries(1))\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/upload/config_test.exs",
    "content": "defmodule Phoenix.LiveView.UploadConfigTest do\n  use ExUnit.Case, async: true\n\n  alias Phoenix.LiveView\n  alias Phoenix.LiveView.{UploadConfig, UploadEntry}\n\n  defp build_socket() do\n    %LiveView.Socket{}\n  end\n\n  defp drop_entry(%UploadConfig{} = conf, ref) do\n    entry = UploadConfig.get_entry_by_ref(conf, ref)\n    UploadConfig.drop_entry(conf, entry)\n  end\n\n  describe \"allow_upload/3\" do\n    test \"raises when no or invalid :accept provided\" do\n      assert_raise ArgumentError, ~r/the :accept option is required/, fn ->\n        LiveView.allow_upload(build_socket(), :avatar, max_entries: 5)\n      end\n\n      assert_raise ArgumentError, ~r/invalid accept filter provided/, fn ->\n        LiveView.allow_upload(build_socket(), :avatar, accept: [])\n      end\n\n      assert_raise ArgumentError, ~r/invalid accept filter provided/, fn ->\n        LiveView.allow_upload(build_socket(), :avatar, accept: :bad)\n      end\n\n      assert_raise ArgumentError, ~r/invalid accept filter provided/, fn ->\n        LiveView.allow_upload(build_socket(), :avatar, accept: \"bad\")\n      end\n\n      assert_raise ArgumentError, ~r/invalid accept filter provided/, fn ->\n        LiveView.allow_upload(build_socket(), :avatar, accept: [\"bad\"])\n      end\n\n      assert_raise ArgumentError, ~r/invalid accept filter provided/, fn ->\n        LiveView.allow_upload(build_socket(), :avatar, accept: ~w(.foobarbaz))\n      end\n\n      assert_raise ArgumentError, ~r/invalid accept filter provided/, fn ->\n        LiveView.allow_upload(build_socket(), :avatar, accept: ~w(.jpg image/jpeg bad))\n      end\n\n      assert_raise ArgumentError, ~r/invalid accept filter provided/, fn ->\n        LiveView.allow_upload(build_socket(), :avatar, accept: ~w(foo/*))\n      end\n    end\n\n    test \":accept supports list of extensions and mime types\" do\n      socket = LiveView.allow_upload(build_socket(), :avatar, accept: ~w(.jpg .jpeg))\n      assert %UploadConfig{name: :avatar} = conf = socket.assigns.uploads.avatar\n      assert conf.accept == \".jpg,.jpeg\"\n      assert conf.acceptable_types == MapSet.new([\"image/jpeg\"])\n\n      socket = LiveView.allow_upload(build_socket(), :avatar, accept: ~w(image/png .jpeg))\n      assert %UploadConfig{name: :avatar} = conf = socket.assigns.uploads.avatar\n      assert conf.accept == \"image/png,.jpeg\"\n      assert conf.acceptable_types == MapSet.new([\"image/jpeg\", \"image/png\"])\n      assert conf.acceptable_exts == MapSet.new([\".jpeg\"])\n\n      doc =\n        ~w(.doc .docx .xml application/msword application/vnd.openxmlformats-officedocument.wordprocessingml.document)\n\n      html_doc = Enum.join(doc, \",\")\n\n      socket = LiveView.allow_upload(build_socket(), :avatar, accept: doc)\n\n      assert %UploadConfig{\n               name: :avatar,\n               accept: ^html_doc\n             } = conf = socket.assigns.uploads.avatar\n\n      assert conf.acceptable_types ==\n               MapSet.new([\n                 \"application/msword\",\n                 \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n                 \"text/xml\"\n               ])\n\n      assert conf.acceptable_exts == MapSet.new(~w(.doc .docx .xml))\n    end\n\n    test \":accept supports :any file\" do\n      socket = LiveView.allow_upload(build_socket(), :avatar, accept: :any)\n      assert %UploadConfig{name: :avatar, accept: :any} = socket.assigns.uploads.avatar\n    end\n\n    test \":accept supports wildcard types\" do\n      socket = LiveView.allow_upload(build_socket(), :avatar, accept: ~w(image/*))\n      assert %UploadConfig{name: :avatar} = conf = socket.assigns.uploads.avatar\n      assert conf.accept == \"image/*\"\n      assert conf.acceptable_types == MapSet.new([\"image/*\"])\n      assert conf.acceptable_exts == MapSet.new([])\n\n      socket = LiveView.allow_upload(build_socket(), :avatar, accept: ~w(audio/*))\n      assert %UploadConfig{name: :avatar} = conf = socket.assigns.uploads.avatar\n      assert conf.accept == \"audio/*\"\n      assert conf.acceptable_types == MapSet.new([\"audio/*\"])\n      assert conf.acceptable_exts == MapSet.new([])\n\n      socket = LiveView.allow_upload(build_socket(), :avatar, accept: ~w(video/*))\n      assert %UploadConfig{name: :avatar} = conf = socket.assigns.uploads.avatar\n      assert conf.accept == \"video/*\"\n      assert conf.acceptable_types == MapSet.new([\"video/*\"])\n      assert conf.acceptable_exts == MapSet.new([])\n\n      socket = LiveView.allow_upload(build_socket(), :avatar, accept: ~w(video/* .gif))\n      assert %UploadConfig{name: :avatar} = conf = socket.assigns.uploads.avatar\n      assert conf.accept == \"video/*,.gif\"\n      assert conf.acceptable_types == MapSet.new([\"image/gif\", \"video/*\"])\n      assert conf.acceptable_exts == MapSet.new([\".gif\"])\n    end\n\n    test \"raises when invalid :max_entries provided\" do\n      assert_raise ArgumentError, ~r/invalid :max_entries value provided/, fn ->\n        LiveView.allow_upload(build_socket(), :avatar, accept: :any, max_entries: -1)\n      end\n\n      assert_raise ArgumentError, ~r/invalid :max_entries value provided/, fn ->\n        LiveView.allow_upload(build_socket(), :avatar, accept: :any, max_entries: 0)\n      end\n\n      assert_raise ArgumentError, ~r/invalid :max_entries value provided/, fn ->\n        LiveView.allow_upload(build_socket(), :avatar, accept: :any, max_entries: \"bad\")\n      end\n    end\n\n    test \"raises when invalid :max_file_size provided\" do\n      assert_raise ArgumentError, ~r/invalid :max_file_size value provided/, fn ->\n        LiveView.allow_upload(build_socket(), :avatar, accept: :any, max_file_size: -1)\n      end\n\n      assert_raise ArgumentError, ~r/invalid :max_file_size value provided/, fn ->\n        LiveView.allow_upload(build_socket(), :avatar, accept: :any, max_file_size: 0)\n      end\n\n      assert_raise ArgumentError, ~r/invalid :max_file_size value provided/, fn ->\n        LiveView.allow_upload(build_socket(), :avatar, accept: :any, max_file_size: \"bad\")\n      end\n    end\n\n    test \"supports optional :max_file_size\" do\n      socket = LiveView.allow_upload(build_socket(), :avatar, accept: :any)\n      assert %UploadConfig{max_file_size: 8_000_000} = socket.assigns.uploads.avatar\n\n      socket =\n        LiveView.allow_upload(build_socket(), :avatar,\n          accept: :any,\n          max_file_size: 10_000_000\n        )\n\n      assert %UploadConfig{max_file_size: 10_000_000} = socket.assigns.uploads.avatar\n    end\n  end\n\n  describe \"disallow_upload/2\" do\n    test \"disallows upload\" do\n      socket = LiveView.allow_upload(build_socket(), :avatar, accept: :any)\n      assert socket.assigns.uploads.avatar.allowed?\n      socket = LiveView.disallow_upload(socket, :avatar)\n      refute socket.assigns.uploads.avatar.allowed?\n    end\n\n    test \"raises when upload has active entries\" do\n      socket = LiveView.allow_upload(build_socket(), :avatar, accept: :any)\n\n      {:ok, socket} =\n        LiveView.Upload.put_entries(\n          socket,\n          socket.assigns.uploads.avatar,\n          [\n            build_client_entry(:avatar, %{\"size\" => 1024})\n          ],\n          nil\n        )\n\n      assert_raise RuntimeError, ~r/unable to disallow_upload/, fn ->\n        LiveView.disallow_upload(socket, :avatar)\n      end\n    end\n  end\n\n  describe \"put_entries/2\" do\n    test \"does not overwrite existing refs\" do\n      socket = LiveView.allow_upload(build_socket(), :avatar, accept: :any, max_entries: 1)\n\n      %{\n        \"name\" => name,\n        \"relative_path\" => relative_path,\n        \"size\" => size,\n        \"ref\" => ref,\n        \"type\" => type\n      } = entry = build_client_entry(:avatar)\n\n      assert {:ok, avatar} = UploadConfig.put_entries(socket.assigns.uploads.avatar, [entry])\n      entries_before = avatar.entries\n\n      assert [\n               %Phoenix.LiveView.UploadEntry{\n                 client_name: ^name,\n                 client_relative_path: ^relative_path,\n                 client_size: ^size,\n                 client_type: ^type,\n                 ref: ^ref\n               }\n             ] = entries_before\n\n      modified_entry = Map.update!(entry, \"size\", fn _ -> 5009 end)\n      assert {:ok, avatar} = UploadConfig.put_entries(avatar, [modified_entry])\n      assert entries_before == avatar.entries\n    end\n\n    test \"replaces sole entry for max_entries of 1\" do\n      socket = LiveView.allow_upload(build_socket(), :avatar, accept: :any, max_entries: 1)\n\n      %{\n        \"name\" => name,\n        \"relative_path\" => relative_path,\n        \"size\" => size,\n        \"ref\" => ref,\n        \"type\" => type\n      } = entry = build_client_entry(:avatar)\n\n      assert {:ok, avatar} = UploadConfig.put_entries(socket.assigns.uploads.avatar, [entry])\n      entries_before = avatar.entries\n\n      assert [\n               %Phoenix.LiveView.UploadEntry{\n                 client_name: ^name,\n                 client_relative_path: ^relative_path,\n                 client_size: ^size,\n                 client_type: ^type,\n                 ref: ^ref\n               }\n             ] = entries_before\n\n      modified_entry = Map.update!(entry, \"ref\", fn _ -> \"1234\" end)\n      assert {:ok, avatar} = UploadConfig.put_entries(avatar, [modified_entry])\n      assert entries_before != avatar.entries\n      assert length(avatar.entries) == 1\n    end\n\n    test \"returns error when greater than max_entries are provided\" do\n      socket = LiveView.allow_upload(build_socket(), :avatar, accept: :any)\n\n      entry = build_client_entry(:avatar)\n\n      assert {:error, avatar} =\n               UploadConfig.put_entries(socket.assigns.uploads.avatar, [\n                 build_client_entry(:avatar),\n                 entry\n               ])\n\n      assert avatar.errors == [{avatar.ref, :too_many_files}]\n    end\n\n    test \"returns error when entry with greater than max_file_size provided\" do\n      socket = LiveView.allow_upload(build_socket(), :avatar, accept: :any)\n      entry = build_client_entry(:avatar, %{\"size\" => 8_000_001})\n      assert {:error, avatar} = UploadConfig.put_entries(socket.assigns.uploads.avatar, [entry])\n      assert avatar.errors == [{entry[\"ref\"], :too_large}]\n\n      socket = LiveView.allow_upload(build_socket(), :avatar, accept: :any, max_file_size: 1024)\n      entry = build_client_entry(:avatar, %{\"size\" => 2048})\n      assert {:error, avatar} = UploadConfig.put_entries(socket.assigns.uploads.avatar, [entry])\n      assert avatar.errors == [{entry[\"ref\"], :too_large}]\n    end\n\n    test \"validates client size less than :max_file_size and generates uuid when valid\" do\n      socket = LiveView.allow_upload(build_socket(), :avatar, accept: :any)\n\n      {:ok, config} =\n        UploadConfig.put_entries(socket.assigns.uploads.avatar, [\n          build_client_entry(:avatar, %{\"size\" => 1024})\n        ])\n\n      assert [%UploadEntry{client_size: 1024, uuid: uuid}] = config.entries\n      assert uuid\n    end\n\n    test \"validates entries accepted by extension\" do\n      socket =\n        build_socket()\n        |> LiveView.allow_upload(:avatar, accept: ~w(.jpg .jpeg), max_entries: 5)\n        |> LiveView.allow_upload(:hero, accept: ~w(.jpg .jpeg), max_entries: 5)\n        |> LiveView.allow_upload(:audio, accept: ~w(.wav), max_entries: 2)\n\n      assert {:ok, config} =\n               UploadConfig.put_entries(socket.assigns.uploads.avatar, [\n                 build_client_entry(:avatar, %{\"name\" => \"photo.jpg\", \"type\" => \"image/jpeg\"}),\n                 build_client_entry(:avatar, %{\"name\" => \"photo.JPG\", \"type\" => \"image/jpeg\"}),\n                 build_client_entry(:avatar, %{\"name\" => \"photo.jpeg\", \"type\" => \"image/jpeg\"}),\n                 build_client_entry(:avatar, %{\"name\" => \"photo.JPEG\", \"type\" => \"image/jpeg\"})\n               ])\n\n      assert [\n               %UploadEntry{client_name: \"photo.jpg\"},\n               %UploadEntry{client_name: \"photo.JPG\"},\n               %UploadEntry{client_name: \"photo.jpeg\"},\n               %UploadEntry{client_name: \"photo.JPEG\"}\n             ] = config.entries\n\n      assert {:ok, config} =\n               UploadConfig.put_entries(socket.assigns.uploads.audio, [\n                 build_client_entry(:audio, %{\"name\" => \"audio.wav\", \"type\" => \"audio/wav\"}),\n                 build_client_entry(:audio, %{\"name\" => \"audio.WAV\", \"type\" => \"audio/wav\"})\n               ])\n\n      assert [\n               %UploadEntry{client_name: \"audio.wav\"},\n               %UploadEntry{client_name: \"audio.WAV\"}\n             ] = config.entries\n\n      hero_config = socket.assigns.uploads.hero\n      entry = build_client_entry(:avatar, %{\"name\" => \"file.gif\"})\n\n      assert {:error, %UploadConfig{} = hero_config} =\n               UploadConfig.put_entries(hero_config, [entry])\n\n      assert hero_config.errors == [{entry[\"ref\"], :not_accepted}]\n\n      hero_config = drop_entry(hero_config, entry[\"ref\"])\n      entry = build_client_entry(:avatar, %{\"name\" => \"file.gif\", \"type\" => \"image/png\"})\n\n      assert {:error, %UploadConfig{} = hero_config} =\n               UploadConfig.put_entries(hero_config, [entry])\n\n      assert hero_config.errors == [{entry[\"ref\"], :not_accepted}]\n\n      hero_config = drop_entry(hero_config, entry[\"ref\"])\n      entry = build_client_entry(:avatar, %{\"name\" => \"file\", \"type\" => \"image/png\"})\n\n      assert {:error, %UploadConfig{} = hero_config} =\n               UploadConfig.put_entries(hero_config, [entry])\n\n      assert hero_config.errors == [{entry[\"ref\"], :not_accepted}]\n    end\n\n    test \"validates entries accepted by type\" do\n      socket =\n        build_socket()\n        |> LiveView.allow_upload(:avatar, accept: ~w(image/png image/jpeg), max_entries: 4)\n        |> LiveView.allow_upload(:hero, accept: ~w(image/png image/jpeg), max_entries: 4)\n\n      assert {:ok, config} =\n               UploadConfig.put_entries(socket.assigns.uploads.avatar, [\n                 build_client_entry(:avatar, %{\"name\" => \"photo\", \"type\" => \"image/png\"}),\n                 build_client_entry(:avatar, %{\"name\" => \"photo\", \"type\" => \"image/jpeg\"})\n               ])\n\n      assert [\n               %UploadEntry{client_name: \"photo\", client_type: \"image/png\"},\n               %UploadEntry{client_name: \"photo\", client_type: \"image/jpeg\"}\n             ] = config.entries\n\n      hero_config = socket.assigns.uploads.hero\n      entry = build_client_entry(:avatar, %{\"name\" => \"photo\", \"type\" => \"image/gif\"})\n      assert {:error, hero_config} = UploadConfig.put_entries(hero_config, [entry])\n      assert hero_config.errors == [{entry[\"ref\"], :not_accepted}]\n\n      hero_config = drop_entry(hero_config, entry[\"ref\"])\n\n      entry =\n        build_client_entry(:avatar, %{\"name\" => \"photo.jpg\", \"type\" => \"application/x-httpd-php\"})\n\n      assert {:error, hero_config} = UploadConfig.put_entries(hero_config, [entry])\n      assert hero_config.errors == [{entry[\"ref\"], :not_accepted}]\n    end\n\n    test \"puts list of entries accepted by extension OR type\" do\n      socket =\n        LiveView.allow_upload(build_socket(), :avatar,\n          accept: ~w(image/* .pdf audio/mpeg),\n          max_entries: 8\n        )\n\n      assert {:ok, config} =\n               UploadConfig.put_entries(socket.assigns.uploads.avatar, [\n                 build_client_entry(:avatar, %{\"name\" => \"photo\", \"type\" => \"image/jpeg\"}),\n                 build_client_entry(:avatar, %{\"name\" => \"photo\", \"type\" => \"image/png\"}),\n                 build_client_entry(:avatar, %{\"name\" => \"photo\", \"type\" => \"image/gif\"}),\n                 build_client_entry(:avatar, %{\"name\" => \"photo\", \"type\" => \"image/webp\"}),\n                 build_client_entry(:avatar, %{\"name\" => \"photo.pdf\"}),\n                 build_client_entry(:avatar, %{\"name\" => \"photo.pdf\", \"type\" => \"application/pdf\"}),\n                 build_client_entry(:avatar, %{\"name\" => \"photo.mp4\", \"type\" => \"audio/mpeg\"})\n               ])\n\n      assert length(config.entries) == 7\n    end\n  end\n\n  test \"supports binary upload name\" do\n    assert LiveView.allow_upload(build_socket(), \"avatar\", accept: ~w(image/png .jpeg))\n  end\n\n  defp build_client_entry(name, attrs \\\\ %{}) do\n    name = \"#{name}_#{System.unique_integer([:positive, :monotonic])}\"\n\n    attrs\n    |> Enum.into(%{\n      \"name\" => name,\n      \"relative_path\" => \"./#{name}\",\n      \"last_modified\" => DateTime.utc_now() |> DateTime.to_unix(),\n      \"size\" => 1024,\n      \"type\" => \"application/octet-stream\"\n    })\n    |> Map.put_new_lazy(\"ref\", &Phoenix.LiveView.Utils.random_id/0)\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/upload/external_test.exs",
    "content": "defmodule Phoenix.LiveView.UploadExternalTest do\n  use ExUnit.Case, async: true\n\n  @endpoint Phoenix.LiveViewTest.Support.Endpoint\n\n  import Phoenix.LiveViewTest\n\n  alias Phoenix.LiveView\n  alias Phoenix.LiveViewTest.Support.UploadLive\n\n  def inspect_html_safe(term) do\n    term\n    |> inspect()\n    |> Phoenix.HTML.html_escape()\n    |> Phoenix.HTML.safe_to_string()\n  end\n\n  def run(lv, func) do\n    GenServer.call(lv.pid, {:run, func})\n  end\n\n  def mount_lv(setup) when is_function(setup, 1) do\n    conn = Plug.Test.init_test_session(Phoenix.ConnTest.build_conn(), %{})\n    {:ok, lv, _} = live_isolated(conn, UploadLive, session: %{})\n    :ok = GenServer.call(lv.pid, {:setup, setup})\n    {:ok, lv}\n  end\n\n  setup %{allow: opts} do\n    external = Keyword.fetch!(opts, :external)\n\n    opts =\n      Keyword.put(opts, :external, fn entry, socket ->\n        apply(__MODULE__, external, [entry, socket])\n      end)\n\n    opts =\n      case Keyword.fetch(opts, :progress) do\n        {:ok, progress} ->\n          Keyword.put(opts, :progress, fn _, entry, socket ->\n            apply(__MODULE__, progress, [entry, socket])\n          end)\n\n        :error ->\n          opts\n      end\n\n    {:ok, lv} = mount_lv(fn socket -> Phoenix.LiveView.allow_upload(socket, :avatar, opts) end)\n\n    {:ok, lv: lv}\n  end\n\n  def preflight(%LiveView.UploadEntry{} = entry, socket) do\n    new_socket =\n      Phoenix.Component.update(socket, :preflights, fn preflights ->\n        [entry.client_name | preflights]\n      end)\n\n    {:ok, %{uploader: \"S3\"}, new_socket}\n  end\n\n  def consume(%LiveView.UploadEntry{} = entry, socket) do\n    if entry.done? do\n      Phoenix.LiveView.consume_uploaded_entry(socket, entry, fn _ -> {:ok, :ok} end)\n    end\n\n    {:noreply, socket}\n  end\n\n  @tag allow: [max_entries: 2, chunk_size: 20, accept: :any, external: :preflight]\n  test \"external with relative path from file_input/4 helper\", %{lv: lv} do\n    avatar =\n      file_input(lv, \"form\", :avatar, [\n        %{\n          name: \"foo1.jpeg\",\n          content: String.duplicate(\"ok\", 100),\n          relative_path: \"some/path/to/foo1.jpeg\"\n        }\n      ])\n\n    assert render_upload(avatar, \"foo1.jpeg\", 1) =~ \"relative path:some/path/to/foo1.jpeg\"\n  end\n\n  @tag allow: [max_entries: 2, chunk_size: 20, accept: :any, external: :preflight]\n  test \"external upload invokes preflight per entry\", %{lv: lv} do\n    avatar =\n      file_input(lv, \"form\", :avatar, [\n        %{name: \"foo1.jpeg\", content: String.duplicate(\"ok\", 100)},\n        %{name: \"foo2.jpeg\", content: String.duplicate(\"ok\", 100)}\n      ])\n\n    assert lv\n           |> form(\"form\", user: %{})\n           |> render_change(avatar) =~ \"foo1.jpeg:0%\"\n\n    assert render_upload(avatar, \"foo1.jpeg\", 1) =~ \"foo1.jpeg:1%\"\n    assert render(lv) =~ \"preflight:#{UploadLive.inspect_html_safe(\"foo1.jpeg\")}\"\n    assert render(lv) =~ \"preflight:#{UploadLive.inspect_html_safe(\"foo2.jpeg\")}\"\n  end\n\n  @tag allow: [max_entries: 1, chunk_size: 20, accept: :any, external: :preflight]\n  test \"external with too many entries\", %{lv: lv} do\n    avatar =\n      file_input(lv, \"form\", :avatar, [\n        %{name: \"foo1.jpeg\", content: String.duplicate(\"ok\", 100)},\n        %{name: \"foo2.jpeg\", content: String.duplicate(\"ok\", 100)}\n      ])\n\n    assert lv\n           |> form(\"form\", user: %{})\n           |> render_change(avatar) =~ \"foo1.jpeg:0%\"\n\n    assert {:error, [[_ref, :too_many_files]]} = render_upload(avatar, \"foo1.jpeg\", 1)\n  end\n\n  @tag allow: [\n         max_entries: 1,\n         chunk_size: 20,\n         auto_upload: true,\n         accept: :any,\n         external: :preflight\n       ]\n  test \"external auto upload with too many entries\", %{lv: lv} do\n    avatar =\n      file_input(lv, \"form\", :avatar, [\n        %{name: \"foo1.jpeg\", content: String.duplicate(\"ok\", 100)},\n        %{name: \"foo2.jpeg\", content: String.duplicate(\"ok\", 100)}\n      ])\n\n    html =\n      lv\n      |> form(\"form\", user: %{})\n      |> render_change(avatar)\n\n    assert html =~ \"foo1.jpeg:0%\"\n    assert html =~ \"foo2.jpeg:0%\"\n\n    assert render_upload(avatar, \"foo1.jpeg\", 1) =~ \"foo1.jpeg:1%\"\n    assert {:error, :not_allowed} = render_upload(avatar, \"foo2.jpeg\", 1)\n  end\n\n  @tag allow: [\n         max_entries: 1,\n         max_file_size: 1,\n         auto_upload: true,\n         accept: :any,\n         external: :preflight\n       ]\n  test \"external auto upload with exceeded max file size\", %{lv: lv} do\n    avatar =\n      file_input(lv, \"form\", :avatar, [\n        %{name: \"foo1.jpeg\", content: String.duplicate(\"ok\", 100)},\n        %{name: \"foo2.jpeg\", content: String.duplicate(\"ok\", 100)}\n      ])\n\n    html =\n      lv\n      |> form(\"form\", user: %{})\n      |> render_change(avatar)\n\n    assert html =~ \"foo1.jpeg:0%\"\n    assert html =~ \"foo2.jpeg:0%\"\n\n    assert {:error, [[_, %{reason: :too_large}]]} = render_upload(avatar, \"foo1.jpeg\", 1)\n    assert {:error, :not_allowed} = render_upload(avatar, \"foo2.jpeg\", 1)\n  end\n\n  def bad_preflight(%LiveView.UploadEntry{} = _entry, socket), do: {:ok, %{}, socket}\n\n  @tag allow: [max_entries: 1, chunk_size: 20, accept: :any, external: :bad_preflight]\n  test \"external preflight raises when missing required :uploader key\", %{lv: lv} do\n    avatar =\n      file_input(lv, \"form\", :avatar, [%{name: \"foo.jpeg\", content: String.duplicate(\"ok\", 100)}])\n\n    assert UploadLive.exits_with(lv, avatar, ArgumentError, fn ->\n             render_upload(avatar, \"foo.jpeg\", 1) =~ \"foo.jpeg:1%\"\n           end) =~ \"external uploader metadata requires an :uploader key.\"\n  end\n\n  def error_preflight(%LiveView.UploadEntry{} = entry, socket) do\n    if entry.client_name == \"bad.jpeg\" do\n      {:error, %{reason: \"bad name\"}, socket}\n    else\n      {:ok, %{uploader: \"S3\"}, socket}\n    end\n  end\n\n  @tag allow: [max_entries: 2, chunk_size: 20, accept: :any, external: :error_preflight]\n  test \"preflight with error return\", %{lv: lv} do\n    avatar =\n      file_input(lv, \"form\", :avatar, [\n        %{name: \"foo.jpeg\", content: String.duplicate(\"ok\", 100)},\n        %{name: \"bad.jpeg\", content: String.duplicate(\"ok\", 100)}\n      ])\n\n    assert {:error, [[ref, %{reason: \"bad name\"}]]} = render_upload(avatar, \"bad.jpeg\", 1)\n    assert {:error, [[^ref, %{reason: \"bad name\"}]]} = render_upload(avatar, \"foo.jpeg\", 1)\n    assert render(lv) =~ \"bad name\"\n  end\n\n  @tag allow: [\n         max_entries: 2,\n         chunk_size: 20,\n         auto_upload: true,\n         accept: :any,\n         external: :error_preflight\n       ]\n  test \"preflight with auto_upload with error return\", %{lv: lv} do\n    avatar =\n      file_input(lv, \"form\", :avatar, [\n        %{name: \"foo.jpeg\", content: String.duplicate(\"ok\", 100)},\n        %{name: \"bad.jpeg\", content: String.duplicate(\"ok\", 100)}\n      ])\n\n    assert {:error, [[_, %{reason: \"bad name\"}]]} = render_upload(avatar, \"bad.jpeg\", 1)\n    html = render_upload(avatar, \"foo.jpeg\", 1)\n    assert html =~ \"foo.jpeg:1%\"\n    assert html =~ \"bad.jpeg:0%\"\n  end\n\n  @tag allow: [max_entries: 2, chunk_size: 20, accept: :any, external: :preflight]\n  test \"consume_uploaded_entries\", %{lv: lv} do\n    upload_complete = \"foo.jpeg:100%\"\n    parent = self()\n\n    avatar =\n      file_input(lv, \"form\", :avatar, [\n        %{\n          name: \"foo.jpeg\",\n          content: String.duplicate(\"ok\", 100),\n          last_modified: 1_594_171_879_000\n        }\n      ])\n\n    assert render_upload(avatar, \"foo.jpeg\", 100) =~ upload_complete\n\n    run(lv, fn socket ->\n      Phoenix.LiveView.consume_uploaded_entries(socket, :avatar, fn meta, entry ->\n        {:ok, send(parent, {:consume, meta, entry.client_name, entry.client_last_modified})}\n      end)\n\n      {:reply, :ok, socket}\n    end)\n\n    assert_receive {:consume, %{uploader: \"S3\"}, \"foo.jpeg\", 1_594_171_879_000}\n    refute render(lv) =~ upload_complete\n  end\n\n  @tag allow: [max_entries: 2, chunk_size: 20, accept: :any, external: :preflight]\n  test \"consume_uploaded_entry\", %{lv: lv} do\n    upload_complete = \"foo.jpeg:100%\"\n    parent = self()\n\n    avatar =\n      file_input(lv, \"form\", :avatar, [%{name: \"foo.jpeg\", content: String.duplicate(\"ok\", 100)}])\n\n    assert render_upload(avatar, \"foo.jpeg\", 100) =~ upload_complete\n\n    run(lv, fn socket ->\n      {[entry], []} = Phoenix.LiveView.uploaded_entries(socket, :avatar)\n\n      Phoenix.LiveView.consume_uploaded_entry(socket, entry, fn meta ->\n        {:ok, send(parent, {:individual_consume, meta, entry.client_name})}\n      end)\n\n      {:reply, :ok, socket}\n    end)\n\n    assert_receive {:individual_consume, %{uploader: \"S3\"}, \"foo.jpeg\"}\n    refute render(lv) =~ upload_complete\n  end\n\n  @tag allow: [\n         max_entries: 5,\n         chunk_size: 20,\n         accept: :any,\n         external: :preflight,\n         progress: :consume\n       ]\n  test \"consume_uploaded_entry/3 maintains entries state after drop\", %{lv: lv} do\n    parent = self()\n\n    # Note we are building a unique `%Upload{}` for each file.\n    # This is to avoid the upload progress calls serializing in a\n    # single UploadClient.\n    files_inputs =\n      for i <- 1..5,\n          file = %{name: \"#{i}.png\", content: String.duplicate(\"ok\", 100)},\n          input = file_input(lv, \"form\", :avatar, [file]) do\n        render_upload(input, file.name, 99)\n        {file, input}\n      end\n\n    tasks =\n      for {file, input} <- files_inputs do\n        Task.async(fn -> render_upload(input, file.name, 1) end)\n      end\n\n    [_ | _] = Task.yield_many(tasks, 5000)\n\n    run(lv, fn socket ->\n      entries = Phoenix.LiveView.uploaded_entries(socket, :avatar)\n      send(parent, {:consistent_consume, :avatar, entries})\n      {:reply, :ok, socket}\n    end)\n\n    assert_receive {:consistent_consume, :avatar, entries}\n    assert entries == {[], []}\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view/utils_test.exs",
    "content": "defmodule Phoenix.LiveView.UtilsTest do\n  use ExUnit.Case, async: true\n\n  alias Phoenix.LiveView.Utils\n  alias Phoenix.LiveViewTest.Support.Endpoint\n\n  test \"sign\" do\n    assert is_binary(Utils.sign_flash(Endpoint, %{\"info\" => \"hi\"}))\n  end\n\n  test \"verify with valid flash token\" do\n    token = Utils.sign_flash(Endpoint, %{\"info\" => \"hi\"})\n    assert Utils.verify_flash(Endpoint, token) == %{\"info\" => \"hi\"}\n  end\n\n  test \"verify with invalid flash token\" do\n    assert Utils.verify_flash(Endpoint, \"bad\") == %{}\n    assert Utils.verify_flash(Endpoint, nil) == %{}\n  end\nend\n"
  },
  {
    "path": "test/phoenix_live_view_test.exs",
    "content": "defmodule Phoenix.LiveViewUnitTest do\n  use ExUnit.Case, async: true\n\n  import Phoenix.LiveView\n\n  alias Phoenix.LiveView.{Utils, Socket}\n  alias Phoenix.LiveViewTest.Support.Endpoint\n\n  @socket Utils.configure_socket(\n            %Socket{\n              endpoint: Endpoint,\n              router: Phoenix.LiveViewTest.Support.Router,\n              view: Phoenix.LiveViewTest.Support.ParamCounterLive\n            },\n            %{\n              connect_params: %{},\n              connect_info: %{},\n              root_view: Phoenix.LiveViewTest.Support.ParamCounterLive,\n              live_temp: %{}\n            },\n            nil,\n            %{},\n            URI.parse(\"https://www.example.com\")\n          )\n\n  describe \"stream_configure/3\" do\n    test \"raises when already streamed\" do\n      configured_socket = stream_configure(@socket, :songs, [])\n\n      streamed_socket =\n        Phoenix.Component.update(configured_socket, :streams, fn streams ->\n          Map.put(streams, :songs, %Phoenix.LiveView.LiveStream{})\n        end)\n\n      assert_raise ArgumentError,\n                   \"cannot configure stream :songs after it has been streamed\",\n                   fn -> stream_configure(streamed_socket, :songs, []) end\n    end\n\n    test \"raises when already configured\" do\n      configured_socket = stream_configure(@socket, :songs, [])\n\n      assert_raise ArgumentError,\n                   \"cannot re-configure stream :songs after it has been configured\",\n                   fn -> stream_configure(configured_socket, :songs, []) end\n    end\n\n    test \"configures a bespoke dom_id\" do\n      dom_id_fun = fn item -> \"tunes-#{item.id}\" end\n      socket = stream_configure(@socket, :songs, dom_id: dom_id_fun)\n\n      assert get_in(socket.assigns.streams, [:__configured__, :songs, :dom_id]) == dom_id_fun\n    end\n  end\n\n  describe \"flash\" do\n    test \"get and put\" do\n      assert put_flash(@socket, :hello, \"world\").assigns.flash == %{\"hello\" => \"world\"}\n      assert put_flash(@socket, :hello, :world).assigns.flash == %{\"hello\" => :world}\n    end\n\n    test \"clear\" do\n      socket = put_flash(@socket, :hello, \"world\")\n      assert clear_flash(socket).assigns.flash == %{}\n      assert clear_flash(socket, :hello).assigns.flash == %{}\n      assert clear_flash(socket, \"hello\").assigns.flash == %{}\n      assert clear_flash(socket, \"other\").assigns.flash == %{\"hello\" => \"world\"}\n    end\n  end\n\n  describe \"get_connect_params\" do\n    test \"raises when not in mounting state and connected\" do\n      socket = Utils.post_mount_prune(%{@socket | transport_pid: self()})\n\n      assert_raise RuntimeError, ~r/attempted to read connect_params/, fn ->\n        get_connect_params(socket)\n      end\n    end\n\n    test \"raises when not in mounting state and disconnected\" do\n      socket = Utils.post_mount_prune(%{@socket | transport_pid: nil})\n\n      assert_raise RuntimeError, ~r/attempted to read connect_params/, fn ->\n        get_connect_params(socket)\n      end\n    end\n\n    test \"returns nil when disconnected\" do\n      socket = %{@socket | transport_pid: nil}\n      assert get_connect_params(socket) == nil\n    end\n\n    test \"returns params connected and mounting\" do\n      socket = %{@socket | transport_pid: self()}\n      assert get_connect_params(socket) == %{}\n    end\n  end\n\n  describe \"get_connect_info\" do\n    test \"raises when not in mounting state and connected\" do\n      socket = Utils.post_mount_prune(%{@socket | transport_pid: self()})\n\n      assert_raise RuntimeError, ~r/attempted to read connect_info/, fn ->\n        get_connect_info(socket, :uri)\n      end\n    end\n\n    test \"raises when not in mounting state and disconnected\" do\n      socket = Utils.post_mount_prune(%{@socket | transport_pid: nil})\n\n      assert_raise RuntimeError, ~r/attempted to read connect_info/, fn ->\n        get_connect_info(socket, :uri)\n      end\n    end\n\n    test \"returns params when connected\" do\n      socket = %{@socket | transport_pid: self(), private: %{connect_info: %{user_agent: \"foo\"}}}\n      assert get_connect_info(socket, :user_agent) == \"foo\"\n    end\n\n    test \"returns params when disconnected\" do\n      conn =\n        Plug.Test.conn(:get, \"/\")\n        |> Plug.Conn.put_req_header(\"user-agent\", \"custom-client\")\n        |> Plug.Conn.put_req_header(\"x-foo\", \"bar\")\n        |> Plug.Conn.put_req_header(\"x-bar\", \"baz\")\n        |> Plug.Conn.put_req_header(\"tracestate\", \"one\")\n        |> Plug.Conn.put_req_header(\"traceparent\", \"two\")\n\n      socket = %{@socket | private: %{connect_info: conn}}\n\n      assert get_connect_info(socket, :user_agent) ==\n               \"custom-client\"\n\n      assert get_connect_info(socket, :x_headers) ==\n               [{\"x-foo\", \"bar\"}, {\"x-bar\", \"baz\"}]\n\n      assert get_connect_info(socket, :trace_context_headers) ==\n               [{\"tracestate\", \"one\"}, {\"traceparent\", \"two\"}]\n\n      assert get_connect_info(socket, :peer_data) ==\n               %{address: {127, 0, 0, 1}, port: 111_317, ssl_cert: nil}\n\n      assert get_connect_info(socket, :uri) ==\n               %URI{host: \"www.example.com\", path: \"/\", port: 80, query: \"\", scheme: \"http\"}\n    end\n  end\n\n  describe \"static_changed?\" do\n    test \"raises when not in mounting state and connected\" do\n      socket = Utils.post_mount_prune(%{@socket | transport_pid: self()})\n\n      assert_raise RuntimeError, ~r/attempted to read static_changed?/, fn ->\n        static_changed?(socket)\n      end\n    end\n\n    test \"raises when not in mounting state and disconnected\" do\n      socket = Utils.post_mount_prune(%{@socket | transport_pid: nil})\n\n      assert_raise RuntimeError, ~r/attempted to read static_changed?/, fn ->\n        static_changed?(socket)\n      end\n    end\n\n    test \"returns false when disconnected\" do\n      socket = %{@socket | transport_pid: nil}\n      assert static_changed?(socket) == false\n    end\n\n    test \"returns true when connected and static do not match\" do\n      refute static_changed?([], %{})\n      refute static_changed?([\"foo/bar.css\"], nil)\n\n      assert static_changed?([\"foo/bar.css\"], %{})\n      refute static_changed?([\"foo/bar.css\"], %{\"foo/bar.css\" => \"foo/bar-123456.css\"})\n\n      refute static_changed?(\n               [\"domain.com/foo/bar.css\"],\n               %{\"foo/bar.css\" => \"foo/bar-123456.css\"}\n             )\n\n      refute static_changed?(\n               [\"//domain.com/foo/bar.css\"],\n               %{\"foo/bar.css\" => \"foo/bar-123456.css\"}\n             )\n\n      refute static_changed?(\n               [\"//domain.com/foo/bar.css?vsn=d\"],\n               %{\"foo/bar.css\" => \"foo/bar-123456.css\"}\n             )\n\n      refute static_changed?(\n               [\"//domain.com/foo/bar-123456.css\"],\n               %{\"foo/bar.css\" => \"foo/bar-123456.css\"}\n             )\n\n      refute static_changed?(\n               [\"//domain.com/foo/bar-123456.css?vsn=d\"],\n               %{\"foo/bar.css\" => \"foo/bar-123456.css\"}\n             )\n\n      assert static_changed?(\n               [\"//domain.com/foo/bar-654321.css\"],\n               %{\"foo/bar.css\" => \"foo/bar-123456.css\"}\n             )\n\n      assert static_changed?(\n               [\"foo/bar.css\", \"baz/bat.js\"],\n               %{\"foo/bar.css\" => \"foo/bar-123456.css\"}\n             )\n\n      assert static_changed?(\n               [\"foo/bar.css\", \"baz/bat.js\"],\n               %{\"foo/bar.css\" => \"foo/bar-123456.css\", \"p/baz/bat.js\" => \"p/baz/bat-123456.js\"}\n             )\n\n      refute static_changed?(\n               [\"foo/bar.css\", \"baz/bat.js\"],\n               %{\"foo/bar.css\" => \"foo/bar-123456.css\", \"baz/bat.js\" => \"baz/bat-123456.js\"}\n             )\n    end\n\n    defp static_changed?(client, latest) do\n      socket = %{@socket | transport_pid: self()}\n      Process.put(:cache_static_manifest_latest, latest)\n      socket = put_in(socket.private.connect_params[\"_track_static\"], client)\n      static_changed?(socket)\n    end\n  end\n\n  describe \"redirect/2\" do\n    test \"requires local path on to\" do\n      assert_raise ArgumentError, ~r\"the :to option in redirect/2 expects a path\", fn ->\n        redirect(@socket, to: \"http://foo.com\")\n      end\n\n      assert_raise ArgumentError, ~r\"the :to option in redirect/2 expects a path\", fn ->\n        redirect(@socket, to: \"//foo.com\")\n      end\n\n      assert redirect(@socket, to: \"/foo\").redirected == {:redirect, %{to: \"/foo\", status: 302}}\n    end\n\n    test \"accepts a custom redirect status for local / external paths\" do\n      assert redirect(@socket, to: \"/foo\", status: 301).redirected ==\n               {:redirect, %{to: \"/foo\", status: 301}}\n\n      assert redirect(@socket, external: \"http://foo.com/bar\", status: 301).redirected ==\n               {:redirect, %{external: \"http://foo.com/bar\", status: 301}}\n    end\n\n    test \"allows external paths\" do\n      assert redirect(@socket, external: \"http://foo.com/bar\").redirected ==\n               {:redirect, %{external: \"http://foo.com/bar\", status: 302}}\n\n      assert redirect(@socket, external: {:javascript, \"alert\"}).redirected ==\n               {:redirect, %{external: \"javascript:alert\", status: 302}}\n    end\n\n    test \"disallows insecure external paths\" do\n      assert_raise ArgumentError, ~r/unsupported scheme given to redirect\\/2/, fn ->\n        redirect(@socket, external: \"javascript:alert('xss');\")\n      end\n    end\n  end\n\n  describe \"push_navigate/2\" do\n    test \"requires local path on to\" do\n      assert_raise ArgumentError, ~r\"the :to option in push_navigate/2 expects a path\", fn ->\n        push_navigate(@socket, to: \"http://foo.com\")\n      end\n\n      assert_raise ArgumentError, ~r\"the :to option in push_navigate/2 expects a path\", fn ->\n        push_navigate(@socket, to: \"//foo.com\")\n      end\n\n      assert push_navigate(@socket, to: \"/counter/123\").redirected ==\n               {:live, :redirect, %{kind: :push, to: \"/counter/123\"}}\n    end\n  end\n\n  describe \"push_patch/2\" do\n    test \"requires local path on to pointing to the same LiveView\" do\n      assert_raise ArgumentError, ~r\"the :to option in push_patch/2 expects a path\", fn ->\n        push_patch(@socket, to: \"http://foo.com\")\n      end\n\n      assert_raise ArgumentError, ~r\"the :to option in push_patch/2 expects a path\", fn ->\n        push_patch(@socket, to: \"//foo.com\")\n      end\n\n      socket = %{@socket | view: Phoenix.LiveViewTest.Support.ParamCounterLive}\n\n      assert push_patch(socket, to: \"/counter/123\").redirected ==\n               {:live, :patch, %{kind: :push, to: \"/counter/123\"}}\n    end\n  end\n\n  describe \"put_private\" do\n    test \"assigns private keys\" do\n      assert @socket.private[:hello] == nil\n      assert put_private(@socket, :hello, \"world\").private[:hello] == \"world\"\n    end\n\n    test \"disallows reserved keys\" do\n      assert_raise ArgumentError, ~r/reserved/, fn ->\n        put_private(@socket, :assign_new, \"boom\")\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "test/support/async_sync.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.AsyncSync do\n  def wait_for_async_ready_and_monitor(name) do\n    receive do\n      :async_ready -> :ok\n    end\n\n    async_ref = Process.monitor(name)\n    send(name, :monitoring)\n\n    receive do\n      :monitoring_received -> :ok\n    end\n\n    async_ref\n  end\n\n  def register_and_sleep(notify_name, register_name) do\n    Process.register(self(), register_name)\n    send(notify_name, :async_ready)\n\n    receive do\n      :monitoring ->\n        send(notify_name, :monitoring_received)\n        Process.sleep(:infinity)\n    end\n  end\nend\n"
  },
  {
    "path": "test/support/controller.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.Controller do\n  use Phoenix.Controller, formats: [:html]\n  import Phoenix.LiveView.Controller\n\n  plug :put_layout, false\n\n  def widget(conn, _) do\n    conn\n    |> put_view(Phoenix.LiveViewTest.Support.LayoutView)\n    |> render(\"widget.html\")\n  end\n\n  def incoming(conn, %{\"type\" => \"live-render-2\"}) do\n    live_render(conn, Phoenix.LiveViewTest.Support.DashboardLive)\n  end\n\n  def incoming(conn, %{\"type\" => \"live-render-3\"}) do\n    live_render(conn, Phoenix.LiveViewTest.Support.DashboardLive,\n      session: %{\"custom\" => :session}\n    )\n  end\n\n  def incoming(conn, %{\"type\" => \"live-render-4\"}) do\n    conn\n    |> put_layout({Phoenix.LiveViewTest.Support.AssignsLayoutView, :app})\n    |> live_render(Phoenix.LiveViewTest.Support.DashboardLive)\n  end\n\n  def incoming(conn, %{\"type\" => \"render-with-function-component\"}) do\n    conn\n    |> put_view(Phoenix.LiveViewTest.Support.LayoutView)\n    |> render(\"with-function-component.html\")\n  end\n\n  def incoming(conn, %{\"type\" => \"render-layout-with-function-component\"}) do\n    conn\n    |> put_view(Phoenix.LiveViewTest.Support.LayoutView)\n    |> put_root_layout(\n      {Phoenix.LiveViewTest.Support.LayoutView, \"layout-with-function-component.html\"}\n    )\n    |> render(\"hello.html\")\n  end\n\n  def not_found(conn, _) do\n    conn\n    |> put_status(:not_found)\n    |> text(\"404\")\n  end\nend\n"
  },
  {
    "path": "test/support/endpoint.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.EndpointOverridable do\n  defmacro __before_compile__(_env) do\n    quote do\n      @parsers Plug.Parsers.init(\n                 parsers: [:urlencoded, :multipart, :json],\n                 pass: [\"*/*\"],\n                 json_decoder: Phoenix.json_library()\n               )\n\n      defoverridable call: 2\n\n      def call(conn, opts) do\n        %{conn | secret_key_base: config(:secret_key_base)}\n        |> Plug.Parsers.call(@parsers)\n        |> Plug.Conn.put_private(:phoenix_endpoint, __MODULE__)\n        |> super(opts)\n      end\n    end\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.Endpoint do\n  use Phoenix.Endpoint, otp_app: :phoenix_live_view\n\n  @before_compile Phoenix.LiveViewTest.Support.EndpointOverridable\n\n  socket \"/live\", Phoenix.LiveView.Socket\n\n  defoverridable url: 0, script_name: 0, config: 1, config: 2, static_path: 1\n  def url(), do: \"http://localhost:4004\"\n  def script_name(), do: []\n  def static_path(path), do: \"/static\" <> path\n  def config(:live_view), do: [signing_salt: \"112345678212345678312345678412\"]\n  def config(:secret_key_base), do: String.duplicate(\"57689\", 50)\n  def config(:cache_static_manifest_latest), do: Process.get(:cache_static_manifest_latest)\n  def config(:otp_app), do: :phoenix_live_view\n  def config(:pubsub_server), do: Phoenix.LiveView.PubSub\n  def config(:render_errors), do: [formats: [html: __MODULE__]]\n  def config(:static_url), do: [path: \"/static\"]\n  def config(which), do: super(which)\n  def config(which, default), do: super(which, default)\n\n  plug Phoenix.LiveViewTest.Support.Router\n\n  def render(template, _assigns) do\n    Phoenix.Controller.status_message_from_template(template)\n  end\nend\n"
  },
  {
    "path": "test/support/layout_view.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.LayoutView do\n  use Phoenix.View, root: \"\"\n  use Phoenix.Component\n\n  use Phoenix.VerifiedRoutes,\n    router: Phoenix.LiveViewTest.Support.Router,\n    endpoint: Phoenix.LiveViewTest.Support.Endpoint,\n    statics: ~w(css)\n\n  def render(\"app.html\", assigns) do\n    # Assert those assigns are always available\n    _ = assigns.live_module\n    _ = assigns.live_action\n\n    [\"LAYOUT\", assigns.inner_content]\n  end\n\n  def render(\"live.html\", assigns) do\n    ~H\"\"\"\n    LIVELAYOUTSTART-{@val}-{@inner_content}-LIVELAYOUTEND\n    \"\"\"\n  end\n\n  def render(\"live_override.html\", assigns) do\n    ~H\"\"\"\n    LIVEOVERRIDESTART-{@val}-{@inner_content}-LIVEOVERRIDEEND\n    \"\"\"\n  end\n\n  def render(\"widget.html\", assigns) do\n    ~H\"\"\"\n    WIDGET:{live_render(@conn, Phoenix.LiveViewTest.Support.ClockLive)}\n    \"\"\"\n  end\n\n  def render(\"with-function-component.html\", assigns) do\n    ~H\"\"\"\n    RENDER:<Phoenix.LiveViewTest.Support.FunctionComponent.render value=\"from component\" />\n    \"\"\"\n  end\n\n  def render(\"layout-with-function-component.html\", assigns) do\n    ~H\"\"\"\n    LAYOUT:<Phoenix.LiveViewTest.Support.FunctionComponent.render value=\"from layout\" />\n    {@inner_content}\n    \"\"\"\n  end\n\n  def render(\"hello.html\", assigns) do\n    ~H\"\"\"\n    Hello\n    \"\"\"\n  end\n\n  def render(\"styled.html\", assigns) do\n    ~H\"\"\"\n    <html>\n      <head>\n        <title>Styled</title>\n        <link rel=\"stylesheet\" href=\"/css/custom.css\" />\n        <link rel=\"stylesheet\" href={~p\"/css/app.css\"} />\n        <link rel=\"stylesheet\" href=\"//example.com/a.css\" />\n        <link rel=\"stylesheet\" href=\"https://example.com/b.css\" />\n        <style>\n          body { background-color: #eee; }\n        </style>\n        <script>\n          console.log(\"script\");\n        </script>\n      </head>\n      <body>\n        {@inner_content}\n      </body>\n    </html>\n    \"\"\"\n  end\n\n  def on_mount_layout(assigns) do\n    ~H\"\"\"\n    <div id=\"on-mount\">\n      {@inner_content}\n    </div>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.AssignsLayoutView do\n  use Phoenix.View, root: \"\"\n\n  def render(\"app.html\", assigns) do\n    [\"title: #{assigns.title}\", assigns.inner_content]\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/assign_async.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.AssignAsyncLive do\n  use Phoenix.LiveView\n\n  import Phoenix.LiveViewTest.Support.AsyncSync\n\n  on_mount({__MODULE__, :defaults})\n\n  def on_mount(:defaults, _params, _session, socket) do\n    {:cont, assign(socket, lc: false)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <.live_component\n      :if={@lc}\n      module={Phoenix.LiveViewTest.Support.AssignAsyncLive.LC}\n      test={@lc}\n      id=\"lc\"\n    />\n\n    <div :if={@data.loading}>data loading...</div>\n    <div :if={@data.ok? && @data.result == nil}>no data found</div>\n    <div :if={@data.ok? && @data.result}>data: {inspect(@data.result)}</div>\n    <div :if={@data.failed}>{inspect(@data.failed)}</div>\n    \"\"\"\n  end\n\n  def mount(%{\"test\" => \"lc_\" <> lc_test}, _session, socket) do\n    {:ok,\n     socket\n     |> assign(lc: lc_test)\n     |> assign_async(:data, fn -> {:ok, %{data: :live_component}} end)}\n  end\n\n  def mount(%{\"test\" => \"bad_return\"}, _session, socket) do\n    {:ok, assign_async(socket, :data, fn -> 123 end)}\n  end\n\n  def mount(%{\"test\" => \"bad_ok\"}, _session, socket) do\n    {:ok, assign_async(socket, :data, fn -> {:ok, %{bad: 123}} end)}\n  end\n\n  def mount(%{\"test\" => \"ok\"}, _session, socket) do\n    {:ok, assign_async(socket, :data, fn -> {:ok, %{data: 123}} end)}\n  end\n\n  def mount(%{\"test\" => \"sup_ok\"}, _session, socket) do\n    {:ok,\n     assign_async(socket, :data, fn -> {:ok, data: 123} end, supervisor: TestAsyncSupervisor)}\n  end\n\n  def mount(%{\"test\" => \"raise\"}, _session, socket) do\n    {:ok, assign_async(socket, :data, fn -> raise(\"boom\") end)}\n  end\n\n  def mount(%{\"test\" => \"sup_raise\"}, _session, socket) do\n    {:ok, assign_async(socket, :data, fn -> raise(\"boom\") end, supervisor: TestAsyncSupervisor)}\n  end\n\n  def mount(%{\"test\" => \"exit\"}, _session, socket) do\n    {:ok, assign_async(socket, :data, fn -> exit(:boom) end)}\n  end\n\n  def mount(%{\"test\" => \"sup_exit\"}, _session, socket) do\n    {:ok, assign_async(socket, :data, fn -> exit(:boom) end, supervisor: TestAsyncSupervisor)}\n  end\n\n  def mount(%{\"test\" => \"lv_exit\"}, _session, socket) do\n    {:ok,\n     assign_async(socket, :data, fn ->\n       register_and_sleep(:assign_async_test_process, :lv_exit)\n     end)}\n  end\n\n  def mount(%{\"test\" => \"cancel\"}, _session, socket) do\n    {:ok,\n     assign_async(socket, :data, fn ->\n       register_and_sleep(:assign_async_test_process, :cancel)\n     end)}\n  end\n\n  def mount(%{\"test\" => \"trap_exit\"}, _session, socket) do\n    Process.flag(:trap_exit, true)\n\n    {:ok,\n     assign_async(socket, :data, fn ->\n       spawn_link(fn -> exit(:boom) end)\n       Process.sleep(100)\n       {:ok, %{data: 0}}\n     end)}\n  end\n\n  def mount(%{\"test\" => \"socket_warning\"}, _session, socket) do\n    {:ok, assign_async(socket, :data, function_that_returns_the_anonymous_function(socket))}\n  end\n\n  defp function_that_returns_the_anonymous_function(socket) do\n    fn ->\n      Function.identity(socket)\n      {:ok, %{data: 0}}\n    end\n  end\n\n  def handle_info(:boom, _socket), do: exit(:boom)\n\n  def handle_info(:cancel, socket) do\n    {:noreply, cancel_async(socket, socket.assigns.data)}\n  end\n\n  def handle_info({:EXIT, pid, reason}, socket) do\n    send(:trap_exit_test, {:exit, pid, reason})\n    {:noreply, socket}\n  end\n\n  def handle_info(:renew_canceled, socket) do\n    {:noreply,\n     assign_async(socket, :data, fn ->\n       Process.sleep(100)\n       {:ok, %{data: 123}}\n     end)}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.AssignAsyncLive.LC do\n  use Phoenix.LiveComponent\n\n  import Phoenix.LiveViewTest.Support.AsyncSync\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      <.async_result :let={data} assign={@lc_data}>\n        <:loading>lc_data loading...</:loading>\n        <:failed :let={{kind, reason}}>{kind}: {inspect(reason)}</:failed>\n        lc_data: {inspect(data)}\n      </.async_result>\n      <.async_result :let={data} assign={@other_data}>\n        <:loading>other_data loading...</:loading>\n        other_data: {inspect(data)}\n      </.async_result>\n    </div>\n    \"\"\"\n  end\n\n  def update(%{test: \"bad_return\"}, socket) do\n    {:ok, assign_async(socket, [:lc_data, :other_data], fn -> 123 end)}\n  end\n\n  def update(%{test: \"bad_ok\"}, socket) do\n    {:ok, assign_async(socket, [:lc_data, :other_data], fn -> {:ok, %{bad: 123}} end)}\n  end\n\n  def update(%{test: \"ok\"}, socket) do\n    {:ok,\n     assign_async(socket, [:lc_data, :other_data], fn ->\n       {:ok, %{other_data: 555, lc_data: 123}}\n     end)}\n  end\n\n  def update(%{test: \"raise\"}, socket) do\n    {:ok, assign_async(socket, [:lc_data, :other_data], fn -> raise(\"boom\") end)}\n  end\n\n  def update(%{test: \"exit\"}, socket) do\n    {:ok, assign_async(socket, [:lc_data, :other_data], fn -> exit(:boom) end)}\n  end\n\n  def update(%{test: \"lv_exit\"}, socket) do\n    {:ok,\n     assign_async(socket, [:lc_data, :other_data], fn ->\n       register_and_sleep(:assign_async_test_process, :lc_exit)\n     end)}\n  end\n\n  def update(%{test: \"cancel\"}, socket) do\n    {:ok,\n     assign_async(socket, [:lc_data, :other_data], fn ->\n       register_and_sleep(:assign_async_test_process, :lc_cancel)\n     end)}\n  end\n\n  def update(%{action: :boom}, _socket), do: exit(:boom)\n\n  def update(%{action: :cancel}, socket) do\n    {:ok, cancel_async(socket, socket.assigns.lc_data)}\n  end\n\n  def update(%{action: :assign_async_reset, reset: reset}, socket) do\n    fun = fn ->\n      Process.sleep(50)\n      {:ok, %{other_data: 999, lc_data: 456}}\n    end\n\n    {:ok, assign_async(socket, [:lc_data, :other_data], fun, reset: reset)}\n  end\n\n  def update(%{action: :renew_canceled}, socket) do\n    {:ok,\n     assign_async(socket, :lc_data, fn ->\n       Process.sleep(100)\n       {:ok, %{lc_data: 123}}\n     end)}\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/cids_destroyed.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.CidsDestroyedLive do\n  use Phoenix.LiveView\n\n  defmodule Button do\n    use Phoenix.LiveComponent\n\n    def mount(socket) do\n      {:ok, assign(socket, counter: 0)}\n    end\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div>\n        <button type=\"submit\">{@text}</button>\n        <div id=\"bumper\" phx-click=\"bump\" phx-target={@myself}>Bump: {@counter}</div>\n      </div>\n      \"\"\"\n    end\n\n    def handle_event(\"bump\", _, socket) do\n      {:noreply, update(socket, :counter, &(&1 + 1))}\n    end\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <%= if @form do %>\n      <form phx-submit=\"event_1\">\n        <.live_component module={Button} id=\"button\" text=\"Hello World\" />\n      </form>\n    <% else %>\n      <div class=\"loader\">loading...</div>\n    <% end %>\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, form: true)}\n  end\n\n  def handle_event(\"event_1\", _params, socket) do\n    send(self(), :event_2)\n    {:noreply, assign(socket, form: false)}\n  end\n\n  def handle_info(:event_2, socket) do\n    {:noreply, assign(socket, form: true)}\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/collocated.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.CollocatedLive do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, world: \"world\")}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.CollocatedComponent do\n  use Phoenix.LiveComponent\nend\n"
  },
  {
    "path": "test/support/live_views/collocated_component.html.heex",
    "content": "Hello collocated <%= @world %> from component!\n"
  },
  {
    "path": "test/support/live_views/collocated_live.html.heex",
    "content": "Hello collocated <%= @world %> from live!\n"
  },
  {
    "path": "test/support/live_views/component_and_nested_in_live.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.ComponentAndNestedInLive do\n  use Phoenix.LiveView\n\n  defmodule NestedLive do\n    use Phoenix.LiveView\n\n    def mount(_params, _session, socket) do\n      {:ok, assign(socket, :hello, \"hello\")}\n    end\n\n    def render(assigns) do\n      ~H\"<div>{@hello}</div>\"\n    end\n\n    def handle_event(\"disable\", _params, socket) do\n      send(socket.parent_pid, :disable)\n      {:noreply, socket}\n    end\n  end\n\n  defmodule NestedComponent do\n    use Phoenix.LiveComponent\n\n    def mount(socket) do\n      {:ok, assign(socket, :world, \"world\")}\n    end\n\n    def render(assigns) do\n      ~H\"<div>{@world}</div>\"\n    end\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, :enabled, true)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <%= if @enabled do %>\n      {live_render(@socket, NestedLive, id: :nested_live)}\n      <.live_component module={NestedComponent} id={:_component} />\n    <% end %>\n    \"\"\"\n  end\n\n  def handle_event(\"disable\", _, socket) do\n    {:noreply, assign(socket, :enabled, false)}\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/component_in_live.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.ComponentInLive.Root do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, :enabled, true)}\n  end\n\n  def render(assigns) do\n    ~H\"{@enabled &&\n  live_render(@socket, Phoenix.LiveViewTest.Support.ComponentInLive.Live, id: :nested_live)}\"\n  end\n\n  def handle_info(:disable, socket) do\n    {:noreply, assign(socket, :enabled, false)}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.ComponentInLive.Live do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  def render(assigns) do\n    ~H\"<.live_component\n  module={Phoenix.LiveViewTest.Support.ComponentInLive.Component}\n  id={:nested_component}\n/>\"\n  end\n\n  def handle_event(\"disable\", _params, socket) do\n    send(socket.parent_pid, :disable)\n    {:noreply, socket}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.ComponentInLive.Component do\n  use Phoenix.LiveComponent\n\n  # Make sure mount is calling by setting assigns in them.\n  def mount(socket) do\n    {:ok, assign(socket, world: \"World\")}\n  end\n\n  def update(_assigns, socket) do\n    {:ok, assign(socket, hello: \"Hello\")}\n  end\n\n  def render(assigns) do\n    ~H\"<div>{@hello} {@world}</div>\"\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/components.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.FunctionComponent do\n  use Phoenix.Component\n\n  def render(assigns) do\n    ~H\"\"\"\n    COMPONENT:{@value}\n    \"\"\"\n  end\n\n  def render_with_inner_content(assigns) do\n    ~H\"\"\"\n    COMPONENT:{@value}, Content: {render_slot(@inner_block)}\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.FunctionComponentWithAttrs do\n  use Phoenix.Component\n\n  defmodule Struct do\n    defstruct []\n  end\n\n  def identity(var), do: var\n  def map_identity(%{} = map), do: map\n\n  attr :attr, :any\n  def fun_attr_any(assigns), do: ~H[]\n\n  attr :attr, :string\n  def fun_attr_string(assigns), do: ~H[]\n\n  attr :attr, :atom\n  def fun_attr_atom(assigns), do: ~H[]\n\n  attr :attr, :boolean\n  def fun_attr_boolean(assigns), do: ~H[]\n\n  attr :attr, :integer\n  def fun_attr_integer(assigns), do: ~H[]\n\n  attr :attr, :float\n  def fun_attr_float(assigns), do: ~H[]\n\n  attr :attr, :map\n  def fun_attr_map(assigns), do: ~H[]\n\n  attr :attr, :list\n  def fun_attr_list(assigns), do: ~H[]\n\n  attr :attr, :global\n  def fun_attr_global(assigns), do: ~H[]\n\n  attr :rest, :global, doc: \"These are passed to the inner input field\"\n  def fun_attr_global_doc(assigns), do: ~H[]\n\n  attr :rest, :global, doc: \"These are passed to the inner input field\", include: ~w(value)\n  def fun_attr_global_doc_include(assigns), do: ~H[]\n\n  attr :rest, :global, include: ~w(value)\n  def fun_attr_global_include(assigns), do: ~H[]\n\n  attr :name, :string, doc: \"The form input name\"\n  attr :rest, :global, doc: \"These are passed to the inner input field\"\n  def fun_attr_global_and_regular(assigns), do: ~H[]\n\n  attr :attr, Struct\n  def fun_attr_struct(assigns), do: ~H[]\n\n  attr :attr, :any, required: true\n  def fun_attr_required(assigns), do: ~H[]\n\n  attr :attr, :any, default: %{}\n  def fun_attr_default(assigns), do: ~H[]\n\n  attr :attr1, :any\n  attr :attr2, :any\n  def fun_multiple_attr(assigns), do: ~H[]\n\n  attr :attr, :any, doc: \"attr docs\"\n  def fun_with_attr_doc(assigns), do: ~H[]\n\n  attr :attr, :any, default: \"foo\", doc: \"attr docs.\"\n  def fun_with_attr_doc_period(assigns), do: ~H[]\n\n  attr :attr, :any,\n    default: \"foo\",\n    doc: \"\"\"\n    attr docs with bullets:\n\n      * foo\n      * bar\n\n    and that's it.\n    \"\"\"\n\n  def fun_with_attr_doc_multiline(assigns), do: ~H[]\n\n  attr :attr1, :any\n  attr :attr2, :any, doc: false\n  def fun_with_hidden_attr(assigns), do: ~H[]\n\n  attr :attr, :any\n  @doc \"fun docs\"\n  def fun_with_doc(assigns), do: ~H[]\n\n  attr :attr, :any\n\n  @doc \"\"\"\n  fun docs\n  [INSERT LVATTRDOCS]\n  fun docs\n  \"\"\"\n  def fun_doc_injection(assigns), do: ~H[]\n\n  attr :attr, :any\n  @doc false\n  def fun_doc_false(assigns), do: ~H[]\n\n  attr :attr, :any\n  defp private_fun(assigns), do: ~H[]\n  def exposes_private_fun_to_avoid_warnings(assigns), do: private_fun(assigns)\n\n  slot(:inner_block)\n  def fun_slot(assigns), do: ~H[]\n\n  slot(:inner_block, doc: \"slot docs\")\n  def fun_slot_doc(assigns), do: ~H[]\n\n  slot(:inner_block, required: true)\n  def fun_slot_required(assigns), do: ~H[]\n\n  slot :named, required: true, doc: \"a named slot\" do\n    attr :attr1, :any, required: true, doc: \"a slot attr doc\"\n    attr :attr2, :any, doc: \"a slot attr doc\"\n  end\n\n  def fun_slot_with_attrs(assigns), do: ~H[]\n\n  slot :named, required: true do\n    attr :attr1, :any, required: true, doc: \"a slot attr doc\"\n    attr :attr2, :any, doc: \"a slot attr doc\"\n  end\n\n  def fun_slot_no_doc_with_attrs(assigns), do: ~H[]\n\n  slot :named,\n    required: true,\n    doc: \"\"\"\n    Important slot:\n\n    * for a\n    * for b\n    \"\"\" do\n    attr :attr1, :any, required: true, doc: \"a slot attr doc\"\n    attr :attr2, :any, doc: \"a slot attr doc\"\n  end\n\n  def fun_slot_doc_multiline_with_attrs(assigns), do: ~H[]\n\n  slot :named, required: true do\n    attr :attr1, :any,\n      required: true,\n      doc: \"\"\"\n      attr docs with bullets:\n\n        * foo\n        * bar\n\n      and that's it.\n      \"\"\"\n\n    attr :attr2, :any, doc: \"a slot attr doc\"\n  end\n\n  def fun_slot_doc_with_attrs_multiline(assigns), do: ~H[]\n\n  attr :attr1, :atom, values: [:foo, :bar, :baz]\n  attr :attr2, :atom, examples: [:foo, :bar, :baz]\n  attr :attr3, :list, values: [[60, 40]]\n  attr :attr4, :list, examples: [[60, 40]]\n  attr :attr5, :atom, default: :foo, values: [:foo, :bar, :baz]\n  attr :attr6, :atom, doc: \"Attr 6 doc\", values: [:foo, :bar, :baz]\n  attr :attr7, :atom, doc: \"Attr 7 doc\", default: :foo, values: [:foo, :bar, :baz]\n  def fun_attr_values_examples(assigns), do: ~H[]\nend\n\ndefmodule Phoenix.LiveViewTest.Support.StatefulComponent do\n  use Phoenix.LiveComponent\n\n  def mount(socket) do\n    {:ok, assign(socket, name: \"unknown\", dup_name: nil, parent_id: nil)}\n  end\n\n  def update(assigns, socket) do\n    if from = assigns[:from] do\n      sent_assigns = Map.merge(assigns, %{id: socket.assigns[:id], myself: socket.assigns.myself})\n      send(from, {:updated, sent_assigns})\n    end\n\n    {:ok, assign(socket, assigns)}\n  end\n\n  def render(%{disabled: true} = assigns) do\n    ~H\"\"\"\n    <div>\n      DISABLED\n    </div>\n    \"\"\"\n  end\n\n  def render(%{socket: _} = assigns) do\n    ~H\"\"\"\n    <div phx-click=\"transform\" id={@id} phx-target={\"#\" <> @id <> include_parent_id(@parent_id)}>\n      {@name} says hi\n      <.live_component :if={@dup_name} module={__MODULE__} id={@dup_name} name={@dup_name} />\n    </div>\n    \"\"\"\n  end\n\n  defp include_parent_id(nil), do: \"\"\n  defp include_parent_id(parent_id), do: \",#{parent_id}\"\n\n  def handle_event(\"transform\", %{\"op\" => op}, socket) do\n    case op do\n      \"upcase\" ->\n        {:noreply, update(socket, :name, &String.upcase(&1))}\n\n      \"title-case\" ->\n        {:noreply,\n         update(socket, :name, fn <<first::binary-size(1), rest::binary>> ->\n           String.upcase(first) <> rest\n         end)}\n\n      \"dup\" ->\n        {:noreply, assign(socket, :dup_name, socket.assigns.name <> \"-dup\")}\n\n      \"push_navigate\" ->\n        {:noreply, push_navigate(socket, to: \"/components?redirect=push\")}\n\n      \"push_patch\" ->\n        {:noreply, push_patch(socket, to: \"/components?redirect=patch\")}\n\n      \"redirect\" ->\n        {:noreply, redirect(socket, to: \"/components?redirect=redirect\")}\n    end\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.WithComponentLive do\n  use Phoenix.LiveView\n\n  def render(%{disabled: :all} = assigns) do\n    ~H\"\"\"\n    Disabled\n    \"\"\"\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    Redirect: {@redirect}\n    <%= for name <- @names do %>\n      <.live_component\n        module={Phoenix.LiveViewTest.Support.StatefulComponent}\n        id={name}\n        name={name}\n        from={@from}\n        disabled={name in @disabled}\n        parent_id={nil}\n      />\n    <% end %>\n    \"\"\"\n  end\n\n  def mount(_params, %{\"names\" => names, \"from\" => from}, socket) do\n    {:ok, assign(socket, names: names, from: from, disabled: [])}\n  end\n\n  def handle_params(params, _url, socket) do\n    {:noreply, assign(socket, redirect: params[\"redirect\"] || \"none\")}\n  end\n\n  def handle_info({:send_update, updates}, socket) do\n    Enum.each(updates, fn {module, args} -> send_update(module, args) end)\n    {:noreply, socket}\n  end\n\n  def handle_event(\"delete-name\", %{\"name\" => name}, socket) do\n    {:noreply, update(socket, :names, &List.delete(&1, name))}\n  end\n\n  def handle_event(\"disable-all\", %{}, socket) do\n    {:noreply, assign(socket, :disabled, :all)}\n  end\n\n  def handle_event(\"dup-and-disable\", %{}, socket) do\n    names = socket.assigns.names\n    new_socket = assign(socket, disabled: names, names: names ++ Enum.map(names, &(&1 <> \"-new\")))\n    {:noreply, new_socket}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.WithMultipleTargets do\n  use Phoenix.LiveView\n\n  def mount(_params, %{\"names\" => names, \"from\" => from} = session, socket) do\n    {\n      :ok,\n      assign(socket,\n        names: names,\n        from: from,\n        disabled: [],\n        message: nil,\n        parent_selector: Map.get(session, \"parent_selector\", \"#parent_id\")\n      )\n    }\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div id=\"parent_id\" class=\"parent\">\n      {@message}\n      <%= for name <- @names do %>\n        <.live_component\n          module={Phoenix.LiveViewTest.Support.StatefulComponent}\n          id={name}\n          name={name}\n          from={@from}\n          disabled={name in @disabled}\n          parent_id={@parent_selector}\n        />\n      <% end %>\n    </div>\n    \"\"\"\n  end\n\n  def handle_event(\"transform\", %{\"op\" => _op}, socket) do\n    {:noreply, assign(socket, :message, \"Parent was updated\")}\n  end\n\n  def handle_event(\"disable\", %{\"name\" => name}, socket) do\n    {:noreply, assign(socket, :disabled, Enum.uniq([name | socket.assigns.disabled]))}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.WithLogOverride do\n  use Phoenix.LiveView, log: :warning\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  def render(assigns), do: ~H[]\nend\n\ndefmodule Phoenix.LiveViewTest.Support.WithLogDisabled do\n  use Phoenix.LiveView, log: false\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  def render(assigns), do: ~H[]\nend\n"
  },
  {
    "path": "test/support/live_views/connect.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.ConnectLive do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    <p>params: {inspect(@params)}</p>\n    <p>uri: {URI.to_string(@uri)}</p>\n    <p>trace: {inspect(@trace)}</p>\n    <p>peer: {inspect(@peer, custom_options: [sort_maps: true])}</p>\n    <p>x-headers: {inspect(@x_headers)}</p>\n    <p>user-agent: {inspect(@user_agent)}</p>\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok,\n     assign(\n       socket,\n       params: get_connect_params(socket),\n       uri: get_connect_info(socket, :uri),\n       trace: get_connect_info(socket, :trace_context_headers),\n       peer: get_connect_info(socket, :peer_data),\n       x_headers: get_connect_info(socket, :x_headers),\n       user_agent: get_connect_info(socket, :user_agent)\n     )}\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/debug_anno.exs",
    "content": "# Note this file is intentionally a .exs file because it is loaded\n# in the test helper with debug_heex_annotations turned on.\ndefmodule Phoenix.LiveViewTest.Support.DebugAnno do\n  use Phoenix.Component\n\n  def remote(assigns) do\n    ~H\"REMOTE COMPONENT: Value: {@value}\"\n  end\n\n  def remote_with_tags(assigns) do\n    ~H\"<div>REMOTE COMPONENT: Value: {@value}</div>\"\n  end\n\n  def local(assigns) do\n    ~H\"LOCAL COMPONENT: Value: {@value}\"\n  end\n\n  def local_with_tags(assigns) do\n    ~H\"<div>LOCAL COMPONENT: Value: {@value}</div>\"\n  end\n\n  def nested(assigns) do\n    ~H\"\"\"\n    <div>\n      <.local_with_tags value=\"local\" />\n    </div>\n    \"\"\"\n  end\n\n  def slot(assigns) do\n    ~H\"\"\"\n    <.intersperse :let={num} enum={[1, 2]}>\n      <:separator>,</:separator>\n      {num}\n    </.intersperse>\n    \"\"\"\n  end\n\n  def slot_with_tags(assigns) do\n    ~H\"\"\"\n    <.intersperse :let={num} enum={[1, 2]}>\n      <:separator><hr /></:separator>\n      <div>{num}</div>\n    </.intersperse>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/debug_anno_opt_out.exs",
    "content": "# Note this file is intentionally a .exs file because it is loaded\n# in the test helper with debug_heex_annotations turned on.\ndefmodule Phoenix.LiveViewTest.Support.DebugAnnoOptOut do\n  use Phoenix.Component\n\n  @debug_heex_annotations false\n  @debug_attributes false\n\n  def slot_with_tags(assigns) do\n    ~H\"\"\"\n    <.intersperse :let={num} enum={[1, 2]}>\n      <:separator><hr /></:separator>\n      <div>{num}</div>\n    </.intersperse>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/elements.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.ElementsLive do\n  use Phoenix.LiveView\n\n  alias Phoenix.LiveView.JS\n\n  def render(assigns) do\n    ~H\"\"\"\n    <%!-- lookups --%>\n    <div id=\"last-event\">{@event}</div>\n    <div id=\"scoped-render\"><span>This</span> is a div</div>\n    <div>This</div>\n    <div id=\"child-component\">\n      <.live_component module={Phoenix.LiveViewTest.Support.ElementsComponent} id={1} />\n    </div>\n    <span phx-no-format>\n      Normalize\n      <span> whitespace</span>\n    </span>\n\n    <%!-- basic render_* --%>\n    <span id=\"span-no-attr\">This is a span</span>\n\n    <span id=\"span-blur-no-value\" phx-blur=\"span-blur\">This is a span</span>\n    <span id=\"span-blur-value\" phx-blur=\"span-blur\" value=\"123\" phx-value-extra=\"456\">\n      This is a span\n    </span>\n    <span id=\"span-blur-phx-value\" phx-blur=\"span-blur\" phx-value-foo=\"123\" phx-value-bar=\"456\">\n      This is a span\n    </span>\n\n    <span id=\"span-focus-no-value\" phx-focus=\"span-focus\">This is a span</span>\n    <span id=\"span-focus-value\" phx-focus=\"span-focus\" value=\"123\" phx-value-extra=\"456\">\n      This is a span\n    </span>\n    <span id=\"span-focus-phx-value\" phx-focus=\"span-focus\" phx-value-foo=\"123\" phx-value-bar=\"456\">\n      This is a span\n    </span>\n\n    <span id=\"span-keyup-no-value\" phx-keyup=\"span-keyup\">This is a span</span>\n    <span id=\"span-keyup-value\" phx-keyup=\"span-keyup\" value=\"123\" phx-value-extra=\"456\">\n      This is a span\n    </span>\n    <span id=\"span-keyup-phx-value\" phx-keyup=\"span-keyup\" phx-value-foo=\"123\" phx-value-bar=\"456\">\n      This is a span\n    </span>\n    <span\n      id=\"span-window-keyup-phx-value\"\n      phx-window-keyup=\"span-window-keyup\"\n      phx-value-foo=\"123\"\n      phx-value-bar=\"456\"\n    >\n      This is a span\n    </span>\n\n    <span id=\"span-keydown-no-value\" phx-keydown=\"span-keydown\">This is a span</span>\n    <span id=\"span-keydown-value\" phx-keydown=\"span-keydown\" value=\"123\" phx-value-extra=\"456\">\n      This is a span\n    </span>\n    <span\n      id=\"span-keydown-phx-value\"\n      phx-keydown=\"span-keydown\"\n      phx-value-foo=\"123\"\n      phx-value-bar=\"456\"\n    >\n      This is a span\n    </span>\n    <span\n      id=\"span-window-keydown-phx-value\"\n      phx-window-keydown=\"span-window-keydown\"\n      phx-value-foo=\"123\"\n      phx-value-bar=\"456\"\n    >\n      This is a span\n    </span>\n\n    <button id=\"button-js-click\" phx-click={JS.push(\"button-click\")}>This is a JS button</button>\n    <button id=\"button-js-click-value\" phx-click={JS.push(\"button-click\", value: %{one: 1})}>\n      This is a JS button with a value\n    </button>\n    <button id=\"button-disabled-click\" phx-click=\"button-click\" disabled>This is a button</button>\n    <span id=\"span-click-no-value\" phx-click=\"span-click\">This is a span</span>\n    <span id=\"span-click-value\" phx-click=\"span-click\" value=\"123\" phx-value-extra=\"&lt;456&gt;\">\n      This is a span\n    </span>\n    <span id=\"span-click-phx-value\" phx-click=\"span-click\" phx-value-foo=\"123\" phx-value-bar=\"456\">\n      This is a span\n    </span>\n\n    <%!-- link handling --%>\n    <a id=\"a-no-attr\">No href link</a>\n    <a href=\"/\" id=\"click-a\" phx-click=\"link\">Regular Link</a>\n    <a href=\"/\" id=\"redirect-a\">Regular Link</a>\n    <.link navigate=\"/example\" id=\"live-redirect-a\">Live redirect</.link>\n    <.link navigate=\"/example\" id=\"live-redirect-replace-a\" replace>Live redirect</.link>\n    <%!-- unrelated phx-click does not disable patching --%>\n    <.link patch=\"/elements?from=uri\" id=\"live-patch-a\" phx-click={JS.dispatch(\"noop\")}>\n      Live patch\n    </.link>\n\n    <button type=\"button\" id=\"live-patch-button\" phx-click={JS.patch(\"/elements?from=uri\")}>\n      Live patch button\n    </button>\n    <button\n      type=\"button\"\n      id=\"live-push-patch-button\"\n      phx-click={JS.push(\"foo\") |> JS.patch(\"/elements?from=uri\")}\n    >\n      Live push patch button\n    </button>\n    <button type=\"button\" id=\"live-redirect-push-button\" phx-click={JS.navigate(\"/example\")}>\n      Live redirect\n    </button>\n    <button\n      type=\"button\"\n      id=\"live-redirect-replace-button\"\n      phx-click={JS.navigate(\"/example\", replace: true)}\n    >\n      Live redirect\n    </button>\n    <button\n      type=\"button\"\n      id=\"live-redirect-patch-button\"\n      phx-click={JS.navigate(\"/example\", replace: true) |> JS.patch(\"/elements?from=uri\")}\n    >\n      Last one wins\n    </button>\n\n    <%!-- hooks --%>\n    <section phx-hook=\"Example\" id=\"hook-section\" phx-value-foo=\"ignore\">Section</section>\n    <section phx-hook=\"Example\" id=\"hook-section-2\" class=\"idless-hook\">Section</section>\n\n    <ul id=\"posts\" phx-update=\"stream\" phx-viewport-top=\"prev-page\" phx-viewport-bottom=\"next-page\" />\n\n    <%!-- forms --%>\n    <a id=\"a-no-form\" phx-change=\"hello\" phx-submit=\"world\">Change</a>\n    <form id=\"empty-form\" phx-change=\"form-change\" phx-submit=\"form-submit\"></form>\n    <form\n      id=\"phx-value-form\"\n      phx-change=\"form-change\"\n      phx-submit=\"form-submit\"\n      phx-value-key=\"val\"\n      phx-value-foo=\"bar\"\n    >\n    </form>\n    <form id=\"form\" phx-change=\"form-change\" phx-submit=\"form-submit\" phx-value-key=\"value\">\n      <input value=\"no-name\" />\n      <input name=\"hello[disabled]\" value=\"value\" disabled />\n      <input name=\"hello[no-type]\" value=\"value\" />\n      <input name=\"hello[latest]\" type=\"text\" value=\"old\" />\n      <input name=\"hello[latest]\" type=\"text\" value=\"new\" />\n      <input name=\"hello[hidden]\" type=\"hidden\" value=\"hidden\" />\n      <input name=\"hello[hidden_or_checkbox]\" type=\"hidden\" value=\"false\" />\n      <input name=\"hello[hidden_or_checkbox]\" type=\"checkbox\" value=\"true\" />\n      <input name=\"hello[hidden_or_text]\" type=\"hidden\" value=\"false\" />\n      <input name=\"hello[hidden_or_text]\" type=\"text\" value=\"true\" />\n      <input name=\"hello[radio]\" type=\"radio\" value=\"1\" />\n      <input name=\"hello[radio]\" type=\"radio\" value=\"2\" checked />\n      <input name=\"hello[radio]\" type=\"radio\" value=\"3\" />\n      <input name=\"hello[not-checked-radio]\" type=\"radio\" value=\"1\" />\n      <input name=\"hello[disabled-radio]\" type=\"radio\" value=\"1\" checked disabled />\n      <input name=\"hello[checkbox]\" type=\"checkbox\" value=\"1\" />\n      <input name=\"hello[checkbox]\" type=\"checkbox\" value=\"2\" checked />\n      <input name=\"hello[checkbox]\" type=\"checkbox\" value=\"3\" />\n      <input name=\"hello[checkbox_no_value]\" type=\"checkbox\" />\n      <input name=\"hello[not-checked-checkbox]\" type=\"checkbox\" value=\"1\" />\n      <input name=\"hello[disabled-checkbox]\" type=\"checkbox\" value=\"1\" checked disabled />\n      <input name=\"hello[multiple-checkbox][]\" type=\"checkbox\" value=\"1\" />\n      <input name=\"hello[multiple-checkbox][]\" type=\"checkbox\" value=\"2\" checked />\n      <input name=\"hello[multiple-checkbox][]\" type=\"checkbox\" value=\"3\" checked />\n      <select name=\"hello[not-selected]\">\n        <option value=\"\" disabled>Disabled Prompt</option>\n        <option value=\"blank\">None</option>\n        <option value=\"1\">One</option>\n        <option value=\"2\">Two</option>\n      </select>\n      <select name=\"hello[not-selected-treeorder]\">\n        <option value=\"\" disabled>Disabled Prompt</option>\n        <optgroup label=\"Nested\">\n          <option value=\"blank\">None</option>\n          <option value=\"1\">One</option>\n        </optgroup>\n        <option value=\"2\">Two</option>\n      </select>\n      <select name=\"hello[not-selected-size]\" size=\"3\">\n        <option value=\"blank\">None</option>\n        <option value=\"1\">One</option>\n        <option value=\"2\">Two</option>\n      </select>\n      <select name=\"hello[selected]\">\n        <option value=\"blank\">None</option>\n        <option value=\"1\" selected>One</option>\n        <option value=\"2\">Two</option>\n      </select>\n      <select name=\"hello[invalid-multiple-selected]\">\n        <option value=\"1\">One</option>\n        <option value=\"2\" selected>Two</option>\n        <option value=\"3\" selected>Three</option>\n      </select>\n      <select name=\"hello[multiple-select][]\" multiple>\n        <option value=\"1\">One</option>\n        <option value=\"2\" selected>Two</option>\n        <option value=\"3\" selected>Three</option>\n      </select>\n      <textarea name=\"hello[textarea]\">Text</textarea>\n      <textarea name=\"hello[textarea_empty]\"></textarea>\n      <textarea name=\"hello[textarea_with_newlines]\"><%= @multiline_text %></textarea>\n      <!-- Mimic textarea from Phoenix.HTML -->\n      <textarea name=\"hello[textarea_nl]\">\n    Text</textarea>\n      <input name=\"hello[ignore-submit]\" type=\"submit\" value=\"ignored\" />\n      <input name=\"hello[ignore-image]\" type=\"image\" value=\"ignored\" />\n      <input name=\"hello[date_text]\" type=\"text\" />\n      {PhoenixHTMLHelpers.Form.date_select(:hello, :date_select)}\n      <input name=\"hello[time_text]\" type=\"text\" />\n      {PhoenixHTMLHelpers.Form.time_select(:hello, :time_select)}\n      <input name=\"hello[naive_text]\" type=\"text\" />\n      {PhoenixHTMLHelpers.Form.datetime_select(:hello, :naive_select)}\n      <input name=\"hello[utc_text]\" type=\"text\" />\n      {PhoenixHTMLHelpers.Form.datetime_select(:hello, :utc_select, second: [])}\n      <input name=\"hello[individual]\" type=\"text\" phx-change=\"individual-changed\" />\n    </form>\n\n    <form id=\"submitter-form\" phx-submit=\"form-submit\">\n      <input name=\"data[a]\" type=\"hidden\" value=\"b\" />\n      <input name=\"input\" type=\"submit\" value=\"yes\" />\n      <input name=\"input_disabled\" type=\"submit\" value=\"yes\" disabled />\n      <input name=\"data[nested]\" id=\"data-nested\" type=\"submit\" value=\"yes\" />\n      <input id=\"input_no_name\" type=\"submit\" value=\"yes\" />\n      <button name=\"button\" type=\"submit\" value=\"yes\">button</button>\n      <button name=\"button_disabled\" type=\"submit\" value=\"yes\" disabled />\n      <button name=\"button_no_submit\" type=\"button\" value=\"this_value_should_never_appear\">\n        button_no_submit\n      </button>\n      <button name=\"button_no_type\" value=\"yes\">button_no_type</button>\n      <button name=\"button_no_value\">Button No Value</button>\n    </form>\n\n    <form data-name=\"form-without-id\" phx-submit=\"form-submit\">\n      <button name=\"button\" type=\"submit\">button</button>\n    </form>\n\n    <form\n      id=\"trigger-form-default\"\n      phx-submit=\"form-submit-trigger\"\n      phx-trigger-action={@trigger_action}\n    >\n      <input type=\"hidden\" name=\"from-form\" value=\"included\" />\n    </form>\n\n    <form id=\"submit-form-default\" action=\"/not_found\"></form>\n\n    <form\n      id=\"trigger-form-value\"\n      action=\"/not_found\"\n      method=\"POST\"\n      phx-submit=\"form-submit-trigger\"\n      phx-trigger-action={@trigger_action}\n    >\n      <input type=\"hidden\" name=\"from-form\" value=\"included\" />\n    </form>\n\n    <form id=\"named\" phx-submit=\"form-submit-named\">\n      <input name=\"child\" />\n    </form>\n    <input form=\"named\" name=\"foo\" />\n    <textarea form=\"named\" name=\"bar\" />\n    <select form=\"named\" name=\"baz\">\n      <option value=\"c\">c</option>\n    </select>\n    <button form=\"named\" name=\"btn\" type=\"submit\" value=\"x\">Submit</button>\n\n    <%!-- @page_title assign is unique --%>\n    <svg>\n      <title>SVG with title</title>\n    </svg>\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    socket =\n      socket\n      |> assign(:event, nil)\n      |> assign(:trigger_action, false)\n      |> assign(:multiline_text, \"This is a test.\\nIt has multiple\\nlines of text.\")\n\n    {:ok, socket}\n  end\n\n  def handle_params(params, _uri, socket) do\n    {:noreply, assign(socket, :event, \"handle_params: #{inspect(params)}\")}\n  end\n\n  def handle_event(\"form-submit-trigger\", _value, socket) do\n    {:noreply, assign(socket, :trigger_action, true)}\n  end\n\n  def handle_event(event, value, socket) do\n    {:noreply, assign(socket, :event, \"#{event}: #{inspect(value)}\")}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.ElementsComponent do\n  use Phoenix.LiveComponent\n\n  alias Phoenix.LiveView.JS\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      <div id=\"component-last-event\">{@event}</div>\n\n      <button\n        id=\"component-button-js-click-target\"\n        phx-click={JS.push(\"button-click\", target: @myself)}\n      >\n        button\n      </button>\n    </div>\n    \"\"\"\n  end\n\n  def mount(socket) do\n    socket = assign(socket, :event, nil)\n\n    {:ok, socket}\n  end\n\n  def handle_params(params, _uri, socket) do\n    {:noreply, assign(socket, :event, \"handle_params: #{inspect(params)}\")}\n  end\n\n  def handle_event(event, value, socket) do\n    {:noreply, assign(socket, :event, \"#{event}: #{inspect(value)}\")}\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/events.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.EventsLive do\n  use Phoenix.LiveView, namespace: Phoenix.LiveViewTest\n\n  def render(assigns) do\n    ~H\"\"\"\n    count: {@count}\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, events: [], count: 0)}\n  end\n\n  def handle_event(\"reply\", %{\"count\" => new_count, \"reply\" => reply}, socket) do\n    {:reply, reply, assign(socket, :count, new_count)}\n  end\n\n  def handle_event(\"reply\", %{\"reply\" => reply}, socket) do\n    {:reply, reply, socket}\n  end\n\n  def handle_event(\"dont-reply\", _, socket) do\n    {:noreply, socket}\n  end\n\n  def handle_call({:run, func}, _, socket), do: func.(socket)\n\n  def handle_info({:run, func}, socket), do: func.(socket)\nend\n\ndefmodule Phoenix.LiveViewTest.Support.EventsMultiJSLive do\n  use Phoenix.LiveView, namespace: Phoenix.LiveViewTest\n  alias Phoenix.LiveView.JS\n\n  def render(assigns) do\n    ~H\"\"\"\n    count: {@count}\n\n    <button\n      id=\"add-one-and-ten\"\n      phx-click={\n        JS.push(\"inc\", value: %{inc: 1})\n        |> JS.push(\"inc\", value: %{inc: 10})\n      }\n    >\n      Add 1 and 10\n    </button>\n\n    <button\n      id=\"reply-values\"\n      phx-click={\n        JS.push(\"reply\", value: %{int: 1})\n        |> JS.push(\"reply\", value: %{int: 2})\n      }\n    >\n      Reply with 1 and 2\n    </button>\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, events: [], count: 0)}\n  end\n\n  def handle_event(\"inc\", %{\"inc\" => v}, socket) do\n    {:noreply, update(socket, :count, &(&1 + v))}\n  end\n\n  def handle_event(\"reply\", %{\"int\" => i}, socket) do\n    {:reply, %{value: i}, socket}\n  end\n\n  def handle_call({:run, func}, _, socket), do: func.(socket)\n\n  def handle_info({:run, func}, socket), do: func.(socket)\nend\n\ndefmodule Phoenix.LiveViewTest.Support.EventsInComponentMultiJSLive do\n  use Phoenix.LiveView, namespace: Phoenix.LiveViewTest\n  alias Phoenix.LiveView.JS\n\n  defmodule Child do\n    use Phoenix.LiveComponent\n\n    def update(assigns, socket) do\n      {:ok, assign(socket, id: assigns.id, count: 0)}\n    end\n\n    def handle_event(\"inc\", %{\"inc\" => v}, socket) do\n      {:noreply, update(socket, :count, &(&1 + v))}\n    end\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div id={@id}>\n        <button\n          id={\"push-to-self-#{@id}\"}\n          phx-click={\n            JS.push(\"inc\", target: \"#child_1\", value: %{inc: 1})\n            |> JS.push(\"inc\", target: \"#child_1\", value: %{inc: 10})\n          }\n        >\n          Both to self\n        </button>\n\n        <button\n          id={\"push-to-other-targets-#{@id}\"}\n          phx-click={\n            JS.push(\"inc\", target: \"#child_2\", value: %{inc: 2})\n            |> JS.push(\"inc\", target: \"#child_1\", value: %{inc: 1})\n            |> JS.push(\"inc\", value: %{inc: -1})\n          }\n        >\n          One to everyone\n        </button>\n\n        {@id} count: {@count}\n      </div>\n      \"\"\"\n    end\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <.live_component module={Child} id={:child_1} />\n    <.live_component module={Child} id={:child_2} /> root count: {@count}\n    \"\"\"\n  end\n\n  def handle_event(\"inc\", %{\"inc\" => v}, socket) do\n    {:noreply, update(socket, :count, &(&1 + v))}\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, events: [], count: 0)}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.EventsInMountLive do\n  use Phoenix.LiveView, namespace: Phoenix.LiveViewTest\n\n  defmodule Child do\n    use Phoenix.LiveView, namespace: Phoenix.LiveViewTest\n\n    def render(assigns) do\n      ~H\"hello!\"\n    end\n\n    def mount(_params, _session, socket) do\n      socket =\n        if connected?(socket),\n          do: push_event(socket, \"child-mount\", %{child: \"bar\"}),\n          else: socket\n\n      {:ok, socket}\n    end\n  end\n\n  def render(assigns) do\n    ~H\"{live_render(@socket, Child, id: :child_live)}\"\n  end\n\n  def mount(_params, _session, socket) do\n    socket =\n      if connected?(socket),\n        do: push_event(socket, \"root-mount\", %{root: \"foo\"}),\n        else: socket\n\n    {:ok, socket}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.EventsInComponentLive do\n  use Phoenix.LiveView, namespace: Phoenix.LiveViewTest\n\n  defmodule Child do\n    use Phoenix.LiveComponent\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div>\n        <button id=\"comp-reply\" phx-click=\"reply\" phx-target={@myself}>\n          bump reply!\n        </button>\n\n        <button id=\"comp-noreply\" phx-click=\"noreply\" phx-target={@myself}>\n          bump no reply!\n        </button>\n      </div>\n      \"\"\"\n    end\n\n    def update(assigns, socket) do\n      socket =\n        if connected?(socket),\n          do: push_event(socket, \"component\", %{count: assigns.count}),\n          else: socket\n\n      {:ok, socket}\n    end\n\n    def handle_event(\"reply\", reply, socket) do\n      {:reply, %{\"comp-reply\" => reply}, socket}\n    end\n\n    def handle_event(\"noreply\", _reply, socket) do\n      {:noreply, socket}\n    end\n  end\n\n  def render(assigns) do\n    ~H\"<.live_component module={Child} id={:child_live} count={@count} />\"\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, :count, 1)}\n  end\n\n  def handle_event(\"bump\", _, socket) do\n    {:noreply, update(socket, :count, &(&1 + 1))}\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/expensive_runtime_checks.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.ExpensiveRuntimeChecksLive do\n  use Phoenix.LiveView\n\n  @impl Phoenix.LiveView\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, :bar, \"bar\")}\n  end\n\n  @impl Phoenix.LiveView\n  def handle_event(\"expensive_assign_async_socket\", _params, socket) do\n    socket\n    |> assign_async(:test, bad_assign_async_function_socket(socket))\n    |> then(&{:noreply, &1})\n  end\n\n  def handle_event(\"expensive_assign_async_assigns\", _params, socket) do\n    socket\n    |> assign_async(:test, bad_assign_async_function_assigns(socket))\n    |> then(&{:noreply, &1})\n  end\n\n  def handle_event(\"good_assign_async\", _params, socket) do\n    socket\n    |> assign_async(:test, good_assign_async_function(socket))\n    |> then(&{:noreply, &1})\n  end\n\n  def handle_event(\"expensive_start_async_socket\", _params, socket) do\n    socket\n    |> start_async(:test, bad_start_async_function_socket(socket))\n    |> then(&{:noreply, &1})\n  end\n\n  def handle_event(\"expensive_start_async_assigns\", _params, socket) do\n    socket\n    |> start_async(:test, bad_start_async_function_assigns(socket))\n    |> then(&{:noreply, &1})\n  end\n\n  def handle_event(\"good_start_async\", _params, socket) do\n    socket\n    |> start_async(:test, good_start_async_function(socket))\n    |> then(&{:noreply, &1})\n  end\n\n  defp bad_assign_async_function_socket(socket) do\n    fn ->\n      {:ok, %{test: do_something_with(socket.assigns.bar)}}\n    end\n  end\n\n  defp bad_assign_async_function_assigns(socket) do\n    assigns = socket.assigns\n\n    fn ->\n      {:ok, %{test: do_something_with(assigns.bar)}}\n    end\n  end\n\n  defp good_assign_async_function(socket) do\n    bar = socket.assigns.bar\n\n    fn ->\n      {:ok, %{test: do_something_with(bar)}}\n    end\n  end\n\n  defp bad_start_async_function_socket(socket) do\n    fn -> do_something_with(socket.assigns.bar) end\n  end\n\n  defp bad_start_async_function_assigns(socket) do\n    assigns = socket.assigns\n\n    fn -> do_something_with(assigns.bar) end\n  end\n\n  defp good_start_async_function(socket) do\n    bar = socket.assigns.bar\n\n    fn -> do_something_with(bar) end\n  end\n\n  defp do_something_with(x), do: x\n\n  @impl Phoenix.LiveView\n  def handle_async(:test, {:ok, _val}, socket), do: {:noreply, socket}\n\n  @impl Phoenix.LiveView\n  def render(assigns) do\n    ~H\"\"\"\n    <h1>Hello!</h1>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/flash.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.FlashLive do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    uri[{@uri}]\n    root[{Phoenix.Flash.get(@flash, :info)}]:info\n    root[{Phoenix.Flash.get(@flash, :error)}]:error\n    <.live_component module={Phoenix.LiveViewTest.Support.FlashComponent} id=\"flash-component\" />\n    child[{live_render(@socket, Phoenix.LiveViewTest.Support.FlashChildLive, id: \"flash-child\")}]\n    \"\"\"\n  end\n\n  def handle_params(_params, uri, socket) do\n    {:noreply, assign(socket, :uri, uri)}\n  end\n\n  def mount(_params, _session, socket), do: {:ok, assign(socket, uri: nil)}\n\n  def handle_event(\"set_error\", %{\"error\" => error}, socket) do\n    {:noreply, socket |> put_flash(:error, error)}\n  end\n\n  def handle_event(\"clear_flash\", %{\"kind\" => kind}, socket) do\n    {:noreply, socket |> clear_flash(kind)}\n  end\n\n  def handle_event(\"redirect\", %{\"to\" => to, \"info\" => info}, socket) do\n    {:noreply, socket |> put_flash(:info, info) |> redirect(to: to)}\n  end\n\n  def handle_event(\"push_navigate\", %{\"to\" => to, \"info\" => info}, socket) do\n    {:noreply, socket |> put_flash(:info, info) |> push_navigate(to: to)}\n  end\n\n  def handle_event(\"push_patch\", %{\"to\" => to, \"info\" => info}, socket) do\n    {:noreply, socket |> put_flash(:info, info) |> push_patch(to: to)}\n  end\n\n  def handle_event(\"push_patch\", %{\"to\" => to, \"error\" => error}, socket) do\n    {:noreply, socket |> put_flash(:error, error) |> push_patch(to: to)}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.FlashComponent do\n  use Phoenix.LiveComponent\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div id={@id} phx-target={@myself} phx-click=\"click\">\n      <span phx-target={@myself} phx-click=\"lv:clear-flash\">Clear all</span>\n      <span phx-target={@myself} phx-click=\"lv:clear-flash\" phx-value-key=\"info\">\n        component[{Phoenix.Flash.get(@flash, :info)}]:info\n      </span>\n      <span phx-target={@myself} phx-click=\"lv:clear-flash\" phx-value-key=\"error\">\n        component[{Phoenix.Flash.get(@flash, :error)}]:error\n      </span>\n    </div>\n    \"\"\"\n  end\n\n  def handle_event(\"click\", %{\"type\" => \"redirect\", \"to\" => to, \"info\" => info}, socket) do\n    {:noreply, socket |> put_flash(:info, info) |> redirect(to: to)}\n  end\n\n  def handle_event(\"click\", %{\"type\" => \"push_navigate\", \"to\" => to, \"info\" => info}, socket) do\n    {:noreply, socket |> put_flash(:info, info) |> push_navigate(to: to)}\n  end\n\n  def handle_event(\"click\", %{\"type\" => \"push_patch\", \"to\" => to, \"info\" => info}, socket) do\n    {:noreply, socket |> put_flash(:info, info) |> push_patch(to: to)}\n  end\n\n  def handle_event(\"click\", %{\"type\" => \"put_flash\", \"info\" => value}, socket) do\n    {:noreply, socket |> put_flash(:info, value)}\n  end\n\n  def handle_event(\"click\", %{\"type\" => \"put_flash\", \"error\" => value}, socket) do\n    {:noreply, socket |> put_flash(:error, value)}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.FlashChildLive do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    {Phoenix.Flash.get(@flash, :info)}\n    \"\"\"\n  end\n\n  def mount(%{\"mount_redirect\" => message}, _uri, socket) do\n    {:ok, socket |> redirect(to: \"/flash-root\") |> put_flash(:info, message)}\n  end\n\n  def mount(%{\"mount_push_navigate\" => message}, _uri, socket) do\n    {:ok, socket |> push_navigate(to: \"/flash-root\") |> put_flash(:info, message)}\n  end\n\n  def mount(_params, _session, socket), do: {:ok, socket}\n\n  def handle_event(\"set_error\", %{\"error\" => error}, socket) do\n    {:noreply, socket |> put_flash(:error, error)}\n  end\n\n  def handle_event(\"redirect\", %{\"to\" => to, \"info\" => info}, socket) do\n    {:noreply, socket |> put_flash(:info, info) |> redirect(to: to)}\n  end\n\n  def handle_event(\"push_navigate\", %{\"to\" => to, \"info\" => info}, socket) do\n    {:noreply, socket |> put_flash(:info, info) |> push_navigate(to: to)}\n  end\n\n  def handle_event(\"push_patch\", %{\"to\" => to, \"info\" => info}, socket) do\n    {:noreply, socket |> put_flash(:info, info) |> push_patch(to: to)}\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/general.ex",
    "content": "alias Phoenix.LiveViewTest.Support.{ClockLive, ClockControlsLive}\n\ndefmodule Phoenix.LiveViewTest.Support.ThermostatLive do\n  use Phoenix.LiveView, container: {:article, class: \"thermo\"}, namespace: Phoenix.LiveViewTest\n\n  defmodule Error do\n    defexception [:plug_status]\n    def message(%{plug_status: status}), do: \"error #{status}\"\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <p>Redirect: {@redirect}</p>\n    <p>The temp is: {@val}{@greeting}</p>\n    <button phx-click=\"dec\">-</button>\n    <button phx-click=\"inc\">+</button>\n    <%= if @nest do %>\n      {live_render(@socket, ClockLive, [id: :clock] ++ @nest)}\n      <%= for user <- @users do %>\n        <i>{user.name} {user.email}</i>\n      <% end %>\n    <% end %>\n    \"\"\"\n  end\n\n  def mount(%{\"raise_connected\" => status}, session, socket) do\n    if connected?(socket) do\n      raise Error, plug_status: String.to_integer(status)\n    else\n      mount(%{}, session, socket)\n    end\n  end\n\n  def mount(%{\"raise_disconnected\" => status}, session, socket) do\n    if connected?(socket) do\n      mount(%{}, session, socket)\n    else\n      raise Error, plug_status: String.to_integer(status)\n    end\n  end\n\n  def mount(_params, session, socket) do\n    nest = Map.get(session, \"nest\", false)\n    users = session[\"users\"] || []\n    val = if connected?(socket), do: 1, else: 0\n\n    {:ok, assign(socket, val: val, nest: nest, users: users, greeting: nil)}\n  end\n\n  def handle_params(params, _url, socket) do\n    {:noreply, assign(socket, redirect: params[\"redirect\"] || \"none\")}\n  end\n\n  def handle_event(\"key\", %{\"key\" => \"i\"}, socket) do\n    {:noreply, update(socket, :val, &(&1 + 1))}\n  end\n\n  def handle_event(\"key\", %{\"key\" => \"d\"}, socket) do\n    {:noreply, update(socket, :val, &(&1 - 1))}\n  end\n\n  def handle_event(\"save\", %{\"temp\" => new_temp} = params, socket) do\n    {:noreply, assign(socket, val: new_temp, greeting: inspect(params[\"_target\"]))}\n  end\n\n  def handle_event(\"save\", new_temp, socket) do\n    {:noreply, assign(socket, :val, new_temp)}\n  end\n\n  def handle_event(\"inactive\", %{\"value\" => msg}, socket) do\n    {:noreply, assign(socket, :greeting, \"Tap to wake – #{msg}\")}\n  end\n\n  def handle_event(\"active\", %{\"value\" => msg}, socket) do\n    {:noreply, assign(socket, :greeting, \"Waking up – #{msg}\")}\n  end\n\n  def handle_event(\"noop\", _, socket), do: {:noreply, socket}\n\n  def handle_event(\"inc\", _, socket), do: {:noreply, update(socket, :val, &(&1 + 1))}\n\n  def handle_event(\"dec\", _, socket), do: {:noreply, update(socket, :val, &(&1 - 1))}\n\n  def handle_call({:set, var, val}, _, socket) do\n    {:reply, :ok, assign(socket, var, val)}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.ClockLive do\n  use Phoenix.LiveView, container: {:section, class: \"clock\"}\n\n  def render(assigns) do\n    ~H\"\"\"\n    time: {@time} {@name}\n    {live_render(@socket, ClockControlsLive,\n      id: :\"#{String.replace(@name, \" \", \"-\")}-controls\",\n      sticky: @sticky\n    )}\n    \"\"\"\n  end\n\n  def mount(:not_mounted_at_router, session, socket) do\n    {:ok, assign(socket, time: \"12:00\", name: session[\"name\"] || \"NY\", sticky: false)}\n  end\n\n  def mount(%{} = params, session, socket) do\n    {:ok,\n     assign(socket, time: \"12:00\", name: session[\"name\"] || \"NY\", sticky: !!params[\"sticky\"])}\n  end\n\n  def handle_info(:snooze, socket) do\n    {:noreply, assign(socket, :time, \"12:05\")}\n  end\n\n  def handle_info({:run, func}, socket) do\n    func.(socket)\n  end\n\n  def handle_call({:set, new_time}, _from, socket) do\n    {:reply, :ok, assign(socket, :time, new_time)}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.ClockControlsLive do\n  use Phoenix.LiveView\n\n  def render(assigns), do: ~H|<button phx-click=\"snooze\">+</button>|\n\n  def mount(_params, _session, socket), do: {:ok, socket}\n\n  def handle_event(\"snooze\", _, socket) do\n    send(socket.parent_pid, :snooze)\n    {:noreply, socket}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.DashboardLive do\n  use Phoenix.LiveView, container: {:div, class: inspect(__MODULE__)}\n\n  def render(assigns) do\n    ~H\"\"\"\n    session: {Phoenix.HTML.raw(inspect(@session))}\n    \"\"\"\n  end\n\n  def mount(_params, session, socket) do\n    {:ok, assign(socket, %{session: session, title: \"Dashboard\"})}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.SameChildLive do\n  use Phoenix.LiveView\n\n  def render(%{dup: true} = assigns) do\n    ~H\"\"\"\n    <%= for name <- @names do %>\n      {live_render(@socket, ClockLive, id: :dup, session: %{\"name\" => name})}\n    <% end %>\n    \"\"\"\n  end\n\n  def render(%{dup: false} = assigns) do\n    ~H\"\"\"\n    <%= for name <- @names do %>\n      {live_render(@socket, ClockLive, session: %{\"name\" => name, \"count\" => @count}, id: name)}\n    <% end %>\n    \"\"\"\n  end\n\n  def mount(_params, %{\"dup\" => dup}, socket) do\n    {:ok, assign(socket, count: 0, dup: dup, names: ~w(Tokyo Madrid Toronto))}\n  end\n\n  def handle_event(\"inc\", _, socket) do\n    {:noreply, assign(socket, :count, socket.assigns.count + 1)}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.RootLive do\n  use Phoenix.LiveView\n  alias Phoenix.LiveViewTest.Support.ChildLive\n\n  def render(assigns) do\n    ~H\"\"\"\n    root name: {@current_user.name}\n    {live_render(@socket, ChildLive, id: :static, session: %{\"child\" => :static})}\n    <%= if @dynamic_child do %>\n      {live_render(@socket, ChildLive, id: @dynamic_child, session: %{\"child\" => :dynamic})}\n    <% end %>\n    \"\"\"\n  end\n\n  def mount(_params, %{\"user_id\" => user_id}, socket) do\n    {:ok,\n     socket\n     |> assign(:dynamic_child, nil)\n     |> assign_new(:current_user, fn ->\n       %{name: \"user-from-root\", id: user_id}\n     end)}\n  end\n\n  def handle_call({:dynamic_child, child}, _from, socket) do\n    {:reply, :ok, assign(socket, dynamic_child: child)}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.ChildLive do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    child {@id} name: {@current_user.name}\n    \"\"\"\n  end\n\n  # The \"user_id\" is carried from the session to the child live view too\n  def mount(_params, %{\"user_id\" => user_id, \"child\" => id}, socket) do\n    {:ok,\n     socket\n     |> assign(:id, id)\n     |> assign_new(:current_user, fn ->\n       %{name: \"user-from-child\", id: user_id}\n     end)}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.OptsLive do\n  use Phoenix.LiveView\n\n  def render(assigns), do: ~H|{@description}. {@canary}|\n\n  def mount(_params, %{\"opts\" => opts}, socket) do\n    {:ok, assign(socket, description: \"long description\", canary: \"canary\"), opts}\n  end\n\n  def handle_call({:exec, func}, _from, socket) do\n    func.(socket)\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.RedirLive do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    Title: {@title}\n    <%= if @child_params do %>\n      {live_render(@socket, __MODULE__, id: :child, session: %{\"child_redir\" => @child_params})}\n    <% end %>\n    \"\"\"\n  end\n\n  def mount(%{\"to\" => to, \"kind\" => kind, \"during\" => during}, _session, socket) do\n    cond do\n      during == \"connected\" and connected?(socket) ->\n        {:ok, do_redirect(socket, kind, to: to)}\n\n      during == \"disconnected\" and not connected?(socket) ->\n        {:ok, do_redirect(socket, kind, to: to)}\n\n      during == \"connected\" ->\n        {:ok, assign(socket, title: \"parent_content\", child_params: nil)}\n    end\n  end\n\n  def mount(%{\"child_to\" => to, \"kind\" => kind, \"during\" => during}, session, socket)\n      when session == %{} do\n    if socket.parent_pid == nil do\n      {:ok,\n       assign(socket,\n         title: \"parent_content\",\n         child_params: %{\"to\" => to, \"kind\" => kind, \"during\" => during}\n       )}\n    else\n      raise \"cannot nest\"\n    end\n  end\n\n  def mount(\n        _params,\n        %{\"child_redir\" => %{\"to\" => to, \"kind\" => kind, \"during\" => during}},\n        socket\n      ) do\n    cond do\n      during == \"connected\" and connected?(socket) ->\n        {:ok, do_redirect(socket, kind, to: to)}\n\n      during == \"disconnected\" and not connected?(socket) ->\n        {:ok, do_redirect(socket, kind, to: to)}\n\n      during == \"connected\" ->\n        {:ok, assign(socket, title: \"child_content\", child_params: nil)}\n    end\n  end\n\n  defp do_redirect(socket, \"push_navigate\", opts), do: push_navigate(socket, opts)\n  defp do_redirect(socket, \"redirect\", opts), do: redirect(socket, opts)\n  defp do_redirect(socket, \"external\", to: url), do: redirect(socket, external: url)\n  defp do_redirect(socket, \"push_patch\", opts), do: push_patch(socket, opts)\nend\n\ndefmodule Phoenix.LiveViewTest.Support.AssignsNotInSocketLive do\n  use Phoenix.LiveView\n\n  def render(assigns), do: ~H|{boom(@socket)}|\n  def mount(_params, _session, socket), do: {:ok, socket}\n  defp boom(socket), do: socket.assigns.boom\nend\n\ndefmodule Phoenix.LiveViewTest.Support.ErrorsLive do\n  use Phoenix.LiveView\n\n  alias Phoenix.LiveView.Socket\n\n  def render(assigns), do: ~H|<div>I crash in mount</div>|\n\n  def mount(%{\"crash_on\" => \"disconnected_mount\"}, _, %Socket{transport_pid: nil}),\n    do: raise(\"boom disconnected mount\")\n\n  def mount(%{\"crash_on\" => \"connected_mount\"}, _, %Socket{transport_pid: pid}) when is_pid(pid),\n    do: raise(\"boom connected mount\")\n\n  def mount(_params, _session, socket), do: {:ok, socket}\n\n  def handle_params(%{\"crash_on\" => \"disconnected_handle_params\"}, _, %Socket{transport_pid: nil}),\n      do: raise(\"boom disconnected handle_params\")\n\n  def handle_params(%{\"crash_on\" => \"connected_handle_params\"}, _, %Socket{transport_pid: pid})\n      when is_pid(pid),\n      do: raise(\"boom connected handle_params\")\n\n  def handle_params(_params, _session, socket), do: {:noreply, socket}\n\n  def handle_event(\"crash\", _params, _socket), do: raise(\"boom handle_event\")\nend\n\ndefmodule Phoenix.LiveViewTest.Support.ClassListLive do\n  use Phoenix.LiveView, container: {:span, class: ~w(foo bar)}\n\n  def render(assigns), do: ~H|Some content|\nend\n\n# empty, but needed to silence warnings about unavailable modules\ndefmodule Phoenix.LiveViewTest.Support.FooBarLive do\n  use Phoenix.LiveView\n  def render(assigns), do: ~H\"\"\nend\n\ndefmodule Phoenix.LiveViewTest.Support.FooBarLive.Index do\n  use Phoenix.LiveView\n  def render(assigns), do: ~H\"\"\nend\n\ndefmodule Phoenix.LiveViewTest.Support.FooBarLive.Nested.Index do\n  use Phoenix.LiveView\n  def render(assigns), do: ~H\"\"\nend\n\ndefmodule Phoenix.LiveViewTest.Support.Live.Nested.Module do\n  use Phoenix.LiveView\n  def render(assigns), do: ~H\"\"\nend\n\ndefmodule Phoenix.LiveViewTest.Support.NoSuffix do\n  use Phoenix.LiveView\n  def render(assigns), do: ~H\"\"\nend\n"
  },
  {
    "path": "test/support/live_views/host.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.HostLive do\n  use Phoenix.LiveView\n  alias Phoenix.LiveViewTest.Support.Router.Helpers, as: Routes\n\n  def handle_params(_params, uri, socket) do\n    {:noreply, assign(socket, :uri, uri)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <p>URI: {@uri}</p>\n    <p>LiveAction: {@live_action}</p>\n    <.link id=\"path\" patch={Routes.host_path(@socket, :path)}>Path</.link>\n    <.link id=\"full\" patch={\"https://app.example.com\" <> Routes.host_path(@socket, :full)}>\n      Full\n    </.link>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/layout.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.ParentLayoutLive do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    {live_render(@socket, Phoenix.LiveViewTest.Support.LayoutLive, session: @session, id: \"layout\")}\n    \"\"\"\n  end\n\n  def mount(_params, session, socket) do\n    {:ok, assign(socket, session: session)}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.LayoutLive do\n  use Phoenix.LiveView, layout: {Phoenix.LiveViewTest.Support.LayoutView, :live}\n\n  def render(assigns), do: ~H|The value is: {@val}|\n\n  def mount(_params, session, socket) do\n    socket\n    |> assign(val: 123)\n    |> maybe_put_layout(session)\n  end\n\n  def handle_event(\"double\", _, socket) do\n    {:noreply, update(socket, :val, &(&1 * 2))}\n  end\n\n  defp maybe_put_layout(socket, %{\"live_layout\" => value}) do\n    {:ok, socket, layout: value}\n  end\n\n  defp maybe_put_layout(socket, _session), do: {:ok, socket}\nend\n"
  },
  {
    "path": "test/support/live_views/lifecycle.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.InitAssigns do\n  alias Phoenix.Component\n\n  def on_mount(:default, _params, _session, socket) do\n    {:cont,\n     socket\n     |> Component.assign(:init_assigns_mount, true)\n     |> Component.assign(:last_on_mount, :init_assigns_mount)}\n  end\n\n  def on_mount(:other, _params, _session, socket) do\n    {:cont,\n     socket\n     |> Component.assign(:init_assigns_other_mount, true)\n     |> Component.assign(:last_on_mount, :init_assigns_other_mount)}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.MountArgs do\n  import Phoenix.LiveView\n\n  def on_mount(:inlined, _params, _session, socket) do\n    qs = URI.encode_query(%{called: true, inlined: true})\n    {:halt, push_navigate(socket, to: \"/lifecycle?#{qs}\")}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.OnMount do\n  def on_mount(:default, _params, _session, socket) do\n    {:cont, socket}\n  end\n\n  def on_mount(:other, _params, _session, socket) do\n    {:cont, socket}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.OtherOnMount do\n  def on_mount(:default, _params, _session, socket) do\n    {:cont, socket}\n  end\n\n  def on_mount(:other, _params, _session, socket) do\n    {:cont, socket}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.HooksLive do\n  use Phoenix.LiveView, namespace: Phoenix.LiveViewTest\n  alias Phoenix.LiveViewTest.Support.InitAssigns\n\n  on_mount InitAssigns\n  on_mount {InitAssigns, :other}\n\n  def render(assigns) do\n    ~H\"\"\"\n    <p>last_on_mount:{inspect(assigns[:last_on_mount])}</p>\n    <p>params_hook:{assigns[:params_hook_ref]}</p>\n    <p>count:{@count}</p>\n    <p>task:{@task}</p>\n    <button id=\"dec\" phx-click=\"dec\">-</button>\n    <button id=\"inc\" phx-click=\"inc\">+</button>\n    <button id=\"patch\" phx-click=\"patch\">?</button>\n    <button id=\"async\" phx-click=\"async\">=</button>\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, count: 0, task: \"\")}\n  end\n\n  def handle_event(\"inc\", _, socket), do: {:noreply, update(socket, :count, &(&1 + 1))}\n  def handle_event(\"dec\", _, socket), do: {:noreply, update(socket, :count, &(&1 - 1))}\n\n  def handle_event(\"patch\", _, socket) do\n    ref = socket.assigns[:params_hook_ref] || 0\n    {:noreply, push_patch(socket, to: \"/lifecycle?ref=#{ref}\")}\n  end\n\n  def handle_event(\"async\", _, socket) do\n    {:noreply, start_async(socket, :task, fn -> true end)}\n  end\n\n  def handle_async(:task, {:ok, true}, socket) do\n    {:noreply, update(socket, :task, &(&1 <> \".\"))}\n  end\n\n  def handle_call({:run, func}, _, socket), do: func.(socket)\n\n  def handle_call({:push_patch, to}, _, socket) do\n    {:reply, :ok, push_patch(socket, to: to)}\n  end\n\n  def handle_info(:noop, socket), do: {:noreply, socket}\n\n  def handle_info({:ping, ref, pid}, socket) when is_reference(ref) and is_pid(pid) do\n    send(pid, {:pong, ref})\n    {:noreply, socket}\n  end\n\n  def handle_info({:run, func}, socket), do: func.(socket)\n\n  ## test helpers\n\n  def attach_hook(lv, name, stage, cb) do\n    run(lv, fn socket ->\n      {:reply, :ok, Phoenix.LiveView.attach_hook(socket, name, stage, cb)}\n    end)\n  end\n\n  def detach_hook(lv, name, stage) do\n    run(lv, fn socket ->\n      {:reply, :ok, Phoenix.LiveView.detach_hook(socket, name, stage)}\n    end)\n  end\n\n  def fetch_lifecycle(lv) do\n    run(lv, fn socket ->\n      {:reply, Map.fetch(socket.private, :lifecycle), socket}\n    end)\n  end\n\n  def exits_with(lv, kind, func) do\n    Process.unlink(proxy_pid(lv))\n\n    try do\n      func.()\n      raise \"expected to exit with #{inspect(kind)}\"\n    catch\n      :exit, {{%mod{message: msg}, _}, _} when mod == kind -> msg\n    end\n  end\n\n  def unlink_and_monitor(lv) do\n    Process.unlink(proxy_pid(lv))\n    Process.monitor(proxy_pid(lv))\n  end\n\n  def run(lv, func) do\n    GenServer.call(lv.pid, {:run, func})\n  end\n\n  def proxy_pid(%{proxy: {_ref, _topic, pid}}), do: pid\nend\n\ndefmodule Phoenix.LiveViewTest.Support.HooksLive.BadMount do\n  use Phoenix.LiveView, namespace: Phoenix.LiveViewTest\n\n  on_mount __MODULE__\n\n  def on_mount(:default, _params, _session, _socket), do: :boom\n\n  def mount(_params, _session, _socket) do\n    raise \"expected to exit before #{__MODULE__}.mount/3\"\n  end\n\n  def render(assigns), do: ~H\"<div></div>\"\nend\n\ndefmodule Phoenix.LiveViewTest.Support.HooksLive.HaltMount do\n  use Phoenix.LiveView, namespace: Phoenix.LiveViewTest\n\n  on_mount {__MODULE__, :hook}\n\n  def on_mount(:hook, _, _, socket), do: {:halt, socket}\n  def render(assigns), do: ~H\"<div></div>\"\nend\n\ndefmodule Phoenix.LiveViewTest.Support.HooksLive.RedirectMount do\n  use Phoenix.LiveView, namespace: Phoenix.LiveViewTest\n\n  def mount(_, _, socket) do\n    case socket.assigns.live_action do\n      :halt -> raise \"mount should not have been called\"\n      _ -> {:ok, socket}\n    end\n  end\n\n  on_mount __MODULE__\n\n  def on_mount(:default, _, _, %{assigns: %{live_action: action}} = socket) do\n    {action, push_navigate(socket, to: \"/lifecycle\")}\n  end\n\n  def render(assigns), do: ~H\"<div></div>\"\nend\n\ndefmodule Phoenix.LiveViewTest.Support.HooksLive.Noop do\n  use Phoenix.LiveView, namespace: Phoenix.LiveViewTest\n\n  def render(assigns) do\n    ~H\"\"\"\n    <h1>Noop</h1>\n    last_on_mount:{inspect(assigns[:last_on_mount])}\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.HaltConnectedMount do\n  alias Phoenix.{Component, LiveView}\n\n  def on_mount(_arg, _params, _session, socket) do\n    if LiveView.connected?(socket) do\n      {:halt, LiveView.push_navigate(socket, to: \"/lifecycle\")}\n    else\n      {:cont, Component.assign(socket, :last_on_mount, __MODULE__)}\n    end\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.HooksAttachInfoComponent do\n  use Phoenix.LiveComponent\n  alias Phoenix.LiveView\n\n  def mount(socket) do\n    {:ok, LiveView.attach_hook(socket, :live_component_hook, :handle_info, &__MODULE__.hook/3)}\n  end\n\n  def hook(_, _, _socket) do\n    raise \"expected to exit before #{__MODULE__}.hook/3\"\n  end\n\n  def render(assigns), do: ~H\"<div></div>\"\nend\n\ndefmodule Phoenix.LiveViewTest.Support.HooksDetachInfoComponent do\n  use Phoenix.LiveComponent\n  alias Phoenix.LiveView\n\n  def mount(socket) do\n    {:ok, LiveView.detach_hook(socket, :live_view_hook, :handle_info)}\n  end\n\n  def render(assigns), do: ~H\"<div></div>\"\nend\n\ndefmodule Phoenix.LiveViewTest.Support.HooksEventComponent do\n  use Phoenix.LiveComponent\n  alias Phoenix.LiveView\n\n  def mount(socket) do\n    {:ok, assign(socket, :counter, 0)}\n  end\n\n  def update(assigns, socket) do\n    socket = assign(socket, assigns)\n    hook = if assigns.reply?, do: &__MODULE__.hook_reply/3, else: &__MODULE__.hook/3\n    {:ok, LiveView.attach_hook(socket, :live_component_hook, :handle_event, hook)}\n  end\n\n  def hook(\"detach\", _, socket),\n    do: {:halt, LiveView.detach_hook(socket, :live_component_hook, :handle_event)}\n\n  def hook(_, _, socket), do: {:halt, assign(socket, :counter, socket.assigns.counter + 1)}\n\n  def hook_reply(\"detach\", _, socket),\n    do: {:halt, LiveView.detach_hook(socket, :live_component_hook, :handle_event)}\n\n  def hook_reply(_, _, socket) do\n    counter = socket.assigns.counter + 1\n    {:halt, %{counter: counter}, assign(socket, :counter, counter)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      <div id=\"detach-component-hook\" phx-click=\"detach\" phx-target={@myself}>Detach</div>\n      <div id=\"hook\" phx-click=\"event\" phx-target={@myself}>counter: {@counter}</div>\n    </div>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.HooksAsyncComponent do\n  use Phoenix.LiveComponent\n  alias Phoenix.LiveView\n\n  def mount(socket) do\n    {:ok, assign(socket, :task, \"\")}\n  end\n\n  def update(assigns, socket) do\n    socket = assign(socket, assigns)\n    hook = &__MODULE__.hook/3\n    {:ok, LiveView.attach_hook(socket, :live_component_hook, :handle_async, hook)}\n  end\n\n  def handle_event(\"detach\", _, socket) do\n    {:noreply, LiveView.detach_hook(socket, :live_component_hook, :handle_async)}\n  end\n\n  def handle_event(\"async\", _, socket) do\n    {:noreply, start_async(socket, :task, fn -> true end)}\n  end\n\n  def handle_async(:task, {:ok, true}, socket) do\n    {:noreply, assign(socket, :task, socket.assigns.task <> \".\")}\n  end\n\n  def hook(\"detach\", _, socket) do\n    {:halt, LiveView.detach_hook(socket, :live_component_hook, :handle_async)}\n  end\n\n  def hook(_, _, socket) do\n    {:halt, assign(socket, :task, socket.assigns.task <> \"o\")}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      <div id=\"detach-component-hook\" phx-click=\"detach\" phx-target={@myself}>Detach</div>\n      <div id=\"async\" phx-click=\"async\" phx-target={@myself}>task: {@task}</div>\n    </div>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.HooksLive.WithComponent do\n  use Phoenix.LiveView, namespace: Phoenix.LiveViewTest\n  alias Phoenix.LiveViewTest.Support.{HooksAttachInfoComponent, HooksDetachInfoComponent}\n  alias Phoenix.LiveViewTest.Support.HooksEventComponent\n  alias Phoenix.LiveViewTest.Support.HooksAsyncComponent\n\n  def mount(params, _session, socket) do\n    type = String.to_existing_atom(params[\"type\"])\n    reply? = Map.get(params, \"reply\", \"false\") |> String.to_existing_atom()\n\n    {:ok,\n     socket\n     |> assign(:component, nil)\n     |> assign(:type, type)\n     |> assign(:reply?, reply?)\n     |> attach_hook(:live_view_hook, :handle_event, fn _, _, socket ->\n       {:cont, socket}\n     end)}\n  end\n\n  def handle_event(\"load\", %{\"val\" => val}, socket) do\n    component =\n      case {val, socket.assigns.type} do\n        {\"attach\", :handle_info} -> HooksAttachInfoComponent\n        {\"detach\", :handle_info} -> HooksDetachInfoComponent\n        {\"attach\", :handle_event} -> HooksEventComponent\n        {\"attach\", :handle_async} -> HooksAsyncComponent\n      end\n\n    {:noreply, assign(socket, :component, component)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <button id=\"attach\" phx-click=\"load\" phx-value-val=\"attach\">Load/Attach</button>\n    <button id=\"detach\" phx-click=\"load\" phx-value-val=\"detach\">Load/Detach</button>\n    <%= if @component do %>\n      <.live_component module={@component} id={:hook} type={@type} reply?={@reply?} />\n    <% end %>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.HooksLive.HandleParamsNotDefined do\n  use Phoenix.LiveView, namespace: Phoenix.LiveViewTest\n\n  def mount(_, _, socket) do\n    {:ok,\n     attach_hook(socket, :assign_url, :handle_params, fn _, url, socket ->\n       {:cont, assign(socket, :url, url)}\n     end)}\n  end\n\n  def render(assigns), do: ~H\"url={assigns[:url]}\"\nend\n\ndefmodule Phoenix.LiveViewTest.Support.HooksLive.HandleInfoNotDefined do\n  use Phoenix.LiveView, namespace: Phoenix.LiveViewTest\n\n  def mount(_, _, socket) do\n    send(self(), {:data, \"somedata\"})\n\n    {:ok,\n     attach_hook(socket, :assign_url, :handle_info, fn message, socket ->\n       {:data, data} = message\n       {:cont, assign(socket, :data, data)}\n     end)}\n  end\n\n  def render(assigns), do: ~H\"data={assigns[:data]}\"\nend\n\ndefmodule Phoenix.LiveViewTest.Support.HooksLive.OnMountOptions do\n  use Phoenix.LiveView, namespace: Phoenix.LiveViewTest\n\n  on_mount {__MODULE__, :temporary_assigns}\n  on_mount {__MODULE__, :layout}\n\n  def on_mount(:temporary_assigns, _params, _session, socket) do\n    {:cont, socket, temporary_assigns: [data: \"Phoenix\"]}\n  end\n\n  def on_mount(:layout, _params, _session, socket) do\n    {:cont, socket, layout: {Phoenix.LiveViewTest.Support.LayoutView, :on_mount_layout}}\n  end\n\n  def render(assigns), do: ~H\"data-{@data}\"\nend\n"
  },
  {
    "path": "test/support/live_views/live_in_component.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.LiveInComponent.Root do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  def render(assigns) do\n    ~H\"<.live_component\n  module={Phoenix.LiveViewTest.Support.LiveInComponent.Component}\n  id={:nested_component}\n/>\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.LiveInComponent.Component do\n  use Phoenix.LiveComponent\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      {live_render(@socket, Phoenix.LiveViewTest.Support.LiveInComponent.Live, id: :nested_live)}\"\n    </div>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.LiveInComponent.Live do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  def render(assigns) do\n    ~H\"\"\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/params.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.ParamCounterLive do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    <p>The value is: {@val}</p>\n    <p>mount: {inspect(@mount_params)}</p>\n    <p>params: {inspect(@params)}</p>\n    \"\"\"\n  end\n\n  def mount(params, session, socket) do\n    on_handle_params = session[\"on_handle_params\"]\n\n    {:ok,\n     assign(\n       socket,\n       val: 1,\n       mount_params: params,\n       test_pid: session[\"test_pid\"],\n       connected?: connected?(socket),\n       on_handle_params: on_handle_params && :erlang.binary_to_term(on_handle_params)\n     )}\n  end\n\n  def handle_params(%{\"from\" => \"handle_params\"} = params, uri, socket) do\n    send(socket.assigns.test_pid, {:handle_params, uri, socket.assigns, params})\n    socket.assigns.on_handle_params.(assign(socket, :params, params))\n  end\n\n  def handle_params(params, uri, socket) do\n    send(socket.assigns.test_pid, {:handle_params, uri, socket.assigns, params})\n    {:noreply, assign(socket, :params, params)}\n  end\n\n  def handle_info({:set, var, val}, socket), do: {:noreply, assign(socket, var, val)}\n\n  def handle_info({:push_patch, to}, socket) do\n    {:noreply, push_patch(socket, to: to)}\n  end\n\n  def handle_info({:push_navigate, to}, socket) do\n    {:noreply, push_navigate(socket, to: to)}\n  end\n\n  def handle_call({:push_patch, func}, _from, socket) do\n    func.(socket)\n  end\n\n  def handle_call({:push_navigate, func}, _from, socket) do\n    func.(socket)\n  end\n\n  def handle_cast({:push_patch, to}, socket) do\n    {:noreply, push_patch(socket, to: to)}\n  end\n\n  def handle_cast({:push_navigate, to}, socket) do\n    {:noreply, push_navigate(socket, to: to)}\n  end\n\n  def handle_event(\"push_patch\", %{\"to\" => to}, socket) do\n    {:noreply, push_patch(socket, to: to)}\n  end\n\n  def handle_event(\"push_navigate\", %{\"to\" => to}, socket) do\n    {:noreply, push_navigate(socket, to: to)}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.ActionLive do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    <p>Live action: {inspect(@live_action)}</p>\n    <p>Mount action: {inspect(@mount_action)}</p>\n    <p>Params: {inspect(@params)}</p>\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, mount_action: socket.assigns.live_action)}\n  end\n\n  def handle_params(params, _url, socket) do\n    {:noreply, assign(socket, params: params)}\n  end\n\n  def handle_event(\"push_patch\", to, socket) do\n    {:noreply, push_patch(socket, to: to)}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.ErrorInHandleParamsLive do\n  use Phoenix.LiveView\n\n  def render(assigns), do: ~H|<div>I crash in handle_params</div>|\n  def mount(_params, _session, socket), do: {:ok, socket}\n  def handle_params(_params, _uri, _socket), do: raise(\"boom\")\nend\n"
  },
  {
    "path": "test/support/live_views/reload_live.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.ReloadLive do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  def render(assigns) do\n    case Application.fetch_env(:phoenix_live_view, :vsn) do\n      {:ok, 1} -> ~H\"<div>Version 1</div>\"\n      {:ok, 2} -> ~H\"<div>Version 2</div>\"\n    end\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/render_with.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.RenderWithLive do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    {:ok,\n     render_with(socket, fn assigns ->\n       ~H\"\"\"\n       FROM RENDER WITH!\n       \"\"\"\n     end)}\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/root_tag_attr.exs",
    "content": "# Note this file is intentionally a .exs file because it is loaded\n# in the test helper with :root_tag_attribute turned on.\ndefmodule Phoenix.LiveViewTest.Support.RootTagAttr do\n  use Phoenix.Component\n\n  defmodule RootTagsWithValuesMacroComponent do\n    @behaviour Phoenix.Component.MacroComponent\n\n    @impl true\n    def transform(_ast, _meta) do\n      {:ok, \"\", %{},\n       [\n         root_tag_attribute: {\"phx-sample-one\", \"test\"},\n         root_tag_attribute: {\"phx-sample-two\", \"test\"}\n       ]}\n    end\n  end\n\n  defmodule RootTagsWithoutValuesMacroComponent do\n    @behaviour Phoenix.Component.MacroComponent\n\n    @impl true\n    def transform(_ast, _meta) do\n      {:ok, \"\", %{},\n       [\n         root_tag_attribute: {\"phx-sample-one\", true},\n         root_tag_attribute: {\"phx-sample-two\", true}\n       ]}\n    end\n  end\n\n  def macro_component_attrs_with_values_within_nestings(assigns) do\n    ~H\"\"\"\n    <div :type={Phoenix.LiveViewTest.Support.RootTagAttr.RootTagsWithValuesMacroComponent}></div>\n    <%= if true do %>\n      <div>\n        <div>\n          <%= if @bool do %>\n            <.inner_block_and_slot>\n              <p>\n                <span>True</span>\n              </p>\n            </.inner_block_and_slot>\n          <% else %>\n            <.inner_block_and_slot>\n              <p>\n                <span>False</span>\n              </p>\n            </.inner_block_and_slot>\n          <% end %>\n        </div>\n      </div>\n    <% end %>\n    \"\"\"\n  end\n\n  def within_nestings(assigns) do\n    ~H\"\"\"\n    <%= if true do %>\n      <div>\n        <div>\n          <%= if @bool do %>\n            <.inner_block_and_slot>\n              <p>\n                <span>True</span>\n              </p>\n            </.inner_block_and_slot>\n          <% else %>\n            <.inner_block_and_slot>\n              <p>\n                <span>False</span>\n              </p>\n            </.inner_block_and_slot>\n          <% end %>\n        </div>\n      </div>\n    <% end %>\n    \"\"\"\n  end\n\n  def macro_component_attrs_with_values(assigns) do\n    ~H\"\"\"\n    <div :type={Phoenix.LiveViewTest.Support.RootTagAttr.RootTagsWithValuesMacroComponent}></div>\n    <div>\n      <div>\n        <.inner_block_and_slot>\n          <div>Inner Block</div>\n          <:test>\n            <div>\n              Named Slot\n            </div>\n          </:test>\n        </.inner_block_and_slot>\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  def macro_component_attrs_without_values(assigns) do\n    ~H\"\"\"\n    <div :type={Phoenix.LiveViewTest.Support.RootTagAttr.RootTagsWithoutValuesMacroComponent}></div>\n    <div>\n      <div>\n        <.inner_block_and_slot>\n          <div>Inner Block</div>\n          <:test>\n            <div>\n              Named Slot\n            </div>\n          </:test>\n        </.inner_block_and_slot>\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  slot :inner_block, required: true\n  slot :test\n\n  def single_self_close(assigns) do\n    ~H\"\"\"\n    <div />\n    \"\"\"\n  end\n\n  def single_with_body(assigns) do\n    ~H\"\"\"\n    <div>Test</div>\n    \"\"\"\n  end\n\n  def multiple_self_close(assigns) do\n    ~H\"\"\"\n    <div />\n    <div />\n    <div />\n    \"\"\"\n  end\n\n  def multiple_with_bodies(assigns) do\n    ~H\"\"\"\n    <div>Test1</div>\n    <div>Test2</div>\n    <div>Test3</div>\n    \"\"\"\n  end\n\n  def nested_tags(assigns) do\n    ~H\"\"\"\n    <div>\n      <div>\n        <div></div>\n      </div>\n      <div>\n        <div></div>\n      </div>\n    </div>\n    <div>\n      <div>\n        <div></div>\n      </div>\n      <div>\n        <div></div>\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  def component_inner_blocks(assigns) do\n    ~H\"\"\"\n    <div>\n      <div>\n        <.inner_block_and_slot>\n          <div>\n            <div>\n              Inner Block 1\n            </div>\n          </div>\n        </.inner_block_and_slot>\n        <.inner_block_and_slot>\n          <div>\n            <div>\n              Inner Block 2\n            </div>\n          </div>\n        </.inner_block_and_slot>\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  def component_named_slots(assigns) do\n    ~H\"\"\"\n    <div>\n      <div>\n        <.inner_block_and_slot>\n          <:test>\n            <div>\n              <div>\n                Inner Block 1\n              </div>\n            </div>\n          </:test>\n        </.inner_block_and_slot>\n        <.inner_block_and_slot>\n          <:test>\n            <div>\n              <div>\n                Inner Block 2\n              </div>\n            </div>\n          </:test>\n        </.inner_block_and_slot>\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  def nested_tags_components_slots(assigns) do\n    ~H\"\"\"\n    <div>\n      <div>\n        <.inner_block_and_slot>\n          <div>\n            <.inner_block_and_slot>\n              <div>\n                <.simple />\n              </div>\n              <:test>\n                <div>\n                  <.simple />\n                </div>\n              </:test>\n            </.inner_block_and_slot>\n          </div>\n          <:test>\n            <div>\n              <.inner_block_and_slot>\n                <div>\n                  <.simple />\n                </div>\n                <:test>\n                  <div>\n                    <.simple />\n                  </div>\n                </:test>\n              </.inner_block_and_slot>\n            </div>\n          </:test>\n        </.inner_block_and_slot>\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  slot :inner_block, required: true\n  slot :test\n\n  defp inner_block_and_slot(assigns) do\n    ~H\"\"\"\n    <section>\n      {render_slot(@inner_block)}\n      <aside :for={test <- @test}>\n        {render_slot(@test)}\n      </aside>\n    </section>\n    \"\"\"\n  end\n\n  defp simple(assigns) do\n    ~H\"\"\"\n    <p>Simple</p>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/start_async.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.StartAsyncLive do\n  use Phoenix.LiveView\n\n  import Phoenix.LiveViewTest.Support.AsyncSync\n\n  on_mount({__MODULE__, :defaults})\n\n  def on_mount(:defaults, _params, _session, socket) do\n    {:cont, assign(socket, lc: false)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <.live_component\n      :if={@lc}\n      module={Phoenix.LiveViewTest.Support.StartAsyncLive.LC}\n      test={@lc}\n      id=\"lc\"\n    /> result: {inspect(@result)}\n    <%= if flash = @flash[\"info\"] do %>\n      flash: {flash}\n    <% end %>\n    \"\"\"\n  end\n\n  def mount(%{\"test\" => \"lc_\" <> lc_test}, _session, socket) do\n    {:ok, assign(socket, lc: lc_test, result: :loading)}\n  end\n\n  def mount(%{\"test\" => \"ok\"}, _session, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:result_task, fn -> :good end)}\n  end\n\n  def mount(%{\"test\" => \"raise\"}, _session, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:result_task, fn -> raise(\"boom\") end)}\n  end\n\n  def mount(%{\"test\" => \"exit\"}, _session, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:result_task, fn -> exit(:boom) end)}\n  end\n\n  def mount(%{\"test\" => \"lv_exit\"}, _session, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:result_task, fn ->\n       register_and_sleep(:start_async_test_process, :start_async_exit)\n     end)}\n  end\n\n  def mount(%{\"test\" => \"cancel\"}, _session, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:result_task, fn ->\n       register_and_sleep(:start_async_test_process, :start_async_cancel)\n     end)}\n  end\n\n  def mount(%{\"test\" => \"trap_exit\"}, _session, socket) do\n    Process.flag(:trap_exit, true)\n\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:result_task, fn ->\n       spawn_link(fn -> exit(:boom) end)\n       Process.sleep(100)\n       :good\n     end)}\n  end\n\n  def mount(%{\"test\" => \"complex_key\"}, _session, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async({:result_task, :foo}, fn -> :complex_key end)}\n  end\n\n  def mount(%{\"test\" => \"navigate\"}, _session, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:navigate, fn -> nil end)}\n  end\n\n  def mount(%{\"test\" => \"patch\"}, _session, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:patch, fn -> nil end)}\n  end\n\n  def mount(%{\"test\" => \"redirect\"}, _session, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:redirect, fn -> nil end)}\n  end\n\n  def mount(%{\"test\" => \"put_flash\"}, _session, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:flash, fn -> \"hello\" end)}\n  end\n\n  def mount(%{\"test\" => \"socket_warning\"}, _session, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:result_task, function_that_returns_the_anonymous_function(socket))}\n  end\n\n  defp function_that_returns_the_anonymous_function(socket) do\n    fn ->\n      Function.identity(socket)\n      :ok\n    end\n  end\n\n  def handle_params(_unsigned_params, _uri, socket) do\n    {:noreply, socket}\n  end\n\n  def handle_async(:result_task, {:ok, result}, socket) do\n    {:noreply, assign(socket, result: result)}\n  end\n\n  def handle_async(:result_task, {:exit, {error, [_ | _] = _stack}}, socket) do\n    {:noreply, assign(socket, result: {:exit, error})}\n  end\n\n  def handle_async(:result_task, {:exit, reason}, socket) do\n    {:noreply, assign(socket, result: {:exit, reason})}\n  end\n\n  def handle_async({:result_task, _}, {:ok, result}, socket) do\n    {:noreply, assign(socket, result: result)}\n  end\n\n  def handle_async(:navigate, {:ok, _result}, socket) do\n    {:noreply, push_navigate(socket, to: \"/start_async?test=ok\")}\n  end\n\n  def handle_async(:patch, {:ok, _result}, socket) do\n    {:noreply, push_patch(socket, to: \"/start_async?test=ok\")}\n  end\n\n  def handle_async(:redirect, {:ok, _result}, socket) do\n    {:noreply, redirect(socket, to: \"/not_found\")}\n  end\n\n  def handle_async(:flash, {:ok, flash}, socket) do\n    {:noreply, put_flash(socket, :info, flash)}\n  end\n\n  def handle_info(:boom, _socket), do: exit(:boom)\n\n  def handle_info(:cancel, socket) do\n    {:noreply, cancel_async(socket, :result_task)}\n  end\n\n  def handle_info(:renew_canceled, socket) do\n    {:noreply,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:result_task, fn ->\n       Process.sleep(100)\n       :renewed\n     end)}\n  end\n\n  def handle_info({:EXIT, pid, reason}, socket) do\n    send(:start_async_trap_exit_test, {:exit, pid, reason})\n    {:noreply, socket}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.StartAsyncLive.TrapExitLeak do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"<div>{@result}</div>\"\n  end\n\n  def mount(_params, _session, socket) do\n    Process.flag(:trap_exit, true)\n    {:ok, socket |> assign(result: :loading) |> start_async(:task, fn -> :done end)}\n  end\n\n  def handle_async(:task, {:ok, _result}, socket) do\n    {:noreply, assign(socket, result: :complete)}\n  end\n\n  # handle_info that deliberately doesn't handle {:EXIT, _, _}\n  def handle_info(:noop, socket) do\n    {:noreply, socket}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.StartAsyncLive.LC do\n  use Phoenix.LiveComponent\n\n  import Phoenix.LiveViewTest.Support.AsyncSync\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      lc: {inspect(@result)}\n    </div>\n    \"\"\"\n  end\n\n  def update(%{test: \"ok\"}, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:result_task, fn -> :good end)}\n  end\n\n  def update(%{test: \"raise\"}, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:result_task, fn -> raise(\"boom\") end)}\n  end\n\n  def update(%{test: \"exit\"}, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:result_task, fn -> exit(:boom) end)}\n  end\n\n  def update(%{test: \"lv_exit\"}, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:result_task, fn ->\n       register_and_sleep(:start_async_test_process, :start_async_exit)\n     end)}\n  end\n\n  def update(%{test: \"cancel\"}, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:result_task, fn ->\n       register_and_sleep(:start_async_test_process, :start_async_cancel)\n     end)}\n  end\n\n  def update(%{test: \"complex_key\"}, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async({:result_task, :foo}, fn -> :complex_key end)}\n  end\n\n  def update(%{test: \"patch\"}, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:patch, fn -> nil end)}\n  end\n\n  def update(%{test: \"navigate\"}, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:navigate, fn -> nil end)}\n  end\n\n  def update(%{test: \"redirect\"}, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:redirect, fn -> nil end)}\n  end\n\n  def update(%{test: \"navigate_flash\"}, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:navigate_flash, fn -> \"hello\" end)}\n  end\n\n  def update(%{action: :cancel}, socket) do\n    {:ok, cancel_async(socket, :result_task)}\n  end\n\n  def update(%{action: :renew_canceled}, socket) do\n    {:ok,\n     socket\n     |> assign(result: :loading)\n     |> start_async(:result_task, fn ->\n       Process.sleep(100)\n       :renewed\n     end)}\n  end\n\n  def handle_async(:result_task, {:ok, result}, socket) do\n    {:noreply, assign(socket, result: result)}\n  end\n\n  def handle_async(:result_task, {:exit, {error, [_ | _] = _stack}}, socket) do\n    {:noreply, assign(socket, result: {:exit, error})}\n  end\n\n  def handle_async(:result_task, {:exit, reason}, socket) do\n    {:noreply, assign(socket, result: {:exit, reason})}\n  end\n\n  def handle_async({:result_task, _}, {:ok, result}, socket) do\n    {:noreply, assign(socket, result: result)}\n  end\n\n  def handle_async(:navigate, {:ok, _result}, socket) do\n    {:noreply, push_navigate(socket, to: \"/start_async?test=ok\")}\n  end\n\n  def handle_async(:patch, {:ok, _result}, socket) do\n    {:noreply, push_patch(socket, to: \"/start_async?test=ok\")}\n  end\n\n  def handle_async(:redirect, {:ok, _result}, socket) do\n    {:noreply, redirect(socket, to: \"/not_found\")}\n  end\n\n  def handle_async(:navigate_flash, {:ok, flash}, socket) do\n    {:noreply, socket |> put_flash(:info, flash) |> push_navigate(to: \"/start_async?test=ok\")}\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/stream_async.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.StreamAsyncLive do\n  use Phoenix.LiveView\n\n  import Phoenix.LiveViewTest.Support.AsyncSync\n\n  on_mount({__MODULE__, :defaults})\n\n  def on_mount(:defaults, params, _session, socket) do\n    socket = socket |> assign(:lc, false)\n\n    if params[\"no_init\"] do\n      {:cont, socket}\n    else\n      {:cont, stream(socket, :my_stream, [%{id: 0, name: \"Initial\"}])}\n    end\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <.live_component\n      :if={@lc}\n      module={Phoenix.LiveViewTest.Support.StreamAsyncLive.LC}\n      test={@lc}\n      id=\"lc\"\n    />\n    <.async_result assign={@my_stream}>\n      <:loading>my_stream loading...</:loading>\n      <:failed :let={{kind, reason}}>{kind}: {inspect(reason)}</:failed>\n      stream loaded!\n    </.async_result>\n    <ul id=\"my-stream\" phx-update=\"stream\">\n      <li :for={{id, item} <- @streams.my_stream} id={id}>{item.name}</li>\n    </ul>\n    \"\"\"\n  end\n\n  def mount(%{\"test\" => \"lc_\" <> lc_test}, _session, socket) do\n    {:ok,\n     socket\n     |> assign(lc: lc_test)\n     |> stream_async(:my_stream, fn -> {:ok, [%{id: 1, name: \"lc_item\"}]} end)}\n  end\n\n  def mount(%{\"test\" => \"bad_return\"}, _session, socket) do\n    {:ok, stream_async(socket, :my_stream, fn -> 123 end)}\n  end\n\n  def mount(%{\"test\" => \"bad_ok\"}, _session, socket) do\n    {:ok, stream_async(socket, :my_stream, fn -> {:ok, \"not enumerable\"} end)}\n  end\n\n  def mount(%{\"test\" => \"ok\"}, _session, socket) do\n    {:ok,\n     socket\n     |> stream_async(:my_stream, fn ->\n       {:ok, [%{id: 1, name: \"First\"}, %{id: 2, name: \"Second\"}]}\n     end)}\n  end\n\n  def mount(%{\"test\" => \"ok_with_opts\"}, _session, socket) do\n    {:ok,\n     socket\n     |> stream_async(:my_stream, fn ->\n       {:ok, [%{id: 1, name: \"First\"}, %{id: 2, name: \"Second\"}], at: 0}\n     end)}\n  end\n\n  def mount(%{\"test\" => \"ok_with_reset\"}, _session, socket) do\n    {:ok,\n     socket\n     |> stream_async(:my_stream, fn ->\n       {:ok, [%{id: 1, name: \"First\"}, %{id: 2, name: \"Second\"}], reset: true}\n     end)}\n  end\n\n  def mount(%{\"test\" => \"error\"}, _session, socket) do\n    {:ok, stream_async(socket, :my_stream, fn -> {:error, :something_wrong} end)}\n  end\n\n  def mount(%{\"test\" => \"raise\"}, _session, socket) do\n    {:ok, stream_async(socket, :my_stream, fn -> raise(\"boom\") end)}\n  end\n\n  def mount(%{\"test\" => \"exit\"}, _session, socket) do\n    {:ok, stream_async(socket, :my_stream, fn -> exit(:boom) end)}\n  end\n\n  def mount(%{\"test\" => \"lv_exit\"}, _session, socket) do\n    {:ok,\n     stream_async(socket, :my_stream, fn ->\n       register_and_sleep(:stream_async_test_process, :stream_async_exit)\n     end)}\n  end\n\n  def mount(%{\"test\" => \"cancel\"}, _session, socket) do\n    {:ok,\n     stream_async(socket, :my_stream, fn ->\n       register_and_sleep(:stream_async_test_process, :cancel_stream)\n     end)}\n  end\n\n  def mount(%{\"test\" => \"reset_option\"}, _session, socket) do\n    {:ok,\n     socket\n     |> stream_async(\n       :my_stream,\n       fn ->\n         Process.sleep(10)\n         {:ok, [%{id: 1, name: \"First\"}]}\n       end,\n       reset: true\n     )}\n  end\n\n  def handle_info(:boom, _socket), do: exit(:boom)\n\n  def handle_info(:cancel, socket) do\n    {:noreply, cancel_async(socket, :my_stream)}\n  end\n\n  def handle_info(:renew_canceled, socket) do\n    {:noreply,\n     stream_async(\n       socket,\n       :my_stream,\n       fn ->\n         Process.sleep(10)\n         {:ok, [%{id: 1, name: \"renewed\"}]}\n       end,\n       reset: true\n     )}\n  end\n\n  def handle_info(:add_items, socket) do\n    {:noreply,\n     stream_async(socket, :my_stream, fn ->\n       {:ok, [%{id: 3, name: \"Third\"}, %{id: 4, name: \"Fourth\"}]}\n     end)}\n  end\n\n  def handle_info(:reset_items, socket) do\n    {:noreply,\n     stream_async(socket, :my_stream, fn ->\n       {:ok, [%{id: 5, name: \"Fifth\"}, %{id: 6, name: \"Sixth\"}], reset: true}\n     end)}\n  end\n\n  def handle_info({:cancel_lc, id}, socket) do\n    send_update(Phoenix.LiveViewTest.Support.StreamAsyncLive.LC, id: id, action: :cancel)\n    {:noreply, socket}\n  end\n\n  def handle_info(_msg, socket), do: {:noreply, socket}\nend\n\ndefmodule Phoenix.LiveViewTest.Support.StreamAsyncLive.LC do\n  use Phoenix.LiveComponent\n\n  import Phoenix.LiveViewTest.Support.AsyncSync\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      <.async_result :let={_} assign={@lc_stream}>\n        <:loading>lc_stream loading...</:loading>\n        <:failed :let={{kind, reason}}>{kind}: {inspect(reason)}</:failed>\n        <ul id=\"lc-stream\" phx-update=\"stream\">\n          <li :for={{id, item} <- @streams.lc_stream} id={id}>lc: {item.name}</li>\n        </ul>\n      </.async_result>\n    </div>\n    \"\"\"\n  end\n\n  def update(%{test: \"bad_return\"}, socket) do\n    {:ok, stream_async(socket, :lc_stream, fn -> 123 end)}\n  end\n\n  def update(%{test: \"bad_ok\"}, socket) do\n    {:ok, stream_async(socket, :lc_stream, fn -> {:ok, \"not enumerable\"} end)}\n  end\n\n  def update(%{test: \"ok\"}, socket) do\n    {:ok,\n     socket\n     |> stream_async(:lc_stream, fn ->\n       {:ok, [%{id: 1, name: \"LC First\"}, %{id: 2, name: \"LC Second\"}]}\n     end)}\n  end\n\n  def update(%{test: \"raise\"}, socket) do\n    {:ok, stream_async(socket, :lc_stream, fn -> raise(\"boom\") end)}\n  end\n\n  def update(%{test: \"exit\"}, socket) do\n    {:ok, stream_async(socket, :lc_stream, fn -> exit(:boom) end)}\n  end\n\n  def update(%{test: \"lv_exit\"}, socket) do\n    {:ok,\n     stream_async(socket, :lc_stream, fn ->\n       register_and_sleep(:stream_async_test_process, :lc_stream_exit)\n     end)}\n  end\n\n  def update(%{test: \"cancel\"}, socket) do\n    {:ok,\n     stream_async(socket, :lc_stream, fn ->\n       register_and_sleep(:stream_async_test_process, :lc_stream_cancel)\n     end)}\n  end\n\n  def update(%{action: :cancel}, socket) do\n    {:ok, cancel_async(socket, :lc_stream)}\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/streams.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.StreamLive do\n  use Phoenix.LiveView\n\n  def run(lv, func) do\n    GenServer.call(lv.pid, {:run, func})\n  end\n\n  def render(%{invalid_consume: true} = assigns) do\n    ~H\"\"\"\n    <div :for={{id, _user} <- Enum.map(@streams.users, & &1)} id={id} />\n    \"\"\"\n  end\n\n  def render(%{no_id: true} = assigns) do\n    ~H\"\"\"\n    <div id=\"users\" phx-update=\"stream\">\n      <div only-child>Empty!</div>\n      <div :for={{id, _user} <- @streams.users} id={id} />\n    </div>\n\n    <style>\n      [only-child] {\n        display: none;\n      }\n      [only-child]:only-child {\n        display: block;\n      }\n    </style>\n    \"\"\"\n  end\n\n  def render(%{extra_item_with_id: true} = assigns) do\n    ~H\"\"\"\n    <div id=\"users\" phx-update=\"stream\">\n      <div :for={{id, user} <- @streams.users} id={id}>{user.name}</div>\n      <div id=\"users-empty\" only-child>Empty!</div>\n    </div>\n\n    <style>\n      [only-child] {\n        display: none;\n      }\n      [only-child]:only-child {\n        display: block;\n      }\n    </style>\n    \"\"\"\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div id=\"users\" phx-update=\"stream\">\n      <div :for={{id, user} <- @streams.users} id={id} data-count={@count}>\n        {user.name}\n        <button phx-click=\"delete\" phx-value-id={id}>delete</button>\n        <button phx-click=\"update\" phx-value-id={id}>update</button>\n        <button phx-click=\"move-to-first\" phx-value-id={id}>make first</button>\n        <button phx-click=\"move-to-last\" phx-value-id={id}>make last</button>\n        <button phx-click=\"move\" phx-value-id={id} phx-value-name=\"moved\" phx-value-at=\"1\">\n          move\n        </button>\n        <button phx-click={Phoenix.LiveView.JS.hide(to: \"##{id}\")}>JS Hide</button>\n      </div>\n    </div>\n    <div id=\"admins\" phx-update=\"stream\">\n      <div :for={{id, user} <- @streams.admins} id={id} data-count={@count}>\n        {user.name}\n        <button phx-click=\"admin-delete\" phx-value-id={id}>delete</button>\n        <button phx-click=\"admin-update\" phx-value-id={id}>update</button>\n        <button phx-click=\"admin-move-to-first\" phx-value-id={id}>make first</button>\n        <button phx-click=\"admin-move-to-last\" phx-value-id={id}>make last</button>\n      </div>\n    </div>\n    <.live_component id=\"stream-component\" module={Phoenix.LiveViewTest.Support.StreamComponent} />\n\n    <button phx-click=\"reset-users\">Reset users</button>\n    <button phx-click=\"reset-users-reorder\">Reorder users</button>\n    \"\"\"\n  end\n\n  @users [\n    %{id: 1, name: \"chris\"},\n    %{id: 2, name: \"callan\"}\n  ]\n\n  @append_users [\n    %{id: 4, name: \"foo\"},\n    %{id: 3, name: \"last_user\"}\n  ]\n\n  def mount(params, _session, socket) do\n    {:ok,\n     socket\n     |> assign(:invalid_consume, false)\n     |> assign(:no_id, false)\n     |> assign(:extra_item_with_id, Map.has_key?(params, \"empty_item\"))\n     |> assign(:count, 0)\n     |> stream(:users, @users)\n     |> stream(:admins, [user(1, \"chris-admin\"), user(2, \"callan-admin\")])}\n  end\n\n  def handle_event(\"delete\", %{\"id\" => dom_id}, socket) do\n    {:noreply, stream_delete_by_dom_id(socket, :users, dom_id)}\n  end\n\n  def handle_event(\"update\", %{\"id\" => \"users-\" <> id}, socket) do\n    {:noreply, stream_insert(socket, :users, user(id, \"updated\"))}\n  end\n\n  def handle_event(\"move-to-first\", %{\"id\" => \"users-\" <> id}, socket) do\n    {:noreply,\n     socket\n     |> stream_delete_by_dom_id(:users, \"users-\" <> id)\n     |> stream_insert(:users, user(id, \"updated\"), at: 0)}\n  end\n\n  def handle_event(\"move-to-last\", %{\"id\" => \"users-\" <> id = dom_id}, socket) do\n    user = user(id, \"updated\")\n\n    {:noreply,\n     socket\n     |> stream_delete_by_dom_id(:users, dom_id)\n     |> stream_insert(:users, user, at: -1)}\n  end\n\n  def handle_event(\"move\", %{\"id\" => \"users-\" <> id = dom_id, \"name\" => name, \"at\" => at}, socket) do\n    at = String.to_integer(at)\n    user = user(id, name)\n\n    {:noreply,\n     socket\n     |> stream_delete_by_dom_id(:users, dom_id)\n     |> stream_insert(:users, user, at: at)}\n  end\n\n  def handle_event(\"reset-users\", _, socket) do\n    {:noreply, socket |> update(:count, &(&1 + 1)) |> stream(:users, [], reset: true)}\n  end\n\n  def handle_event(\"reset-users-reorder\", %{}, socket) do\n    {:noreply,\n     socket\n     |> update(:count, &(&1 + 1))\n     |> stream(:users, [user(3, \"peter\"), user(1, \"chris\"), user(4, \"mona\")], reset: true)}\n  end\n\n  def handle_event(\"stream-users\", _, socket) do\n    {:noreply, stream(socket, :users, @users)}\n  end\n\n  def handle_event(\"append-users\", _, socket) do\n    {:noreply, stream(socket, :users, @append_users, at: -1)}\n  end\n\n  def handle_event(\"admin-delete\", %{\"id\" => dom_id}, socket) do\n    {:noreply, stream_delete_by_dom_id(socket, :admins, dom_id)}\n  end\n\n  def handle_event(\"admin-update\", %{\"id\" => \"admins-\" <> id}, socket) do\n    {:noreply, stream_insert(socket, :admins, user(id, \"updated\"))}\n  end\n\n  def handle_event(\"admin-move-to-first\", %{\"id\" => \"admins-\" <> id}, socket) do\n    {:noreply,\n     socket\n     |> stream_delete_by_dom_id(:admins, \"admins-\" <> id)\n     |> stream_insert(:admins, user(id, \"updated\"), at: 0)}\n  end\n\n  def handle_event(\"admin-move-to-last\", %{\"id\" => \"admins-\" <> id = dom_id}, socket) do\n    user = user(id, \"updated\")\n\n    {:noreply,\n     socket\n     |> stream_delete_by_dom_id(:admins, dom_id)\n     |> stream_insert(:admins, user, at: -1)}\n  end\n\n  def handle_event(\"consume-stream-invalid\", _, socket) do\n    {:noreply, assign(socket, :invalid_consume, true)}\n  end\n\n  def handle_event(\"stream-no-id\", _, socket) do\n    {:noreply, assign(socket, :no_id, true) |> stream(:users, @users)}\n  end\n\n  def handle_event(\"stream-extra-with-id\", _, socket) do\n    {:noreply, assign(socket, :extra_item_with_id, true) |> stream(:users, @users)}\n  end\n\n  def handle_call({:run, func}, _, socket), do: func.(socket)\n\n  defp user(id, name) do\n    %{id: id, name: name}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.StreamComponent do\n  use Phoenix.LiveComponent\n\n  def run(lv, func) do\n    GenServer.call(lv.pid, {:run, func})\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div id=\"c_users\" phx-update=\"stream\">\n      <div :for={{id, user} <- @streams.c_users} id={id}>\n        {user.name}\n        <button phx-click=\"delete\" phx-value-id={id} phx-target={@myself}>delete</button>\n        <button phx-click=\"update\" phx-value-id={id} phx-target={@myself}>update</button>\n        <button phx-click=\"move-to-first\" phx-value-id={id} phx-target={@myself}>make first</button>\n        <button phx-click=\"move-to-last\" phx-value-id={id} phx-target={@myself}>make last</button>\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  def update(%{reset: {stream, collection}}, socket) do\n    {:ok, stream(socket, stream, collection, reset: true)}\n  end\n\n  def update(%{send_assigns_to: test_pid}, socket) when is_pid(test_pid) do\n    send(test_pid, {:assigns, socket.assigns})\n    {:ok, socket}\n  end\n\n  def update(_assigns, socket) do\n    users = [user(1, \"chris\"), user(2, \"callan\")]\n    {:ok, stream(socket, :c_users, users)}\n  end\n\n  def handle_event(\"reset\", %{}, socket) do\n    {:noreply, stream(socket, :c_users, [], reset: true)}\n  end\n\n  def handle_event(\"delete\", %{\"id\" => dom_id}, socket) do\n    {:noreply, stream_delete_by_dom_id(socket, :c_users, dom_id)}\n  end\n\n  def handle_event(\"update\", %{\"id\" => \"c_users-\" <> id}, socket) do\n    {:noreply, stream_insert(socket, :c_users, user(id, \"updated\"))}\n  end\n\n  def handle_event(\"move-to-first\", %{\"id\" => \"c_users-\" <> id}, socket) do\n    {:noreply,\n     socket\n     |> stream_delete_by_dom_id(:c_users, \"c_users-\" <> id)\n     |> stream_insert(:c_users, user(id, \"updated\"), at: 0)}\n  end\n\n  def handle_event(\"move-to-last\", %{\"id\" => \"c_users-\" <> id = dom_id}, socket) do\n    user = user(id, \"updated\")\n\n    {:noreply,\n     socket\n     |> stream_delete_by_dom_id(:c_users, dom_id)\n     |> stream_insert(:c_users, user, at: -1)}\n  end\n\n  defp user(id, name) do\n    %{id: id, name: name}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.HealthyLive do\n  use Phoenix.LiveView\n\n  @healthy_stuff %{\n    \"fruits\" => [\n      %{id: 1, name: \"Apples\"},\n      %{id: 2, name: \"Oranges\"}\n    ],\n    \"veggies\" => [\n      %{id: 3, name: \"Carrots\"},\n      %{id: 4, name: \"Tomatoes\"}\n    ]\n  }\n\n  def render(assigns) do\n    ~H\"\"\"\n    <p>\n      <.link patch={other(@category)}>Switch</.link>\n    </p>\n\n    <h1>{String.capitalize(@category)}</h1>\n\n    <ul id=\"items\" phx-update=\"stream\">\n      <li :for={{dom_id, item} <- @streams.items} id={dom_id}>\n        {item.name}\n      </li>\n    </ul>\n    \"\"\"\n  end\n\n  defp other(\"fruits\" = _current_category) do\n    \"/healthy/veggies\"\n  end\n\n  defp other(\"veggies\" = _current_category) do\n    \"/healthy/fruits\"\n  end\n\n  def mount(%{\"category\" => category} = _params, _session, socket) do\n    socket =\n      socket\n      |> assign(:category, category)\n      |> stream(:items, [])\n\n    {:ok, socket}\n  end\n\n  def handle_params(%{\"category\" => category} = _params, _url, socket) do\n    socket =\n      socket\n      |> assign(:category, category)\n      |> stream(:items, Map.fetch!(@healthy_stuff, category), reset: true)\n\n    {:noreply, socket}\n  end\n\n  def handle_event(\"load-more\", %{}, socket) do\n    new_items = [\n      %{id: 5, name: \"Pumpkins\"},\n      %{id: 6, name: \"Melons\"}\n    ]\n\n    {:noreply,\n     socket\n     |> stream(:items, new_items, at: -1)}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.StreamResetLive do\n  use Phoenix.LiveView\n\n  # see https://github.com/phoenixframework/phoenix_live_view/issues/2994\n\n  def mount(params, _session, socket) do\n    socket\n    |> stream(:items, [\n      %{id: \"a\", name: \"A\"},\n      %{id: \"b\", name: \"B\"},\n      %{id: \"c\", name: \"C\"},\n      %{id: \"d\", name: \"D\"}\n    ])\n    |> assign(:use_phx_remove, is_map(params) && params[\"phx-remove\"])\n    |> then(&{:ok, &1})\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <ul phx-update=\"stream\" id=\"thelist\">\n      <li\n        :for={{id, item} <- @streams.items}\n        id={id}\n        phx-remove={if @use_phx_remove, do: Phoenix.LiveView.JS.hide()}\n      >\n        {item.name}\n      </li>\n    </ul>\n\n    <button phx-click=\"filter\">Filter</button>\n    <button phx-click=\"reorder\">Reorder</button>\n    <button phx-click=\"reset\">Reset</button>\n    <button phx-click=\"prepend\">Prepend</button>\n    <button phx-click=\"append\">Append</button>\n    <button phx-click=\"bulk-insert\">Bulk insert</button>\n    <button phx-click=\"insert-at-one\">Insert at 1</button>\n    <button phx-click=\"insert-existing-at-one\">Insert C at 1</button>\n    <button phx-click=\"delete-insert-existing-at-one\">Delete C and insert at 1</button>\n    <button phx-click=\"prepend-existing\">Prepend C</button>\n    <button phx-click=\"append-existing\">Append C</button>\n    <button phx-click=\"new-update-only\">Add E (update only)</button>\n    <button phx-click=\"existing-update-only\">Update C (update only)</button>\n    \"\"\"\n  end\n\n  def handle_event(\"filter\", _, socket) do\n    {:noreply,\n     stream(\n       socket,\n       :items,\n       [\n         %{id: \"b\", name: \"B\"},\n         %{id: \"c\", name: \"C\"},\n         %{id: \"d\", name: \"D\"}\n       ],\n       reset: true\n     )}\n  end\n\n  def handle_event(\"reorder\", _, socket) do\n    {:noreply,\n     stream(\n       socket,\n       :items,\n       [\n         %{id: \"b\", name: \"B\"},\n         %{id: \"a\", name: \"A\"},\n         %{id: \"c\", name: \"C\"},\n         %{id: \"d\", name: \"D\"}\n       ],\n       reset: true\n     )}\n  end\n\n  def handle_event(\"reset\", _, socket) do\n    {:noreply,\n     stream(\n       socket,\n       :items,\n       [\n         %{id: \"a\", name: \"A\"},\n         %{id: \"b\", name: \"B\"},\n         %{id: \"c\", name: \"C\"},\n         %{id: \"d\", name: \"D\"}\n       ],\n       reset: true\n     )}\n  end\n\n  def handle_event(\"prepend\", _, socket) do\n    {:noreply,\n     stream_insert(\n       socket,\n       :items,\n       %{id: \"a\" <> \"#{System.unique_integer()}\", name: \"#{System.unique_integer()}\"},\n       at: 0\n     )}\n  end\n\n  def handle_event(\"append\", _, socket) do\n    {:noreply,\n     stream_insert(\n       socket,\n       :items,\n       %{id: \"a\" <> \"#{System.unique_integer()}\", name: \"#{System.unique_integer()}\"},\n       at: -1\n     )}\n  end\n\n  def handle_event(\"bulk-insert\", _, socket) do\n    {:noreply,\n     stream(\n       socket,\n       :items,\n       Enum.reverse([\n         %{id: \"e\", name: \"E\"},\n         %{id: \"f\", name: \"F\"},\n         %{id: \"g\", name: \"G\"}\n       ]),\n       at: 1\n     )}\n  end\n\n  def handle_event(\"insert-at-one\", _, socket) do\n    {:noreply,\n     stream_insert(\n       socket,\n       :items,\n       %{id: \"a\" <> \"#{System.unique_integer()}\", name: \"#{System.unique_integer()}\"},\n       at: 1\n     )}\n  end\n\n  def handle_event(\"insert-existing-at-one\", _, socket) do\n    {:noreply,\n     stream_insert(\n       socket,\n       :items,\n       %{id: \"c\", name: \"C\"},\n       at: 1\n     )}\n  end\n\n  def handle_event(\"delete-insert-existing-at-one\", _, socket) do\n    {:noreply,\n     socket\n     |> stream_delete_by_dom_id(:items, \"items-c\")\n     |> stream_insert(\n       :items,\n       %{id: \"c\", name: \"C\"},\n       at: 1\n     )}\n  end\n\n  def handle_event(\"prepend-existing\", _, socket) do\n    {:noreply,\n     stream_insert(\n       socket,\n       :items,\n       %{id: \"c\", name: \"C\"},\n       at: 0\n     )}\n  end\n\n  def handle_event(\"append-existing\", _, socket) do\n    {:noreply,\n     stream_insert(\n       socket,\n       :items,\n       %{id: \"c\", name: \"C\"},\n       at: -1\n     )}\n  end\n\n  def handle_event(\"new-update-only\", _, socket) do\n    {:noreply, stream_insert(socket, :items, %{id: \"e\", name: \"E\"}, at: -1, update_only: true)}\n  end\n\n  def handle_event(\"existing-update-only\", _, socket) do\n    {:noreply,\n     stream_insert(socket, :items, %{id: \"c\", name: \"C #{System.unique_integer()}\"},\n       at: -1,\n       update_only: true\n     )}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.StreamResetLCLive do\n  use Phoenix.LiveView\n\n  # see https://github.com/phoenixframework/phoenix_live_view/issues/2982\n\n  defmodule InnerComponent do\n    use Phoenix.LiveComponent\n\n    def render(assigns) do\n      ~H\"\"\"\n      <li id={@id}>\n        {@item.name}\n      </li>\n      \"\"\"\n    end\n  end\n\n  def mount(_params, _session, socket) do\n    socket\n    |> stream(:items, [\n      %{id: \"a\", name: \"A\"},\n      %{id: \"b\", name: \"B\"},\n      %{id: \"c\", name: \"C\"},\n      %{id: \"d\", name: \"D\"}\n    ])\n    |> then(&{:ok, &1})\n  end\n\n  def handle_event(\"reorder\", _, socket) do\n    socket =\n      stream(\n        socket,\n        :items,\n        [\n          %{id: \"e\", name: \"E\"},\n          %{id: \"a\", name: \"A\"},\n          %{id: \"f\", name: \"F\"},\n          %{id: \"g\", name: \"G\"}\n        ],\n        reset: true\n      )\n\n    {:noreply, socket}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <ul phx-update=\"stream\" id=\"thelist\">\n      <.live_component\n        :for={{id, item} <- @streams.items}\n        module={InnerComponent}\n        id={id}\n        item={item}\n      />\n    </ul>\n\n    <button phx-click=\"reorder\">Reorder</button>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.StreamLimitLive do\n  use Phoenix.LiveView\n\n  # see https://github.com/phoenixframework/phoenix_live_view/issues/2686\n\n  def mount(_params, _session, socket) do\n    socket = stream_configure(socket, :items, [])\n\n    {:noreply, socket} = handle_event(\"configure\", %{\"at\" => \"-1\", \"limit\" => \"-5\"}, socket)\n    {:ok, socket}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <form phx-submit=\"configure\">\n      at: <input type=\"text\" name=\"at\" value={@at} /> limit:\n      <input type=\"text\" name=\"limit\" value={@limit} />\n      <button type=\"submit\">recreate stream</button>\n    </form>\n\n    <div>configured with at: {@at}, limit: {@limit}</div>\n\n    <button phx-click=\"insert_10\">add 10</button>\n    <button phx-click=\"insert_1\">add 1</button>\n    <button phx-click=\"clear\">clear</button>\n\n    <ul id=\"items\" phx-update=\"stream\">\n      <li :for={{id, item} <- @streams.items} id={id}>{item.id}</li>\n    </ul>\n    \"\"\"\n  end\n\n  def handle_event(\"configure\", %{\"at\" => at, \"limit\" => limit}, socket) do\n    socket =\n      socket\n      |> assign(limit: String.to_integer(limit), at: String.to_integer(at), last_id: 0)\n      |> new_stream()\n\n    {:noreply, socket}\n  end\n\n  def handle_event(\"insert_10\", _params, socket) do\n    %{limit: l, at: a, last_id: last_id} = socket.assigns\n    items = for n <- 1..10, do: %{id: last_id + n}\n    opts = [at: a, limit: l]\n\n    socket =\n      socket\n      |> assign(last_id: last_id + 10)\n      |> stream(:items, items, opts)\n\n    {:noreply, socket}\n  end\n\n  def handle_event(\"insert_1\", _params, socket) do\n    %{limit: l, at: a, last_id: last_id} = socket.assigns\n    item = %{id: last_id + 1}\n    opts = [at: a, limit: l]\n\n    socket =\n      socket\n      |> assign(last_id: last_id + 1)\n      |> stream_insert(:items, item, opts)\n\n    {:noreply, socket}\n  end\n\n  def handle_event(\"clear\", _params, socket) do\n    socket =\n      socket\n      |> assign(last_id: 0)\n      |> stream(:items, [], reset: true)\n\n    {:noreply, socket}\n  end\n\n  defp new_stream(socket) do\n    %{limit: l, at: a} = socket.assigns\n    items = for n <- 1..10, do: %{id: n}\n    opts = [reset: true, at: a, limit: l]\n\n    socket\n    |> assign(last_id: 10)\n    |> stream(:items, items, opts)\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.StreamNestedLive do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    :timer.send_interval(50, self(), :tick)\n\n    {:ok, assign(socket, :foo, 1)}\n  end\n\n  def handle_info(:tick, socket) do\n    {:noreply, update(socket, :foo, &(&1 + 1))}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div id=\"nested-container\">\n      {@foo}\n      {live_render(@socket, Phoenix.LiveViewTest.Support.StreamResetLive, id: \"nested\")}\n    </div>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.StreamInsideForLive do\n  # https://github.com/phoenixframework/phoenix_live_view/issues/3129\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    socket\n    |> stream(:items, [])\n    |> start_async(:foo, fn ->\n      Process.sleep(50)\n    end)\n    |> then(&{:ok, &1})\n  end\n\n  def handle_async(:foo, {:ok, _}, socket) do\n    {:noreply,\n     stream(socket, :items, [\n       %{id: \"a\", name: \"A\"},\n       %{id: \"b\", name: \"B\"},\n       %{id: \"c\", name: \"C\"},\n       %{id: \"d\", name: \"D\"}\n     ])}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div :for={_i <- [1]}>\n      <ul phx-update=\"stream\" id=\"thelist\">\n        <li :for={{id, item} <- @streams.items} id={id}>\n          {item.name}\n        </li>\n      </ul>\n    </div>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.StreamNestedComponentResetLive do\n  use Phoenix.LiveView\n\n  defmodule InnerComponent do\n    use Phoenix.LiveComponent\n\n    # we already initialized the stream\n    def update(assigns, %{assigns: %{id: _}} = socket) do\n      {:ok, assign(socket, assigns)}\n    end\n\n    # first mount\n    def update(assigns, socket) do\n      items =\n        if connected?(socket) do\n          [\n            %{id: assigns.id <> \"-a\", name: \"N-A\"},\n            %{id: assigns.id <> \"-b\", name: \"N-B\"},\n            %{id: assigns.id <> \"-c\", name: \"N-C\"},\n            %{id: assigns.id <> \"-d\", name: \"N-D\"}\n          ]\n        else\n          [\n            %{id: assigns.id <> \"-e\", name: \"N-E\"},\n            %{id: assigns.id <> \"-f\", name: \"N-F\"},\n            %{id: assigns.id <> \"-g\", name: \"N-G\"},\n            %{id: assigns.id <> \"-h\", name: \"N-H\"}\n          ]\n        end\n\n      socket\n      |> assign(assigns)\n      |> stream(:nested, items, reset: true)\n      |> then(&{:ok, &1})\n    end\n\n    def handle_event(\"reorder\", _, socket) do\n      socket =\n        stream(\n          socket,\n          :nested,\n          [\n            %{id: socket.assigns.id <> \"-e\", name: \"N-E\"},\n            %{id: socket.assigns.id <> \"-a\", name: \"N-A\"},\n            %{id: socket.assigns.id <> \"-f\", name: \"N-F\"},\n            %{id: socket.assigns.id <> \"-g\", name: \"N-G\"}\n          ],\n          reset: true\n        )\n\n      {:noreply, socket}\n    end\n\n    def render(assigns) do\n      ~H\"\"\"\n      <li id={@id}>\n        {@item.name}\n        <div id={@id <> \"-nested\"} phx-update=\"stream\" style=\"display: flex; gap: 4px;\">\n          <span :for={{id, item} <- @streams.nested} id={id}>{item.name}</span>\n        </div>\n        <button phx-click=\"reorder\" phx-target={@myself}>Reorder</button>\n      </li>\n      \"\"\"\n    end\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <ul phx-update=\"stream\" id=\"thelist\">\n      <.live_component\n        :for={{id, item} <- @streams.items}\n        module={InnerComponent}\n        id={id}\n        item={item}\n      />\n    </ul>\n\n    <button phx-click=\"reorder\" id=\"parent-reorder\">Reorder</button>\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    socket\n    |> stream(:items, [\n      %{id: \"a\", name: \"A\"},\n      %{id: \"b\", name: \"B\"},\n      %{id: \"c\", name: \"C\"},\n      %{id: \"d\", name: \"D\"}\n    ])\n    |> then(&{:ok, &1})\n  end\n\n  def handle_event(\"reorder\", _, socket) do\n    socket =\n      stream(\n        socket,\n        :items,\n        [\n          %{id: \"e\", name: \"E\"},\n          %{id: \"a\", name: \"A\"},\n          %{id: \"f\", name: \"F\"},\n          %{id: \"g\", name: \"G\"}\n        ],\n        reset: true\n      )\n\n    {:noreply, socket}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.HighFrequencyStreamAndNoStreamUpdatesLive do\n  use Phoenix.LiveView\n\n  def mount(_params, _session, socket) do\n    :timer.send_interval(50, self(), :tick)\n\n    {:ok, assign(socket, :foo, 1) |> stream(:items, [])}\n  end\n\n  def handle_info(:tick, socket) do\n    {:noreply, update(socket, :foo, &(&1 + 1))}\n  end\n\n  def handle_event(\"insert_item\", _, socket) do\n    {:noreply, stream_insert(socket, :items, %{id: System.unique_integer(), name: \"Item\"})}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div id=\"mystream\" phx-update=\"stream\">\n      <div :for={{id, item} <- @streams.items} id={id}>\n        {item.name}, {item.id}\n      </div>\n    </div>\n    <p>{@foo}</p>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/test_warnings.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.DuplicateIdLive do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div id=\"a\">\n      <div id=\"b\">\n        <div id=\"a\" />\n      </div>\n    </div>\n    \"\"\"\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.DynamicDuplicateComponentLive do\n  use Phoenix.LiveView\n\n  defmodule LiveComponent do\n    use Phoenix.LiveComponent\n\n    def mount(socket) do\n      {:ok, socket}\n    end\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div>\n        <.live_component\n          :if={@render_child}\n          module={Phoenix.LiveViewTest.Support.DynamicDuplicateComponentLive.LiveComponent2}\n          id=\"duplicate\"\n        /> Other content of LiveComponent {@id}\n      </div>\n      \"\"\"\n    end\n  end\n\n  defmodule LiveComponent2 do\n    use Phoenix.LiveComponent\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div>I am LiveComponent2</div>\n      \"\"\"\n    end\n  end\n\n  defmodule NestedLive do\n    use Phoenix.LiveView\n\n    def render(assigns) do\n      ~H\"\"\"\n      <.live_component\n        module={Phoenix.LiveViewTest.Support.DynamicDuplicateComponentLive.LiveComponent3}\n        id=\"inside-nested\"\n      />\n      \"\"\"\n    end\n  end\n\n  defmodule LiveComponent3 do\n    use Phoenix.LiveComponent\n\n    def render(assigns) do\n      ~H\"\"\"\n      <div>I am a LC inside nested LV</div>\n      \"\"\"\n    end\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, render_first: true, render_second: true, render_duplicate: false)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <.live_component\n      id=\"First\"\n      module={Phoenix.LiveViewTest.Support.DynamicDuplicateComponentLive.LiveComponent}\n      render_child={true}\n    />\n    <.live_component\n      id=\"Second\"\n      module={Phoenix.LiveViewTest.Support.DynamicDuplicateComponentLive.LiveComponent}\n      render_child={@render_duplicate}\n    />\n\n    {live_render(@socket, Phoenix.LiveViewTest.Support.DynamicDuplicateComponentLive.NestedLive,\n      id: \"nested\"\n    )}\n\n    <button phx-click=\"toggle_duplicate\">Toggle duplicate LC</button>\n    \"\"\"\n  end\n\n  def handle_event(\"toggle_duplicate\", _, socket) do\n    {:noreply, assign(socket, :render_duplicate, !socket.assigns.render_duplicate)}\n  end\n\n  def handle_event(\"toggle_first\", _, socket) do\n    {:noreply, assign(socket, :render_first, !socket.assigns.render_first)}\n  end\n\n  def handle_event(\"toggle_second\", _, socket) do\n    {:noreply, assign(socket, :render_second, !socket.assigns.render_second)}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.FormMissingIdLive do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    <form phx-change=\"foo\" phx-submit=\"bar\"></form>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/update.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.TZLive do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    time: {@time} {@name}\n    \"\"\"\n  end\n\n  def mount(:not_mounted_at_router, session, socket) do\n    {:ok, assign(socket, time: \"12:00\", items: [], name: session[\"name\"] || \"NY\")}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.ShuffleLive do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    <%= for zone <- @time_zones do %>\n      <div id={\"score-\" <> zone[\"id\"]}>\n        {live_render(@socket, Phoenix.LiveViewTest.Support.TZLive,\n          id: \"tz-#{zone[\"id\"]}\",\n          session: %{\"name\" => zone[\"name\"]}\n        )}\n      </div>\n    <% end %>\n    \"\"\"\n  end\n\n  def mount(_params, %{\"time_zones\" => time_zones}, socket) do\n    {:ok, assign(socket, time_zones: time_zones)}\n  end\n\n  def handle_event(\"reverse\", _, socket) do\n    {:noreply, assign(socket, :time_zones, Enum.reverse(socket.assigns.time_zones))}\n  end\nend\n"
  },
  {
    "path": "test/support/live_views/upload_live.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.UploadLive do\n  use Phoenix.LiveView\n\n  def render(%{uploads: _} = assigns) do\n    ~H\"\"\"\n    <%= for preflight <- @preflights do %>\n      preflight:{inspect(preflight)}\n    <% end %>\n    <%= for name <- @consumed do %>\n      consumed:{name}\n    <% end %>\n    <form phx-change=\"validate\" phx-submit=\"save\">\n      <%= for entry <- @uploads.avatar.entries do %>\n        {@prefix}:{entry.client_name}:{entry.progress}%\n        channel:{inspect(Phoenix.LiveView.UploadConfig.entry_pid(@uploads.avatar, entry))}\n        <%= for msg <- upload_errors(@uploads.avatar) do %>\n          config_error:{inspect(msg)}\n        <% end %>\n        <%= for msg <- upload_errors(@uploads.avatar, entry) do %>\n          entry_error:{inspect(msg)}\n        <% end %>\n        relative path:{entry.client_relative_path}\n      <% end %>\n      <.live_file_input upload={@uploads.avatar} />\n      <button type=\"submit\">save</button>\n    </form>\n    \"\"\"\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      loading...\n    </div>\n    \"\"\"\n  end\n\n  def mount(_params, session, socket) do\n    prefix =\n      case session do\n        %{\"prefix\" => prefix} -> prefix\n        _ -> \"lv\"\n      end\n\n    {:ok, assign(socket, preflights: [], consumed: [], prefix: prefix)}\n  end\n\n  def handle_call({:setup, setup_func}, _from, socket) do\n    {:reply, :ok, setup_func.(socket)}\n  end\n\n  def handle_call({:run, func}, _from, socket), do: func.(socket)\n\n  def handle_event(\"validate\", _params, socket) do\n    {:noreply, socket}\n  end\n\n  ## test helpers\n\n  def inspect_html_safe(term) do\n    term\n    |> inspect()\n    |> Phoenix.HTML.html_escape()\n    |> Phoenix.HTML.safe_to_string()\n  end\n\n  def exits_with(lv, upload, kind, func) do\n    Process.unlink(proxy_pid(lv))\n    Process.unlink(upload.pid)\n\n    try do\n      func.()\n      raise \"expected to exit with #{inspect(kind)}\"\n    catch\n      :exit, {{%mod{message: msg}, _}, _} when mod == kind -> msg\n    end\n  end\n\n  def run(lv, func) do\n    GenServer.call(lv.pid, {:run, func})\n  end\n\n  def proxy_pid(%{proxy: {_ref, _topic, pid}}), do: pid\nend\n\ndefmodule Phoenix.LiveViewTest.Support.NestedUploadLive do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    {live_render(@socket, Phoenix.LiveViewTest.Support.UploadLive,\n      id: \"upload\",\n      session: %{\"prefix\" => \"nested_lv\"}\n    )}\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.UploadComponent do\n  use Phoenix.LiveComponent\n\n  def render(%{uploads: _} = assigns) do\n    ~H\"\"\"\n    <div>\n      <%= for preflight <- @preflights do %>\n        preflight:{inspect(preflight)}\n      <% end %>\n      <%= for name <- @consumed do %>\n        consumed:{name}\n      <% end %>\n      <%= for msg <- upload_errors(@uploads.avatar) do %>\n        config_error:{inspect(msg)}\n      <% end %>\n      <form phx-change=\"validate\" id={@id} phx-submit=\"save\" phx-target={@myself}>\n        <%= for entry <- @uploads.avatar.entries do %>\n          component:{entry.client_name}:{entry.progress}%\n          channel:{inspect(Phoenix.LiveView.UploadConfig.entry_pid(@uploads.avatar, entry))}\n          <%= for msg <- upload_errors(@uploads.avatar, entry) do %>\n            entry_error:{inspect(msg)}\n          <% end %>\n        <% end %>\n        <.live_file_input upload={@uploads.avatar} />\n        <button type=\"submit\">save</button>\n      </form>\n    </div>\n    \"\"\"\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      loading...\n    </div>\n    \"\"\"\n  end\n\n  def update(assigns, socket) do\n    new_socket =\n      case assigns[:run] do\n        {func, from} ->\n          {:reply, reply, new_socket} = func.(socket)\n          if from, do: GenServer.reply(from, reply)\n          new_socket\n\n        nil ->\n          socket\n\n        other ->\n          {:other, other}\n      end\n\n    {:ok,\n     new_socket\n     |> assign(preflights: [])\n     |> assign(consumed: [])\n     |> assign(assigns)}\n  end\n\n  def handle_event(\"validate\", _params, socket) do\n    {:noreply, socket}\n  end\nend\n\ndefmodule Phoenix.LiveViewTest.Support.UploadLiveWithComponent do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>\n      <%= if @uploads_count > 0 do %>\n        <%= for i <- 0..@uploads_count do %>\n          <.live_component module={Phoenix.LiveViewTest.Support.UploadComponent} id={\"upload#{i}\"} />\n        <% end %>\n      <% end %>\n    </div>\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, uploads_count: 1)}\n  end\n\n  def handle_call({:setup, setup_func}, _from, socket) do\n    {:reply, :ok, setup_func.(socket)}\n  end\n\n  def handle_call({:uploads, count}, _from, socket) do\n    {:reply, :ok, assign(socket, :uploads_count, count)}\n  end\n\n  def handle_call({:run, func}, from, socket) do\n    send_update(Phoenix.LiveViewTest.Support.UploadComponent, id: \"upload0\", run: {func, from})\n    {:noreply, socket}\n  end\nend\n"
  },
  {
    "path": "test/support/router.ex",
    "content": "defmodule Phoenix.LiveViewTest.Support.Router do\n  use Phoenix.Router\n  import Phoenix.LiveView.Router\n\n  pipeline :setup_session do\n    plug Plug.Session,\n      store: :cookie,\n      key: \"_live_view_key\",\n      signing_salt: \"/VEDsdfsffMnp5\"\n\n    plug :fetch_session\n  end\n\n  pipeline :browser do\n    plug :setup_session\n    plug :accepts, [\"html\"]\n    plug :fetch_live_flash\n  end\n\n  pipeline :bad_layout do\n    plug :put_root_layout, {UnknownView, :unknown_template}\n  end\n\n  scope \"/\", Phoenix.LiveViewTest.Support do\n    pipe_through [:browser]\n\n    live \"/thermo\", ThermostatLive\n    live \"/thermo/:id\", ThermostatLive\n    live \"/thermo-container\", ThermostatLive, container: {:span, style: \"thermo-flex<script>\"}\n    live \"/\", ThermostatLive, as: :live_root\n    live \"/clock\", ClockLive\n    live \"/redir\", RedirLive\n    live \"/elements\", ElementsLive\n\n    live \"/render-with\", RenderWithLive\n    live \"/same-child\", SameChildLive\n    live \"/root\", RootLive\n    live \"/opts\", OptsLive\n    live \"/shuffle\", ShuffleLive\n    live \"/components\", WithComponentLive\n    live \"/multi-targets\", WithMultipleTargets\n    live \"/assigns-not-in-socket\", AssignsNotInSocketLive\n    live \"/log-override\", WithLogOverride\n    live \"/log-disabled\", WithLogDisabled\n    live \"/errors\", ErrorsLive\n    live \"/live-reload\", ReloadLive\n    live \"/assign_async\", AssignAsyncLive\n    live \"/start_async\", StartAsyncLive\n    live \"/stream_async\", StreamAsyncLive\n\n    live \"/expensive-runtime-checks\", ExpensiveRuntimeChecksLive\n\n    live \"/duplicate-id\", DuplicateIdLive\n    live \"/dynamic-duplicate-component\", DynamicDuplicateComponentLive\n    live \"/form-missing-id\", FormMissingIdLive\n\n    # controller test\n    get \"/controller/:type\", Controller, :incoming\n    get \"/widget\", Controller, :widget\n    get \"/not_found\", Controller, :not_found\n    post \"/not_found\", Controller, :not_found\n\n    # router test\n    live \"/router/thermo_defaults/:id\", DashboardLive\n    live \"/router/thermo_session/:id\", DashboardLive\n    live \"/router/thermo_container/:id\", DashboardLive, container: {:span, style: \"flex-grow\"}\n    live \"/router/thermo_session/custom/:id\", DashboardLive, as: :custom_live\n    live \"/router/foobarbaz\", FooBarLive, :index\n    live \"/router/foobarbaz/index\", FooBarLive.Index, :index\n    live \"/router/foobarbaz/show\", FooBarLive.Index, :show\n    live \"/router/foobarbaz/nested/index\", FooBarLive.Nested.Index, :index\n    live \"/router/foobarbaz/nested/show\", FooBarLive.Nested.Index, :show\n    live \"/router/foobarbaz/custom\", FooBarLive, :index, as: :custom_foo_bar\n    live \"/router/foobarbaz/with_live\", Live.Nested.Module, :action\n    live \"/router/foobarbaz/nosuffix\", NoSuffix, :index, as: :custom_route\n\n    # integration layout\n    live_session :styled_layout,\n      root_layout: {Phoenix.LiveViewTest.Support.LayoutView, :styled} do\n      live \"/styled-elements\", ElementsLive\n    end\n\n    live_session :app_layout, root_layout: {Phoenix.LiveViewTest.Support.LayoutView, :app} do\n      live \"/layout\", LayoutLive\n    end\n\n    scope \"/\" do\n      pipe_through [:bad_layout]\n\n      # The layout option needs to have higher precedence than bad layout\n      live \"/bad_layout\", LayoutLive\n\n      live_session :parent_layout, root_layout: false do\n        live \"/parent_layout\", ParentLayoutLive\n      end\n    end\n\n    # integration params\n    live \"/counter/:id\", ParamCounterLive\n    live \"/action\", ActionLive\n    live \"/action/index\", ActionLive, :index\n    live \"/action/:id/edit\", ActionLive, :edit\n\n    # integration flash\n    live \"/flash-root\", FlashLive\n    live \"/flash-child\", FlashChildLive\n\n    # integration events\n    live \"/events\", EventsLive\n    live \"/events-in-mount\", EventsInMountLive\n    live \"/events-in-component\", EventsInComponentLive\n    live \"/events-multi-js\", EventsMultiJSLive\n    live \"/events-multi-js-in-component\", EventsInComponentMultiJSLive\n\n    # integration components\n    live \"/component_in_live\", ComponentInLive.Root\n    live \"/cids_destroyed\", CidsDestroyedLive\n    live \"/component_and_nested_in_live\", ComponentAndNestedInLive\n\n    # integration lifecycle\n    live \"/lifecycle\", HooksLive\n    live \"/lifecycle/bad-mount\", HooksLive.BadMount\n    live \"/lifecycle/halt-mount\", HooksLive.HaltMount\n    live \"/lifecycle/redirect-cont-mount\", HooksLive.RedirectMount, :cont\n    live \"/lifecycle/redirect-halt-mount\", HooksLive.RedirectMount, :halt\n    live \"/lifecycle/components/:type\", HooksLive.WithComponent\n    live \"/lifecycle/handle-params-not-defined\", HooksLive.HandleParamsNotDefined\n    live \"/lifecycle/handle-info-not-defined\", HooksLive.HandleInfoNotDefined\n    live \"/lifecycle/on-mount-options\", HooksLive.OnMountOptions\n\n    # integration stream\n    live \"/stream\", StreamLive\n    live \"/stream/reset\", StreamResetLive\n    live \"/stream/reset-lc\", StreamResetLCLive\n    live \"/stream/limit\", StreamLimitLive\n    live \"/stream/nested\", StreamNestedLive\n\n    live \"/stream/high-frequency-stream-and-non-stream-updates\",\n         HighFrequencyStreamAndNoStreamUpdatesLive\n\n    live \"/stream/nested-component-reset\", StreamNestedComponentResetLive\n    live \"/stream/inside-for\", StreamInsideForLive\n\n    # healthy\n    live \"/healthy/:category\", HealthyLive\n\n    # integration connect\n    live \"/connect\", ConnectLive\n\n    # live_patch\n    scope host: \"app.example.com\" do\n      live \"/with-host/full\", HostLive, :full\n      live \"/with-host/path\", HostLive, :path\n    end\n\n    # live_session\n    live_session :test do\n      live \"/thermo-live-session\", ThermostatLive\n      live \"/thermo-live-session/nested-thermo\", ThermostatLive\n      live \"/clock-live-session\", ClockLive\n      live \"/classlist\", ClassListLive\n    end\n\n    live_session :admin, session: %{\"admin\" => true} do\n      live \"/thermo-live-session-admin\", ThermostatLive\n      live \"/clock-live-session-admin\", ClockLive\n    end\n\n    live_session :mfa, session: {__MODULE__, :session, [%{\"inlined\" => true}]} do\n      live \"/thermo-live-session-mfa\", ThermostatLive\n    end\n\n    live_session :merged, session: %{\"top-level\" => true} do\n      live \"/thermo-live-session-merged\", ThermostatLive\n    end\n\n    live_session :lifecycle, on_mount: Phoenix.LiveViewTest.Support.HaltConnectedMount do\n      live \"/lifecycle/halt-connected-mount\", HooksLive.Noop\n    end\n\n    live_session :mount_mod_arg, on_mount: {Phoenix.LiveViewTest.Support.MountArgs, :inlined} do\n      live \"/lifecycle/mount-mod-arg\", HooksLive.Noop\n    end\n\n    live_session :mount_mods,\n      on_mount: [Phoenix.LiveViewTest.Support.OnMount, Phoenix.LiveViewTest.Support.OtherOnMount] do\n      live \"/lifecycle/mount-mods\", HooksLive.Noop\n    end\n\n    live_session :mount_mod_args,\n      on_mount: [\n        {Phoenix.LiveViewTest.Support.OnMount, :other},\n        {Phoenix.LiveViewTest.Support.OtherOnMount, :other}\n      ] do\n      live \"/lifecycle/mount-mods-args\", HooksLive.Noop\n    end\n\n    live_session :layout, layout: {Phoenix.LiveViewTest.Support.LayoutView, :live_override} do\n      live \"/dashboard-live-session-layout\", LayoutLive\n    end\n  end\n\n  scope \"/\", as: :user_defined_metadata, alias: Phoenix.LiveViewTest.Support do\n    live \"/sessionless-thermo\", ThermostatLive\n    live \"/thermo-with-metadata\", ThermostatLive, metadata: %{route_name: \"opts\"}\n  end\n\n  def session(%Plug.Conn{}, extra), do: Map.merge(extra, %{\"called\" => true})\nend\n"
  },
  {
    "path": "test/support/telemetry_test_helpers.ex",
    "content": "defmodule Phoenix.LiveView.TelemetryTestHelpers do\n  @moduledoc false\n\n  import ExUnit.Callbacks, only: [on_exit: 1]\n\n  def attach_telemetry(prefix) when is_list(prefix) do\n    unique_name = :\"PID#{System.unique_integer()}\"\n    Process.register(self(), unique_name)\n\n    for suffix <- [:start, :stop, :exception] do\n      :telemetry.attach(\n        {suffix, unique_name},\n        prefix ++ [suffix],\n        fn event, measurements, metadata, :none ->\n          send(unique_name, {:event, event, measurements, metadata})\n        end,\n        :none\n      )\n    end\n\n    on_exit(fn ->\n      for suffix <- [:start, :stop] do\n        :telemetry.detach({suffix, unique_name})\n      end\n    end)\n  end\nend\n"
  },
  {
    "path": "test/support/templates/heex/dead_with_function_component.html.heex",
    "content": "pre: <%= @pre %>\n<Phoenix.LiveViewTest.Support.FunctionComponent.render value=\"the value\"/>\npost: <%= @post %>\n"
  },
  {
    "path": "test/support/templates/heex/dead_with_function_component_with_inner_content.html.heex",
    "content": "pre: <%= @pre %>\n<Phoenix.LiveViewTest.Support.FunctionComponent.render_with_inner_content value=\"the value\">\n  The inner content\n</Phoenix.LiveViewTest.Support.FunctionComponent.render_with_inner_content>\npost: <%= @post %>\n"
  },
  {
    "path": "test/support/templates/heex/dead_with_live.html.eex",
    "content": "pre: <%= @pre %>\n<%= render \"inner_live.html\", assigns %>\npost: <%= @post %>"
  },
  {
    "path": "test/support/templates/heex/inner_dead.html.eex",
    "content": "dead: <%= @inner_content %>"
  },
  {
    "path": "test/support/templates/heex/inner_live.html.heex",
    "content": "live: <%= @inner_content %>"
  },
  {
    "path": "test/support/templates/heex/live_with_comprehension.html.heex",
    "content": "pre: <%= @pre %>\n<%= for point <- @points do %>\n  x: <%= point.x %>\n  <%= render \"inner_live.html\", assigns %>\n  y: <%= point.y %>\n<% end %>\npost: <%= @post %>"
  },
  {
    "path": "test/support/templates/heex/live_with_dead.html.heex",
    "content": "pre: <%= @pre %>\n<%= render \"inner_dead.html\", assigns %>\npost: <%= @post %>"
  },
  {
    "path": "test/support/templates/heex/live_with_live.html.heex",
    "content": "pre: <%= @pre %>\n<%= render \"inner_live.html\", assigns %>\npost: <%= @post %>"
  },
  {
    "path": "test/support/templates/leex/dead_with_live.html.eex",
    "content": "pre: <%= @pre %>\n<%= render \"inner_live.html\", assigns %>\npost: <%= @post %>"
  },
  {
    "path": "test/support/templates/leex/inner_dead.html.eex",
    "content": "dead: <%= @inner_content %>"
  },
  {
    "path": "test/support/templates/leex/inner_live.html.leex",
    "content": "live: <%= @inner_content %>"
  },
  {
    "path": "test/support/templates/leex/live_with_comprehension.html.leex",
    "content": "pre: <%= @pre %>\n<%= for point <- @points do %>\n  x: <%= point.x %>\n  <%= render \"inner_live.html\", assigns %>\n  y: <%= point.y %>\n<% end %>\npost: <%= @post %>"
  },
  {
    "path": "test/support/templates/leex/live_with_dead.html.leex",
    "content": "pre: <%= @pre %>\n<%= render \"inner_dead.html\", assigns %>\npost: <%= @post %>"
  },
  {
    "path": "test/support/templates/leex/live_with_live.html.leex",
    "content": "pre: <%= @pre %>\n<%= render \"inner_live.html\", assigns %>\npost: <%= @post %>"
  },
  {
    "path": "test/test_helper.exs",
    "content": "Application.put_env(:phoenix_live_view, :debug_heex_annotations, true)\nApplication.put_env(:phoenix_live_view, :debug_attributes, true)\nCode.require_file(\"test/support/live_views/debug_anno.exs\")\nCode.require_file(\"test/support/live_views/debug_anno_opt_out.exs\")\nApplication.put_env(:phoenix_live_view, :debug_attributes, false)\nApplication.put_env(:phoenix_live_view, :debug_heex_annotations, false)\n\nApplication.put_env(:phoenix_live_view, :root_tag_attribute, \"phx-r\")\nCode.require_file(\"test/support/live_views/root_tag_attr.exs\")\nApplication.delete_env(:phoenix_live_view, :root_tag_attribute)\n\n{:ok, _} = Phoenix.LiveViewTest.Support.Endpoint.start_link()\nExUnit.start()\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2021\",\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"noEmit\": false,\n    \"strict\": false,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"baseUrl\": \"./assets/js\",\n    \"stripInternal\": true,\n    \"paths\": {\n      \"*\": [\"*\", \"phoenix_live_view/*\"]\n    },\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"outDir\": \"./assets/js/types\"\n  },\n  \"include\": [\n    \"./assets/js/phoenix_live_view/*.js\",\n    \"./assets/js/phoenix_live_view/*.ts\"\n  ]\n}\n"
  }
]